Vue3组件库打包指南,一次生成esm、esm-bundle、commonjs、umd
本文是阅读Varlet 组件库源代码系列的第二篇文章。阅读本文,了解如何将Vue3 组件库打包成不同的格式。
上一篇文章提到,组件库是在启动服务之前打包的。运行的命令是:
varlet-cli编译显然是varlet-cli提供的命令。
处理函数编译完毕现在让我们仔细看看这个函数做了什么。
//varlet-cli/src/commands/compile.tsexport async functioncompile(cmd: { noUmd: boolean }) { process.env.NODE_ENV='compile'等待RemoveDir() //.}//varlet-cli/src /命令/compile.tsexport function deleteDir() { //ES_DIR: varlet-ui/es //LIB_DIR: varlet-ui/lib //HL_DIR: varlet-ui/highlight //UMD_DIR: varlet-ui/umd 返回Promise。 all([remove(ES_DIR),remove(LIB_DIR),remove(HL_DIR),remove(UMD_DIR)])} 首先设置当前环境变量,然后清除关联的输出目录。
//varlet-cli/src/commands/compile.tsexport async function apply(cmd: { noUmd: boolean }) { //. process.env.TARGET_MODULE='module' wait runTask('module',compileModule) process.env .TARGET_MODULE='esm-bundle'等待runTask('esm Bundle', ()=compileModule('esm-bundle')) process.env.TARGET_MODULE='commonjs'等待runTask('commonjs', ()=compileModule(' commonjs')) process.env.TARGET_MODULE='umd' !cmd.noUmd(await runTask('umd', ()=compileModule('umd')))} 接下来依次打包四个产品,运行这个方法都是同一个compileModule,后面我们会详细分析。
组件基础配置:以Button组件为例,回顾一下打包前的组件结构。
典型的组件由四个主要文件组成。
.less: style.vue: 组件index.ts: 组件导出,组件注册方法提供props.ts: 组件props定义
对于样式部分,Varlet 使用less 语言。如果样式数量比较少,样式直接内联写入Vue单文件样式块中。否则,将创建一个单独的样式文件(例如,button.less)。每个组件不仅介绍了自己,还介绍了一些基本样式以及除了样式之外其他组件的样式。
Index.ts文件用于导出组件并提供组件的注册方法。
props.ts 文件用于声明组件的props 类型。
有些组件使用.tsx 而不是.vue,有些组件包含其他文件。例如,某些组件还有一个Provide.ts 文件,用于将数据注入到后代组件中。
首先,让我们从高层次看一下整个包装过程。主要功能是compileModule。
//varlet-cli/src/compiler/compileModule.tsexport 异步函数compileModule(modules: 'umd' | 'commonjs' | 'esm-bundle' | boolean=false) { if (modules==='umd') { //打包umd格式await CombiUMD() return } if (modules==='esm-bundle') { //打包esm-bundle格式await CombiESSMBundle() return } //打包commonjs和模块格式//打包过程前设置环境变量.env.BABEL_MODULE=module==='commonjs' 'commonjs' : 'module' //输出目录//ES_DIR: varlet-ui/es //LIB_DIR: varlet-ui/lib const dest=module===' commonjs ' LIB_DIR : ES_DIR //SRC_DIR:varlet-ui/src,直接将组件源代码目录复制到输出目录wait copy(SRC_DIR, dest) //读取输出目录const moduleDir: string[]=wait readdir(dest) ) //遍历并打包各个组件await Promise.all( //遍历各个组件的目录moduleDir.map((filename: string)={ const file: string=solve(dest, filename ) if (isDir(file)) { //在每个组件目录下新建两个样式入口文件EnsureFileSync(resolve(file, './style/index.js')) EnsureFileSync(resolve(file, './style/less.js')) } //打包该组件return isDir(file)compileDir(file) : null }) ) //遍历varlet-ui/src/目录并删除所有现有的['index.vue', 'index.tsx', 'index.ts', 'index .jsx', 'index.js'] 这些文件之一的目录const publicDirs=wait getPublicDirs() //整体等待(modules==='commonjs'compileCommonJSEntry(dest, publicDirs) : applyESEntry(dest, publicDirs ) )} umd 和esm-bundle 格式都遵循Vite 提供的打包方式,将所有内容打包到一个文件中。 commonjs 和module 分别封装各个组件,并不将所有组件的内容打包在一起。 Vite不提供此功能,需要您自行处理。具体操作如下:
首先将组件源代码目录varlet/src/下的所有组件文件复制到对应的输出目录下,然后检查输出目录内的各个组件目录: 创建两种样式的导出文件,删除不需要的目录和文件(test、例如,文档)。 分别编译Vue单文件、ts文件、less文件。一旦所有打包完成,它会遍历所有组件并动态生成整个导出文件。以compileESEntry方法为例,查看整个导出文件的生成。
//varlet-cli/src/compiler/compileScript.tsexport 异步函数compileESEntry(dir: string, publicDirs: string[]) { const import: string[]=[] constplugins: string[]=[] const constInternalComponents: string[]=[] const cssImports: string[]=[] constlessImports: string[]=[] const publicComponents: string[]=[] //遍历组件目录名称publicDirs.forEach((dirname: string)={ //驼峰连字符转换const publicComponent=bigCamelize(dirname ) //收集组件名称publicComponents.push(publicComponent) //收集组件导入语句imports.push(`import ${publicComponent}, * as ${publicComponent}Module from ' ./${dirname}'` ) //收集内部组件的import 语句constInternalComponents.push( `export const _${publicComponent}Component=${publicComponent}Module._${publicComponent}Component || {} ` ) //收集插件注册语句plugins Push(` ${publicComponent }.install app.use(${publicComponent})`) //收集样式导入语句cssImports.push(`import './${dirname}/style'`)lessImports.push(`import './${dirname }/style/less'`) }) //拼接组件注册方法const install=`function install(app) { ${plugins.join('\n ')}}` //注意拼接导出入口Index. js 文件不包含任何样式constindexTemplate=`\${imports.join( '\n')}\n${constInternalComponents.join('\n')}\n${install} import { install, ${publicComponents .join(',\n ')}}export default { install, $ {publicComponents.join(',\n ')}}` //合并css import 语句const styleTemplate=`\$ { cssImports.join(' \n')}` //合并umdIndex.js 文件Masu.该文件稍后将在打包umd 和esm-bundle 格式时用作打包入口。请注意const umdTemplate=`\ ${ 。 import.join('\n')}\n${cssImports.join('\n')}\n${install}export { install, ${publicComponents.join(',\n ')}} 默认导出{ install, ${publicComponents.join(',\n ')}}` //不合并import 语句constlessTemplate=`\${lessImports.join('\n')}` //合并内容对应文件await Promise.all([ writeFile(resolve(dir, 'index.js'), IndexTemplate, 'utf-8'), writeFile(resolve(dir, 'umdIndex.js'), umdTemplate, 'utf - 8' ), writeFile (resolve(dir, 'style.js'), styleTemplate, 'utf-8'), writeFile(resolve(dir, 'less.js'), lessTemplate, 'utf-8'), ] ) }打包模块和commonjs格式,打包成umd和esm-bundle格式依赖于模块格式打包产品,而打包成module和commonjs格式是同一套逻辑,所以我们先看一下如何打包。将其转换为两种格式。这两种格式分别对每个组件进行打包,生成单独的入口和样式文件,然后生成统一的导出入口。它不会将所有组件的内容打包到同一个文件中,从而更容易按需部署和删除。删除不必要的内容并减小文件大小。为每个组件封装compileDir方法。
//varlet-cli/src/compiler/compileModule.tsexport async function applyDir(dir: string) { //读取组件目录const dirs=wait readdir(dir) //遍历组件目录下的文件wait Promise.all( dirs .map((filename)={ const file=solve(dir, filename) //删除组件目录下的__test__、example、docs 目录; [TESTS_DIR_NAME, EXAMPLE_DIR_NAME, DOCS_DIR_NAME].includes( filename) RemoveSync(file) ) //对于.d.ts 文件或样式目录(为样式入口文件预先创建的目录),返回if (isDTS(file) || filename===STYLE_DIR_NAME) { return Promise.resolve() } //编译文件return CombiFile(file) }) )} 删除不需要的目录,然后对需要编译的文件调用compileFile 方法。
//varlet-cli/src/compiler/compileModule.tsexport async functioncompileFile(file: string) { isSFC(file) (await compiSFC(file))//编译vue文件isScript(file) (await cancelScriptFile(file))//编译js file isLess(file) (await compiLess(file))//编译less file isDir(file) (await compiDir(file))//对目录进行递归} 分别处理3种类型的文件我们来处理一下,请看一下一。
编译Vue 单文件//varlet-cli/src/compiler/compileSFC.tsimport { parse } from '@vue/compiler-sfc'export async functioncompileSFC(sfc: string) { //读取Vue 单文件的内容constsources: string=wait readFile(sfc, 'utf-8') //使用@vue/compiler-sfc包解析单个文件const {descriptor }=parse(sources, { sourceMap: false }) //提取单个文件的各个部分const { script , scriptSetup, template,styles }=descriptor //Varlet 暂时不支持setup 语法if (scriptSetup) { logger.warning( `\n Varlet Cli 不支持编译脚本setup 语法\\n ${sfc}` ) return } //.} 使用@vue/compiler-sfc 包解析Vue 单个文件。 parse 方法可以解析单个Vue 文件中的每个块。对于每个块,@vue/compiler-sfc 包都提供了相应的块。稍后我们将解释如何编译它。
//varlet-cli/src/compiler/compileSFC.tsimport hash from 'hash-sum'export async functioncompileSFC(sfc: string) { //. //Scoped //检查是否存在作用域样式块const hasScope=style .some((style)=style.scoped) //哈希单个文件的内容以生成ID const id=hash(sources) //生成stylescopeId constscopeId=hasScope ` data-v-${id } ` : '' //.} 这一步主要是检查样式块是否有作用域块。如果存在,则会生成范围ID 作为CSS 范围,以防止与其他样式发生冲突。您必须使用使这两个ID 相关的编译方法。
//varlet-cli/src/compiler/compileSFC.tsimport { COMPILETemplate } from '@vue/compiler-sfc' 导出异步函数COMPILESFC(sfc: string) { //. if (script) { //template //编译模板是渲染函数const render=template COMPILETemplate({ id, source: template.content, filename: sfc, COMPILEROptions: {scopeId, },}) //插入渲染函数let { content }=script if (render) { const { code }=render content=jectRender(content, code) } //. }}使用@vue/compiler-sfc包的compileTemplate方法将解析后的模板部分编译为渲染函数,并使用injectRender方法调用。将渲染函数插入到脚本中。
//varlet-cli/src/compiler/compileSFC.tsconst NORMAL_EXPORT_START_RE=/export\s+default\s+{/const DEFINE_EXPORT_START_RE=/export\s+default\s+defineComponent\s*\(\s*{/导出函数) insertRender(script: string, render: string): string { if (DEFINE_EXPORT_START_RE.test(script.trim())) { return script.trim().replace( DEFINE_EXPORT_START_RE, `${render}\nexport 默认DefineComponent({ render, \ ` ) } if (NORMAL_EXPORT_START_RE.test(script.trim())) { return script.trim().replace( NORMAL_EXPORT_START_RE, `${render}\nexport default { render,\ ` ) } return script}看一个兼容的小例子。例如生成的渲染函数为:
import function render(_ctx, _cache) { //.}脚本内容如下:
exportdefaultdefineComponent({ name: 'VarButton', //.}) 插入渲染后,脚本应如下所示:
export function render(_ctx, _cache) { //.}exportdefaultdefineComponent({ render, name: 'VarButton', ///.}) 实际上将render 函数的内容与脚本的内容合并。 script实际上是组件的一个选项对象,因此组件的绘图功能也被添加到组件对象中。接下来,运行compileSFC 方法。
//varlet-cli/src/compiler/compileSFC.tsimport {compileStyle } from '@vue/compiler-sfc'导出异步函数compileSFC(sfc: string) { //. if (script) { //.//script //编译js等待cancelScript(content, sfc) //style //编译样式(letindex=0;indexstyles.length;index++) { const style: SFCStyleBlock=styles[index] //replaceExt方法指定一个文件名,如xxx.vue,并使用第二个参数来替换文件扩展名。例如,处理后返回xxxSfc.less const file=ReplaceExt(sfc, `Sfc${index ||)。 ''}.${style.lang || 'css'}`) //编译样式块let { code }=applyStyle({ source: style.content, filename: file, id:scopeId,scoped: style.scoped, }) //去掉样式中的import语句code=extractStyleDependency(file, code, STYLE_IMPORT_RE, style.lang as 'css' | 'less', true) //将解析后的样式写入文件writeFileSync(file, clearEmptyLine( code), ' utf-8') //对于样式,该块采用less 语言,因此它也会被编译成css 文件。 style.lang==='less' (await COMPILELEss(file)) } }} compi 被调用。
leScript方法编译script内容,这个方法我们下一小节再说。然后遍历style块,每个块都会生成相应的样式文件,比如Button.vue组件存在一个less语言的style块 那么会生成一个ButtonSfc.less,因为是less,所以同时也会再编译生成一个ButtonSfc.css文件,当然这两个样式文件里只包括内联在Vue单文件中的样式,不包括使用@import导入的样式,所以生成的这两个样式文件都是空的: 编译样式块使用的是@vue/compiler-sfc的compileStyle方法,它会帮我们处理extractStyleDependencies方法会提取并去除样式中的导入语句: // varlet-cli/src/compiler/compileStyle.tsimport { parse, resolve } from 'path'export function extractStyleDependencies( file: string, code: string, reg: RegExp,// /@import\s+['"](.+)['"]\s*;/g expect: 'css' | 'less', self: boolean) { const { dir, base } = parse(file) // 用正则匹配出样式导入语句 const styleImports = code.match(reg) [] // 这两个文件是之前创建的 const cssFile = resolve(dir, './style/index.js') const lessFile = resolve(dir, './style/less.js') const modules = process.env.BABEL_MODULE // 遍历导入语句 styleImports.forEach((styleImport: string) => { // 去除导入源的扩展名及处理导入的路径,因为index.js和less.js两个文件和Vue单文件不在同一个层级,所以导入的相对路径需要修改一下 const normalizedPath = normalizeStyleDependency(styleImport, reg) // 将导入语句写入创建的两个文件中 smartAppendFileSync( cssFile, modules === 'commonjs' `require('${normalizedPath}.css')\n` : `import '${normalizedPath}.css'\n` ) smartAppendFileSync( lessFile, modules === 'commonjs' `require('${normalizedPath}.${expect}')\n` : `import '${normalizedPath}.${expect}'\n` ) }) // 上面已经把Vue单文件中style块内的导入语句提取出去了,另外之前也提到了每个style块本身也会创建一个样式文件,所以导入这个文件的语句也需要追加进去: if (self) { smartAppendFileSync( cssFile, modules === 'commonjs' `require('${normalizeStyleDependency(base, reg)}.css')\n` : `import '${normalizeStyleDependency(base, reg)}.css'\n` ) smartAppendFileSync( lessFile, modules === 'commonjs' `require('${normalizeStyleDependency(base, reg)}.${expect}')\n` : `import '${normalizeStyleDependency(base, reg)}.${expect}'\n` ) } // 去除样式中的导入语句 return code.replace(reg, '')}到这里,一共生成了四个文件: 编译less文件script部分的编译比较复杂,我们最后再看,先看一下less文件的处理。 // varlet-cli/src/compiler/compileStyle.tsimport { render } from 'less'export async function compileLess(file: string) { const source = readFileSync(file, 'utf-8') const { css } = await render(source, { filename: file }) writeFileSync(replaceExt(file, '.css'), clearEmptyLine(css), 'utf-8')}很简单,使用less包将less编译成css,然后写入文件即可,到这里又生成了一个css文件: 编译script文件script部分,主要是ts、tsx文件,Varlet大部分组件是使用Vue单文件编写的,不过也有少数组件使用的是tsx,编译调用了compileScriptFile方法: // varlet-cli/src/compiler/compileScript.tsexport async function compileScriptFile(file: string) { const sources = readFileSync(file, 'utf-8') await compileScript(sources, file)}读取文件,然后调用compileScript方法,前面Vue单文件中解析出来的script部分内容调用的也是这个方法。 兼容模块导入// varlet-cli/src/compiler/compileScript.tsexport async function compileScript(script: string, file: string) { const modules = process.env.BABEL_MODULE // 兼容模块导入 if (modules === 'commonjs') { script = moduleCompatible(script) } // ...}首先针对commonjs做了一下兼容处理: // varlet-cli/src/compiler/compileScript.tsexport const moduleCompatible = (script: string): string => { const moduleCompatible = get(getVarletConfig(), 'moduleCompatible', {}) Object.keys(moduleCompatible).forEach((esm) => { const commonjs = moduleCompatible[esm] script = script.replace(esm, commonjs) }) return script}替换一些导入语句,Varlet组件开发是基于ESM规范的,使用其他库时导入的肯定也是ESM版本,所以编译成commonjs模块时需要修改成对应的commonjs版本,Varlet引入的第三方库不多,主要就是dayjs: 使用babel编译继续compileScript方法: // varlet-cli/src/compiler/compileScript.tsimport { transformAsync } from '@babel/core'export async function compileScript(script: string, file: string) { // ... // 使用babel编译js let { code } = (await transformAsync(script, { filename: file,// js内容对应的文件名,babel插件会用到 })) as BabelFileResult // ...}接下来使用@babel/core包编译js内容,transformAsync方法会使用本地的配置文件,因为打包命令是在varlet-ui/目录下运行的,所以babel会在这个目录下寻找配置文件: 编译成module还是commonjs格式的判断也在这个配置中,有关配置的详解,有兴趣的可以阅读最后的附录小节。 提取样式导入语句继续compileScript方法: // varlet-cli/src/compiler/compileScript.tsexport const REQUIRE_CSS_RE = /( extractStyleDependencies方法前面已经介绍了,所以这一步的操作就是提取并去除script内的样式导入语句。 转换其他导入语句// varlet-cli/src/compiler/compileScript.tsexport async function compileScript(script: string, file: string) { // ... code = replaceVueExt(code as string) code = replaceTSXExt(code as string) code = replaceJSXExt(code as string) code = replaceTSExt(code as string) // ...}这一步的操作是把script中的各种类型的导入语句都修改为导入.js文件,因为这些文件最后都会被编译成js文件,比如button/index.ts文件内导入了Button.vue组件: import Button from './Button.vue'// ...转换后会变成: import Button from './Button.js'// ...继续: // varlet-cli/src/compiler/compileScript.tsexport async function compileScript(script: string, file: string) { // ... removeSync(file) writeFileSync(replaceExt(file, '.js'), code, 'utf8')}最后就是把处理完的script内容写入文件。到这里.vue,.ts、.tsx文件都已处理完毕: 小节到这里,打包成module和commonjs格式就完成了,总结一下所做的事情: less文件直接使用less包编译成同名的css文件;ts、tsx等文件使用babel编译成js文件;提取并去除其中的样式导入语句,并将该样式导入语句写入单独的文件、修改.vue、.ts等类型的导入语句为对应的编译后的js;Vue单文件使用@vue/compiler-sfc解析并对各个块分别使用对应的函数进行编译;每个style块也会提取并去除其中的样式导入语句,并将该导入语句写入单独的文件,剩下的样式内容会分别创建一个对应的样式文件,如果是less块,同时会编译并创建一个同名的css文件;template的编译结果会合并到script内,然后script的内容会重复上一步ts文件的处理逻辑;所有组件都编译完了,再动态创建整体的导出文件,一共生成了四个文件:打包成esm-bundle打包成esm-bundle格式调用的是compileESMBundle方法: // varlet-cli/src/compiler/compileModule.tsimport { build } from 'vite'export function compileESMBundle() { return new Promise((resolve, reject) => { const config = getESMBundleConfig(getVarletConfig()) build(config) .then(() => resolve()) .catch(reject) })}getVarletConfig方法会把varlet-cli/varlet.default.config.js和varlet-ui/varlet.config.js两个配置进行合并,看一下getESMBundleConfig方法: // varlet-cli/src/config/vite.config.jsexport function getESMBundleConfig(varletConfig: Record): InlineConfig { const name = get(varletConfig, 'name')// name默认为Varlet const fileName = `${kebabCase(name)}.esm.js`// 输出文件名,varlet.esm.js return { logLevel: 'silent', build: { emptyOutDir: true,// 清空输出目录 lib: {// 指定构建为库 name,// 库暴露的全局变量 formats: ['es'],// 构建格式 fileName: () => fileName,// 打包出口 entry: resolve(ES_DIR, 'umdIndex.js'),// 打包入口 }, rollupOptions: {// 传给rollup的配置 external: ['vue'],// 外部化处理不需要打包进库的依赖 output: { dir: ES_DIR,// 输出目录,ES_DIR:varlet-ui/es exports: 'named',// 既存在命名导出,也存在默认导出,所以设置为named,详情:https://rollupjs.org/guide/en/#outputexports globals: {// 在umd构建模式下为外部化的依赖提供一个全局变量 vue: 'Vue', }, }, }, }, plugins: [clear()], }}其实就是使用如上的配置来调用Vite的build方法进行打包,可参考库模式,可以看到打包入口为前面打包module格式时生成的umdIndex.js文件。因为Vite开发环境使用的是esbuild,生产环境打包使用的是rollup,所以想要深入玩转Vite,这几个东西都需要了解,包括各自的配置选项、插件开发等,还是不容易的。打包完成后会在varlet-ui/es/目录下生成两个文件: 打包成umd格式打包成umd格式调用的是compileUMD方法: // varlet-cli/src/compiler/compileModule.tsimport { build } from 'vite'export function compileUMD() { return new Promise((resolve, reject) => { const config = getUMDConfig(getVarletConfig()) build(config) .then(() => resolve()) .catch(reject) })}整体和打包esm-bundle是一样的,只不过获取的配置不一样: // varlet-cli/src/config/vite.config.jsexport function getUMDConfig(varletConfig: Record): InlineConfig { const name = get(varletConfig, 'name')// name默认为Varlet const fileName = `${kebabCase(name)}.js`// 将驼峰式转换成-连接 return { logLevel: 'silent', build: { emptyOutDir: true, lib: { name, formats: ['umd'],// 设置为umd fileName: () => fileName, entry: resolve(ES_DIR, 'umdIndex.js'),// ES_DIR:varlet-ui/es,打包入口 }, rollupOptions: { external: ['vue'], output: { dir: UMD_DIR,// 输出目录,UMD_DIR:varlet-ui/umd exports: 'named', globals: { vue: 'Vue', }, }, }, }, // 使用了两个插件,作用如其名 plugins: [inlineCSS(fileName, UMD_DIR), clear()], }}大部分配置是一样的,打包入口同样也是varlet-ui/es/umdIndex.js,打包结果会在varlet-ui/umd/目录下生成一个varlet.js文件,Varlet和其他组件库稍微有点不一样的地方是它把样式也都打包进了js文件,省去了使用时需要再额外引入样式文件的麻烦,这个操作是inlineCSS插件做的,这个插件也是Varlet自己编写的,代码也很简单: // varlet-cli/src/config/vite.config.jsfunction inlineCSS(fileName: string, dir: string): PluginOption { return { name: 'varlet-inline-css-vite-plugin',// 插件名称 apply: 'build',// 设置插件只在构建时被调用 closeBundle() {// rollup钩子,打包完成后调用的钩子 const cssFile = resolve(dir, 'style.css') if (!pathExistsSync(cssFile)) { return } const jsFile = resolve(dir, fileName) const cssCode = readFileSync(cssFile, 'utf-8') const jsCode = readFileSync(jsFile, 'utf-8') const injectCode = `;(function(){var style=document.createElement('style');style.type='text/css';\style.rel='stylesheet';style.appendChild(document.createTextNode(\`${cssCode.replace(/\\/g, '\\\\')}\`));\var head=document.querySelector('head');head.appendChild(style)})();` // 将【动态将样式插入到页面】的代码插入到js代码内 writeFileSync(jsFile, `${injectCode}${jsCode}`) // 将该样式文件复制到varlet-ui/lib/style.css文件里 copyFileSync(cssFile, resolve(LIB_DIR, 'style.css')) // 删除样式文件 removeSync(cssFile) }, }}这个插件所做的事情就是在打包完成后,读取生成的style.css文件,然后拼接一段js代码,这段代码会把样式动态插入到页面,然后把这段js合并到生成的js文件中,这样就不用自己手动引入样式文件了。同时,也会把样式文件复制一份到lib目录下,也就是commonjs产物的目录。最后再回顾一下这个打包顺序: 你会发现这个顺序是有原因的,ems-bundle的打包入口依赖module的产物,umd打包会给commonjs复制一份样式文件,所以打包umd需要在commonjs后面。 附录:babel配置详解上文编译script、ts、tsx内容使用的是babel,提到了会使用本地的配置文件: 主要就是配置了一个presets,presets即babel的预设,作用是方便使用一些共享配置,可以简单了解为包含了一组插件,babel的转换是通过各种插件进行的,所以使用预设可以免去自己配置插件,可以使用本地的预设,也可以使用发布在npm包里的预设,预设可以传递参数,比如上图,使用的是@varlet/cli包里附带的一个预设: 预设其实就是一个js文件,导出一个函数,这个函数可以接受两个参数,api可以访问babel自身导出的所有模块,同时附带了一些配置文件指定的api,options为使用预设时传入的参数,这个函数需要返回一个对象,这个对象就是具体的配置。 // varlet-cli/src/config/babel.config.tsmodule.exports = (api : ConfigAPI, options: PresetOption = {}) => { if (api) { // 设置不要缓存该配置,每次都执行函数重新获取 api.cache.never() } // 判断打包格式 const isCommonJS = process.env.NODE_ENV === 'test' || process.env.BABEL_MODULE === 'commonjs' return { presets: [ [ require.resolve('@babel/preset-env'), { // 编译为commonjs模块类型时需要将ESM模块语法转换成commonjs模块语法,否则保留ESM模块语法 modules: isCommonJS 'commonjs' : false, loose: options.loose,// 是否允许@babel/preset-env预设中配置的插件开启松散转换,https://cloud.tencent.com/developer/article/1418101 }, ], require.resolve('@babel/preset-typescript'), require('./babel.sfc.transform'), ], plugins: [ [ require.resolve('@vue/babel-plugin-jsx'), { enableObjectSlots: options.enableObjectSlots, }, ], ], }}export default module.exports又配置了三个预设,无限套娃,@babel/preset-env预设是一个智能预设,会根据你的目标环境自动判断需要转换哪些语法,@babel/preset-typescript用来支持ts语法,babel.sfc.transform是varlet自己编写的,用来转换Vue单文件。还配置了一个babel-plugin-jsx插件,用来在Vue中支持JSX语法。预设和插件的应用顺序是有规定的: 插件在预设之前运行多个插件按从第一个到最后一个顺序运行多个预设按从最后一个到第一个顺序运行基于此我们可以大致窥探一下整个转换流程,首先运行插件@vue/babel-plugin-jsx转换JSX语法,然后运行预设babel.sfc.transform: // varlet-cli/src/config/babel.sfc.transform.tsimport { readFileSync } from 'fs'import { declare } from '@babel/helper-plugin-utils'module.exports = declare(() => ({ overrides: [ { test: (file: string) => { if (/\.vue$/.test(file)) { const code = readFileSync(file, 'utf8') return code.includes('lang="ts"') || code.includes("lang='ts'") } return false }, plugins: ['@babel/plugin-transform-typescript'], }, ],}))通过babel的overrides选项来根据条件注入配置,当处理的是Vue单文件的内容,并且使用的是ts语法,那么就会注入一个插件@babel/plugin-transform-typescript,用于转换ts语法,非Vue单文件会忽略这个配置,进入下一个preset:@babel/preset-typescript,这个预设也包含了前面的@babel/plugin-transform-typescript插件,但是这个预设只会在.ts文件才会启用ts插件,所以前面才需要自行判断Vue单文件并手动配置ts插件,ts语法转换完毕后最后会进入@babel/preset-env,进行js语法的转换。