Express 集成Sequelize+Sqlite3 & 默认开启WAL & 进程间通信 Conf & 打包成可执行 exe 文件

发布于:2025-06-05 ⋅ 阅读:(22) ⋅ 点赞:(0)

代码:express-exe: 将Express开发的js打包成exe服务丢给客户端使用

实现目标

  1. Express 集成 Sequelize 操作 Sqlite3 数据库;

  2. 启动 Sqlite3 时默认开启 WAL 模式,避免读写互锁,支持并发读;

  3. 利用 Conf 实现主进程与 Express 服务的底层通信;

  4. 打包成可执行文件,在 mac 和 windows 两套系统中均可使用;

Express 集成 Sequelize

  1. Express 在启动时,会检查所有可能的 require 类库,并将该类库实例化,作为单例方式向外导出使用

  2. 这样包括一些类库引入时需要前置构建的工作,即可交给启动函数来执行,执行完毕后,再对外抛出

  3. 遇到异步类函数比较难操作,传统的 commonJS 没有顶层 await,实现等待处理比较麻烦

  4. createModel 函数无需 async/await 化,虽然能保证模型 findAll 之前必然完成 wal 和 sync,但实际操作是主进程以子进程方式先启动 http 服务,之后才开始运行主渲染进程 和 webview 进程,两者产生并发性操作几乎为 0,因此无需耗费心思在每次 User 使用前都等待 db 的统一化操作

  5. Model 只是一种映射类 和 db 链接是两个概念,前者存在的意义是实现 sql 语句拼接的函数化以及返回 sql 的对象化填充时有个 Map 对应,sequelize 才是将 Model 转化的 sql 语句拿去执行的人

    1. 因此,我们可以集中管理 sequelize,封装一个 sequelize 收集类专门收集所有的 db 连接,等服务关闭时,统一关闭 db 链接

以下就是上面实现的具体代码,分为 userModel.js,instanceModel.js,sequelizeCollector.js 三个文件

// userModel.js
// src/models/userModel.js
const { DataTypes } = require('sequelize');
const { createModel } = require('./instanceModel');

const { Model, sequelize } = createModel('User', {
    id: {
        type: DataTypes.INTEGER,
        autoIncrement: true,
        primaryKey: true,
    },
    name: {
        type: DataTypes.STRING,
        allowNull: false,
    },
    email: {
        type: DataTypes.STRING,
        allowNull: false,
        unique: true,
    },
});

module.exports = { User: Model, sequelize };

// instanceModel.js
// src/models/instanceModel.js
const sequelizeCollector = require("../db/sequelizeCollector");

function createModel(modelName, attributes, options = {}) {
    // Get Sequelize instance from collector
    const sequelize = sequelizeCollector.getInstance(modelName);

    // 定义模型
    const Model = sequelize.define(modelName, attributes, {
        tableName: modelName.toLowerCase(),
        freezeTableName: true,
        timestamps: true,
        ...options,
    });

    // 同步模型到数据库(创建表)
    Model.sync({ force: false }).then(() => {
        console.log(`${modelName} table synchronized`);
    }).catch((err) => {
        console.error(`Failed to sync ${modelName} table:`, err);
    });

    return { Model, sequelize };
}

module.exports = { createModel };

// sequelizeCollector.js
const { Sequelize } = require('sequelize');
const path = require('path');
const ConfigManager = require("../config/ConfManager");

class SequelizeCollector {
    constructor() {
        this.connections = new Map(); // Using Map to store modelName -> sequelize instance
        this.configManager = new ConfigManager({ configName: "starter.http", configPath: "./config" });
    }

    // 添加 Sequelize 连接
    addConnection(modelName, sequelizeInstance) {
        if (sequelizeInstance && typeof sequelizeInstance.close === 'function') {
            this.connections.set(modelName, sequelizeInstance);
            console.log(`Sequelize connection added for model: ${modelName}`);
        }
    }

