组件库开发系列 - webpack 和 rollup 打包 (带视频和源码)

1,203 阅读2分钟

源代码是无法直接在浏览器端运行的,所以需要打包工具转换代码到es5。当然我们需要熟悉babel, 熟悉前端的打包工具 webpackrollupgulp,其中webpack偏向于应用开发,rollup偏向库开发。

需求:

  • 1、ES6转ES5,支持JSX

  • 2、生成ESM和UMD规范文件,UMD区分压缩和未压缩版

  • 3、支持SASS预编译样式

github webpack打包地址 项目地址

B站 webpack打包视频地址

github rollup打包地址 项目地址

B站 roolup打包视频地址

使用 webpack 打包 UMD 和 ESM

我们还行先用webpack的方式来编译组件库,从中可以看到一些困难,再换成rollup的方式,对比下看到rollup推荐更简单

npm i webpack webpack-cli terser-webpack-plugin clean-webpack-plugin copy-webpack-plugin -D

UMD 很容易生成

webpack.config.js

const TerserPlugin = require("terser-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");

module.exports = {
  entry: {
    myUI: "./src/index.js",
    "myUI.min": "./src/index.js",
  },
  output: {
    // 导出的文件名
    filename: "[name].js",
    path: __dirname + "/dist",
    // umd selft is not defined 报错
    globalObject:'this',
    library: {
      // 指定库的全局变量名称
      name: "myUI",
      type: "umd",
      export: "default",
    }
  },
  mode: "none",
  optimization: {
    // 默认mode:'development'会压缩文件,mode:'none'就不会压缩了,使用TerserPlugin只对min.js压缩
    minimize: true,
    minimizer: [
      new TerserPlugin({
        include: /\.min\.js$/,
      }),
    ],
  },
  plugins: [
    // 清空 dist 目录
    new CleanWebpackPlugin(),
    // 复制 umd main.js 文件,to 会根据output定位
    // new CopyPlugin({
    //   patterns: [{ from: "./main.js", to: "./main.js" }],
    // }),
  ],
};

ESM 很麻烦生成

webpack还不支持esm模式打包,该特性仍然是实验性的,并且没有完全支持,确保事先启用 [experiments.outputModule]

module.exports = {
  // …
  experiments: {
    outputModule: true,
  },
  output: {
    library: {
      // do not specify a `name` here
      type: 'module',
    },
  },
};

所以操作方式改为:

每一个组件都作为入口文件导出'umd'模式,都打包成单独的文件

