微信小程序> VUE组件转换为微信小程序组件

VUE组件转换为微信小程序组件

浏览量:10954 时间: 来源:微信小程序

本文通过对 VUE 组件的 JavaScript 、CSS模块进行转换,比如 JavaScript模块,包括外层对象,生命周期钩子函数,赋值语句,组件的内部数据,组件的对外属性等等来实现把一个 VUE 组件转换为 一个微信小程序组件。

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

一、  AST是什么?

通过AST树查看网站,通过他你可以方便的查看代码的语法树,挑你喜欢的库。你可以在线把玩 AST,而且除了 JavaScript,HTML, CSS 还有很多其它语言的 AST 库,让我们对他有一个感性而直观的认识。

请看下图,看看AST语法树长什么样子:

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

国际惯例,先来一个小demo

输入数据

function square(n) { return n * n;}复制代码

处理数据

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 代码 }复制代码

输出数据

function square(x) { return x * x;}复制代码

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

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


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

没有使用 AST 将 VUE 组件转换成小程序组件的简易版本介绍

下方是两段代码,简单的逻辑,实现思路,匹配目标字符串,替换字符,然后生成文件。

regs:{//通过标签匹配来替换对应的小程序支持的标签 toViewStartTags: /(toViewEndTags: /(<\/h1>)|(<\/s>)|(<\/em>)|(<\/ul>)|(<\/li>)|(<\/dl>)|(<\/i>)|(<\/span>)/g, toBlockStartTags: /(toBlockEndTags: /(<\/div>)|(<\/p>)/g,}, signObj: {//通过标签查找来分离脚本,模板和CSS tempStart: '', styleStart: '', scriptStart: '' }复制代码

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

//文件 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

export default { //组件的对外属性 props: { max: { type: Number, value: 99 }              }, //组件的内部数据 data(){ return { num:10000 }              }, //组件的方法 methods: {                 textFn() { this.num = 2 }, onMyButtonTap: function(){ this.num = 666 },              }            }复制代码

处理后我们得到的微信小程序组件 JavaScript 部分代码

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插件):

//to AST const ast = babylon.parse(code, { sourceType: "module", plugins: ["flow"]}); //AST 转换 node,nodetype很关键 traverse(ast, {   enter(path) { //打印出node.type console.log("enter: " + path.node.type);   }})ObjectProperty(path) { //props 替换为 properties if (path.node.key.name === "props") {      path.node.key.name = "properties";    }} //修改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 树与代码

export default {// VUE 组件的惯用写法用于导出对象模块 data(){    }, methods:{    },    props:{    }}复制代码

转换之后的 AST 树与代码

components({//小程序组件的构造器 data(){    }, methods:{    }, props:{    }})复制代码

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

我们做了什么(在转换中进入到 ExportDefault 中做对应的处理):

//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

//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] 第二种思路呢,就是我们上面展示的这种,不过有一个关键的地方要注意一下:

//我把 ExportDefaultDeclaration 的处理放到最后来执行,拿到 AST 首先进行转换。然后在创建得到新的小程序组件JS部分的 AST 即可 traverse(ast, { enter(path) {},      ObjectProperty(path) {},      ObjectMethod(path) {}, //...... ExportDefaultDeclaration(path) { //... }})复制代码

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

path.insertBefore( t.expressionStatement(t.stringLiteral("start.."))); path.insertAfter( t.expressionStatement(t.stringLiteral("end..")));复制代码

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


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

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

转换之前的 JavaScript 代码

export default { data(){//函数表达式 return { num: 10000, arr: [1, 2, 3], obj: { d1: "val1", d2: "val2" }      }    }}复制代码

处理后我们得到的

export default { data: {//对象表达式 num: 10000, arr: [1, 2, 3], obj: { d1: "val1", d2: "val2" }  }};复制代码

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

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

我们对 JavaScript 动了什么手脚(亦可封装成babel插件):

const ast = babylon.parse(code, {  sourceType: "module",  plugins: ["flow"]});//AST 转换node、nodetype很关键 traverse(ast, {  enter(path) {    //打印出node.type console.log("enter: " + path.node.type);  }})复制代码

我们的转换部分都尽量在一个 Traverse 中处理,减少 AST 树遍历的性能消耗

//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样本

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;  }`;复制代码

处理后我们得到的

.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代码的一个简单的转换。

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语法,为每个处理过程简单的描述创建流程图。

版权声明

即速应用倡导尊重与保护知识产权。如发现本站文章存在版权问题,烦请提供版权疑问、身份证明、版权证明、联系方式等发邮件至197452366@qq.com ,我们将及时处理。本站文章仅作分享交流用途,作者观点不等同于即速应用观点。用户与作者的任何交易与本站无关,请知悉。

  • 头条
  • 搜狐
  • 微博
  • 百家
  • 一点资讯
  • 知乎