用JSON描述一段简单逻辑

我想用JSON来描述一段简单的逻辑代码,生为一段配置化的文件。怎么描述成本最低?逻辑代码类似于这样

if( body.type === 1 ){
    body.renderTime = body.time
}else if(body.type === 0){
    body.renderTime = body.content
}

补一个配置代码:
在exec位置放逻辑配置代码

 {
        name: '供应材料价格信息',
        component: 'dyDragTable',
        dataOrigin: 'supplyContractPriceList',
        hookName: 'commonProcessNumber|changeMaterialPrice',
        style: {
          width: [150, 100, 80, 150, 150, 100, 150, 150],
        },
        exec:{
            // 这里放简单处理逻辑配置
        },
        children: {
          materialName: '材料名称',
          bindStation: '站点',
          materialPrice: '原材料价格',
          beginTime: '原起始时间',
          endTime: '原结束时间',
          changeMaterialPrice: '变更后材料价格',
          changeBeginTime: '变更后起始时间',
          changeEndTime: '变更后结束时间',
        },
        func: {
          isShow(busData: any, opt: any) {
            if (busData.contractType === '0') {
              opt.isComponentShow = '1'
            }
          },
        },
      },
阅读 3.6k
3 个回答

如果按你提供的样本,最简单的是配置系列条件,这里的exec应该是一个数组:

[
  {
    condition: "type",
    value: 1,
    targetProperty: "renderTime",
    targetValue: "time",
  },
  {
    condition: "type",
    value: 0,
    targetProperty: "renderTime",
    targetValue: "content",
  },
]

然后写一个方法来应用配置:

function exec(body, config) {
  config.forEach((item) => {
    if (body[item.condition] === item.value) {
      body[item.targetProperty] = body[item.targetValue];
    }
  });
}

或者把事情搞复杂,写一个DSL解决,exec里面存的就是自定义的语言了,看起来很高大上不是吗(警告:除了增加复杂性,没有卵用,纯属我今天摸鱼时间多 😄)代码太长直接戳这里

咱的语言长的像这样:

body.type:1 => body.renderTime = body.time
body.type:0 => body.renderTime = body.content
body.type:3 => {
    body.someProperty = body.someField
    body.foo = body.bar
}

这是语法:

ifStmt     :=  expression "=>" statement (ifStmt ) ?
statement  :=  assignment | block
block      :=  "{" ( statement )* "}"
assignment := ( access "." )? IDENTIFIER "=" assignment 
              | access
equality   := access ( ":" access )* ;
access       :=  primary ( "." IDENTIFIER) * ;
primary    :=  NUMBER | IDENTIFIER

我们的“语言”很简单,只有几个有限的符号.

export enum TokenType {
  IDENTIFIER = 1,
  NUMBER,
  COLON,
  DOT,
  LEFT_BRACE,
  RIGHT_BRACE,
  EQUAL_ARROW,
  EQUAL,
  EOF,
}

词法解析很容易:

export class Token {
  constructor(public type: TokenType, public lexeme: string) {}
  toString() {
    return `${TokenType[this.type].padEnd(12, " ")} ${this.lexeme}`;
  }
}

function tokenize(input: string) {
  let start = 0;
  let current = 0;
  const tokens: Token[] = [];
  const isAtEnd = () => current >= input.length;
  const isAlpha = (c: string) => /[a-zA-Z]/.test(c);
  const isDigit = (c: string) => /[0-9]/.test(c);
  const isAlphaNumeric = (c: string) => /[a-zA-Z0-9]/.test(c);
  const advance = () => input[current++];
  const peek = () => input[current];
  const match = (expected: string) => {
    if (isAtEnd()) return false;
    if (input.charAt(current) !== expected) return false;
    current++;
    return true;
  };
  const addToken = (type: TokenType, literal: unknown = null) => {
    const text = input.substring(start, current);
    tokens.push(new Token(type, text));
  };
  const identifier = () => {
    while (!isAtEnd() && isAlphaNumeric(peek())) advance();
    addToken(TokenType.IDENTIFIER);
  };
  const number = () => {
    while (!isAtEnd() && isDigit(peek())) advance();
    addToken(TokenType.NUMBER, Number(input.substring(start, current)));
  };
  const scanToken = () => {
    const char = advance();
    switch (char) {
      case "=":
        addToken(match(">") ? TokenType.EQUAL_ARROW : TokenType.EQUAL);
        break;
      case ".":
        addToken(TokenType.DOT);
        break;
      case ":":
        addToken(TokenType.COLON);
        break;
      case "{":
        addToken(TokenType.LEFT_BRACE);
        break;
      case "}":
        addToken(TokenType.RIGHT_BRACE);
        break;
      default:
        if (isDigit(char)) {
          number();
        } else if (isAlpha(char)) {
          identifier();
        }
    }
  };
  while (!isAtEnd()) {
    start = current;
    scanToken();
  }
  tokens.push(new Token(TokenType.EOF, ""));
  return tokens;
}

