布景
你是否从前被多而途径复杂的import句子搞得晕头转向?日常开发中咱们常常会运用各种函数引证,如埋点、东西函数等。但是,这些引证的当地往往分散在代码的各个旮旯,十分复杂,给代码的维护和更新带来了很大的困扰,那么就离不开咱们了解的CV。直到有一天…我懒得甚至连cv都按不动了。这时分有个斗胆主意,能够通过剖析代码,找到对应的作用域,把这些信息收集起来,继而判别对应文件有没有imported,有,越过;没有,就加上。
在运用这个插件时,开发者只需求在代码中运用函数引证即可,无需手动增加import句子,插件会主动完结这个进程。总的来说,这个插件能够大大简化函数引证的进程,让开发者更加专心于业务逻辑的完结。
现有计划
- 编辑器的代码片段,相似vscode中的代码片段。
- 编辑器主动导入功用。(下面依据vscode)
- 输入函数名,会有智能剖析,回车会有主动导入
- vscode中Auto Import插件,输入对应的函数名,会有import句子,不过只能针关于npm包中的剖析。
- …
- 打包计划: antfu大佬供给的unplugin-auto-import(unplugin是antfu写的一系列构建东西插件)
- 手动复制粘贴。
- …
思考
关于文件的剖析,毫无疑问便是今天的主角babel,会进行:
- 查找方针函数的引证
- 对引证的文件打标
- 查看打标文件是否现已import了,没有加上import
- 关于babel的插件参数进行规划,支撑动态匹配
以上动作完结后仅仅对ast的处理完结,并没有generate生成code。这一步至关重要~
我在规划这个插件的时分,想过…
- 打包东西?webpack、vite、gulp…
- node文件监听?Chokidar、fs.watch(watchFile)…
- 又或者是vscode插件?watch?害,感觉被watch洗脑了
- …
不过这些通通不行!
- 打包东西 ❌ 假如仅仅在打包的时分去刺进import,那会导致源码可读性很差!哪天被CR代码的时分,你这个函数哪来的???
- watch ❌ 文件change事件监听?假如文件操作频繁,每次触发必然影响性能。
但你要信任万能的JS社区总有令你满足的轮子!
信任很多小伙伴现已猜到了prettier,没错儿,便是它,结合vscode插件,在每次保存的时分一致注入import,简直不要太完美
技术选型
- 剖析代码:babel
- 代码输出:prettier
Prettier
官网插件开发:www.prettier.cn/docs/plugin…
开发插件
插件需求有5个模块。
module.exports = {
languages: { // 通常是插件的描述信息
name: string, // 插件名
since?: string,
parsers: string[], // 用到的parser
group?: string,
tmScope?: string,
aceMode?: string,
codemirrorMode?: string,
codemirrorMimeType?: string,
aliases?: string[],
extensions?: string[], // 格式化的文件后缀名
filenames?: string[],
linguistLanguageId?: number,
vscodeLanguageIds?: string[],
},
parsers: { // 调用需求的解析器
// key必须存在于languages的parsers
[key]: (parse, locStart, locEnd, hasPragma, preprocess) => {
// locStart, locEnd - node节点检测方位
// parse - 解析器,https://www.prettier.cn/docs/options.html#parser界说了一切22种原生解析器
// hasPragma - 过滤注释的函数
// preprocess - 在parse之前的预处理钩子
}
},
printers, // prettier从ast到输出最终代码的中心格式Doc。https://www.prettier.cn/docs/plugins.html#printers
options, // 界说了配置文件可传入的参数,SupportOption类型
defaultOptions // 覆盖配置文件的特点配置
}
很明显想要完结咱们的主动import的功用,肯定是在parsers或者printers中处理就好了。咱们想一下,既然咱们想把代码补充完整,阐明交给解析器之前得增加好import,所以这儿用preprocess最合适的。
作用
为了便利演示,会参加些不存在的import导入句子,大家主要看下作用就好~ 下面栗子中jsonParser是一个加上try catch的JSON.parse简易封装函数。
单函数
jsonParser函数分别现已导入过json-handler文件和没导入过的作用
多函数
配置 – jsonParser和testFn函数
module.exports = [
{
importAutoPathName: "@/utils/json-handler",
importAutoFnName: "jsonParser",
},
{
importAutoPathName: "@/utils/testFn",
importAutoFnName: "testFn",
},
];
禁用
运用注释 auto-import-disable-next-line
结合Vue模板
vue模板本质上会把script标签中的内容提取出一个ts/js文件
tsx中的作用
注意:假如需求在tsx、jsx中运用,则在tools文件的parse参加jsx插件选项
parser.parse(code, {
plugins: ["jsx"],
})
神仙打架
文件中现已存在了函数的其他引证
假如文件中现已存在了方针函数(如上图的testFn)的引证,则插件会继续增加配置的引证。归纳考虑,会加上引证,后续交由EsLint查看,关于代码逻辑性的查看并不在prettier责任范围内,需求开发者自行判别处理。
代码完结
下载依靠
npm i @babel/core prettier typescript -D
配置prettier运转脚本语言
"scripts": {
"format": "prettier --write \"**/*.{ts,js,css}\""
},
增加.prettierrc.js文件
const { resolve } = require("path");
module.exports = {
useTabs: false,
tabWidth: 2,
overrides: [
{
files: "*.{json,babelrc,eslintrc,remarkrc}",
options: {
useTabs: false,
},
},
],
"import-auto-config": resolve(__dirname, "import_auto_config.js"),
plugins: ["./plugin/tools.js"],
};
增加插件文件plugin/tools.js
const babelParsers = require("prettier/parser-babel").parsers;
const typescriptParsers = require("prettier/parser-typescript").parsers;
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generate = require("@babel/generator").default;
const temp = require("@babel/template").default;
const t = require("@babel/types");
const crypto = require("crypto");
const newLine = crypto.randomUUID();
function autoImportPreprocessor(code, opt) {
// 获取配置
const importConfig = opt["import-auto-config"];
let config = [];
try {
const list = require(importConfig);
if (list.length) {
// 去除无效配置
config = list
.filter((item) => item.importAutoPathName && item.importAutoFnName)
.map((item, index) => ({
sort: index,
...item,
}));
}
} catch (error) {}
let importInfo;
function init() {
importInfo = {}; // 引证信息
}
const ast = parser.parse(code.replaceAll("\n\n", `\n"${newLine}";\n`), {
sourceType: "unambiguous",
});
// 禁用信息收集
const disableLineList = ast.comments
.filter((item) => {
return (
item.type === "CommentLine" &&
item.value.trim() === "auto-import-disable-next-line"
);
})
.map((item) => item?.loc?.start?.line);
traverse(ast, {
Program: {
enter(ProgramPath) {
init();
ProgramPath.traverse({
ImportDeclaration(path) {
// 找出相似 import { a,b } from "@/utils/json-handler"; 的 @/utils/json-handler 引证节点
const source = path.node?.source;
// 找出当时path节点的引证途径是否在配置文件中
const idx = config.findIndex(
(item) => item.importAutoPathName === source.value
);
if (idx < 0) return;
if (
t.isStringLiteral(source, {
value: config[idx].importAutoPathName,
})
) {
// 获取该节点上的一切引证
const imported =
path.node?.specifiers
.filter((specifier) => {
return (
t.isImportSpecifier(specifier) &&
t.isIdentifier(specifier.imported)
);
})
.map((item) => {
return item.imported?.name;
}) || [];
if (imported.length) {
// 假如有引证,则删去该节点,Program.exit退出时一致修正ast增加
path.remove();
}
// 增加importInfo信息,key为方法名
importInfo[config[idx].importAutoFnName] = {
...config[idx],
imported,
};
}
},
CallExpression(path) {
// 找出方针函数的调用
const callee = path.node?.callee;
const calleeIdx = config.findIndex(
(item) => item.importAutoFnName === callee.name
);
if (calleeIdx < 0) return;
if (
t.isIdentifier(callee, {
name: config[calleeIdx].importAutoFnName,
})
) {
const info = importInfo?.[config[calleeIdx].importAutoFnName];
const startLineNum = callee?.loc?.start?.line;
if (info) {
// info存在阐明这个方法对应的文件现已被导入过了
info.isNeedImport = true;
info.startLineNum = startLineNum;
} else {
// 这个文件还未导入
importInfo[config[calleeIdx].importAutoFnName] = {
...config[calleeIdx],
imported: [],
isNeedImport: !disableLineList.includes(startLineNum),
startLineNum: startLineNum,
};
}
}
},
});
},
exit(path) {
const importInfoList = Object.values(importInfo);
const importAstList = []; // 有sort的import的ast
const extraList = []; // 无sort的import的ast
importInfoList.forEach((item) => {
if (!item.isNeedImport) return;
const {
imported,
importAutoFnName,
importAutoPathName,
startLineNum,
sort,
} = item;
// 整个Program节点中有 fnName 的调用
let ast = null;
if (imported.length) {
// pathName 有引证过
// 之前没有引证过 && 不在禁用列表中
if (
!imported.includes(importAutoFnName) &&
!disableLineList.includes(startLineNum - 1)
) {
imported.push(importAutoFnName);
}
ast = temp.ast(
`import { ${imported.join(",")} } from "${importAutoPathName}";`
);
} else {
// pathName 没有引证过
if (!disableLineList.includes(startLineNum - 1)) {
ast = temp.ast(
`import { ${importAutoFnName} } from "${importAutoPathName}";`
);
}
}
// 依据配置排序刺进import,防止import方位反复改变
typeof sort === "number"
? importAstList.splice(sort, 0, ast)
: extraList.push(ast);
});
const sortList = [...importAstList, ...extraList].reverse();
// 刺进import句子
sortList.forEach((item) => path.unshiftContainer("body", item));
},
},
});
const formatCode = generate(ast).code;
const result = formatCode.replaceAll(`"${newLine}";`, "\n");
return result;
}
module.exports = {
languages: [
{
name: "auto-import",
},
],
parsers: {
typescript: {
...typescriptParsers.typescript,
preprocess: autoImportPreprocessor,
},
babel: {
...babelParsers.babel,
preprocess: autoImportPreprocessor,
},
},
options: {
"import-auto-config": {
type: "string",
default: "import_auto_config.js",
category: "auto-import",
description: "主动导入函数配置文件",
},
},
};
增加import_auto_config.js配置文件
特点 | 类型 | 阐明 |
---|---|---|
importAutoPathName | string | 方法对应的path链接,需求一个绝对途径 |
importAutoFnName | string | 方法名 |
module.exports = [
{
importAutoPathName: "@/utils/json-handler",
importAutoFnName: "jsonParser",
},
{
importAutoPathName: "@/utils/testFn",
importAutoFnName: "testFn",
},
];
增加tsconfig中的path别名界说
{
"compilerOptions": {
...,
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
}
}
总结
在日常的开发中,咱们常常需求完结一些繁琐重复的作业,其中包括手动import模块、编写重复代码等。其中,主动import东西是一种十分实用的东西,它能够主动导入所需的代码,进步了开发功率,而且有效地削减了心智担负。针对这些问题,代码提效东西应运而生。另外,咱们还能够总结作业中的重复性作业,进行代码封装和轮子制作,进一步进步作业功率。 总的来说,代码提效东西是促进程序员作业功率的重要东西,不仅能够削减作业担负,进步作业功率,还能够让开发人员有更多的时刻思考和优化程序结构,发明更好的价值。