最近写了一个 Prettier 插件,能够达到这样的作用:

写一个搭档见了会打你的 Prettier 插件

搭档每次保存代码的时分,import 句子的次序都会随机变。

但是他去 prettier 装备文件里还啥也发现不了。

所以就会一脸懵逼。

那么这个搭档发现了会打你的 prettier 插件是怎样完成的呢?

Prettier 的原理

前端的编译东西都是从源码到源码的转化,所以都是 parse、transform、generate 这三步:

写一个搭档见了会打你的 Prettier 插件

parse 是把源码字符串转化成 AST 的对象树,transform 是对 AST 做增删改,而 generate (或许叫 printer)是把转化后的 AST 递归打印成方针代码。

prettier 其实也基于编译完成的,只不过不做中心的转化,仅仅 parse 和 print(也能够叫 generate),所以分为两步:

写一个搭档见了会打你的 Prettier 插件

它首要的格局化功用都是在 print 阶段做的。

整个流程还是比较简单的,那它是怎样支撑那么多言语的呢?

当然是每种言语有各自的 parser 和 printer 呀!

比方它内置了这些 parser:

写一个搭档见了会打你的 Prettier 插件

ts、js、css、scss、html 等都支撑,便是由于不同的后缀名会启用不同的 parser 和 printer。

并且,它是支撑插件的,你完全能够经过 prettier 插件来完成任何一种言语的格局化。

很简单想到,插件自然也是指定什么后缀名的文件,用什么 parser 和 printer,所以是这样的格局:

写一个搭档见了会打你的 Prettier 插件

咱们看一个真实的插件,格局化 nginx 装备文件的 prettier 插件 prettier-plugin-nginx:

languages 部分便是指定这个言语的姓名,什么后缀名的文件,用什么 parser。

写一个搭档见了会打你的 Prettier 插件

然后 parser 部分便是完成字符串到 AST 的 parse:

写一个搭档见了会打你的 Prettier 插件

printer 部分便是把 AST 打印成代码:

写一个搭档见了会打你的 Prettier 插件

当然,prettier 插件里的 printer 不是直接打印成字符串,而是打印成一种 Doc 的格局,便于 prettier 再做一层格局操控。

总归,想扩展一种新的言语的格局化,只需完成 parser 和 printer 就好了。

但前面那个修正 imports 的插件也不是新言语呀,不是 js/ts 代码么?这种怎样写 prettier 插件?

写一个搭档见了会打你的 Prettier 插件

其实 parser 还能够指定一个预处理器:

写一个搭档见了会打你的 Prettier 插件

在 parse 之前对内容做一些修正:

所以完整的 prettier 流程应该是这样的:

写一个搭档见了会打你的 Prettier 插件

那咱们写一个 prettier 插件,对 js/ts/vue/flow 的代码都做下同样的预处理,不就能完成随机打乱 imports 的作用么~

写一个搭档见了会打你的 Prettier 插件

咱们来写一下:

只需求对 prettier 默许的 babel 和 typescript 的 parser 做修正就能够了。

其他装备保持不变,仅仅修正下 preprocess 部分:

const babelParsers = require("prettier/parser-babel").parsers;
const typescriptParsers = require("prettier/parser-typescript").parsers;
function myPreprocessor(code, options) {
  return code + 'guangguangguang';
}
module.exports = {
  parsers: {
    babel: {
      ...babelParsers.babel,
      preprocess: myPreprocessor,
    },
    typescript: {
      ...typescriptParsers.typescript,
      preprocess: myPreprocessor,
    },
  },
};

我在代码后加了一个 guangguangguang。

在 prettier 装备文件里引进这个插件:

写一个搭档见了会打你的 Prettier 插件

然后咱们跑下 prettier:

写一个搭档见了会打你的 Prettier 插件

咱们写的第一个 prettier 插件收效了!

并且除了 js、ts,在 vue 文件里也会收效:

写一个搭档见了会打你的 Prettier 插件

这是由于在 parse vue 的 sfc 的时分,script 的部分还是用 babel 或许 tsc 的。

当然,一般咱们会装备 vscode 在保存的时分自动调用 prettier 来格局化。

这需求装置 prettier 插件:

写一个搭档见了会打你的 Prettier 插件

然后依照它的文档来装备 settings:

写一个搭档见了会打你的 Prettier 插件

直接这样配就行:

{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true
}

然后就每次保存自动用 prettier 格局化了:

写一个搭档见了会打你的 Prettier 插件

然后咱们开端完成打乱 imports 的功用。

要找到 imports 的代码,然后做一些修正,自然会想到经过 babel 的 api。

所以咱们能够这样写:

先引进这几个包:

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generate = require("@babel/generator").default;
const types = require("@babel/types");
const _ = require("lodash");

parser、traverse、generate 这几个包都很好懂,便是对应 babel 编译的 3 个过程的。

types 包是用于创立 AST 的。

由于有的包是 esm 导出的,所以用 commonjs 的方法导入需求取 .default 属性。

然后引进 lodash,一些东西函数。

