程序员

【实战指南】如何写一款小程序 Prettier 插件

作者:admin 2021-04-24 我要评论

Prettier 是一款开箱即用的代码格式化工具 主要特点是配置简单、便于集成、支持扩展。Prettier 原本聚焦于 Web 开发领域 由于表现优秀 社区也利用其扩展机制支持...

在说正事之前,我要推荐一个福利:你还在原价购买阿里云、腾讯云、华为云服务器吗?那太亏啦!来这里,新购、升级、续费都打折,能够为您省60%的钱呢!2核4G企业级云服务器低至69元/年,点击进去看看吧>>>)

Prettier 是一款开箱即用的代码格式化工具 主要特点是配置简单、便于集成、支持扩展。Prettier 原本聚焦于 Web 开发领域 由于表现优秀 社区也利用其扩展机制支持了 Java、PostgreSQL 等语言的格式化。

无需赘言 开发团队借助 Prettier 让团队成员保持一致的代码风格 不完全等同于代码规范 非常有必要 毕竟代码虽然是机器运行的 但主要是人在阅读 而且强迫人接受一种他可能不喜欢的风格 自然不如让工具自动统一来的容易。

其他内容大家可以查看官方文档了解更多 此处不过多介绍了。

认识插件

Prettier 主要聚焦于 Web 开发领域 因此 JavaScript、CSS 和 HTML 是默认支持的 甚至 JSX 和 Vue 也是内置支持的。

但是显然 假如你发明了一种全新的 DSL Prettier 是不认的。那怎么办 写一款 插件

所以 插件就是让 Prettier 能够支持你自己的编程语言的一种方式。本质上 它就是一个 普通的 JavaScript Module 暴露以下 5 个模块

诸位明鉴 下文代码是以 TypeScript 写就的。

// index.ts
import { SupportLanguage, Parser, Printer, SupportOption, ParserOptions } from prettier 
// 支持的语言列表
export const languages: Partial SupportLanguage 
// 每个语言对应的 parser
export const parsers: Record string, Parser 
// 核心的格式化逻辑
export const printers: Record string, Printer 
// 可选 插件的自定义配置项 此处 PluginOptions 需自行定义
export const options: Record keyof PluginOptions, SupportOption 
// 可选 默认配置项
export const defaultOptions: Partial ParserOptions 
复制代码
写一个小程序 AXML 插件吧

阿里小程序 钉钉小程序、支付宝小程序等等 的上层 DSL 早已统一 但是一直都没有 AXML 自动格式化工具。

Prettier 对 JS/TS 是内置支持的 .acss 其实就是 CSS。

可能有人尝试过将 .axml 设置为 XML 文件类型来做格式化 但肯定效果不理想。因为无法格式化 AXML 文件中的 JS 表达式。

今天我们就写一个起码的 AXML 的 Prettier 插件吧。

languages

我们为小程序 AXML 这门语言命名为 axml 其 parser 列表是 [ axml ] a.1 。也就是说可以为它指定多个 parser 但通常一个就够了。我们就使用 axml 这个 parser 其定义见下文 。

parser 是将源代码解析为 AST 的工具。

