Commit cd3269e1 by wusiyi

更新readme

parent 947867a5
### 前言: # 生产软件
## 项目启动 (2026.05.11)
1. 安装 vue 脚手架 3.0
```shell
# 卸载1.x或2.x旧版本
npm uninstall -g @vue/cli
# 安装@vue/cli 版本=3.0.0
npm install -g @vue/cli@3.0.0
# 查看vue-cli版本
vue -V
```
2. 使用 cnpm 安装依赖, cnpm 版本=7.1.1
```shell
npm install -g cnpm@7.1.1
cnpm -v
cnpm install
```
3. 启动项目
```shell
# 测试环境
npm run electron:serve:test
# 生产环境
npm run electron:serve:prod
```
## 项目打包
```shell
# 测试环境
npm run electron:build:test
# 生产环境
npm run electron:build:prod
```
- 如果打包报错 rcedit 相关,关闭杀毒软件并使用管理员权限运行命令
## 项目搭建
### 前言
本文讲述 Vue 3.0 + Electron + Express + Lowdb 框架搭建过程, 以及少量示例代码; 本文讲述 Vue 3.0 + Electron + Express + Lowdb 框架搭建过程, 以及少量示例代码;
...@@ -11,7 +56,7 @@ ...@@ -11,7 +56,7 @@
3. 部署 Express 充当 Vue+Electron 的 Web Restful Api 后端, 并通过 Lowdb 记录一些 App 的系统设置信息到文件中, 以便于下一次启动时仍能访问(不同于 Browser 端保存)。 3. 部署 Express 充当 Vue+Electron 的 Web Restful Api 后端, 并通过 Lowdb 记录一些 App 的系统设置信息到文件中, 以便于下一次启动时仍能访问(不同于 Browser 端保存)。
4. 让 Vue+Electron+Express 三者共同协作, 看起来是某一个云上服务的 App 形式的 Client 端。 4. 让 Vue+Electron+Express 三者共同协作, 看起来是某一个云上服务的 App 形式的 Client 端。
### 准备部分: ### 准备部分
- 安装 node.js - 安装 node.js
...@@ -99,7 +144,7 @@ ...@@ -99,7 +144,7 @@
```scss ```scss
// 全局CSS常量定义 // 全局CSS常量定义
@import './config.scss'; @import "./config.scss";
// 所有修改element-ui的样式, 以避免单页面scoped中修改权限不够的问题 // 所有修改element-ui的样式, 以避免单页面scoped中修改权限不够的问题
//@import "./elementui.scss" //@import "./elementui.scss"
``` ```
...@@ -152,46 +197,46 @@ ...@@ -152,46 +197,46 @@
- 手工生成 vue.config.js - 手工生成 vue.config.js
```javascript ```javascript
const path = require('path') const path = require("path");
module.exports = { module.exports = {
// 基本路径 // 基本路径
publicPath: process.env.NODE_ENV === 'production' ? '' : '/', publicPath: process.env.NODE_ENV === "production" ? "" : "/",
// 输出文件目录 // 输出文件目录
outputDir: process.env.NODE_ENV === 'production' ? 'dist' : 'devdist', outputDir: process.env.NODE_ENV === "production" ? "dist" : "devdist",
// eslint-loader 是否在保存的时候检查 // eslint-loader 是否在保存的时候检查
lintOnSave: false, lintOnSave: false,
/** vue3.0内置了webpack所有东西, /** vue3.0内置了webpack所有东西,
* webpack配置,see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md * webpack配置,see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
**/ **/
chainWebpack: (config) => { chainWebpack: config => {
const svgRule = config.module.rule('svg') const svgRule = config.module.rule("svg");
svgRule.uses.clear() svgRule.uses.clear();
svgRule svgRule
.use('svg-sprite-loader') .use("svg-sprite-loader")
.loader('svg-sprite-loader') .loader("svg-sprite-loader")
.options({ .options({
symbolId: 'icon-[name]', symbolId: "icon-[name]",
include: ['./src/icons'], include: ["./src/icons"]
}) });
config.module config.module
.rule('pug') .rule("pug")
.test(/\.pug$/) .test(/\.pug$/)
.use('pug-html-loader') .use("pug-html-loader")
.loader('pug-html-loader') .loader("pug-html-loader")
.end() .end();
}, },
configureWebpack: (config) => { configureWebpack: config => {
config.resolve = { config.resolve = {
// 配置解析别名 // 配置解析别名
extensions: ['.js', '.json', '.vue'], // 自动添加文件名后缀 extensions: [".js", ".json", ".vue"], // 自动添加文件名后缀
alias: { alias: {
vue: 'vue/dist/vue.js', vue: "vue/dist/vue.js",
'@': path.resolve(__dirname, './src'), "@": path.resolve(__dirname, "./src"),
'@c': path.resolve(__dirname, './src/components'), "@c": path.resolve(__dirname, "./src/components")
},
} }
};
}, },
// 生产环境是否生成 sourceMap 文件 // 生产环境是否生成 sourceMap 文件
productionSourceMap: false, productionSourceMap: false,
...@@ -204,15 +249,15 @@ ...@@ -204,15 +249,15 @@
// css预设器配置项 // css预设器配置项
loaderOptions: { loaderOptions: {
sass: { sass: {
prependData: `@import "./src/styles/main.scss";`, prependData: `@import "./src/styles/main.scss";`
}, }
}, },
// 启用 CSS modules for all css / pre-processor files. // 启用 CSS modules for all css / pre-processor files.
requireModuleExtension: true, // 是否开启支持‘foo.module.css’样式 requireModuleExtension: true // 是否开启支持‘foo.module.css’样式
}, },
// use thread-loader for babel & TS in production build // use thread-loader for babel & TS in production build
// enabled by default if the machine has more than 1 cores // enabled by default if the machine has more than 1 cores
parallel: require('os').cpus().length > 1, parallel: require("os").cpus().length > 1,
/** /**
* PWA 插件相关配置,see https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa * PWA 插件相关配置,see https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa
*/ */
...@@ -220,35 +265,35 @@ ...@@ -220,35 +265,35 @@
// webpack-dev-server 相关配置 // webpack-dev-server 相关配置
devServer: { devServer: {
open: false, // 编译完成是否打开网页 open: false, // 编译完成是否打开网页
host: '0.0.0.0', // 指定使用地址,默认localhost,0.0.0.0代表可以被外界访问 host: "0.0.0.0", // 指定使用地址,默认localhost,0.0.0.0代表可以被外界访问
port: 8090, // 访问端口 port: 8090, // 访问端口
https: false, // 编译失败时刷新页面 https: false, // 编译失败时刷新页面
hot: true, // 开启热加载 hot: true, // 开启热加载
hotOnly: false, hotOnly: false,
proxy: { proxy: {
// 配置跨域 // 配置跨域
'/devApi': { "/devApi": {
//要访问的跨域的api的域名 //要访问的跨域的api的域名
target: 'http://www.web-jshtml.cn', target: "http://www.web-jshtml.cn",
ws: true, ws: true,
changOrigin: true, changOrigin: true,
pathRewrite: { pathRewrite: {
'^/devApi': '/productapi', "^/devApi": "/productapi"
}, }
}, }
}, },
overlay: { overlay: {
// 全屏模式下是否显示脚本错误 // 全屏模式下是否显示脚本错误
warnings: true, warnings: true,
errors: true, errors: true
}, }
// before: app => {} // before: app => {}
}, },
/** /**
* 第三方插件配置 * 第三方插件配置
*/ */
pluginOptions: {}, pluginOptions: {}
} };
``` ```
- 改造其中的 vue 为 pug 格式 - 改造其中的 vue 为 pug 格式
...@@ -285,14 +330,14 @@ ...@@ -285,14 +330,14 @@
export const lang = { export const lang = {
slice: { slice: {
placeholder: { placeholder: {
name: '请输出切片名称', name: "请输出切片名称",
nst: '请选择NST模板', nst: "请选择NST模板"
}, },
tips: { tips: {
sla: '带宽:{0} Mbps, 时延: {1} ms', sla: "带宽:{0} Mbps, 时延: {1} ms"
}, }
},
} }
};
``` ```
- 把 i18n 加入到 Vue, 修改 src/main.js: - 把 i18n 加入到 Vue, 修改 src/main.js:
...@@ -400,64 +445,64 @@ vue add electron-builder ...@@ -400,64 +445,64 @@ vue add electron-builder
修改了 App 窗口大小, 取消了跨域限制, 取消了菜单栏, 修改了 App 的窗口 Icon: 修改了 App 窗口大小, 取消了跨域限制, 取消了菜单栏, 修改了 App 的窗口 Icon:
```javascript ```javascript
'use strict' "use strict";
import { app, protocol, BrowserWindow, Menu } from 'electron' import { app, protocol, BrowserWindow, Menu } from "electron";
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib' import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
const isDevelopment = process.env.NODE_ENV !== 'production' const isDevelopment = process.env.NODE_ENV !== "production";
let win let win;
protocol.registerSchemesAsPrivileged([ protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { secure: true, standard: true } }, { scheme: "app", privileges: { secure: true, standard: true } }
]) ]);
function createWindow() { function createWindow() {
win = new BrowserWindow({ win = new BrowserWindow({
width: 1200, width: 1200,
height: 800, height: 800,
icon: './src/assets/logo.png', icon: "./src/assets/logo.png",
webPreferences: { webPreferences: {
webSecurity: false, webSecurity: false,
nodeIntegration: true, nodeIntegration: true
}, }
}) });
// 取消菜单 // 取消菜单
Menu.setApplicationMenu(null) Menu.setApplicationMenu(null);
if (process.env.WEBPACK_DEV_SERVER_URL) { if (process.env.WEBPACK_DEV_SERVER_URL) {
win.loadURL(process.env.WEBPACK_DEV_SERVER_URL) win.loadURL(process.env.WEBPACK_DEV_SERVER_URL);
if (!process.env.IS_TEST) win.webContents.openDevTools() if (!process.env.IS_TEST) win.webContents.openDevTools();
} else { } else {
createProtocol('app') createProtocol("app");
win.loadURL('app://./index.html') win.loadURL("app://./index.html");
} }
win.on('closed', () => { win.on("closed", () => {
win = null win = null;
}) });
} }
app.on('activate', () => { app.on("activate", () => {
if (win === null) { if (win === null) {
createWindow() createWindow();
} }
}) });
app.on('ready', async () => { app.on("ready", async () => {
createWindow() createWindow();
}) });
if (isDevelopment) { if (isDevelopment) {
if (process.platform === 'win32') { if (process.platform === "win32") {
process.on('message', (data) => { process.on("message", data => {
if (data === 'graceful-exit') { if (data === "graceful-exit") {
app.quit() app.quit();
} }
}) });
} else { } else {
process.on('SIGTERM', () => { process.on("SIGTERM", () => {
app.quit() app.quit();
}) });
} }
} }
``` ```
...@@ -529,72 +574,72 @@ vue add electron-builder ...@@ -529,72 +574,72 @@ vue add electron-builder
- src/backend/store/db.js: 提供数据对象访问能力 - src/backend/store/db.js: 提供数据对象访问能力
```javascript ```javascript
import Datastore from 'lowdb' import Datastore from "lowdb";
import FileSync from 'lowdb/adapters/FileSync' import FileSync from "lowdb/adapters/FileSync";
import path from 'path' import path from "path";
import fs from 'fs-extra' import fs from "fs-extra";
import LodashId from 'lodash-id' import LodashId from "lodash-id";
// 引入remote模块 // 引入remote模块
import { app, remote } from 'electron' import { app, remote } from "electron";
// 根据process.type来分辨在哪种模式使用哪种模块, // 根据process.type来分辨在哪种模式使用哪种模块,
// 在主进程调用 为 browser, 在渲染进程调用为 renderer // 在主进程调用 为 browser, 在渲染进程调用为 renderer
const APP = process.type === 'renderer' ? remote.app : app const APP = process.type === "renderer" ? remote.app : app;
// 获取用户目录 C:\Users\shihe\AppData\Roaming\vue-node-lowdb // 获取用户目录 C:\Users\shihe\AppData\Roaming\vue-node-lowdb
const STORE_PATH = APP.getPath('userData') const STORE_PATH = APP.getPath("userData");
if (process.type !== 'renderer') { if (process.type !== "renderer") {
// 如果不存在路径,创建 // 如果不存在路径,创建
if (!fs.pathExistsSync(STORE_PATH)) { if (!fs.pathExistsSync(STORE_PATH)) {
fs.mkdirpSync(STORE_PATH) fs.mkdirpSync(STORE_PATH);
} }
} }
const adapter = new FileSync(path.join(STORE_PATH, 'database.json')) // 初始化lowdb读写的json文件名以及存储路径 const adapter = new FileSync(path.join(STORE_PATH, "database.json")); // 初始化lowdb读写的json文件名以及存储路径
const db = Datastore(adapter) // lowdb接管该文件 const db = Datastore(adapter); // lowdb接管该文件
//通过lodash-id这个插件可以很方便地为每个新增的数据自动加上一个唯一标识的id字段 //通过lodash-id这个插件可以很方便地为每个新增的数据自动加上一个唯一标识的id字段
db._.mixin(LodashId) db._.mixin(LodashId);
// 初始化 ( 示例 ) // 初始化 ( 示例 )
if ( if (
!db !db
.read() .read()
.has('NSTs') .has("NSTs")
.value() .value()
) { ) {
db.set('NSTs', []).write() db.set("NSTs", []).write();
db.get('NSTs') db.get("NSTs")
.insert({ label: '差动保护', value: 'nst_001' }) .insert({ label: "差动保护", value: "nst_001" })
.write() .write();
db.get('NSTs') db.get("NSTs")
.insert({ label: '龙门吊', value: 'nst_002' }) .insert({ label: "龙门吊", value: "nst_002" })
.write() .write();
} }
if ( if (
!db !db
.read() .read()
.has('PLMNs') .has("PLMNs")
.value() .value()
) { ) {
db.read() db.read()
.set('PLMNs', []) .set("PLMNs", [])
.write() .write();
db.read() db.read()
.get('PLMNs') .get("PLMNs")
.insert({ label: '中国移动01', value: '960-001' }) .insert({ label: "中国移动01", value: "960-001" })
.write() .write();
db.read() db.read()
.get('PLMNs') .get("PLMNs")
.insert({ label: '中国联通03', value: '960-003' }) .insert({ label: "中国联通03", value: "960-003" })
.write() .write();
db.read() db.read()
.get('PLMNs') .get("PLMNs")
.insert({ label: '中国电信07', value: '960-007' }) .insert({ label: "中国电信07", value: "960-007" })
.write() .write();
} }
// ES6写法: 暴露 // ES6写法: 暴露
export { db as default } export { db as default };
``` ```
其他 lowdb 的详细信息可以参考 LowDB.md 文件, 以及网址: https://www.jianshu.com/p/d46abfa4ddc9 其他 lowdb 的详细信息可以参考 LowDB.md 文件, 以及网址: https://www.jianshu.com/p/d46abfa4ddc9
...@@ -612,39 +657,39 @@ vue add electron-builder ...@@ -612,39 +657,39 @@ vue add electron-builder
代码: src/backend/webserver/index.js: 代码: src/backend/webserver/index.js:
```javascript ```javascript
import express from 'express' import express from "express";
import router from './routes/index.js' import router from "./routes/index.js";
const PORT = 3000 const PORT = 3000;
const webApp = express() const webApp = express();
//webApp.use(logger("./logs")); //webApp.use(logger("./logs"));
webApp.use(express.json()) webApp.use(express.json());
webApp.use(express.urlencoded({ extended: false })) webApp.use(express.urlencoded({ extended: false }));
webApp.use('/', router) webApp.use("/", router);
// catch 404 // catch 404
webApp.use((req, res, next) => { webApp.use((req, res, next) => {
res.status(404).send('Sorry! 404 Error.') res.status(404).send("Sorry! 404 Error.");
}) });
// error handler, 4个参数 // error handler, 4个参数
webApp.use((err, req, res, next) => { webApp.use((err, req, res, next) => {
// set locals, only providing error in development // set locals, only providing error in development
res.locals.message = err.message res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {} res.locals.error = req.app.get("env") === "development" ? err : {};
// render the error page // render the error page
res.status(err.status || 500) res.status(err.status || 500);
res.json({ res.json({
message: err.message, message: err.message,
error: err, error: err
}) });
}) });
webApp.set('port', PORT) webApp.set("port", PORT);
webApp.listen(PORT, () => console.log(`App listening on port ${PORT}`)) webApp.listen(PORT, () => console.log(`App listening on port ${PORT}`));
export { webApp as default } export { webApp as default };
``` ```
* Express 的 API 路由: * Express 的 API 路由:
...@@ -652,24 +697,24 @@ vue add electron-builder ...@@ -652,24 +697,24 @@ vue add electron-builder
代码: src/backend/webserver/routes/index.js 代码: src/backend/webserver/routes/index.js
```javascript ```javascript
import express from 'express' import express from "express";
// 导入slice mgt的具体数据操作函数 // 导入slice mgt的具体数据操作函数
import sliceMgt from '../entity/function.js' import sliceMgt from "../entity/function.js";
// 导入system config 的具体数据操作函数 // 导入system config 的具体数据操作函数
// let sysConfig = require("../entity/be_sysConfig.js"); // let sysConfig = require("../entity/be_sysConfig.js");
import sysConfig from '../entity/be_sysConfig.js' import sysConfig from "../entity/be_sysConfig.js";
// 生成路由对象 // 生成路由对象
let router = express.Router() let router = express.Router();
// 设置路由 // 设置路由
router.get('/nsmf/v1/nsts', sliceMgt.getNSTs) router.get("/nsmf/v1/nsts", sliceMgt.getNSTs);
router.get('/nsmf/v1/plmns', sliceMgt.getPLMNs) router.get("/nsmf/v1/plmns", sliceMgt.getPLMNs);
router.get('/nsmf/v1/nsmfConfig', sysConfig.getNSMFConfig) router.get("/nsmf/v1/nsmfConfig", sysConfig.getNSMFConfig);
router.put('/nsmf/v1/nsmfConfig', sysConfig.setNSMFConfig) router.put("/nsmf/v1/nsmfConfig", sysConfig.setNSMFConfig);
// ES6写法: 暴露路由 // ES6写法: 暴露路由
export { router as default } export { router as default };
``` ```
* sliceMgt 实体文件: * sliceMgt 实体文件:
...@@ -677,9 +722,9 @@ vue add electron-builder ...@@ -677,9 +722,9 @@ vue add electron-builder
代码: src/backend/webserver/entity/function.js 代码: src/backend/webserver/entity/function.js
```javascript ```javascript
import db from '@/backend/store/db.js' import db from "@/backend/store/db.js";
let sliceMgt = new Object() let sliceMgt = new Object();
// wrap函数把不支持promise的库转为支持, 可以支持异步 // wrap函数把不支持promise的库转为支持, 可以支持异步
// const wrap = fn => (...args) => fn(...args).catch(args[2]); // const wrap = fn => (...args) => fn(...args).catch(args[2]);
// router.get( // router.get(
...@@ -692,18 +737,18 @@ vue add electron-builder ...@@ -692,18 +737,18 @@ vue add electron-builder
// ); // );
sliceMgt.getNSTs = (req, res) => { sliceMgt.getNSTs = (req, res) => {
console.log('GET: NST List') console.log("GET: NST List");
// 查询数据 // 查询数据
let data = db.get('NSTs').value() let data = db.get("NSTs").value();
res.json(data) res.json(data);
} };
sliceMgt.getPLMNs = (req, res) => { sliceMgt.getPLMNs = (req, res) => {
console.log('GET: PLMN List') console.log("GET: PLMN List");
res.send('getPLMNs') res.send("getPLMNs");
} };
export { sliceMgt as default } export { sliceMgt as default };
``` ```
be_sysConfig.js 类似处理 be_sysConfig.js 类似处理
...@@ -743,7 +788,15 @@ vue add electron-builder ...@@ -743,7 +788,15 @@ vue add electron-builder
```json ```json
[ [
{ "label": "差动保护", "value": "nst_001", "id": "007b1880-f259-4993-b786-a5d93310b306" }, {
{ "label": "龙门吊", "value": "nst_002", "id": "1ff1f498-b308-4649-a42e-77e7293e42b6" } "label": "差动保护",
"value": "nst_001",
"id": "007b1880-f259-4993-b786-a5d93310b306"
},
{
"label": "龙门吊",
"value": "nst_002",
"id": "1ff1f498-b308-4649-a42e-77e7293e42b6"
}
] ]
``` ```
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment