vue-devtools

  1. 从chrome应用市场安装或者通过github clone 导入插件
  2. 界面功能介绍

给大客培训准备的PPT转成了md

从左向右,依次简单介绍一下

2.1 components 显示当前点击组件的可用数据,并可以修改和添加,修改可实时反应在界面中,非常方便调试一些业务流程比较繁琐,可以任意展示其中的一步操作的界面

2.2 vuex 使用vuex时,这里可以方便查看state,mutaitons, action等信息

2.3 events 记录事件,比如点击事件,当你点击之后,这里会显示出来点击事件来源于哪一个组件,以及事件名等信息

2.4 Routing 记录路由的变化,以及路由相关信息。下拉routes 可以列出当前应用所有的路由

2.5 performance 性能监控

2.6 setting 皮肤设置等

2.7 Refresh 刷新

AST 抽象语法树无处不在-第二回合

一,Export Default 到 Component 构造器的转换

接着上回(AST 抽象语法树无处不在-第一回合)我们继续 VUE 组件转换为小程序组件中的 JavaScript 部分的转换。
首先我们看一下要转换前后的语法树与代码如下(明确转换目标):

转换之前的 AST 树与代码

1
2
3
4
5
6
7
8
export default {// VUE 组件的惯用写法用于导出对象模块
data(){
},
methods:{
},
props:{
}
}
转换之后的 AST 树与代码

1
2
3
4
5
6
7
8
components({//小程序组件的构造器
data(){
},
methods:{
},
props:{
}
})

通过以上转换之前和转换之后代码和 AST 的对比我们明确了转换目标就是 ExportDefault 到 Component构造器的转换,下面看一下我们是如何处理的:

我们做了什么(在转换中进入到 ExportDefault 中做对应的处理):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//ExportDefault 到 Component构造器的转换
ExportDefaultDeclaration(path) {
//创建 CallExpression Component({})
function insertBeforeFn(path) {
const objectExpression = t.objectExpression(propertiesAST);
test = t.expressionStatement(
t.callExpression(//创建名为 Compontents 的调用表达式,参数为 objectExpression
t.identifier("Compontents"),[
objectExpression
]
)
);
//最终得到的语法树
console.log("test",test)
}
if (path.node.type === "ExportDefaultDeclaration") {
if (path.node.declaration.properties) {
//提取属性并存储
propertiesAST = path.node.declaration.properties;
//创建 AST 包裹对象
insertBeforeFn(path);
}
//得到我们最终的转换结果
console.log(generate(test, {}, code).code);

对于 ExportDefault => Component 构造器转换还有一种转换思路 下面我们看一下:

[1] 第一种思路是先提取 ExportDefault 内部所有节点的 AST ,并做处理,然后创建Component构造器,插入提取处理后的 AST,得到最终的 AST

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//propertiesAST 这个就是我们拿到的 AST,然后在对应的分支内做对应的处理 以下分别为 data,methods,props,其他的钩子同样处理即可
propertiesAST.map((item, index) => {
if (item.type === "ObjectProperty") {
//props 替换为 properties
if (item.key.name === "props") {
item.key.name = "properties";
}
} else if (item.type === "ObjectMethod") {
//...
} else {
//...
console.log("other node type", item.type);
}
});

[2] 第二种思路呢,就是我们上面展示的这种,不过有一个关键的地方要注意一下:

1
2
3
4
5
6
7
8
9
10
//我把 ExportDefaultDeclaration 的处理放到最后来执行,拿到 AST 首先进行转换。然后在创建得到新的小程序组件JS部分的 AST 即可
traverse(ast, {
enter(path) {},
ObjectProperty(path) {},
ObjectMethod(path) {},
//......其他
ExportDefaultDeclaration(path) {
//参见上方
}
})

如果你想在 AST 开始处与结尾处插入,请参考:

1
2
3
4
5
6
path.insertBefore(
t.expressionStatement(t.stringLiteral("start.."))
);
path.insertAfter(
t.expressionStatement(t.stringLiteral("end.."))
);

二,VUE 组件转换为小程序组件中的 Data 部分的处理:

关于 Data 部分的处理实际上就是:函数表达式转换为对象表达式 (FunctionExpression 转换为 ObjectExpression)

转换之前的 JavaScript 代码
1
2
3
4
5
6
7
8
9
10
11
12
export default {
data(){//函数表达式
return {
num: 10000,
arr: [1, 2, 3],
obj: {
d1: "val1",
d2: "val2"
}
}
}
}
处理后我们得到的
1
2
3
4
5
6
7
8
9
10
export default {
data: {//对象表达式
num: 10000,
arr: [1, 2, 3],
obj: {
d1: "val1",
d2: "val2"
}
}
};

通过如上的代码对比,我们看到了我们的转换前后代码的变化:

转换前后 AST 树对比图明确转换目标:




我们对 JavaScript 动了什么手脚(亦可封装成babel插件):
1
2
3
4
const ast = babylon.parse(code, {
sourceType: "module",
plugins: ["flow"]
});
1
2
3
4
5
6
7
//AST 转换node、nodetype很关键
traverse(ast, {
enter(path) {
//打印出node.type
console.log("enter: " + path.node.type);
}
})
我们的转换部分都尽量在一个 Traverse 中处理,减少 AST 树遍历的性能消耗
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//Data 函数表达式 转换为 Object
ObjectMethod(path) {
// console.log("path.node ",path.node )// data, add, textFn
if (path.node.key.name === "data") {
// 获取第一级的 BlockStatement,也就是 Data 函数体
path.traverse({
BlockStatement(p) {
//从 Data 中提取数据属性
datas = p.node.body[0].argument.properties;
}
});
//创建对象表达式
const objectExpression = t.objectExpression(datas);
//创建 Data 对象并赋值
const dataProperty = t.objectProperty(
t.identifier("data"),
objectExpression
);
//插入到原 Data 函数下方
path.insertAfter(dataProperty);
//删除原 Data 函数节点
path.remove();
}
}

三,CSS 部分的处理:

那 CSS 我们也是必须要处理的一部分,let try

以下是我们要处理的css样本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const code = `
.text-ok{
position: absolute;
right: 150px;
color: #e4393c;
}
.nut-popup-close{
position: absolute;
top: 50px;
right: 120px;
width: 50%;
height: 200px;
display: inline-block;
font-size: 26px;
}`;
处理后我们得到的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text-ok {
position: absolute;
right: 351rpx;
color: #e4393c;
}

.nut-popup-close {
position: absolute;
top: 117rpx;
right: 280.79rpx;
width: 50%;
height: 468rpx;
display: inline-block;
font-size: 60.84rpx;
}

通过前后代码的对比,我们看到了单位尺寸的转换(比如:top: 50px; 转换为 top: 117rpx;)。
单位的转换( px 转为了 rpx )

CSS 又做了哪些处理呢?

同样也有不少的 CSS Code Parsers 供我们选择 Cssom ,CssTree等等,
我们拿 Cssom 来实现上方css代码的一个简单的转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
var ast = csstree.parse(code);
csstree.walk(ast, function(node) {
if(typeof node.value == "string" && isNaN(node.value) != true){
let newVal = Math.floor((node.value*2.34) * 100) / 100;//转换比例这个根据情况设置即可
if(node.type === "Dimension"){//得到要转换的数字尺寸
node.value = newVal;
}
}
if(node.unit === "px"){//单位的处理
node.unit = "rpx"
}
});
console.log(csstree.generate(ast));

当然这只是一个 demo,实际项目中使用还的根据项目的实际情况出发,SCSS,LESS等等的转换与考虑不同的处理场景哦!

总结

希望,本文对大家有所帮助,在技术探索的路上,我们一往无前, Paladin 精神永存!
感谢大家的耐心阅读,也欢迎大家关注 全栈探索公众号,每周都会有技术好文推出!
发现个好东东,如下:

将 JavaScript 代码转化生成 SVG 流程图 js2flowchart( 4.5 k stars 在 GitHub )

当你拥有 AST 时,可以做任何你想要做的事。把AST转回成字符串代码并不是必要的,你可以通过它画一个流程图,或者其它你想要的东西。
js2flowchart使用场景是什么呢?通过流程图,你可以解释你的代码,或者给你代码写文档;通过可视化的解释学习其他人的代码;通过简单的js语法,为每个处理过程简单的描述创建流程图。
马上用最简单的方式尝试一下吧,去线上编辑看看 js-code-to-svg-flowchart [9]。
此处有必要附上截图一张。

扩展阅读

[1] https://astexplorer.net/

[2] https://babeljs.io/docs/en/next/babel-types.html#objectexpression

[3] https://github.com/babel/babel/tree/6.x/packages/babel-types

[4] http://esprima.org/demo/parse.html#

[5] https://segmentfault.com/a/1190000014178462?utm_source=tag-newest

[6] https://zh.wikipedia.org/wiki/%E6%8A%BD%E8%B1%A1%E8%AA%9E%E6%B3%95%E6%A8%B9

[7] https://itnext.io/ast-for-javascript-developers-3e79aeb08343

[8] https://babeljs.io/docs/en/next/babel-types.html#callexpression

[9] https://github.com/Bogdan-Lyashenko/js-code-to-svg-flowchart

通过操作抽象语法树将 VUE 组件转换为微信小程序组件


简介:
首先我们介绍一下本文的关键点:抽象语法树,它是以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
通过操作这棵树,可以精确的定位到声明、赋值、运算语句,从而实现对代码的优化、变更等操作。
本文通过对 VUE 组件的 JavaScript 、CSS模块进行转换,比如 JavaScript模块,包括外层对象,生命周期钩子函数,赋值语句,组件的内部数据,组件的对外属性等等来实现把一个 VUE 组件转换为 一个小程序组件。

AST 抽象语法树,似乎我们平时并不会接触到。实际上在我们的项目当中,CSS 预处理,JSX 亦或是 TypeScript 的处理,代码格式化美化工具,Eslint, Javascript 转译,代码压缩,Webpack, Vue-Cli,ES6 转 ES5,当中都离不开 AST 抽象语法树这个绿巨人。先卖个关子,让我们看一下 AST 到的官方解释:

It is a hierarchical program representation that presents source code structure according to the grammar of a programming language, each AST node corresponds to an item of a source code.

中文的解释有:
抽象语法树(abstract syntax tree或者缩写为 AST ),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。和抽象语法树相对的是具体语法树(concrete syntaxtree),通常称作分析树(parse tree)。一般的,在源代码的翻译和编译过程中,语法分析器创建出分析树。一旦 AST 被创建出来,在后续的处理过程中,比如语义分析阶段,会添加一些信息。
抽象语法树,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
通过操作这棵树,可以精确的定位到声明、赋值、运算语句,从而实现对代码的优化、变更等操作。这些并不是我们想要看到的。

对于 AST 面纱的神秘感,似乎已经将要揭开。不错,在我刚接触到他的时候,同样感觉确实是难。但是当你开始了解了它以后,你就会越来越喜欢它。
因为他实在太强大了。AST 本身并不难,难点在于转换的目标对象与源对象的语法差异,当中水深毋庸置疑。但是,这才能更加激起我们探索他的欲望。在开始之前,我们先看一下 抽象语法树到底长什么样子。

一、 一探究竟 AST

通过 astexplorer [1] (AST树查看网站),通过他你可以方便的查看代码的语法树,挑你喜欢的库。你可以在线把玩 AST,而且除了 JavaScript,HTML, CSS 还有很多其它语言的 AST 库,让我们对他有一个感性而直观的认识。
请看下图,看看AST语法树长什么样子:


此图看到的是一个 ExportDefaultDeclaration 也就是export default {},还有他的位置信息,注释失信,tokens等等。

国际惯例,先来一个小demo
输入数据
1
2
3
function square(n) {
return n * n;
}
处理数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
astFn() {
const code = `function square(n) {
return n * n;
}`;
//得到 AST 语法树
const ast = babylon.parse(code);

traverse(ast, {
enter(path) {
console.log("enter: " + path.node.type);
//如下的语句匹配到 return 中的 n 与参数 n,并且将它替换为 x。
if (path.node.type === "Identifier" && path.node.name === "n") {
path.node.name = "x";
}
}
});
generate(ast, {}, code);//将 AST 转换为代码
console.log(generate(ast, {}, code).code );//打印出转换后的 JavaScript 代码
}
输出数据
1
2
3
function square(x) {
return x * x;
}

我们看一下我们得到的 AST 树



接下来我们插入一段 把 VUE 组件转换为微信小程序组件正则版本的处理

二、 简单粗暴的版本(VUE 组件转换为微信小程序组件)

没有使用 AST 将 VUE 组件转换成小程序组件的简易版本介绍
下方是两段代码,简单的逻辑,实现思路,匹配目标字符串,替换字符,然后生成文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
regs:{//通过标签匹配来替换对应的小程序支持的标签
toViewStartTags: /(<h1)|(<s)|(<em)|(<ul)|(<li)|(<dl)|(<i)|(<span)/g,
toViewEndTags: /(<\/h1>)|(<\/s>)|(<\/em>)|(<\/ul>)|(<\/li>)|(<\/dl>)|(<\/i>)|(<\/span>)/g,
toBlockStartTags: /(<div)|(<p)/g,
toBlockEndTags: /(<\/div>)|(<\/p>)/g,
},
signObj: {//通过标签查找来分离脚本,模板和CSS
tempStart: '<template>',
tempEnd: '</template>',
styleStart: '<style scoped>',
styleEnd: '</style>',
scriptStart: '<script>',
scriptEnd: '</script>'
}

上方是正则版本的一些模板匹配规则,经过后续的一系列处理把一个 VUE组件处理得到对应的小程序的 WXML ,WXSS,JSON,JS,4个文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//文件
const wxssFilePath = path.join(dirPath, `${mpFile}.wxss`);
const jsFilePath = path.join(dirPath, `${mpFile}.js`);
const wxmlFilePath = path.join(dirPath, `${mpFile}.wxml`);
const jsonFilePath = path.join(dirPath, `${mpFile}.json`);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath);
}
fs.writeFile(wxssFilePath, wxssContent, err => {
if (err) throw err;
//console.log(`dist目录下生成${mpFile}.wxss文件成功`);
fs.writeFile(jsFilePath, jsContent, err => {
if (err) throw err;
// console.log(`dist目录下生成${mpFile}.js文件成功`);
fs.writeFile(wxmlFilePath, wxmlContent, err => {
if (err) throw err;
//console.log(`dist目录下生成${mpFile}.wxml文件成功`);
fs.writeFile(jsonFilePath, jsonContent, err => {
if (err) throw err;
console.log(`dist目录下生成${mpFile}.json文件成功`)
resolve(`生成${mpFile}.json文件成功`);
})
})
});
});

