不少时候,咱们为了复用或者概括总结,会把组件抽离出来发到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&组件库源码