突然好奇了下ElementPlus打包的源码,今天想着去看一下,顺便记录一下看这块代码的过程
去github下载源码到本地,如果没有🪜,gitee去下载一下element-plus: 🎉 Vue 3 的桌面端组件库 (gitee.com)(😀我在公司我也没🪜)
拿到源码这么多目录,我特么看啥啊❓❓❓ 冷静一下先别急,我要看打包,找下package.json打包的scripts命令。有了:"build": "pnpm run -C internal/build start"
,ok懂了我要去internal/build目录下面看start命令。我继续找啊找,发现了"start": "gulp --require @esbuild-kit/cjs-loader -f gulpfile.ts"
万能的deepseek啊🙏🙏🙏,@esbuild-kit/cjs-loader
这个是啥我没见过。

好的我懂了💡~ 啊不对,gulp文档打开一下先,看下说明

其实ts文件gulp没法识别啊,所以--require @esbuild-kit/cjs-loader
,这个就是Node.js 参数,要求在执行脚本前先加载 @esbuild-kit/cjs-loader
,这样gulpfile就可以是ts文件了
ok那我们可以去看gulpfile.ts写的啥了
如果不了解gulp的,请看这里gulp.js - 基于流(stream)的自动化构建工具 | gulp.js中文网 (gulpjs.com.cn)
gulpfile.ts看一下
typescript
import path from 'path'
import { copyFile, mkdir } from 'fs/promises'
import { copy } from 'fs-extra'
import { parallel, series } from 'gulp'
import {
buildOutput,
epOutput,
epPackage,
projRoot,
} from '@element-plus/build-utils'
import { buildConfig, run, runTask, withTaskName } from './src'
import type { TaskFunction } from 'gulp'
import type { Module } from './src'
export const copyFiles = () =>
Promise.all([
copyFile(epPackage, path.join(epOutput, 'package.json')),
copyFile(
path.resolve(projRoot, 'README.md'),
path.resolve(epOutput, 'README.md')
),
copyFile(
path.resolve(projRoot, 'typings', 'global.d.ts'),
path.resolve(epOutput, 'global.d.ts')
),
])
export const copyTypesDefinitions: TaskFunction = (done) => {
const src = path.resolve(buildOutput, 'types', 'packages')
const copyTypes = (module: Module) =>
withTaskName(`copyTypes:${module}`, () =>
copy(src, buildConfig[module].output.path, { recursive: true })
)
return parallel(copyTypes('esm'), copyTypes('cjs'))(done)
}
export const copyFullStyle = async () => {
await mkdir(path.resolve(epOutput, 'dist'), { recursive: true })
await copyFile(
path.resolve(epOutput, 'theme-chalk/index.css'),
path.resolve(epOutput, 'dist/index.css')
)
}
export default series(
withTaskName('clean', () => run('pnpm run clean')),
withTaskName('createOutput', () => mkdir(epOutput, { recursive: true })),
parallel(
runTask('buildModules'),
runTask('buildFullBundle'),
runTask('generateTypesDefinitions'),
runTask('buildHelper'),
series(
withTaskName('buildThemeChalk', () =>
run('pnpm run -C packages/theme-chalk build')
),
copyFullStyle
)
),
parallel(copyTypesDefinitions, copyFiles)
)
export * from './src'
我们直接定位到series这个任务的方法
less
export default series(
withTaskName('clean', () => run('pnpm run clean')),
withTaskName('createOutput', () => mkdir(epOutput, { recursive: true })),
parallel(
runTask('buildModules'),
runTask('buildFullBundle'),
runTask('generateTypesDefinitions'),
runTask('buildHelper'),
series(
withTaskName('buildThemeChalk', () =>
run('pnpm run -C packages/theme-chalk build')
),
copyFullStyle
)
),
parallel(copyTypesDefinitions, copyFiles)
)
好多封装的函数,我们一个一个看下
internal\build\src\utils\process.ts的run
函数说明
typescript
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(' ')
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')
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)
})
spawn
是干啥的呢,创建子进程执行命令的。
chalk
,改终端字符串颜色的,这样输出更好看噻,做命令行工具常用的东西
consola
,是一个日志输出的,比咋们常用的console更友好
projRoot
,明显就是我们项目根目录(我有信心,我不点进去@element-plus/build-utils就能看出来😏😏😏)
这段代码这个 run
函数,就是帮助在 Node.js 中执行 shell 命令
里面会打印一下日志,这个consola.info(`run: ${chalk.green(`${cmd} ${args.join(' ')}`)}`)
就是打印日志
紧接着下面这个代码,其实就是使用 spawn
创建子进程执行我们的命令。stdio: 'inherit'
就是子进程共享父进程的终端。shell: process.platform === 'win32'
使得构建脚本能够在 Windows 和非 Windows 平台上一致地工作(就是兼容一下大家的系统嘛,我是用的windows💻)
ini
const app = spawn(cmd, args, {
cwd,
stdio: 'inherit',
shell: process.platform === 'win32',
})
主进程退出,肯定要杀死子进程,所以有这个代码
dart
const onProcessExit = () => app.kill('SIGHUP')
process.on('exit', onProcessExit)
子进程退出,要移除主进程的监听,以及正常退出就resolve,异常就直接报错
javascript
app.on('close', (code) => {
process.removeListener('exit', onProcessExit)
if (code === 0) resolve()
else
reject(
new Error(`Command failed. \n Command: ${command} \n Code: ${code}`)
)
})
总结一下,run
函数就是帮我们运行命令的,并且输出一下运行日志。(自己的项目可以也可以偷过来用一下😼😼😼我们争做代码的搬运工)
internal\build\src\utils\gulp.ts的两个函数withTaskName
和runTask
函数说明
typescript
import { buildRoot } from '@element-plus/build-utils'
import { run } from './process'
import type { TaskFunction } from 'gulp'
export const withTaskName = <T extends TaskFunction>(name: string, fn: T) =>
Object.assign(fn, { displayName: name })
export const runTask = (name: string) =>
withTaskName(`shellTask:${name}`, () =>
run(`pnpm run start ${name}`, buildRoot)
)
我们先来看下withTaskName,这个函数传入一个name(string类型)和一个fn(要是TaskFunction的类型),然后给这个函数的displayName字段添加上name。经过我的分析,其实就是给对应的gulp任务定义一下打印日志的名称。我们可以写个demo看一下,修改一下gulpfile.ts
中export default series
这里的代码
javascript
export const gege = async () => {
console.log('唱、跳、rap、篮球')
}
gege.displayName = '鸽鸽'
export default series(gege)
然后我们执行pnpm build
看下效果,可以看到gulp帮我们输出了对应的任务名称
然后看下runTask
,参数有一个name,函数里面调用withTaskName,给对应函数添加上了displayName为shellTask:${name}
,这个函数是调用上面已经说过的run方法,执行了一下pnpm run start ${name}
,执行的目录是buildRoot(/internal/build
)。
举例说明:例如这个runTask('buildModules')
,这里的/internal/build下package.json的start命令是"start": "gulp --require @esbuild-kit/cjs-loader -f gulpfile.ts",
,也就是会执行gulp --require @esbuild-kit/cjs-loader -f gulpfile.ts "buildModules"
。也就会执行gulpfile.ts导出的buildModules任务