上方是处理得到的 WXML ,WXSS,JSON,JS,4个文件,并且生成到对应的目录下。代码实现用时较短,后续更改方案,并没有做优化,这里就不做详细展开讨论这个实现方案了。

回到正题 介绍一下,AST 抽象语法树的核心部分:

三、 抽象语法树三大法宝

Babel 是 JavaScript 编译器,更确切地说是源码到源码的编译器,通常也叫做“ 转换编译器(transpiler)”。 意思是说你为 Babel 提供一些 JavaScript 代码,Babel 更改这些代码,然后返回给你新生成的代码。

babel-core:Babel 的编译器;它暴露了 babel.transform 方法。

[1] babylon:Babylon 是 Babel 的解析器。用于生成 AST 语法树。

[2] babel-traverse:Babel 的遍历器,所有的transformers都使用该工具遍历所有的 AST (抽象语法树),维护了整棵树的状态,并且负责替换、移除和添加节点。我们可以和 Babylon 一起使用来遍历和更新节点。

[3] babel-generator:Babel 的代码生成器。它读取AST并将其转换为代码。

整个编译器就被分成了三部分:分析器、转换器、生成器,大致的流程是:

输入字符串 -> babylon分析器 parse -> 得到 AST -> 转换器 -> 得到 AST -> babel-generator -> 输出

总结核心三步:

AST 三大法宝
babylon.parse => traverse 转换 AST => generate(ast, {}, code).code) 生成

感兴趣的童鞋,可以在网上或者看参考资料都有介绍。该铺垫的都铺垫的差不多了,进入正题。

我们到底是如何通过 AST 将 VUE 组件转换为微信小程序组件的呢?

四、 VUE 组件转换为微信小程序组件中 组件的对外属性、赋值语句的转换处理

