关于Vite插件的内容

兼容性注意:

Vite 需要 Node.js 版本 18+,20+。然而,有些模板需要依赖更高的 Node 版本才能正常运行,当你的包管理器发出警告时,请注意升级你的 Node 版本。

背景

Vite 使用 Rollup 来打包构建你的生产环境代码。

注意,开发环境下 Vite 是并不需要打包这个过程的,打包这个动作由浏览器来接管, Vite 仅需在浏览器请求源码时以 ESM 方式提供源码即可。

那么,为什么 Vite 要选择 Rollup 来构建生产环境代码呢❓

两方面原因:

  • 其一,利用 Rollup 现有的基础建设与周边生态。
  • 其二, Rollup 拥有设计出色且灵活的插件API

🔉这里得提一下 EsBuild ,相信大家对它也不陌生,它在 Vite 中的重要性一点也不亚于 Rollup ,两者是 Vite 的双引擎架构的重要扮演者。

简单总结一下 EsBuildVite 中的作用:

  • 依赖预构建,EsBuild 会在开发阶段帮忙完成第三方依赖的预构建,这也是 Vite 启动快的一个重要原因。
    (关于依赖预构建的具体目的:传送门
  • 单文件编译,将 .jsx/.ts/.tsx 文件转译成 js ,这部分能力在开发与生产环境都得到应用。
  • 代码压缩,Vite 从2.6版本开始,默认使用 EsBuild 来进行生产环境代码的压缩。

这里说了一下 EsBuild,主要是想表明 EsBuildVite 构建项目代码整个过程中基本都参与了,不管是开发环境还是生产环境的构建,不管是打包过程还是转译过程,总之 EsbuildVite 构建的重要性能利器。

但是❗ EsBuild 这么好,为何不直接选用它打包❓

主要原因还是 Vite 目前的插件API 与使用 esbuild 作为打包器并不兼容。

具体原因官方有解释:点我点我😃

如何开发开发一个Vite插件呢?

首先,先去看看文档 👉👉👉 传送门

文档太复杂?没关系,咱们直接看总结。😉

如果你熟悉 Rollup 并开发过它的插件,那么等于你已经会开发 Vite 插件,因为 Vite 是在 Rollup 插件API上扩展的,然后带有一些 Vite 独有的配置项。

由此 Vite 插件可以划分成:兼容Rollup插件Vite专属插件 两大类。

其次,再来看看语法:

javascript 复制代码
// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({ 
  plugins: [
    {
      name: "给你的插件取个名字", // 必填
      // 13个生命周期钩子
      options() {},
      buildStart() {},
      ...
    }
  ],
})

Vite 插件语法非常简单,一个对象就是一个插件,对象 name 属性要求必填,对于 name 属性的命名规则,可以遵循官方的一些约定。传送门

除了 name 属性,剩下的就只要找到对应的生命周期钩子编写相应逻辑即可。

13个生命周期钩子

Vite 插件的生命周期钩子可以划分成两类,其一是共享的通用钩子,其二是 Vite 独有钩子。

7个共享的通用钩子:

  • options:在服务器启动时被调用,在此钩子中可以替换或者操作 rollup 的配置。
    (options: InputOptions) => InputOptions | null

  • buildStart:在服务器启动时被调用,在此钩子中可以访问到 rollup 构建前的最终配置,但不可再修改。
    (options: InputOptions) => void

  • resolveId:在每个传入模块请求时 被调用,此钩子有一个特性,多个插件拥有此钩子,一旦前面插件中该钩子有了结果之后,后续其他插件中该钩子就不会被执行,这种特性的钩子在 Webpack 中被称为"垄断钩子"(下面会仔细解释)。
    (source: string, ...) => ResolveIdResult

  • load:在解析每个模块时调用,它经常和 resolveId 成对出现,配合使用,它用来加载当前处理的模块文件,主要可以用来指定某个 import 语句加载特定模块,它也属于一个垄断钩子。
    (id: string) => LoadResult

  • transform:在解析每个模块时调用,它可以被用来转换单个模块,也就是能将源码进行转换,输出转换后我们期望的代码,这是最重要的一个钩子❗
    (code: string, id: string) => TransformResult

  • buildEnd:在服务器关闭时被调用,此时 rollup 已经完成产物,但还未写出到本地磁盘中。
    (error?: Error) => void

  • closeBundle:在服务器关闭时被调用,可用于清理可能正在运行的任何外部服务。
    closeBundle: () => Promise<void> | void

