Vite是一种相对于webpack更现代化,更高效的前端框架构建工具,具有以下的特点:
不需捆绑:服务器冷启动非常快。
按需编译代码: 只编译当前界面实际导入的代码,不必整个应用被捆绑后才开始开发,利于多页面应用开发。
热更新(Hot Module Replacement, HWR)的性能与模块总数解耦,速度快无关应用大小。
注意:1. 项目整个页面重载的比基于绑定的webpack项目稍慢,因基于<script module>导入方式易引起大量网络请求,本地开发忽略不计。
2.默认内置的esbuild编译器,在将TypeScript转换为javascript的工作上的性能表现优异,比常用的tsc工具快20-30倍,HMR更新可以在50ms内反映在浏览器中。
一、创建项目
首先通过如下命令创建一个vite项目,并安装好Electron依赖,相应的npm指令如下:
npm init vite viteElectron
cd viteElectron
npm install
npm install electron --save-dev
yarn add @vitejs/plugin-vue --dev
一般情况下为一个工程安装依赖时,都是安装为开发依赖(--save-dev),因为生产依赖都会在Electron-builder打包时被放到安装包内。但Electron-builder会另外准备Electron,所以不需要自己的生产依赖。
二、定义启动脚本
在项目的package.json文件中添加启动脚本如下:
#package.json
"script" {
"start": "node ./script/dev",
}
项目开发成功后,开发者可以通过npm run start 方式启动应用。
在根目录的script/dev/下, 创建文件index.js,它是启动开发环境的脚本,它主要完成如下四项工作:
(1)启动Vue项目的开发服务。
(2)设置环境变量
(3)编译主进程代码
(4)启动Electron子进程,并加载Vue项目的首页。
三、启动开发任务
主要工作是通过vite内置koa服务加载Vue项目,让它运行在http://localhost下,有这个服务,我们修改界面代码时,Vite的热更新机制会使修改的内容实时反馈到界面。Vite提供了命令行启动项目外,也提供了javascript API供开发人员通过编码方式启动项目。在index.js文件内直接调API来启动项目,关键代码如下:
let vite=require("vite")
let vue=require("@vitejs/plugin-vue")
let dev = {
server: null,
serverPort: 1600,
electronProcess: null,
/*
async createServer() {
let options = {
configFile: false,
root: process.cwd(),
server: {
port: this.serverPort,
},
plugins: [vue()],
};
this.server = await vite.createServer(options);
await this.server.listen();
},
*/
createServer () {
return new Promise((resolve, reject) => {
let options = {
root:process.cwd(),
enableEsbuild: true
};
this.server = vite.createServer(options);
this.server.on("error", (e) => this.serverOnErr(e));
this.server.on("data", (e) => console.log(e.toString()));
this.server.listen(this.serverPort, () => {
console.log(`http://localhost:${this.serverPort}`);
resolve();
});
});
},
async start(){
await this.createServer();
},
};
dev.start();
在以上代码中,首先定义dev对象,使用dev对象的start方法启动开发环境的http服务,在createServer中,首先在vite.createServer创建http服务,并用server.listen()方法启动服务,两个createServer函数,经验证第二个函数可以正常启动,第一个函数无法启动http服务,代码如上,原因不明。
四、设置环境变量
本节提到的环境变量并不是操作系统的环境变量,也不是npm的环境变量,而是应用程序的环境变量,是process.env对象内的各个属性和值。开发人员环境变量保存在script/dev/env.js中,代码如下:
module.exports={
APP_VERSION: require("../../package.json").vesion,
ENV_NOW: 'development',
HTTP_SERVER: "******.com",
SENTRY_SERVICE: "https://******.com/34",
ELECTRON_DISABLE_SECURITY_WARNINGS: true,
};
APP_VERSION:项目的版本号
ENV_NOW: 在开发环境中被设置成develoment,在生产中被设置成production
HTTP_SERVER: 服务器域名
SENTRY_SERVICE: 日志服务
ELECTRON_DISABLE_SECURITY_WARINGES: 屏蔽Electron开发者调试工具安全警告。
自定义获取环境变量的函数代码如下:
//设置环境变量
getEnvScript(){
let env=require("./env.js");
env.WEB_PORT=this.serverPort;
env.RES_DIR=path.join(process.cwd(),"resource/release");
let script="";
for(let v in env){
script += `process.env.${v}="${env[v]}";`;
}
return script;
}
上述代码返回一段javascript字符串,这段javascript代码是由一系列的设置process.env属性的语句组成,这些设置process.env的语句包含env.js中定义的属性,还额外包含WEB_PORT和RES_DIR两个环境变量。WEB_PORT是通过vite启动的http服务端口号。RES_DIR是指存放外部资源的目录,在运行期间指向当前工程的resource/release子目录。
五、构建主进程代码
启动了开发环境http服务,准备环境变量后,然后就是编译主进程代码,因为Electron应用启动后首先执行的是主进程代码,所以先把此工作完成,Vite自带的esbuild编译代码快,所以使用esbuild编译,代码如下:
buildMain(){
let entryFilePath=path.join(process.cwd(),"src/main/app.ts");
let outfile=path.join(process.cwd(),"release/bundled/entry.js")
esbuild.buildSync({
entryPoints: [entryFilePath],
outfile,
minify: false,
bundle: true,
platform: "node",
sourcemap: true,
external: ["electron"],
});
let envScript=this.getEnvScript();
let js=`${envScript}${os.EOL}${fs.readFileSync(outfile)}`;
fs.writeFileSync(outfile,js);
},
在代码中,首先获取 到主进程入口文件的绝对路径entryFilePath,这个文件在当前工程的src/main子目录下,其次定义了主进程代码编译完成后输出文件的路径outfile,这个文件路径指向当前工程的release/bundled子目录。
接着通过esbuild.buildSync方法编译主进程代码逻辑,因并不需要压缩代码,所以minify配置为false。把external配置为electron是为了不让编译工具尝试加载Electron模块内容,因为运行环境自带此模块,所以编译工具不必编译此模块代码。
最后通过getEnvScript方法得到设置环境变量的javascript代码字符串,并通过fs模块writeFileSync方法把它写到主进程代码文件的第一行,即在主进程代码逻辑执行前,先完成环境变量的设置工作。
此段逻辑有三个关键点需注意:
(1)必须逐一附加环境变量到process.env对象上,而不能如下批量更改
process.env={...process.env,...${JSON.stringify(env)}}
因应用如果加载第三方原生addon,而第三方addon依赖当前应用的环境变量,批量更改将使它无力获取当前应用环境变量。
(2)设置http服务端口到环境变量,可以在主进程中通过如下方式加载页面:
if(process.env.ENV_NOW==="development"){
return 'http://localhost:${process.env.WEB_PORT}/#${url}';
} else {
return 'app:// ./index.html/#${url}';
}
(3) 设置RES_DIR环境变量目的是使代码能方便地访问到这些外部资源。
(4) 在渲染进程中使用环境变量,必须写作process["env"].ENV_NOW,而不能写作process.env.ENV_NOW。
六、启动Electron子进程
启动Electron必须在准备好http服务,编译主进程代码后执行,模仿electron包内的cli.js实现如下逻辑。
createElectronProcess() {
this.electronProcess = spawn(
require("electron").toString(),
[path.join(process.cwd(), "release/bundled/entry.js")],
{ cwd: process.cwd() }
);
this.electronProcess.on("close", () => {
this.server.close();
process.exit();
});
this.electronProcess.stdout.on("data", (data) => {
data = data.toString();
console.log(data);
});
},
此段代码首先通过nodejs的child_process模块的spawn方法启动Electron子进程,启动这个子进程时,传递命令行参数:path.join(this.bundleDir,"entry.js"),此参数是编译主进程后生成的入口文件,这样Electron启动时会自动加载并执行主进程逻辑。Electron子进程启动后监听退出事件close,一旦Electron子进程退出,vite启动的http服务也退出:this.server.close(),同时整个开发环境也退出 。
至此,Electron开发环境搭建完成,下面是主进程代码文件的演示性代码:
let start = Date.now();
import { app, BrowserWindow, protocol } from "electron";
import path from "path";
import fs from "fs";
protocol.registerSchemesAsPrivileged([
{
scheme: "app",
privileges: {
standard: true,
supportFetchAPI: true,
secure: true,
corsEnabled: true,
},
},
]);
let mainWindow: BrowserWindow;
app.on("ready", () => {
protocol.registerBufferProtocol("app", (request, response) => {
let pathName = new URL(request.url).pathname;
let extension = path.extname(pathName).toLowerCase();
if (!extension) return;
pathName = decodeURI(pathName);
let filePath = path.join(__dirname, pathName);
fs.readFile(filePath, (error, data) => {
if (error) return;
let mimeType = "";
if (extension === ".js") {
mimeType = "text/javascript";
} else if (extension === ".html") {
mimeType = "text/html";
} else if (extension === ".css") {
mimeType = "text/css";
} else if (extension === ".svg") {
mimeType = "image/svg+xml";
} else if (extension === ".json") {
mimeType = "application/json";
}
response({ mimeType, data });
});
});
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
webSecurity: false,
nodeIntegration: true,
contextIsolation: false,
},
});
if (app.isPackaged) {
console.log(start, Date.now() - start,'index.html');
mainWindow.loadURL(`app://./index.html`);
} else {
console.log(start, Date.now() - start,`http://localhost:${process.env.WEB_PORT}/`);
mainWindow.loadURL(`http://localhost:${process.env.WEB_PORT}/`);
}
});
在当前工程目录下启动命令行,执行npm run start命令,效果如下:
七、配置调试环境
配置VSCode的调试环境,主要是在工程根目录下创建.vscode子目录,并在此目录下创建名为launch.json文件,代码如下:
{
"version": "0.2.0",
"configurations": [
{
"type":"node",
"request": "launch",
"name": "Start",
"program": "${workspaceFolder}/script/dev/index.js",
"cwd": "${workspaceFolder}"
}
]
}
配置完成后点击“运行”图标,将出现名为Start的开始调试启动项。
八、打包源码
首先,在script\release目录下创建名为index.js 的文件,通过脚本文件执行如下四项任务以完成安装包的制作工作。
(1)构建渲染进程代码
(2)构建主进程代码
(3)安装工程依赖包
(4)制成安装文件
index.js代码如下:
let vite = require("vite");
let path = require("path");
let esbuild = require("esbuild");
let os = require("os");
let fs = require("fs");
let vue = require("@vitejs/plugin-vue");
let dev = {
getEnvScript() {
let env = require("./env.js");
let script = "";
for (let v in env) {
script += `process.env.${v}="${env[v]}";`;
}
script += `process.env.RES_DIR = process.resourcesPath;`;
return script;
},
buildMain() {
let entryFilePath = path.join(process.cwd(), "src/main/app.ts");
let outfile = path.join(process.cwd(), "release/bundled/entry.js");
esbuild.buildSync({
entryPoints: [entryFilePath],
outfile,
minify: true,
bundle: true,
platform: "node",
sourcemap: false,
external: ["electron"],
});
let envScript = this.getEnvScript();
let js = `${envScript}${os.EOL}${fs.readFileSync(outfile)}`;
fs.writeFileSync(outfile, js);
},
async buildRender() {
let options = {
root: process.cwd(),
build: {
enableEsbuild: true,
minify: true,
outDir: path.join(process.cwd(), "release/bundled"),
},
plugins: [vue()],
};
await vite.build(options);
},
buildModule() {
let pkgJsonPath = path.join(process.cwd(), "package.json");
let localPkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
let electronConfig = localPkgJson.devDependencies.electron.replace("^", "");
delete localPkgJson.scripts;
delete localPkgJson.devDependencies;
localPkgJson.main = "entry.js";
localPkgJson.devDependencies = { electron: electronConfig };
fs.writeFileSync(
path.join(process.cwd(), "release/bundled/package.json"),
JSON.stringify(localPkgJson)
);
fs.mkdirSync(path.join(process.cwd(), "release/bundled/node_modules"));
},
buildInstaller() {
let options = {
config: {
directories: {
output: path.join(process.cwd(), "release"),
app: path.join(process.cwd(), "release/bundled"),
},
files: ["**"],
extends: null,
productName: "yourProductName",
appId: "com.yourComp.yourProduct",
asar: true,
extraResources: require("../common/extraResources.js"),
win: require("../common/winConfig.js"),
mac: require("../common/macConfig.js"),
nsis: require("../common/nsisConfig.js"),
publish: [{ provider: "generic", url: "" }],
},
project: process.cwd(),
};
let builder = require("electron-builder");
return builder.build(options);
},
async start() {
await this.buildRender();
await this.buildMain();
await this.buildModule();
this.buildInstaller();
},
};
dev.start();
此env.js文件是release目录下的env.js,而不是dev目录下env.js,此种做法区分了开发环境与生产环境的环境变量,最重要区别是ENV_NOW环境变量,一为production,另一个为development。
九、打包依赖
Electron依赖包安装为开发依赖,目的是为了在制作安装包时,避免其被安装两次。
主进程代码与渲染进程代码被编译后放置在[ypurProject]\release\bundled目录下,不需要另外安装 ,一些特殊的库,则需要特殊的打包依赖操作。