前言
DSL, 领域特定语言,即一种为特定领域所设计的编程语言。今日前端早读课文章由 @cadeli 分享,公号:WeChatFE 授权。
正文从这开始~~
本文参考腾讯问卷实践 —— 背景知识与问题介绍
想必大家都有过创建一份调查问卷的经历,调查问卷通常可以为各个问题之间设计一定的逻辑,比如下图
如果用户第一题选择了第一个选项,那么会导致第二道题目的出现。
作为一个想要发布一份问卷的用户,也就是用研,他可能需要到问卷的发布页面去点点点进行页面逻辑的配置,这不仅费时费力,而且会有一个问题,如果遇到一些比较专业且复杂的逻辑难以在 UI 上进行简单的交互,这时候可能就需要找问卷的开发者单独定制。然后就是下面的一连串沟通成本!
用研:Hey,麻烦帮我开发一个问卷
开发:说一下这次的需求
用研:这次是一份调查 xxxx 的问卷,我先会在第一题咨询 xxx,如果用户 xxx,那就会 xxx。。。。
开发:这里没懂,这里没懂,这里没懂,这里没懂。。。。
用研:好的我解释一下,好的我解释一下,好的我解释一下,好的我解释一下
作为开发者,没有相应领域的知识,需要理解用研的需求会比较困难;作为用研,没有开发的能力导致需求修改需要频繁与开发对齐,花费大量时间。那么有没有一种方法,可以让用研不依赖开发,就可以自己定制问卷呢,答案就是 DSL。
什么是 DSL
DSL 全称为 Domain Specific Language,译为领域特定语言,即一种为特定领域所设计的编程语言。可以简单的理解为设计 DSL 就是设计一套语法来描述一系列的相关行为。抽象到比较低级的层面,C++、JavaScript 就是机器指令集的 DSL;在比较高级的层面举例的话,markdown 就是文档编写的 DSL,sql 语句就是数据库查询的 DSL。为什么不直接用机器指令编写代码,而要使用高级语言如 C++ 等,就是因为高级语言对于开发者的门槛更低,更容易上手,更有迹可循。究其原因,DSL 是为人类设计的,而指令是为机器设计的。
内部 DSL 和外部 DSL
下面通过一个简单的例子来说明内部 DSL 和外部 DSL 的区别。我们要怎么描述两个星期前这一个时间呢。很容易想到以下三种解法:
解法一:
new Date(Date.now() - 1000 * 60 * 60 * 24 * 7 * 2);
解法二:
2 weeks ago
解法三:
(2).weeks().ago();
作为一个前端程序员,敏锐的你肯定第一眼就看出来了第一种写法就是标准的 js 代码,可以直接在浏览器就能跑出结果。第二种像是一种自然语言,第三种类似一种伪代码。第二和第三种其实就是外部 DSL 和内部 DSL 了。很明显,第二种解法和第三种解法是不能直接运行的,因为 DSL 需要在特定的环境下运行。
很显然 DSL 比编程语言更加直观,而外部 DSL 又比内部 DSL 更加直观,不信你可以问问自己的男 / 女朋友,看他们更倾向使用哪种语法(没有男 / 女朋友的也可以去问问自己的产品)
为什么有外部 DSL 和内部 DSL 的区别呢?外部 DSL 可以理解为是一门独立的语言,比如解法二,你为了让它能够正常运行,就必须编写一套编译器来支持它运行。而对于内部 DSL(解法三),你可以利用拓展现有的语言环境来支持这种语法,比如说我只要在 js 注入这段逻辑
Number.prototype.weeks = function() {
return this * 1000 * 60 * 60 * 24 * 7
}
Number.prototype.ago = function() {
return new Date(Date.now() - this)
}
就可以让解法三运行出结果了。受限于语言环境,内部 DSL 的语法也看起来比较别扭,需要遵循宿主语言的语法规则。
什么场景用 DSL
DSL 的设计与开发也是有一定的成本的,那么什么时候需要用 DSL 呢?概括性的总结就是,
当用户不是系统的开发者,但又需要定制逻辑的时候,就需要一门 DSL。
比如问卷的定制,再比如数据库的自定义查询。展开来说,如果一件事情有大量的重复,需要频繁的进行多方的沟通,需要很多的 GUI 操作,那么你就有可能需要一门 DSL。
e.g. 一份问卷 DSL
这是一份用研提出的问卷需求
这份问卷是这样的,首先会在第一题询问有没有发烧,如果选择了没有发烧,就直接跳转到结束页;然后如果选择了有, 显示第二题。第二题询问体温多少度,如果体温低于 37 度也 跳转到结束页...
然后我们提取出逻辑相关的内容
第一题:选择了没有发烧,跳转到结束页
选择了有,显示第二题
如果体温低于 37 度,跳转到结束页
转成伪代码表述
if Q1A1 then branch to END
if Q1A2 then show Q2
if Q2A < 37 then branch to END
稍微整理一下,你就可以设计出一套问卷 DSL 语法了
if Q1A2 then show Q2
if Q2A gt 37 then show Q3
设计你的 DSL
上面讲了那么多,设计完语法之后就要动手来实现了。学过大学编译原理的我们都知道,高级语言转机器指令需要经过一系列过程:
词法分析 -> 语法分析 -> 语义分析 -> 生成抽象语法树 -> 中间代码 -> 优化 -> 目标代码
学过是学过,但要把书本上的理论知识,比如正则表达式、状态机等等再实现一次,门槛有点过于高了。
好在有一位伟人说过,不要重复造轮子。热衷于开源的大牛们开发出了一个专门用来生成 DSL 解析器的解析器生成器,利用它们,我们可以快速地设计实现自己的 DSL 而不需要熟读编译原理。比较流行的有 PEG.js 和 jison。本文使用 PEG.js 进行举例,试着实现了上文中设计的问卷 DSL 中的其中一句 if Q1A2 then show Q2 ,原理大同小异,这里实现的比较粗糙,仅做抛砖引玉。
1、安装引入 npm 包 npm i pegjs
2、遵循 pegjs 规则设计语法
const grammar = `
/* 定义输入的语法格式,这里为 if xxx then xxx */
Start
= 'if' _ exp:Expression _ 'then' _ op:Operation {
/* 返回一个 输入为所有题目,输出为过滤后的题目 的函数 */
return new Function('questions', 'const result = [questions[0]];' +
'if(' + exp + ')' + op + 'return result;');
}
/* 定义Expression类型如何翻译 */
Expression "expression"
= 'Q'q:[0-9]'A'a:[0-9] {
return 'questions[' + q + '-1].answer === ' + a
}
/* 定义Operation类型如何翻译 */
Operation "operation"
= 'show Q'q:[0-9] {
return '{result.push(questions['+ q + '-1]);}'
}
/* 定义 _ 如何翻译 */
_ "whitespace"
= ' '
`;
3、parser = pegjs.generate(grammar)
生成 DSL 编译器
4、然后就可以使用 parser.parse 来对输入 DSL 进行解析生成对应的逻辑处理函数啦
完整 demo 已放到 codepen,大家可以在阅读原文处进行在线编辑尝试实现更多的腾讯问卷的 DSL,或者创造属于自己的 DSL~
PEG.js 同时也支持在线生成解析器并下载下来,这样就不需要在项目中安装 PEG.js 库
不轻易用 DSL
DSL 看起来很牛逼很高大上,事实上使用起来需要很大的成本,如你所见,你需要设计一套自己的语法,并为之编写生成解析器提供给用户使用;用户在使用你的 DSL 的时候,你极大可能需要提供一个 DSL 编辑器使得用户的编写体验更流畅并提供语法检查,还需要编写文档供用户查阅;对于用户来说,虽然不需要直接学习编程语言,但是不论设计的多好的 DSL,依然存在学习成本,依然需要花费一定的时间才能上手你设计的 DSL。
在想要使用 DSL 的时候,先问一问自己,能不能不使用 DSL,有没有现成的标准 DSL,能不能使用内部 DSL。
当你确信没有更好的解法了,那就拥抱 DSL 吧。
关于本文
作者:@cadeli
原文:https://mp.weixin.qq.com/s/cP_jWDObNIuchmA1SOSwqg
这期前端早读课
对你有帮助,帮” 赞 “一下,
期待下一期,帮” 在看” 一下 。