VSCode的Pettier插件原理和设置

2023-11-15

最近写了一个 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 插件的案例只是学习用,不建议大家把这种插件引入项目,否则后果自负[旺柴]。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

VSCode的Pettier插件原理和设置 的相关文章

  • 如何在javascript中动态向对象数组添加值?

    这是一个对象数组 var data label 1 value 12 label 1 value 12 label 1 value 12 label 1 value 12 我如何动态地为这些添加值 我尝试了以下代码但没有成功 var lab
  • 有什么方法可以复制 div 的渲染 HTML 吗?

    我正在开发电子邮件签名生成器 我想通过按按钮复制最终签名 而不是手动选择签名并将其复制到剪贴板 这意味着我需要它们的图像 文本和样式 我尝试了几种变体 包括 w3schools 的变体 但没有成功 其中一些只是复制文本 但没有样式 例子 h
  • jQuery UI Datepicker 奇怪的行为

    我在使用 jqueryUI 使用简单的日期选择器时遇到了一个奇怪的问题 我只想显示两个月的日历 包括上个月和当前月份 我使用了这段代码 function picker datepicker numberOfMonths 2 showCurr
  • Dialogflow Fulfillment Webhook 调用失败

    I am new to dialogflow fulfillment and I am trying to retrieve news from news API based on user questions I followed doc
  • Chrome 开发工具命中代码但未命中断点

    我在 chrome 开发工具上启用了断点 并且在一行上有一个断点 我知道 chrome 正在运行 因为我将断点放在具有以下语句的行上 alert why is this not breaking 如果我在本地主机中找到该文件 则断点有效 断
  • 仅在 Chrome 上我收到此错误:Uncaught TypeError: Illegal constructor [关闭]

    Closed 这个问题是无法重现或由拼写错误引起 help closed questions 目前不接受答案 当我在 Chrome 上加载 jQuery 时 我会收到此错误 Uncaught TypeError Illegal constr
  • 如何将类型添加到 Vite 库构建中?

    我按照vite文档使用图书馆模式 https vitejs dev guide build html library mode我能够生成一个工作组件库 我创建了该项目vue ts预设 在我的组件中 我定义了道具及其类型 并使用了一些接口 但
  • Pjax动画

    我终于让 pjax 工作了 但我还有另一个问题 如何添加一些 jquery 动画 如淡出 幻灯片旧内容和淡入 幻灯片新内容 默认情况下 pjax 只是更改内容 没有任何好看的效果 任何帮助将非常感激 此致 基本上 你有一堆事件 https
  • 修改 Twitter 帖子上可编辑 Div 的内容

    我正在编写一个 chrome 扩展 它可以帮助用户在 Twitter 上输入内容 当在 twitter 上写推文时 twitter 会打开一个可编辑的 div 容器 当用户输入内容时 twitter 大概正在使用某些网络框架 会生成子 di
  • Rangy:插入符号下的单词(再次)

    我正在尝试创建一个预输入代码以添加到 wysihtml5 富文本编辑器 基本上 我需要能够插入人员 标签引用 例如 Twitter Github Facebook 我发现一些人试图实现同样的事情的代码 http jsfiddle net A
  • 冒泡可用于图像加载事件吗?

    我可以用吗 window addEventListner 某种程度上来说 我所有的图像都有一个display none 图像加载后 我想设置display inline 这样我就可以规范下载图像时显示的内容 在这种情况下 我无法预加载图像
  • for循环中需要声明变量吗?

    有什么区别 for var i 0 i lt 5 i for i 0 i lt 5 i 是否有必要包含 var 关键字 我知道 var 关键字会影响变量范围 但我无法理解是否有必要在 for 循环中包含该关键字 在第二个示例中 您的变量是全
  • 在 Nodejs 中,如何停止 FOR 循环直到 MongoDB 调用返回

    我正在研究下面的代码片段 我有一个名为 stuObjList 的 JSON 对象数组 我想循环遍历数组以查找具有特定标志集的特定 JSON 对象 然后进行数据库调用以检索更多数据 当然 FOR 循环不会等待数据库调用返回并到达 j leng
  • 如何使用 javascript 更改文件扩展名

    有谁知道在 Javascript 中更改文件扩展名的简单方法吗 例如 我有一个带有 first docx 的变量 但我需要将其更改为 first html 这将改变字符串包含文件名 let file first docx file file
  • 使用ExternalInterface和IE从JavaScript获取Flash中的当前URL

    我正在尝试获取 Flash 播放器当前所在的 URL 不是 swf 文件的 URL 而是浏览器指向的 URL 到目前为止我已经使用过 var st String ExternalInterface call window location
  • 使用 File API polyfill 读取数据 URL

    我正在尝试使用文件 API 库 https github com mailru FileAPI https github com mailru FileAPI 作为不支持文件 API 的浏览器的后备 以便将文件作为数据 URL 读取并将其传
  • 使圆圈与 d3.js 上的多线匹配相同的颜色过滤?

    我有一个多线图 当按每种水果过滤时会更新 每条线条颜色对应不同的销售年份 在 的帮助下Shashank https stackoverflow com users 5569282 shashank 每个数据点线上的圆圈已添加到组中 而不是直
  • Keycloak-js updateToken(minValidity) 需要澄清

    我在Keycloak js中阅读了很多该方法的示例 但没有对以下方法进行明确的解释 updateToken minValidity number KeycloakPromise
  • Chrome 调试器注入 javascript

    我有这样的好奇心 是否可以以某种方式在我的页面中注入 javascript 并执行它并调试它 正如您在控制台中所做的那样 但在控制台中您无法暂停并观察变量 是否可以调试我通过控制台输入的代码 为什么无法调试通过 XHR 接收的代码 Than
  • 区分 NaN 输入和输入类型为“number”的空输入

    我想使用 type number 的表单输入 并且只允许输入数字

