Webpack 5.x 开发 React 组件库

Webpack 5.x 开发 React 组件库

说明

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

实现目标

  • 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 而是打包为一个整体,主要直接提供给浏览器使用(<script src='xxx.umd.js'>
  • cjs: 即 CommonJS 规范定义的模块,同样提供给 node 和 打包工具使用(旧版本的 Webpack, Gulp等不能直接导入 ES Module 的情况)

首先需要关闭掉 babel 配置中 @babel/preset-env 对于 es module 的转化功能(设置 modules: false),将转化完全交给 webpack

输出 umd 格式

webpack 提供 output.library 选项来配置输出 library

module.exports = {
  mode: process.env.NODE_ENV || 'development', // 一般编译环境为 production
  entry: entryFile,
  output: {
    path: path.resolve(__dirname, 'dist/umd/'),
    filename: 'webpack-ui.production.min.js', // 或者 webpack-ui.development.js
    globalObject: 'this', // 使得在 web node 都可用
    umdNamedDefine: true, // 给生成的 umd 模块中的 amd 部分命名
    library: {
      type: 'umd', // 指定输出 的格式,与设置 libraryTarget 一个效果
      name: 'WebpackUI', // umd 格式可以给浏览器script 直接引用,这里可以设置一个 浏览器环境的全局变量名称,浏览器环境可以直接通过 WebpackUI.Button 来使用组件
      auxiliaryComment: '这里是插入的注释',
    },
  },
 optimization: {
    minimize: true, // production 环境的 压缩是自动开启的,如果需要输出非压缩文件要给这个设置为 false
    minimizer: [
      `...`, // webpack 5 提供用来继承已存在的 minimizer
      new CssMinimizerPlugin(),
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({ filename: 'webpack-ui.production.min.css' }), // webpack-ui.development.css
  ]
};
输出 cjs 格式

一般来讲 cjs 或者 esm 的组件库输出,都会被第三方通过 npm install 来使用,所以这两种代码输出的可以是不压缩的代码,以保证一定的可读性;但是如果第三发使用也通过 webpack 作为打包工具,那么这里就会遇到问题 TypeError: __webpack_modules__[moduleId] is not a function

这个问题的原因是,一个使用 webpack 作为打包工具的第三方使用者,使用了我们通过 webpack 5.x 且没有被 terser 压缩过的 package 引起的 #11827,解决办法就是输出压缩后的代码(相应的,代码基本不可读)

module.exports = {
  mode: process.env.NODE_ENV || 'development',
  entry: {
    index: {import: entryFile, filename: 'index.js'},
    ...componentEntryFiles // 以组件为维度的入口
  },
  
  output: {
    path: path.resolve(__dirname, 'dist/cjs/'),
    filename: 'components/[name]/index.js',
    library: {
      type: 'commonjs2',
      auxiliaryComment: '这里是插入的注释kl',
    },
  },
  plugins: [
    new MiniCssExtractPlugin({ filename: 'components/[name]/style/index.css'}),
  ]
}  
输出 esm 格式

很遗憾,虽然 webpack 5.x 官网上说输出 esm library 已经支持 outputModule 但是经过试验,目前为止(webpack 5.15.0) 都不支持输出 esm 的 library, 会有如下的报错

在这里插入图片描述

经过调查 这个功能貌似还在开发中 #11827 只是预计将要在 Webpack 5.x 中实现,因此如果要实现输出 esm 类型的文件,我们不得不借助其他打包工具如 gulp rollup

根据 webpack 的 release v5.22.0 显示,esm 模块的输出正在逐步实现中

使用组件库时如何进行按需加载

与 Rollup 类似的解决方案

方案一、用户使用现代打包工具 – ES Module tree-shaking 方案

虽然会用其他打包工具实现 输出 esm , 但是使用上是一样的

用户使用现代打包工具(Webpack, Rollup),引用我们的组件库时,会查找对应 "module": "dist/es/index.js" 字段的ES模块代码,只要他的打包工具开启了 tree-shaking 功能(Webpack production 模式自动开启, Rollup 自动开启)即可实现 tree-shaking 带来的 JS 按需加载能力

CSS 方面,用户需要在它们项目的入口引入 import '@mjz-test/rollup-ui/dist/es/style/index.css'; 全量的样式。

方案二、用户使用非现代打包工具或者用户可以使用 ES Module 但同时也想要 css 方面的按需引入

用户需要使用 babel-plugin-import 作为按需加载的 babel 工具,其实现原理是将对组件的引用,重新指向所下载组件库目录下的某个文件,来实现“定向”的引用,顺便也可以将 css 也定向的引用

// 用户的 babel.config.js
module.exports = function () {
    return {
        plugins: [
            [
              'import', // 使用 babel-plugin-import
              {
                libraryName: '@mjz-test/rollup-ui',
                libraryDirectory: 'dist/cjs',
                camel2DashComponentName: false,
                style: true,
              },
              'rollup-ui',
            ]
        ]
    }
}
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值