說到前端編譯方案,也就是如何打包項目,如何編譯組件,可選方案有不少,好比:css
若是你喜歡零配置的 parcel,那麼項目和組件均可以拿它來編譯。前端
若是你業務比較複雜,須要使用 webpack 作深度定製,那麼常見組合是:項目 - webpack,組件 - gulp。node
但項目與組件的編譯存在異同點,不一樣構建工具支持的生態也存在異同點。react
type="module"
)。項目構建的目的主要在於發佈 CDN,因此你們通常不在意構建腳本的通用性。換句話說,不管項目使用了怎樣的構建方式,怎樣理解 import
語句,甚至寫出 require.context
等自定義語法,只要最終編譯出符合瀏覽器規範的代碼(考慮到兼容性)就足夠。webpack
組件構建的目的主要在於發佈 NPM,除了 ESNext 規範會使用 Babel 編譯成 ES3,大部分代碼寫的很收斂,甚至對 SASS 的使用都要與 Typescript 插件一塊兒組合成複雜的 Gulp Task。git
因此每每你們會對項目採起復雜的構建約束策略,而對組件的編譯採起相對簡單的辦法,確保發佈代碼的通用性。github
因此在大部分項目使用 webpack 支持 worker-loader 時,編寫組件時發現這段代碼不靈了。或者至少你得付出一些代價,由於組件的調試依然能夠利用 webpack-dev-server,這時能夠加上 worker-loader,但因爲 gulp 沒有靠譜的 worker 插件,你的組件可能須要將 Worker 引用部分原樣輸出,但願由引用它的項目作掉對 worker-loader 的支持。web
其實這種心態是很危險的,不只致使了組件不通用,甚至引起了各構建工具的 Tree Shaking 優化。緣由就是構建組件的代碼太原始,冗餘的代碼沒有刪除,甚至直接引用的 SASS 代碼仍然保留,更危險的是帶上了一些特殊 webpack loader 才支持的語法。typescript
之因此說 Antd 是一個擁有優秀基因的前端組件庫,是由於他遵循了前端組件最基本的代碼素養:gulp
因此一個 靠譜的組件庫 的產出文件,應該符合基本 ES 模塊化規範,且不包括任何特殊語法。
可是這引起了一個新的問題:組件開發體驗比項目差不少。
好比組件想使用雪碧圖自動優化、想使用 worker-loader 方便快捷的調用多線程,想用本身的 css modules,甚至想把項目裏一堆 PostCSS 快捷語法搬過來時怎麼辦?難道組件開發就不能得到與項目開發同樣的體驗嗎?
要解決這個問題,筆者介紹一種基於 webpack 的通用構建方案,讓本地調試、CDN 打包、ES6 -> ES3 轉換 都使用統一套配置代碼,同一套 loader。
核心思想只有一句話:利用 webpack-node-externals 忽略 Webpack 對指向 node_modules 的 require 或 import 語句:
development
模式。production
模式。production
模式,且利用 webpack-node-externals 插件忽略 node_modules。能夠想像,根據第三條,若是全部組件都按照這個模式輸出代碼,那麼 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
的插件了!
本文從 webpack
爲切入點,但其實還能夠從 parcel
或 gulp
爲切入點,實現前端項目、組件構建體系的統一。
不過從可定製性來看,webpack
插件生態更完善,因此筆者選擇了 webpack
。
留下一個思考題:你的項目、組件是如何構建的呢?是用了一套代碼,仍是兩套呢?
討論地址是: 精讀《如何編譯前端項目與組件》 · Issue #125 · dt-fe/weekly
若是你想參與討論,請點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。