前端大概要知道的 AST
大厂技术高级前端Node进阶点击上方程序员成长指北,关注公众号回复1,加入高级Node交流群认识 AST定义: 在计算机科学中,抽象语法树是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。从定义中我们只需要知道一件事就行,那就是 AST 是一种树形结...
大厂技术 高级前端 Node进阶
点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群
认识 AST
定义: 在计算机科学中,抽象语法树是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。
从定义中我们只需要知道一件事就行,那就是 AST 是一种树形结构,并且是某种代码的一种抽象表示。
在线可视化网站:https://astexplorer.net/ ,利用这个网站我们可以很清晰的看到各种语言的 AST 结构。
estree[1]
estree 就是 es 语法对应的标准 AST,作为一个前端也比较方便理解。我们以官方文档为例
https://github.com/estree/estree/blob/master/es5.md
下面看一个代码
console.log('1')
AST 为
{
"type": "Program",
"start": 0, // 起始位置
"end": 16, // 结束位置,字符长度
"body": [
{
"type": "ExpressionStatement", // 表达式语句
"start": 0,
"end": 16,
"expression": {
"type": "CallExpression", // 函数方法调用式
"start": 0,
"end": 16,
"callee": {
"type": "MemberExpression", // 成员表达式 console.log
"start": 0,
"end": 11,
"object": {
"type": "Identifier", // 标识符,可以是表达式或者结构模式
"start": 0,
"end": 7,
"name": "console"
},
"property": {
"type": "Identifier",
"start": 8,
"end": 11,
"name": "log"
},
"computed": false, // 成员表达式的计算结果,如果为 true 则是 console[log], false 则为 console.log
"optional": false
},
"arguments": [ // 参数
{
"type": "Literal", // 文字标记,可以是表达式
"start": 12,
"end": 15,
"value": "1",
"raw": "'1'"
}
],
"optional": false
}
}
],
"sourceType": "module"
}
看两个稍微复杂的代码
const b = { a: 1 };
const { a } = b;
function add(a, b) {
return a + b;
}
这里建议读者自己将上述代码复制进上面提到的网站中,自行理解 estree 的各种节点类型。当然了,我们也不可能看一篇文章就记住那么多类型,只要心里有个大致的概念即可。
认识 acorn[2]
由 JavaScript 编写的 JavaScript 解析器,类似的解析器还有很多,比如 Esprima[3] 和 Shift[4] ,关于他们的性能,Esprima 的官网给了个测试地址[5],但是由于 acron 代码比较精简,且 webpack 和 eslint 都依赖 acorn,因此我们这次从 acorn 下手,了解如何使用 AST。
![0c1421e89e5e662e9ebfd79690231c38.png](https://i-blog.csdnimg.cn/blog_migrate/2af9f1c1f9db1adbb016d05e1626462f.png)
基本操作
acorn 的操作很简单
import * as acorn from 'acorn';
const code = 'xxx';
const ast = acorn.parse(code, options)
这样我们就能拿到代码的 ast 了,options 的定义如下
interface Options {
ecmaVersion: 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 'latest'
sourceType?: 'script' | 'module'
onInsertedSemicolon?: (lastTokEnd: number, lastTokEndLoc?: Position) => void
onTrailingComma?: (lastTokEnd: number, lastTokEndLoc?: Position) => void
allowReserved?: boolean | 'never'
allowReturnOutsideFunction?: boolean
allowImportExportEverywhere?: boolean
allowAwaitOutsideFunction?: boolean
allowSuperOutsideMethod?: boolean
allowHashBang?: boolean
locations?: boolean
onToken?: ((token: Token) => any) | Token[]
onComment?: ((
isBlock: boolean, text: string, start: number, end: number, startLoc?: Position,
endLoc?: Position
) => void) | Comment[]
ranges?: boolean
program?: Node
sourceFile?: string
directSourceFile?: string
preserveParens?: boolean
}
ecmaVersion ECMA 版本,默认时 es7
locations 默认为 false,设置为 true 时节点会携带一个 loc 对象来表示当前开始与结束的行数。
onComment 回调函数,每当代码执行到注释的时候都会触发,可以获取当前的注释内容
获得 ast 之后我们想还原之前的函数怎么办,这里可以使用 astring[6]
import * as astring from 'astring';
const code = astring.generate(ast);
实现普通函数转换为箭头函数
接下来我们就可以利用 AST 来实现一些字符串匹配不太容易实现的操作,比如将普通函数转化为箭头函数。
我们先来看两个函数的AST有什么区别
function add(a, b) {
return a + b;
}
const add = (a, b) => {
return a + b;
}
{
"type": "Program",
"start": 0,
"end": 41,
"body": [
{
"type": "FunctionDeclaration",
"start": 0,
"end": 40,
"id": {
"type": "Identifier",
"start": 9,
"end": 12,
"name": "add"
},
"expression": false,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 13,
"end": 14,
"name": "a"
},
{
"type": "Identifier",
"start": 16,
"end": 17,
"name": "b"
}
],
"body": {
"type": "BlockStatement",
"start": 19,
"end": 40,
"body": [
{
"type": "ReturnStatement",
"start": 25,
"end": 38,
"argument": {
"type": "BinaryExpression",
"start": 32,
"end": 37,
"left": {
"type": "Identifier",
"start": 32,
"end": 33,
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 36,
"end": 37,
"name": "b"
}
}
}
]
}
}
],
"sourceType": "module"
}
{
"type": "Program",
"start": 0,
"end": 43,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 43,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 43,
"id": {
"type": "Identifier",
"start": 6,
"end": 9,
"name": "add"
},
"init": {
"type": "ArrowFunctionExpression",
"start": 12,
"end": 43,
"id": null,
"expression": false,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 13,
"end": 14,
"name": "a"
},
{
"type": "Identifier",
"start": 16,
"end": 17,
"name": "b"
}
],
"body": {
"type": "BlockStatement",
"start": 22,
"end": 43,
"body": [
{
"type": "ReturnStatement",
"start": 28,
"end": 41,
"argument": {
"type": "BinaryExpression",
"start": 35,
"end": 40,
"left": {
"type": "Identifier",
"start": 35,
"end": 36,
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 39,
"end": 40,
"name": "b"
}
}
}
]
}
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}
找到区别之后我们就可以有大致的思路
找到
FunctionDeclaration
将其替换为
VariableDeclaration
VariableDeclarator
节点
在
VariableDeclarator
节点的init
属性下新建ArrowFunctionExpression
节点
并将
FunctionDeclaration
节点的相关属性替换到ArrowFunctionExpression
上即可
但是由于 acorn 处理的 ast 只是单纯的对象,并不具备类似 dom 节点之类的对节点的操作能力,如果需要操作节点,需要写很多工具函数, 所以我这里就简单写一下。
import * as acorn from "acorn";
import * as astring from 'astring';
import { createNode, walkNode } from "./utils.js";
const code = 'function add(a, b) { return a+b; } function dd(a) { return a + 1 }';
console.log('in:', code);
const ast = acorn.parse(code);
walkNode(ast, (node) => {
if(node.type === 'FunctionDeclaration') {
node.type = 'VariableDeclaration';
const variableDeclaratorNode = createNode('VariableDeclarator');
variableDeclaratorNode.id = node.id;
delete node.id;
const arrowFunctionExpressionNode = createNode('ArrowFunctionExpression');
arrowFunctionExpressionNode.params = node.params;
delete node.params;
arrowFunctionExpressionNode.body = node.body;
delete node.body;
variableDeclaratorNode.init = arrowFunctionExpressionNode;
node.declarations = [variableDeclaratorNode];
node.kind = 'const';
}
})
console.log('out:', astring.generate(ast))
结果如下
![7aaade7c3207005ffce1df37901f63f7.png](https://i-blog.csdnimg.cn/blog_migrate/e0ca78efb1ba0fcbc946533e0bb4948e.png)
如果想要代码更加健壮,可以使用 recast[7],提供了对 ast 的各种操作
// 用螺丝刀解析机器
const ast = recast.parse(code);
// ast可以处理很巨大的代码文件
// 但我们现在只需要代码块的第一个body,即add函数
const add = ast.program.body[0]
console.log(add)
// 引入变量声明,变量符号,函数声明三种“模具”
const {variableDeclaration, variableDeclarator, functionExpression} = recast.types.builders
// 将准备好的组件置入模具,并组装回原来的ast对象。
ast.program.body[0] = variableDeclaration("const", [
variableDeclarator(add.id, functionExpression(
null, // Anonymize the function expression.
add.params,
add.body
))
]);
//将AST对象重新转回可以阅读的代码
const output = recast.print(ast).code;
console.log(output)
这里只是示例代码,展示 recast 的一些操作,最好的情况还是能遍历节点自动替换。
这样我们就完成了将普通函数转换成箭头函数的操作,但 ast 的作用不止于此,作为一个前端在工作中可能涉及 ast 的地方,就是自定义 eslint 、 stylelint 等插件,下面我们就趁热打铁,分别实现。
实现一个 ESlint 插件
介绍
ESlint 使用 Espree (基于 acron) 解析 js 代码,利用 AST 分析代码中的模式,且完全插件化。
ESlint 配置
工作中我们最常接触的就是 eslint 的配置,我们写的插件也需要从这里配置从而生效
// .eslintrc.js
moudule.export = {
extends: ['eslint:recommend'],
parser: '@typescript-eslint/parser', // 解析器,
plugins: ['plugin1'], // 插件
rules: {
semi: ['error', 'alwayls'],
quotes: ['error', 'double'],
'plugin1/rule1': 'error',
},
processor: '', // 特定文件中使用 eslint 检测
}
parser,默认使用 espree[8],对 acorn[9] 的一层封装,将 js 代码转化为抽象语法树 AST。
import * as espree from "espree";
const ast = espree.parse(code);
经常使用的还有 @typescript-eslint/parser[10] ,这里可以拓展 ts 的 lint;
开发一个 eslint 插件
准备
eslint 官方也有个介绍,如何给 eslint 做贡献 https://eslint.org/docs/developer-guide/contributing/
安装 yeoman 并初始化环境,yeoman 就是一个脚手架,方便创建 eslint 的插件和 rule
npm install -g yo generator-eslint
创建一个插件文件夹并进入
创建 plugin
yo eslint:plugin
![e777703625530a6afe43b99c9f41a26e.png](https://i-blog.csdnimg.cn/blog_migrate/07e3d2a25468b9de90ff4b7cfb77b159.png)
最重要的是 ID,这样插件发布之后,会以 eslint-plugin-[id] 的形式发布到 npm 上,不可以使用特殊字符。
创建 rule 规则
yo eslint:rule
![703c7d2ef84225b8115f8ba795e2128c.png](https://i-blog.csdnimg.cn/blog_migrate/dd3eaad74b0cf9fd8359b8c8fde54a8f.png)
这里的 id 会生成 eslint-plugin-[id]
插件唯一标识符
生成的文件列表为
![c8586968fdb827b931ec45a31094b98f.png](https://i-blog.csdnimg.cn/blog_migrate/87c8b9d7bf36489d51d7331bea22d0a5.png)
然后就可以实现插件了
这时候我们可以回头看一下刚刚生成的文件
rules/cpf-plugin.js
可以参考
https://cn.eslint.org/docs/developer-guide/working-with-rules
/**
* @fileoverview cpf better
* @author tsutomu
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/**
* @type {import('eslint').Rule.RuleModule}
*/
module.exports = {
meta: { // 这条规则的元数据,
type: null, // 类别 `problem`, `suggestion`, or `layout`
docs: { // 文档
description: "cpf better",
category: "Fill me in",
recommended: false,
url: null, // URL to the documentation page for this rule
},
fixable: null, // Or `code` or `whitespace`
schema: [], // 重点, eslint 可以通过识别参数从而避免无效的规则配置 Add a schema if the rule has options
},
create(context) {
// variables should be defined here
//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
// any helper functions should go here or else delete this section
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
return {
// visitor functions for different types of nodes
};
},
};
Eslint 的插件需要根据它规定的特定规则进行编写
Meta 中比较重要的是 schema,主要是设置入参,我们来看一下 shcema 的规则 https://eslint.org/docs/developer-guide/working-with-rules#options-schemas
JSONSchema 定义 https://json-schema.org/understanding-json-schema/
大致有两种形式,enum 和 object
schema: [
{
"enum": ["always", "never"]
},
{
"type": "object",
"properties": { // 这里的意思就是可以有个叫 exceptRange 的参数,值为布尔类型
"exceptRange": {
"type": "boolean"
}
},
"additionalProperties": false
}
]
下面看下 create,返回了一个对象,需要在其中编写遇到对应节点所需要执行的方法, context 则提供了一些方便的方法,包括
context.report
上报错误和context.getSourceCode
获取源代码。
create(context: RuleContext): RuleListener;
interface RuleContext {
id: string;
options: any[];
settings: { [name: string]: any };
parserPath: string;
parserOptions: Linter.ParserOptions;
parserServices: SourceCode.ParserServices;
getAncestors(): ESTree.Node[];
getDeclaredVariables(node: ESTree.Node): Scope.Variable[];
getFilename(): string;
getPhysicalFilename(): string;
getCwd(): string;
getScope(): Scope.Scope;
getSourceCode(): SourceCode;
markVariableAsUsed(name: string): boolean;
report(descriptor: ReportDescriptor): void;
}
no-console 插件源码解析
写自己的插件之前,不妨看下官方的插件源码,也更方便理解里面的各种概念。
/**
* @fileoverview Rule to flag use of console object
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "disallow the use of `console`",
recommended: false,
url: "https://eslint.org/docs/rules/no-console"
},
schema: [
{
type: "object",
properties: {
allow: {
type: "array",
items: {
type: "string"
},
minItems: 1,
uniqueItems: true
}
},
additionalProperties: false
}
],
messages: {
unexpected: "Unexpected console statement."
}
},
create(context) {
const options = context.options[0] || {};
const allowed = options.allow || [];
/**
* Checks whether the given reference is 'console' or not.
* @param {eslint-scope.Reference} reference The reference to check.
* @returns {boolean} `true` if the reference is 'console'.
*/
function isConsole(reference) {
const id = reference.identifier;
return id && id.name === "console";
}
/**
* Checks whether the property name of the given MemberExpression node
* is allowed by options or not.
* @param {ASTNode} node The MemberExpression node to check.
* @returns {boolean} `true` if the property name of the node is allowed.
*/
function isAllowed(node) {
const propertyName = astUtils.getStaticPropertyName(node);
return propertyName && allowed.indexOf(propertyName) !== -1;
}
/**
* Checks whether the given reference is a member access which is not
* allowed by options or not.
* @param {eslint-scope.Reference} reference The reference to check.
* @returns {boolean} `true` if the reference is a member access which
* is not allowed by options.
*/
function isMemberAccessExceptAllowed(reference) {
const node = reference.identifier;
const parent = node.parent;
return (
parent.type === "MemberExpression" &&
parent.object === node &&
!isAllowed(parent)
);
}
/**
* Reports the given reference as a violation.
* @param {eslint-scope.Reference} reference The reference to report.
* @returns {void}
*/
function report(reference) {
const node = reference.identifier.parent;
context.report({
node,
loc: node.loc,
messageId: "unexpected"
});
}
return {
"Program:exit"() {
const scope = context.getScope(); // 获取当前作用域,及全局作用域
const consoleVar = astUtils.getVariableByName(scope, "console"); // 向上遍历,查找
const shadowed = consoleVar && consoleVar.defs.length > 0; // 这里是判断别名
/*
* 'scope.through' includes all references to undefined
* variables. If the variable 'console' is not defined, it uses
* 'scope.through'.
*/
// 如果 console 是未定义的,那么他就在 scope.through 中
const references = consoleVar
? consoleVar.references
: scope.through.filter(isConsole);
if (!shadowed) {
references
.filter(isMemberAccessExceptAllowed)
.forEach(report);
}
}
};
}
};
对照看一下console.log 的ast ,在最上面
Scope 作用域定义
interface Scope {
type:
| "block"
| "catch"
| "class"
| "for"
| "function"
| "function-expression-name"
| "global" // 及 Program
| "module"
| "switch"
| "with"
| "TDZ";
isStrict: boolean;
upper: Scope | null; // 父级作用域
childScopes: Scope[]; // 子级作用域
variableScope: Scope;
block: ESTree.Node;
variables: Variable[]; // 变量
set: Map<string, Variable>; // 变量 set 便于快速查找
references: Reference[]; // 此范围所有引用的数组
through: Reference[]; // 由未定义的变量组成的数组
functionExpressionScope: boolean;
}
Scope 相关的源码可以参考 https://github.com/estools/escope,scope 可视化可以看这里 http://mazurov.github.io/escope-demo/
这里的 through 就是当前作用域无法解析的变量,比如
function a() {
function b() {
let c = d;
}
}
这里面明显是 d 无法解析,那么
![d6fea69587985175c25ac9c0cdd5b0b4.png](https://i-blog.csdnimg.cn/blog_migrate/08d159e77c5bab47c592c71b588812c9.png)
可以看到,在全局作用域的 through 中可以找到这个 d。
自动修复
可以再 report 中调用 fix 相关的函数来进行修复,下面是 fix 的
interface RuleFixer {
insertTextAfter(nodeOrToken: ESTree.Node | AST.Token, text: string): Fix;
insertTextAfterRange(range: AST.Range, text: string): Fix;
insertTextBefore(nodeOrToken: ESTree.Node | AST.Token, text: string): Fix;
insertTextBeforeRange(range: AST.Range, text: string): Fix;
remove(nodeOrToken: ESTree.Node | AST.Token): Fix;
removeRange(range: AST.Range): Fix;
replaceText(nodeOrToken: ESTree.Node | AST.Token, text: string): Fix;
replaceTextRange(range: AST.Range, text: string): Fix;
}
interface Fix {
range: AST.Range;
text: string;
}
用法为
report(context, message, type, {
node,
loc,
fix: (fixer) {
return fixer.inserTextAfter(token, string);
}
})
可以看下 eqeqeq
的写法,这是一个禁用 ==
!=
并且修复为=== !==
的规则
return {
BinaryExpression(node) {
const isNull = isNullCheck(node);
if (node.operator !== "==" && node.operator !== "!=") {
if (enforceInverseRuleForNull && isNull) {
report(node, node.operator.slice(0, -1));
}
return;
}
if (config === "smart" && (isTypeOfBinary(node) ||
areLiteralsAndSameType(node) || isNull)) {
return;
}
if (!enforceRuleForNull && isNull) {
return;
}
report(node, `${node.operator}=`);
}
};
修复的代码在 report 中
function report(node, expectedOperator) {
const operatorToken = sourceCode.getFirstTokenBetween(
node.left,
node.right,
token => token.value === node.operator
);
context.report({
node,
loc: operatorToken.loc,
messageId: "unexpected",
data: { expectedOperator, actualOperator: node.operator },
fix(fixer) {
// If the comparison is a `typeof` comparison or both sides are literals with the same type, then it's safe to fix.
if (isTypeOfBinary(node) || areLiteralsAndSameType(node)) {
return fixer.replaceText(operatorToken, expectedOperator);
}
return null;
}
});
}
实现 no-getNodeRef
实现一个禁用 getNodeRef 的插件
当我们在内部使用的跨端框架中使用下面的配置之后,将不再支持 getNodeRef
属性,取而代之的是使用 createSelectorQuery
compilerNGOptions: {
removeComponentElement: true,
},
首先我们先看一下 getNodeRef
的用法
class A extends C {
componmentDidMount() {
this.getNodeRef('');
}
}
在上面的可视化网站中可以看到下面的
![5b702fdb1e51b5877092d6a6bed4b54c.png](https://i-blog.csdnimg.cn/blog_migrate/914faf6646c841d7a46fde2e49dca7a3.png)
那么我们就可以很暴力的写出 rule ,如下
return {
// visitor functions for different types of nodes
CallExpression: (node) => {
if(node.callee.property && node.callee.property.name === 'getNodeRef' && node.callee.object.type === 'ThisExpression') {
context.report({
node,
message: '禁用 getNodeRef'
})
}
}
};
测试的代码
const ruleTester = new RuleTester();
ruleTester.run("no-getnoderef", rule, {
valid: [
// give me some code that won't trigger a warning
{
code: 'function getNodeRef() {}; getNodeRef();'
}
],
invalid: [{
code: " this.getNodeRef('');",
errors: [{
message: "禁用 getNodeRef",
type: "CallExpression"
}],
}, ],
});
测试的结果
![2499e0433ec1b014e696bf1a9c12db27.png](https://i-blog.csdnimg.cn/blog_migrate/683c602e38fab13962e60d84114dccdc.png)
实现 care-about-scroll
并没有什么实际的用处,仅仅是因为我们使用的框架中的 scroll event 有 bug,android 和 IOS 端参数有问题,安卓的 e.detail.scrollHeight 对应 ios 的 e.detail.scrollTop,再次说明,这是个框架的 bug,在这里使用仅仅为了演示 eslint 编写插件的一些能力。
我们的预期目标是在同一个函数中,如果使用了上述一个属性和没有使用另一个属性,则出现提示。
代码为
return {
// visitor functions for different types of nodes
"Identifier": (node) => {
if((node.name === 'scrollHeight' || node.name === 'scrollTop') && node.parent && node.parent.object.property.name === 'detail') {
const block = findUpperNode(node, 'BlockStatement');
if(block) {
let checked = false;
walkNode(block, (_node) => {
if(_node.type === 'Identifier' && _node.name === IDENTIFIERS[node.name]) {
checked = true;
return true;
}
return false;
});
if(!checked) {
context.report({node, message: `缺少 ${IDENTIFIERS[node.name]}`})
}
}
}
}
};
测试代码如下
ruleTester.run("care-about-scroll", rule, {
valid: [
// give me some code that won't trigger a warning
{
code: "function handleScroll(e) { var a = e.detail.scrollTop; var b = e.detail.scrollHeight; }"
}
],
invalid: [{
code: "function handleScroll(e) { var a = e.detail.scrollTop; }",
errors: [{
message: "缺少 scrollHeight",
type: "Identifier"
}],
}, ],
});
发布插件
登录之后直接发布即可
npm publish
![60be1924275a49f8d74e1b31fc9f6c5e.png](https://i-blog.csdnimg.cn/blog_migrate/5ad14f309330e20c7252d12fc4f779a2.png)
使用插件
首先按照刚刚发布的插件
npm i eslint-plugin-cpf -D
在 eslintrc.js 中新增配置
moudule.exports = {
plugins: ['cpfabc'],
rules: {
'cpfabc/no-getnoderef': 'error',
'cpfabc/care-about-scroll': 'error',
}
}
效果如下
![c19e5f525a5f13f25a0558cbc9bdbbee.png](https://i-blog.csdnimg.cn/blog_migrate/a40679e60fc455650422680f5d736fd5.png)
![3bdb2c03ac5317ce058c95d391651f70.png](https://i-blog.csdnimg.cn/blog_migrate/c1276e930d945400bf618bae47f6780a.png)
更改代码后正常
![1d8b071c1c8f2698964ca9495d2a96d3.png](https://i-blog.csdnimg.cn/blog_migrate/20487d01f20bb3fde24f245b4c639bc5.png)
实现一个 Stylelint 插件
介绍
Stylelint 插件和 eslint 插件的区别主要是
解释器,postcss
入口,这里可以使用本地文件开发
Ast,因为 css 本身就有结构,这里更像 dom 树,每个节点有 type 和 nodes(子节点),甚至并没有对 less 之类的代码进行转换。也因此 stylelint 的插件写起来更像直接对字符串进行处理,不会体现 ast 的作用。
但是整体的思想都是一样的,css 的节点类型也少很多,可以参考https://github.com/postcss/postcss/blob/main/docs/writing-a-plugin.md
Root
: 根节点,指代当前 css 文件AtRule
: @开头的一些属性,如 @mediaRule
: 常用的 css 选择器Declaration
: 键值对,如 color: redComment
: 注释
实现 cpf-style-plugin/max-depth-2
内部跨端框架 的 ttss 最多支持两层的 css 组合选择器,即下面是可行的
div div {}
而下面是不行的
div div div {}
而对于 less
.a {
&-b {
&-c {
}
}
}
/
.a-b-c {}
其实只有一层,所以我们的代码需要注意这点
首先建立一个文件叫 cpf-style-plugin.js
const stylelint = require('stylelint');
const { ruleMessages, report } = stylelint.utils;
const ruleName = 'cpf-style-plugin/max-depth-2';
const messages = ruleMessages(ruleName, {
//… linting messages can be specified here
expected: '不允许三层',
test: (...any) => `${JSON.stringify(any)}xxx`,
});
module.exports.ruleName = ruleName;
module.exports.messages = messages;
module.exports = stylelint.createPlugin(ruleName, function ruleFunction() {
return function lint(postcssRoot, postcssResult) {
function helperDep(n, dep) {
if (n.nodes) {
n.nodes.forEach((newNode) => {
if (newNode.type === 'rule') {
const selectorNum = newNode.selector
.split(' ')
.reduce((p, c) => p + (/^[a-zA-z.#].*/.test(c) ? 1 : 0), 0);
if (dep + selectorNum > 2) {
report({
message: messages.expected,
node: newNode,
result: postcssResult,
});
}
helperDep(newNode, dep + selectorNum);
}
});
}
}
helperDep(postcssRoot, 0);
};
});
这里有区别的是,eslint 都是对标准语法树进行操作,而这里的 css 树,准确来说应该是 less 的 ast 树,并不会先转成 css 再进行我们的 lint 操作,因此我们需要考虑 rule 节点可能以 &
开头,也导致写法上有一点别扭。
使用插件
使用的话只需要更改 stylelintrc.js 即可
module.exports = {
snippet: ['less'],
extends: "stylelint-config-standard",
plugins: ['./cpf-style-plugin.js'],
rules: {
'color-function-notation': 'legacy',
'cpf-style-plugin/max-depth-2': true,
}
}
看一下效果
![724b67e9cd385b3df8bf2b8ae3428b6e.png](https://i-blog.csdnimg.cn/blog_migrate/d611bb3707f9d4178b9d1932cd8e7245.png)
实现一个 React Live Code
你可能会觉得 live code 和 ast 有啥关系,只不过是放入 new Function
即可,但是形如 import export
等功能,利用字符串匹配实现是不太稳定的,我们可以利用 AST 来实现这些方法,这里为了简洁,最后一行表示 export default
,思想是一样的,利用 AST 查找到我们需要的参数即可。
https://codesandbox.io/s/react-live-editor-3j7t2?file=/src/index.js
![07bc6c73e61fdb39e7387e04ae0fc002.png](https://i-blog.csdnimg.cn/blog_migrate/bf586bb1f4282f61bbf14c7e4500b5b1.png)
其中上半部分为编辑器,下半部分为事实的效果,我们的工作是分析最后一行的组件并展示出来。
其中编辑器的部分负责代码的样式,使用的是 react-simple-code-editor[11],主要的用法如下
<Editor
value={code}
onValueChange={code => {xxx}}
/>
所以主要的工作在获取编辑器代码之后的工作
首先我们需要将 JSX 代码转换为 es5 代码,这里用到 @babel/standalone[12],这是一个环境使用的 babel 插件,可以这么使用
import { transform as babelTransform } from "@babel/standalone";
const tcode = babelTransform(code, { presets: ["es2015", "react"] }).code;
然后我们需要获取最后一行代码
<Greet />
并将其转化为,其实也就是找到React.createElement(Greet)
这个,这里就可以使用 ast 进行查找。过程略过,我们得到了这个节点rnode
,最后将这个rnode
转换为React.createElement
,我们最终得到了这样的代码
code = "'use strict';
var _x = _interopRequireDefault(require('x'));
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
function Greet() {
return React.createElement('span', null, 'Hello World!');
}
render(React.createElement(Greet, null));"
将上述的代码塞入 new Function 中执行。
const renderFunc = return new Function("React", "render", "require", code);
最后执行上述的代码
import React from "react";
function render(node) {
ReactDOM.render(node, domElement);
}
function require(moduleName) {
// 自定义
}
renderFunc(React, render, require)
参考资料
[1]
estree: https://github.com/estree/estree
[2]acorn: https://github.com/acornjs/acorn
[3]Esprima: https://github.com/jquery/esprima
[4]Shift: https://github.com/shapesecurity/shift-parser-js
[5]测试地址: https://esprima.org/test/compare.html
[6]astring: https://www.npmjs.com/package/astring
[7]recast: https://www.npmjs.com/package/recast
[8]espree: https://github.com/eslint/espree
[9]acorn: https://github.com/acornjs/acorn
[10]@typescript-eslint/parser: https://typescript-eslint.io/docs/linting/
[11]react-simple-code-editor: https://www.npmjs.com/package/react-simple-code-editor
[12]@babel/standalone: https://babeljs.io/docs/en/babel-standalone
- END -Node 社群
我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。
“分享、点赞、在看” 支持一波👍
![Logo](https://devpress.csdnimg.cn/79de2bf0b7994defa4242ef90d5513fa.jpg)
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)