6个独有钩子:

  • handleHotUpdate:执行自定义 HMR 更新处理,也就是你能根据自己的需要改变 HMR 的更新行为。
    (ctx: HmrContext) => Array<ModuleNode> | void | Promise<Array<ModuleNode> | void>

  • config:在解析 vite 配置前调用,此钩子能接收到开发者对 vite 的所有配置,可以在此进行自定义配置。
    (config: UserConfig, env: { mode: string, command: string }) => UserConfig | null | void

  • configResolved:在解析 vite 配置后调用,可以获取到最终合并好的配置信息,不可在此钩子中再去修改配置,只能根据配置去运行不同的事情。
    (config: ResolvedConfig) => void | Promise<void>

  • configureServer:用于配置开发服务器的钩子。
    (server: ViteDevServer) => (() => void) | void | Promise<(() => void) | void>

  • configurepreviewserver:用于配置预览服务器的钩子。
    (server: PreviewServer) => (() => void) | void | Promise<(() => void) | void>

  • transformindexhtml:转换 index.html 的专用钩子,经常用于替换 .html 文件中需要动态生成的变量。
    IndexHtmlTransformHook | { order?: 'pre' | 'post', handler: IndexHtmlTransformHook }

Vite官网有稍微介绍了一下这13个钩子,但是想细致了解7个 共享的通用钩子得去Rollup官网

了解完语法后,基本就需要尝试上手去开发第一个插件,实践才能出真理。✅

vite-plugin-externals

问题❓

先来创建一个测试项目,任何技术栈都可以,只要有 vite 即可:

javascript 复制代码
npm create vite@latest your-project-name -- --template vue-ts

安装 axios 作为测试对象:

javascript 复制代码
npm install axios -S

main.ts 中使用:

javascript 复制代码
import { createApp } from "vue";
import './style.css';
import App from "./App.vue";

import axios from "axios";
console.log(axios);

createApp(App).mount("#app");

启动项目,在浏览器控制台你应该能看到打印:

然后呢❓这是打算做什么呢❓

不着急,再来。

先卸载 axios

javascript 复制代码
npm uninstall axios

重启项目,此时,控制台应该会开始报错了。

该报错通过 vite:import-analysis 被分析了出来。

然后在 index.html 文件中通过 CDN 的形式引入 axios

javascript 复制代码
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Vite插件</title>
  <script src="https://unpkg.com/axios@1.6.4/dist/axios.min.js"></script>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>

报错一样还在,因为通过 CDN 引入的 axios 模块是挂载在 window 上的, import axios from 'axios' 并不能准确找到它。

而我们这次就是要编写一个 Vite 插件来解决这个报错,小编将该插件命名为 vite-plugin-externals

解决❗

进入 vite.config.js 文件,定义插件:

javascript 复制代码
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [
    vue(),
    {
      name: "vite-plugin-externals", // 必填
    },
  ],
});

必填的 name 属性安排上,然后就要找找需要使用到哪些生命周期钩子了。

这里我们本质是要改变 import axios from 'axios 语句导入模块的行为,从13个生命周期钩子能找到 resolveIdload 是符合我们本次需求的。

怎么才能知道自己需要使用什么钩子呢?小编只能告诉多练多写,熟了之后你就会和使用 Vue 的生命周期钩子一样了,就那么顺滑。👻👻👻

由于该插件的代码量不多,我们直接贴上来看:

javascript 复制代码
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [
    vue(),
    {
      name: "vite-plugin-externals",
      resolveId(source) {
        if (source === "axios")  return "\0" + source;
      },
      load(id) {
        if (id === "\0axios") return "export default window.axios";
      },
    },
  ],
});

加完插件后,此时,你的项目应该又一切正常了。🥳

\0{id} :这是一个来自 rollup 生态的人为约定,Vite 同样也遵循这一约定,它规定使用了虚拟模块的插件在解析时应该将模块 ID 加上前缀 \0

Vite解释 Rollup解释

啥意思呢❓

你可以认为像 axios 加上了前缀后,便不会被任何插件处理了。你可以尝试把 load 钩子去掉,你会发现控制台并没有报错了,因为 Vite 中已经没有插件在处理 axios 模块的导入了,那就不会有报错抛出了。

而一个虚拟 ID 为 \0{id} 在浏览器中开发时,最终会被编码为 /@id/__x00__{id}

但由于没有插件正确处理 axios 模块的导入,故它无法正确找到导入的模块。

而我们可以自己手动处理 axios 模块导入,加上 load 钩子。

一个坑⭕

🍊🍊🍊 关于 resolveId 钩子的调用时机?

它的概念说的是在每个传入模块请求时被调用。

真的是这样子吗?我们来做个测试,将 main.ts 文件改造一下:

javascript 复制代码
import { createApp } from 'vue';
import './style.css';
import App from './App.vue';

// 引入一个新模块
import fn from './test.ts';
fn();

createApp(App).mount('#app');

创建 test.ts 文件:

javascript 复制代码
export default function fn() {
  console.log('fn');
}

vite.config.ts 文件:

javascript 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [
    vue(),
    {
      name: 'vite-plugin-test',
      resolveId(source) {
        console.log('resolveId------:', source); // 不会输出打印
      }
    }
  ]
});

重新启动项目,你会发现 vite-plugin-test 插件的 resolveId 钩子并没有被执行!!!

这好像和概念上说的调用时机有点不一致,我们引入的 test.ts 文件也应该算是一个模块了,在浏览器控制台也能看到该模块被单独请求。而且项目中默认还请求了 vuestyle.css 等等模块呢,为什么 resolveId 没有被调用?

这是怎么回事呢❓😤

具体原因嘛,牵扯的还有点广,且听小编娓娓道来。

首先,我们先来加个配置,让 resolveId 钩子能被调用执行。

javascript 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [
    vue(),
    {
      name: 'vite-plugin-test',
      enforce: 'pre', // 提升插件顺序
      resolveId(source) {
        console.log('resolveId------:', source);
      }
    }
  ]
});

Vite 插件可以额外指定一个 enforce: 'pre' 配置项用于提升插件的应用顺序。

插件顺序在文档上有提及,可以去看看:传送门

此时,看控制台应该能打印出一些东西了,你会发现 resolveId 钩子被调用了很多次,并且回传的参数像是各种路径,其中也有我们自己导入的 ./test.ts 模块。

