Rollup 和 Gulp 是 Element Plus 打包过程中的两个重要工具。Rollup 负责模块化打包,而 Gulp 则负责任务调度和自动化。本篇文章将详细的讲述主文件 index.ts 打包的具体流程,并让你了解 Rollup 和 Gulp 在打包过程的具体使用。本篇篇幅比较长,但是搞懂了这个打包的具体流程就可以读懂整个 Element Plus 的打包流程了,其他的组件库也可以举一反三。
本篇文章你可以学到:
首先这本篇中不会出现两个打包工具所有详细用法介绍,本篇主要会阐述组件打包过程中 Rollup 和 Gulp 出现的用法。
Rollup 是一个现代 JavaScript 模块打包工具,特别适合用于构建库和工具。它的最大特点是支持 Tree Shaking,即删除未使用的代码,从而生成更小的文件包。
在项目中使用 Rollup 的第一步是创建一个 rollup.config.js 文件,这是 Rollup 的配置文件,用于定义入口、输出以及所需的插件。
最简使用
// rollup.config.js
export default {
input: 'src/index.js', // 入口文件
output: {
file: 'dist/bundle.js', // 输出文件
format: 'cjs', // 输出格式为 CommonJS
},
};
在这个例子中,input 指定了入口文件 src/index.js,output 定义了打包后的文件存放路径和格式(cjs 表示 CommonJS 模块格式)。
运行 Rollup 命令:
npx rollup -c
这将根据 rollup.config.js 的配置打包文件。
Rollup 本身只负责打包模块,它无法处理诸如 ES6+ 代码转换、CSS 文件等非 JavaScript 文件。为了解决这些问题,我们需要使用 Rollup 插件。
import babel from '@rollup/plugin-babel';
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'cjs',
},
plugins: [
babel({
babelHelpers: 'bundled', // 允许插件打包时使用 Babel helpers
presets: ['@babel/preset-env'], // 使用 Babel 转换最新的 JavaScript 语法
}),
],
};
这个例子中,我们引入了 @rollup/plugin-babel 插件,它通过 Babel 将最新的 ES6+ 代码转换成兼容性更好的 ES5 代码。
Tree Shaking 是 Rollup 的核心功能之一,它会自动移除项目中没有用到的代码,从而减小打包后的文件体积。
// utils.js
export function usedFunction() {
console.log('This function is used.');
}
export function unusedFunction() {
console.log('This function is not used.');
}
// index.js
import { usedFunction } from './utils.js';
usedFunction();
在这个例子中,usedFunction 被导入并使用,而 unusedFunction 没有被使用。打包后的文件中,Rollup 会自动移除 unusedFunction,从而优化文件体积。
Rollup 支持多种输出格式,如 cjs(CommonJS)、es(ES Modules)、umd(通用模块)等,适合不同的应用场景。
export default {
input: 'src/index.js',
output: [
{
file: 'dist/bundle.cjs.js',
format: 'cjs', // CommonJS 格式
},
{
file: 'dist/bundle.esm.js',
format: 'es', // ES Modules 格式
},
],
};
通过这种配置,可以同时生成两种不同格式的包,供不同的使用场景选择。
详细用法移步官网:,,。
Gulp 是一个基于流的任务自动化工具,适合处理复杂的构建流程,比如文件的编译、压缩、监听和热更新等。
Gulp 的核心概念是任务(Task),一个任务定义了需要执行的操作,比如将 Sass 文件编译成 CSS,或者压缩 JavaScript 文件。
初级使用
// gulpfile.js
const { src, dest } = require('gulp');
function copy() {
return src('src/*.js') // 指定源文件
.pipe(dest('dist')); // 复制到目标文件夹
}
exports.default = copy;
bash
复制代码
npx gulp
Gulp 的强大之处在于它可以通过插件完成许多自动化任务,比如文件编译、压缩、图片优化等。
const { src, dest } = require('gulp');
const sass = require('gulp-sass')(require('sass'));
function compileSass() {
return src('src/styles/*.scss')
.pipe(sass()) // 使用 gulp-sass 编译 Sass
.pipe(dest('dist/styles'));
}
exports.default = compileSass;
使用 gulp-sass 插件来编译 Sass 文件,Element Plus 打包样式文件也使用 gulp-sass来打包。
Gulp 可以监听文件的变化,并在文件发生更改时自动执行相应的任务。
const { watch, series } = require('gulp');
const { compileSass } = require('./compileSass');
function watchFiles() {
watch('src/styles/*.scss', compileSass); // 当 Sass 文件发生变化时,自动编译
}
exports.default = series(compileSass, watchFiles);
这个任务会在启动后监听 src/styles/*.scss 文件的变化,当文件被修改时,自动执行 Sass 编译任务。
Gulp 还可以用于压缩 JavaScript 和 CSS 文件,以减少生产环境中的文件大小。
const { src, dest } = require('gulp');
const terser = require('gulp-terser');
function minifyJs() {
return src('src/*.js')
.pipe(terser()) // 使用 gulp-terser 压缩 JavaScript
.pipe(dest('dist'));
}
exports.default = minifyJs;
使用 gulp-terser 插件来压缩 JavaScript 文件,生成的文件会比原始文件体积更小。
Gulp 提供了 series 和 parallel 方法,允许我们串行或并行执行多个任务。
const { series, parallel } = require('gulp');
function clean() {
// 清理任务
}
function buildJs() {
// 打包 JavaScript 文件
}
function buildCss() {
// 编译 Sass 或 CSS 文件
}
exports.default = series(clean, parallel(buildJs, buildCss)); // 先执行 clean,然后并行执行 buildJs 和 buildCss
clean 任务会先被执行,接着 buildJs 和 buildCss 会并行执行,极大地提高了构建效率。
具体详细用法:。
pnpm i @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-replace @vitejs/plugin-vue @vitejs/plugin-vue-jsx rollup-plugin-esbuild unplugin-vue-macros rollup gulp@4.0.2 -D
为什么安装 gulp@4.0.2 版本?
如果安装最新版本的 gulp 版本5.xx,在执行自动化模块构建命令时会终端会抛出识别不到模块错误。降低版本以下命令才会生效。
"start": "gulp --require @esbuild-kit/cjs-loader -f gulpfile.ts",
使用 FzMiniAlias 自定义插件,目的是在打包过程中处理项目中的路径别名。比如,在代码中我们可能会使用别名,而不是使用相对路径 FzMiniAlias 插件确保 Rollup 能够正确识别这些别名。Element Plus 组件中自定义插件是ElementPlusAlias()。
VueMacros 是一个插件集,用来处理 Vue 3 的高级特性,比如宏(macros),用于简化 Vue 3 开发。setupComponent: false:不启用组件内的宏功能。setupSFC: false:不启用单文件组件(SFC)的宏功能。 关闭这些功能可以减少构建时间和避免不必要的复杂性。
plugins: {}:
VueMacros 插件的 plugins 选项中,我们启用了 vue 和 vueJsx 插件,分别用于处理 .vue 文件和 Vue 中的 JSX 语法。vue() 插件:isProduction: true:指定打包环境为生产环境。这样做可以让 Rollup 自动进行一些优化。template.compilerOptions:这里我们进一步自定义了 Vue 模板编译器的选项。hoistStatic: false:不提升静态节点。通常提升静态节点可以提高渲染性能,但在某些复杂的 Vue 模板中可能会导致问题,因此这里选择关闭此功能。cacheHandlers: false:不缓存事件处理器。缓存事件处理器可以提高性能,但在某些动态场景中,我们可能不希望缓存,所以关闭此选项。vueJsx() 插件:为什么使用 JSX?:
JSX 提供了更大的灵活性,尤其在处理复杂动态组件时,能够提供比模板语法更高的可编程性。
这是 Rollup 官方提供的 @rollup/plugin-node-resolve 插件,主要用于解析模块路径。默认情况下,Rollup 只会解析相对路径的模块。使用这个插件可以让 Rollup 识别并打包来自 node_modules 的依赖。具体可以参照官网 Rollup 的插件详细介绍。
extensions: [...]:指定解析时允许的文件扩展名。这里包括了 .mjs(ES 模块)、.js(JavaScript)、.json 和 .ts(TypeScript)文件。这样 Rollup 就能自动识别这些文件类型,无需在 import 语句中显式指定扩展名。
这是 @rollup/plugin-commonjs 插件,用于将 CommonJS 模块转换为 ES 模块。很多第三方库依旧使用 CommonJS(特别是在 node_modules 中),而 Rollup 主要支持 ES 模块,因此这个插件非常重要,它能够让 Rollup 正确处理这些旧式模块。
为什么需要 CommonJS 支持?:
虽然 ES 模块是现代 JavaScript 的标准模块系统,但许多 npm 库仍然以 CommonJS 格式发布。为了能够打包这些库,我们需要使用 commonjs() 插件进行转换。
esbuild 是一个超快速的 JavaScript 和 TypeScript 编译器。我们使用 rollup-plugin-esbuild 插件在 Rollup 中集成 esbuild,这样可以大幅加快编译速度。
exclude: []:这个选项告诉 esbuild 不要排除任何文件,确保所有文件都可以通过 esbuild 进行处理。sourceMap: minify:决定是否生成 Source Map。minify 参数控制了是否需要生成源映射。target:指定输出的 JavaScript 代码的目标环境。例如,target 可以是 es2015,这样生成的代码就可以在支持 ES6 的浏览器中运行。loaders:定义文件类型的 loader。在这个配置中,.vue 文件会使用 TypeScript 编译器进行处理。define:在打包过程中,process.env.NODE_ENV 被替换为 'production',以确保生产环境的优化生效。treeShaking: true:启用 Tree Shaking,它会移除未使用的代码,减少包的体积。legalComments: 'eof':表示版权声明等注释应当保留在文件末尾。这对于遵循开源协议的库非常重要。这是 @rollup/plugin-replace 插件,主要用于在打包时替换代码中的某些字符串。在这里,我们将所有的 process.env.NODE_ENV 替换为 'production',这能触发 Vue 和其他库的生产环境优化。
为什么要替换 NODE_ENV?:
在生产环境中,Vue 和其他库会执行额外的优化(如移除调试信息、启用性能优化),但这些优化通常需要通过 process.env.NODE_ENV 来判断当前环境是否是生产环境。
根据 minify 这个参数来判断是否需要将 esbuild 中的压缩打包文件加入到 plugins 中,这个参数也和输出打包文件格式相关。
const plugins: Plugin[] = [
FzMiniAlias(),
VueMacros({
setupComponent: false,
setupSFC: false,
plugins: {
vue: vue({
isProduction: true,
template: {
compilerOptions: {
hoistStatic: false,
cacheHandlers: false,
},
},
}),
vueJsx: vueJsx(),
},
}),
nodeResolve({
extensions: ['.mjs', '.js', '.json', '.ts'],
}),
commonjs(),
esbuild({
exclude: [],
sourceMap: minify,
target,
loaders: {
'.vue': 'ts',
},
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
},
treeShaking: true,
legalComments: 'eof',
}),
replace({
'process.env.NODE_ENV': JSON.stringify('production'),
}),
]
if (minify) {
plugins.push(
minifyPlugin({
target,
sourceMap: true,
})
)
}
const bundle = await rollup({
input: path.resolve(epRoot, 'index.ts'),// packages/fz-mini/index.ts
plugins, //rollup 打包使用插件
external: await generateExternal({ full: true }),
treeshake: true,
})
获取需要打包的入口文件,可以是单个文件也可以是 glob 函数匹配的文件路径。主文件打包引入的 index.ts 导出所有组件、常量、指令和 hooks,并定义了安装整个组件库的方法 。
rollup 打包的配置项,用来丰富你的打包功能。
这个配置项定义了打包时不包含的外部依赖(即在打包时排除的模块)。通常用于避免将项目的依赖包在一起,以减小打包文件体积,并确保引用的库在运行时从外部环境中加载。 。
创建一个获取 package.json 文件中的所有 dependencies 键名和 peerDependencies 键名函数。
import type { ProjectManifest } from '@pnpm/types'
export const getPackageManifest = (pkgPath: string) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require(pkgPath) as ProjectManifest
}
export const getPackageDependencies = (
pkgPath: string
): Record<'dependencies' | 'peerDependencies', string[]> => {
const manifest = getPackageManifest(pkgPath)
const { dependencies = {}, peerDependencies = {} } = manifest
return {
dependencies: Object.keys(dependencies),
peerDependencies: Object.keys(peerDependencies),
}
}
动态排除打包函数generateExternal:用于生成一个函数,帮助 rollup 确定是否将模块视为外部依赖。
参数 full 的布尔值决定了是全量打包还是不全量打包,如果 full 为 true ,则这个函数接收到的 id 与 peerDependencies中的依赖进行匹配筛选,如果匹配到了返回 true ,意味着这个模块中的这个 id 依赖被排除。id 一般是模块中的依赖路径 ep: 可能是 lodash 或 lodash/cloneDeep。模块中的依赖是需要打包的目标文件中的所引入的第三方库。
import { epPackage, getPackageDependencies } from '@element-plus/build-utils'
export const generateExternal = async (options: { full: boolean }) => {
const { dependencies, peerDependencies } = getPackageDependencies(epPackage)
return (id: string) => {
const packages: string[] = [...peerDependencies]
if (!options.full) {
packages.push('@vue', ...dependencies)
}
return [...new Set(packages)].some(
(pkg) => id === pkg || id.startsWith(`${pkg}/`)
)
}
}
generateExternal 函数是如何将模块中的依赖视为外部依赖并在打包依赖中排除?
内部函数 (id: string) => {} 接受一个参数 id,表示模块导入依赖的名称或路径。Rollup 在处理模块依赖时,会调用 external 函数并传入当前模块的 id,以判断该模块是否应被标记为外部依赖。若函数返回 true,则模块会被视为外部依赖,从而不会被打包到最终输出文件中;若返回 false,则模块会被包含在打包文件中。
如果packages = ['vue', '@vueuse/core', '@vue', 'lodash', 'axios']。如果导入的依赖是 vue ,那么 id ='vue' ,检查 packages.some(pkg => id === pkg || id.startsWith(${pkg}/)),packages 数组里面存在 'vue' 依赖名,所以这个将会被视为外部依赖。
想要了解更多 Rollup 额外的输出配置可以去官网了解,本章只讲述组件中用到的配置项()
await writeBundles(bundle, [
{
format: 'umd',
file: path.resolve(
epOutput,
'dist',
formatBundleFilename('index.full', minify, 'js')
),
exports: 'named',
name: PKG_CAMELCASE_NAME,
globals: {
vue: 'Vue',
},
sourcemap: minify,
banner,
},
{
format: 'esm',
file: path.resolve(
epOutput,
'dist',
formatBundleFilename('index.full', minify, 'mjs')
),
sourcemap: minify,
banner,
},
])
打包输出文件配置应该符合以下功能:
file: :
输出文件的路径由 resolve 函数拼接而成也可以是 glob 函数匹配到的文件名。这里使用了格式化文件名函数 formatBundleFilename来区分文件是否被压缩和想要声明的后缀名。
export function formatBundleFilename(
name: string,
minify: boolean,
ext: string
) {
return `${name}${minify ? '.min' : ''}.${ext}`
}
format: 'umd' 和 format: 'es':
指定输出的模块格式。umd 用于通用模块定义,适合在多种环境中使用;es 则输出标准 ES Module 格式,适合现代 JavaScript 项目。
sourcemap: true:
将生成 Source Map 文件,用于调试压缩后的代码。
exports: 'named':
指定打包生成的模块的导出方式,以适应不同的 JavaScript 模块环境和需求。 详细介绍( )。
default:仅导出默认导出(default export),适用于只有一个主要导出或希望主要提供默认导出的模块。named:仅导出命名导出(named export),适合需要导出多个命名变量或函数的模块。name: 'FzMini':
指定全局变量的名称,当在浏览器中以 <script> 标签引入 UMD 文件时,这个库将被挂载到 window.FzMini 上。
globals: { vue: 'Vue' }:
这里声明 vue 是外部依赖,表示不将 Vue 打包进库,而是依赖外部的 Vue 实例。在浏览器中,它将对应 window.Vue。
export const withTaskName = <T extends TaskFunction>(name: string, fn: T) =>
Object.assign(fn, { displayName: name })
export const buildFull = (minify: boolean) => async () =>
Promise.all([buildFullEntry(minify), buildFullLocale(minify)])
export const buildFullBundle: TaskFunction = parallel(
withTaskName('buildFullMinified', buildFull(true)),
withTaskName('buildFull', buildFull(false))
)
withTaskName 函数为传进来的函数对象添加 displayName 属性,作用也就是用于方便地标识该任务函数的名称。
这里我将会详细的介绍 Element Plus 通过 Gulp 来自动化构建的具体流程,让你彻底搞定 Gulp 在 Element Plus 组件库的应用。
export default series(
// 在根目录的 package.json 中配置命令 "clean": "pnpm run clean:dist && pnpm run -r --parallel clean",
withTaskName('clean',()=>run('pnpm run clean')),
// 创建 build 目录
withTaskName('createOutput', () => mkdir(epOutput, { recursive: true })),
parallel(
runTask('buildFullBundle')
),
)
)
export * from './src'
import { spawn } from 'child_process'
import chalk from 'chalk'
import consola from 'consola'
import { projRoot } from '@element-plus/build-utils'
export const run = async (command: string, cwd: string = projRoot) =>
new Promise<void>((resolve, reject) => {
const [cmd, ...args] = command.split(' ') // command => pnpm run clean - cmd:pnpm ...arg:['run','clean']
consola.info(`run: ${chalk.green(`${cmd} ${args.join(' ')}`)}`)
//
const app = spawn(cmd, args, {
cwd, //当前工作目录
stdio: 'inherit', //输出标准流按照父进程,输出在主进程控制台显示信息
shell: process.platform === 'win32',
})
const onProcessExit = () => app.kill('SIGHUP')
//子进程结束时触发,code!==0 时抛出异常
app.on('close', (code) => {
process.removeListener('exit', onProcessExit)
if (code === 0) resolve()
else
reject(
new Error(`Command failed. \n Command: ${command} \n Code: ${code}`)
)
})
//主进程退出时触发,杀死子进程,在主进程退出时用来清理子进程
process.on('exit', onProcessExit)
})
command :接受一个命令字符串,如 pnpm run start,并将其分解为命令 (cmd) 和参数 (args)。
consola:将执行命令输出在控制台中。
spawn:使用 Node.js 的 spawn 方法创建子进程,执行指定的命令。
stdio: 'inherit':让子进程的输出直接显示在主进程的控制台中。
shell: process.platform === 'win32':为 Windows 平台启用 shell 模式,确保命令的兼容性。
onProcessExit:在主进程退出时,执行 app.kill('SIGHUP'),确保子进程终止,避免僵尸进程。
app.on('close'):监听子进程的关闭事件,如果子进程正常退出(code === 0),调用 resolve;否则,调用 reject 并传回错误信息。
export const runTask = (name: string) =>
withTaskName(`shellTask:${name}`, () =>
run(`pnpm run start ${name}`, buildRoot)
)
--require参数的作用是告诉 Node.js 在运行 Gulp 时,先加载 @esbuild-kit/cjs-loader 模块。@esbuild-kit/cjs-loader 是一个兼容 CommonJS 和 ESM 的加载器,主要目的是让 TypeScript 文件可以直接被 Node.js 执行,而无需提前编译为 JavaScript。cjs-loader 允许我们直接使用 TypeScript 编写的 gulpfile.ts 文件,并让 Node.js 识别 TypeScript 语法。gulpfile.js,而在 Element Plus 中,为了利用 TypeScript 的类型检查和自动完成功能,Gulp 配置文件使用了 TypeScript 格式(gulpfile.ts)。这个参数告诉 Gulp 使用该文件作为配置入口,以加载和执行其中定义的任务。buildFullBundle 模块的 呢?首先我们得明白 gulp 是如何执行 series 中得任务组合。
export default series(
// 在根目录的 package.json 中配置命令 "clean": "pnpm run clean:dist && pnpm run -r --parallel clean",
withTaskName('clean',()=>run('pnpm run clean')),
// 创建 build 目录
withTaskName('createOutput', () => mkdir(epOutput, { recursive: true })),
parallel(
runTask('buildFullBundle')
),
)
)
export * from './src'
在这个 default 任务中,调用了 runTask('buildFullBundle'),这正是 buildFullBundle 任务被执行的关键。
runTask('buildFullBundle') 如何执行 buildFullBundle?runTask('buildFullBundle') 会创建一个带有 shellTask:buildFullBundle 名称的任务,并通过 run 函数执行命令 pnpm run start buildFullBundle。
由于 package.json 文件中的 start 脚本定义了 gulp --require @esbuild-kit/cjs-loader -f gulpfile.ts,这意味着运行 pnpm run start buildFullBundle 会启动 Gulp 并加载 gulpfile.ts 文件。
当运行 pnpm run start buildFullBundle 时,pnpm 会先在 package.json 中找到 start 脚本定义:
"start": "gulp --require @esbuild-kit/cjs-loader -f gulpfile.ts"
这个 start 脚本实际上是一个命令别名,它等价于手动执行:
gulp --require @esbuild-kit/cjs-loader -f gulpfile.ts
将额外参数 buildFullBundle 传递给 gulp 命令 在运行 pnpm run start buildFullBundle 时,buildFullBundle 被当作参数传递给 pnpm run start 命令。pnpm 会将 buildFullBundle 追加到 start 脚本的末尾,于是完整命令变为:
gulp --require @esbuild-kit/cjs-loader -f gulpfile.ts buildFullBundle
出现这三个图片的内容说明就已经打包主文件成功了。恭喜你!!
本篇篇幅比较长,但是干货还是很多的,可能结构不是那么的完美,可以对着 Element Plus 的源码部分理解起来可能更加通顺。最后在总结一下这篇文章的知识点,下篇介绍模块打包,类型打包和样式打包。
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- huatuo9.cn 版权所有 赣ICP备2023008801号-1
违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务