在gulpfile.ts中有一行导出export * from './src'
,一层一层查找可以找到对应的buildModules
这个任务的定义,在这个文件internal\build\src\tasks\modules.ts
clean
和createOutput
任务
scss
export default series(
withTaskName('clean', () => run('pnpm run clean')),
withTaskName('createOutput', () => mkdir(epOutput, { recursive: true }))
// parallel(
// runTask('buildModules'),
// runTask('buildFullBundle'),
// runTask('generateTypesDefinitions'),
// runTask('buildHelper'),
// series(
// withTaskName('buildThemeChalk', () =>
// run('pnpm run -C packages/theme-chalk build')
// ),
// copyFullStyle
// )
// )
// parallel(copyTypesDefinitions, copyFiles)
)
clean任务呢,在项目根目录目录帮我们执行了pnpm run clean
命令,

这个命令先执行了pnpm run clean:dist
,也就是rimraf dist
清理一下dist目录,打包之前一般都要先干的事情。然后pnpm run -r --parallel clean
,-r
表示对所有子项目也都执行clean
命令,--parallel
表示并行执行。这里其实清理项目和项目的子包下面的打包文件。
然后是createOutput任务,这个任务就是创建一下文件夹,这个文件夹epOutput
是dist/element-plus
我们只保留这两个任务运行一下pnpm build
,可以看到删除了原来的dist,也创建了对应的目录
🥱🥱🥱🥱🥱🥱🥱🥱🥱🥱🥱🥱🥱🥱🥱🥱累了我先更新到这,后续请看下回分解