手写自动导入插件体会前端发展

基本介绍

  • 目的在于体会 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效果和游戏的,各种分化,到如今,作为前端人要面对的就是都要懂、多要会做的大趋势
  • 近期在深入研究前端架构,对于架构的理解也更加的深入,同时也感到技术发展的迅速。
  • 也看到的架构后面的一些发展:
    • 类似引入这种重复性的工作,都会逐步转移到架构层面解决
    • 各种工具,都会用效率更高的技术语言重新编写
    • 通过架构能力+工具性能两个方面来推动前端开发效率进入新的台阶

看山的三重境界

  • 在写这块内容的时候,脑子里突然想起了这句话:看山是山,看山不是山,看山还是山。
  • 联想到开发工作上,很有感触,顺便记录一下

需求描述

  • 需求阶段一:现在有个需求,封装一个输入框,能够让用户输入内容,平台可以得到用户输入内容即可
  • 需求阶段二:在阶段一基础上需要添加一些定制化提示内容,宽度也略有不同
  • 需求阶段三:在之前基础上,需要针对性提供输入框美化效果
  • 等等

早期的自己

  • 早期的自己面对这种需求,需要什么就重新定义一个字段,不断地增加新的字段
  • 没有考虑要分类型,考虑后续扩展,考虑后续组件的稳定性
  • 这阶段就是:看山是山

中期的自己

  • 自己有了经验,在看到这种需求,就开始做全方位的设计,类型多少种,每种类型服务的场景是什么等等
  • 本来简单的一个需求,硬生生因为程序设计和考虑的复杂度,被无限拉长周期,最后出现很多设计好的场景并没有出现
  • 这阶段就是:看山不是山

后期的自己

  • 经过前两个阶段,早期看似轻松完成工作,实际后续的持续开发和维护成本非常高
  • 中期的维护成本很低,但是开发成本过高
  • 而后期的自己,兼容两者,在完成基础需求的基础上,让程序支持无限可扩展性;当有新的需求进入可以建立新的类型去实现。

感悟

  • 这段看似和文章内容没什么关系,实际是我这段时间封装组件、工具的一种心得体会
  • 反思和回顾自己工作的不足和需要改进的地方,正好在开发这块的时候有了灵感
相关推荐
转角羊儿2 分钟前
uni-app文章列表制作⑧
前端·javascript·uni-app
大G哥8 分钟前
python 数据类型----可变数据类型
linux·服务器·开发语言·前端·python
hong_zc32 分钟前
初始 html
前端·html
小小吱37 分钟前
HTML动画
前端·html
Bio Coder1 小时前
学习用 Javascript、HTML、CSS 以及 Node.js 开发一个 uTools 插件,学习计划及其周期
javascript·学习·html·开发·utools
糊涂涂是个小盆友1 小时前
前端 - 使用uniapp+vue搭建前端项目(app端)
前端·vue.js·uni-app
浮华似水1 小时前
Javascirpt时区——脱坑指南
前端
王二端茶倒水1 小时前
大龄程序员兼职跑外卖第五周之亲身感悟
前端·后端·程序员
_oP_i1 小时前
Web 与 Unity 之间的交互
前端·unity·交互
钢铁小狗侠2 小时前
前端(1)——快速入门HTML
前端·html