小编把它们按不同颜色的框框,将它们划分成四类,分别有:绝对路径、相对路径、Vue相关的、/@fs/*开头的。

这个时候, resolveId 钩子的调用时机才和概念说的一致了❗❗❗

那么,可能你还会有一点疑问,为什么需要提升插件的顺序呢❓不提升为什么就不执行呢❓

原因是 Vite 的插件可不是仅仅只有我们在 vite.config.ts 文件中字面配置的这两个, Vite 内部也内置了很多其他的插件,这点我们可以用 configResolved 钩子来查看。

javascript 复制代码
export default defineConfig({
  plugins: [
    vue(),
    {
      name: 'vite-plugin-test',
      configResolved(config) {
        // Vite的插件列表
        console.log(config.plugins.map(item => item.name)); 
      }
    }
  ]
});

通过 configResolved 钩子回传的参数可以获取到 Vite 最终配置的所有插件列表。

可以再对比一下,加不加 enforce 配置的区别:

能看到我们的插件在插件列表中的位置是不一样的。

我们还可以从源码的角度来看看这个提升的过程,源码位置👉:传送门

javascript 复制代码
export async function resolvePlugins(
  config: ResolvedConfig,
  prePlugins: Plugin[],
  normalPlugins: Plugin[],
  postPlugins: Plugin[],
): Promise<Plugin[]> {
  const isBuild = config.command === 'build'
  const isWorker = config.isWorker
  const buildPlugins = isBuild
    ? await (await import('../build')).resolveBuildPlugins(config)
    : { pre: [], post: [] }
  const { modulePreload } = config.build

  return [
    ...(isDepsOptimizerEnabled(config, false) ||
    isDepsOptimizerEnabled(config, true)
      ? [
          isBuild
            ? optimizedDepsBuildPlugin(config)
            : optimizedDepsPlugin(config),
        ]
      : []),
    isBuild ? metadataPlugin() : null,
    !isWorker ? watchPackageDataPlugin(config.packageCache) : null,
    preAliasPlugin(config),
    aliasPlugin({
      entries: config.resolve.alias,
      customResolver: viteAliasCustomResolver,
    }),
    ...prePlugins, // 通过 enforce: 'pre' 配置可以将我们的插件提升到这里
    modulePreload !== false && modulePreload.polyfill
      ? modulePreloadPolyfillPlugin(config)
      : null,
    resolvePlugin({
      ...config.resolve,
      root: config.root,
      isProduction: config.isProduction,
      isBuild,
      packageCache: config.packageCache,
      ssrConfig: config.ssr,
      asSrc: true,
      fsUtils: getFsUtils(config),
      getDepsOptimizer: (ssr: boolean) => getDepsOptimizer(config, ssr),
      shouldExternalize:
        isBuild && config.build.ssr
          ? (id, importer) => shouldExternalizeForSSR(id, importer, config)
          : undefined,
    }),
    htmlInlineProxyPlugin(config),
    cssPlugin(config),
    config.esbuild !== false ? esbuildPlugin(config) : null,
    jsonPlugin(
      {
        namedExports: true,
        ...config.json,
      },
      isBuild,
    ),
    wasmHelperPlugin(config),
    webWorkerPlugin(config),
    assetPlugin(config),
    ...normalPlugins, // 普通配置的插件在这个地方
    wasmFallbackPlugin(),
    definePlugin(config),
    cssPostPlugin(config),
    isBuild && buildHtmlPlugin(config),
    workerImportMetaUrlPlugin(config),
    assetImportMetaUrlPlugin(config),
    ...buildPlugins.pre,
    dynamicImportVarsPlugin(config),
    importGlobPlugin(config),
    ...postPlugins,
    ...buildPlugins.post,
    ...(isBuild
      ? []
      : [clientInjectionsPlugin(config), importAnalysisPlugin(config)]),
  ].filter(Boolean) as Plugin[]
}

我们普通配置的插件最终会在 normalPlugins 附近,而通过 enforce: 'pre' 配置的插件则会在上面的 prePlugins 附近。

然后到这里又和 resolveId 调用时机有什么关系呢❓

不急,不知道你有没有印象?在上面 "13个生命周期钩子" 的地方,小编提过 resolveId 钩子属于"垄断钩子",一旦前面插件中该钩子有了结果之后,后续其他插件中该钩子就不会被执行。😋

啥意思呢❓

例如:

javascript 复制代码
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [
    vue(),
    {
      name: 'vite-plugin-test',
      enforce: 'pre',
      resolveId(source) {
        if (source.indexOf('test') !== -1) {
          return `export function fn() { console.log('fn');}`; // 处理掉 test 模块
        }
      }
    },
    {
      name: 'vite-plugin-test2',
      enforce: 'pre',
      resolveId(source) {
        console.log('resolveId------2:', source);
      },
    },
  ],
});

我们重新配置两个插件并且都配置上 enforce 选项,来看第二个插件的 resolveId 钩子输出,你会发现其中没有了 ./test.ts 模块的影子了。

这是因为该模块的导入已经被第一个插件的 resolveId 钩子处理过了,所以并不会在后续的钩子中被执行了,这下,你应该能体会、理解到什么是垄断钩子了吧。😮

那么,再回到我们这个例子中,为什么这里不执行了呢?

javascript 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [
    vue(),
    {
      name: 'vite-plugin-test',
      resolveId(source) {
        console.log('resolveId------:', source); // 不会输出打印
      }
    }
  ]
});

原因很明显了,现有的项目中各种模块的导入已经被 Vite 内置的插件处理过了,所以到了这个插件基本也没有需要处理的模块了。

那你可能又会问,又有哪些模块 Vite 不会帮我们处理呢?

一般可能是以下三种情况:

  • 加了特定符号的模块,如 \0{id}virtual:{id}/virtual:{id}
  • 未能被正常找到的第三方模块。
  • 未配置项目别名却通过别名导入的模块。

其实,总结起来本质就是一些错误的模块导入。

这些错误导入的模块,Vite 要么是没法处理,要么是不需要它处理,所以它选择继续往下抛,并报错来提示。而我们就可以根据报错去代码中解决,或者也可以从插件这方面来解决。

到这里,你应该就能知道为什么 resolveId 钩子有时候是不执行了吧❗

然后然后...还没完呢😁

上面不是还提过 resolveId 钩子接收的参数,大概可以划分成四类:"绝对路径、相对路径、Vue相关的、/@fs/*开头的",而这些由 Vite 内置的插件帮我们处理了。

刨根问底一下,具体是哪些插件处理呢❓

主要是 vite:resolvevite:vue 插件。

vite:resolve插件源码

vite:vue插件源码

可以直接看看源码中关于 resolveId 钩子的代码,对照着找找是不是有处理小编说的那些分类的逻辑代码,应该是能看得懂的。👻

噢,还有,vite:resolve 插件源码它其中开头一段就是避免 Vite 去处理各种虚拟模块(\0{id})。

插件列表还有一些处理别名(@)模块导入的插件 vite:pre-aliasalias,还有处理样式导入 vite:css 等等。

最后,还有一点。😐

咱们都已经把 resolveId 应用层面上研究得如此透彻了,气氛都到这了,总要了解一下 resolveId 垄断钩子具体是如何实现的吧?它是如何实现垄断这个过程的?

resolveId 源码:传送门

javascript 复制代码
async resolveId(rawId, importer = join(root, 'index.html'), options) {
  ...
  let id: string | null = null
  const partial: Partial<PartialResolvedId> = {}
  // 遍历所有插件
  for (const plugin of getSortedPlugins('resolveId')) {
    ...
    // 记录正在执行的插件
    ctx._activePlugin = plugin

    // ...
    // 执行插件resolveId钩子
    const handler = getHookHandler(plugin.resolveId)
    const result = await handleHookPromise(
      ...
    )
    // 如果没有返回值继续调用剩余插件的 resolveId 钩子函数,如果有返回值退出循环
    if (!result) continue
    
    // 如果有返回值,则记录起来,直接跳出
    if (typeof result === 'string') {
      id = result
    } else {
      id = result.id
      Object.assign(partial, result)
    }

    ...
    // 直接跳出
    break;
  }

  ...
  
  // 最后返回一个对象;对象内有一个属性 id,值是解析后的绝对路径
  if (id) {
    partial.id = isExternalUrl(id) ? id : normalizePath(id)
    return partial as PartialResolvedId
  } else {
    return null
  }
},

到此,这个小坑基本也填上了。

最后的最后,你有没有觉得上述的插件功能看着是不是有点熟悉?像不像当初 Webpack 配置 externals 配置项一样?

html-webpack-externals-plugin

webpack-cdn-plugin

vite-plugin-resolve

vite-plugin-import

接下来,我们继续来开发第二个 Vite 插件练练手,这个插件与按需加载这方面有关,插件大概效果与 babel-plugin-import 一致,其中原理是通过了 transform 钩子来实现。

问题❓

还是一样,先创建测试项目:

javascript 复制代码
npm create vite@latest your-project-name -- --template vue-ts

安装 ant-design-vue 作为测试对象:

javascript 复制代码
npm install ant-design-vue -S

main.ts 中使用:

javascript 复制代码
import { createApp } from 'vue';
import './style.css';
import App from './App.vue';

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

const app = createApp(App);

app.use(Button); // 全局注册一个按钮组件

app.mount('#app')

App.vue 中使用组件:

javascript 复制代码
<template>
  <a-button type="primary">一个按钮</a-button>
</template>

从上面截图可以看到 ant-design-vue 整个包被引入了,有5M的大小,但我们仅仅只使用了 a-button 一个组件而已。🥶

当然,这也正常,因为我们使用了 import { Button } from 'ant-design-vue'; 语句导入整个包

🚫注意,这里有个容易掉入的误区,很多新手的小伙伴可能会觉得不是使用解构语法导入了吗?应该就不会导入其他组件了呀?这不就是按需加载了吗❓❓❓

再者加上官网说直接使用 import { Button } from 'ant-design-vue'; 语句就会有按需引入的效果了,更让人容易混乱。

然而并不是,有没有注意到前面还有一句是 "基于 ES modules 的 tree shaking"。 关键点还是在于 tree shaking (摇树),不过这玩意一般是在打包过程才有作用。

也就是说你可以直接这样子使用,当项目打包的时候 tree shaking 会帮你完成按需引入的效果。

不过,导入整个包这也只是在开发环境会这样子,当我们构建生产环境代码的时候,会有 rolluptree-shaking 帮我们完成按需加载的功能。

而如果我们不想开发环境每次都导入整个包,也可以手动去导入 a-button 包就行。

javascript 复制代码
import { createApp } from 'vue';
import './style.css';
import App from './App.vue';

// import { Button } from 'ant-design-vue';
import Button from 'ant-design-vue/es/button';

const app = createApp(App);

app.use(Button);

app.mount('#app')

可以看到这次就只有 a-button 的包,大小才590B。

我们只是改了一行代码,由
import { Button } from 'ant-design-vue';

变成
import Button from 'ant-design-vue/es/button';

咱们就手动完成了按需加载的功能,而这也是早前按需加载功能的本质原理。

其实,可以说在 tree-shaking 还没有广泛应用之前,很多组件库实现按需加载的功能基本都是依据该原理去实现的,只是它们可能做得更多,它们可能还要兼顾不同规范的按需、样式的按需、语言的按需等等。

例如,在 Vue2 之前一般组件库会使用到 babel-plugin-import 插件来实现组件的按需加载。

而它文档上解释的原理也是大致如此的过程:

它包含了样式的按需加载过程。

ant-design-vue 4.0 版本使用了 CSS in JS ,让样式的按需加载也可以直接基于 ESMtree-shaking

而这次我们要再编写一个 Vite 插件来完成按需加载这个转换过程,小编将该插件命名为 vite-plugin-import

解决❗

记得先把 main.ts 改回 import { Button } from 'ant-design-vue'; 形式,我们要通过插件来完成按需引入的功能,而不是手动。

进入 vite.config.js 文件,一样先定义插件:

javascript 复制代码
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [
    vue(),
    {
      name: "vite-plugin-import", // 必填
    },
  ],
});

由于,这次要涉及源码的操作,需要牵扯到 AST (抽象语法树)的知识,如果你还不太了解 AST 可以先去自行去学习一下哦,不过小编会尽量写明注释,应该问题不大。👻

先来看看这个插件的代码:

javascript 复制代码
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [
    vue(),
    {
      name: 'vite-plugin-import',
      transform(code, id) {
        // 先匹配 main.ts 文件
        if (id.indexOf('main') !== -1) {
          // vite提供了将源码转成AST的方法
          const ast = this.parse(code);
          // 存储需要更改的节点
          let antVNode = null;
          // 可能导出多个组件 import { Button, Spin, ...} from 'ant-design-vue';
          const antVImports = [];
          
          treeWalk(ast, {
            // ImportDeclaration 从在线的AST网站中确认即可
            ImportDeclaration(node) {
              if (node.source.value === 'ant-design-vue') {
                antVNode = node;
                for (const spec of node.specifiers) {
                  // 这种转换方式的前提都是确定了组件库的模块结构
                  antVImports.push(`import ${spec.local.name} from 'ant-design-vue/lib/${spec.local.name.toLocaleLowerCase()}'`)
                }
              }
            }
          });
          
          if (antVNode) {
            return code.slice(0, antVNode.start) + antVImports.join(';') + code.slice(antVNode.end);
          }
        };
      },
    },
  ],
})

/**
 * @name 通过递归遍历AST树
 * @param { Record<string, any> | Array<Record<string, any>> } ast 抽象语法树,这里值的类型先整成any吧
 * @param { { [type: string]: (ast: Record<string, any>) => void } } visitor 需要访问的节点,可以按照在线AST网站对照即可
 */