转换之前的 VUE 组件代码 Demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default {
//组件的对外属性
props: {
max: {
type: Number,
value: 99
}
},
//组件的内部数据
data(){
return {
num:10000
}
},
//组件的方法
methods: {
textFn() {
this.num = 2
},
onMyButtonTap: function(){
this.num = 666
},
}
}
处理后我们得到的微信小程序组件 JavaScript 部分代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
export default {
properties: {
//组件的对外属性
max: {
type: Number,
value: 99
}
},
//组件的内部数据
data(){
return {
num: 10000
}
},
//组件的方法
methods: {
textFn() {
this.setData({
num: 2
});
},
onMyButtonTap: function () {
this.setData({
num: 666
});
}
}
};
我们对js动了什么手脚(亦可封装成babel插件):
1
2
3
4
5
//to AST
const ast = babylon.parse(code, {
sourceType: "module",
plugins: ["flow"]
});
1
2
3
4
5
6
7
//AST 转换 node,nodetype很关键
traverse(ast, {
enter(path) {
//打印出node.type
console.log("enter: " + path.node.type);
}
})
1
2
3
4
5
6
ObjectProperty(path) {
//props 替换为 properties
if (path.node.key.name === "props") {
path.node.key.name = "properties";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//修改methods中使用到数据属性的方法中。this.prop至this.data.prop等 与 this.setData的处理。
MemberExpression(path) {
let datasVals = datas.map((item,index) =>{
return item.key.name //拿到data属性中的第一级
})
if (//含有this的表达式 并且包含data属性
path.node.object.type === "ThisExpression" &&
datasVals.includes(path.node.property.name)
) {
path.get("object").replaceWithSourceString("this.data");
//判断是不是赋值操作
if (
(t.isAssignmentExpression(path.parentPath) &&
path.parentPath.get("left") === path) ||
t.isUpdateExpression(path.parentPath)
) {
const expressionStatement = path.findParent(parent =>
parent.isExpressionStatement()
);
//......
}
}
}
转换之前的js代码的部分 AST 树:


具体的 API 使用,童鞋们看一下 babel 相关的文档了解一下。

五, VUE 组件转换为微信小程序组件中 Export Default 到 Component 构造器的转换 与 生命周期钩子函数,事件函数的处理

首先我们看一下要转换前后的语法树与代码如下(明确转换目标):

转换之前的 AST 树与代码

1
2
3
4
5
6
7
8
export default {// VUE 组件的惯用写法用于导出对象模块
data(){
},
methods:{
},
props:{
}
}
转换之后的 AST 树与代码

1
2
3
4
5
6
7
8
components({//小程序组件的构造器
data(){
},
methods:{
},
props:{
}
})

通过以上转换之前和转换之后代码和 AST 的对比我们明确了转换目标就是 ExportDefault 到 Component构造器的转换,下面看一下我们是如何处理的:

我们做了什么(在转换中进入到 ExportDefault 中做对应的处理):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//ExportDefault 到 Component构造器的转换
ExportDefaultDeclaration(path) {
//创建 CallExpression Component({})
function insertBeforeFn(path) {
const objectExpression = t.objectExpression(propertiesAST);
test = t.expressionStatement(
t.callExpression(//创建名为 Compontents 的调用表达式,参数为 objectExpression
t.identifier("Compontents"),[
objectExpression
]
)
);
//最终得到的语法树
console.log("test",test)
}
if (path.node.type === "ExportDefaultDeclaration") {
if (path.node.declaration.properties) {
//提取属性并存储
propertiesAST = path.node.declaration.properties;
//创建 AST 包裹对象
insertBeforeFn(path);
}
//得到我们最终的转换结果
console.log(generate(test, {}, code).code);

对于 ExportDefault => Component 构造器转换还有一种转换思路 下面我们看一下:

[1] 第一种思路是先提取 ExportDefault 内部所有节点的 AST ,并做处理,然后创建Component构造器,插入提取处理后的 AST,得到最终的 AST

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//propertiesAST 这个就是我们拿到的 AST,然后在对应的分支内做对应的处理 以下分别为 data,methods,props,其他的钩子同样处理即可
propertiesAST.map((item, index) => {
if (item.type === "ObjectProperty") {
//props 替换为 properties
if (item.key.name === "props") {
item.key.name = "properties";
}
} else if (item.type === "ObjectMethod") {
if (path.node.key.name === "mounted") {
path.node.key.name = "ready";
} else if (path.node.key.name === "created") {
path.node.key.name = "attached";
} else if (path.node.key.name === "destroyed") {
path.node.key.name = "detached";
} else if (path.node.type === "ThisExpression") {
if (path.parent.property.name === "$emit") {
path.parent.property.name = "triggerEvent";
}
} else {
void null;
}
}
} else if (path.node.key.name === "methods") {
path.traverse({
enter(path) {
if (path.node.type === "ThisExpression") {
if (path.parent.property.name === "$emit") {
path.parent.property.name = "triggerEvent";
}
}
}
})
}
else {
//...
console.log("node type", item.type);
}
});

[2] 第二种思路呢,就是我们上面展示的这种,不过有一个关键的地方要注意一下:

1
2
3
4
5
6
7
8
9
10
//我把 ExportDefaultDeclaration 的处理放到最后来执行,拿到 AST 首先进行转换。然后在创建得到新的小程序组件JS部分的 AST 即可
traverse(ast, {
enter(path) {},
ObjectProperty(path) {},
ObjectMethod(path) {},
//......
ExportDefaultDeclaration(path) {
//...
}
})

如果你想在 AST 开始处与结尾处插入,可使用 path 操作:

1
2
3
4
5
6
path.insertBefore(
t.expressionStatement(t.stringLiteral("start.."))
);
path.insertAfter(
t.expressionStatement(t.stringLiteral("end.."))
);

注:关于微信小程序不支持 computed , 与 watch,我们具体的初期采用的方案是挂载 computed 和 watch 方法到每一个微信小程序组件,让小程序组件也支持这两个功能。

六,VUE 组件转换为微信小程序组件中 的 Data 部分的处理:

关于 Data 部分的处理实际上就是:函数表达式转换为对象表达式 (FunctionExpression 转换为 ObjectExpression)

转换之前的 JavaScript 代码
1
2
3
4
5
6
7
8
9
10
11
12
export default {
data(){//函数表达式
return {
num: 10000,
arr: [1, 2, 3],
obj: {
d1: "val1",
d2: "val2"
}
}
}
}
处理后我们得到的
1
2
3
4
5
6
7
8
9
10
export default {
data: {//对象表达式
num: 10000,
arr: [1, 2, 3],
obj: {
d1: "val1",
d2: "val2"
}
}
};

通过如上的代码对比,我们看到了我们的转换前后代码的变化:

转换前后 AST 树对比图明确转换目标:




我们对 JavaScript 动了什么手脚(亦可封装成babel插件):
1
2
3
4
const ast = babylon.parse(code, {
sourceType: "module",
plugins: ["flow"]
});
1
2
3
4
5
6
7
//AST 转换node、nodetype很关键
traverse(ast, {
enter(path) {
//打印出node.type
console.log("enter: " + path.node.type);
}
})
我们的转换部分都尽量在一个 Traverse 中处理,减少 AST 树遍历的性能消耗
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//Data 函数表达式 转换为 Object
ObjectMethod(path) {
// console.log("path.node ",path.node )// data, add, textFn
if (path.node.key.name === "data") {
// 获取第一级的 BlockStatement,也就是 Data 函数体
path.traverse({
BlockStatement(p) {
//从 Data 中提取数据属性
datas = p.node.body[0].argument.properties;
}
});
//创建对象表达式
const objectExpression = t.objectExpression(datas);
//创建 Data 对象并赋值
const dataProperty = t.objectProperty(
t.identifier("data"),
objectExpression
);
//插入到原 Data 函数下方
path.insertAfter(dataProperty);
//删除原 Data 函数节点
path.remove();
}
}

七,VUE 组件转换为微信小程序组件中 CSS 部分的处理:

那 CSS 我们也是必须要处理的一部分,let try

以下是我们要处理的css样本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const code = `
.text-ok{
position: absolute;
right: 150px;
color: #e4393c;
}
.nut-popup-close{
position: absolute;
top: 50px;
right: 120px;
width: 50%;
height: 200px;
display: inline-block;
font-size: 26px;
}`;
处理后我们得到的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text-ok {
position: absolute;
right: 351rpx;
color: #e4393c;
}

.nut-popup-close {
position: absolute;
top: 117rpx;
right: 280.79rpx;
width: 50%;
height: 468rpx;
display: inline-block;
font-size: 60.84rpx;
}

通过前后代码的对比,我们看到了单位尺寸的转换(比如:top: 50px; 转换为 top: 117rpx;)。
单位的转换( px 转为了 rpx )

CSS 又做了哪些处理呢?

同样也有不少的 CSS Code Parsers 供我们选择 Cssom ,CssTree等等,
我们拿 Cssom 来实现上方css代码的一个简单的转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
var ast = csstree.parse(code);
csstree.walk(ast, function(node) {
if(typeof node.value == "string" && isNaN(node.value) != true){
let newVal = Math.floor((node.value*2.34) * 100) / 100;//转换比例这个根据情况设置即可
if(node.type === "Dimension"){//得到要转换的数字尺寸
node.value = newVal;
}
}
if(node.unit === "px"){//单位的处理
node.unit = "rpx"
}
});
console.log(csstree.generate(ast));

当然这只是一个 demo,实际项目中使用还的根据项目的实际情况出发,SCSS,LESS等等的转换与考虑不同的处理场景哦!

注:本文有些模块的转换实现还未在小程序开发工具中测试。

插播一个通过 AST 实现的好东东:

将 JavaScript 代码转化生成 SVG 流程图 js2flowchart( 4.5 k stars 在 GitHub )

当你拥有 AST 时,可以做任何你想要做的事。把AST转回成字符串代码并不是必要的,你可以通过它画一个流程图,或者其它你想要的东西。
js2flowchart使用场景是什么呢?通过流程图,你可以解释你的代码,或者给你代码写文档;通过可视化的解释学习其他人的代码;通过简单的js语法,为每个处理过程简单的描述创建流程图。
马上用最简单的方式尝试一下吧,去线上编辑看看 js-code-to-svg-flowchart [8]。
此处有必要附上截图一张。

八、总结:

通过以上我们的介绍,我们大概对抽象语法树有了初步的了解。总体思路是:我们用Babel的解析器 把 JavaScript 源码转化为抽象语法树,
再通过 Babel 的遍历器遍历 AST (抽象语法树),替换、移除和添加节点,得到一个新的 AST 树。最后, 使用,Babel 的代码生成器 Babel Generator 模块 读取 处理后的 AST 并将其转换为代码。任务就完成了!

本文通过对 VUE 组件转换为微信小程序组件的转换部分包括如下内容:

  1. VUE 组件 JavaScript模块 对外属性转换为小程序对外属性的处理
  2. VUE 组件 JavaScript模块 内部数据的转换为小程序内部数据的处理
  3. VUE 组件 JavaScript模块 methods 中的赋值语句转换为小程序赋值语句的处理
  4. VUE 组件 JavaScript模块 外层对象,生命周期钩子函数的处理与 CSS 模块的简易处理

希望,本文对大家有所帮助,在技术探索的路上,我们一往无前, Paladin 精神永存!
感谢大家的耐心阅读,也欢迎大家关注【全栈探索公众号】,每周都会有技术好文推出!

扩展阅读

[1] https://astexplorer.net/

[2] https://babeljs.io/docs/en/next/babel-types.html#objectexpression

[3] https://github.com/babel/babel/tree/6.x/packages/babel-types

[4] http://esprima.org/demo/parse.html#

[5] https://segmentfault.com/a/1190000014178462?utm_source=tag-newest

[6] https://zh.wikipedia.org/wiki/%E6%8A%BD%E8%B1%A1%E8%AA%9E%E6%B3%95%E6%A8%B9

[7] https://itnext.io/ast-for-javascript-developers-3e79aeb08343

[8] https://github.com/Bogdan-Lyashenko/js-code-to-svg-flowchart

AST 抽象语法树无处不在-第一回合


简介:
抽象语法树,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
通过操作这棵树,可以精确的定位到声明、赋值、运算语句,从而实现对代码的优化、变更等操作。

AST 抽象语法树,似乎我们平时并不会接触到。实际上在我们的项目当中,CSS 预处理,JSX 亦或是 TypeScript 的处理,代码格式化美化工具,Eslint, Javascript 转译,代码压缩,Webpack, Vue-Cli,ES6 转 ES5,当中都离不开 AST 抽象语法树这个绿巨人。先卖个关子,让我们看一下 AST 到的官方解释:

It is a hierarchical program representation that presents source code structure according to the grammar of a programming language, each AST node corresponds to an item of a source code.

中文的解释有:
抽象语法树(abstract syntax tree或者缩写为 AST ),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。和抽象语法树相对的是具体语法树(concrete syntaxtree),通常称作分析树(parse tree)。一般的,在源代码的翻译和编译过程中,语法分析器创建出分析树。一旦 AST 被创建出来,在后续的处理过程中,比如语义分析阶段,会添加一些信息。
抽象语法树,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
通过操作这棵树,可以精确的定位到声明、赋值、运算语句,从而实现对代码的优化、变更等操作。这些并不是我们想要看到的。

对于 AST 面纱的神秘感,似乎已经将要揭开。不错,在我刚接触到他的时候,同样感觉确实是难。但是当你开始了解了它以后,你就会越来越喜欢它。
因为他实在太强大了。AST 的水深是毋庸置疑的。但是,这才能更加激起我们探索他的欲望。

一、 一探究竟 AST

通过 astexplorer [1] (AST树查看网站),通过他你可以方便的查看代码的语法树,挑你喜欢的库。你可以在线把玩 AST,而且除了 JavaScript,HTML, CSS 还有很多其它语言的 AST 库,让我们对他有一个感性而直观的认识。
请看下图,看看AST语法树长什么样子:


此图看到的是一个 ExportDefaultDeclaration 也就是export default {},还有他的位置信息,注释失信,tokens等等。

国际惯例,先来一个小demo
输入数据
1
2
3
function square(n) {
return n * n;
}
处理数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
astFn() {
const code = `function square(n) {
return n * n;
}`;
//得到 AST 语法树
const ast = babylon.parse(code);

traverse(ast, {
enter(path) {
console.log("enter: " + path.node.type);
//如下的语句匹配到 return 中的 n 与参数 n,并且将它替换为 x。
if (path.node.type === "Identifier" && path.node.name === "n") {
path.node.name = "x";
}
}
});
generate(ast, {}, code);//将 AST 转换为代码
console.log(generate(ast, {}, code).code );//打印出转换后的 JavaScript 代码
}
输出数据
1
2
3
function square(x) {
return x * x;
}

我们看一下我们得到的 AST 树


二、 简单粗暴的版本

没有使用 AST 将 VUE 组件转换成小程序组件的简易版本介绍
下方是两段代码,简单的逻辑,实现思路,匹配目标字符串,替换字符,然后生成文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
regs:{//通过标签匹配来替换对应的小程序支持的标签
toViewStartTags: /(<h1)|(<s)|(<em)|(<ul)|(<li)|(<dl)|(<i)|(<span)/g,
toViewEndTags: /(<\/h1>)|(<\/s>)|(<\/em>)|(<\/ul>)|(<\/li>)|(<\/dl>)|(<\/i>)|(<\/span>)/g,
toBlockStartTags: /(<div)|(<p)/g,
toBlockEndTags: /(<\/div>)|(<\/p>)/g,
},
signObj: {//通过标签查找来分离脚本,模板和CSS
tempStart: '<template>',
tempEnd: '</template>',
styleStart: '<style scoped>',
styleEnd: '</style>',
scriptStart: '<script>',
scriptEnd: '</script>'
}

上方是正则版本的一些模板匹配规则,经过后续的一系列处理把一个 VUE组件处理得到对应的小程序的 WXML ,WXSS,JSON,JS,4个文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//文件
const wxssFilePath = path.join(dirPath, `${mpFile}.wxss`);
const jsFilePath = path.join(dirPath, `${mpFile}.js`);
const wxmlFilePath = path.join(dirPath, `${mpFile}.wxml`);
const jsonFilePath = path.join(dirPath, `${mpFile}.json`);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath);
}
fs.writeFile(wxssFilePath, wxssContent, err => {
if (err) throw err;
//console.log(`dist目录下生成${mpFile}.wxss文件成功`);
fs.writeFile(jsFilePath, jsContent, err => {
if (err) throw err;
// console.log(`dist目录下生成${mpFile}.js文件成功`);
fs.writeFile(wxmlFilePath, wxmlContent, err => {
if (err) throw err;
//console.log(`dist目录下生成${mpFile}.wxml文件成功`);
fs.writeFile(jsonFilePath, jsonContent, err => {
if (err) throw err;
console.log(`dist目录下生成${mpFile}.json文件成功`)
resolve(`生成${mpFile}.json文件成功`);
})
})
});
});

上方是处理得到的 WXML ,WXSS,JSON,JS,4个文件,并且生成到对应的目录下。代码实现用时较短,后续更改方案,并没有做优化,这里就不做详细展开讨论这个实现方案了。

三、 AST 三大法宝

Babel 是 JavaScript 编译器,更确切地说是源码到源码的编译器,通常也叫做“ 转换编译器(transpiler)”。 意思是说你为 Babel 提供一些 JavaScript 代码,Babel 更改这些代码,然后返回给你新生成的代码。

babel-core:Babel 的编译器;它暴露了 babel.transform 方法。

[1] babylon:Babylon 是 Babel 的解析器。用于生成 AST 语法树。

[2] babel-traverse:Babel 的遍历器,所有的transformers都使用该工具遍历所有的 AST (抽象语法树),维护了整棵树的状态,并且负责替换、移除和添加节点。我们可以和 Babylon 一起使用来遍历和更新节点。

[3] babel-generator:Babel 的代码生成器。它读取AST并将其转换为代码。

整个编译器就被分成了三部分:分析器、转换器、生成器,大致的流程是:

输入字符串 -> babylon分析器 parse -> 得到 AST -> 转换器 -> 得到 AST -> babel-generator -> 输出

总结核心三步:

AST 三大法宝
babylon.parse => traverse 转换 AST => generate(ast, {}, code).code) 生成

