🤔从开源角度去思考,如何优化一个组件库

承接上文,我们用 rollup 搭建了一个组件库 rollup-build,但还有些需要优化的地方:

  1. 构建速度不够快
  2. 构建的产物只有 esm 模块,没有 umd 模块,iife 模块

本篇文章内容是基于上篇文章的内容,所以了解上篇文章的内容会对阅读更有帮助

传送门:🎉纯干货不废话,用rollup搭建组件库,再用vite搭建测试环境 - 掘金

构建产物准备了三种模块,每种模块都有它的作用:

  1. esm 用来支持 esmodule 环境的, 在大多数的开发环境,都是 esmodule,并且 esm 模式支持 tree shaking。所以 esm 是必要的
  2. umd 模块,支持三种模式的引入,一种是 commonjs,一种是 script,还有一种是 amd; 对于 commonjs 在一些 node 环境中是必要的。script 的场景就是在消费端将rollup-build external 的时候,就需要用 script 导入了。amd 的场景我还没遇到过

对于 commonjs,我还没有遇到要用 commojs 来开发前端页面的,但是很多库都支持,咱也不能落后不是。 可能一些构建工具在引入库的时候就是 commonjs? 不过对于 script,我倒认为需求场景比较多

  1. iife 模块,iife 模块也是 script 导入使用,但相较于 umd,我更倾向于认为 iife是适配低版本浏览器,只支持 es5,甚至更低的那种。在 script 引入组件库rollup-build的依赖(ps: react, reactd-dom),然后引入组件库rollup-build,就可以直接使用。要做到这一点,就需要在 iife 的构建产物中包含所有的 es6 语法的 polyfill。

下面开始

准备

因为需要用到三种模块的构建,所以为了项目结构清晰,需要将不同模块的构建配置文件分开。

创建一个 config 文件夹,并且在里面创建三个 rollup 配置文件

由于不同配置文件的插件配置是高度重合的,所以单独列出一个getPlugins.js文件处理插件,减少代码的重复性

getPlugin.js

javascript 复制代码
// getPlugins.js

import commonjs from "@rollup/plugin-commonjs";
import image from "@rollup/plugin-image";
import resolve from "@rollup/plugin-node-resolve";
import nodeExternals from "rollup-plugin-node-externals";
import postcss from "rollup-plugin-postcss";

const basePlugins = [resolve(), commonjs(), postcss({ extract: "rollup-build.css" }), image()];

首先准备一个基础的 basePlugins,里面有各个模块都需要的基础功能,

  • resolve() , 解析第三方依赖
  • commonjs(), 解析 commonjs 模块
  • postcss({ extract: "rollup-build.css" }),解析各种 css 文件格式,其中的参数表示将组件库的 css 样式都提取出来放到一个rollup-build.css文件中
  • image(),解析各种图片格式,将图片转换为 base64 格式

准备 esm 的插件

javascript 复制代码
// getPlugins.js

import esbuild from "rollup-plugin-esbuild";
import nodeExternals from "rollup-plugin-node-externals";

export const esmPlugins = [...basePlugins, nodeExternals(), esbuild()];

构建 esm 模块,编译插件需要处理 ts 和 react 代码,上篇文章中是用rollup-plugin-typescript处理 ts 代码,用rollup-plugin-babel处理 react 代码,输出的最后结果是 es6 代码。

这份工作完全可以交给 esbuild 来做,esbuild 可以同时处理 ts 和 react 代码,而且速度还一个数量级

在代码编译, esbuild 相较于 webpack,babel 会快很多

nodeExternals 是用来排除所有的所有的第三方依赖的,这样组件库的构建产物体积就会足够的小

准备 umd 模块

javascript 复制代码
// getPlugins.js
export const umdPlugins = [
	...basePlugins,
	nodeExternals(),
	esbuild({
			tsconfigRaw: {
			compilerOptions: {
				jsx: "react",
			},
		},
	}),
];

