开发一个抽取主题包的vite插件

110 阅读3分钟

我正在参加「掘金·启航计划」

项目需要在打包完成后生成几套主题色,以便在部署到不同服务器时,可以选择配置某一套主题。

这是以前的一个需求,当时是使用了一个webpack插件,具体包名忘记了,正好最近在学习vite,就尝试封装一个vite插件来解决这个问题。

开始之前,先简单了解一下vite插件开发

vite插件需要在相应的钩子函数注册需要操作的函数

  • option 只会触发一次,替换或操作传递给 rollup 的选项对象,当返回 null 不会替换任何内容。
  • buildStart 在每个 rollup 构建时调用,一般用来仅仅访问传递给rollup的选项而不操作。
  • resolveId 可用于定义自定义的 id 路径解析器,用于定位依赖。
  • load 可用于定义自定义的模块解析 loader,比如可以直接返回一个已经转换好的 ast。
  • transform 也可以返回 ast,但在这个时候已经拿到了具体路径文件的 code,所以一般用于转换已经加载后的模块。
  • config vite独有钩子,在解析 vite 配置前调用,可以再这里扩展或直接修改用户原始定义。
  • configResolved vite独有钩子,在解析 vite 配置后调用。使用这个钩子读取和存储最终解析的配置。
  • configureServer vite独有钩子,用于配置开发服务器的钩子,可在该 hook 访问开发服务器实例,可以用来添加自定义中间件拦截请求。
  • transformIndexHtml vite独有钩子,转换 index.html 的专用钩子。
  • handleHotUpdate vite独有钩子,可用于自定义hmr更新处理。
  • buildEnd 构建完成时调用,可以拿到构建失败的错误信息。
  • closeBundle 可在这个时期清理任何可能正在运行的外部服务。

这个插件里我使用到了configResolvedtransformtransformIndexHtmlbuildEnd

configResolved

判断当前环境,只在打包的时候执行文件输出

name: 'vite:color',
configResolved(config: ResolvedConfig) {
  isProd = config.command === 'build'
},

transform

  • 对加载的资源进行更改
  • 判断当前依赖是否为样式文件(css、less、scss、styl)
  • 根据用户传入的transform修改代码片段
  • 设置缓存
  • 如果是生产模式下,根据规则提取代码到输出文件列表中
async transform(code: string, id: string) {
  if (isTargetFile(id)) {
    if (cache.has(id)) {
      return { code: cache.get(id), map: null }
    }
    const sourceCode = transformCode(code)
    cache.set(id, sourceCode)
    if (isProd) extractCode(code)
    return { code: sourceCode, map: null }
  }
},

buildEnd

  • 主要针对生产模式下,输出主题包资源
  • 遍历输出文件列表
  • 处理外部引入的资源,如cdn
  • 压缩代码
  • 输出资源
if (isProd) {
    // 遍历即将输出的资源
    for await (const file of outputFiles) {
      // 处理外部引入
      if (file.external?.length) {
        for await (const url of file.external) {
          if (LINK_CSS_REG.test(url)) {
            try {
              let code:string | unknown
              if (cache.has(url)) {
                code = cache.get(url)
              } else {
                console.log(chalk.cyan('\n🛠️ [vite-plugin-color]') + `- extracting external css from ${url}...`)
                code = await fetchFile(url)
                cache.set(url, code as string)
              }

              if (typeof code === 'string') {
                const extractCode = extractColor(file.extractRegs)(code).join('')
                file.code += file.transform ? `${file.transform(extractCode)}` : extractCode
              } else throw new Error('error for fetch external css')
            } catch (error) {
              console.error(chalk.cyan('❌ [vite-plugin-color]') + ` - ${error}`)
            }
          }
        }
      }

      // 压缩代码
      if (file.minify !== false) {
        file.code = await minifyCSS(file.code, Object.assign({ returnPromise: true }, file.minifyOptions))
      }

      // 输出主题包
      // @ts-ignore
      this.emitFile({
        type: 'asset',
        name: file.output,
        fileName: file.output,
        source: file.code,
      })
    }
    console.log(chalk.cyan('✨ [vite-plugin-color]') + ` - successfully!\n`)
    // 清除缓存
    cache.clear()
  }

