Vue3组件库打包指南,一次生成esm、esm-bundle、commonjs、umd四种格式
读完本篇,你可以了解到如何将组件库打包成各种格式
上一篇里提到了启动服务前会先进行一下组件库的打包,运行的命令为:
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))
}
umd
和esm-bundle
两种格式都会把所有内容都打包到一个文件,用的是Vite
提供的方法进行打包。
commonjs
和module
是单独打包每个组件,不会把所有组件的内容都打包到一起,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格式
打包成umd
和esm-bundle
两种格式依赖module
格式的打包产物,而打包成module
和commonjs
两种格式是同一套逻辑,所以我们先来看看是如何打包成这两种格式的。
这两种格式就是单独打包每个组件,生成单独的入口文件和样式文件,然后再生成一个统一的导出入口,不会把所有组件的内容都打包到同一个文件,方便按需引入,去除不需要的内容,减少文件体积。
打包每个组件的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',// ...
})
注入render
后script
的内容变成了:
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-sfc
的compileStyle
方法,它会帮我们处理<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
部分,主要是ts
、tsx
文件,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
文件都已处理完毕:
小节
到这里,打包成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.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.js
和varlet-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()],}
}
其实就是使用如上的配置来调用Vite
的build
方法进行打包,可参考库模式,可以看到打包入口为前面打包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配置详解
上文编译script
、ts
、tsx
内容使用的是babel
,提到了会使用本地的配置文件:
主要就是配置了一个presets
,presets
即babel
的预设,作用是方便使用一些共享配置,可以简单了解为包含了一组插件,babel
的转换是通过各种插件进行的,所以使用预设可以免去自己配置插件,可以使用本地的预设,也可以使用发布在npm
包里的预设,预设可以传递参数,比如上图,使用的是@varlet/cli
包里附带的一个预设:
预设其实就是一个js
文件,导出一个函数,这个函数可以接受两个参数,api
可以访问babel
自身导出的所有模块,同时附带了一些配置文件指定的api
,options
为使用预设时传入的参数,这个函数需要返回一个对象,这个对象就是具体的配置。
// 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.transform
是varlet
自己编写的,用来转换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四种格式相关推荐
- 从0搭建Vue3组件库(五): 如何使用Vite打包组件库
本篇文章将介绍如何使用 vite 打包我们的组件库,同时告诉大家如何使用插件让打包后的文件自动生成声明文件(*.d.ts) 打包配置 vite 专门提供了库模式的打包方式,配置其实非常简单,首先全局安 ...
- 看了它--你也能轻松部署vue3组件库
开发组件库场景 作为前端开发人员,当你运行一个 npm install element-plus -S,会安装 element-plus,打开 node_modules 能看到它对应很多相关联的依赖包 ...
- 使用 Vite 和 TypeScript 从零打造一个属于自己的 Vue3 组件库
前言 随着前端技术的发展,业界涌现出了许多的UI组件库.例如我们熟知的ElementUI,Vant,AntDesign等等.但是作为一个前端开发者,你知道一个UI组件库是如何被打造出来的吗? 读完这篇 ...
- 猿创征文 | 开箱即用 yyg-cli:快速创建 vue3 组件库和vue3 全家桶项目
1 yyg-cli 是什么 yyg-cli 是优雅哥开发的快速创建 vue3 项目的脚手架.在 npm 上发布了两个月,11月1日进行了大升级,发布 1.1.0 版本:支持创建 vue3 全家桶项目和 ...
- Blazor 组件库开发指南
翻译自 Waqas Anwar 2021年5月21日的文章 <A Developer's Guide To Blazor Component Libraries> [1] Blazor 的 ...
- 手把手教你写一个Vue3组件库但是乞丐版
好久没写文章了,最近在研究一些组件库的实现方法,分享一下.在这我这篇文章之前其实已经有一篇文章讲了Vue如何打包组件库了(最底部),但是这篇文章一是没有源码二是Vue3和Vue2的组件库写法有点不一样 ...
- VUE3组件库-input组件
theme: mk-cute 这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战 VUE3组件库-input组件 大家好,今天一起来学习vue3实现input组件,希望对你有帮助 目录预览 基 ...
- js和php能生成一样的随机数_JavaScript_JS生成某个范围的随机数【四种情况详解】,前言:
JS没有现成的函数,能 - phpStudy...
JS生成某个范围的随机数[四种情况详解] 前言: JS没有现成的函数,能够直接生成指定范围的随机数. 但是它有个函数:Math.random() 这个函数可以生成 [0,1) 的一个随机数. 利用它 ...
- C# 中GUID生成格式的四种格式
在C#中GUID生成的四种格式 var uuid = Guid.NewGuid().ToString(); // 9af7f46a-ea52-4aa3-b8c3-9fd484c2af12var uui ...
最新文章
- 基于多核DSP处理器DM8168的视频处理方法
- 中国工业自动化行业需求现状及投资风险评估报告2022-2027年版
- python写linux脚本_Linux下设置python脚本文件为服务
- 原码、反码、补码的运算 【2分钟掌握】
- Spring Boot 正确中使用JPA实战
- Druid-基本概念
- GRTN赋能淘系内容业务的演进路线及未来规划
- OCP考试052考试,新的考题还有答案整理-23题
- 重复download CRM已经存在的parent equipment
- html dom 修改,HTML DOM - 修改
- 信息学奥赛一本通 1113:不与最大数相同的数字之和 | OpenJudge NOI 1.9 07
- 【译】ASP.NET MVC 5 教程 - 4:添加模型
- 火狐浏览器配置webDriver
- 用什么手机软件可测试无线信道,wifi信道
- Shiro笔记 教程
- php压缩文件夹(整理最新版)
- Mac修改文件名的颜色
- Windows——卸载MinGW的方法
- 合成孔径雷达干涉测量InSAR数据处理、地形三维重建、形变信监息提取、测
- vector 多维向量定义及其初始化
热门文章
- samba配置共享用户home目录
- 第三十五篇、基于Arduino uno,获取DS18B20温度传感器的温度值——结果导向
- adplus处理进程崩溃和界面卡住
- wiremock学习
- Linux基础——“ shell命令 概述”了解shell和基本linux
- 天锋w2019_天锋W2019复刻的如此登峰造极?网友大呼:三星你怎么看?
- ftp 报错 200 Type set to A
- 计算机中关于字节和位的关系,字节和位的关系
- BottomNavigationBar酷炫导航栏
- python代码对齐快捷键_PyCharm 格式化代码 常用快捷键