likes
comments
collection
share

手把手教学编写vite插件定制化业务开发前言 在这篇文章,我将向大家讲述一个使用vite编写插件完成自己公司的业务开发,

作者站长头像
站长
· 阅读数 17

前言

在这篇文章,我将向大家讲述一个使用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的插件库里面有这个类似的插件,只不过我当时并不知道,就自己重复造了一个轮子,不过这个轮子完全是基于我们公司业务的,所以对我来说的话,更加的自由。

这是那个插件的项目地址:xiaweiss/vite-plugin-ali-oss: Upload the production files bundled in the project to Ali OSS, except for html (github.com)

1、实现思路

在上传的过程中,我们就不要去考虑解析代码中的资源了,因为既有css、又有js、还有其它多媒体资源(比如图片)。

我们可以采用一个投机倒把的方式,vite可以配置一个叫做publicDir的属性(即Webpack的publicPath) 手把手教学编写vite插件定制化业务开发前言 在这篇文章,我将向大家讲述一个使用vite编写插件完成自己公司的业务开发, 共享选项 | 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:外部资源链接元素

link有2种预加载方式,分别是preloadprefetch,关于这两个预加载的方式,在网红讲师袁进的短视频里面就已经提到过。

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的这种标记位置,一会儿那些生成的预加载的标签才能放到这个位置上。

手把手教学编写vite插件定制化业务开发前言 在这篇文章,我将向大家讲述一个使用vite编写插件完成自己公司的业务开发, 于是我就参考nuxt的那种思路,给我的模板文件上加一个注释节点,一会儿替换掉这个注释节点即可。

3、代码实现

因为我在实践中vite生命周期并没有按照我预期的那样执行,所以,我在阿里云OSS上传插件处理的过程中对外部抛出一个事件,然后在自动插入预加载标签的插件里面设置监听,最终完成处理。

所以,对我之前的上传插件进行了一些改造。 手把手教学编写vite插件定制化业务开发前言 在这篇文章,我将向大家讲述一个使用vite编写插件完成自己公司的业务开发, 我们目前的约定是凡是以 __link 结尾的资源,都做这个处理。

收集完成之后,我们在把这部分的数据通知给别的插件。 手把手教学编写vite插件定制化业务开发前言 在这篇文章,我将向大家讲述一个使用vite编写插件完成自己公司的业务开发,

然后在插件里面读取内容,写入最终的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插件定制化业务开发前言 在这篇文章,我将向大家讲述一个使用vite编写插件完成自己公司的业务开发,

总结

大家不要把vite插件看的有多么神秘,它只不过是在项目的构建处理阶段,利用各个生命周期,改写了产物的特征,从而智能的实现了业务,所以如果你对这方面有兴趣的话,第一个最重要的先决条件是一定要熟练的掌握NodeJS的处理

对于在不同的生命周期内可以做什么事儿,大家可以通过查看开源项目就可以积累到一定的经验,因为这部分内容很多人都还没有探索到,属于前端深水区的内容,要想继续进步的话,就只能查看前端先驱们的代码了。

关于vite的插件,后续还会有文章向大家展示更加有意思的内容。

本文是自己通过阅读开源项目的源码结合自己的一些项目需求所著,由于笔者水平有限,如果文中有任何纰漏或者错误还请大家指出,谢谢。

转载自:https://juejin.cn/post/7410347333647040538
评论
请登录