随机推荐

  • 0-1背包问题(双限制条件)

    给定n种物品和一个背包 物品i的重量是wi 体积是bi 其价值为vi 背包的容量为c 容积为d 问应如何选择装入背包中的物品 使得装入背包中物品的总价值最大 在选择装入背包的物品时 对每种物品只有两个选择 装入或不装入 且不能重复装入 输入
  • 华为校招机试 - 发广播(Java)

    题目描述 某地有N个广播站 站点之间有些有连接 有些没有 有连接的站点在接受到广播后会互相发送 给定一个N N的二维数组matrix 数组的元素都是字符 0 或者 1 matrix i j 1 代表i和j站点之间有连接 matrix i j
  • 使用R语言和LSTM实现时间序列异常检测

    目录 引言 长短期记忆网络 LSTM 时间序列异常检测的步骤 准备工作 数据获取
  • ES6 Iterator

    不同数据集合怎么用统一的方式读取 可以用for of循环了 转载于 https www cnblogs com moneyss p 10641507 html
  • 双向链表,单向链表//循环

    head h ifndef HEAD H define HEAD H include
  • 将Oracle zip安装文件转换成Linux可用的iso镜像

    在Oracle官网下载得到的zip安装文件 要在VMware虚拟机上挂载iso安装Oracle 所以想将zip文件转化为iso 除了使用UltraISO软件外 这里尝试使用Linux mkisofs命令生成ISO 9660文件 mkisof
  • IOS 网络初探(一) - NSURLConnection

    在IOS中 除了最基本的socket外 苹果提供了NSURLConnection类来实现网络通信 请求服务器数据 GET方式 请求服务器数据分成异步和同步两种方式 先来看看异步 非阻塞 NSURL url NSURL URLWithStri
  • LPDDR4 JEDEC标准测试实例解析--写操作

    在LPDDR4的JEDEC标准中 写操作相关的时序参数要求基本都是围绕DQS信号 如下图所示 图中的单位 tCK 指的是时钟信号的工作周期 tCK的测试方法如下图所示 Write preamble and postamble设置 这一步对于
  • 3.1 Git 分支 - 分支简介

    3 1 Git 分支 分支简介 版本说明 版本 作者 日期 备注 0 1 loon 2019 3 23 初稿 目录 文章目录 3 1 Git 分支 分支简介 版本说明 目录 1 分支简介 Figure 9 首次提交对象及其树结构 Figur
  • Pycharm

    运行该程序 窗口底部会出现终端面板 Terminal pane 显示你的代码输出结果 使用Pycharm在运行过程中 查看每个变量的操作 show variables pytharm能不能像MATLAB一样显示中间变量的值呢 答案是可以的
  • Vue3.0 组合式 API 分析与实践

    本文带大家深入理解组合式 API 的设计详情 同时加入我们的实践经验总结 01 背景 Vue3 x 版本的出现带来了许多令人眼前一亮的新特性 其中组合式 API Composition API 一组附加的 基于功能的 API 被作为一种新的
  • 51单片机学习之-中断

    中断 在51单片机中共5个中断源 分别为 总的来说分为内部中断与外部中断 先说内部中断 也就是上图的两种定时器中断 T0 T1 它们的使用与4个寄存器有关 现在来介绍四种寄存器 中断允许寄存器 中断优先级寄存器 定时器工作方式寄存器 定时器
  • 151. 反转字符串中的单词

    1 题目地址 151 反转字符串中的单词 2 题目描述 给你一个字符串 s 请你反转字符串中 单词 的顺序 单词 是由非空格字符组成的字符串 s 中使用至少一个空格将字符串中的 单词 分隔开 返回 单词 顺序颠倒且 单词 之间用单个空格连接
  • vba与python的优缺点_对比VBA学Python操作Excel

    关于先学VBA还是先Python 或者作为办公族选什么 这些问题 我已经写了专题 就不再啰嗦 建议先阅读 讨论 学习VBA还是Python 但是大家一直还是被Python吸引 所以小编就说一期Python操作Excel吧 大部分教编程的 对
  • tensorflow 混合精度训练相关报错

    如题 在混合精度开发的时候遇到的报错 先列环境 V100服务器 ubuntu16 04 tensorflow 1 14 0 cuda10 0 python3 6 报错一 ValueError opt must be an instance
  • MySQL进阶语句

    目录 常用查询 order by按关键字排序 升序排序 降序排序 结合where进行条件过滤再排序 多字段排序 and or判断 and or 且与或的使用 嵌套 多条件使用 distinct 查询不重复记录 GROUP BY对结果进行分组
  • 经济,jiaxi

  • moviepy音视频开发:音频合成类CompositeAudioClip介绍

    前往老猿Python博文目录 CompositeAudioClip是AudioClip的直接子类 用于将几个音频剪辑合成为一个音频剪辑 CompositeAudioClip类只有一个构造方法 在构造方法内定义了一个内嵌函数make fram
  • 蓝屏错误代码分析和解决方法

    文字代码 可能的原因 可尝试的解决方法 0x00000001 APC INDEX MISMATCH 驱动出现问题 或安装了错误 不兼容的驱动 更新 重新安装相应设备官网的驱动 0x00000005 INVALID PROCESS ATTAC
  • VSCode的Pettier插件原理和设置

    最近写了一个 Prettier 插件 可以达到这样的效果 同事每次保存代码的时候 import 语句的顺序都会随机变 但是他去 prettier 配置文件里还啥也发现不了 于是就会一脸懵逼 那么这个同事发现了会打你的 prettier 插件