// index.ts
import { SupportLanguage } from prettier 
export const languages: Partial SupportLanguage [] [
 name: axml ,
 parsers: [ axml ], // (a.1)
 extensions: [ .axml ], // (a.2)
复制代码
parsers

在 index.ts 中新增 export const parsers。

// index.ts
import { SupportLanguage, Parser } from prettier 
import parse from ./parse 
// prettier 指定 node 参数为 any 因为不同 parser 返回的 node 类型不尽相同
function locStart(node: any): number {
 return node.startIndex;
function locEnd(node: any): number {
 return node.endIndex;
export const languages: Partial SupportLanguage [] [
 name: axml ,
 parsers: [ axml ],
 extensions: [ .axml ],
export const parsers: Record string, Parser {
 // 注意此处的 key 必须要与 languages 的 parsers 对应
 axml: {
 parse, // (b.1)
 locStart,
 locEnd,
 // 为 ast 格式命个名 后面会用到
 astFormat: axml-ast ,
复制代码

parse b.1 是一个函数 在揭开它的面纱之前 我们先要确定解析 AXML 的 parser。

类 XML 的 DSL 市面上有很多 parser 我们就和小程序官方实现保持一致 使用 htmlparser2 来解析 AXML。所以 parse b.1 的定义如下

// parse.ts
import { parseDOM } from htmlparser2 
import { Node } from domhandler 
export default function parse(text: string): Node[] {
 const dom parseDOM(text, {
 xmlMode: true,
 withStartIndices: true,
 withEndIndices: true,
 return dom;
复制代码

htmlparser2 解析出来的 AST 相对简单 可以查看 这里 感受一下。

这里实际上还有一个棘手的问题 AXML 中的“无值属性” 如 view someAttr / 其实是模仿了 JSX 的语义 即”布尔属性“ view someAttr / 等价于 view someAttr {true} / JSX 语法 但在 XML 以及 htmlparser2 这个 parser 中 它被解析为 view someAttr / 。这个需要我们特殊处理。

接下来是核心逻辑了。

printers
// index.ts
import { SupportLanguage, Parser, Printer } from prettier 
import parse from ./parse 
import print from ./print 
import embed from ./embed 
// ... 省略
export const printers: Record string, Printer {
 // 对应 parsers 中的 astFormat
 axml-ast : {
 print, // (c.1)
 embed, // (c.2)
复制代码

print c.1 函数负责目标语言源代码本身的格式化逻辑 embed c.2 函数则用来处理目标语言当中内嵌的其他语言的格式化。

对于小程序 AXML 来说 htmlparser2 解析出来的 AST 只有以下 3 种类型 node.type

tag - 标签 view /view 等等text - 标签内的文本comment - 注释 !-- -- 和 HTML 注释格式一致print

在 print c.1 中

// print.ts
import { FastPath, Doc, ParserOptions, doc } from prettier 
const { concat } doc.builders;
export default function print(
 path: FastPath,
 _options: ParserOptions,
 _print: (path: FastPath) Doc // (c.3)
): Doc {
 // 获取 AST 中的 node
 const node path.getValue();
 if (!node) return 
 // htmlparser2 的 AST 是一个数组 因此我们需要调用 _print 它会递归调用我们自己定义的 print
 if (Array.isArray(node)) {
 return concat(path.map(_print));
 // 继续判断 node.type 返回不同内容 限于篇幅 省略
复制代码

每一个格式化的代码片段 Prettier 将之称为 Doc c.3 。

需要注意的是 AXML 中有两个地方会存在 JS 表达式 expression 标签 tag 的属性 attribute 和文本 text 它们存在于 {{}} 当中。这些表达式也需要格式化

要处理 {{}} 中的 JS 表达式 则需要通过 embed c.2 在 embed 函数中可以调用其他 parser 来处理目标文本 用法见下文 。因为是 JS 表达式 我们调用 Prettier 内置的 babel parser 来处理 JS 表达式就行了。

这就要求我们先解析 {{}}。{{}} 格式是非常流行的所谓 mustache 风格 出于教学目的 我们直接用 mustache.js 来解析。

实际上简单地用 mustache.js 会有问题 因为类似 {{!a b}} 这样的片段在 mustache.js 是有语义的 {{! 表示注释 但在 AXML 里 它仅表示 !a b 表达式。这里我们就不展开了。 另 小程序框架是自行实现了一个 {{}} 的解析器。

embed

Prettier 在执行时 embed c.2 会优先于 print c.1 执行 如果 embed 返回了非 null 的值 则结束格式化 反之 继续执行 print 中的逻辑。

在 embed c.2 中

// embed.ts
import { FastPath, Doc, ParserOptions, Options, doc } from prettier 
import { DataNode, Element } from domhandler 
import { parse } from mustache 
const {
 group, // (d.1) Prettier 最基本的方法 会根据 printWidth 等配置项自动换行 或不换行 
 concat, // 拼接 Doc 的方法 类似 Array.prototype.concat
 line, // 一个换行 如果父级 group(d.1) 后不需换行 则将其转换为一个空格
 indent, // 一个缩进 如果父级 group(d.1) 后不需换行 则忽略
 softline, // 一个换行 如果父级 group(d.1) 后不需换行 则忽略
} doc.builders;
export default function embed(
 path: FastPath,
 print: (path: FastPath) Doc,
 textToDoc: (text: string, options: Options) Doc, // (d.2)
 options: ParserOptions // (d.3)
): Doc | null {
 const node path.getValue();
 // 返回 null 则交给 print(c.1) 继续执行
 if (!node || !node.type) return null;
 switch (node.type) {
 // 文本类型
 case text :
 const text (node as DataNode).data;
 // 1. 调用 mustache.parse 解析文本
 // 2. 调用 textToDoc(d.2) 格式化 JS 表达式 如有 
 // 3. 拼接 {{ 、格式化好的表达式、 }} 如有 
 // 4. 调用 group(d.1) 方法包裹前面拼接好的内容
 // 标签类型
 case tag :
 // 1. 如果有 children 递归调用
 // 2. 提取 attribute 调用 mustache.parse 解析文本
 // 3. 调用 textToDoc(d.2) 格式化 JS 表达式 如有 
 // 4. 拼接 {{ 、格式化好的表达式、 }} 如有 
 // 5. 调用 group(d.1) 方法包裹前面拼接好的内容
 default:
 // 返回 null 则交给 print(c.1) 继续执行
 return null;
复制代码

特别说明一下 textToDoc d.2 方法 要解析 JS 表达式 按如下方式使用即可

// embed.ts
// ...
const doc: Doc textToDoc(expressionExtractedByMustache, {
 parser: babel ,
 semi: false,
 singleQuote: true,
return indent(concat([softline, doc]));
复制代码

options d.3 参数就是我们指定的一些配置项了 也包含自定义的配置项 见下文 。

此外 关于 group、indent 等方法 建议大家 查阅文档

当然还有一些需要特别注意的地方 比如 style 属性可以直接这样写 style {{height: 100% , width: 100% }} 实际上所有的对象型属性都可以简化写成这样 大括号里提取出来的文本并不是合法的 JS 表达式 需要我们特殊处理。此种细节都要考虑到。

options

index.ts 中的 export const options 用于指定插件所支持的自定义配置项。

假如我们希望小程序 AXML 插件支持一个 axmlBracketSameLine 的配置项 其作用类似 jsxBracketSameLine

那么可以这样定义

// index.ts
import { SupportLanguage, Parser, Printer, SupportOption, ParserOptions } from prettier 
// ... 省略
interface PluginOptions {
 axmlBracketSameLine: boolean;
// 插件自定义的配置项
export const options: Record keyof PluginOptions, SupportOption {
 axmlBracketSameLine: {
 name: axmlBracketSameLine ,
 category: Global ,
 type: boolean ,
 default: false,
 description: Put the of a multiline AXML element on a new line ,
复制代码

这样 上文的 options d.3 参数中就可以读到 options.axmlBracketSameLine 以此决定是否要将开标签的结束字符 放置在同一行。

defaultOptions

插件的默认配置项 会覆盖 Prettier 的同名默认配置项 可以指定内置配置项和插件自定义配置项。

例如

// index.ts
import { SupportLanguage, Parser, Printer, SupportOption, ParserOptions } from prettier 
// ... 省略
export const defaultOptions: Partial ParserOptions {
 tabWidth: 2, // 2 个空格缩进
 printWidth: 80, // 打印宽度 80
复制代码

到这里 我们的 AXML 插件就开发完成了。

使用插件

插件使用起来非常简单 只需将我们的插件发布到 npm 或 yarn 或私有化的 npm 服务如 tnpm 且其 package 名称以下述字符开头 Prettier 执行时就会自动加载插件、自动识别文件类型并调用对应插件

prettier/plugin-prettier-plugin- scope /prettier-plugin-

假设我们将小程序 AXML 插件发布到 npm 上 并命名为 prettier-plugin-axml 那么只需要在你的项目中安装

npm i --save-dev prettier prettier-plugin-axml
复制代码

然后执行

./node_modules/.bin/prettier --write src/**/*.axml 
复制代码

就大功告成了。

因为我们已经在 extensions 中 a.2 指定了文件后缀为 .axml 所以 prettier 会自动为此类文件匹配我们的插件 因此不用显式指定 plugin。

总结

概括来说 要开发一个 Prettier 插件 总共分三步

用一个或多个 parser 把源代码解析为 AST 调用 Prettier 的 API 按需加入换行、空格、缩进等 没了。

是不是很简单呢

参考链接Prettier plugin 文档prettier/plugin-xml


作者 钉钉前端团队


本文转自网络,原文链接:https://developer.aliyun.com/article/783725

版权声明:本文转载自网络,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。本站转载出于传播更多优秀技术知识之目的,如有侵权请联系QQ/微信:153890879删除

相关文章
  • Wordpress 计划放弃对 IE 11 的支持

    Wordpress 计划放弃对 IE 11 的支持

  • 微软 Windows10 Reunion 预览版 v0.5

    微软 Windows10 Reunion 预览版 v0.5

  • 鸿蒙内核源码分析(异常接管篇) | 中文

    鸿蒙内核源码分析(异常接管篇) | 中文

  • 如何在Windows 10上安装WSL 2(最新教

    如何在Windows 10上安装WSL 2(最新教

腾讯云代理商
海外云服务器