const TerserPlugin = require("terser-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const glob = require("glob");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const path = require("path");
const fs = require("fs");

const browserslistrc = (() => {
  const content = fs.readFileSync(
    path.resolve(__dirname, ".browserslistrc"),
    "UTF-8"
  );
  return content.split("\n").map((m) => m.trim());
})();

// 获得所有组件 {"button":"./src/components/button/index.js"}
const componentsObject = glob
  .sync(`src/components/**/index.js`, {
    dot: true,
  })
  .map((x) => path.resolve(x))
  .map((x) => path.dirname(x).split(path.sep).pop())
  .reduce((p, name) => {
    p[name] = `./src/components/${name}/index.js`;
    return p;
  }, {});

module.exports = {
  entry: {
    myUI: "./src/index.js",
    "myUI.min": "./src/index.js",
    ...componentsObject,
  },
  output: {
    // 导出的文件名
    filename: "[name].js",
    path: __dirname + "/dist",
    // umd self is not defined 报错
    globalObject: "this",
    library: {
      // 指定库的全局变量名称
      name: "[name]",
      type: "umd",
      export: "default",
    },
  },
  mode: "none",
  optimization: {
    // 默认mode:'development'会压缩文件,mode:'none'就不会压缩了,使用TerserPlugin只对min.js压缩
    minimize: true,
    minimizer: [
      new TerserPlugin({
        include: /\.min\.js$/,
      }),
    ],
  },
  plugins: [
    // 清空 dist 目录
    new CleanWebpackPlugin(),
    // 拆分 css 到独立文件
    new MiniCssExtractPlugin({
      filename: "theme-chalk/[name].css",
    }),
    // 复制 umd main.js 文件,to 会根据output定位
    // new CopyPlugin({
    //   patterns: [{ from: "./main.js", to: "./main.js" }],
    // }),
  ],
  resolve: {
    extensions: [".js", ".ts", ".jsx", ".tsx"],
  },
  module: {
    rules: [
      {
        test: /\.(woff|woff2|eot|ttf)$/,
        type: "asset",
        generator: {
          filename: "fonts/[name].[contenthash:8][ext]",
        },
      },
      {
        test: /\.(png|jpe?g|gif|svg)/i,
        type: "asset", // asset  asset/inline  asset/resource
        parser: {
          dataUrlCondition: {
            maxSize: 10 * 1024, // 10K
          },
        },
      },
      {
        test: /\.(jsx?|tsx?)$/i,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              [
                "@babel/preset-env",
                {
                  useBuiltIns: "usage",
                  corejs: "3",
                  targets: { browsers: browserslistrc },
                },
              ],
              [
                "@babel/preset-react",
                {
                  runtime: "automatic", // classic automatic
                },
              ],
              "@babel/preset-typescript",
            ],
          },
        },
      },
      {
        test: /\.(css|sass|scss)/i,
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader",
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: [
                  [
                    "autoprefixer",
                    {
                      overrideBrowserslist: browserslistrc,
                    },
                  ],
                ],
              },
            },
          },
          "sass-loader",
        ],
      },
    ],
  },
};

方式一:通过文件路径进行引用,单独去引入每个需要的组件完全路径

import 'echarts/lib/chart/pie'
import 'echarts/lib/component/title' ,

方式二:使用 babel-plugin-component 引入组件

使用 babel-plugin-component 就能像下面一样引用组件了,需要在babel中增加配置

import { Button, Select } from 'element-ui'
Vue.use(Button)
Vue.use(Select)

babel-plugin-component插件将:

import { Button } from 'myLib'

转换成:

var button = require('myLib/lib/button')
require('myLib/lib/theme-chalk/button.css')
  • lib是默认查找的文件夹,所以我们也把组件js代码生成到lib文件夹下

  • 组件的css分离到单独的文件,这里我们用mini-css-extract-plugin插件,放到 lib/theme-chalk 目录下

修改 babel.config.js 配置:

module.exports = {
  presets: ["@vue/cli-plugin-babel/preset"],
  plugins: [
    [
      "component",
      {
        libraryName: "element-ui",
        styleLibraryName: "theme-chalk",
      },
      "element-ui",
    ],
    [
      "component",
      {
        libraryName: "myui",
        /**
         * styleLibraryName: 'theme-chalk' 等价于下面,不过会要求有base.css
         */
        styleLibrary: {
          name: "theme-chalk", // same with styleLibraryName
          base: false, // if theme package has a base.css
        },
      },
    ],
  ],
};

报错解决

  • Uncaught TypeError: (0 , vue__WEBPACK_IMPORTED_MODULE_0__.pushScopeId) is not a function

       新建vue3项目测试正常,vue2项目会这样报错,可能是一些webpack版本问题
    
  • JSX 编写组件,Invalid VNode type: Symbol(Text)

       本地用 npm link 项目调试报错,发布到npmjs上安装就正常了
    

使用 Rollup 打包

和webpack相比,rollup更加的小巧简介,它更加适用于构建各种类库,要比webpack方便很多,按需加载组件的时候也不需要借助插件,不需要像上面webpack还需要bable-plugin-component,在类库打包方面是挺优秀的

大家如果先看了 rollup从入门到打包一个按需加载的组件库前端工程化(一)从零开始搭建组件库这篇文章,最终会出现两个问题:

    - esm 规范 打包在一个文件里面后,新项目像 `import { button } from 'myui';` 依然没有摇树

    - css文件全部打包进一个文件了,css还是得要全量引入
    

