前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文打透前端研发需要了解的DSL

一文打透前端研发需要了解的DSL

原创
作者头像
brzhang
发布2024-04-08 16:13:52
3690
发布2024-04-08 16:13:52
举报
文章被收录于专栏:玩转全栈玩转全栈

注意,本文有些难度,不太适合新手阅读,时候有一定编码经验的人阅读。废话应有点多了,ok,我们开始吧。

无论是前端研发还是后端研发,都会遇到DSL,DSL是Domain Specific Language的缩写,即领域特定语言。DSL是一种专门用于解决某一领域问题的语言,它的语法和语义都是针对这个领域的,而不是通用的。DSL可以分为内部DSL和外部DSL,内部DSL是在一种通用编程语言上构建的DSL,而外部DSL是一种独立的DSL。整理这篇文章也是因为在一次腾讯在深圳举办的前端技术分享会上,有人提到了DSL,然后看到他们用DSL解决的问题,觉得很有意思,所以后来就研究了一下DSL。

什么是DSL?

那么,什么是 DSL 呢?举一个例子,我们知道 SQL 是一种 DSL,它是用来操作数据库的语言,它的语法和语义都是针对数据库操作的。又比如,正则表达式也是一种 DSL,它是用来匹配字符串的语言,它的语法和语义都是针对字符串匹配的。DSL 可以帮助我们解决一些特定领域的问题,提高我们的开发效率。事实上,我们前端使用的 HTML、CSS、JavaScript 也可以看作是一种 DSL,它们都是用来构建 Web 应用的语言,它们的语法和语义都是针对 Web 应用的,而不是通用的。甚至流行的前端框架,如 React、Vue、Angular 等,也可以看作是一种 DSL,它们都是用来构建 Web 应用的框架,它们的语法和语义都是针对 Web 应用的。

内部 DSL 和外部 DSL

DSL 可以分为内部 DSL 和外部 DSL,内部 DSL 是在一种通用编程语言上构建的 DSL,而外部 DSL 是一种独立的 DSL。内部 DSL 的优点是可以利用通用编程语言的优势,例如类型检查、代码补全、调试等,但是它的语法和语义受到通用编程语言的限制。这里我举一个例子你就可以轻松理解,比如我们通过 JavaScript 来构建一个 DSL,这个 DSL 可以用来描述一个简单的计算器,例如这个是我们的 DSL:

代码语言:javascript
复制
const calculator = {
    add: (a, b) => a + b,
    sub: (a, b) => a - b,
    mul: (a, b) => a * b,
    div: (a, b) => a / b
};

console.log(calculator.add(1, 2)); // 3
console.log(calculator.sub(3, 2)); // 1
console.log(calculator.mul(2, 3)); // 6
console.log(calculator.div(6, 2)); // 3

这个 DSL 是在 JavaScript 上构建的,它的语法和语义都是针对计算器的,但是它受到 JavaScript 的限制,例如不能定义新的语法规则、不能定义新的语义规则等。而外部 DSL 则没有这些限制。

外部 DSL 的优点是可以根据领域的需求自定义语法和语义,但是它的开发和维护成本较高。在实际开发中,我们可以根据需求选择合适的 DSL,以提高我们的开发效率。缺点是,DSL 的开发和维护成本较高,需要有一定的技术水平。但是不要慌,希望这篇文章可以帮助你更好地理解并且应用外部 DSL 到你的项目中。

外部DSL 的应用场景

DSL 可以应用在很多领域,比如配置文件、模板引擎、规则引擎、领域建模等。在实际开发中,我们可以根据需求选择合适的 DSL,以提高我们的开发效率。下面我将使用一个实际研发中遇到的例子来说明外部 DSL的应用。再次之前,我们也许要先了解一两个工具,一个是 js 写的 DSL 解析器,叫做 nearley。另外一个也是 js 写的 ,叫做 jison 。这两个工具都是用来解析 DSL 的,你可以根据自己的需求选择合适的工具。

好的,下面我就来一个实际的案例了。

实际案例