使用最简单的递归下降生成语法树,基本上就把上面bnf描述变成一个个js方法就可以了,清晰起见我用class实现

class Parser {
  private current = 0;
  constructor(private tokens: Token[]) {}
  parse() {
    try {
      return this.ifStatment();
    } catch (e: any) {
      console.log(e.message);
    }
  }

  ifStatment() {
    const condition = this.expression();
    this.consume(TokenType.EQUAL_ARROW);
    const thenBranch = this.statement();
    let elseBranch = null as any;
    while (!this.isAtEnd()) {
      elseBranch = this.ifStatment();
    }
    return new If(condition, thenBranch, elseBranch);
  }

  statement() {
    if (this.match(TokenType.LEFT_BRACE)) {
      return this.blockStatement();
    }
    return this.expressionStatement();
  }

  blockStatement() {
    let statements = [] as Expression[];
    while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
      statements.push(this.expressionStatement()!);
    }
    this.consume(TokenType.RIGHT_BRACE, "expect '}' after block.");

    return new Block(statements);
  }

  expressionStatement() {
    let expr = this.expression();
    return new Expression(expr);
  }

  expression() {
    return this.assignment();
  }

  assignment(): Expr {
    const expr = this.equality();
    if (this.match(TokenType.EQUAL)) {
      let equals = this.previous();
      let value = this.assignment();
      if (expr instanceof Get) {
        return new Set(expr.obj, expr.name, value);
      }
      this.error(equals, "Invalid assignment target");
    }
    return expr;
  }

  equality(): Expr {
    let left = this.access();
    while (this.match(TokenType.COLON)) {
      const operator = this.previous();
      const right = this.access();
      left = new Binary(left, operator, right);
    }
    return left;
  }

  access() {
    let expr = this.primary();
    while (1) {
      if (this.match(TokenType.DOT)) {
        const name = this.consume(TokenType.IDENTIFIER);
        expr = new Get(expr, name);
      } else break;
    }
    return expr;
  }

  primary(): Expr {
    if (this.match(TokenType.NUMBER)) {
      return new Literal(this.previous().lexeme);
    }
    if (this.match(TokenType.IDENTIFIER)) {
      return new Variable(this.previous());
    }
    throw this.error(this.peek(), "Expect Expression.");
  }

  error(token: Token, message: string) {
    return new Error(`[${token.type}] ${token.lexeme} ${message}`);
  }

  consume(type: TokenType, message?: string) {
    if (this.check(type)) return this.advance();
    if (!message) message = `Unexpected token '${this.peek().lexeme}'`;
    throw this.error(this.peek(), message || `Expect ${TokenType[type]}`);
  }

  match(...types: TokenType[]) {
    for (let type of types) {
      if (this.check(type)) {
        this.advance();
        return true;
      }
    }
    return false;
  }

  advance() {
    if (!this.isAtEnd()) this.current++;
    return this.previous();
  }

  previous() {
    return this.tokens[this.current - 1];
  }

  check(type: TokenType) {
    if (this.isAtEnd()) return false;
    return this.peek().type === type;
  }

  peek() {
    return this.tokens[this.current];
  }

  isAtEnd() {
    return this.peek().type === TokenType.EOF;
  }
}

最后访问者模式遍历ast,生成js code,要把事情变得更复杂也可以写一个interpreter

import { Binary, ExprVisitor, Get, Literal, Set, Variable } from "./expr";
import { TokenType } from "./jlang";
import { If, Stmt, StmtVisitor, Expression, Block } from "./stmt";

export class Emitter implements StmtVisitor<string>, ExprVisitor<string> {
  constructor(private stmt: Stmt) {}
  indent = 0;
  visitBlock(stmt: Block): string {
    return stmt.statements.map((s) => s.accept(this)).join("\n");
  }
  visitSet(expr: Set): string {
    return (
      expr.obj.accept(this) +
      "." +
      expr.name.lexeme +
      " = " +
      expr.value.accept(this)
    );
  }
  visitExpressionStmt(stmt: Expression): string {
    return "".padStart(this.indent, "\t") + stmt.expr.accept(this) + ";";
  }
  visitLiteral(expr: Literal): string {
    return expr.value.toString();
  }
  visitVariable(expr: Variable): string {
    return expr.name.lexeme;
  }
  visitGet(expr: Get): string {
    return expr.obj.accept(this) + "." + expr.name.lexeme;
  }
  visitBinary(expr: Binary): string {
    const op =
      expr.operator.type === TokenType.COLON ? "==" : expr.operator.lexeme;
    return expr.left.accept(this) + " " + op + " " + expr.right.accept(this);
  }

