知識
不管是網(wǎng)站,軟件還是小程序,都要直接或間接能為您產(chǎn)生價(jià)值,我們在追求其視覺表現(xiàn)的同時(shí),更側(cè)重于功能的便捷,營銷的便利,運(yùn)營的高效,讓網(wǎng)站成為營銷工具,讓軟件能切實(shí)提升企業(yè)內(nèi)部管理水平和效率。優(yōu)秀的程序?yàn)楹笃谏壧峁┍憬莸闹С郑?
您當(dāng)前位置>首頁 » 新聞資訊 » 小程序相關(guān) >
【實(shí)戰(zhàn)指南】如何寫一款小程序 Prettier 插件
發(fā)表時(shí)間:2021-1-5
發(fā)布人:葵宇科技
瀏覽次數(shù):86
Prettier 是一款開箱即用的代碼格式化工具,主要特點(diǎn)是配置簡單、便于集成、支持?jǐn)U展。Prettier 原本聚焦于 Web 開發(fā)領(lǐng)域,由于表現(xiàn)優(yōu)秀,社區(qū)也利用其擴(kuò)展機(jī)制支持了 Java、PostgreSQL 等語言的格式化。
無需贅言,開發(fā)團(tuán)隊(duì)借助 Prettier 讓團(tuán)隊(duì)成員保持一致的代碼風(fēng)格(不完全等同于代碼規(guī)范)非常有必要,畢竟代碼雖然是機(jī)器運(yùn)行的,但主要是人在閱讀;而且強(qiáng)迫人接受一種他可能不喜歡的風(fēng)格,自然不如讓工具自動統(tǒng)一來的容易。
其他內(nèi)容大家可以查看官方文檔了解更多,此處不過多介紹了。
認(rèn)識插件
Prettier 主要聚焦于 Web 開發(fā)領(lǐng)域,因此 JavaScript、CSS 和 HTML 是默認(rèn)支持的,甚至 JSX 和 Vue 也是內(nèi)置支持的。
但是顯然,假如你發(fā)明了一種全新的 DSL,Prettier 是不認(rèn)的。那怎么辦?寫一款 插件!
所以,插件就是讓 Prettier 能夠支持你自己的編程語言的一種方式。本質(zhì)上,它就是一個(gè) 普通的 JavaScript Module,暴露以下 5 個(gè)模塊:
諸位明鑒,下文代碼是以 TypeScript 寫就的。
// index.ts
import { SupportLanguage, Parser, Printer, SupportOption, ParserOptions } from 'prettier';
// 支持的語言列表
export const languages: Partial<SupportLanguage>[];
// 每個(gè)語言對應(yīng)的 parser
export const parsers: Record<string, Parser>;
// 核心的格式化邏輯
export const printers: Record<string, Printer>;
// 可選,插件的自定義配置項(xiàng),此處 PluginOptions 需自行定義
export const options: Record<keyof PluginOptions, SupportOption>;
// 可選,默認(rèn)配置項(xiàng)
export const defaultOptions: Partial<ParserOptions>;
復(fù)制代碼
寫一個(gè)小程序 AXML 插件吧
阿里小程序(釘釘小程序、支付寶小程序等等)的上層 DSL 早已統(tǒng)一,但是一直都沒有 AXML 自動格式化工具。
Prettier 對 JS/TS 是內(nèi)置支持的,
.acss
其實(shí)就是 CSS。
可能有人嘗試過將 .axml
設(shè)置為 XML 文件類型來做格式化,但肯定效果不理想。因?yàn)闊o法格式化 AXML 文件中的 JS 表達(dá)式。
今天我們就寫一個(gè)起碼的 AXML 的 Prettier 插件吧。
languages
我們?yōu)樾〕绦?AXML 這門語言命名為 axml
,其 parser 列表是 ['axml']
(a.1)。也就是說可以為它指定多個(gè) parser,但通常一個(gè)就夠了。我們就使用 axml
這個(gè) parser(其定義見下文)。
parser
是將源代碼解析為 AST 的工具。
// index.ts
import { SupportLanguage } from 'prettier';
export const languages: Partial<SupportLanguage>[] = [
{
name: 'axml',
parsers: ['axml'], // (a.1)
extensions: ['.axml'], // (a.2)
},
];
復(fù)制代碼
parsers
在 index.ts 中新增 export const parsers
。
// index.ts
import { SupportLanguage, Parser } from 'prettier';
import parse from './parse';
// prettier 指定 `node` 參數(shù)為 any,因?yàn)椴煌?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 對應(yīng)
axml: {
parse, // (b.1)
locStart,
locEnd,
// 為 ast 格式命個(gè)名,后面會用到
astFormat: 'axml-ast',
},
};
復(fù)制代碼
parse
(b.1)是一個(gè)函數(shù),在揭開它的面紗之前,我們先要確定解析 AXML 的 parser。
類 XML 的 DSL 市面上有很多 parser,我們就和小程序官方實(shí)現(xiàn)保持一致,使用 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;
}
復(fù)制代碼
htmlparser2 解析出來的 AST 相對簡單,可以查看 這里 感受一下。
這里實(shí)際上還有一個(gè)棘手的問題,AXML 中的“無值屬性”(如:
<view someAttr />
)其實(shí)是模仿了 JSX 的語義,即”布爾屬性“(<view someAttr />
等價(jià)于<view someAttr={true} />
(JSX 語法)),但在 XML 以及 htmlparser2 這個(gè) parser 中,它被解析為<view someAttr="" />
。這個(gè)需要我們特殊處理。
接下來是核心邏輯了。
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> = {
// 對應(yīng) parsers 中的 astFormat
'axml-ast': {
print, // (c.1)
embed, // (c.2)
},
};
復(fù)制代碼
print
(c.1)函數(shù)負(fù)責(zé)目標(biāo)語言源代碼本身的格式化邏輯,embed
(c.2)函數(shù)則用來處理目標(biāo)語言當(dāng)中內(nèi)嵌的其他語言的格式化。
對于小程序 AXML 來說,htmlparser2 解析出來的 AST 只有以下 3 種類型(node.type
):
tag
- 標(biāo)簽,<view></view>
等等text
- 標(biāo)簽內(nèi)的文本comment
- 注釋,<!-- -->
,和 HTML 注釋格式一致
在 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 是一個(gè)數(shù)組,因此我們需要調(diào)用 _print,它會遞歸調(diào)用我們自己定義的 print
if (Array.isArray(node)) {
return concat(path.map(_print));
}
// 繼續(xù)判斷 node.type,返回不同內(nèi)容,限于篇幅,省略
}
復(fù)制代碼
每一個(gè)格式化的代碼片段,Prettier 將之稱為 Doc
(c.3)。
需要注意的是,AXML 中有兩個(gè)地方會存在 JS 表達(dá)式(expression),標(biāo)簽(tag
)的屬性(attribute
)和文本(text
),它們存在于 {{}}
當(dāng)中。這些表達(dá)式也需要格式化!
要處理 {{}}
中的 JS 表達(dá)式,則需要通過 embed
(c.2),在 embed
函數(shù)中可以調(diào)用其他 parser 來處理目標(biāo)文本(用法見下文)。因?yàn)槭?JS 表達(dá)式,我們調(diào)用 Prettier 內(nèi)置的 babel
parser 來處理 JS 表達(dá)式就行了。
這就要求我們先解析 {{}}
。{{}}
格式是非常流行的所謂 mustache
風(fēng)格,出于教學(xué)目的,我們直接用 mustache.js 來解析。
實(shí)際上簡單地用 mustache.js 會有問題,因?yàn)轭愃?
{{!a && b}}
這樣的片段在 mustache.js 是有語義的({{!
表示注釋);但在 AXML 里,它僅表示!a && b
表達(dá)式。這里我們就不展開了。 另,小程序框架是自行實(shí)現(xiàn)了一個(gè){{}}
的解析器。
embed
Prettier 在執(zhí)行時(shí),embed
(c.2)會優(yōu)先于 print
(c.1)執(zhí)行:如果 embed
返回了非 null
的值,則結(jié)束格式化;反之,繼續(xù)執(zhí)行 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 最基本的方法,會根據(jù) printWidth 等配置項(xiàng)自動換行(或不換行)
concat, // 拼接 Doc 的方法,類似 Array.prototype.concat
line, // 一個(gè)換行,如果父級 group(d.1) 后不需換行,則將其轉(zhuǎn)換為一個(gè)空格
indent, // 一個(gè)縮進(jìn),如果父級 group(d.1) 后不需換行,則忽略
softline, // 一個(gè)換行,如果父級 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) 繼續(xù)執(zhí)行
if (!node || !node.type) return null;
switch (node.type) {
// 文本類型
case 'text':
const text = (node as DataNode).data;
// 1. 調(diào)用 mustache.parse 解析文本
// 2. 調(diào)用 textToDoc(d.2) 格式化 JS 表達(dá)式(如有)
// 3. 拼接 `{{`、格式化好的表達(dá)式、`}}`(如有)
// 4. 調(diào)用 group(d.1) 方法包裹前面拼接好的內(nèi)容
// 標(biāo)簽類型
case 'tag':
// 1. 如果有 children,遞歸調(diào)用
// 2. 提取 attribute,調(diào)用 mustache.parse 解析文本
// 3. 調(diào)用 textToDoc(d.2) 格式化 JS 表達(dá)式(如有)
// 4. 拼接 `{{`、格式化好的表達(dá)式、`}}`(如有)
// 5. 調(diào)用 group(d.1) 方法包裹前面拼接好的內(nèi)容
default:
// 返回 null,則交給 print(c.1) 繼續(xù)執(zhí)行
return null;
}
}
復(fù)制代碼
特別說明一下 textToDoc
(d.2)方法,要解析 JS 表達(dá)式,按如下方式使用即可:
// embed.ts
// ...
const doc: Doc = textToDoc(expressionExtractedByMustache, {
parser: 'babel',
semi: false,
singleQuote: true,
});
return indent(concat([softline, doc]));
復(fù)制代碼
options(d.3)參數(shù)就是我們指定的一些配置項(xiàng)了,也包含自定義的配置項(xiàng)(見下文)。
此外,關(guān)于 group
、indent
等方法,建議大家 查閱文檔。
當(dāng)然還有一些需要特別注意的地方,比如
style
屬性可以直接這樣寫style="{{height: '100%', width: '100%'}}"
(實(shí)際上所有的對象型屬性都可以簡化寫成這樣),大括號里提取出來的文本并不是合法的 JS 表達(dá)式,需要我們特殊處理。此種細(xì)節(jié)都要考慮到。
options
index.ts 中的 export const options
用于指定插件所支持的自定義配置項(xiàng)。
假如我們希望小程序 AXML 插件支持一個(gè) axmlBracketSameLine
的配置項(xiàng),其作用類似 jsxBracketSameLine。
那么可以這樣定義:
// index.ts
import { SupportLanguage, Parser, Printer, SupportOption, ParserOptions } from 'prettier';
// ... 省略
interface PluginOptions {
axmlBracketSameLine: boolean;
}
// 插件自定義的配置項(xiàng)
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',
},
};
復(fù)制代碼
這樣,上文的 options(d.3)參數(shù)中就可以讀到 options.axmlBracketSameLine
,以此決定是否要將開標(biāo)簽的結(jié)束字符 >
放置在同一行。
defaultOptions
插件的默認(rèn)配置項(xiàng),會覆蓋 Prettier 的同名默認(rèn)配置項(xiàng),可以指定內(nèi)置配置項(xiàng)和插件自定義配置項(xiàng)。
例如:
// index.ts
import { SupportLanguage, Parser, Printer, SupportOption, ParserOptions } from 'prettier';
// ... 省略
export const defaultOptions: Partial<ParserOptions> = {
tabWidth: 2, // 2 個(gè)空格縮進(jìn)
printWidth: 80, // 打印寬度 80
};
復(fù)制代碼
到這里,我們的 AXML 插件就開發(fā)完成了。
使用插件
插件使用起來非常簡單,只需將我們的插件發(fā)布到 npm(或 yarn,或私有化的 npm 服務(wù)如 tnpm),且其 package 名稱以下述字符開頭,Prettier 執(zhí)行時(shí)就會自動加載插件、自動識別文件類型并調(diào)用對應(yīng)插件:
@prettier/plugin-
prettier-plugin-
@<scope>/prettier-plugin-
假設(shè)我們將小程序 AXML 插件發(fā)布到 npm 上,并命名為 prettier-plugin-axml
,那么只需要在你的項(xiàng)目中安裝:
npm i --save-dev prettier prettier-plugin-axml
復(fù)制代碼
然后執(zhí)行:
./node_modules/.bin/prettier --write "src/**/*.axml"
復(fù)制代碼
就大功告成了。
因?yàn)槲覀円呀?jīng)在
extensions
中(a.2)指定了文件后綴為.axml
,所以 prettier 會自動為此類文件匹配我們的插件,因此不用顯式指定plugin
。
總結(jié)
概括來說,要開發(fā)一個(gè) Prettier 插件,總共分三步:
- 用一個(gè)或多個(gè) parser 把源代碼解析為 AST;
- 調(diào)用 Prettier 的 API 按需加入換行、空格、縮進(jìn)等;
- 沒了。
是不是很簡單呢?
作者:釘釘前端團(tuán)隊(duì)
來源:掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。
相關(guān)案例查看更多
相關(guān)閱讀
- 網(wǎng)站建設(shè)專家
- 網(wǎng)站建設(shè)服務(wù)
- 百度人工排名
- 出入小程序
- 小程序設(shè)計(jì)
- 做網(wǎng)站
- 云南網(wǎng)站建設(shè)靠譜公司
- 微信分銷
- web學(xué)習(xí)路線
- 智慧農(nóng)貿(mào)市場
- painter
- 表單
- 云南小程序開發(fā)公司哪家好
- 手機(jī)網(wǎng)站建設(shè)
- 報(bào)廢車拆解系統(tǒng)
- 云南網(wǎng)站建設(shè)制作
- 軟件開發(fā)
- 網(wǎng)絡(luò)公司聯(lián)系方式
- flex
- 麗江小程序開發(fā)
- APP
- 退款
- 報(bào)廢車管理
- 云南網(wǎng)站建設(shè)百度
- 正規(guī)網(wǎng)站建設(shè)公司
- 花農(nóng)小程序
- 云南網(wǎng)站建設(shè)專業(yè)品牌
- 小程序開發(fā)平臺前十名
- 網(wǎng)站建設(shè)方案 doc
- 云南網(wǎng)站建設(shè)首選