umd 模块相比于 esm 模块,有个不同的地方--esbuild 的参数。这个参数的作用是使构建之后的 react 组件生成 reactElement 函数是 React.createElement,而不是 jsx/runtime中的函数。

这是 umd 构建产物 -- Input 组件 的截图

这样做的目的是用 script 引入的时候,可以少引入一个jsx/runtime的包

使用了这种设置,是不是需要在组件中显示引入 React变量呢?答案是:不需要

准备 iife 模块

javascript 复制代码
// getPlugins.js
import { babel } from "@rollup/plugin-babel";
import { DEFAULT_EXTENSIONS } from "@babel/core";

export const iifePlugins = [
    ...basePlugins,
    esbuild({
            tsconfigRaw: {
                    compilerOptions: {
                            jsx: "react",
                    },
            },
    }),
    // 处理低版本浏览器的polyfill
    babel({
    presets: [
            [
                "@babel/preset-env",
                {
                        targets: "> 0.25%, not dead, IE 10",
                },
            ],
    ],
    exclude: /node_modules/,
    extensions: [...DEFAULT_EXTENSIONS, ".ts", ".tsx"],
    babelHelpers: "inline", //将所有的polyfill代码打包进bundle中
    }),
];

准备 iife 模块时,不需要将所有的第三方依赖都 external 掉,需要 external 的依赖会在 rollup.external 配置中单独列出来,这也是为了减少 script 引入产物的考量。所以这里拿掉了nodeExternals()插件

因为 esbuild 并不能很好支持 es5 代码,所以,还单独加了一个 babel 插件,目的是将 es6 代码转译成 es5。其中的一些配置需要我们注意

  • 一个 targets,表示我们需要支持的浏览器数量,
  • 还有一个是 extensions,用来告诉 bable 插件,你需要它处理哪些文件。它默认是没有 ts、tsx 后缀的,所以我们额外添加。(这是坑,需要特别注意)
  • babelHelpers 设置成 inline, 目的将 polyfill 代码都打包进构建产物中,而不是向 runtime 一样引入其中。虽然会导致构建产物增加,不过这是没有办法的事情

其实要实现这个目的,babelHelpers: bundle其实是个更好的选择,但我用了这个值,构建就会报错,这是为什么:

他们的区别在于 inline 会在构建产物每个文件都放一份 polyfill,而 bundle 对构建产物的所有文件都只有一个 polyfill。所以inline 模式会比 bundle 的构建产物体积更大。

不过,其实对我也没有影响,因为我构建产物只有一个文件呀😄

到此,getPlugins.js文件也就准备好了:

javascript 复制代码
// getPlugins.js

import { DEFAULT_EXTENSIONS } from "@babel/core";
import { babel } from "@rollup/plugin-babel";
import commonjs from "@rollup/plugin-commonjs";
import image from "@rollup/plugin-image";
import resolve from "@rollup/plugin-node-resolve";
import esbuild from "rollup-plugin-esbuild";
import nodeExternals from "rollup-plugin-node-externals";
import postcss from "rollup-plugin-postcss";
const basePlugins = [resolve(), commonjs(), postcss({ extract: "rollup-build.css" }), image()];

export const esmPlugins = [...basePlugins, nodeExternals(), esbuild()];


export const umdPlugins = [
    ...basePlugins,
    nodeExternals(),
    esbuild({
        tsconfigRaw: {
            compilerOptions: {
                    jsx: "react",
            },
        },
    })
];

// script的依赖过多是不友好的
export const iifePlugins = [
    ...basePlugins,
    esbuild({
        tsconfigRaw: {
            compilerOptions: {
                    jsx: "react",
            },
        },
    }),
    // 处理低版本浏览器的polyfill
    babel({
        presets: [
            [
                "@babel/preset-env",
                {
                    targets: "> 0.25%, not dead, IE 10",
                },
            ],
        ],
        exclude: /node_modules/,
        extensions: [...DEFAULT_EXTENSIONS, ".ts", ".tsx"],
        babelHelpers: "inline", //将所有的polyfill代码打包进bundle中
}),
];

