Webpack 5.x 开发 React 组件库

Webpack 5.x 开发 React 组件库

说明

Webpack 5.x 相比于 Webpack 4.x 有了很多重大改进,有些改进对于我们使用它开发组件库有了更好的支持。

  • Webpack 5 发布 – 升级内容
  • 需要 node 版本 v10.13.0 以上
  • Webpack V5.15.0

实现目标

  • Tree-shaking 支持
  • Code-splitting 代码分割实现(组件级别的分割)
  • 对外输出模块类型 esm、umd、commonjs (当前版本 Webpack v5.15.0 没能实现输出 esm library, esm 需要通过其他构建工具实现 webpack v5.22.0 开始逐步实现输出 esm)
  • 公共依赖不打包仅组件中(external 掉),使用 peerDependencies 让使用方决定使用版本
  • 打包后 按照组件拆分
  • 样式文件抽离(css in js 除外)同样按照组件拆分 (可拆分,但拆分后不能自动引入js 模块,esm 需要全局引入样式文件,cjs 可借助 babel-plugin-import 顺便引用)
  • 不支持 Tree-shaking 的环境可使用 babel-plugin-import 实现组件的按需引入
  • 静态资源例如图片 字体文件正确引入(仅能内联引入,即打包进组件或者样式中,不可拆分,否则会有引用路径问题)
  • test 支持
  • 输出 Typescript 类型声明
  • eslint lint-stage husky prettier 集成

Webpack 5.x 重要升级功能

  • 更小的打包体积:不再为 Node.js 模块 自动引用 Polyfills;按照构建目标优化(target); 废弃代码删除。
  • 更好的 Tree-shaking 支持:ES module 的使用情况分析能力更强;针对 CommonJs 模块的一定程度 Tree-shaking 支持。
  • 更好的缓存:内置持久缓存。
  • 模块联邦:跨应用间的模块共享(微前端)
  • 可以支持生成 ES6 格式的 Library (官方文档上声明支持,但实际上截止 Webpack 5.15.0 还没有实现#2933)
  • 不再使用 eslint-loader 改用 eslint-webpack-plugin
  • 高度可定制化 entry 入口配置
  • 内置的 asset 资源处理 loader
  • 其他 api 改动

重要功能实现

1. 如何排除公共依赖,不打包进组件中

Webpack 5.x 的 externals 配置有所增强,同时 Api 有所改动

针对 npm node_modules 中的公共依赖,通过传入正则排除以第三方package 开头的package:

module.exports = {
    externals: [
        /^react\/.+$/, 
        /^react-dom\/.+$/, 
        /^lodash\/.+$/,
        /^@babel\/runtime\/.+$/
    ]
}

2. 如何处理 Typescript

与 Rollup 一样通过 babel 来处理

3. 静态资源如何处理

webpack 可以直接通过 import 在 js 中使用图片资源,不需要引入插件

无论 js 中引入的图片还是 css 中引入的图片,都是通过以下方式最终打包进代码中的(inline 即不输出单独的图片 asset 而是集成到了 js 或者 css 中)

module.exports = {
    module: {
        rules: [
            {
                test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
                type: 'asset/inline', // 使用webpack 5 内置 loader
            }
        ]
    }
}

4. cjs(esm 待定) 模块如何按照组件维度进行 code-splitting 拆分

打包时,将输出的分割包按照组件维度分割,同样是使用 “多入口文件的方式来实现”,这样打包后文件会按照入口来分割,实现组件维度的拆分

module.exports = {
    entry: {
        index: { import: 'src/index.ts', filename: 'index.js },
        Button: 'src/components/Button/index.tsx',
        Alert: 'src/components/Alert/index.tsx',
    },
    output: {
        filename: 'components/[name]/index.js',
        library: {
            type: 'commonjs2', // commonjs 是 只有 export=moduleName; commonjs2 还支持 module.exports = moduleName
        }
    }
}

Webpack 5. 对 entry 有了增强,每个入口点可以配置成一个对象,对象含有如下属性

module.exports = {
    entry: {
        index: { 
            import: 'src/index.ts',  // 入口文件地址
            filename: 'index.js, // 打包后输出文件地址,会覆盖 output.fileName 的配置
            dependOn: ['Button', 'Alert'], // 将其他 entry 块设置为 dependOn 后 当前文件如果引用了这几个块,则不会将其打包进来,可以一定程度的实现代码复用
         },
        Button: 'src/components/Button/index.tsx',
        Alert: 'src/components/Alert/index.tsx',
    },
}

理论上我们可以通过设置 entry 入口文件的 dependOn 来解决输出模块间因相互引用导致的代码重复问题,但是,dependOn 必须在编译时明确的清除,当前入口点的依赖情况,不能做到一定程度的自动化,因此要解决输出代码重复的问题还是通过 webpack 的 externals 来实现: 我们可以把对内部组件间的引用都理解为不需要打包的外部模块,将其排除打包

module.exports = {
    externals: [
        /^react\/.+$/, 
        /^react-dom\/.+$/, 
        /^lodash\/.+$/,
        /^@babel\/runtime\/.+$/,
        ({context, request}, callback) => {
            // 避免 Button 重复打包
            path.resolve(context, request) === path.join(__dirname, `src/components/Button`)
            ? callback(null, request) : callback();
        }
    ]
}
# 输出后文件格式 dist/cjs/
.
├── components
│   ├── Alert
│   │   ├── index.js
│   │   └── style
│   │       └── index.css
│   ├── Button
│   │   ├── index.js
│   │   └── style
│   │       └── index.css
└── index.js

5. 样式文件如何输出及样式文件如何按照组件维度进行 code-splitting 拆分

Webpack 上样式文件抽离是通过 mini-css-extract-plugin 这个插件来实现的

module.exports = {
    module: {
        rules: [
            {
                test: /\.less$/,
                use: [
                  { loader: MiniCssExtractPlugin.loader },
                  {
                    loader: 'css-loader',
                    options: { sourceMap: true },
                  },
                  {
                    loader: 'postcss-loader',
                    options: { postcssOptions: { plugins: [autoprefixer({ env: BABEL_ENV })] } },
                  },
                  { loader: 'less-loader', options: { lessOptions: { javascriptEnabled: true } } },
                ],
            }
        ]
    },
      plugins: [
        new MiniCssExtractPlugin({ filename: 'components/[name]/style/index.css'}),
      ]
}

理论上每个 entry 入口点都会有一个对应的 样式文件,除非这个入口点引入的模块中没有样式。输出指定目录结构的 样式文件很有必要,因为针对 cjs 组件库来讲,使用者要想实现按需加载一般会使用 babel-plugin-import 这个babel 插件会将对组件的引用,重新指向某个文件,同时引用其下的样式文件,以上配置输出的 组件中可能没有样式文件,这就需要我们后续通过其他方式生成了(例如 build 后通过脚本补齐一个空的样式文件)

6. 对外输出 esm umd commonjs 规范的模块

  • esm: 就是 ES Module 的模块(import export)主要提供给现代的打包工具(Webpack, Rollup)(npm 引入)使用,现代的打包工具会识别 package.json 中的 module 字段,如果包含这个字段,则会优先加载使用这个字段所对应的 ES Module, 在结合组件库的 sideEffect 配置可以实现 tree-shaking , 从而实现代码体积优化
  • umd: 是一个通用模块定义,结合amd cjs iife 为一体,其打包后不会按照组件 code-splitting 而是打包为一个整体,主要直接提供给浏览器使用(

你可能感兴趣的:(前端构建工具,webpack,组件库,react,library,webpack,5)