在实际的研发中,我们会在特定的领域遇到一些特定的问题,如果使用通用编程语言来解决这些问题,可能会比较繁琐。这时,我们可以使用 DSL 来解决这些问题,提高我们的开发效率。下面我将使用一个实际的案例来说明外部 DSL 的应用。

假设我们是一个大型电信公司,我们的客户主要分为两类:标准客户(standard)和高级客户(premium)。公司提供两种主要的产品:"product1" 和 "product2"。每当客户购买了这两种产品中的任何一种,我们需要发送一份合同给他们。

合同的内容由一个标准模板("contract_template")生成,签名方式为自动签名("auto")或者手动签署 ("manuel") ,并使用公司的印章("company_seal")。合同的有效期为一年("one_year"),付款条款为每月付款("monthly")。

然而,我们不会向所有客户发送合同。只有当以下条件都满足时,我们才会发送合同,假设一个场景:

客户是高级客户; 客户的信用等级大于或等于 700; 客户购买了 "product2"。 在这个场景中,我们可以使用上述的 DSL 来描述这个场景,例如:

代码语言:javascript
复制
send_contract {
  recipient: {
    name: "customer.name"
    email: "customer.email"
    id_card: "customer.id_card"
    phone: "customer.phone"
  }
  template: "contract_template"
  signature_method: "auto"
  seal_setting: "company_seal"
  valid_period: "one_year"
  payment_terms: "monthly"
  send if customer.type in ["premium"] and customer.credit_rating >= 700 and customer.product in ["product2"]
}

ok,上述的 DSL 是没有办法直接运行的,要运行还是得程序来,我们可以使用 nearley 或者 jison 来编写 DSL 的语法规则。这里就以 jison 为例,来编写 DSL 的语法规则。

jison 是一个 JavaScript 的解析器生成器,它可以从类似 BNF 的语法描述中生成一个解析器。以下是一个基于你提供的 DSL 示例的简化的 jison 语法规则:

代码语言:javascript
复制
/* Lexical rules */
%lex
%%
\s+                   /* skip whitespace */
"send_contract"       { return 'SEND_CONTRACT'; }
"\{"                  { return '{'; }
"\}"                  { return '}'; }
"\:"                  { return ':'; }
"\""                  { return '"'; }
[a-zA-Z_][a-zA-Z0-9_]*  { return 'IDENTIFIER'; }
"and"                 { return 'AND'; }
"or"                  { return 'OR'; }
"in"                  { return 'IN'; }
">="                  { return 'GE'; } // 这里匹配采取的是贪婪匹配,可以参考 jison 的文档
"<="                  { return 'LE'; }
"<"                   { return 'LT'; }
">"                   { return 'GT'; }
"="                   { return 'EQ'; }
[0-9]+                { return 'NUMBER'; }
.                     { return 'INVALID'; }
/lex

/* Operator precedence */
%start expressions
%left OR
%left AND
%left GE LE GT LT EQ
%left IN

%% /* language grammar */

expressions
    : contract
    ;

contract
    : SEND_CONTRACT '{' contract_body '}' { $$ = { type: 'contract', body: $3 }; }
    ;

contract_body
    : recipient template signature_method seal_setting valid_period payment_terms send_condition
    ;

recipient
    : "recipient" ':' '{' recipient_body '}' { $$ = { type: 'recipient', body: $4 }; }
    ;

recipient_body
    : IDENTIFIER ':' '"' IDENTIFIER '"' { $$ = { [$1]: $4 }; }
    ;

template
    : "template" ':' '"' IDENTIFIER '"' { $$ = { type: 'template', name: $4 }; }
    ;

signature_method
    : "signature_method" ':' '"' IDENTIFIER '"' { $$ = { type: 'signature_method', method: $4 }; }
    ;

seal_setting
    : "seal_setting" ':' '"' IDENTIFIER '"' { $$ = { type: 'seal_setting', setting: $4 }; }
    ;

valid_period
    : "valid_period" ':' '"' IDENTIFIER '"' { $$ = { type: 'valid_period', period: $4 }; }
    ;

payment_terms
    : "payment_terms" ':' '"' IDENTIFIER '"' { $$ = { type: 'payment_terms', terms: $4 }; }
    ;

