布景

你是否从前被多而途径复杂的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文件和没导入过的作用

一键三连、自动导入:打造属于你自己的prettier工具

多函数

配置 – jsonParser和testFn函数

module.exports = [
  {
    importAutoPathName: "@/utils/json-handler",
    importAutoFnName: "jsonParser",
  },
  {
    importAutoPathName: "@/utils/testFn",
    importAutoFnName: "testFn",
  },
];

一键三连、自动导入:打造属于你自己的prettier工具

禁用

运用注释 auto-import-disable-next-line

一键三连、自动导入:打造属于你自己的prettier工具

结合Vue模板

vue模板本质上会把script标签中的内容提取出一个ts/js文件

一键三连、自动导入:打造属于你自己的prettier工具

tsx中的作用

一键三连、自动导入:打造属于你自己的prettier工具

注意:假如需求在tsx、jsx中运用,则在tools文件的parse参加jsx插件选项

parser.parse(code, {
  plugins: ["jsx"],
})

神仙打架

文件中现已存在了函数的其他引证

一键三连、自动导入:打造属于你自己的prettier工具

假如文件中现已存在了方针函数(如上图的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东西是一种十分实用的东西,它能够主动导入所需的代码,进步了开发功率,而且有效地削减了心智担负。针对这些问题,代码提效东西应运而生。另外,咱们还能够总结作业中的重复性作业,进行代码封装和轮子制作,进一步进步作业功率。 总的来说,代码提效东西是促进程序员作业功率的重要东西,不仅能够削减作业担负,进步作业功率,还能够让开发人员有更多的时刻思考和优化程序结构,发明更好的价值。