读完本篇,你可以了解到如何将组件库打包成各种格式

上一篇里提到了启动服务前会先进行一下组件库的打包,运行的命令为:

varlet-cli compile

显然是varlet-cli提供的一个命令:

处理函数为compile,接下来我们详细看一下这个函数都做了什么。

// varlet-cli/src/commands/compile.ts
export async function compile(cmd: { noUmd: boolean }) {process.env.NODE_ENV = 'compile'await removeDir()// ...
}// varlet-cli/src/commands/compile.ts
export function removeDir() {// ES_DIR:varlet-ui/es// LIB_DIR:varlet-ui/lib// HL_DIR:varlet-ui/highlight// UMD_DIR:varlet-ui/umdreturn Promise.all([remove(ES_DIR), remove(LIB_DIR), remove(HL_DIR), remove(UMD_DIR)])
}

首先设置了一下当前的环境变量,然后清空相关的输出目录。

// varlet-cli/src/commands/compile.ts
export async function compile(cmd: { noUmd: boolean }) {// ...process.env.TARGET_MODULE = 'module'await runTask('module', compileModule)process.env.TARGET_MODULE = 'esm-bundle'await runTask('esm bundle', () => compileModule('esm-bundle'))process.env.TARGET_MODULE = 'commonjs'await runTask('commonjs', () => compileModule('commonjs'))process.env.TARGET_MODULE = 'umd'!cmd.noUmd && (await runTask('umd', () => compileModule('umd')))
}

接下来依次打包了四种类型的产物,方法都是同一个compileModule,这个方法后面会详细分析。

组件的基本组成

Button组件为例看一下未打包前的组件结构:

一个典型组件的构成主要是四个文件:

.less:样式

.vue:组件

index.ts:导出组件,提供组件注册方法

props.ts:组件的props定义

样式部分Varlet使用的是less语言,样式比较少的话会直接内联写到Vue单文件的style块中,否则会单独创建一个样式文件,比如图中的button.less,每个组件除了引入自己本身的样式外,还会引入一些基本样式、其他组件的样式:

index.ts文件用来导出组件,提供组件的注册方法:

props.ts文件用来声明组件的props类型:

有的组件没有使用.vue,而是.tsx,也有些组件会存在其他文件,比如有些组件就还存在一个provide.ts文件,用于向子孙组件注入数据。

打包的整体流程

首先大致过一遍整体的打包流程,主要函数为compileModule

