利用gulp打包React组件库

665 阅读4分钟

背景

我司之前的组件之间都是现在项目中,后续因为要剥离上传到NPM上,所以选择了用dumi去写组件的说明文档,利用gulp将组件库打包成esm、umd格式。

前置知识

关于ESM、UMD、CJS

  1. UMD(Universal Module Definition,通用模块定义):UMD模块是一种能够在不同环境下使用的模块。这种模块定义支持AMD、CommonJS和全局变量等不同的模块系统。一般我们在项目中都是使用〈script src="xxxx"> 来挂载。
  2. CJS(CommonJS):CJS模块是Node.js使用的模块定义方式。这种模块定义使用require函数来加载模块,并使用module.exportsexports对象来导出模块,但是需注意它的加载是同步的。
  3. ESM(ECMAScript模块):ESM模块是ECMAScript标准化的模块定义方式。这种模块定义使用import关键字来加载模块,并使用export关键字来导出模块。

简单来说,每个打包格式就是对应于不同规范下的产物,而我们现在前端工程化项目里引入的组件库一般都是ESM格式的打包产物。

关于gulp的一些基本认识

Gulp是一款基于流的自动化构建工具,它可以帮助开发者自动化地执行各种重复的任务,例如编译Sass、压缩JavaScript文件、合并CSS文件等。Gulp使用Node.js编写,并且具有高度的可配置性和灵活性,因此在前端开发中被广泛使用。

gulp使用

  1. 通过gulp.task()注册任务
  2. 通过gulp.src()方法获取到想要处理的文件地址
  3. 把文件流通过pipe方法导入到gulp的插件中
  4. 把经过插件处理后的流在通过pipe方法导入到gulp.dest()中
  5. gulp.dest()方法则把流中的内容写入到文件中

遇到的问题

因为之前组件是写在项目中,所以后续打包遇到了很多问题,包括但不仅限于以下几点

  1. 组件开发的时候文件组织格式与命名没有统一的规范,导致每个组件各有特点
  2. less文件存在循环引入
  3. 部分组件中还有png图片,图片体积还不小
  4. less 文件中别名的处理 and so on

利用gulp处理组件库中的less文件

项目中的less文件在import的时候使用了路径别名,所以在编译less文件的时候需要首先将路径别名替换成相对路径,此处用到的是gulp-style-aliases插件。

    funtcion compile() {
            // 清理之前打包的产物
            rimraf.sync(esDir);
            return gulp
                    // 需要处理的文件
                   .src(['src/**/style/index.less', 'src/**/style/styles.module.less', 'src/**/style/style.module.less'])
                   .pipe(
                      // 替换less中的路径别名
                     aliases({
                       '~@components': './src',
                     }),
                   )
                   .pipe(
                     through2.obj(function (file, encoding, next) {
                        if (file.isNull()) {
                          return next(null, file);
                        }
                        if (file.isStream()) {
                          return next(new Error('Streaming not supported'));
                        }
                        const cloneFile = file.clone(); // 复制一份打包后可以保留less源文件
                        const content = file.contents.toString().replace(/^\uFEFF/, '');
                        cloneFile.contents = Buffer.from(content);
                        const cloneCssFile = cloneFile.clone();
                        this.push(cloneFile);
                        if (file.path.match(/(.*?)\.less$/) || file.path.match(/(.*?)\.module\.less$/)) {
                          // 在transformLess方法里面处理less
                          transformLess(cloneCssFile.contents.toString(), cloneCssFile.path)
                            .then((css) => {
                              cloneCssFile.contents = Buffer.from(css);
                              // 处理完后的新less文件 修改文件后缀
                              cloneCssFile.path = file.path.match(/(.*?)\.module\.less$/)
                                ? cloneCssFile.path.replace(/\.module\.less$/, '.css')
                                : cloneCssFile.path.replace(/\.less$/, '.css');
                              this.push(cloneCssFile);
                              next();
                            })
                            .catch((e) => {
                              console.error(e);
                            });
                        } else {
                          next();
                        }
      }),
                   )
                   .pipe(gulp.dest(esDir));
        }
   

transformLess文件中主要是对已经替换过路径别名的less文件进行一些其他处理,包括用post-css和autoprefixer来处理css中的前缀

