概述
毋庸置疑,對前端開發者而言,當下正是一個日升月恒的美好時代!在久遠的過去,Web 頁面的開發技術鏈條非常原始而粗糙,那時候的 JavaScript 更多用來點綴 Web 頁面交互而不是用來構建一個完整的應用。直到 2009年5月 Ryan Dahl 正式發布 NodeJS,JavaScript 終于有機會脫離 Web 瀏覽器獨立運行,隨之而來的是,基于 JavaScript 構建應用程序的能力被擴展到越來越多場景,我們得以用相同的語言、技術棧、工具獨立開發桌面端、服務端、命令行、微前端、PWA 等應用形態。
相應地,我們需要更好的構建、模塊化以及打包能力來應對不同形態的工程化需求,所幸 Webpack 提供的功能特性,能夠充分支撐這些場景。
前面兩個章節我們已經詳細介紹了如何使用 Webpack 構建 NPM Library,以及如何基于 Module Federation 搭建微前端架構。本文將繼續匯總這些特化場景需求,包括:
- 如何使用 Webpack 構建 Progressive Web Apps 應用;
- 如何使用 Webpack 構建 Node 應用;
- 如何使用 Webpack 構建 Electron 應用。
構建 PWA 應用
PWA 全稱 Progressive Web Apps (漸進式 Web 應用),原始定義很復雜,可以簡單理解為 一系列將網頁如同獨立 APP 般安裝到本地的技術集合,借此,我們即可以保留普通網頁輕量級、可鏈接(SEO 友好)、低門檻(只要有瀏覽器就能訪問)等優秀特點,又同時具備獨立 APP 離線運行、可安裝等優勢。
實現上,PWA 與普通 Web 應用的開發方法大致相同,都是用 CSS、JS、HTML 定義應用的樣式、邏輯、結構,兩者主要區別在于,PWA 需要用一些新技術實現離線與安裝功能:
- ServiceWorker: 可以理解為一種介于網頁與服務器之間的本地代理,主要實現 PWA 應用的離線運行功能。例如
ServiceWorker
可以將頁面靜態資源緩存到本地,用戶再次運行頁面訪問這些資源時,ServiceWorker
可攔截這些請求并直接返回緩存副本,即使此時用戶處于離線狀態也能正常使用頁面;
-
manifest 文件:描述 PWA 應用信息的 JSON 格式文件,用于實現本地安裝功能,通常包含應用名、圖標、URL 等內容,例如:
// manifest.json {"icons": [{"src": "/icon_120x120.0ce9b3dd087d6df6e196cacebf79eccf.png","sizes": "120x120","type": "image/png"}],"name": "My Progressive Web App","short_name": "MyPWA","display": "standalone","start_url": ".","description": "My awesome Progressive Web App!" }
我們可以選擇自行開發、維護 ServiceWorker
及 manifest
文件 ,也可以簡單點使用 Google 開源的 Workbox 套件自動生成 PWA 應用的殼,首先安裝依賴:
yarn add -D workbox-webpack-plugin webpack-pwa-manifest
其中:
workbox-webpack-plugin
:用于自動生成ServiceWorker
代碼的 Webpack 插件;webpack-pwa-mainifest
:根據 Webpack 編譯結果,自動生成 PWA Manifest 文件的 Webpack 插件。
之后,在 webpack.config.js
配置文件中注冊插件:
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { GenerateSW } = require("workbox-webpack-plugin");
const WebpackPwaManifest = require("webpack-pwa-manifest");module.exports = {// ...plugins: [new HtmlWebpackPlugin({title: "Progressive Web Application",}),// 自動生成 Manifest 文件new WebpackPwaManifest({name: "My Progressive Web App",short_name: "MyPWA",description: "My awesome Progressive Web App!",publicPath: "/",icons: [{// 桌面圖標,注意這里只支持 PNG、JPG、BMP 格式src: path.resolve("src/assets/logo.png"),sizes: [150],},],}),// 自動生成 ServiceWorker 文件new GenerateSW({clientsClaim: true,skipWaiting: true,}),],
};
之后,執行編譯命令如 npx webpack
就可以生成如下資源:
├─ 8-1_pwa
│ ├─ src
│ │ ├─ xxx
│ ├─ dist
│ │ ├─ icon_150x150.119e95d3213ab9106b0f95100015a20a.png
│ │ ├─ index.html
│ │ ├─ main.js
│ │ ├─ manifest.22f4938627a3613bde0a011750caf9f4.json
│ │ ├─ service-worker.js
│ │ ├─ workbox-2afe96ff.js
│ └─ webpack.config.js
接下來,運行并使用 Chrome 打開頁面,打開開發者工具,切換到 Applicatios > Service Workers
面板,可以看到:
這表明 Service Worker 已經正常安裝到瀏覽器上。此外,地址欄右方還會出現一個下載圖標:
點擊該圖標可將應用下載到本地,并在桌面創建應用圖標 —— 效果如同安裝獨立 App 一樣。
提示:PWA 是一種復雜度較高的技術,前文只是介紹了一種 Webpack 構建 PWA 的簡單方法,感興趣的同學可以擴展閱讀:
- developer.chrome.com/docs/workbo…
- developers.google.com/web/fundame…
構建 Node 應用
注意,在開發 Node 程序時使用 Webpack 的必要性并不大,因為 Node 本身已經有完備的模塊化系統,并不需要像 Web 頁面那樣把所有代碼打包成一個(或幾個)產物文件!即使是為了兼容低版本 Node 環境,也可以使用更簡單的方式解決 —— 例如 Babel,引入 Webpack 反而增加了系統復雜度以及不少技術隱患。
不過,出于學習目的,我們還是可以了解一下使用 Webpack 構建 Node 程序的方法及注意事項,包括:
- 需要 Webpack 的
target
值設置為node
,這能讓 Webpack 忽略fs/path
等原生 Node 模塊; - 需要使用
externals
屬性過濾node_modules
模塊,簡單起見,也可以直接使用webpack-node-externals
庫; - 需要使用
node
屬性,正確處理__dirname
、__filename
值。
一個典型的 Node 構建配置如下:
const nodeExternals = require("webpack-node-externals");module.exports = merge(WebpackBaseConfig, {// 1. 設置 target 為 nodetarget: "node",entry: ...,module: [...],// 2. 過濾 node_modules 模塊externals: [nodeExternals()],// 3. 設置 __dirname, __filename 值node: {__filename: false,__dirname: false,},
});
在此基礎上,我們可以復用大多數 Loader、Plugin 及 Webpack 基礎能力實現各種構建功能。
不過,需要特別注意,在 Node 代碼中請務必慎用動態 require
語句,你很可能會得到預期之外的效果!例如對于下面的示例目錄:
├─ example
│ ├─ src
│ │ ├─ foo.js
│ │ ├─ bar.js
│ │ ├─ unused.js
│ │ └─ main.js
│ ├─ package.json
│ └─ webpack.config.js
其中 main.js
為入口文件,代碼:
const modules = ['foo', 'bar'].map(r => require(`./${r}.js`));
可以看到在 main.js
中并沒有引用 unused.js
,但打包產物中卻包含了 src
目錄下所有文件:
這是因為 Webpack 遇到示例中的 require
語句時,僅僅依靠詞法規則、靜態語義、AST 等手段并不能推斷出實際依賴情況,只能退而求其次粗暴地將所有可能用到的代碼一股腦合并進來,這種處理手段很可能會帶來許多意想不到的結果,很可能觸發 BUG!
綜上,建議盡量不要使用 Webpack 構建 Node 應用。
構建 Electron 應用
Electron 是一種使用 JavaScript、HTML、CSS 等技術構建跨平臺桌面應用開發框架,這意味著我們能用我們熟悉的大部分 Web 技術 —— 例如 React、Vue、Webpack 等開發桌面級應用程序。實際上,許多大名鼎鼎的應用如 VSCode、Facebook Messenger、Twitch,以及國內諸多小程序 IDE 都是基于 Electron 實現的。
與 Web 頁面不同,Electron 應用由一個 主進程 及若干 渲染進程 組成,進程之間以 IPC 方式通訊,其中:
- 主進程是一個 Node 程序,能夠使用所有 Node 能力及 Electron 提供的 Native API,主要負責應用窗口的創建與銷毀、事件注冊分發、版本更新等;
- 渲染進程本質上是一個 Chromium 實例,負責加載我們編寫的頁面代碼,渲染成 Electron 應用界面。
- 提示:Chromium 是一個非常簡潔的開源瀏覽器,許多瀏覽器都基于 Chromium 二次開發而成,例如 Chrome、Microsoft Edge、Opera 等。
Electron 這種多進程機構,要求我們能在同一個項目中同時支持主進程與若干渲染進程的構建,兩者打包需求各有側重。接下來我們將通過一個簡單示例,逐步講解如何使用 Webpack 搭建一套完備的 Electron 應用構建環境,示例文件結構如下:
8-3_electron-wp
├─ package.json
├─ webpack.main.config.js // 主進程構建配置
├─ webpack.renderer.config.js // 渲染進程構建配置
├─ src
│ ├─ main.js
│ ├─ pages
│ │ ├─ home
│ │ ├─ index.js
│ │ ├─ login
│ │ ├─ index.js
其中:
src/main.js
為主進程代碼;src/pages/${page name}/
目錄為渲染進程 —— 即桌面應用中每一個獨立頁面的代碼;- 由于主進程、渲染進程的打包差異較大,這里為方便演示,直接寫成兩個配置文件:
webpack.main.config.js
與webpack.renderer.config.js
。
Electron 主進程打包配置
主進程負責應用窗口的創建銷毀,以及許多跨進程通訊邏輯,可以理解為 Electron 應用的控制中心,簡單示例:
// src/main.js
const { app, BrowserWindow } = require("electron");// 應用啟動后
app.whenReady().then(() => {// 創建渲染進程實例const win = new BrowserWindow({width: 800,height: 600});// 使用 BrowserWindow 實例打開頁面win.loadFile("home.html");
});
代碼核心邏輯是在應用啟動后 (app.whenReady
鉤子),創建 BrowserWindow
實例并打開頁面。
- 提示:建議結合 Electron 官方提供的 完整示例 一起學習。
Electron 主進程本質上是一個 Node 程序,因此許多適用于 Node 的構建工具、方法也同樣適用主進程,例如 Babel、TypeScript、ESLint 等。與普通 Node 工程相比,構建主進程時需要注意:
- 需要將
target
設置為electron-main
,Webpack 會自動幫我們過濾掉一些 Electron 組件,如clipboard
、ipc
、screen
等; - 需要使用
externals
屬性排除node_modules
模塊,簡單起見也可以直接使用 webpack-node-externals 包; - 生產環境建議將
devtools
設置為false
,減少包體積。
對應的配置腳本:
// webpack.main.config.js
const path = require("path");
const nodeExternals = require("webpack-node-externals");module.exports = {// 主進程需要將 `target` 設置為 `electron-main`target: "electron-main",mode: process.env.NODE_ENV || "development",// 開發環境使用 `source-map`,保持高保真源碼映射,方便調試devtool: process.env.NODE_ENV === "production"? false: "source-map",entry: {main: path.join(__dirname, "./src/main"),},output: {filename: "[name].js",path: path.join(__dirname, "./dist"),},externals: [nodeExternals()],
};
至此,一個非常簡單的主進程腳本與構建環境示例就搭建完畢了,執行下述命令即可完成構建工作:
npx webpack -c webpack.main.config.js
另外,安裝 Electron 過程中可能會遇到網絡超時問題,這是因為資源域已經被墻了,可以使用阿里云鏡像解決:
ELECTRON_MIRROR="https://cdn.npm.taobao.org/dist/electron/" npm i -D electron
Electron 渲染進程打包配置
Electron 渲染進程本質上就一個運行在 Chromium 瀏覽器上的網頁,開發方法基本等同于我們日常開發的普通 Web 頁面,例如我們可以用 React 開發 Electron 渲染進程:
// src/home/index.js
import React from "react";
import ReactDOM from "react-dom";const root = document.createElement("div");ReactDOM.render(<h1>Hello world!</h1>, root);document.body.append(root);
相應的,我們可以復用大部分普通 Web 頁面構建的方式方法,主要差異點:
- 需要將 Webpack 的
target
配置設置為electron-renderer
; - Electron 應用通常包含多個渲染進程,因此我們經常需要開啟多頁面構建配置;
- 為實現渲染進程的 HMR 功能,需要對主進程代碼稍作改造。
第一點很簡單:
// webpack.renderer.config.js
module.exports = {// 渲染進程需要將 `target` 設置為 `electron-renderer`target: "electron-renderer"
};
提示:Webpack 為 Electron 提供了三種特殊
target
值:electron-main/electron-renderer/electron-preload
,分別用于主進程、Renderer 進程、Preload 腳本三種場景。
第二點可以用多 entry
配置實現,如:
// webpack.renderer.config.js
// 入口文件列表
const entries = {home: path.join(__dirname, "./src/pages/home"),login: path.join(__dirname, "./src/pages/login"),
};// 為每一個入口創建 HTMLWebpackPlugin 實例
const htmlPlugins = Object.keys(entries).map((k) =>new HtmlWebpackPlugin({title: `[${k}] My Awesome Electron App`,filename: `${k}.html`,chunks: [k],})
);module.exports = {mode: process.env.NODE_ENV || "development",entry: entries,target: "electron-renderer",plugins: [...htmlPlugins],// ...
};
第三點,由于 Webpack 的 HMR 功能強依賴于 WebSocket 實現通訊,但 Electron 主進程常用文件協議 file://
打開頁面,該協議不支持 WebSocket 接口,為此我們需要改造主進程啟動代碼,以 HTTP 方式打開頁面代碼,如:
function createWindow() {const win = new BrowserWindow({//...});if (process.env.NODE_ENV === "development") {// 開發環境下,加載 http 協議的頁面,方便啟動 HMRwin.loadURL("http://localhost:8080/home");} else {// 生產環境下,依然使用 `file://` 協議win.loadFile(path.join(app.getAppPath(), "home.html"));}
}
- 提示:在生產環境中,出于性能考慮,Electron 主進程通常會以 File URL Scheme 方式直接加載本地 HTML 文件,這樣我們就不必為了提供 HTML 內容而專門啟動一個 HTTP 服務進程。不過,同一份代碼,用 File URL Scheme 和用 HTTP 方式打開,瀏覽器提供的接口差異較大,開發時注意區分測試接口兼容性。
至此,改造完畢
總結
綜上,Webpack 不僅能構建一般的 Web 應用,理論上還適用于一切以 JavaScript 為主要編程語言的場景,包括 PWA、Node 程序、Electron 等,只是不同場景下的具體構建需求略有差異:
- PWA:需要使用
workbox-webpack-plugin
自動生成ServiceWorker
代碼;使用webpack-pwa-mainifest
Manifest 文件; - Node 程序:需要設置 Webpack 配置項
target = "node"
;需要使用 externals 屬性過濾node_modules
模塊;需要使用 node 屬性正確處理 Node 全局變量; - Electron 桌面應用:需要為主進程、渲染進程分別設置不同的構建腳本;同時需要注意開發階段使用 HMR 的注意事項。
這種強大、普適的構建能力正是 Webpack 的核心優勢之一,同類工具無出其右者,雖然不能一招鮮吃天下,但也足夠覆蓋大多數前端應用場景。站在學習的角度,你可以將主要精力放在 Webpack 基礎構建邏輯、配置規則、常用組件上,遇到特殊場景時再靈活查找相應 Loader、Plugin 以及其它生態工具,就可以搭建出適用的工程化環境。