// varlet-cli/src/compiler/compileModule.ts
export async function compileModule(modules: 'umd' | 'commonjs' | 'esm-bundle' | boolean = false) {if (modules === 'umd') {// 打包umd格式await compileUMD()return}if (modules === 'esm-bundle') {// 打包esm-bundle格式await compileESMBundle()return}// 打包commonjs和module格式// 打包前设置一下环境变量process.env.BABEL_MODULE = modules === 'commonjs' ? 'commonjs' : 'module'// 输出目录// ES_DIR:varlet-ui/es// LIB_DIR:varlet-ui/libconst dest = modules === 'commonjs' ? LIB_DIR : ES_DIR// SRC_DIR:varlet-ui/src,直接将组件的源码目录复制到输出目录await copy(SRC_DIR, dest)// 读取输出目录const moduleDir: string[] = await readdir(dest)// 遍历打包每个组件await Promise.all(// 遍历每个组件目录moduleDir.map((filename: string) => {const file: string = resolve(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 = await getPublicDirs()// 生成整体的入口文件await (modules === 'commonjs' ? compileCommonJSEntry(dest, publicDirs) : compileESEntry(dest, publicDirs))
}

umdesm-bundle两种格式都会把所有内容都打包到一个文件,用的是Vite提供的方法进行打包。

commonjsmodule是单独打包每个组件,不会把所有组件的内容都打包到一起,Vite没有提供这个能力,所以需要自行处理,具体操作为:

  • 先把组件源码目录varlet/src/下的所有组件文件都复制到对应的输出目录下;
  • 然后在输出目录遍历每个组件目录:
    • 创建两个样式的导出文件;
    • 删除不需要的目录、文件(测试、示例、文档);
    • 分别编译Vue单文件、ts文件、less文件;
  • 全部打包完成后,遍历所有组件,动态生成整体的导出文件;

compileESEntry方法为例看一下整体导出文件的生成:

// varlet-cli/src/compiler/compileScript.ts
export async function compileESEntry(dir: string, publicDirs: string[]) {const imports: string[] = []const plugins: string[] = []const constInternalComponents: string[] = []const cssImports: string[] = []const lessImports: string[] = []const publicComponents: string[] = []// 遍历组件目录名称publicDirs.forEach((dirname: string) => {// 连字符转驼峰式const publicComponent = bigCamelize(dirname)// 收集组件名称publicComponents.push(publicComponent)// 收集组件导入语句imports.push(`import ${publicComponent}, * as ${publicComponent}Module from './${dirname}'`)// 收集内部组件导入语句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文件的内容,注意它是不包含样式的const indexTemplate = `\
${imports.join('\n')}\n
${constInternalComponents.join('\n')}\n
${install}
export {install,${publicComponents.join(',\n  ')}
}export default {install,${publicComponents.join(',\n  ')}
}
`// 拼接css导入语句const styleTemplate = `\
${cssImports.join('\n')}
`// 拼接umdIndex.js文件,这个文件是用于后续打包umd和esm-bundle格式时作为打包入口,注意它是包含样式导入语句的const umdTemplate = `\
${imports.join('\n')}\n
${cssImports.join('\n')}\n
${install}
export {install,${publicComponents.join(',\n  ')}
}export default {install,${publicComponents.join(',\n  ')}
}
`// 拼接less导入语句const lessTemplate = `\
${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'),])
}

打包成module和commonjs格式

打包成umdesm-bundle两种格式依赖module格式的打包产物,而打包成modulecommonjs两种格式是同一套逻辑,所以我们先来看看是如何打包成这两种格式的。

这两种格式就是单独打包每个组件,生成单独的入口文件和样式文件,然后再生成一个统一的导出入口,不会把所有组件的内容都打包到同一个文件,方便按需引入,去除不需要的内容,减少文件体积。

打包每个组件的compileDir方法:

// varlet-cli/src/compiler/compileModule.ts
export async function compileDir(dir: string) {// 读取组件目录const dirs = await readdir(dir)// 遍历组件目录下的文件await Promise.all(dirs.map((filename) => {const file = resolve(dir, filename)// 删除组件目录下的__test__目录、example目录、docs目录;[TESTS_DIR_NAME, EXAMPLE_DIR_NAME, DOCS_DIR_NAME].includes(filename) && removeSync(file)// 如果是.d.ts文件或者是style目录(前面为样式入口文件创建的目录)直接返回if (isDTS(file) || filename === STYLE_DIR_NAME) {return Promise.resolve()}// 编译文件return compileFile(file)}))
}

删除了不需要的目录,然后针对需要编译的文件调用了compileFile方法:

// varlet-cli/src/compiler/compileModule.ts
export async function compileFile(file: string) {isSFC(file) && (await compileSFC(file))// 编译vue文件isScript(file) && (await compileScriptFile(file))// 编译js文件isLess(file) && (await compileLess(file))// 编译less文件isDir(file) && (await compileDir(file))// 如果是目录则进行递归
}

分别处理三种文件,让我们一一来看。

编译Vue单文件

// varlet-cli/src/compiler/compileSFC.ts
import { parse } from '@vue/compiler-sfc'export async function compileSFC(sfc: string) {// 读取Vue单文件内容const sources: string = await 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 does not support compiling script setup syntax\
\n  The error in ${sfc}`)return}// ...
}

使用@vue/compiler-sfc包来解析Vue单文件,parse方法可以解析出Vue单文件中的各个块,针对各个块,@vue/compiler-sfc包都提供了相应的编译方法,后续都会涉及到。

// varlet-cli/src/compiler/compileSFC.ts
import hash from 'hash-sum'export async function compileSFC(sfc: string) {// ...// scoped// 检查是否存在scoped作用域的样式块const hasScope = styles.some((style) => style.scoped)// 将单文件的内容进行hash生成idconst id = hash(sources)// 生成样式的scopeIdconst scopeId = hasScope ? `data-v-${id}` : ''// ...
}

这一步主要是检查style块是否存在作用域块,存在的话会生成一个作用域id,作为css的作用域,防止和其他样式冲突,这两个id相关的编译方法需要用到。

// varlet-cli/src/compiler/compileSFC.ts
import { compileTemplate } from '@vue/compiler-sfc'export async function compileSFC(sfc: string) {// ...if (script) {// template// 编译模板为渲染函数const render =template &&compileTemplate({id,source: template.content,filename: sfc,compilerOptions: {scopeId,},})// 注入render函数let { content } = scriptif (render) {const { code } = rendercontent = injectRender(content, code)}// ...}
}

使用@vue/compiler-sfc包的compileTemplate方法将解析出的模板部分编译为渲染函数,然后调用injectRender方法将渲染函数注入到script中:

// varlet-cli/src/compiler/compileSFC.ts
const NORMAL_EXPORT_START_RE = /export\s+default\s+{/
const DEFINE_EXPORT_START_RE = /export\s+default\s+defineComponent\s*\(\s*{/export function injectRender(script: string, render: string): string {if (DEFINE_EXPORT_START_RE.test(script.trim())) {return script.trim().replace(DEFINE_EXPORT_START_RE,`${render}\nexport default defineComponent({render,\`)}if (NORMAL_EXPORT_START_RE.test(script.trim())) {return script.trim().replace(NORMAL_EXPORT_START_RE,`${render}\nexport default {render,\`)}return script
}

兼容两种导出方式,以一个小例子来看一下,比如生成的渲染函数为:

export function render(_ctx, _cache) {// ...
}

script的内容为:

export default defineComponent({name: 'VarButton',// ...
})

注入renderscript的内容变成了:

export function render(_ctx, _cache) {// ...
}
export default defineComponent({render,name: 'VarButton',/// ...
})

其实就是把渲染函数的内容和script的内容合并了,script其实就是组件的选项对象,所以同时也把组件的渲染函数添加到组件对象上。

继续compileSFC方法:

// varlet-cli/src/compiler/compileSFC.ts
import { compileStyle } from '@vue/compiler-sfc'export async function compileSFC(sfc: string) {// ...if (script) {// ...// script// 编译jsawait compileScript(content, sfc)// style// 编译样式for (let index = 0; index < styles.length; index++) {const style: SFCStyleBlock = styles[index]// replaceExt方法接收文件名称,比如xxx.vue,然后使用第二个参数替换文件名称的扩展名,比如处理完会返回xxxSfc.lessconst file = replaceExt(sfc, `Sfc${index || ''}.${style.lang || 'css'}`)// 编译样式块let { code } = compileStyle({source: style.content,filename: file,id: scopeId,scoped: style.scoped,})// 去除样式中的导入语句code = extractStyleDependencies(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))}}
}

调用了compileScript方法编译script内容,这个方法我们下一小节再说。然后遍历style块,每个块都会生成相应的样式文件,比如Button.vue组件存在一个less语言的style

那么会生成一个ButtonSfc.less,因为是less,所以同时也会再编译生成一个ButtonSfc.css文件,当然这两个样式文件里只包括内联在Vue单文件中的样式,不包括使用@import导入的样式,所以生成的这两个样式文件都是空的:

编译样式块使用的是@vue/compiler-sfccompileStyle方法,它会帮我们处理<style scoped>, <style module> 以及css变量注入的问题。

extractStyleDependencies方法会提取并去除样式中的导入语句:

// varlet-cli/src/compiler/compileStyle.ts
import { parse, resolve } from 'path'export function extractStyleDependencies(file: string,code: string,reg: RegExp,//     /@import\s+['"](.+)['"]\s*;/gexpect: '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.ts
import { 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部分,主要是tstsx文件,Varlet大部分组件是使用Vue单文件编写的,不过也有少数组件使用的是tsx,编译调用了compileScriptFile方法:

// varlet-cli/src/compiler/compileScript.ts
export async function compileScriptFile(file: string) {const sources = readFileSync(file, 'utf-8')await compileScript(sources, file)
}

读取文件,然后调用compileScript方法,前面Vue单文件中解析出来的script部分内容调用的也是这个方法。

兼容模块导入

// varlet-cli/src/compiler/compileScript.ts
export 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.ts
export 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.ts
import { transformAsync } from '@babel/core'export async function compileScript(script: string, file: string) {// ...// 使用babel编译jslet { 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.ts
export const REQUIRE_CSS_RE = /(?<!['"`])require\(\s*['"](\.{1,2}\/.+\.css)['"]\s*\);?(?!\s*['"`])/g
export const REQUIRE_LESS_RE = /(?<!['"`])require\(\s*['"](\.{1,2}\/.+\.less)['"]\s*\);?(?!\s*['"`])/g
export const IMPORT_CSS_RE = /(?<!['"`])import\s+['"](\.{1,2}\/.+\.css)['"]\s*;?(?!\s*['"`])/g
export const IMPORT_LESS_RE = /(?<!['"`])import\s+['"](\.{1,2}\/.+\.less)['"]\s*;?(?!\s*['"`])/gexport async function compileScript(script: string, file: string) {// ...code = extractStyleDependencies(file,code as string,modules === 'commonjs' ? REQUIRE_CSS_RE : IMPORT_CSS_RE,'css')code = extractStyleDependencies(file,code as string,modules === 'commonjs' ? REQUIRE_LESS_RE : IMPORT_LESS_RE,'less')// ...
}

extractStyleDependencies方法前面已经介绍了,所以这一步的操作就是提取并去除script内的样式导入语句。

转换其他导入语句

// varlet-cli/src/compiler/compileScript.ts
export 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.ts
export async function compileScript(script: string, file: string) {// ...removeSync(file)writeFileSync(replaceExt(file, '.js'), code, 'utf8')
}

最后就是把处理完的script内容写入文件。

到这里.vue.ts.tsx文件都已处理完毕:

小节

到这里,打包成modulecommonjs格式就完成了,总结一下所做的事情:

  • less文件直接使用less包编译成同名的css文件;
  • tstsx等文件使用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.ts
import { build } from 'vite'export function compileESMBundle() {return new Promise<void>((resolve, reject) => {const config = getESMBundleConfig(getVarletConfig())build(config).then(() => resolve()).catch(reject)})
}

getVarletConfig方法会把varlet-cli/varlet.default.config.jsvarlet-ui/varlet.config.js两个配置进行合并,看一下getESMBundleConfig方法:

// varlet-cli/src/config/vite.config.js
export function getESMBundleConfig(varletConfig: Record<string, any>): InlineConfig {const name = get(varletConfig, 'name')// name默认为Varletconst fileName = `${kebabCase(name)}.esm.js`// 输出文件名,varlet.esm.jsreturn {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/esexports: 'named',// 既存在命名导出,也存在默认导出,所以设置为named,详情:https://rollupjs.org/guide/en/#outputexportsglobals: {// 在umd构建模式下为外部化的依赖提供一个全局变量vue: 'Vue',},},},},plugins: [clear()],}
}

其实就是使用如上的配置来调用Vitebuild方法进行打包,可参考库模式,可以看到打包入口为前面打包module格式时生成的umdIndex.js文件。

因为Vite开发环境使用的是esbuild,生产环境打包使用的是rollup,所以想要深入玩转Vite,这几个东西都需要了解,包括各自的配置选项、插件开发等,还是不容易的。

打包完成后会在varlet-ui/es/目录下生成两个文件:

打包成umd格式

打包成umd格式调用的是compileUMD方法:

// varlet-cli/src/compiler/compileModule.ts
import { build } from 'vite'export function compileUMD() {return new Promise<void>((resolve, reject) => {const config = getUMDConfig(getVarletConfig())build(config).then(() => resolve()).catch(reject)})
}

整体和打包esm-bundle是一样的,只不过获取的配置不一样:

// varlet-cli/src/config/vite.config.js
export function getUMDConfig(varletConfig: Record<string, any>): InlineConfig {const name = get(varletConfig, 'name')// name默认为Varletconst fileName = `${kebabCase(name)}.js`// 将驼峰式转换成-连接return {logLevel: 'silent',build: {emptyOutDir: true,lib: {name,formats: ['umd'],// 设置为umdfileName: () => fileName,entry: resolve(ES_DIR, 'umdIndex.js'),// ES_DIR:varlet-ui/es,打包入口},rollupOptions: {external: ['vue'],output: {dir: UMD_DIR,// 输出目录,UMD_DIR:varlet-ui/umdexports: '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.js
function 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配置详解

上文编译scripttstsx内容使用的是babel,提到了会使用本地的配置文件:

主要就是配置了一个presetspresetsbabel的预设,作用是方便使用一些共享配置,可以简单了解为包含了一组插件,babel的转换是通过各种插件进行的,所以使用预设可以免去自己配置插件,可以使用本地的预设,也可以使用发布在npm 包里的预设,预设可以传递参数,比如上图,使用的是@varlet/cli包里附带的一个预设:

预设其实就是一个js文件,导出一个函数,这个函数可以接受两个参数,api可以访问babel自身导出的所有模块,同时附带了一些配置文件指定的apioptions为使用预设时传入的参数,这个函数需要返回一个对象,这个对象就是具体的配置。

// varlet-cli/src/config/babel.config.ts
module.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.transformvarlet自己编写的,用来转换Vue单文件。

还配置了一个babel-plugin-jsx插件,用来在Vue中支持JSX语法。

预设和插件的应用顺序是有规定的:

  • 插件在预设之前运行
  • 多个插件按从第一个到最后一个顺序运行
  • 多个预设按从最后一个到第一个顺序运行

基于此我们可以大致窥探一下整个转换流程,首先运行插件@vue/babel-plugin-jsx转换JSX语法,然后运行预设babel.sfc.transform

// varlet-cli/src/config/babel.sfc.transform.ts
import { 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语法的转换。

Vue3组件库打包指南,一次生成esm、esm-bundle、commonjs、umd四种格式相关推荐

  1. 从0搭建Vue3组件库(五): 如何使用Vite打包组件库

    本篇文章将介绍如何使用 vite 打包我们的组件库,同时告诉大家如何使用插件让打包后的文件自动生成声明文件(*.d.ts) 打包配置 vite 专门提供了库模式的打包方式,配置其实非常简单,首先全局安 ...

  2. 看了它--你也能轻松部署vue3组件库

    开发组件库场景 作为前端开发人员,当你运行一个 npm install element-plus -S,会安装 element-plus,打开 node_modules 能看到它对应很多相关联的依赖包 ...

  3. 使用 Vite 和 TypeScript 从零打造一个属于自己的 Vue3 组件库

    前言 随着前端技术的发展,业界涌现出了许多的UI组件库.例如我们熟知的ElementUI,Vant,AntDesign等等.但是作为一个前端开发者,你知道一个UI组件库是如何被打造出来的吗? 读完这篇 ...

  4. 猿创征文 | 开箱即用 yyg-cli:快速创建 vue3 组件库和vue3 全家桶项目

    1 yyg-cli 是什么 yyg-cli 是优雅哥开发的快速创建 vue3 项目的脚手架.在 npm 上发布了两个月,11月1日进行了大升级,发布 1.1.0 版本:支持创建 vue3 全家桶项目和 ...

  5. Blazor 组件库开发指南

    翻译自 Waqas Anwar 2021年5月21日的文章 <A Developer's Guide To Blazor Component Libraries> [1] Blazor 的 ...

  6. 手把手教你写一个Vue3组件库但是乞丐版

    好久没写文章了,最近在研究一些组件库的实现方法,分享一下.在这我这篇文章之前其实已经有一篇文章讲了Vue如何打包组件库了(最底部),但是这篇文章一是没有源码二是Vue3和Vue2的组件库写法有点不一样 ...

  7. VUE3组件库-input组件

    theme: mk-cute 这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战 VUE3组件库-input组件 大家好,今天一起来学习vue3实现input组件,希望对你有帮助 目录预览 基 ...

  8. js和php能生成一样的随机数_JavaScript_JS生成某个范围的随机数【四种情况详解】,前言: JS没有现成的函数,能 - phpStudy...

    JS生成某个范围的随机数[四种情况详解] 前言: JS没有现成的函数,能够直接生成指定范围的随机数. 但是它有个函数:Math.random()  这个函数可以生成 [0,1) 的一个随机数. 利用它 ...

  9. C# 中GUID生成格式的四种格式

    在C#中GUID生成的四种格式 var uuid = Guid.NewGuid().ToString(); // 9af7f46a-ea52-4aa3-b8c3-9fd484c2af12var uui ...

最新文章

  1. 基于多核DSP处理器DM8168的视频处理方法
  2. 中国工业自动化行业需求现状及投资风险评估报告2022-2027年版
  3. python写linux脚本_Linux下设置python脚本文件为服务
  4. 原码、反码、补码的运算 【2分钟掌握】
  5. Spring Boot 正确中使用JPA实战
  6. Druid-基本概念
  7. GRTN赋能淘系内容业务的演进路线及未来规划
  8. OCP考试052考试,新的考题还有答案整理-23题
  9. 重复download CRM已经存在的parent equipment
  10. html dom 修改,HTML DOM - 修改
  11. 信息学奥赛一本通 1113:不与最大数相同的数字之和 | OpenJudge NOI 1.9 07
  12. 【译】ASP.NET MVC 5 教程 - 4:添加模型
  13. 火狐浏览器配置webDriver
  14. 用什么手机软件可测试无线信道,wifi信道
  15. Shiro笔记 教程
  16. php压缩文件夹(整理最新版)
  17. Mac修改文件名的颜色
  18. Windows——卸载MinGW的方法
  19. 合成孔径雷达干涉测量InSAR数据处理、地形三维重建、形变信监息提取、测
  20. vector 多维向量定义及其初始化

热门文章

  1. samba配置共享用户home目录
  2. 第三十五篇、基于Arduino uno,获取DS18B20温度传感器的温度值——结果导向
  3. adplus处理进程崩溃和界面卡住
  4. wiremock学习
  5. Linux基础——“ shell命令 概述”了解shell和基本linux
  6. 天锋w2019_天锋W2019复刻的如此登峰造极?网友大呼:三星你怎么看?
  7. ftp 报错 200 Type set to A
  8. 计算机中关于字节和位的关系,字节和位的关系
  9. BottomNavigationBar酷炫导航栏
  10. python代码对齐快捷键_PyCharm 格式化代码 常用快捷键