#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」 学习笔记

<<:  Youtube Reports API 教学 - 最後一次做 OAuth2.0 授权

>>:  【Day 28】 服务器监控 on AWS

30天打造品牌特色电商网站 Day.6 Figma实作第一个网站

今天带大家简单制作一张首页的画面。 导览列 选用Dektop(1440px X 1024px)的fr...

笔记我使用 NSIS 制作 Windows 安装档的过程

NSIS (Nullsoft Scriptable Install System) 是一个建立安装...

Day13 线性回归实作

https://github.com/PacktPublishing/Machine-Learni...

Day 1 (html)

1.版本 但是遇到旧的也不要更改,因为可能别人版本还没这麽新 <!DOCTYPE html&g...

Day 9 Self-attention(三) input相关联性计算

Self-attention 首先先把a1乘上Wq,就会得到q1,q的意思是query,也就是查询的...