感兴趣的童鞋,可以在网上或者看参考资料都有介绍,这里篇幅有限,感兴趣我们下回详细展开。

四、 AST 到底怎么转换呢?

使用 AST 将 VUE 组件转换为小程序组件的部分实现。
今天我们主要分析 JavaScript 的部分处理。

转换之前的 VUE 组件代码 Demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default {
//组件的对外属性
props: {
max: {
type: Number,
value: 99
}
},
//组件的内部数据
data(){
return {
num:10000
}
},
//组件的方法
methods: {
textFn() {
this.num = 2
},
onMyButtonTap: function(){
this.num = 666
},
}
}
处理后我们得到的小程序组件 JavaScript 部分代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
export default {
properties: {
//组件的对外属性
max: {
type: Number,
value: 99
}
},
//组件的内部数据
data(){
return {
num: 10000
}
},
//组件的方法
methods: {
textFn() {
this.setData({
num: 2
});
},
onMyButtonTap: function () {
this.setData({
num: 666
});
}
}
};
我们对js动了什么手脚(亦可封装成babel插件):
1
2
3
4
5
//to AST
const ast = babylon.parse(code, {
sourceType: "module",
plugins: ["flow"]
});
1
2
3
4
5
6
7
//AST 转换 node,nodetype很关键
traverse(ast, {
enter(path) {
//打印出node.type
console.log("enter: " + path.node.type);
}
})
1
2
3
4
5
6
ObjectProperty(path) {
//props 替换为 properties
if (path.node.key.name === "props") {
path.node.key.name = "properties";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//修改methods中使用到数据属性的方法中。this.prop至this.data.prop等 与 this.setData的处理  感兴趣的童鞋可以尝试一下,代码仅供参考,提供一个思路,实际使用请详细阅读相关的文档来处理。
MemberExpression(path) {
let datasVals = datas.map((item,index) =>{
return item.key.name //拿到data属性中的第一级
})
if (//含有this的表达式 并且包含data属性
path.node.object.type === "ThisExpression" &&
datasVals.includes(path.node.property.name)
) {
path.get("object").replaceWithSourceString("this.data");
//判断是不是赋值操作
if (
(t.isAssignmentExpression(path.parentPath) &&
path.parentPath.get("left") === path) ||
t.isUpdateExpression(path.parentPath)
) {
const expressionStatement = path.findParent(parent =>
parent.isExpressionStatement()
);
//篇幅有限 省略部分
}
}
}
转换之前的js代码的部分 AST 树:


具体的 API 使用,童鞋们看一下babel相关的文档了解一下,一回生,二回熟嘛。

五、总结:

通过以上我们的介绍,我们大概对抽象语法树有了初步的了解。总体思路是:我们用Babel的解析器 把 JavaScript 源码转化为抽象语法树,
再通过 Babel 的遍历器遍历 AST (抽象语法树),替换、移除和添加节点,得到一个新的 AST 树。最后, 使用,Babel 的代码生成器 Babel Generator模块 读取 处理后的 AST 并将其转换为代码。任务就完成了!

本文对于 VUE 组件转换为小程序组件的转换部分包括如下内容:

  1. VUE 组件对外属性转换为小程序对外属性的处理
  2. VUE 组件内部数据的转换为小程序内部数据的处理
  3. VUE 组件中 methods 中的赋值语句转换为小程序赋值语句的处理简单介绍
    具体转换过程中涉及到的问题还有很多,有机会再做展开!
    感谢大家的耐心阅读,希望本文对大家有所帮助,在技术探索的路上,我们一往无前。
    也欢迎大家关注【全栈探索公众号】,每周都会有技术好文推出!

扩展阅读

[1] https://astexplorer.net/

[2] https://babeljs.io/docs/en/next/babel-types.html#objectexpression

[3] https://github.com/babel/babel/tree/6.x/packages/babel-types

[4] http://esprima.org/demo/parse.html#

[5] https://segmentfault.com/a/1190000014178462?utm_source=tag-newest

[6] https://zh.wikipedia.org/wiki/%E6%8A%BD%E8%B1%A1%E8%AA%9E%E6%B3%95%E6%A8%B9

[7] https://itnext.io/ast-for-javascript-developers-3e79aeb08343

函数式编程四特征

本文分为五个部分:

  1. 为什么要用函数式编程
  2. 函数式编程的概要
  3. 函数式编程的四特征
  4. 函数式编程四特征总结

一,为什么要用函数式编程

面向对象的一些特点

  1. 某些场景代码充斥着无数的for循环,以及无数的分支判断语句,使代码的可读性,健壮性降低
  2. 某些场景代码定位问题比较困难,因为某些方法的每次调用输出都不一样,并且会改变输入数据,(数据经常被一些不纯函数无意间修改)导致代码出错率增加
  3. 函数使用不够灵活,不能自由组合,复用率低

二,函数式编程概要

注:本文用到的函数式库是ramda,类似函数式库有underscore,lodash等
通过此图我们先大概看一下函数式编程的函数组合的特性

接下来我们具体看一下函数式编程

函数式思想-λ演算

函数式思想展现的是一种纯粹的数学思维。来源于Lambda演算 ( λ ) (演算是一套用于研究函数定义、函数应用和递归的形式系统)
函数并不代表任何物质(对象,相对于面向对象思想而言),而它仅仅代
表一种针对数据的转换行为。它们是对行为的最高抽象,具有非凡
的抽象能力和表现力。函数式编程, 就是用函数(计算)来表示程序, 用函数(计算)的组合来表达程序的组合的思维方式.

函数式编程的本质

  1. 编程任务 (一个或多个这样的任务,就组成了程序)

编程主要就是写中间的那部分运算逻辑,如图:

现在,主流写法是过程式编程和面向对象编程,还有擅长纯运算函数式编程(强调将计算过程分解成可复用的函数)

  1. 我们可以把整个运算过程,想象成一根水管(pipe),数据从这头进去,那头出来,如图:

函数式编程思维, 就是用函数(计算)来表示程序, 用函数(计算)的组合来表达程序的组合的思维方式.

三,函数式编程的四特征

FP特点一 纯函数

纯函数是指只处理传入的参数。

  1. 一定有返回值(返回一个新的值,不改变原始数据)
  2. 至少传入一个参数
  3. 没有副作用,输入相同,保证输出相同(幂等函数)

javascript内置函数有不少纯函数,也有不少非纯函数。

纯函数:
1
2
3
4
5
1. Array.prototype.slice

2. Array.prototype.map

3. String.prototype.toUpperCase
非纯函数:
1
2
3
4
5
1. Math.random

2. Date.now

3. Array.ptototype.splice

我们看一下非纯函数 splice 和 纯函数 slice

1
2
3
4
5
//splice 是典型的非纯函数,每次的返回值不一次,而且会把输入数据改变
var r1 = [1,2,3,4,5,6,7];
r1.splice(0,3);// 输出[1,2,3] 此时r1为[4, 5, 6, 7]
r1.splice(0,3);// 输出[4, 5, 6] 此时r1为[7]
r1.splice(0,3);// 输出[7] 此时r1为[]
1
2
3
4
5
//slice 是纯函数,每次的返回值都一样,而且会把输入数据不会被改变
var r=[1,2,3,4]
r.slice(0,3);//输出 [1, 2, 3] 此时 r 为  [1, 2, 3, 4]
r.slice(0,3);//输出 [1, 2, 3] 此时 r 为  [1, 2, 3, 4]
r.slice(0,3);//输出 [1, 2, 3] 此时 r 为  [1, 2, 3, 4]

这就是我们强调使用纯函数的原因,因为纯函数相对于非纯函数来说,在可缓存性、可移植性、可测试性以及并行计算方面都有着巨大的优势。

FP特点二 不可变

函数式编程中 (不可变数据使代码更简单和安全)

  1. 没有变量 由于历史原因存储的值仍然叫变量,但他们是不可变的。比如说,一旦 x 中存了一个值,它的生命周期中就一直是这个值;
  2. 没有循环 通过递归来做循环 非递归的循环更容易出问题,因为需要使用可变变量
1
2
3
4
5
6
//通过for来求和,使用可变变量i,是存在一定风险的。
var acc = 0;
for (var i = 1; i <=10; ++i){
acc += i;
}
console.log(acc);//55

当然我们也可以使用let来规避,但其实也是存在风险的

1
2
3
4
5
6
7

var acc = 0;

for (let i1 = 1; i1 <=10; ++i1){ i1=111
acc += i1;
}
console.log(acc);//111
1
2
3
4
5
6
//函数式当中递归的处理方式,很显然,此函数只要输入相同,输出就一定相同,不存在可变变量。
function sumRange(start, end, acc){
if (start > end) return acc;
return sumRange(start + 1, end, acc + start)
}
console.log(sumRange(1, 10, 0));//55

还有一种不错的,用js内置的reduce来处理,看上去好了很多。这是js趋向于函数式的一种体现。

1
2
3
4
5
var arr = [0,1,2,3,4,5,6,7,8,9,10]; 
//reduce 数组成员依次执行指定函数,每一次的运算结果都会进入一个累积变量。
arr.reduce(function (preValue,curValue,index,array) {
return preValue + curValue;
});

那么,我们用ramdajs中的reduce来处理一下

1
2
3
4
5
var add = function (a, b) {
return a + b;
};
R.reduce(add, 0)(R.range(0,11)) // 55
//R.range(0,11) 输出为 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

FP特点三 柯里化

柯里化(又称部分求值),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数(多参转为单参函数)

举栗子
1
2
3
4
5
6
var addFourNumbers = (a, b, c, d) =>
a + b + c + d;
var curriedAddFourNumbers = R.curry(addFourNumbers);//通过R.curry对普通函数进行柯里化
var f = curriedAddFourNumbers(1, 2);//先传入部分参数,直到参数传齐才求值,惰性求值
var g = f(3);
g(4) // 10

map我们经常使用,但是,我们有什么理由用函数式里的map呢
首先,看一下map的基本使用

1
2
3
4
5
var double = x => x * 2;
var oneParam = R.map(double);
oneParam([2,4,5,6,7]) //[4, 8, 10, 12, 14]
oneParam({x: 1, y: 2, z: 3}) //{x: 2, y: 4, z: 6}
//和js原型上的map的区别是 数据的接收方式不同,与R.map是柯里化后的。
1
2
3
4
5
6
7
8
9
var users = [
{name: 'chet', age:25},
{name:'joe', age:24}
];
var a =R.pipe(
R.sortBy(R.prop('age')), //根据age的属性值来排序,输出排序后的数组对象 输出[{name:'joe', age:24},{name: 'chet', age:25}]
R.map(R.prop('name')), // 输入 [{name:'joe', age:24},{name: 'chet', age:25}] 输出["joe", "chet"]
R.join(',') ); //相当于 R.join(',')(["joe", "chet"])
a(users) // 最终得到 "joe,chet"

FP特点四 高阶函数

函数可以作为参数被传递;函数可以作为返回值输出。
将复用的粒度降低到函数级别,相对于面向对象语言,复用的粒度更低。

举栗子
1
2
3
4
5
compose (将多个函数合并成一个函数,从右到左执行)
R.compose(Math.abs, R.add(1), R.multiply(2))(-4) // 7
//R.multiply(2) 输出-8
//R.add(1) 输入-8, 输出-7
//Math.abs 输入-7, 输出7
1
2
3
4
5
6
7
8
pipe(将多个函数合并成一个函数,从左到右执行)
var negative = x => -1 * x;
var increaseOne = x => x + 1;
var f = R.pipe(Math.pow, negative, increaseOne);
f(3, 4) // -80 => -(3^4) + 1
// Math.pow 输入 3和4,输出 81
// negative 输入 81, 输出 -81
// increaseOne 输入 -81, 输出 -80

函数式库在mvvm框架中的使用

####只要是处理数据,我们随时可以使用ramdajs这样的函数式库来处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
created(){
indexApi.getFloorsData((res) => {
let data = (typeof(res) === 'string' ? JSON.parse(res) : res);
if(data.code == 200) {
this.floorData = data;
let errorImg = commonImg.state.placeholderImgFifth;
let noObj = (item) => R.type(item.imgurl) != "Object";
let addProp = (item) => R.merge(item)({'imgurl': {src: item.imgurl,error: errorImg,loading: errorImg}});
let addImgProp = R.pipe(R.filter(noObj),R.map(addProp));
let baseItems = R.pipe(R.take(10),R.pluck('baseItems'))(this.floorData.floorMenuItems);//所有楼层数据
let baseItemsAddImgList = R.map(addImgProp)(baseItems);//增加默认图对象
let hasProdAndSkuNo = (item) => R.has('productId')(item) && R.has('skuNo')(item);
var prodIdSkuIdList = (item) => R.assoc("prodIdSkuId",R.prop('productId')(item) + '-' + R.prop('skuNo')(item))(item);
let mapPlusIds = R.pipe(R.filter(hasProdAndSkuNo),R.map(prodIdSkuIdList));

var baseItemsImgsAndproIdAndSkuNoList = R.map(mapPlusIds)(baseItemsAddImgList);//写入属性prodIdSkuId到baseItems中

var proIdAndSkuNoListList = R.pipe(R.map(R.pluck('prodIdSkuId')),R.join(','))(baseItemsImgsAndproIdAndSkuNoList);//得到productId-skuNo串

this.floorData.floorMenuItems =R.zipWith(R.assoc('baseItems'),baseItemsImgsAndproIdAndSkuNoList,this.floorData.floorMenuItems);//数据组合
}
indexApi.getPrice(proIdAndSkuNoListList, (priceRes) => {
let priceData = typeof(priceRes) === 'string' ? JSON.parse(priceRes) : priceRes;
if(R.prop('code')(priceData.result) == 200) {
R.flatten(baseItemsImgsAndproIdAndSkuNoList).map(function(v,index){;
Object.keys(priceData.data).map((pidSid) => {
if(R.equals(pidSid,v.prodIdSkuId)){
this.$set(v,'newPrice',priceData.data[pidSid]['price'].pcPrice);
}
});
});
}
});
}, (error) => {
console.log(error);
});
}

四,函数式编程四特征总结

  1. 纯函数 也称幂等函数 (纯函数,没有“副作用”)
  2. 不可变 (使代码更简单和安全)
  3. 柯里化 (多参函数转为可接收单参数函数)
  4. 高阶函数 (使函数的复用粒度更低)

通过 柯里化 , 高阶函数 可以得到接近自然语言的表达

当下流行的vue,react也都提供了函数式组件,使用函数式组件最大的好处就是它能够将容器型和展示型组件明确区分开来,避免产生大型以及杂乱的组件。没有 this 关键字也就意味着没有快捷方式在整个应用中随机地展开状态。

五,常见的函数式编程语言

###比较纯粹的函数式编程语言

比较纯粹的函数式编程语言 支持函数式的编程语言
Clojure C++
Erlang Java
Haskell javascript
Lisp php
Perl python
Schema ruby
Scala
elm

五,参考资料

  1. https://baike.baidu.com/item/%CE%BB%E6%BC%94%E7%AE%97/8019133?fr=aladdin
  2. https://ramdajs.com/
  3. https://baike.baidu.com/item/%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B/4035031?fr=aladdin
  4. 文中的代码片段可以直接引用ramdajs来运行。

最后,感谢您的耐心阅读,欢迎沟通,如有问题,欢迎指正。