VUE 组件转换为微信小程序组件的方法


Posted in Javascript onNovember 06, 2019

简介:

首先我们介绍一下本文的关键点:抽象语法树,它是以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

通过操作这棵树,可以精确的定位到声明、赋值、运算语句,从而实现对代码的优化、变更等操作。

本文通过对 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

输入数据

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: /(<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个文件。

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

马上用最简单的方式尝试一下吧,去线上编辑看看 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 模块的简易处理

总结

以上所述是小编给大家介绍的VUE 组件转换为微信小程序组件的方法,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

Javascript 相关文章推荐
利用JS重写Cognos右键菜单的实现代码
Apr 11 Javascript
jQuery中filter(),not(),split()使用方法
Jul 06 Javascript
javascript学习笔记(九)javascript中的原型(prototype)及原型链的继承方式
Apr 12 Javascript
使用jQuery实现验证上传图片的格式与大小
Dec 03 Javascript
详解Matlab中 sort 函数用法
Mar 20 Javascript
JavaScript预解析及相关技巧分析
Apr 21 Javascript
jQuery中deferred对象使用方法详解
Jul 14 Javascript
带你了解session和cookie作用原理区别和用法
Aug 14 Javascript
对vue事件的延迟执行实例讲解
Aug 28 Javascript
js实现倒计时秒杀效果
Mar 25 Javascript
jquery将信息遍历到界面上实例代码
Jan 21 jQuery
微信小程序实现自定义动画弹框/提示框的方法实例
Nov 06 Javascript
vuex存储复杂参数(如对象数组等)刷新数据丢失的解决方法
Nov 05 #Javascript
解决vue.js提交数组时出现数组下标的问题
Nov 05 #Javascript
js+html实现点名系统功能
Nov 05 #Javascript
vuex 实现getter值赋值给vue组件里的data示例
Nov 05 #Javascript
在Vue mounted方法中使用data变量详解
Nov 05 #Javascript
解决vue项目F5刷新mounted里的函数不执行问题
Nov 05 #Javascript
vue input标签通用指令校验的实现
Nov 05 #Javascript
You might like
外媒评选出10支2020年最受欢迎的Dota2战队
2021/03/05 DOTA
真正面向对象编程:PHP5.01发布
2006/10/09 PHP
php中文本操作的类
2007/03/17 PHP
PHP使用数组实现队列
2012/02/05 PHP
分享PHP守护进程类
2015/12/30 PHP
Symfony实现行为和模板中取得request参数的方法
2016/03/17 PHP
php检查函数必传参数是否存在的实例详解
2017/08/28 PHP
原生php实现excel文件读写的方法分析
2018/04/25 PHP
php 自定义函数实现将数据 以excel 表格形式导出示例
2019/11/13 PHP
PHP图像处理 imagestring添加图片水印与文字水印操作示例
2020/02/06 PHP
跨浏览器的事件对象介绍
2012/06/27 Javascript
关于IE BUG与字符串截取substr的解决办法
2013/04/10 Javascript
js confirm()方法的使用方法实例
2013/07/13 Javascript
js语法学习之判断一个对象是否为数组
2014/05/13 Javascript
js获取网页可见区域、正文以及屏幕分辨率的高度
2014/05/15 Javascript
javascript中String对象的slice()方法分析
2014/12/20 Javascript
基于jQuery实现的双11天猫拆红包抽奖效果
2015/12/01 Javascript
详解JavaScript权威指南之对象
2016/09/27 Javascript
js控制div层的叠加简单方法
2016/10/15 Javascript
微信小程序 教程之数据绑定
2016/10/18 Javascript
angularjs中ng-attr的用法详解
2016/12/31 Javascript
React Native中NavigatorIOS组件的简单使用详解
2018/01/27 Javascript
JavaScript动态创建二维数组的方法示例
2019/02/01 Javascript
谈谈node.js中的模块系统
2020/09/01 Javascript
[44:41]Fnatic vs Liquid 2018国际邀请赛小组赛BO2 第二场 8.16
2018/08/17 DOTA
python使用生成器实现可迭代对象
2018/03/20 Python
python用for循环求和的方法总结
2019/07/08 Python
python文字和unicode/ascll相互转换函数及简单加密解密实现代码
2019/08/12 Python
浅谈keras中loss与val_loss的关系
2020/06/22 Python
在vscode中启动conda虚拟环境的思路详解
2020/12/25 Python
企业介绍信范文
2015/01/30 职场文书
工程质检员岗位职责
2015/04/08 职场文书
2015年前台文员工作总结
2015/05/18 职场文书
仓库管理制度范本
2015/08/04 职场文书
MySQL sql_mode修改不生效的原因及解决
2021/05/07 MySQL
多人盗宝《绿林侠盗》第三赛季4.5上线 跨平台实装
2022/04/03 其他游戏