send_condition
    : "send" "if" condition { $$ = { type: 'send_condition', condition: $3 }; }
    ;

condition
    : IDENTIFIER IN '[' IDENTIFIER_list ']' { $$ = { type: 'in_condition', variable: $1, values: $4 }; }
    | IDENTIFIER GE NUMBER { $$ = { type: 'ge_condition', variable: $1, value: $3 }; }
    | IDENTIFIER AND IDENTIFIER { $$ = { type: 'and_condition', left: $1, right: $3 }; }
    ;

IDENTIFIER_list
    : IDENTIFIER ',' IDENTIFIER_list { $$ = [$1].concat($3); }
    | IDENTIFIER { $$ = [$1]; }
    ;

如果你想了解更多的语法规则和词法规则,推荐你查看编译原理的相关书籍,这里就不细讲了。

这个文件定义了词法规则(在 %lex/lex 之间)和语法规则(在 %% 和文件的末尾之间)。词法规则定义了你的 DSL 中的各种符号(例如关键字、标识符和操作符),而语法规则定义了这些符号如何组合成有效的表达式。

这个文件可以通过 jison 的命令行工具来编译成一个 JavaScript 文件,然后你可以在你的代码中使用这个文件来解析你的 DSL。那么解析后的结果是什么呢?解析后的结果是一个抽象语法树(AST),它是一个树状结构,用来表示你的 DSL 的语法结构。

代码语言:javascript
复制
{
  type: 'contract',
  body: {
    recipient: {
      name: 'customer.name',
      email: 'customer.email',
      id_card: 'customer.id_card',
      phone: 'customer.phone'
    },
    template: { type: 'template', name: 'contract_template' },
    signature_method: { type: 'signature_method', method: 'auto' },
    seal_setting: { type: 'seal_setting', setting: 'company_seal' },
    valid_period: { type: 'valid_period', period: 'one_year' },
    payment_terms: { type: 'payment_terms', terms: 'monthly' },
    send_condition: {
      type: 'send_condition',
      condition: {
        type: 'and_condition',
        left: {
          type: 'and_condition',
          left: { type: 'in_condition', variable: 'customer.type', values: ['premium'] },
          right: { type: 'ge_condition', variable: 'customer.credit_rating', value: 700 }
        },
        right: { type: 'in_condition', variable: 'customer.product', values: ['product2'] }
      }
    }
  }
}

我们可以有一个函数来执行这个 AST,例如:

代码语言:javascript
复制
class ContractSender {
  constructor() {
    this.recipient = {};
    this.template = '';
    this.signatureMethod = '';
    this.sealSetting = '';
    this.validPeriod = '';
    this.paymentTerms = '';
    this.sendCondition = null;
  }

  setRecipient(info) {
    this.recipient = info;
  }

  setTemplate(template) {
    this.template = template;
  }

  setSignatureMethod(method) {
    this.signatureMethod = method;
  }

  setSealSetting(setting) {
    this.sealSetting = setting;
  }

  setValidPeriod(period) {
    this.validPeriod = period;
  }

  setPaymentTerms(terms) {
    this.paymentTerms = terms;
  }

  setSendCondition(condition) {
    this.sendCondition = condition;
  }

  send() {
    if (this.sendCondition()) {
      console.log(`Sending contract to ${this.recipient.name}...`);
      // Here you would actually send the contract.
    } else {
      console.log(`Not sending contract to ${this.recipient.name}.`);
    }
  }
}

function execute(ast) {
  const sender = new ContractSender();

  sender.setRecipient(ast.body.recipient);
  sender.setTemplate(ast.body.template.name);
  sender.setSignatureMethod(ast.body.signature_method.method);
  sender.setSealSetting(ast.body.seal_setting.setting);
  sender.setValidPeriod(ast.body.valid_period.period);
  sender.setPaymentTerms(ast.body.payment_terms.terms);

  // Here we assume that the send condition is a simple function that returns a boolean.
  // In a real application, you would probably want to convert the send condition from the AST into a function.
  sender.setSendCondition(() => true);

  sender.send();
}

