最近新搭建了兩個 CMS (內容管理)系統,爲了減小開發切換項目成本,和下降用戶使用成本,兩個系統使用了統一的腳手架 antd-pro。css
在功能開發的過程當中發現,兩個系統之間存在不少相同的功能、邏輯。可能 A 項目寫一下,後面 B 項目須要一樣的實現又得再寫一遍。前端
例如登陸頁面的 UI、個性化 Table 組件封裝、PageLoading 組件、請求封裝等等,都是些 ts/tsx 文件,部分組件可能會依賴 less,jpeg/png/svg 等資源。node
無需多言,這樣的存在確定是不合理的,如何避免 copy/paste?首先想到的把公共模塊抽離出來發佈一個 npm package。webpack
說到模塊,首先想到的就是如何打包,常見的打包工具備一下幾種:git
工具 | 優勢 | 缺點 |
---|---|---|
webpack/rollup | 沒有實現不了的 | 1. 配置麻煩(多個 entry/多個 output,還要保留組件路徑) |
typescript + babel | 邏輯清晰,tsc 生成 d.ts + babel copy-files 能夠很輕鬆把組件的 less、d.ts、js 文件生成到指定目錄 | 只能處理 js、沒法處理 less、靜態資源 |
零配置打包工具 parcel、microbundle | 零配置 | 場景固定,可配置項少,定製化成本極高 |
經過簡單地對比,要定製化打包組件、還要打包 less/images,基本上只能選 webpack/rollup 了。github
打包成 commonjs
規範,編譯 less
爲 css
,生成 d.ts
文件。web
包目錄樹大概是這樣:正則表達式
@scope/common
- package.json
- dist/
- assets/
- logo.jpg
- components/
- MyComponent/
- index.css
- index.js
- index.d.ts
複製代碼
到時候在項目中使用組件和 Antd
組件單獨使用方式同樣。手動引入 js 組件,再引入 css
文件typescript
import MyComponent1 from '@scope/common/dist/components/MyComponent1'
import '@scope/common/dist/components/MyComponent/index.css'
import MyComponent2 from '@scope/common/dist/components/MyComponent2'
import '@scope/common/dist/components/MyComponent2/index.css'
複製代碼
常規操做,方案確定是可行的,可是可是對比抽離模塊以前的方式,用起來真的很!不!方!便!原來的用法,直接引入組件就能夠了,自動會將組件內部的其餘依賴打包,簡潔太多了。npm
import MyComponent1 from '../../components/MyComponent1'
import MyComponent2 from '../../components/MyComponent2'
複製代碼
並且還有另外一個問題,本來組件內使用的圖片資源怎麼辦?所有處理成 base64 嗎?這樣包體積會大幅增長。
回到最初的需求,抽離公共模塊的目的是爲了讓組件、邏輯獲得複用,避免一樣的代碼散落在各處。這部分代碼只是咱們從兩個管理系統抽離出來的公共代碼,不具有全局通用性。
抽離後的用法最好也跟原來的方式同樣,以 tsx?
文件爲入口,內部的其餘依賴都能被正確的 loader 處理。
大膽假設:直接發佈 typescript、less 代碼,由實際使用方來決定如何打包。
固然能夠了!,對於打包工具 webpack 來講,須要打包的文件放在哪一個目錄下還不是都同樣。哪些文件使用哪些 loader
,是經過 module.rule
中的 test
, include
, exclude
參數來配置的。
例如一下的配置,webpack 會讓 my-project 下,非 node_modules 的代碼都通過 babel-loader
module: {
rules: [
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: [
/my-project/
],
exclude: [
/node_modules/
],
use: [
... babel-loader
]
}
]
}
複製代碼
umi(antd-pro 封裝的框架) 使用的打包工具就是 webpack,從源碼上看,umi@3.0.2 以上版本就會自動處理 node_modules 下的 Typescript
webpackConfig.module
.rule('ts-in-node_modules')
.test(/\.(jsx|ts|tsx)$/)
.include.add(/node_modules/).end()
.use('babel-loader')
.loader(require.resolve('babel-loader'))
.options(babelOpts);
複製代碼
這裏用的是 webpack-chian 的語法,它能夠經過鏈式寫法生成 webpack 配置。
本來文章到這裏就結束了。當我滿心歡喜開發時又出問題了,我將抽離出來的公共包,使用 npm link
到管理項目中後,報錯了。
從報錯上看,tsx 並無正確通過 babel-loader,why??? 仔細確認生成的 webpack 配置項以後,將目光鎖定在了 webpack 自己。難道是軟鏈引發的?
查閱文檔,找到 webpack 軟鏈相關的配置項 resolve.symlinks
Whether to resolve symlinks to their symlinked location. When enabled, symlinked resources are resolved to their real path, not their symlinked location. Note that this may cause module resolution to fail when using tools that symlink packages (like npm link).
默認狀況下,webpack 會將軟鏈解析成真實路徑(關掉這個配置可能會致使其餘問題)。因此問題是,使用 npm link
時,正則表達式 /node_modules/ test
匹配不到這個路徑,致使最終公共模塊的代碼沒有通過 babel-loader
處理。
這個報錯在 npm publish
以後再 install
下來的狀況是不會出現的。
問題明確了,剩下的就是修改 webpack 配置。
umi 經過 chainWebpack 項修改配置,用的仍是 webpack-chain 語法。這裏只須要把公共模塊的絕對路徑加上就好了。
// config/config.ts
chainWebpack(memo) {
memo.module
.rule('ts-in-node_modules')
.include.add(require('path').join(__dirname, '../../packages/'));
return memo;
}
複製代碼
Done, 如今能夠無痛抽離公共模塊了~ Happy Coding。
爲了方便組件的使用,能夠定義 index.tsx 來爲作 reexport,而不是深刻到組件目錄。
// index.tsx reexport
export { MyComponent } from './src/components/MyComponent'
// new usage
import { MyComponent } from '@scope/common'
vs
// old way
import MyComponent from '@scope/common/src/components/MyComponent'
複製代碼
還須要修改 package.json
,聲明 main
和 types
{
main: './index.tsx',
types: './index.tsx'
}
複製代碼
項目中咱們使用 lerna 來管理多個前端項目,全部代碼都在同一個文件夾下,若是不發包而是經過相對路徑去引用公共文件夾下的代碼能夠嗎?
- packages/
- module-a/
- module-b/
- commons/
// 在 module-a 中直接經過路徑訪問
import PageLoading from '../../../commons/PageLoading'
複製代碼
這種形式的問題是,module-a 代碼範圍超出了它自己應該負責的範疇。而且沒有 npm 版本的概念,牽一髮而動全身,容易出現【不肯定】的更新,還有分支依賴的問題。