transformIndexHtml

  • 自动将输出的主题包注入index.html
transformIndexHtml() {
  if (isProd) {
    const injectTags = outputFiles.filter(item => item.injectTo).map(item => {
      if (typeof item.injectTo === 'object') return item.injectTo
      return {
        tag: 'link',
        attrs: {
          rel: 'stylesheet',
          href: `./${item.output}`,
        },
        injectTo: item.injectTo,
      }
    })

    return injectTags
  }
},

核心代码

import extractColor from './extract'
import { HtmlTagDescriptor, ResolvedConfig } from 'vite'
import { minifyCSS, patchReg, isTargetFile, formatOption } from './utils'
import { propType, optionType } from './types'
import { LINK_CSS_REG } from './contants'
import { fetchFile } from './utils'
import chalk from 'chalk'

export { propType, optionType, HtmlTagDescriptor }

export default (options: optionType) => {
  options = formatOption(options)

  const transformCode = (code: string) => (options as Array<propType>).reduce((pre, curr) => curr.transform ? curr.transform(pre) : pre, code)
  const extractRegs = (extract: string[]) => extract.map(patchReg)
  const extractCode = (code: string) => {
    outputFiles.forEach(file => {
      const extractCode = extractColor(file.extractRegs)(code).join('')
      file.code += file.transform ? `${file.transform(extractCode)}` : extractCode
    })
  }

  let isProd: boolean
  let cache: Map<string, string> = new Map()
  const outputFiles = options.filter(item => item.output).map(item => ({
    ...item,
    extractRegs: extractRegs(item.extract),
    code: '',
  }))

  return {
    name: 'vite:color',
    configResolved(config: ResolvedConfig) {
      isProd = config.command === 'build'
    },
    async transform(code: string, id: string) {
      if (isTargetFile(id)) {
        if (cache.has(id)) {
          return { code: cache.get(id), map: null }
        }
        const sourceCode = transformCode(code)
        cache.set(id, sourceCode)
        if (isProd) extractCode(code)
        return { code: sourceCode, map: null }
      }
    },
    async buildEnd() {
      if (isProd) {
        for await (const file of outputFiles) {
          if (file.external?.length) {
            for await (const url of file.external) {
              if (LINK_CSS_REG.test(url)) {
                try {
                  let code:string | unknown
                  if (cache.has(url)) {
                    code = cache.get(url)
                  } else {
                    console.log(chalk.cyan('\n🛠️ [vite-plugin-color]') + `- extracting external css from ${url}...`)
                    code = await fetchFile(url)
                    cache.set(url, code as string)
                  }
  
                  if (typeof code === 'string') {
                    const extractCode = extractColor(file.extractRegs)(code).join('')
                    file.code += file.transform ? `${file.transform(extractCode)}` : extractCode
                  } else throw new Error('error for fetch external css')
                } catch (error) {
                  console.error(chalk.cyan('❌ [vite-plugin-color]') + ` - ${error}`)
                }
              }
            }
          }

          if (file.minify !== false) {
            file.code = await minifyCSS(file.code, Object.assign({ returnPromise: true }, file.minifyOptions))
          }

          // @ts-ignore
          this.emitFile({
            type: 'asset',
            name: file.output,
            fileName: file.output,
            source: file.code,
          })
        }
        console.log(chalk.cyan('✨ [vite-plugin-color]') + ` - successfully!\n`)
        cache.clear()
      }
    },
    transformIndexHtml() {
      if (isProd) {
        const injectTags = outputFiles.filter(item => item.injectTo).map(item => {
          if (typeof item.injectTo === 'object') return item.injectTo
          return {
            tag: 'link',
            attrs: {
              rel: 'stylesheet',
              href: `./${item.output}`,
            },
            injectTo: item.injectTo,
          }
        })

        return injectTags
      }
    },
  }
}

开源

vite-plugin-color已经开源,有兴趣的可以体验一下

vite-plugin-color