当前位置:主页 > 查看内容

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

发布时间:2021-04-24 00:00| 位朋友查看

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

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
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!
上一篇:快速掌握 PolarDB-X 拆分规则变更能力! 下一篇:没有了

推荐图文


随机推荐