本文为
Varlet
组件库源码主题浏览系列第二篇,读完本篇,你能够理解到如何将一个 Vue3 组件库打包成各种格局
上一篇里提到了启动服务前会先进行一下组件库的打包,运行的命令为:
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/umd
return 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/lib
const 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 生成 id
const id = hash(sources)
// 生成款式的 scopeId
const 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} = script
if (render) {const { code} = render
content = 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
// 编译 js
await compileScript(content, sfc)
// style
// 编译款式
for (let index = 0; index < styles.length; index++) {const style: SFCStyleBlock = styles[index]
// replaceExt 办法接管文件名称,比方 xxx.vue,而后应用第二个参数替换文件名称的扩展名,比方解决完会返回 xxxSfc.less
const 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*;/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.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 编译 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.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*['"`])/g
export 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 默认为 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.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 默认为 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.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
语法的转换。