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

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

理解完之后我们就可以

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

就像这样,嘿嘿。

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

项目结构

结构解析

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

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

  • 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 中交由组件库的使用者去选择。按需加载的好坏处是由具体的项目环境而定, 需要具体情况具体分析

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

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

就这样,通过巧妙的文件结构,目标 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 这个很好用的工具,能很方便匹配出对应的文件。最后返回的是一个文件路径的映射对象,我们可以在控制台看看输入了哪些文件。

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 相似。

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

我们需要这样去配置,过滤掉 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

最终发布

  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 支持一下。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章