藉助 Parcel 搭建 Electron 應用開發環境
ead>theme: juejin
本文為稀土掘金技術社群首發簽約文章,14天內禁止轉載,14天后未獲授權禁止轉載,侵權必究!
前言
先對這個系列的專欄做一個簡單的介紹。
本專欄主要是通過 Electron
和 ProseMirror
兩個框架來開發一個本地的 Markdown 編輯器,UI 框架我會選擇 React
,然後使用 Typescript
來開發。專欄的內容會涉及到更新升級、自定義視窗、多 tab 欄的實現、跨程序通訊、Sqlite
資料庫實踐、應用上架等。
雖然這個專案是從零開發,但是不會講解太多的關於如何配置 Webpack
等方面的知識,相關內容你們可以搜尋其他博文進行學習。當然,有任何問題歡迎直接在評論區提問,我會進行解答。
本文所示程式碼均在 Github 倉庫中
main 和 renderer
使用過 Electron 的同學都知道 main
主程序 和 renderer
渲染程序。
main 負責使用 Nodejs 處理和系統互動的部分,renderer 負責渲染呈現在使用者面前的 UI 介面。他倆通過 IPC 進行通訊。
當我們要開始開發一個 electron 專案時,這裡以主流的構建工具 Webpack 為例。當編譯 main 程式碼時,編譯目標要選擇 electron-main
,這樣生產依賴的庫的程式碼就不會打包到最終的 main.js 中,依舊保持類似 const fse = require("fs-extra")
的形式,在 runtime 時動態從 app.asar 裡的 node_modules 裡引入。而編譯 renderer 時,以往我們是選擇 electron-renderer
,現在 electron 提供了一種 preload
的方式後,我們可以直接選擇 web
為編譯目標,這樣 renderer 的程式碼裡也不用混雜一些 node 程式碼了。
新特性 preload,類似於混合開發中的 JSBridge, 藉助 contextBridge.exposeInMainWorld('JSBridge', {})
可以往頁面中注入一個物件用來和 native 部分進行互動,在 electron 中就是和 main 主程序進行互動。
這個新設計帶來的好處是顯而易見的 。當你想要將一個 web 應用改造成擁有原生能力的桌面應用時,你只要在 preload 中實現一個 sdk,就能很簡單的將 web 應用接入進來。另一方面,禁止在renderer 中執行 node 程式碼,大大提高了安全性(防止有的庫會用 nodejs 違規操作使用者系統)。
這裡簡單看一下,我們要如何使用 preload
:
javascript
const win = new BrowserWindow({
width: 800,
height: 600,
show: false,
webPreferences: {
nodeIntegration: true,
nodeIntegrationInSubFrames: true,
devTools: app.isPackaged ? false : true,
contextIsolation: true,
preload: path.join(app.getAppPath(), './static/preload.js'),
},
})
上面的 preload 我用的是編譯後的檔案,preload 的編譯也是採用 electron-main
的標準。
renderer 渲染程序
我們現在開始建立專案,我給它取名為 ENotes
。
bash
mkdir enotes
cd enotes
yarn init -y
新增 React、ReactDom 等依賴庫
bash
yarn add react react-dom
yarn add parcel typescript @types/react @types/react-dom -D
yarn tsc --init --locale zh-cn
經常有同學會抱怨 tsconfig.json
不會配置,注意上面我加了 --locale zh-cn
的引數,這樣生成出來的 tsconfig.json 裡註釋就是中文的,如下圖所示。
現在的目錄結構是這樣的
├── package.json
├── public
│ └── template.html
├── src
│ └── renderer
│ └── index.tsx
├── tsconfig.json
└── yarn.lock
template.html 模版中,我簡單寫了如下程式碼。 ```html
``
我自己專案中是用 webpack 來編譯程式碼的,這裡為了方便,我選擇使用 [parcel](http://parceljs.org/) 來編譯 renderer 程式碼。接下來,命令列執行
yarn parcel public/template.html, 訪問
http://localhost:1234`(預設埠 1234) 就可以看到 web 介面了。
main 主程序
光有 web 頁面還不行,我們用 electron 給它加件衣服。我們先安裝 electron
bash
yarn add electron
接下來我們要實現
~~~mermaid graph LR 啟動應用 --> 顯示視窗 --> 載入網頁 ~~~
```javascript // src/main/index.ts import { app, BrowserWindow } from "electron"; import path from "path";
function createWindow() { const win = new BrowserWindow({ width: 800, height: 600, show: false, webPreferences: { devTools: app.isPackaged ? false : true, }, }); return win; }
app.whenReady().then(() => { const win = createWindow(); app.isPackaged ? win.loadFile(path.join(app.getAppPath(), "dist", "template.html")) : win.loadURL("http://localhost:1234");
win.webContents.on("dom-ready", () => { win.show(); }); }); ```
main 主程序程式碼,依舊使用 parcel 來編譯。在專案根目錄下建立 config/main.mjs
檔案,如下。
```javascript import { Parcel } from "@parcel/core";
let bundler = new Parcel({ entries: "./src/main/index.ts", defaultConfig: "@parcel/config-default", targets: { main: { distDir: "dist", context: "electron-main" }, }, });
await bundler.run(); ``` 我們在 package.json 的 scripts 中新增一些啟動指令碼。
concurrently
可以同時執行多個 npm 指令碼
bash
yarn add concurrently -D
json
// package.json
{
"scripts": {
"start": "concurrently \"npm:start:renderer\" \"npm:start:main\"",
"start:renderer": "parcel public/template.html",
"start:main": "node config/main.mjs && electron dist/index.js"
}
}
執行一下 yarn start
,就可以看到應用啟動了。
preload 預載入指令碼
接下來我們要使用 preload 為網頁增加點原生功能,看看一個簡單的 preload 該如何寫。
```typescript // src/main/preload.ts import { contextBridge } from "electron";
contextBridge.exposeInMainWorld("Bridge", { test: () => { console.log("bridge is working"); }, }); ```
同樣需要配置 config/preload.mjs
, 可以參考 main.mjs。
我們把之前程式碼裡的 createWindow
調整下,讓頁面預載入 preload。
typescript
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
show: false,
webPreferences: {
nodeIntegration: true,
nodeIntegrationInSubFrames: true,
devTools: app.isPackaged ? false : true,
contextIsolation: true,
preload: path.join(app.getAppPath(), "dist", "preload.js"),
},
});
return win;
}
在 package.json 中的 scripts 加上 preload 的編譯。
json
{
"scripts": {
"start": "concurrently \"npm:start:renderer\" \"npm:start:preload\" \"npm:start:main\"",
"start:renderer": "parcel public/template.html",
"start:preload": "node config/preload.mjs",
"start:main": "node config/main.mjs && electron dist/index.js"
}
}
使用 win.webContents.openDevTools({
mode: "detach"
});
,或者視窗顯示以後,用快捷鍵 CmdOrCtrl+Shift+I
, 開啟 devtools。我們測試一下 bridge 是否成功注入到頁面裡了。
看起來已經成功注入到頁面中。
這個例子不是很能體現 preload 的作用,我們換一個,實現一個 showMessage
方法,呼叫 electron 的提示彈窗。
```typescript // src/main/preload.ts import { contextBridge, ipcRenderer, MessageBoxOptions } from "electron";
contextBridge.exposeInMainWorld("Bridge", { showMessage: (options: MessageBoxOptions) => { ipcRenderer.invoke("showMessage", options); }, });
```
```typescript // src/main/index.ts import { dialog, MessageBoxOptions } from 'electron'
ipcMain.handle("showMessage", (_e, options: MessageBoxOptions) => { // win 是之前建立的視窗 dialog.showMessageBox(win, options); }); ``` 讓我們再測試一下。
生產環境
之前的 npm 指令碼並沒有區分環境,我們需要通過設定環境變數來實現這個。
cross-env
可以很方便地設定環境變數
bash
yarn add cross-env -D
"start:main": "cross-env NODE_ENV=development node config/main.mjs && electron dist/index.js"
單元測試
為了保證程式碼的可靠性,我們還需要為專案新增單元測試、e2e測試等。由於 Electron 不適合做 e2e 測試,所以這裡我就新增單元測試,用的是最近很火的 Vitest
。
bash
yarn add vitest -D
```js // vitest.config.js import { defineConfig } from "vitest/config";
export default defineConfig({ test: { include: ["/tests//.[jt]s?(x)", "/?(.)+(spec|test).[tj]s?(x)"], }, });
json
{
"scripts": {
"test": "vitest"
}
}
```
這樣就配置好了,是不是很簡單。我們簡單寫一個示例測試一下。
```ts // src/utils/node/index.ts import { basename, extname } from "path";
export function getFileNameWithoutExt(filePath: string) {
const name = basename(filePath);
const ext = extname(filePath);
return name.substring(0, name.lastIndexOf(ext));
}
ts
// src/utils/node/tests/index.test.ts
import { describe, test, expect } from 'vitest'
import { getFileNameWithoutExt } from '..'
describe('utils', () => { test('getFileName', () => { const filePath = '/xx/test.md' const fileName = getFileNameWithoutExt(filePath) expect(fileName).toEqual('test') }) }) ```
到這裡,基本的開發環境已經配置好了,接下來就可以愉快的開發了。
總結
用 Parcel 來編譯程式碼,明顯比 Webpack 方便了很多。
該專案的程式碼我放在這裡了 ENotes