    // 获取或创建 Sequelize 实例
    getInstance(modelName) {
        // Check if instance already exists
        if (this.connections.has(modelName)) {
            return this.connections.get(modelName);
        }

        // Create new Sequelize instance if it doesn't exist
        const dbPath = path.join(this.configManager.get("dbPath"), '/sqlite', `${modelName.toLowerCase()}.db`);

        const sequelize = new Sequelize({
            dialect: 'sqlite',
            storage: dbPath,
            logging: false,
        });

        // Enable WAL mode
        sequelize.query('PRAGMA journal_mode = WAL;').then(() => {
            console.log(`WAL mode enabled for database: ${dbPath}`);
        }).catch((err) => {
            console.error(`Failed to enable WAL mode for ${dbPath}:`, err);
        });

        // Add to connections
        this.addConnection(modelName, sequelize);
        return sequelize;
    }

    // 移除 Sequelize 连接
    removeConnection(modelName) {
        if (this.connections.has(modelName)) {
            this.connections.delete(modelName);
            console.log(`Sequelize connection removed for model: ${modelName}`);
        }
    }

    // 关闭所有 Sequelize 连接
    async closeAllConnections() {
        const closePromises = Array.from(this.connections.entries()).map(async ([modelName, sequelize]) => {
            try {
                await sequelize.close();
                this.connections.delete(modelName);
                console.log(`Sequelize connection closed for model: ${modelName}`);
            } catch (error) {
                console.error(`Error closing Sequelize connection for ${modelName}:`, error);
            }
        });

        await Promise.all(closePromises);
    }

    // 获取当前连接数量
    getConnectionCount() {
        return this.connections.size;
    }
}

// 单例模式
const sequelizeCollector = new SequelizeCollector();

module.exports = sequelizeCollector;

// userService.js
// services/userService.js
const formatUtils = require('../utils/format'); // 引入工具模块
const {User} = require('../models/userModel');

// 获取所有用户
async function getUsers() {
    try {
        /**
         * 下面注释的代码是将 createModel 函数 async/await
         * 这种方式可保证 wal 和 sync 均完成的后再执行 findAll
         * 但考虑现实情况, wal 和 sync 操作不需要 await
         * 因为是单步操作 协程调度下 单步操作基本为交替操作
         * wal 和 sync 距离很近 操作可在 findAll 前完成
         * 从输出的 console 也能判断该结论
         * 此外 因为主渲染进程 和 webview 都在 http 启动之后才开始运行
         * 所以 wal 和 sync 异步操作没有任何影响
         * 在协程逻辑下 sync 操作不会和 findAll 同时存在
         * 因此 sync 理论上会先执行后再执行 findAll
         */
            // const {User, sequelize} = await UserPromise;
            // console.log(User)
        const users = await User.findAll();
        return users.map(user => ({
            ...user.toJSON(),
            name: formatUtils.capitalize(user.name),
            email: formatUtils.capitalize(user.email),
            createdAt: formatUtils.formatDate(user.createdAt),
        }));
    } catch (error) {
        throw new Error(`Error fetching users: ${error.message}`);
    }
}

打成可执行文件

  1. 利用 windows 的 wsl,运行在 centos 系统中

    1. CentOS7 的类库不支持 node18 版本,会报类库错误

升级为 CentOS8 会遇到以下问题

wsl centos8 二进制安装文件

https://github.com/wsldl-pg/CentWSL/releases

除了将基本镜像更换为腾讯云,还要把下面截图的两个文件里面的镜像更换为腾讯云

curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.cloud.tencent.com/repo/centos8_base.repo
CentOS8 需要额外更改两个文件

解决阿里云CentOS8 yum安装appstream报错,更新yum后无法makecache的问题_errors during downloading metadata for repository -CSDN博客

搞定镜像后,要安装开发包,否则报prebuild-install 错误,这样在pkg . 打包时就不会报prebuild-install错误了

dnf groupinstall -y "Development Tools"
dnf install -y python3 python3-devel
yum install -y python39
echo "alias python3=/usr/bin/python3.9" >> ~/.bashrc
source ~/.bashrc

macos 打包直接双击运行问题

  1. 双击运行默认路径是 /User/xxx 用户根目录,必须到指定路径,使用 ./pkg-express 方式运行,才能正确寻找路径

  2. 因此需要使用绝对路径更好

注意

  1. 切换不同平台时,需要运行 npm rebuild,否则是没有这个平台的二进制的,即使都能打包出来对应的可执行文件,但不可运行


网站公告

今日签到

点亮在社区的每一天
去签到