Re从零开始的组件库构建与发布流程

4,148 阅读8分钟

很多时候,我们为了复用或者归纳总结,会把组件抽离出来发到npm上。但是这个过程你会发现一个问题,就是应该怎么更好的发布管理维护这些组件呢。最后会发现网上的其他教程不是太零散了,就是有些细节不大到位。这里借这个机会好好总结一下,要是看完觉得有帮助的话,不妨个,关注一下哈哈。

理解完之后我们就可以

  1. 编写发布一个可用的组件库
  2. 能以import { demoComponent } from 'xxxUI'方式引入
  3. 也能以import demoComponent from xxxUI/component/demoComponent方式引入
  4. 各个组件打包相对独立,互不干扰
  5. 输出的组件能够简单易用和具有良好的兼容性
  6. 组件库能根据用户的配置实现按需加载
  7. 组件库能根据用户的配置实现Tree Shaking
  8. 组件通过单元测试
  9. 打包发布到npm

就像这样,嘿嘿。

image

以下都以react组件库为例,其实vue是也一样的,只是babel配置有所区别

项目结构

结构解析

先来看看组件库的项目结构

image

嗯,看起来很是复杂,第一印象应该都在想着都是些什么乱七八糟的文件,下面先来解释一下。

  • 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'这种方式去引用组件了。

// 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这种形式单独引入组件了。

组件库按需加载

根据以上目录结构和引入方式我们可以知道,通过import { demoComponent } from 'xxxUI'这种形式去引入会使得整个组件库都引入到开发项目中,有时只需要用到其中的两三个组件,这种情况是我们不想看到的。而通过import demoComponent from xxxUI/component/demoComponent这种形式去引用,就能做到只引入某个需要用到的组件,这刚好能解决这个问题。但是每次引入都要写这么长的一串,很不方便。这个时候就需要用到babel-plugin-import这个插件了。

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中配置需要转换的路径

// .babelrc
{
    ...
    "plugins":[
        "import", {
            "libraryName": "xxxUI",
            "libraryDirectory": "component",
        }
    ]
}

需要注意的是,这里需要组件库的使用者去配置,而不是写在组件库的.babelrc中。如果组件库支持按需加载,这个配置应该写在README.md中交由组件库的使用者去选择。按需加载的好坏处是由具体的项目环境而定,需要具体情况具体分析

image
没设置按需加载时,整个组件库都打包进去了。

image
设置了按需加载,只加载用到的组件。

就这样,通过巧妙的文件结构,目标2,3,6已达成。

输入

明确了项目结构,接下来就是需要收集组件源码了。通常来讲,只需要在webpackentry配置中只需要设置入口文件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这个很好用的工具,能很方便匹配出对应的文件。最后返回的是一个文件路径的映射对象,我们可以在控制台看看输入了哪些文件。

image

ok,接下来就是要怎样处理这些源文件了。

编译处理和组件库Tree Shaking

这里的处理过程很简单,逻辑就是配置babeles6+的源码处理成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相似。

image

再来看看output的配置,由于我们在文件输入时保持了文件路径信息,所以这里直接更改后缀之后输出到dist即可。libraryTarget的作用在于设置打包格式,这里采用umd标准。如果设置了library,那么将会导出成单入口的引用形式import xxxUI from 'xxxUI',这是我们不希望的。librarylibraryTarget的取值根据项目类型的不同而不同。详情看这里

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也跟着打包进来了,这会导致打包之后组件库的体积很大。

image

我们需要这样去配置,过滤掉import进来的第三方包

externals: [
    function(context, request, callback) {
        // 允许编译以下后缀文件
        if (/.jsx|.jpg|.png|.gif|.svg|.jpeg$/g.test(request)) {
            return callback();
        }
        callback(null, request);
    }
]

image

可以看到变化巨大!现在整个包大小只有120kb(除去样式)

由于样式是独立抽离出来的,只需要将样式copy到dist目录即可,当然可配置插件自动完成。

new CopyPlugin([{
    from: './sass',
    to: './sass'
}])

达成目标的4,5

最终发布

  1. 先去官网完成注册
  2. npm login登录
  3. 添加.npmignore文件,将需要忽略的文件列出来
  4. 添加README.md,写出必要的说明,这是一个好习惯
  5. package.jsonscript中添加命令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&组件库源码

最近终于将代码整理好了,前期写的实在有点不大好看。 在线组件Demo&组件库源码

组件库中组件怎么写呢?

web安全系列