不少時候,咱們爲了複用或者概括總結,會把組件抽離出來發到npm
上。可是這個過程你會發現一個問題,就是應該怎麼更好的發佈
和管理
維護這些組件呢。最後會發現網上的其餘教程不是太零散了,就是有些細節不大到位。這裏借這個機會好好總結一下,要是看完以爲有幫助的話,不妨個贊
,關注一下哈哈。css
import { demoComponent } from 'xxxUI'
方式引入import demoComponent from xxxUI/component/demoComponent
方式引入打包相對獨立
,互不干擾簡單易用
和具備良好的兼容性
按需加載
Tree Shaking
單元測試
npm
就像這樣,嘿嘿。vue
如下都以react組件庫爲例,其實vue是也同樣的,只是babel配置有所區別node
先來看看組件庫的項目結構react
嗯,看起來非常複雜,第一印象應該都在想着都是些什麼亂七八糟的文件,下面先來解釋一下。webpack
src
存放核心代碼dist
存放最後打包輸出的代碼sass
樣式單獨抽離放置(固然能夠跟組件放一塊兒,這裏的目的是即便不用相關的組件,單獨使用相關樣式也是沒問題)__mocks__
(mock對象),coverage
(覆蓋率),test,jest.config.js
(jest配置)這些都是與單元測試相關的下一章會有詳細介紹.npmignore
與.gitignore
做用相似.babelrc
大名鼎鼎的babel應該都知道的咱們先把目光聚焦到src
核心代碼目錄下,首先咱們將組件存放在component
中,在外層用index
去引用component
中的組件,因爲在不提供具體路徑的狀況下,import
引入時會默認找到index
。這樣在打包輸出後,就能經過import { demoComponent } from 'xxxUI'
這種方式去引用組件了。git
// index.jsx
import demoComponent from './component/demoComponent';
export {
demoComponent
};
複製代碼
// demoComponent.jsx
export default class demoComponent extends Component {
render() {
return (
<div> hello world </div>
);
}
}
複製代碼
而後使用這種形式去導出組件,就能經過import demoComponent from xxxUI/component/demoComponent
這種形式單獨引入組件了。es6
根據以上目錄結構和引入方式咱們能夠知道,經過import { demoComponent } from 'xxxUI'
這種形式去引入會使得整個組件庫都引入到開發項目中,有時只須要用到其中的兩三個組件,這種狀況是咱們不想看到的。而經過import demoComponent from xxxUI/component/demoComponent
這種形式去引用,就能作到只引入某個須要用到的組件,這恰好能解決這個問題。可是每次引入都要寫這麼長的一串,很不方便。這個時候就須要用到babel-plugin-import
這個插件了。github
import { demoComponent, demoComponent1, demoComponent2 } from 'xxxUI'
// 使用babel-plugin-import插件能自動將以上這種調用形式在AST(抽象語法樹)中改寫成如下形式。
// 這樣就能方便地引入相關組件,又不用擔憂一次所有引入致使包過大的問題
import demoComponent from xxxUI/component/demoComponent
import demoComponent1 from xxxUI/component/demoComponent
import demoComponent2 from xxxUI/component/demoComponent
複製代碼
最後在.babelrc
中配置須要轉換的路徑web
// .babelrc
{
...
"plugins":[
"import", {
"libraryName": "xxxUI",
"libraryDirectory": "component",
}
]
}
複製代碼
須要注意的是,這裏須要組件庫的使用者去配置,而不是寫在組件庫的
.babelrc
中。若是組件庫支持按需加載,這個配置應該寫在README.md
中交由組件庫的使用者去選擇。按需加載的好壞處是由具體的項目環境而定,須要具體狀況具體分析
。sql
就這樣,經過巧妙的文件結構,目標2,3,6
已達成。
明確了項目結構,接下來就是須要收集組件源碼了。一般來說,只須要在webpack
的entry
配置中只須要設置入口文件index
就能夠了,就像這樣entry: path.resolve(__dirname, 'src', 'index.jsx')
。但因爲咱們須要每一個組件互相獨立單獨打包,因此須要一個個組件去引入,同時也要保持相應的文件結構。
function getFileCollection() {
const globPath = './src/**/*.*(jsx|js)';
const files = glob.sync(globPath);
return files;
}
function entryConfig() {
let entryObj = {};
getFileCollection().forEach(item => {
const filePath = item.replace('./src', '');
entryObj[filePath] = path.resolve(__dirname, item);
});
return entryObj;
}
複製代碼
在這裏使用了glob
這個很好用的工具,能很方便匹配出對應的文件。最後返回的是一個文件路徑的映射對象,咱們能夠在控制檯看看輸入了哪些文件。
ok,接下來就是要怎樣處理這些源文件了。
這裏的處理過程很簡單,邏輯就是配置babel
將es6+
的源碼處理成es5
的兼容代碼,順便也將svg
小圖標轉化爲base64
格式嵌入。這樣作更可能是爲了讓用戶以儘可能小的配置,儘可能小的上手成本就能使用這個組件庫。這裏若是同時保留了es6
代碼,就可以讓讓開發者能夠自由配置Tree Shaking
了(好比開發者只用到了某個組件中的某個方法的場景下,就不必引入整個組件了)。關於開發者如何配置Tree Shaking
最後會講到。
Es6 Modules從語法層面提供了模塊化功能,
Tree Shaking
就是基於ES6模塊化的,在編譯打包節點能夠在AST
(抽象語法樹)中靜態分析,將沒有用到的代碼剔除掉。咱們通過編譯打包後的es5代碼是沒法進行Tree Shaking
的。
// webpack.config中的loader配置
rules: [{
test: /.jsx|.js$/,
loader: 'babel-loader',
exclude: /node_modules/
}, {
test: /\.(jpg|png|gif|svg|jpeg)$/,
loader: 'url-loader',
exclude: /node_modules/
}]
複製代碼
// .babelrc
{
"presets": [
["@babel/preset-env", {
// 瀏覽器兼容方案配置
"targets": {
"browsers": [
">0.25%",
"not ie 11",
"not op_mini all"
]
}
}],
"@babel/preset-react",
],
"plugins": [
// 一些必備的轉換插件
"@babel/plugin-proposal-function-bind",
"@babel/plugin-proposal-class-properties",
// 解決編譯中產生的重複的工具函數
"@babel/plugin-transform-runtime",
"transform-remove-console"
]
}
複製代碼
達成目標的第7點。
打包編譯輸出到dist
目錄,要注意的是dist
目錄中的結構要與src
目錄保持一致才能使組件和組件間的引用路徑不會亂,就像這樣,dist
目錄結構跟src
類似。
再來看看output
的配置,因爲咱們在文件輸入時保持了文件路徑信息,因此這裏直接更改後綴以後輸出到dist便可。libraryTarget
的做用在於設置打包格式,這裏採用umd
標準。若是設置了library
,那麼將會導出成單入口的引用形式import xxxUI from 'xxxUI'
,這是咱們不但願的。library
與libraryTarget
的取值根據項目類型的不一樣而不一樣。詳情看這裏
output: {
filename: (chunkData) => {
let filePath = chunkData.chunk.name;
const filename = filePath.replace('.jsx', '.js');
return filename;
},
path: __dirname + '/dist',
libraryTarget: 'umd',
// library: 'xxxUI'
}
複製代碼
萬事俱備了,但按照這樣打包後會發現,怎麼第三方包react,react-dom
也跟着打包進來了,這會致使打包以後組件庫的體積很大。
咱們須要這樣去配置,過濾掉import
進來的第三方包
externals: [
function(context, request, callback) {
// 容許編譯如下後綴文件
if (/.jsx|.jpg|.png|.gif|.svg|.jpeg$/g.test(request)) {
return callback();
}
callback(null, request);
}
]
複製代碼
能夠看到變化巨大!如今整個包大小隻有120kb
(除去樣式)
因爲樣式是獨立抽離
出來的,只須要將樣式copy到dist
目錄便可,固然可配置插件自動完成。
new CopyPlugin([{
from: './sass',
to: './sass'
}])
複製代碼
達成目標的4,5
點
npm login
登陸.npmignore
文件,將須要忽略的文件列出來README.md
,寫出必要的說明,這是一個好習慣package.json
的script
中添加命令webpack --mode production && npm publish ./dist
。這裏意思是採用生產模式打包並將dist
目錄發佈上npm
。到最後README.md
使用手冊能夠這樣寫
// 安裝
npm i -S xxxUI
// webpack配置處理樣式
{
test: /\.scss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', "postcss-loader", 'sass-loader'],
include: [
path.join(__dirname, 'node_modules/xxxUI/sass/')
]
}
// 在index.jsx中引入樣式
import "xxxUI/sass/index.scss";
// 可選項---------------
// .babelrc 配置按需加載
"plugins": [
[
"import",
{
"libraryName": "xxxUI",
"libraryDirectory": "component",
}
],
// ...
]
// 可選項---------------
// 配置Tree Shaking
// webpack.config.js
// ...
{
test: /\.scss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', "postcss-loader", 'sass-loader'],
include: [
path.join(__dirname, 'node_modules/xxxUI/sass/')
],
// 樣式無需進行Tree Shaking
sideEffects: true
}
// ...
optimization: {
usedExports: true,
minimizer: [
new TerserPlugin({})
]
}
// .babelrc
"presets": [
[
"@babel/preset-env",
{
// 想達到Tree Shaking效果這裏
"modules": false,
}
]
]
複製代碼
babel中modules的選項有
'amd' | 'umd' | 'systemjs' | 'commonjs' | false
這幾個,因爲Tree Shaking基於ES6 Modules,這裏就不能轉換成其餘標準,只能選false
,即採用本來文件的模塊標準編譯。
搞定,一個實用的組件庫就發佈完成了,快來動手試試吧。
等等,彷佛還漏了單元測試,實際上是裏面須要注意的點(keng)太多了,一次講不完,將在下一篇《Re從零開始的組件單元測試》
中詳細展開。
SluckyUI
的源碼和項目構造就是按照這套模式去搭建的,在細節方面有其餘考量,可能會有所不一樣,但思路是不變的。SluckyUI
的理念是打造一個組件庫種子,讓其餘開發者可以進行快速二次開發,減小沒必要要的造輪子,但當中的編寫還有不少尚不完善的地方,不妨點個start
支持一下。
最近終於將代碼整理好了,前期寫的實在有點不大好看。 在線組件Demo&組件庫源碼