Angular源码学习之 DSL
主要回答几下几个问题
- 什么是 DSL
- 为什么使用 DSL
- 何时使用 DSL
什么是 DSL
DSL(Domain Specified Language)领域专用语言。 要理解什么是领域专用语言,需要先了解其创建背景。即为什么会诞生这样一种事物,其发明的目的是为了解决什么问题。
总的来说 DSL 是为了解决系统(包括硬件系统和软件系统)构建初期,使用者和构建者的语言模型不一致导致需求收集的困难。 举一个具体的例子来说。在构建证券交易系统的过程中,在证券交易活动中存在许多专业的金融术语和过程。现在要为该交易过程创建一个软件解决方案,那么开发者/构建者就必须了解证券交易活动,其中涉及到哪些对象、它们之间的规则以及约束条件是怎么样的。那么就让领域专家(这里就是证券交易专家)来描述证券交易活动中涉及的活动。但是领域专家习惯使用他们熟练使用的行业术语来表达,解决方案的构建者无法理解。如果解决方案的模型构建者要理解交易活动,就必须让领域专家用双方都能理解的自然语言来解释。这种解释的过程中,解决方案的模型构建者就理解了领域知识。这个过程中双方使用的语言就被称为“共同语言”。
共同语言称为解决方案模型构建者用来表达解决方案中的词汇的基础。构建者将这些共同语言对应到模型中,在程序中就是模块名、在数据模型中就是实体名、在测试用例中就是对象。
在上面的描述,可以看到在需求收集的过程中,如果要成功构建模型,则需要一种领域专家和构建者(也就是通常的领域分析师/业务分析师)都能理解的“共同语言”。但是这种共同语言的创建过程没有保证,不能够保证在收集过程中得到的信息完整的描述了领域活动中所有的业务规则和活动。
如果能够让领域专家通过简单的编程方式描述领域中的所有活动和规则,那么就能在一定程度上保证描述的完整性。
DSL 就是为了解决这个问题而提出的。
常见的 DSL
- 软件构建领域 Ant
- UI 设计师 HTML
- 硬件设计师 VHDL
DSL 的特点
- 用于专门领域,不能用于其他领域
- 表现力有限
- 不描述解答域,仅描述问题域
DSL 与通用编程语言的区别
- DSL 有更高级的抽象,不涉及类似数据结构的细节
- DSL 表现力有限,其只能描述该领域的模型,而通用编程语言能够描述任意的模型
DSL 分类
要理解DSL 分类需要先理解一个概念。“元语言抽象”。 它是指通过一种语言来构建另一种语言。比如 Java 就是构建在 C 上的语言。 其中,从什么语言构建而来,这种构建来源的语言称为“宿主语言”。
根据是否从宿主语言构建而来,DSL 分为:
- 内部 DSL(从一种宿主语言构建而来)
- 外部 DSL(从零开始构建的语言,需要实现语法分析器等)
如何构建 DSL
构建DSL 要满足三个原则:
- 能够完整描述领域
- 简单易用
- 隐藏实现细节
实例代码
@Component({
selector: ‘my-test-component’,
templateUrl: './myTest.component.html',
styleUrls: ['./ myTest.component.less'],
providers: [myTestService],
animations: [
trigger('hideShow', [
state('hide', style({
transform: 'rotateY(90deg)',
display: 'none'
})),
state('show', style({
transform: 'rotateY(0deg)',
})),
transition('hide <=> show', [
animate('0.5s')
])
]),
]
})
Angular源码学习之 AST
AST (abstract syntax code) 详解与运用
了解AST之前,我们先来简单陈述一下JavaScript引擎的工作原理:
从上图中我们可以看到,JavaScript引擎做的第一件事情就是把JavaScript代码编译成抽象语法树。
1. 什么是AST抽象语法树
我们都知道,在传统的编译语言的流程中,程序的一段源代码在执行之前会经历三个步骤,统称为"编译":
分词/词法分析 这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码块统称为词法单元(token)。
举个例子: let a = 1,
这段程序通常会被分解成为下面这些词法单元:
let 、a、=、1、
空格是否被当成此法单元,取决于空格在这门语言中的意义。
解析/语法分析 这个过程是将词法单元流转换成一个由元素嵌套所组成的代表了程序语法结构的树,这个树被称为"抽象语法树"(abstract syntax code,AST) 代码生成 将AST转换成可执行代码的过程被称为代码生成. 抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,之所以说是抽象的,抽象表示把js代码进行了结构化的转化,转化为一种数据结构。这种数据结构其实就是一个大的json对象,json我们都熟悉,他就像一颗枝繁叶茂的树。有树根,有树干,有树枝,有树叶,无论多小多大,都是一棵完整的树。
简单理解,就是把我们写的代码按照一定的规则转换成一种树形结构。
2. AST的用途
AST的作用不仅仅是用来在JavaScript引擎的编译上,我们在实际的开发过程中也是经常使用的,比如我们常用的babel插件将 ES6转化成ES5、使用 UglifyJS来压缩代码 、css预处理器、开发WebPack插件、Vue-cli前端自动化工具等等,这些底层原理都是基于AST来实现的,AST能力十分强大, 能够帮助开发者理解JavaScript这门语言的精髓。
3. AST的结构
我们先来看一组简单的AST树状结构:
const team = 'kenko'
经过转化,输出如下AST树状结构:
{
"type": "Program",
"start": 0,
"end": 18,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 18,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 18,
"id": {
"type": "Identifier",
"start": 6,
"end": 8,
"name": "team"
},
"init": {
"type": "Literal",
"start": 11,
"end": 18,
"value": "kenko",
"raw": "'kenko'"
}
}
],
我们可以看到,一个标准的AST结构可以理解为一个json对象,那我们就可以通过一些方法去解析和操作它,这里我们先提供一个在线检测工具,大家可以自行去体验: https://esprima.org/demo/parse.html#
4. AST编译过程
AST编译流程图:
我们可以看到,AST工具会源代码经过四个阶段的转换:
1.词法分析scanner
var company = 'kenko'
假如有以上代码,在词法分析阶段,会先对整个代码进行扫描,生成tokens流,扫描过程如下:
我们会通过条件判断语句判断这个字符是 字母, "/" , "数字" , 空格 , "(" , ")" , ";" 等等。
如果是字母会继续往下看如果还是字母或者数字,会继续这一过程直到不是为止,这个时候发现找到的这个字符串是一个 "var", 是一个Keyword,并且下一个字符是一个 "空格", 就会生成
{
"type" : "Keyword" ,
"value" : "var"
}
放入数组中。
它继续向下找发现了一个字母 'company'(因为找到的上一个值是 "var" 这个时候如果它发现下一个字符不是字母可能直接就会报错返回)并且后面是空格,生成
{
"type" : "Identifier" ,
"value" : "company"
}
放到数组中。
发现了一个 "=", 生成了
{
"type" : "Punctuator" ,
"value" : "="
}
放到了数组中。
发现了'kenko',生成了
{
"type" : "String" ,
"value" : "kenko"
}
放到了数组中。
解析如下:
2. parser生成AST树
这里我们使用esprima去生成, 安装相关依赖
npm i esprima --save
以如下代码为例:
const company = 'kenko'
要得到其对应的AST,我们对其进行如下操作:
const esprima = require('esprima');
let code = 'const company = "kenko" ';
const ast = esprima.parseScript(code);
console.log(ast);
运行结果如下:
$ node test.js
Script {
type: 'Program',
body: [
VariableDeclaration {
type: 'VariableDeclaration',
declarations: [Array],
kind: 'const'
}
],
sourceType: 'script'
}
这样我们就得到了一棵AST树
3.traverse对AST树遍历,进行增删改查
这里我们使用estraverse去完成, 安装相关依赖
npm i estraverse --save
还是上面的代码, 我们更改为
const team = 'clarify'
const esprima = require('esprima');
const estraverse = require('estraverse');
let code = 'const company = "kenko" ';
const ast = esprima.parseScript(code);
estraverse.traverse(ast, {
enter: function (node) {
node.name = 'team';
node.value = "clarify";
}
});
console.log(ast);
运行结果如下:
$ node test.js
Script {
type: 'Program',
body: [
VariableDeclaration {
type: 'VariableDeclaration',
declarations: [Array],
kind: 'const',
name: 'team',
value: 'clarify'
}
],
sourceType: 'script',
name: 'team',
value: 'clarify'
}
这样一来,我们就完成了对AST的遍历更新。
4.generator将更新后的AST转化成代码 这里我们使用escodegen去生成, 安装相关依赖
npm i escodegen --save
整体代码结构如下:
const esprima = require('esprima');
const estraverse = require('estraverse');
const escodegen = require('escodegen');
let code = 'const company = "kenko" ';
const ast = esprima.parseScript(code);
estraverse.traverse(ast, {
enter: function (node) {
node.name = 'team';
node.value = "clarify";
}
});
const transformCode = escodegen.generate(ast);
console.log(transformCode);
会得到如下结果:
$ node test.js
const team = 'clarify';
这样一来,我们就完成了对一段简单代码的AST编译过程。
5. babel原理浅析
Babel插件就是作用于抽象语法树。
Babel 的三个主要处理步骤分别是: 解析(parse),转换(transform),生成(generate)。
解析
将代码解析成抽象语法树(AST),每个js引擎(比如Chrome浏览器中的V8引擎)都有自己的AST解析器,而Babel是通过Babylon(GitHub - babel/babylon: PSA: moved into babel/babel as @babel/parser -->)实现的。解析过程有两个阶段:词法分析和语法分析,词法分析阶段把字符串形式的代码转换为令牌(tokens)流,令牌类似于AST中节点;而语法分析阶段则会把一个令牌流转换成 AST的形式,同时这个阶段会把令牌中的信息转换成AST的表述结构。
转换
转换步骤接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 Babel通过babel-traverse对其进行深度优先遍历,维护AST树的整体状态,并且可完成对其的替换,删除或者增加节点,这个方法的参数为原始AST和自定义的转换规则,返回结果为转换后的AST。
生成
代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)(http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/)。. 代码生成其实很简单:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。 Babel通过babel-generator再转换成js代码,过程就是深度优先遍历整个AST,然后构建可以表示转换后代码的字符串。
结语:
AST抽象语法树的知识点作为JavaScript中(任何编程语言中都有ast这个概念)相对基础的,也是最不可忽略的知识,带给我们的启发是无限可能的,它就像一把螺丝刀,能够拆解javascript这台庞大的机器,让我们能够看到一些本质的东西,同时也能通过它批量构建任何javascript代码。