iT邦幫忙

2021 iThome 鐵人賽

DAY 28
1
Modern Web

JavaScript Easy Go!系列 第 28

#28 Click! Serve! Desktop

昨天已經把大部分的 GUI 弄完了,之前也已經寫好了伺服器的程式,今天我們把兩邊拼起來吧!

搬移 server.js

我們把之前寫的 server.js 搬過來新的專案資料夾中:

然後因為前面沒有 config.js 幫忙檔怪東西,輸出也改成在 GUI 中,所以我們也得改一下 server.js。

Logger

因為現在我們的輸出應該要在 GUI 上面,而不是本來的 Terminal,所以新增一個 Logger 類別:

class Logger {
    #events = {};

    constructor(log, logfile = null) {
        this._log = log;
        this._logfile = logfile;
        if (this._log && this._logfile) {
            this._stream = fs.createWriteStream(this._logfile, { flags: "a" });
            this._stream.write(`\n========\n`);
            this.closed = false;
        }
    }

    log(type = "INFO", message) {
        if (typeof this.#events[type] === "function") this.#events[type](message);
        if (this._stream && !this.closed) this._stream.write(`${new Date().toISOString()} [${type}] ${message}\n`);
        return this;
    }

    info(message) {
        return this.log("INFO", message);
    }

    success(message) {
        return this.log("SUCCESS", message);
    }

    error(message) {
        return this.log("ERROR", message);
    }

    warn(message) {
        return this.log("WARN", message);
    }

    debug(message) {
        return this.log("DEBUG", message);
    }

    close() {
        if (this._stream && !this.closed) {
            this._stream.end();
            this.closed = true;
        }
        return this;
    }

    on(event, callback) {
        this.#events[event] = callback;
        return this;
    }
}

這個 Logger 很簡單,就...寫紀錄而已,但你可以用 on 來掛事件監聽。

check 函式

因為現在少了 config.js 來阻擋怪怪的東西,所以新增一個 check 函式做型別檢查,希望從 agent.js 往後丟時就把型別處裡好。

function check({ port, folder, log, logfile }) {
    if (typeof port !== "number") return false;
    if (typeof folder !== "string") return false;
    if (typeof log !== "boolean") return false;
    if (log && typeof logfile !== "string") return false;
    return true;
}

修改 createServer

原本的 createServer 函式需要稍做修改才能套用 Logger 和 check。

function createServer({ port, folder, log, logfile }, on = {}) {
    const logger = new Logger(log, logfile);
    for (const event in on) logger.on(event.toUpperCase(), on[event]);

    if (!check({ port, folder, log, logfile })) {
        logger.error("Invalid server configuration");
        return null;
    }

    try {
        const app = new Koa();
        app.use(async (ctx, next) => {
            logger.info(`Process ${ctx.request.method} ${ctx.request.url} from ${ctx.request.ip}`);
            await next();
        });
        app.use(require("koa-static")(folder));
        const server = app.listen(port);

        logger.success(`Server started at port ${port}`);
        logger.success(`Serving static files from ${folder}`);
        logger.success(`Visit http://localhost:${port}/ to see your website.`);
        if (log) logger.info(`Log file: ${logfile}`);

        return async () => {
            server.close();
            logger.info(`Server closed.`);
            logger.close();
        };
    } catch (error) {
        logger.error(error);
        logger.close();
        console.error(error);
        return null;
    }
}

我們讓 createServer 有了第二個參數,用來掛上 Logger 的事件監聽。
然後用 check 來檢查參數。
(但這其實有個小問題,就是 log 和 logfile 在還沒檢查就丟進 Logger 了,因為檢查出錯誤時需要 logger)
其它的部分因為有 Logger ,所以簡化了一些 log 相關的程式。

前端完善

其實就是加上 logs 的 CSS 還有加一些 id 和 class 而已。

log 的 CSS,用偽元素來標示類別:

.log {
    padding: 0 8px;
    word-break: break-all;
    transition: all 0.2s;
}

.log:hover {
    background: var(--nord4);
}

.log.success {
    color: var(--nord14);
    text-shadow: 0 0 var(--nord1);
}
.log.success::before {
    content: "[success] ";
}

.log.info {
    color: var(--nord9);
}
.log.info::before {
    content: "[info] ";
}

.log.error {
    color: var(--nord11);
}
.log.error::before {
    content: "[error] ";
}

.log.warn {
    color: var(--nord12);
}
.log.warn::before {
    content: "[warn] ";
}

agent.js 與 main.js 的溝通

我們讓 agent.js 用 IPC 向 main.js 傳送請求及接收資訊。

啟動/停止伺服器

我們在 registerListener 中加上啟動和停止的請求機制:

document.querySelector("#launch").addEventListener("click", async () => {
    if (document.querySelector("#launch").classList.contains("launched")) {
        ipc.send("server-stop", +document.querySelector("#port").value);
        return;
    } else {
        // 取得輸入值
        const folder = document.querySelector("#folder").value;
        const port = +document.querySelector("#port").value;
        const log = document.querySelector("#log").checked;
        const logfile = document.querySelector("#logfile").value;

        // 將輸入值傳給 main.js
        ipc.send("server-launch", { folder, port, log, logfile });
        document.querySelector("#launch").classList.add("launched");
        document.querySelector("#launch").innerHTML = "停止";
    }
});

我們用啟動按鈕的 class 來判斷是啟動還是停止。

然後停止後必須回復啟動按鈕狀態:

ipc.on("server-stopped", async (evt) => {
    document.querySelector("#launch").classList.remove("launched");
    document.querySelector("#launch").innerHTML = "啟動";
});

伺服器停止後會用 IPC 向前端通知。

接收伺服器訊息

在 agent.js 中接收各種訊息並新增至畫面上:

ipc.on("log-info", async (evt, msg) => {
    const logs = document.querySelector("#logs");
    const log = document.createElement("div");
    log.classList.add("log", "info");
    log.innerHTML = msg;
    logs.appendChild(log);
});

ipc.on("log-success", async (evt, msg) => {
    const logs = document.querySelector("#logs");
    const log = document.createElement("div");
    log.classList.add("log", "success");
    log.innerHTML = msg;
    logs.appendChild(log);
});

ipc.on("log-error", async (evt, msg) => {
    const logs = document.querySelector("#logs");
    const log = document.createElement("div");
    log.classList.add("log", "error");
    log.innerHTML = msg;
    logs.appendChild(log);
});

ipc.on("log-warn", async (evt, msg) => {
    const logs = document.querySelector("#logs");
    const log = document.createElement("div");
    log.classList.add("log", "warn");
    log.innerHTML = msg;
    logs.appendChild(log);
});

各種訊息大同小異,差別只在套用的 class 所用的 CSS。

main.js 的請求接收

我們的 main.js 會收到兩種伺服器相關的請求,一是啟動,二是停止:

const createServer = require("./server");
const Server = {}; // 保留未來擴充多伺服器的可能
ipc.on("server-launch", async (evt, config) => {
    console.log("server-launch", config);
    Servers[config.port] = createServer(config, {
        info: (msg) => evt.sender.send("log-info", msg),
        success: (msg) => evt.sender.send("log-success", msg),
        error: (msg) => evt.sender.send("log-error", msg),
        warn: (msg) => evt.sender.send("log-warn", msg),
    });
    if (Servers[config.port] === null) {
        delete Servers[config.port];
        evt.sender.send("log-error", "啟動伺服器失敗");
        evt.sender.send("server-stopped");
    }
});

ipc.on("server-stop", async (evt, port) => {
    console.log("server-stop", port);
    await Servers[port]();
    delete Servers[port];
    evt.sender.send("server-stopped");
});

main.js 的部分相對簡單,只需要轉發兩種請求並掛上監聽而已。

程式就完成了。

成品


大概就是這樣,錯誤也很正常的噴回前端了。

接下來

當然就是打包成跨平台應用程式啊。


每日鐵人賽熱門 Top 10 (1011)

以 10/11 20:00 ~ 10/12 20:00 文章觀看數增加值排名