rollup.config.es.mjs

这是 esm 模块的配置文件,内容很简单:

javascript 复制代码
// rollup.config.es.mjs
import { esmPlugins } from "./getPlugins.js";

/**@type {import('rollup').RollupOptions} */
export default {
	input: "./src/index.tsx",
	output: {
		dir: "./dist/es",
		format: "esm",
		sourcemap: true,
		preserveModules: true,
	},
	plugins: esmPlugins,
};

入口时 src 中的 index.tsx, 输出目录是dist文件夹下面的es文件夹。格式是 esm,生成 sourcemap,preserveModules: true的意思是编译不打包,构建产物仍然是按照开发的目录结构

rollup.config.umd.mjs

这是 umd 模块的配置文件:

javascript 复制代码
// rollup.config.umd.mjs

import { umdPlugins } from "./getPlugins.js";

/**@type {import('rollup').RollupOptions} */
export default {
	input: "./src/index.tsx",
	output: {
		file: "./dist/umd/rollup-build.js",
		format: "umd",
		name: "RB",
		sourcemap: true,
		globals: {
			react: "React",
			"react-dom": "ReactDOM",
		},
	},
	plugins: umdPlugins,
};

入口和 esm 模块一样,出口是 dist 文件夹下面的 umd 文件夹。umd 还需要给出 script 场景下的全局变量名RB(取的是 rollup-build 首字母)

其中 globals 的配置目的是告诉 rollup,组件库中引入的 react,react-dom,在 script 场景中,它们的全局变量名是什么。这个变量名不能是自定义的,要看具体的 react umd 格式文件中是什么

那 react 举个例子:

先找到 ract 中 umd 格式的文件

红圈圈出来的位置,就是浏览器环境下的定义,给 global 上赋值了一个 React 属性,所以 react 库在 script 环境下的全局变量名称,就是 React

红圈上面两行,分别是 commonjs 环境,和 AMD 环境,这就是为什么 umd 可以支持三种环境的原因所在了

每 external 一个库,就要在 globals 中专门列出来一个,万一依赖库有十多个,那就太麻烦了。有没有一个库是专门维护这样一个映射关系的?

rollup.config.iife.mjs

这是 iife 模块的配置文件:

javascript 复制代码
// rollup.config.iife.mjs

import { iifePlugins } from "./getPlugins.js";

/**@type {import('rollup').RollupOptions} */
export default {
	input: "./src/index.tsx",
	output: {
		file: "./dist/iife/rollup-build.js",
		format: "iife",
		name: "RB",
		sourcemap: true,
		globals: {
			react: "React",
			"react-dom": "ReactDOM",
		},
	},
	plugins: iifePlugins,
	external: ["react", "react-dom"],
};

相同的地方就不讲了,重点关注和其他模块配置不同的地方

  • format 要改成 iife
  • 输出路径,是在dist/iife目录下面
  • 要额外地写出要 external 哪些库。原因之前提到过,减少 script 场景下需要额外引入的依赖,所以只将消费端大概率也会 external 的库,external 出去

到此,所有模块的配置文件都准备好了,下面准备 package.json

package.json

json 复制代码
{
  "main": "./dist/umd/rollup-build.js",
  "types": "./dist/types",
  "module": "./dist/es/index.js",
}

这个配置是面向消费端的,配置了不同模块的入口。如果 commonjs 模块引入,node 就会返回 umd 下面的文件,如果是 esmodule 模块引入,node 就会返回 es 下面的文件。

下面来添加构建的 npm 脚本

