1 引言
說到前端編譯方案,也就是如何打包項目,如何編譯元件,可選方案有很多,比如:
- 通過 webpack / parcel / gulp 建構項目。
- 通過 parcel / gulp / babel 建構元件。
如果你喜歡零配置的 parcel,那麼項目群組件都可以拿它來編譯。
如果你業務比較複雜,需要使用 webpack 做深度定制,那麼常見組合是:項目 - webpack,元件 - gulp。
但項目與元件的編譯存在異同點,不同建構工具支援的生态也存在異同點。
webpack parcel gulp 生态的差別
- babel 一般不會解析子產品,也就是一般僅做代碼預處理,而不會改變檔案結構,也對 require、import 語句不敏感。
- webpack / parcel 主要就是解決子產品化打包問題,因為浏覽器還不支援(現在部分支援
)。type="module"
- gulp 理論上可以将 babel、webpack、parcel 作為插件,但這是後來的事。曆史上由于 gulp 是作為 grunt 的替代品出現,當時要解決的問題是處理浏覽器相容問題,打包 scss 或 less,做一些公共資源替換,雪碧圖等,最後可以順帶合并到一個檔案,但子產品化功能遠遠比 webpack 弱,基本上隻能合并,但不能 “了解子產品概念”。
項目建構與元件建構的差別
項目建構的目的主要在于釋出 CDN,是以大家一般不在乎建構腳本的通用性。換句話說,無論項目使用了怎樣的建構方式,怎樣了解
import
語句,甚至寫出
require.context
等自定義文法,隻要最終編譯出符合浏覽器規範的代碼(考慮到相容性)就足夠。
元件建構的目的主要在于釋出 NPM,除了 ESNext 規範會使用 Babel 編譯成 ES3,大部分代碼寫的很收斂,甚至對 SASS 的使用都要與 Typescript 插件一起組合成複雜的 Gulp Task。
是以往往大家會對項目采取複雜的建構限制政策,而對元件的編譯采取相對簡單的辦法,確定釋出代碼的通用性。
是以在大部分項目使用 webpack 支援 worker-loader 時,編寫元件時發現這段代碼不靈了。或者至少你得付出一些代價,因為元件的調試依然可以利用 webpack-dev-server,這時可以加上 worker-loader,但由于 gulp 沒有靠譜的 worker 插件,你的元件可能需要将 Worker 引用部分原樣輸出,希望由引用它的項目做掉對 worker-loader 的支援。
其實這種心态是很危險的,不僅導緻了元件不通用,甚至引發了各建構工具的 Tree Shaking 優化。原因就是建構元件的代碼太原始,備援的代碼沒有删除,甚至直接引用的 SASS 代碼仍然保留,更危險的是帶上了一些特殊 webpack loader 才支援的文法。
之是以說 Antd 是一個擁有優秀基因的前端元件庫,是因為他遵循了前端元件最基本的代碼素養:
- 編譯後的代碼全部符合基本 JS 規範,換個角度來說,使用 webpack 内置基本 js loader 就能完全解析。
- 将 css 代碼抽離出來,這樣不會強制項目對 node_modules 的代碼應用 css-loader。
是以一個 靠譜的元件庫 的産出檔案,應該符合基本 ES 子產品化規範,且不包括任何特殊文法。
但是這引發了一個新的問題:元件開發體驗比項目差很多。
比如元件想使用雪碧圖自動優化、想使用 worker-loader 友善快捷的調用多線程,想用自己的 css modules,甚至想把項目裡一堆 PostCSS 快捷文法搬過來時怎麼辦?難道元件開發就不能獲得與項目開發一樣的體驗嗎?
要解決這個問題,筆者介紹一種基于 webpack 的通用建構方案,讓本地調試、CDN 打包、ES6 -> ES3 轉換 都使用統一套配置代碼,同一套 loader。
2 精讀
核心思想隻有一句話:利用
webpack-node-externals忽略 Webpack 對指向 node_modules 的 require 或 import 語句:
可以想像,根據第三條,如果所有元件都按照這個模式輸出代碼,那麼 webpack 對 node_modules 編譯時,隻需要将所有
require
代碼進行合并,不需要執行任何 loader,也不需要壓縮,不需要 TreeShaking,因為這些在元件代碼編譯時全部已經做好了,這種建構效率幾乎達到最大。
實際案例
我們拿支援
typescript
、
sass
css-modules
worker-loader
的場景作為案例。
我們建立三個檔案
entry.tsx
entry.worker.ts
與
entry.scss
:
entry.scss:
.container {
border: 1px solid #ccc;
}
.primary {
color: blue;
&:hover {
color: green;
}
}
entry.worker.ts:
import hello from "hello";
const ctx: Worker = self as any;
ctx.onmessage = event => {
ctx.postMessage(hello());
};
export default null as any;
entry.tsx:
import * as React from "react";
import styles from "./entry.scss";
import * as MyWorker from "./parser.worker";
const worker = new MyWorker();
export default () => (
<div className={styles.container}>
<button className={styles.primary}>Click Me.</button>
</div>
);
在上面三個檔案中,我們分别利用了 Typescript 編譯、SCSS 編譯、css-modules 解析、worker-loader 解析(利用 webpack 自動生成字元串代碼并利用 Blob URL 方式載入,這樣就不需要建立新檔案也可以用 worker 了,也不會存在跨域問題)。
為了支援這幾個特性對如上代碼做調試、項目釋出、元件釋出,我們分别看下這三個場景該如何配置編譯腳本。
本地調試
本地調試是不用區分元件與項目的。因為無論何種情況,都需要進行基本的項目編譯,載入所有自定義 loader 并打成一個 bundle 包。
此時我們隻要維護一份
webpack
配置即可:
const webpackConfig = {
mode: "development",
module: {
rules: [
{
test: /\.worker\.tsx?$/,
use: {
loader: "worker-loader",
options: {
inline: true
}
},
include: path.join(projectRootPath, "src")
},
{
test: /\.tsx?$/,
use: [
[
"babel-loader",
{
plugins: [
[
"babel-plugin-react-css-modules",
{
filetypes: {
".scss": {
syntax: "postcss-scss"
}
}
}
]
]
}
],
"ts-loader"
],
include: path.join(projectRootPath, "src")
},
{
test: /\.scss$/,
use: [
"style-loader",
[
"css-loader",
{
importLoaders: 1,
modules: true
}
],
"sass-loader"
],
include: path.join(projectRootPath, "src")
}
]
}
};
export default webpackConfig;
利用這個配置加上
webpack-dev-server
即可完成元件與項目的本地調試。
項目釋出
項目釋出時,需要将所有代碼打入到一個 bundle 包,此時隻需使用
webpack-cli
即可,對配置做如下修改:
export default {
...webpackConfig,
mode: "production"
};
元件釋出
元件釋出時,依然使用
webpack-cli
建構,但利用
webpack-node-externals
忽略對
node_modules
的解析。
import * as nodeExternals from "webpack-node-externals";
export default {
...webpackConfig,
mode: "production",
externals: [nodeExternals()]
};
此時編譯的元件代碼,包含了 Typescript 編譯、SCSS 編譯、css-modules 解析、worker-loader 解析,但所有
node_modules
代碼都保持原樣,比如下面的代碼:

做了代碼去重、按需加載、打包、壓縮,但因為保持了
require
原樣,是以大小隻有源碼體積。
同時上述三個場景都在複用 webpack 一套代碼的基礎上,利用了 webpack 的生态,是以維護性和拓展性都很強。後續再加入新功能,再也不需要到處找
babel
或
gulp
的插件了!
3 總結
本文從
webpack
為切入點,但其實還可以從
parcel
gulp
為切入點,實作前端項目、元件建構體系的統一。
不過從可定制性來看,
webpack
插件生态更完善,是以筆者選擇了
webpack
。
留下一個思考題:你的項目、元件是如何建構的呢?是用了一套代碼,還是兩套呢?
讨論位址是: 精讀《如何編譯前端項目與元件》 · Issue #125 · dt-fe/weekly
如果你想參與讨論,請
點選這裡,每周都有新的主題,周末或周一釋出。前端精讀 - 幫你篩選靠譜的内容。