function treeWalk(
	ast: Record<string, any> | Array<Record<string, any>>,
	visitor: { [type: string]: (ast: Record<string, any>) => void }
) {
  // 如果你在vite.config.ts中Object.values报错,可以改一下tsconfig.node.json的compilerOptions.lib
  for (const node of Object.values(ast)) {
    if (node && typeof node === 'object') {
      visitor[node.type]?.(node);
      // 递归
      treeWalk(node, visitor);
    }
  }
}

transform 钩子的作用应该就比较好理解了,我们能从它的参数接收到每个模块的源码内容,还有模块的路径,你可以尝试打印它的参数观察观察。😉

而这次我们仅需要改变一个 main.ts 模块的源码,在拿到想要的源码后,我们需要先将源码转成 AST 才好操作。

源码转 AST 的方式有很多,可以用 Babel 提供的 @babel/parser 包,也可以使用 Rollup 源码中的 this.parse 方法进行转换。

而有了 AST 后,我们需要去遍历 AST ,它本质是一个树结构,这里小编在网上找了一个比较简单的现成方法来完成 AST 的遍历。

或者遍历 AST 你也可以使用 Babel 提供的 @babel/traverse 包。

用法基本和小编上面的遍历方法一样:

javascript 复制代码
import traverse from '@babel/traverse';

