在写hst-virtual-dom的时候,由于没有能力自己写一个语法解析器,仍然使用了内部是eval的Function来解析插值。在查看了angular的$parse源码之后,发现angular自己实现了一套ast来解析js字符串脚本,于是觉得这可能是我一直在寻找的东西。经过Google之后,发现有一个叫angular-expressions的库将这个模块从angular中剥离出来,成为一个独立的模块,可以在node环境中运行,于是决定研究一下这个库。
打开parse.js这个文件,我天,竟然有4000多行代码,肯定是不可能看了。最后决定,只研究它外层的封装,而不深入它的内层去研究,只要用它就可以了。
angular解析执行器用法
先来看下angular里面怎么使用$parse:
let parser = $parse('project.name') let value = parser($scope)
上面实际上得到的结果是$scope.project.name
,但是为什么要用$parse去执行,而不直接调用呢?这是因为这个project.name可能是一个directive的attr传进来的。举个例子:
<div my-directive="members[2].name"></div>
然后你要在my-directive的代码里面去使用字符串的形式获取。你得到一个字符串members[2].name
,怎么通过这个字符串,从$scope上获取你想要的东西呢?这个时候就得靠$parse。
安全的eval
JavaScript里面虽然有一个eval可以用来把字符串作为脚本进行运行,但是它被认为是不安全的。为了能够解析并运行字符串,不同的框架都做了尝试,而无疑,angular是里面都佼佼者。基于angular的$parse而生的angular-expressions把这个功能从angular里面提取出来,你可以这样去使用它:
var expressions = require("angular-expressions"); var parse = function(str){ return expressions.compile(str)(); } console.log(parse('1 + 1')); // 2 console.log(parse('1 ? 1 : 0'))
这是一个简单的执行器,可以用来执行简单的js表达式并得到结果。
NOT eval
但它和eval的表现形式完全不同,eval里面是一个完整的js上下文环境,最直接的体现就是this。如果在执行eval之前this就是有所指向的,那么eval里面的this和前面这个this是一样的指向。但是我们这里的parse完全不同,它要求有一个作用域,在未传作用域的情况下,里面的任何变量都是undefined,而在传了作用域之后,里面的变量将作为作用域对象的属性进行查询,这和angular的模板里面的变量名与$scope之间的关系是一样的。因此,我们要在parse里面使用变量,就必须传一个scope,我们将代码改造为:
var expressions = require("angular-expressions"); var parse = function(str, scope){ return expressions.compile(str)(scope); }
于是,我们就可以在传了scope的情况下,在str里面写一些变量:
var value = parse('age + 12', { age: 3 }) // 15
甚至,我们还可以在str里面对scope不存在的属性赋值:
var obj = {} parse('sub.name = "mabo"', obj) console.log(obj.sub.name) // mabo
这可真是神奇的操作。
插值的好帮手
有了这样的操作,我们就很容易将它运用到模板引擎的插值运算中了。例如我们可以仿造vuejs的插值方式进行逻辑处理:
<button @click="onClick">xx</button>
{ methods: { onClick() { alert('ok') } } }
我们怎样才能将模版中的onClick正确解析为methods.onClick呢?因为vue已经做了一道工序,也就是this.onClick = methods.onClick,而且onClick里面的this还指向了vue实例。所以在模板解析完成之后,我们得到一个virtual dom并且规矩规则对这个button的click事件进行绑定:
buttonElement.addEventListener('click', (e) => { let scope = { $this: this, $event: e, } let onClick = parse('$this.' + attrs.bind.click, scope) // attrs.bind.click就是指@click的值 if (typeof onClick === 'function') { onClick.call(this, e) } }, false)
这段代码一上,你甚至可以在模板中写@click="onClick($event)"。至于类似于{{text}}
这样的插值,完全不在话下。
ToeX
基于上面的思想,我自己写了一个package,取名ToeX,实现了对angular-expressions的封装,使得api更加方便实用。我已经发布到github,你可以在这里看到源码。看下它的简单实用:
var ToeX = require('toex') var toex = new ToeX($scope) // 过滤器 toex.filter('double', value => value * 2) // 求值 var normalExpressionResult = toex.parse('1 + 1 | double') // 这里演示了使用过滤器 var scopeExpressionResult = toex.parse('key + 1') // 赋值 toex.assign('name', 'tomy')
它一共3个api,使用超级简单,可以考虑用到任何需要的地方。
小结
好了,本文主要介绍了一个基于angular.$parse服务的一个库,并且基于它,做了一些扩展的想法。当我们在有需求的时候,可能会用上这个库。