精讀《如何編譯前端專案與元件》

黃子毅發表於2019-01-21

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 是一個擁有優秀基因的前端元件庫,是因為他遵循了前端元件最基本的程式碼素養:

  1. 編譯後的程式碼全部符合基本 JS 規範,換個角度來說,使用 webpack 內建基本 js loader 就能完全解析。
  2. 將 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 語句:

  1. 進行專案/元件除錯時,開啟 development 模式。
  2. 進行專案編譯時,開啟 production 模式。
  3. 進行元件編譯時,開啟 production 模式,且利用 webpack-node-externals 外掛忽略 node_modules。

可以想像,根據第三條,如果所有元件都按照這個模式輸出程式碼,那麼 webpack 對 node_modules 編譯時,只需要將所有 require 程式碼進行合併,不需要執行任何 loader,也不需要壓縮,不需要 TreeShaking,因為這些在元件程式碼編譯時全部已經做好了,這種構建效率幾乎達到最大。

實際案例

我們拿支援 typescriptsasscss-modulesworker-loader 的場景作為案例。

我們建立三個檔案 entry.tsx entry.worker.tsentry.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 的生態,因此維護性和擴充性都很強。後續再加入新功能,再也不需要到處找 babelgulp 的外掛了!

3 總結

本文從 webpack 為切入點,但其實還可以從 parcelgulp 為切入點,實現前端專案、元件構建體系的統一。

不過從可定製性來看,webpack 外掛生態更完善,所以筆者選擇了 webpack

留下一個思考題:你的專案、元件是如何構建的呢?是用了一套程式碼,還是兩套呢?

討論地址是:精讀《如何編譯前端專案與元件》 · Issue #125 · dt-fe/weekly

如果你想參與討論,請點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 – 幫你篩選靠譜的內容。

來源:https://juejin.im/post/5c451ab06fb9a049fe357481#comment

相關文章