兼容性注意:
Vite 需要 Node.js 版本 18+,20+。然而,有些模板需要依赖更高的 Node 版本才能正常运行,当你的包管理器发出警告时,请注意升级你的 Node 版本。
背景
Vite
使用 Rollup 来打包构建你的生产环境代码。
注意,开发环境下 Vite
是并不需要打包这个过程的,打包这个动作由浏览器来接管, Vite
仅需在浏览器请求源码时以 ESM
方式提供源码即可。
那么,为什么 Vite
要选择 Rollup
来构建生产环境代码呢❓
两方面原因:
- 其一,利用
Rollup
现有的基础建设与周边生态。 - 其二,
Rollup
拥有设计出色且灵活的插件API。
🔉这里得提一下
EsBuild
,相信大家对它也不陌生,它在Vite
中的重要性一点也不亚于Rollup
,两者是Vite
的双引擎架构的重要扮演者。简单总结一下
EsBuild
在Vite
中的作用:
- 依赖预构建,
EsBuild
会在开发阶段帮忙完成第三方依赖的预构建,这也是Vite
启动快的一个重要原因。
(关于依赖预构建的具体目的:传送门)- 单文件编译,将
.jsx/.ts/.tsx
文件转译成js
,这部分能力在开发与生产环境都得到应用。- 代码压缩,
Vite
从2.6版本开始,默认使用EsBuild
来进行生产环境代码的压缩。这里说了一下
EsBuild
,主要是想表明EsBuild
在Vite
构建项目代码整个过程中基本都参与了,不管是开发环境还是生产环境的构建,不管是打包过程还是转译过程,总之Esbuild
是Vite
构建的重要性能利器。
但是❗ 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个生命周期钩子能找到 resolveId
与 load
是符合我们本次需求的。
怎么才能知道自己需要使用什么钩子呢?小编只能告诉多练多写,熟了之后你就会和使用
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
。
啥意思呢❓
你可以认为像 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
文件也应该算是一个模块了,在浏览器控制台也能看到该模块被单独请求。而且项目中默认还请求了 vue
、 style.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:resolve
与 vite:vue
插件。
可以直接看看源码中关于 resolveId
钩子的代码,对照着找找是不是有处理小编说的那些分类的逻辑代码,应该是能看得懂的。👻
噢,还有,
vite:resolve
插件源码它其中开头一段就是避免Vite
去处理各种虚拟模块(\0{id}
)。
插件列表还有一些处理别名(@)模块导入的插件 vite:pre-alias
与 alias
,还有处理样式导入 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
配置项一样?
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
会帮你完成按需引入的效果。
不过,导入整个包这也只是在开发环境会这样子,当我们构建生产环境代码的时候,会有 rollup
的 tree-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
,让样式的按需加载也可以直接基于ESM
的tree-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
时,会有两个地方高亮,分别是imported
与local
,那我们应该选择哪一个呢?答案是要选择
local
因为它包含了as
的重命名情况。
这个插件其实也不复杂,就是操作 AST
部分麻烦一点,我们本质就干一件事情。
将下面语句
import { Button } from 'ant-design-vue';
变成
import Button from 'ant-design-vue/es/button';
编写完插件后,你可以重新启动项目,看看浏览器请求是否达到了按需引入的项目。
至此,本篇文章就写完啦,撒花撒花。
希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。