天天看點

Vue3元件庫打包指南,一次生成esm、esm-bundle、commonjs、umd四種格式元件的基本組成打包的整體流程打包成module和commonjs格式打包成esm-bundle打包成umd格式附錄:babel配置詳解

本文為

Varlet

元件庫源碼主題閱讀系列第二篇,讀完本篇,你可以了解到如何将一個Vue3元件庫打包成各種格式

上一篇裡提到了啟動服務前會先進行一下元件庫的打包,運作的指令為:

varlet-cli compile
           

顯然是

varlet-cli

包提供的一個指令:

Vue3元件庫打包指南,一次生成esm、esm-bundle、commonjs、umd四種格式元件的基本組成打包的整體流程打包成module和commonjs格式打包成esm-bundle打包成umd格式附錄:babel配置詳解

處理函數為

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

元件為例看一下未打包前的元件結構:

Vue3元件庫打包指南,一次生成esm、esm-bundle、commonjs、umd四種格式元件的基本組成打包的整體流程打包成module和commonjs格式打包成esm-bundle打包成umd格式附錄:babel配置詳解

一個典型元件的構成主要是四個檔案:

.less:樣式

.vue:元件

index.ts:導出元件,提供元件注冊方法

props.ts:元件的props定義

樣式部分

Varlet

使用的是

less

語言,樣式比較少的話會直接内聯寫到

Vue

單檔案的

style

塊中,否則會單獨建立一個樣式檔案,比如圖中的

button.less

,每個元件除了引入自己本身的樣式外,還會引入一些基本樣式、其他元件的樣式:

Vue3元件庫打包指南,一次生成esm、esm-bundle、commonjs、umd四種格式元件的基本組成打包的整體流程打包成module和commonjs格式打包成esm-bundle打包成umd格式附錄:babel配置詳解

index.ts

檔案用來導出元件,提供元件的注冊方法:

Vue3元件庫打包指南,一次生成esm、esm-bundle、commonjs、umd四種格式元件的基本組成打包的整體流程打包成module和commonjs格式打包成esm-bundle打包成umd格式附錄:babel配置詳解

props.ts

檔案用來聲明元件的

props

類型:

Vue3元件庫打包指南,一次生成esm、esm-bundle、commonjs、umd四種格式元件的基本組成打包的整體流程打包成module和commonjs格式打包成esm-bundle打包成umd格式附錄:babel配置詳解

有的元件沒有使用

.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

Vue3元件庫打包指南,一次生成esm、esm-bundle、commonjs、umd四種格式元件的基本組成打包的整體流程打包成module和commonjs格式打包成esm-bundle打包成umd格式附錄:babel配置詳解

那麼會生成一個

ButtonSfc.less

,因為是

less

,是以同時也會再編譯生成一個

ButtonSfc.css

檔案,當然這兩個樣式檔案裡隻包括内聯在

Vue

單檔案中的樣式,不包括使用

@import

導入的樣式,是以生成的這兩個樣式檔案都是空的:

Vue3元件庫打包指南,一次生成esm、esm-bundle、commonjs、umd四種格式元件的基本組成打包的整體流程打包成module和commonjs格式打包成esm-bundle打包成umd格式附錄:babel配置詳解

編譯樣式塊使用的是

@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, '')
}
           
Vue3元件庫打包指南,一次生成esm、esm-bundle、commonjs、umd四種格式元件的基本組成打包的整體流程打包成module和commonjs格式打包成esm-bundle打包成umd格式附錄:babel配置詳解

到這裡,一共生成了四個檔案:

Vue3元件庫打包指南,一次生成esm、esm-bundle、commonjs、umd四種格式元件的基本組成打包的整體流程打包成module和commonjs格式打包成esm-bundle打包成umd格式附錄:babel配置詳解

編譯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

檔案:

Vue3元件庫打包指南,一次生成esm、esm-bundle、commonjs、umd四種格式元件的基本組成打包的整體流程打包成module和commonjs格式打包成esm-bundle打包成umd格式附錄:babel配置詳解

編譯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