function transformLess(lessContent, lessFilePath, config = {}) {
  const { cwd = process.cwd() } = config;
  const resolvedLessFile = path.resolve(cwd, lessFilePath);

  const lessOpts = {
    paths: [path.dirname(resolvedLessFile)],
    filename: resolvedLessFile,
    javascriptEnabled: true,
  };
  return less
    .render(lessContent, lessOpts)
    .then((result) =>
      postcss(lessFilePath.includes('.module.less') ? [autoprefixer, scopedNameGenerator] : [autoprefixer]).process(
        result.css,
        { from: lessFilePath },
      ),
    )
    .then((r) => {
      if (lessFilePath.includes('.module.less')) {
        const jsonFile = `${lessFilePath}.json`;
        // fs.existsSync 以同步的方法来检测是否存在目录
        if (fs.existsSync(jsonFile)) {
          fs.unlink(jsonFile, (err) => {
            if (err) {
              throw err;
            }
          });
        }
      }
      return r.css;
    });
}

module.exports = transformLess;

处理ts文件

主要使用的是gulp-typescript和gulp-path-alias插件 gulp-typescript需要传入一个配置文件

const tsconfig = {
  ...compilerOptions,
  noUnusedParameters: false, // 不能有未使用的变量
  noUnusedLocals: false, // 不能有未使用的本地变量
  strictNullChecks: true, //严格的null检查
  target: 'esnext', // 编译目标
  moduleResolution: 'node', // 模块的查找规则
  declaration: true, //是否生成声明文件
  allowSyntheticDefaultImports: true, //允许默认导入
  allowJs: true,
  preserveSymlinks: true,
  noImplicitAny: false,
  paths: {
    '@components/*': ['./src/*'],
  },
};

gulp-path-alias主要是用于解决ts中配置的路径别名问题

当然还要配置babel,以便于支持jsx和一些其他语法解析

babel({
        presets: [
          [
            '@babel/preset-env',
            {
              modules: false,
            },
          ],
          '@babel/preset-react',
        ],
        plugins: [['@babel/plugin-transform-runtime', { useESModules: true, helpers: true, regenerator: true }]],
      }),

打包umd格式

利用gulp将组件库打包成umd格式,需要基于esm产物进行二次打包,这时gulp的入口文件需要指定为整个esm产物中最外层的index.js入口文件(这个文件汇集了所有的组件,通过export分别对外导出)

这里需要使用webpack-stream插件进行处理(一个能允许你在gulp中使用webpack的插件),然后配置一些webpack参数就可以打包成umd了,需要注意的是externals参数,该参数在webpack中提供了从输出的 bundle 中排除依赖,移除第三方依赖,能够大大的减轻umd包的体积大小

    function umdWebpack() {
  rimraf.sync(umdDir);
  return gulp
    .src('./dist/es/index.js')
    .pipe(
      webpackStream(
        {
          output: {
            filename: 'components.js',
            library: {
              type: 'umd',
              name: 'Components',
            },
            path: path.resolve(__dirname, 'dist/es'),
            globalObject: 'this',
          },
          mode: 'production',
          resolve: {
            extensions: ['.js', '.json'],
            alias: {
              // 将 './lib/es' 指定为根路径
              '../screen/hooks/useVisible': path.resolve(__dirname, './dist/es/'),
            },
          },
          module: {
            rules: [
              {
                test: /\.css$/i,
                use: ['style-loader', 'css-loader'],
              },
              {
                test: /\.svg$/,
                use: [
                  {
                    loader: 'svg-inline-loader',
                    options: {
                      removeTags: ['title', 'desc'],
                      removeSVGTagAttrs: true,
                    },
                  },
                ],
              },
            ],
          },
          externals: [
            {
              react: {
                commonjs: 'react',
                commonjs2: 'react',
                amd: 'react',
                root: 'React',
              },
              'react-dom': {
                commonjs: 'react-dom',
                commonjs2: 'react-dom',
                amd: 'react-dom',
                root: 'ReactDOM',
              },
              lodash: {
                commonjs: 'lodash',
                commonjs2: 'lodash',
                amd: 'lodash',
                root: 'lodash',
              },
              antd: {
                commonjs: 'antd',
                commonjs2: 'antd',
                amd: 'antd',
                root: 'antd',
              },
            },
          ],
        },
        webpack,
      ),
    )

    .pipe(gulp.dest('./dist/umd/'));
}