  1. +143 Day 21: 人工智慧在音樂領域的應用 (AI作曲-基因演算法四 掌握生殺大權-Interactive Fitness Function)
    • 作者: fd2
    • 系列:人工智慧在音樂領域的應用
  2. +138 Day27 海鮮義大利燉飯Risotto
    • 作者: headhunter_sharon
    • 系列:雪倫的30天拜託冰箱
  3. +136 Day 22: 人工智慧在音樂領域的應用 (AI作曲-基因演算法五 基於規則(Rule-Based)的Fitness Function)
    • 作者: fd2
    • 系列:人工智慧在音樂領域的應用
  4. +129 表單處理 Object 裡的 Array
    • 作者: Chris
    • 系列:Vue.js 進階心法
  5. +129 [職場]不放過每個細節,完成一場 0 失誤的專案 Demo!
    • 作者: 寶寶出頭天
    • 系列:全端工程師生存筆記
  6. +117 Day 23: 人工智慧在音樂領域的應用 (AI作曲-基因演算法六 總要敬老尊賢吧?)
    • 作者: fd2
    • 系列:人工智慧在音樂領域的應用
  7. +117 Proxmox VE 設定客體機高可用性
    • 作者: Jason Cheng (節省哥)
    • 系列:突破困境:企業開源虛擬化管理平台
  8. +113 Day 27: 人工智慧在音樂領域的應用 (索尼-Flow Machine、谷歌-Magenta )
    • 作者: fd2
    • 系列:人工智慧在音樂領域的應用
  9. +108 Day 26: 人工智慧在音樂領域的應用 (AI作曲 - 生成對抗網路 Gan (幹) )
    • 作者: fd2
    • 系列:人工智慧在音樂領域的應用
  10. +102 【Day 27】Google Apps Script - API Blueprint 篇 - Apiary 建立專案與版本控制
    • 作者: Jason Hung
    • 系列:「Google Apps Script」 學習筆記

上一篇
#27 做點 GUI 吧!
下一篇
#29 Electron 打包應用程式
系列文
JavaScript Easy Go!31

尚未有邦友留言

立即登入留言