Vue3元件庫打包指南,一次生成esm、esm-bundle、commonjs、umd四種格式元件的基本組成打包的整體流程打包成module和commonjs格式打包成esm-bundle打包成umd格式附錄:babel配置詳解

使用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

會在這個目錄下尋找配置檔案:

Vue3元件庫打包指南,一次生成esm、esm-bundle、commonjs、umd四種格式元件的基本組成打包的整體流程打包成module和commonjs格式打包成esm-bundle打包成umd格式附錄: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

檔案都已處理完畢:

Vue3元件庫打包指南,一次生成esm、esm-bundle、commonjs、umd四種格式元件的基本組成打包的整體流程打包成module和commonjs格式打包成esm-bundle打包成umd格式附錄:babel配置詳解

小節

到這裡,打包成

module

commonjs

格式就完成了,總結一下所做的事情:

  • less

    檔案直接使用

    less

    包編譯成同名的

    css

    檔案;
  • ts

    tsx

    等檔案使用

    babel

    編譯成

    js

    檔案;提取并去除其中的樣式導入語句,并将該樣式導入語句寫入單獨的檔案、修改

    .vue

    .ts

    等類型的導入語句來源為對應的編譯後的

    js

    路徑;
  • Vue

    單檔案使用

    @vue/compiler-sfc

    解析并對各個塊分别使用對應的函數進行編譯;每個

    style

    塊也會提取并去除其中的樣式導入語句,并将該導入語句寫入單獨的檔案,剩下的樣式内容會分别建立一個對應的樣式檔案,如果是

    less

    塊,同時會編譯并建立一個同名的

    css

    檔案;

    template

    的編譯結果會合并到

    script

    内,然後

    script

    的内容會重複上一步

    ts

    檔案的處理邏輯;
  • 所有元件都編譯完了,再動态建立整體的導出檔案,一共生成了四個檔案:
Vue3元件庫打包指南,一次生成esm、esm-bundle、commonjs、umd四種格式元件的基本組成打包的整體流程打包成module和commonjs格式打包成esm-bundle打包成umd格式附錄:babel配置詳解

打包成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/

目錄下生成兩個檔案:

Vue3元件庫打包指南,一次生成esm、esm-bundle、commonjs、umd四種格式元件的基本組成打包的整體流程打包成module和commonjs格式打包成esm-bundle打包成umd格式附錄:babel配置詳解

打包成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

産物的目錄。

最後再回顧一下這個打包順序:

Vue3元件庫打包指南,一次生成esm、esm-bundle、commonjs、umd四種格式元件的基本組成打包的整體流程打包成module和commonjs格式打包成esm-bundle打包成umd格式附錄:babel配置詳解

你會發現這個順序是有原因的,

ems-bundle

的打包入口依賴

module

的産物,

umd

打包會給

commonjs

複制一份樣式檔案,是以打包

umd

需要在

commonjs

後面。

附錄:babel配置詳解

上文編譯

script

ts

tsx

内容使用的是

babel

,提到了會使用本地的配置檔案:

Vue3元件庫打包指南,一次生成esm、esm-bundle、commonjs、umd四種格式元件的基本組成打包的整體流程打包成module和commonjs格式打包成esm-bundle打包成umd格式附錄:babel配置詳解

主要就是配置了一個

presets

presets

babel

的預設,作用是友善使用一些共享配置,可以簡單了解為包含了一組插件,

babel

的轉換是通過各種插件進行的,是以使用預設可以免去自己配置插件,可以使用本地的預設,也可以使用釋出在

npm

包裡的預設,預設可以傳遞參數,比如上圖,使用的是

@varlet/cli

包裡附帶的一個預設:

Vue3元件庫打包指南,一次生成esm、esm-bundle、commonjs、umd四種格式元件的基本組成打包的整體流程打包成module和commonjs格式打包成esm-bundle打包成umd格式附錄:babel配置詳解

預設其實就是一個

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

文法。

  • 插件在預設之前運作
  • 多個插件按從第一個到最後一個順序運作
  • 多個預設按從最後一個到第一個順序運作
// 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'],
    },
  ],
}))
           

繼續閱讀