traverse.default(ast, {
  ...
});

其实,由于 AST 一直比较固定,就那一套,所以操作 AST 的方式基本网上都有,我们直接找来用就行了。😉

还有就是关于 ImportDeclaration 节点的问题,这里你可能需要知道一下 AST 具体是长什么样子的,你可以打印输出它来观察,也可以通过在线的一个站点来查看 AST 的模样。 传送门

我们把 main.ts 模块的源码复制上面,鼠标移动到 import { Button } from 'ant-design-vue'; 语句,对应可以看到右边的节点高亮了。

找到对应的节点,对应着递归方法去瞧瞧,你应该就能明白啦。😂

这里还有一个小注意点❗

当我们把鼠标移动到 Button 时,会有两个地方高亮,分别是 importedlocal,那我们应该选择哪一个呢?

答案是要选择 local 因为它包含了 as 的重命名情况。

这个插件其实也不复杂,就是操作 AST 部分麻烦一点,我们本质就干一件事情。

将下面语句
import { Button } from 'ant-design-vue';

变成
import Button from 'ant-design-vue/es/button';

编写完插件后,你可以重新启动项目,看看浏览器请求是否达到了按需引入的项目。


至此,本篇文章就写完啦,撒花撒花。

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。

老样子,点赞+评论=你会了,收藏=你精通了。

相关推荐
Front思24 分钟前
vue使用高德地图
javascript·vue.js·ecmascript
zqx_71 小时前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己1 小时前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称2 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色2 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2342 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河2 小时前
CSS总结
前端·css
BigYe程普3 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
余生H3 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
花花鱼3 小时前
@antv/x6 导出图片下载,或者导出图片为base64由后端去处理。
vue.js