  visitIfStmt(stmt: If): string {
    let str = `if (${stmt.condition.accept(this)})`;
    str += " {\n";
    str += stmt.thenBranch.accept(this);
    str += "\n}";
    if (stmt.elseBranch) {
      str += " else {\n";
      str += stmt.elseBranch.accept(this);
      str += "\n" + "}";
    }
    return str;
  }

  emit(): string {
    return this.stmt.accept(this);
  }
}

表达式和语句的数据结构

import { Token } from "./jlang";

export interface ExprVisitor<T> {
  visitBinary(expr: Binary): T;
  visitGet(expr: Get): T;
  visitSet(expr: Set): T;
  visitVariable(expr: Variable): T;
  visitLiteral(expr: Literal): T;
}
export abstract class Expr {
  abstract accept<T>(visitor: ExprVisitor<T>): T;
}

export class Variable extends Expr {
  accept<Variable>(visitor: ExprVisitor<Variable>) {
    return visitor.visitVariable(this);
  }
  constructor(public name: Token) {
    super();
  }
}

export class Get extends Expr {
  accept<Get>(visitor: ExprVisitor<Get>) {
    return visitor.visitGet(this);
  }
  constructor(public obj: Expr, public name: Token) {
    super();
  }
}

export class Set extends Expr {
  accept<Set>(visitor: ExprVisitor<Set>) {
    return visitor.visitSet(this);
  }
  constructor(public obj: Expr, public name: Token, public value: Expr) {
    super();
  }
}

export class Binary extends Expr {
  accept<Binary>(visitor: ExprVisitor<Binary>) {
    return visitor.visitBinary(this);
  }
  constructor(public left: Expr, public operator: Token, public right: Expr) {
    super();
  }
}

export class Literal extends Expr {
  accept<Literal>(visitor: ExprVisitor<Literal>) {
    return visitor.visitLiteral(this);
  }
  constructor(public value: any) {
    super();
  }
}
import { Expr } from "./expr";

export interface StmtVisitor<T> {
  visitIfStmt(stmt: If): T;
  visitExpressionStmt(stmt: Expression): T;
  visitBlock(stmt: Block): T;
}

export abstract class Stmt {
  abstract accept<T>(visitor: StmtVisitor<T>): T;
}
export class Expression extends Stmt {
  accept<Expression>(visitor: StmtVisitor<Expression>) {
    return visitor.visitExpressionStmt(this);
  }
  constructor(public expr: Expr) {
    super();
  }
}

export class Block extends Stmt {
  accept<Block>(visitor: StmtVisitor<Block>) {
    return visitor.visitBlock(this);
  }
  constructor(public statements: Stmt[]) {
    super();
  }
}

export class If extends Stmt {
  accept<If>(visitor: StmtVisitor<If>) {
    return visitor.visitIfStmt(this);
  }
  constructor(
    public condition: Expr,
    public thenBranch: Stmt,
    public elseBranch?: Stmt
  ) {
    super();
  }
}

使用的时候利用new Function生成方法执行就可以了:

function run() {
  let input = `
    body.type:1 => body.renderTime = body.time
    body.type:0 => body.renderTime = body.content
    body.type:3 => {
        body.someProperty = body.someField
        body.foo = body.bar
    }
    `;
  const tokens = tokenize(input);
  const stmt = new Parser(tokens).parse();
  if (!stmt) return;
  const emitter = new Emitter(stmt);
  const fn = new Function("body", emitter.emit());
  var body = {
    type: 1,
    renderTime: "renderTime",
    time: "time",
    someProperty: "someProperty",
    foo: "foo",
    bar: "bar",
  };
  fn(body);
  console.log(body);
}

那就 ast ?反正也没参照物。

image.png

JSON 本身只描述数据,不描述逻辑。不过看你这个逻辑,是可以翻译成数据的,就是“映射表”,用一个对象来描述:

{
    "1": "time",
    "0": "content"
}

使用的时候就是一堆查表操作

body.renderTime = body[dict[body.type]];

如果映射表的 Key 都是数字,而且有序,那直接用数组就可以描述

["content", "time"]

使用代码不变,算是特殊的查表操作。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题