本文为Varlet组件库源码主题浏览系列第二篇,读完本篇,你能够理解到如何将一个Vue3组件库打包成各种格局

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

varlet-cli compile

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

处理函数为compile,接下来咱们具体看一下这个函数都做了什么。

// varlet-cli/src/commands/compile.tsexport async function compile(cmd: { noUmd: boolean }) {    process.env.NODE_ENV = 'compile'    await removeDir()    // ...}// varlet-cli/src/commands/compile.tsexport 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.tsexport 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.tsexport 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))}

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

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

  • 先把组件源码目录varlet/src/下的所有组件文件都复制到对应的输入目录下;
  • 而后在输入目录遍历每个组件目录:

    • 创立两个款式的导出文件;
    • 删除不须要的目录、文件(测试、示例、文档);
    • 别离编译Vue单文件、ts文件、less文件;
  • 全副打包实现后,遍历所有组件,动静生成整体的导出文件;

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

// varlet-cli/src/compiler/compileScript.tsexport 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.tsexport 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.tsexport 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.tsimport { 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.tsimport 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.tsimport { 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.tsconst 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.tsimport { 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-sfccompileStyle办法,它会帮咱们解决<style scoped>, <style module> 以及css变量注入的问题。

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局部,次要是tstsx文件,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 = /(?<!['"`])require\(\s*['"](\.{1,2}\/.+\.css)['"]\s*\);?(?!\s*['"`])/gexport const REQUIRE_LESS_RE = /(?<!['"`])require\(\s*['"](\.{1,2}\/.+\.less)['"]\s*\);?(?!\s*['"`])/gexport const IMPORT_CSS_RE = /(?<!['"`])import\s+['"](\.{1,2}\/.+\.css)['"]\s*;?(?!\s*['"`])/gexport 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.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文件都已处理完毕:

大节

到这里,打包成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.tsimport { 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.jsexport 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()],  }}

其实就是应用如上的配置来调用Vitebuild办法进行打包,可参考库模式,能够看到打包入口为后面打包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<void>((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<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.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配置详解

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

次要就是配置了一个presetspresetsbabel的预设,作用是方便使用一些共享配置,能够简略理解为蕴含了一组插件,babel的转换是通过各种插件进行的,所以应用预设能够免去本人配置插件,能够应用本地的预设,也能够应用公布在npm 包里的预设,预设能够传递参数,比方上图,应用的是@varlet/cli包里附带的一个预设:

预设其实就是一个js文件,导出一个函数,这个函数能够承受两个参数,api能够拜访babel本身导出的所有模块,同时附带了一些配置文件指定的apioptions为应用预设时传入的参数,这个函数须要返回一个对象,这个对象就是具体的配置。

// 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.transformvarlet本人编写的,用来转换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语法的转换。