不少時候,咱們爲了複用或者概括總結,會把組件抽離出來發到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&組件庫源碼