手把手教学编写vite插件定制化业务开发前言 在这篇文章,我将向大家讲述一个使用vite编写插件完成自己公司的业务开发,
前言
在这篇文章,我将向大家讲述一个使用vite编写插件完成自己公司的业务开发,大家可以通过学习这个例子,从而掌握vite的插件开发。
首先,向大家描述一下公司的业务场景,我们公司的前端页面,静态资源都存放在阿里云的OSS存储中,因为这个OSS存储提供了CDN的能力,可以使得在大部分地方访问速度都差不多,还有一个原因是,我们的前端页面主要部署的是Html静态文件,项目依托Docker进行部署,而如果把静态资源打包到Docker镜像中,会随着内容的不断增多导致Docker镜像的膨胀,久而久之,可能构建速度就会越来越慢。因此,当vite构建完成之后,首要的任务就是把这些资源上传到阿里云OSS存储容器中。
在阅读这篇文章之前,希望你对vite的构建流程有一定的认知,并且已经阅读过我的上一篇阐述vite-vue-plugin
的文章。
Vite插件的前置知识点
插件 API | Vite 官方中文文档 (vitejs.cn)
Vite 插件 是 Rollup 插件接口的一种扩展。查看 Rollup 插件兼容性章节 获取更多信息。
Vite插件跟Rollup插件的关系,就跟TS与JS的关系是一样的,Vite插件是Rollup的超集。
如果插件不使用 Vite 特有的钩子,可以作为 兼容 Rollup 的插件 来实现,推荐使用 Rollup 插件名称约定。
- Rollup 插件应该有一个带
rollup-plugin-
前缀、语义清晰的名称。- 在 package.json 中包含
rollup-plugin
和vite-plugin
关键字。这样,插件也可以用于纯 Rollup 或基于 WMR 的项目。
对于 Vite 专属的插件:
- Vite 插件应该有一个带
vite-plugin-
前缀、语义清晰的名称。- 在 package.json 中包含
vite-plugin
关键字。- 在插件文档增加一部分关于为什么本插件是一个 Vite 专属插件的详细说明(如,本插件使用了 Vite 特有的插件钩子)。
Vite插件是一个函数,这个函数需要返回一个对象,关于这个对象是什么样子的,大家可以从Vite的类型定义PluginOption
看一下它支持的属性和方法。
一个 Vite 插件可以额外指定一个
enforce
属性(类似于 webpack 加载器)来调整它的应用顺序。enforce
的值可以是pre
或post
。解析后的插件将按照以下顺序排列:
- Alias
- 带有
enforce: 'pre'
的用户插件- Vite 核心插件
- 没有 enforce 值的用户插件
- Vite 构建用的插件
- 带有
enforce: 'post'
的用户插件- Vite 后置构建插件(最小化,manifest,报告)
为什么vite原生支持处理css、scss、less这样的文件,就是因为vite自己内置了一些插件来处理这些内容。
最后,还有一个apply
字段可以控制这个插件的生效时机,比如下文中,我们要提到的阿里云OSS上传插件就是一个只会在build阶段才会生效的插件。
function myPlugin() {
return {
name: 'vite-plugin-build-only-demo',
// 指定只会在构建过程中才会生效的插件
apply: 'build' // 或 'serve'
}
}
阿里云OSS上传插件
其实vite的插件库里面有这个类似的插件,只不过我当时并不知道,就自己重复造了一个轮子,不过这个轮子完全是基于我们公司业务的,所以对我来说的话,更加的自由。
1、实现思路
在上传的过程中,我们就不要去考虑解析代码中的资源了,因为既有css、又有js、还有其它多媒体资源(比如图片)。
我们可以采用一个投机倒把的方式,vite可以配置一个叫做publicDir
的属性(即Webpack的publicPath)
共享选项 | Vite 官方中文文档 (vitejs.cn)
我只要把这些文件按照对应的规则传到阿里云OSS的服务上去,反正最终html文件中能够引用到这个资源就可以了,有了这个思路之后,我们的方向就明确了,设置vite的publicDir的配置,在项目构建完成之后,把构建的产物除了html以外的所有内容按照一定的规则传到阿里云OSS中。
沿着这个思路,我们找到了一个Rollup的生命周期:closeBundle
插件开发 | Rollup 中文文档 (rollupjs.org)
2、编码实现
在理清楚思路以后,剩下的内容就都是NodeJS的知识点了,如果对NodeJS不熟悉的同学,可以自行查阅相应的资料进行学习。
主要的实现思路就是获取到所有的文件,然后我为了方便资源查找,我对其进行了归类,最终可以在一个统一的OSS目录里面管理,出了问题好排查,然后把文件通过gzip压缩,最后通过阿里云OSS提供的API上传到桶中,然后再把已上传的文件删除掉就好。
function uploadResource(params: { baseURL: string; cwd: string }): PluginOption {
const { baseURL, cwd } = params
return {
name: 'vite-plugin-activity-upload-to-resource-aliyun-oss',
apply: 'build',
async closeBundle() {
const ossClient = new OSS({
accessKeyId: OSS_CONFIG.accessKeyId,
accessKeySecret: OSS_CONFIG.accessKeySecret,
bucket: OSS_CONFIG.bucket,
endpoint: OSS_CONFIG.endpoint,
})
let contentType
const charsetMimes: Record<string, string> = {
'.js': 'utf-8',
'.css': 'utf-8',
'.html': 'utf-8',
'.htm': 'utf-8',
'.svg': 'utf-8',
}
const gzipMimes: Record<string, number> = {
'.plist': 6,
'.html': 6,
'.htm': 6,
'.js': 6,
'.css': 6,
'.svg': 6,
}
// 根据项目配置获取产物的目录
const outputDistDir = getActivityOutputPath(cwd)
// 如果dist目录不存在,先创建一个目录
if (!existsSync(outputDistDir)) {
mkdirsSync(outputDistDir)
}
// 解析到项目中除了html以外的文件
const files = await readDir(outputDistDir)
// eslint-disable-next-line no-restricted-syntax
for (const filePath of files) {
let content = readFileSync(filePath)
const ext = extname(filePath)
contentType = mime.getType(ext) || 'application/octet-stream'
if (charsetMimes[ext]) {
contentType += '; charset=' + charsetMimes[ext]
}
const withoutCdnHostBaseUrl = baseURL.replace(OSS_CONFIG.urlPrefix, '')
const fileName = basename(filePath)
const fileExtName = extname(fileName)
const regExp = new RegExp(`${fileExtName}$`)
const fileNameWithoutExt = fileName.replace(regExp, '')
// 根据rollup配置的策略读取上传的文件前缀
const extPath = getExtPathStrategy(filePath)
const key = `${withoutCdnHostBaseUrl}/${extPath}/${fileName}`
const cdnResourcePath = OSS_CONFIG.urlPrefix + key
const headers: Record<string, unknown> = {
'Access-Control-Allow-Origin': '*',
'Content-Type': contentType,
'Cache-Control': 'max-age=315360000',
Expires: dayjs().add(10, 'years').toDate().toUTCString(),
}
if (gzipMimes[ext]) {
headers['Content-Encoding'] = 'gzip'
content = Buffer.from(
pako.gzip(content, {
level: gzipMimes[ext] as any,
})
)
}
const resp = await ossClient.put(key, content, { headers })
if (resp) {
logger.success(`文件${fileName}上传完成!`)
unlink(filePath)
}
}
// 删除空文件夹
removeEmptyDir(outputDistDir)
},
}
}
上述代码都是我的业务代码,我也没有贴全,如果对此有兴趣的同学可以私聊我获取(OSS的key和secret是不可能给你的喔,否则我要提桶跑路了,😄)。
文件上传过程中需要用到gzip
压缩,我使用NodeJS的原生模块zlib
试了很久也没有成功,所以最终就采用的是pako
。
有的同学看到这儿可能觉得看了一个寂寞,明明在讲Vite插件,怎么给搞到NodeJS了,我可没有忽悠大家,编写一个Vite插件就是这么简单,只不过大家平时并没有查看源码的习惯,所以觉得很高大上。
自动插入预加载资源标签插件
1、什么是图片预加载?
这是在性能优化章节里面比较重要的一个环节,图片预加载就是在首屏渲染的时候,如果某个图片是页面渲染就需要的话,那么就有预加载的必要了。
采用了图片预加载的手段之后,能够显著的提升前端页面的FCP(First Contentful Pain) 性能指标,对于我的项目来说,主要是H5营销页面,我们的首屏几乎都是些比较大,占屏幕很大位置的一个图,而这种资源还影响了项目的LCP(Largest Contentful Paint) 性能指标。
link有2种预加载方式,分别是preload
和prefetch
,关于这两个预加载的方式,在网红讲师袁进的短视频里面就已经提到过。
preload
预加载页面渲染就需要用到内容,prefetch
预加载页面要用到的内容,但是不是渲染时立刻就要用到的内容,这个跟Vue或者React的路由懒加载思路是一样的。
像一些视频网站,首页的banner用到的视频用到的就是这种技术,比如Youtube,爱奇艺,B站。
举个🌰:
<link
rel="preload"
href="https://xxx.png"
as="image"
type="image/png"
crossorigin="anonymous" />
2、实现思路
这个插件,我们要借用一下UmiJS所提到的约定大于配置的观点。
为什么我要这样设计呢?首先,如果引入一个配置的话,到时候我的脚手架需要去解析这个配置,而且,我还需要根据资源的名称去做映射,不同的资源名称可能相同,还需要根据绝对路径进行处理。然后引入多余的配置的话,项目也会多出来配置文件,表现形式上不够直接。
如果用一个约定,就可以在构建时自动根据资源的名称决定是否插入预加载标签。
我们在之前的阿里云OSS上传插件中,完成上传时,可以把这种带特征名称的资源搜集起来,然后把这些资源存下来,在预加载插件执行的时候读取这些资源完成插入。
不过,我们需要稍微的修改一下模板index.html文件,也就是说要给这个文件上留一个类似slot的这种标记位置,一会儿那些生成的预加载的标签才能放到这个位置上。
于是我就参考nuxt的那种思路,给我的模板文件上加一个注释节点,一会儿替换掉这个注释节点即可。
3、代码实现
因为我在实践中vite生命周期并没有按照我预期的那样执行,所以,我在阿里云OSS上传插件处理的过程中对外部抛出一个事件,然后在自动插入预加载标签的插件里面设置监听,最终完成处理。
所以,对我之前的上传插件进行了一些改造。 我们目前的约定是凡是以 __link 结尾的资源,都做这个处理。
收集完成之后,我们在把这部分的数据通知给别的插件。
然后在插件里面读取内容,写入最终的html文件:
/**
* 根据构建之后的产物,自动将优化的link插入到html中
*/
function autoOptimizeAssets(params: { baseURL: string; cwd: string }): PluginOption {
const { cwd } = params
return {
name: 'vite-plugin-activity-assets-optimize-processor',
apply: 'build',
enforce: 'post',
async closeBundle() {
return new Promise((resolvePromise) => {
$event.on('finish-upload-resource', (resourceList: string[]) => {
if (resourceList.length === 0) {
logger.success('=====================无预加载资源处理=====================')
return
}
const htmlPath = resolve(cwd, 'dist/index.html')
let htmlContent = readFileSync(htmlPath, 'utf-8')
const results = resourceList
.map((src) => {
const ext = extname(src).replace(/^\./, '')
const linkTag = `<link as="image" rel="preload" href="${src}" type="image/${ext}" />`
return linkTag
})
.join('\n')
htmlContent = htmlContent.replace(/<!--\s*link\s+outlet\s*-->/i, results)
writeFileSync(htmlPath, htmlContent)
logger.success('=====================已成功处理预加载资源=====================')
resolvePromise()
})
})
},
}
}
我上述的资源只考虑了图片,如果你需要支持所有的资源类型的话,那么,在之前上传的过程中,最好把mime-type
解析到,也一并传过来,具体需要就具体对待了,反正我们目前还不需要。
最终效果就是产物里面就已经自动插入了预期的预加载标签了:
总结
大家不要把vite插件看的有多么神秘,它只不过是在项目的构建处理阶段,利用各个生命周期,改写了产物的特征,从而智能的实现了业务,所以如果你对这方面有兴趣的话,第一个最重要的先决条件是一定要熟练的掌握NodeJS的处理。
对于在不同的生命周期内可以做什么事儿,大家可以通过查看开源项目就可以积累到一定的经验,因为这部分内容很多人都还没有探索到,属于前端深水区的内容,要想继续进步的话,就只能查看前端先驱们的代码了。
关于vite的插件,后续还会有文章向大家展示更加有意思的内容。
本文是自己通过阅读开源项目的源码结合自己的一些项目需求所著,由于笔者水平有限,如果文中有任何纰漏或者错误还请大家指出,谢谢。
转载自:https://juejin.cn/post/7410347333647040538