基本介绍
- 目的在于体会
unplugin-auto-import
这款插件的实际原理,真实的感受到前端架构这几年的飞速发展 - 这里实现
vue
elementui
常用模块的自动导入,窥探插件内部核心原理,初探如何开发一款定制化插件 - 这里建议想体验的小伙伴,直接安装、运行项目,根据下面说明自己打断点体会(代码注释非常全面)
- 源代码地址,边思考,边打断点,方便理解
实现原理
- 流程就是:源代码分析 => 通过 AST 手动操作加工新的 AST => 生成新的源代码 => 注入到浏览器端
代码流转原理图
- 这里展示
Vite
是如何用插件处理文件,并成功让浏览器正确识别的整个流程
源码解析
- 位置:
src/index.js
引入我们要用到的依赖
js
// 引入源代码解析器,将源代码(例如 JavaScript、JSX、TypeScript 等)转换成抽象语法树(AST)
import { parse } from "@babel/parser";
// AST 操作器,对 AST 进行深度遍历,可以实现插入、删除或替换节点
import traverse from "@babel/traverse";
// AST 节点生成器,用来创建新节点
import * as t from "@babel/types";
// 代码还原器:将 AST 转换为代码
import generate from "@babel/generator";
定义我们开发中常见的源代码
js
// 定义常用的 vue 和 elementui 使用源代码
const sourceCode = `
const count = ref(0);
const doubled = computed(() => count.value * 2);
ElMessage('this is a message.');
ElMessageBox.confirm("Are you sure you want to close this?")
.then(() => {
done();
})
.catch(() => {
// catch error
});
`;
源代码转换成可操作的 AST 树
js
// 使用 parse 方法将源代码字符串转换为 AST(抽象语法树)
const ast = parse(sourceCode, {
sourceType: "module", // 以 ES 模块解析
plugins: ["jsx", "typescript"], // 解析 JSX 和 TS 语法
});
利用 Set 特性收集节点名称
js
// 定义 Set 集合,避免重复导入
const importsToAddVue = new Set();
const importsToAddElement = new Set();
// 使用 traverse 遍历 AST
traverse.default(ast, {
// 当遍历到类型为 Identifier 的节点时查找我们所要的元素
Identifier(path) {
// 节点名称属于 vue 包,添加到 importsToAddVue 集合中
const nodes = ["ref", "computed"];
// 节点名称属于 element 包,添加到 importsToAddElement 集合中
const elements = ["ElMessage", "ElMessageBox"];
// 执行 vue 添加操作
if (nodes.includes(path.node.name)) {
importsToAddVue.add(path.node.name);
}
// 执行 element 添加操作
if (elements.includes(path.node.name)) {
importsToAddElement.add(path.node.name);
}
},
});
把收集到的节点,加工成节点数组
js
// 存储 vue 所要重建 AST 的内容
const vueList = [];
// 存储 element 所要重建 AST 的内容
const elementList = [];
// 遍历添加 vue 相关
Array.from(importsToAddVue).forEach((item) => {
const specifiersName = t.identifier(item);
vueList.push(t.importSpecifier(specifiersName, specifiersName));
});
// 遍历添加 element 相关
Array.from(importsToAddElement).forEach((item) => {
const specifiersName = t.identifier(item);
elementList.push(t.importSpecifier(specifiersName, specifiersName));
});
把节点数组加工成新的 AST 树
js
// 生成 ImportDeclaration 类型的所有 AST 节点集合
const importDeclarations = [
t.importDeclaration(
// 创建 importSpecifier 节点数组,表示导入的具体 API(如:ref、computed)
vueList,
// 创建 stringLiteral 节点,表示导入的模块来源(如:'vue')
t.stringLiteral("vue")
),
t.importDeclaration(
// 创建 importSpecifier 节点数组,表示导入的具体 API(如:ElMessage、ElMessageBox)
elementList,
// 创建 stringLiteral 节点,表示导入的模块来源(例如:'element')
t.stringLiteral("element")
),
];
根据新的 AST 树形成新的源码
js
// 将生成的导入语句添加到原始 AST 的开头
ast.program.body.unshift(...importDeclarations);
// 使用 generate 方法将修改后的 AST 转换回源代码
const { code: updatedCode } = generate.default(ast, {}, sourceCode);
// 输出包含导入功能的新源代码,即实现了自动导入功能
console.log(updatedCode);
工程化配置自动导入
- 自动导入
API
和自动导入组件
shell
pnpm install -D unplugin-vue-components unplugin-auto-import
- 配置
vite.config.ts
js
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
js
plugins: [
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
- 上面两个插件是各大软件库推荐的:
unplugin-auto-import
是按需自动导入API
,就是本文重点说的内容unplugin-vue-components
是按需组件自动导入,默认src/components
内部组件无需引入直接可以全局使用
AutoImport 翻译了一下配置内容,仅供参考
js
AutoImport({
// targets to transform
// 要转换的目标
include: [
/\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
/\.vue$/,
/\.vue\?vue/, // .vue
/\.md$/, // .md
],
// global imports to register
// 全局引入注册
imports: [
// presets
// 预设
'vue',
'vue-router',
// custom
// 自定义
{
'@vueuse/core': [
// named imports
// 名称导入
'useMouse', // import { useMouse } from '@vueuse/core',
// alias
// 别名导入
['useFetch', 'useMyFetch'], // import { useFetch as useMyFetch } from '@vueuse/core',
],
'axios': [
// default imports
// 默认导入
['default', 'axios'], // import { default as axios } from 'axios',
],
'[package-name]': [
'[import-names]',
// alias
['[from]', '[alias]'],
],
},
// example type import
// 示例类型导入
{
from: 'vue-router',
imports: ['RouteLocationRaw'],
type: true,
},
],
// Array of strings of regexes that contains imports meant to be filtered out.
// 正则表达式字符串数组,包含要过滤的导入内容
ignore: [
'useMouse',
'useFetch'
],
// Enable auto import by filename for default module exports under directories
// 为目录下的默认模块导出启用按文件名自动导入
defaultExportByFilename: false,
// Auto import for module exports under directories
// 目录下模块导出的自动导入
// by default it only scan one level of modules under the directory
// 默认情况下,它只扫描目录下的一级模块
dirs: [
// './hooks',
// './composables' // only root modules 仅限根模块
// './composables/**', // all nested modules 所有嵌套模块
// ...
],
// Filepath to generate corresponding .d.ts file.
// 生成相应.d.ts文件的文件路径。
// Defaults to './auto-imports.d.ts' when `typescript` is installed locally.
// 当"typescript"本地安装时,默认为"./auto-imports.d.ts"。
// Set `false` to disable.
// 设置 false 为禁用
dts: './auto-imports.d.ts',
// Array of strings of regexes that contains imports meant to be ignored during
// 包含要在导入过程中忽略的导入的正则表达式字符串数组
// the declaration file generation. You may find this useful when you need to provide
// 生成声明文件。当您需要提供时,您可能会发现这很有用
// a custom signature for a function.
// 函数的自定义签名
ignoreDts: [
'ignoredFunction',
/^ignore_/
],
// Auto import inside Vue template
// Vue 模板内自动导入
// see https://github.com/unjs/unimport/pull/15 and https://github.com/unjs/unimport/pull/72
vueTemplate: false,
// Custom resolvers, compatible with `unplugin-vue-components`
// 自定义解析器,与"unplugin-vue-components"兼容
// see https://github.com/antfu/unplugin-auto-import/pull/23/
resolvers: [
/* ... */
],
// Inject the imports at the end of other imports
// 在其他导入的末尾注入导入
injectAtEnd: true,
// Generate corresponding .eslintrc-auto-import.json file.
// 生成对应的 .eslintrc-auto-import.json 文件。
// eslint globals Docs - https://eslint.org/docs/user-guide/configuring/language-options#specifying-globals
eslintrc: {
enabled: false, // Default `false` 默认 false
filepath: './.eslintrc-auto-import.json', // Default `./.eslintrc-auto-import.json`
globalsPropValue: true, // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
},
})
项目建立流程
- 建立文件夹
shell
mkdir folder-name
- 初始化项目
shell
npm init -y
- 安装依赖
js
pnpm i @babel/parser @babel/traverse @babel/types @babel/generator
思考前端的发展
- 这几年前端一直有后端化的趋势,无论是
ES
规范,还是各种新兴技术,都是在用后端的方式解决前端事情 - 早期的前端只是写静态页面,最多开发一些页面效果,其余的都要交给后端处理,这种开发模式,开发效率低、调试难度大、测试困难等
- 从2014年开始,随着jQuery的落幕,MVVM思想的诞生,Angular、React、Vue以数据驱动为核心的框架开始走上历史舞台,让前端可做的工作更加的多样化,后端回归本质
- 之后的几年,前端不断分化,有专做架构的、有专做业务开发的、有专做视觉3D效果和游戏的,各种分化,到如今,作为前端人要面对的就是都要懂、多要会做的大趋势
- 近期在深入研究前端架构,对于架构的理解也更加的深入,同时也感到技术发展的迅速。
- 也看到的架构后面的一些发展:
- 类似引入这种重复性的工作,都会逐步转移到架构层面解决
- 各种工具,都会用效率更高的技术语言重新编写
- 通过架构能力+工具性能两个方面来推动前端开发效率进入新的台阶
看山的三重境界
- 在写这块内容的时候,脑子里突然想起了这句话:看山是山,看山不是山,看山还是山。
- 联想到开发工作上,很有感触,顺便记录一下
需求描述
- 需求阶段一:现在有个需求,封装一个输入框,能够让用户输入内容,平台可以得到用户输入内容即可
- 需求阶段二:在阶段一基础上需要添加一些定制化提示内容,宽度也略有不同
- 需求阶段三:在之前基础上,需要针对性提供输入框美化效果
- 等等
早期的自己
- 早期的自己面对这种需求,需要什么就重新定义一个字段,不断地增加新的字段
- 没有考虑要分类型,考虑后续扩展,考虑后续组件的稳定性
- 这阶段就是:看山是山
中期的自己
- 自己有了经验,在看到这种需求,就开始做全方位的设计,类型多少种,每种类型服务的场景是什么等等
- 本来简单的一个需求,硬生生因为程序设计和考虑的复杂度,被无限拉长周期,最后出现很多设计好的场景并没有出现
- 这阶段就是:看山不是山
后期的自己
- 经过前两个阶段,早期看似轻松完成工作,实际后续的持续开发和维护成本非常高
- 中期的维护成本很低,但是开发成本过高
- 而后期的自己,兼容两者,在完成基础需求的基础上,让程序支持无限可扩展性;当有新的需求进入可以建立新的类型去实现。
感悟
- 这段看似和文章内容没什么关系,实际是我这段时间封装组件、工具的一种心得体会
- 反思和回顾自己工作的不足和需要改进的地方,正好在开发这块的时候有了灵感