// Parse your DSL into an AST.
const ast = parse(myDSL);

// Execute the AST.
execute(ast);

这样,咱们的合同管理员就可以根据 DSL 来发送合同了。当然你可能说这个 DSL 貌似比较简单呀,确实,在实际的场景中远远比这个复杂,但是为了简化而好理解,我这里省略了太多太多,目的是希望你能够理解 DSL 的基本原理,然后根据自己的需求来编写你自己的 DSL。

这就完了吗?

并没有,我们在写代码的时候,如果没有语法着色,没有代码补全,没有代码提示,那么我们的开发效率会大大降低。实现语法着色,代码补全,我们可能需要做一个让编辑器识别我们的自定义语言,这个过程叫做语言支持。Monaco Editor 和 Ace Editor 都支持自定义语言支持,你可以根据自己的需求来实现自定义语言支持。这里我以 Monaco Editor 为例,来说明如何实现自定义语言支持。

Monaco Editor 是一个由微软开发的基于浏览器的代码编辑器,它提供了很多强大的特性,包括语法高亮、代码自动补全、代码提示等。你可以通过定义自己的语言支持来让 Monaco Editor 支持你的 DSL。

以下是一个如何使用 Monaco Editor 来实现自定义语言支持的基本步骤:

  1. 1. 定义语言规则

首先,你需要定义你的 DSL 的语法规则。这可以通过创建一个包含 tokenizer 属性的对象来实现。tokenizer 属性是一个数组,每个元素都是一个包含两个元素的数组:一个正则表达式和一个标记类型。例如:

代码语言:javascript
复制
const myDSL = {
  tokenizer: {
    root: [
      [/send_contract/, "keyword"],
      [/"/, "string"],
      [/:/, "delimiter"],
      [/[{}]/, "delimiter.bracket"],
      [/[a-zA-Z_][a-zA-Z0-9_]*/, "identifier"],
      [/[0-9]+/, "number"],
      [/\s+/, "white"]
    ],
  },
};
  1. 1. 注册语言和主题

然后,你需要调用 monaco.languages.registermonaco.languages.setMonarchTokensProvider 来注册你的 DSL。你还可以调用 monaco.editor.defineTheme 来定义你的 DSL 的主题。例如:

代码语言:javascript
复制
monaco.languages.register({ id: "ct" });

monaco.languages.setMonarchTokensProvider("ct", myDSL);

monaco.editor.defineTheme("ctTheme", {
  base: "vs",
  inherit: true,
  rules: [
    { token: "keyword", foreground: "880000", fontStyle: "bold" },
    { token: "string", foreground: "008800", fontStyle: "italic" },
    { token: "number", foreground: "000088" },
  ],
});
  1. 1. 创建编辑器

最后,你可以调用 monaco.editor.create 来创建一个编辑器实例,并设置它的语言和主题。例如:

代码语言:javascript
复制
const editor = monaco.editor.create(document.getElementById("container"), {
  value: "",
  language: "ct",
  theme: "ctTheme",
});

以上只是实现语法高亮的基本步骤,如果你还想实现代码自动补全和代码提示,你可能需要使用 monaco.languages.registerCompletionItemProvidermonaco.languages.registerHoverProvider 等 API。具体的实现取决于你的 DSL 的复杂性和你的具体需求。这里就不细讲下去了,我实际上已经跑题了,如果你感兴趣,完全,可以查看 Monaco Editor 的官方文档来了解更多信息。

总结

让我们来整体回顾一下 DSL 解决需求的过程:

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是DSL?
    • 内部 DSL 和外部 DSL
    • 外部DSL 的应用场景
      • 实际案例
        • 这就完了吗?
        • 总结
        相关产品与服务
        命令行工具
        腾讯云命令行工具 TCCLI 是管理腾讯云资源的统一工具。使用腾讯云命令行工具,您可以快速调用腾讯云 API 来管理您的腾讯云资源。此外,您还可以基于腾讯云的命令行工具来做自动化和脚本处理,以更多样的方式进行组合和重用。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档