第一步,调用 parser.parse 把代码转成 AST。

function myPreprocessor(code, options) {
  const ast = parser.parse(code, {
    plugins: ["typescript", "jsx"],
    sourceType: "module",
  });
}

如果 parse ts 和 jsx 代码,需求别离指定 typescript 和 jsx 插件。

sourceType 为 module 代表是有 import 或许 export 的模块代码。

第二步,把 imports 节点找出来。

const importNodes = [];
traverse(ast, {
    ImportDeclaration(path) {
      importNodes.push(_.clone(path.node));
      path.remove();
    }
});

遍历 AST,声明对 import 句子的处理。

具体什么代码是什么 AST 能够在 astexplorer.net 可视化检查:

写一个搭档见了会打你的 Prettier 插件

把 AST 节点用 lodash的 clone 函数仿制一份,放到数组里。

然后把原 AST 的 import 节点删掉。

第三步,对 imports 节点排序。

这一步就用 lodash 的 shuffle 函数就行:

const newImports = _.shuffle(importNodes);

第四步,打印成方针代码。

修正完 AST,把它打印成方针代码就好了,只不过现在是两部分代码,别离 generate,然后拼接起来:

const newAST = types.file({
    type: "Program",
    body: newImports,
});
const newCode =  generate(newAST).code +
    "n" +
    generate(ast, {
      retainLines: true,
    }).code;

import 句子需求包裹一层 file 的根结点,用 @babel/types 包的 api 创立:

写一个搭档见了会打你的 Prettier 插件

generate 的时分能够加一个 retainLines 为 true,也便是打印的时分保留在源码中的行数,这样打印完了行数不会变。

至此,这个随机打乱 imports 次序的 prettier 插件咱们就完成了。

完整代码如下:

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 types = require("@babel/types");
const _ = require("lodash");
function myPreprocessor(code, options) {
  const ast = parser.parse(code, {
    plugins: ["typescript", "jsx"],
    sourceType: "module",
  });
  const importNodes = [];
  traverse(ast, {
    ImportDeclaration(path) {
      importNodes.push(_.clone(path.node));
      path.remove();
    },
  });
  const newImports = _.shuffle(importNodes);
  const newAST = types.file({
    type: "Program",
    body: newImports,
  });
  const newCode =  generate(newAST).code +
    "n" +
    generate(ast, {
      retainLines: true,
    }).code;
  return newCode;
}
module.exports = {
  parsers: {
    babel: {
      ...babelParsers.babel,
      preprocess: myPreprocessor,
    },
    typescript: {
      ...typescriptParsers.typescript,
      preprocess: myPreprocessor,
    },
  },
};

咱们来试一下。

在 js/ts 文件中:

写一个搭档见了会打你的 Prettier 插件

在 vue 文件中:

写一个搭档见了会打你的 Prettier 插件

都收效了!(由于 prettier 插件有缓存,不收效的话关掉再打开编辑器就好了)

至此,咱们这个搭档发现了会打你的插件完成了!

有的同学说,但是在装备文件里会引进呀,这个也太明显了吧。

写一个搭档见了会打你的 Prettier 插件

其实不是的。默许 prettier 会加载 node_modules 下的所有 prettier-plugin-xx 的或许 @xxx/prettier-plugin-yy 的插件,不需求手动指定 plugins,这个只需咱们本地开发的时分需求这样指定。

比方社区有 prettier-plugin-sort-import 这个插件,用于 import 排序的:

写一个搭档见了会打你的 Prettier 插件

就不需求自己引进就能够直接做装备了:

写一个搭档见了会打你的 Prettier 插件

所以,只需装置这个打乱 imports 的 prettier 插件的依靠,prettier 就会自动应用,搭档不看 package.json 就很难发现。

总结

prettier 是基于编译技能完成的,前端的编译都是 parse、transform、generate 这三个过程,prettier 也是,只不过不需求中心的 transform。

它只包含 parser 和 printer 这两部分,但是支撑很多 language。每种 language 都有自己的 parser 和 printer。

写一个支撑新的言语的格局化的 prettier 插件,只需求一个导出 languages、parsers、pritners 装备的文件:

  • languages 部分指定言语的姓名,文件后缀名,用什么 parser 等。
  • parsers 部分完成字符串到 AST 的 parse,还能够指定预处理函数 preprocess。
  • printers 部分完成 AST 到 doc 的打印,doc 是 prettier 的一种中心格局,便于 prettier 再做一层统一的格局操控,之后再打印为字符串

今日咱们写的 prettier 插件并不是完成新言语的支撑,所以只用到了 preprocess 对代码做了预处理,经过 babel 的 api 来对代码做了 imports 的处理。

所以,会了 babel 插件就会写 prettier 插件对 js/ts 做预处理,同理,会了 postcss、posthtml 等也能够用来对 css、scss、less、html 等做预处理,在格局化代码时参加一些自定义逻辑。

最后,文中的 prettier 插件的案例仅仅学习用,不主张我们把这种插件引进项目,否则后果自负[旺柴]。