所以还是得 umd和每个组件分开打包,然后web项目依然通过 babel-plugin-component 按需引入

装包

npm i rollup rollup-plugin-babel @babel/core @babel/preset-env rollup-plugin-commonjs rollup-plugin-postcss autoprefixer@8.0.0 rollup-plugin-vue@6.0.0 @vue/compiler-sfc cssnano rollup-plugin-terser sass rollup-plugin-delete @vue/babel-preset-app glob -D

vue2和vue3项目所用的rollup-plugin-vue版本不一样,vue的编译器也不一样

  • vue2:rollup-plugin-vue^5.1.9 + vue-template-compiler
  • vue3:rollup-plugin-vue^6.0.0 + @vue/compiler-sfc

Rollup 和 webpack 打包差不多

  • 区分 umd 和 esm 文件,esm文件还是按照组件文件多个入口

rollup.config.js

import babel from "rollup-plugin-babel";
import commonjs from "rollup-plugin-commonjs";
import postcss from "rollup-plugin-postcss";
import autoprefixer from "autoprefixer";
import cssnano from "cssnano";
import vue from "rollup-plugin-vue";
import { terser } from "rollup-plugin-terser";
import del from "rollup-plugin-delete";
const glob = require("glob");
const path = require("path");

// 获得所有组件 {"button":"./src/components/button/index.js"}
const componentsObject = glob
  .sync(`src/components/**/index.js`, {
    dot: true,
  })
  .map((x) => path.resolve(x))
  .map((x) => path.dirname(x).split(path.sep).pop())
  .reduce((p, name) => {
    p[name] = `./src/components/${name}/index.js`;
    return p;
  }, {});

const configFn = (name) => ({
  plugins: [
    vue(),
    babel({
      exclude: "node_modules/**",
      runtimeHelpers: true,
    }),
    commonjs(),
    postcss({
      plugins: [autoprefixer(), cssnano()],
      extract: `theme-chalk/${name}.css`,
    }),
    terser(),
  ],
  external: [
    //外部库, 使用'umd'文件时需要先引入这个外部库
    "vue",
  ],
});

const comConfigs = Object.keys(componentsObject).map((name) => {
  const config = configFn(name);
  config.input = [componentsObject[name]];
  config.output = {
    file: "./lib/" + name + ".js",
    format: "es",
  };
  return config;
});

const umdConfig = {
  input: "./src/index.js",
  output: [
    {
      file: "./dist/my-lib-umd.js",
      format: "umd",
      name: "myLib",
    },
    {
      file: "./dist/my-lib-es.js",
      format: "es",
    },
    {
      file: "./dist/my-lib-cjs.js",
      format: "cjs",
    },
  ],
  ...configFn("index"),
};
umdConfig.plugins.unshift(del({ targets: ["lib/*", "dist/*"] }));

export default [umdConfig, ...comConfigs];

package.json

{
  "name": "tryuirollup",
  "version": "1.0.12",
  "description": "",
  "main": "dist/my-lib-cjs.js",
  "module": "dist/my-lib-es.js",
  "scripts": {
    "build": "rollup -c"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.18.10",
    "@babel/preset-env": "^7.18.10",
    "@vue/babel-preset-app": "^5.0.8",
    "@vue/compiler-sfc": "^3.2.37",
    "autoprefixer": "^8.0.0",
    "cssnano": "^5.1.12",
    "rollup": "^2.77.2",
    "rollup-plugin-babel": "^4.4.0",
    "rollup-plugin-commonjs": "^10.1.0",
    "rollup-plugin-delete": "^2.0.0",
    "rollup-plugin-postcss": "^4.0.2",
    "rollup-plugin-terser": "^7.0.2",
    "rollup-plugin-vue": "^6.0.0",
    "sass": "^1.54.4"
  },
  "dependencies": {
    "glob": "^8.0.3"
  }
}