目录

---------

前置知识

命名规范(来自官方)

正如 rollup 中的插件名称约定一样,vite 插件也需要一定的命名规范:

  1. 名字应当带有 vite-plugin- 前缀
  2. package.json 中包含 vite-plugin 关键字
  3. 在插件文档中增加一部分关于本插件是一个 Vite 专属插件的详细说明

如果你的插件只适用于特定的框架,它的名字应该遵循以下前缀格式:

external 插件

该插件要达到的效果是(以 jQuery 为例):

html
PlainText
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.js"></script>

这时候全局 window 对象就有 jQuery 或者 $ 属性了

虽然我们没有安装 jquery 依赖,但可以写 vite 插件实现安装的效果

Vue
<script lang="ts">
import jquery from 'jquery'
console.log(jquery)
</script>

语法会报错,但是能成功打印在浏览器控制台上

resolveId

resolveId 能够接受每个文件的模块引用关系,例如以下:

Tip: vite 规定当第一个插件中的 resolveId 执行之后,那么后面的 resolveId 都不会执行(官方源码内置的插件也有 resolveId),所以我们需要加上 enforce: 'pre' 告诉 vite 此插件的优先级在所有插件前面

Typscript
// main.ts
import App from './App'

// HelloWorld.vue
import { ref } from 'vue'

// vite.config.ts
export default defineConfig({
  plugins: [
    vue(),
    {
      name: 'vite-plugin-xxx',
      enforce: 'pre',
      resolveId(source) {
        console.log(source)
        // ./App
        // vue
      }
    }
  ],
})

resolveId 还可以返回一个值,用于下一级处理,返回的值可以加 \0 表示只被当前插件识别处理,其他插件不会处理

Typscript
// vite.config.ts
export default defineConfig({
  plugins: [
    vue(),
    {
      name: 'vite-plugin-xxx',
      enforce: 'pre',
      resolveId(source) {
        return '\0' + source
      }
    }
  ],
})

load

上面我们讲过 resolveId 可以返回一个字符串,我们可以用 load 来获取:

Typscript
// vite.config.ts
export default defineConfig({
  plugins: [
    vue(),
    {
      name: 'vite-plugin-xxx',
      enforce: 'pre',
      resolveId(source) {
        return '\0' + source
      },
      load(id) {
        console.log(id)
        // \0./App
        // \0vue
      }
    }
  ],
})

load 也可以返回一个字符串,该字符串表示我们该如何处理用户的导入模块

具体实现

Typscript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugin: [
    vue(),
    {
      name: 'vite-plugin-external',
      enforce: 'pre',
      resolveId(source) {
        if(source === 'jquery') {
          return '\0' + source
        }
      },
      load(id) {
        if(id === '\0jquery') {
          // 虚拟模块
          // 表示当我们 import $ from 'jquery' 时,实际上的 `jquery` 模块内容是 export default window.jQuery
          return `export default window.jQuery`
        }
      }
    }
  ]
})

antd 按需引入插件

效果如下:

Typscript
- import { Button } from 'antd'
+ import Button from 'antd/es/button'

大致的原理通过 ast 语法树来替换,例如以下代码:

Typscript
import { Button } from 'ant-design-vue'

转换后的 ast 语法树(重点关注红色框中的内容):

transform

transform 可以对文件的源码进行操作,我们利用此函数将 import { Button } from 'antd' 改为 import Button from 'antd/es/button'

transform 内部包含 parse 转换 ast 函数

Typscript
export default defineConfig({
  plugins: [
  	{
      name: 'vite-plugin-demand-import',
      // code 表示源码,id 跟文件名有关
      transform(code, id) {
        // 使用内置的 parse 转换 ast 语法树
        const ast = this.parse(code)
      }
    }
  ]
})

treeWalk 函数

  1. 首先定义 treeWalk 函数
Typscript
function treeWalk(
	ast: Record<string, any> | Array<Record<string, any>>,
  visitor: { [type: string]: (ast: Record<string, any>) => void }
) {
	for(const node of Object.values(ast)) {
    if(node && typeof node === 'object') {
      // ?. 语法
      visitor[node.type]?.(node)
      treeWalk(node, visitor)
    }
  }
}

上述函数中出现了 ?. 语法,这表示如果 visitor[node.type] 不是函数,那么就不执行

  1. treeWalk 函数的使用

例如我们想要 ast 中的 ImportDeclaration 属性:

Typscript
treeWalk(ast, {
  ImportDeclaration(node) {
    // node 就是 ImportDeclaration 中的内容
  }
})

具体实现

Typscript
transform(code, id) {
  const ast = this.parse(code)
  // 从 antd 导入的成员集合
  const antdImports: string[] = []
  // 使用 start 和 end 定位代码块
  let start = 0
  let end = -1
  // 如果文件名以 .vue 结尾
  if (id.endsWith('.vue')) {
    treeWalk(ast, {
      ImportDeclaration(node) {
        // .vue 文件中导入了 ant-design-vue 模块
        if (node.source.value === 'ant-design-vue') {
          // 以下两个 if 代码待会解释
          if (!start)  start = node.start
          if (end)  end = node.end
          // 循环获得 import 语句内容
          for (const spec of node.specifiers) {
            // import 语句的设计代码符合 ant-design-vue 构建后的目录结构
            antdImports.push(`import ${spec.local.name} from 'ant-design-vue/es/${spec.local.name.toLowerCase()}'`)
          }
        }
      },
    })
    // 如果数组不为空,那么就将转换后的代码返回
    if (antdImports.length) {
      return (
        code.slice(0, start) + // import antd 前面的部分
        antdImports.join('\n') + // 转换后的部分
        code.slice(end) // import antd 后面的部分
      )
    }
  }
},

为什么会有两个 if 判断呢?我们导入 antd 成员时,一般这样:

Typscript
import { Button, Table } from 'ant-design-vue'

但有的用户会这样:

Typscript
import { Button } from 'ant-design-vue'
import { Table } from 'ant-design-vue'

虽然这样会被某些 eslint 规则报错,但为了完善,还是加上了,另外,此插件不支持在两个 antd 导入模块中导入新的模块,例如下面代码会报错的:

Typscript
import { Button } from 'ant-design-vue'
import { ref } from 'ref'
import { Table } from 'ant-design-vue'

补充: configResolved

使用 configResolved 可以看到配置信息,例如打印所有 plugins 的名字:

Typscript
export default defineConfig({
  plugins: [
		{
			name: 'vite-plugin-test',
      configResolved(config) {
        console.log(config.plugins.map(p => p.name))
      }
		}
	]
})