我正在参加「掘金·启航计划」
项目需要在打包完成后生成几套主题色,以便在部署到不同服务器时,可以选择配置某一套主题。
这是以前的一个需求,当时是使用了一个webpack插件,具体包名忘记了,正好最近在学习vite,就尝试封装一个vite插件来解决这个问题。
开始之前,先简单了解一下vite插件开发
vite插件需要在相应的钩子函数注册需要操作的函数
option只会触发一次,替换或操作传递给 rollup 的选项对象,当返回 null 不会替换任何内容。buildStart在每个 rollup 构建时调用,一般用来仅仅访问传递给rollup的选项而不操作。resolveId可用于定义自定义的 id 路径解析器,用于定位依赖。load可用于定义自定义的模块解析 loader,比如可以直接返回一个已经转换好的 ast。transform也可以返回 ast,但在这个时候已经拿到了具体路径文件的 code,所以一般用于转换已经加载后的模块。configvite独有钩子,在解析 vite 配置前调用,可以再这里扩展或直接修改用户原始定义。configResolvedvite独有钩子,在解析 vite 配置后调用。使用这个钩子读取和存储最终解析的配置。configureServervite独有钩子,用于配置开发服务器的钩子,可在该 hook 访问开发服务器实例,可以用来添加自定义中间件拦截请求。transformIndexHtmlvite独有钩子,转换 index.html 的专用钩子。handleHotUpdatevite独有钩子,可用于自定义hmr更新处理。buildEnd构建完成时调用,可以拿到构建失败的错误信息。closeBundle可在这个时期清理任何可能正在运行的外部服务。
这个插件里我使用到了configResolved、transform、transformIndexHtml、buildEnd
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已经开源,有兴趣的可以体验一下