json 复制代码
{
  "scripts": {
    "prebuild": "rimraf dist && tsc",
    "build": "npm run build:es & npm run build:umd & npm run build:iife",
    "build:es": "rollup -c ./config/rollup.config.es.mjs",
    "build:umd": "rollup -c ./config/rollup.config.umd.mjs",
    "build:iife": "rollup -c ./config/rollup.config.iife.mjs",
  },
}

当执行npm run build,就会同时执行三种模块的构建,

&&&不相同,&&是串行执行,用来连接两个命令,如果前面的命令执行失败,就会阻塞后面的命令。&是并行执行,也是连接两个命令,但前后命令不会相互影响

不同模块的构建并没有前后关系,所以这里用并行执行

其中,还有个 prebuild 的命令,其会在npm run build之前执行。prebuild 内容中,首先是删除 dist 目录,然后执行 tsc,用来生成 ts 的类型文件,还兼有类型检查的功能。如果 ts 类型报错,就会停止构建功能。

当然,报错停止构建后,类型文件也没必要输出了,可以在 tsconfig 中加上"noEmitOnError": true,就可以优化的实现这一点了。

构建脚本也准备好了,下面来执行下看看吧!

两个细节:

  1. 可以看到 log 是交替出现了,印证了构建过程是并行执行的
  2. iife 模块构建的时间是最长的,因为用到了 babel 对 es 代码降级

对比下 umd 和 iife 构建后的组件代码:

可以看到 iife 中函数被转换为了 function 形式,不是箭头函数

配置文件的再优化

rollup 支持导出数组类型的配置文件,也就是说可以同时对不同入口,不同模块进行构建

可以这么做:

在 config 文件夹下面创建一个 rollup.config.js 文件:

js 复制代码
//rollup.config.js

import rollupConfigIife from "./rollup.config.iife.mjs";
import rollupConfigUmd from "./rollup.config.umd.mjs";
import rollupConfigEs from "./rollup.config.es.mjs";

export default [rollupConfigEs, rollupConfigUmd, rollupConfigIife];

文件中将不同模块的配置文件放在一数组中重新导出

package.json 中可以修改 npm run build的脚本内容:

json 复制代码
{
  "scripts": {
    "build": "rollup -c ./config/rollup-config.js",
  },
}

其他的构建脚本可以不删除,类似与npm run build:esnpm run build:umd,如果有特别需求,可以单独对某个模块进行构建

来执行一下:

可以看到, umd 构建时间显著小于 esm,也许是重用了 esm 模块的构建中间产物吧🤔

总结

这篇文章分享如何对 rollup 构建的组件库进行优化,优化主要两个方向,一个构建速度,构建速度用 esbuild 替代 bable;还有一个是不同模块的构建产物,构建产物分成了三种,分别是 esm, umd, iife。不同模块有其不同的特点,用来适应不同的使用场景

构建产物是准备好了,那怎么对产物进行测试呢?下篇文章来讲吧

rollup 构建相关的另外两篇文章:

  1. 🎉纯干货不废话,用rollup搭建组件库,再用vite搭建测试环境
  2. 👊承接上文,用vite搭建rollup组件库的开发环境
相关推荐
魏大帅。2 分钟前
Axios 的 responseType 属性详解及 Blob 与 ArrayBuffer 解析
前端·javascript·ajax
花花鱼9 分钟前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k093312 分钟前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
EricWang135833 分钟前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端
September_ning34 分钟前
React.lazy() 懒加载
前端·react.js·前端框架
时差95337 分钟前
【面试题】Hive 查询:如何查找用户连续三天登录的记录
大数据·数据库·hive·sql·面试·database
web行路人43 分钟前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
超雄代码狂1 小时前
ajax关于axios库的运用小案例
前端·javascript·ajax
长弓三石1 小时前
鸿蒙网络编程系列44-仓颉版HttpRequest上传文件示例
前端·网络·华为·harmonyos·鸿蒙
小马哥编程1 小时前
【前端基础】CSS基础
前端·css