最近写了一个 Prettier 插件,能够达到这样的作用:
搭档每次保存代码的时分,import 句子的次序都会随机变。
但是他去 prettier 装备文件里还啥也发现不了。
所以就会一脸懵逼。
那么这个搭档发现了会打你的 prettier 插件是怎样完成的呢?
Prettier 的原理
前端的编译东西都是从源码到源码的转化,所以都是 parse、transform、generate 这三步:
parse 是把源码字符串转化成 AST 的对象树,transform 是对 AST 做增删改,而 generate (或许叫 printer)是把转化后的 AST 递归打印成方针代码。
prettier 其实也基于编译完成的,只不过不做中心的转化,仅仅 parse 和 print(也能够叫 generate),所以分为两步:
它首要的格局化功用都是在 print 阶段做的。
整个流程还是比较简单的,那它是怎样支撑那么多言语的呢?
当然是每种言语有各自的 parser 和 printer 呀!
比方它内置了这些 parser:
ts、js、css、scss、html 等都支撑,便是由于不同的后缀名会启用不同的 parser 和 printer。
并且,它是支撑插件的,你完全能够经过 prettier 插件来完成任何一种言语的格局化。
很简单想到,插件自然也是指定什么后缀名的文件,用什么 parser 和 printer,所以是这样的格局:
咱们看一个真实的插件,格局化 nginx 装备文件的 prettier 插件 prettier-plugin-nginx:
languages 部分便是指定这个言语的姓名,什么后缀名的文件,用什么 parser。
然后 parser 部分便是完成字符串到 AST 的 parse:
printer 部分便是把 AST 打印成代码:
当然,prettier 插件里的 printer 不是直接打印成字符串,而是打印成一种 Doc 的格局,便于 prettier 再做一层格局操控。
总归,想扩展一种新的言语的格局化,只需完成 parser 和 printer 就好了。
但前面那个修正 imports 的插件也不是新言语呀,不是 js/ts 代码么?这种怎样写 prettier 插件?
其实 parser 还能够指定一个预处理器:
在 parse 之前对内容做一些修正:
所以完整的 prettier 流程应该是这样的:
那咱们写一个 prettier 插件,对 js/ts/vue/flow 的代码都做下同样的预处理,不就能完成随机打乱 imports 的作用么~
咱们来写一下:
只需求对 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 插件收效了!
并且除了 js、ts,在 vue 文件里也会收效:
这是由于在 parse vue 的 sfc 的时分,script 的部分还是用 babel 或许 tsc 的。
当然,一般咱们会装备 vscode 在保存的时分自动调用 prettier 来格局化。
这需求装置 prettier 插件:
然后依照它的文档来装备 settings:
直接这样配就行:
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
}
然后就每次保存自动用 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 可视化检查:
把 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 创立:
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 文件中:
在 vue 文件中:
都收效了!(由于 prettier 插件有缓存,不收效的话关掉再打开编辑器就好了)
至此,咱们这个搭档发现了会打你的插件完成了!
有的同学说,但是在装备文件里会引进呀,这个也太明显了吧。
其实不是的。默许 prettier 会加载 node_modules 下的所有 prettier-plugin-xx 的或许 @xxx/prettier-plugin-yy 的插件,不需求手动指定 plugins,这个只需咱们本地开发的时分需求这样指定。
比方社区有 prettier-plugin-sort-import 这个插件,用于 import 排序的:
就不需求自己引进就能够直接做装备了:
所以,只需装置这个打乱 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 插件的案例仅仅学习用,不主张我们把这种插件引进项目,否则后果自负[旺柴]。