前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文看懂如何将VUE组件转换为微信小程序组件

一文看懂如何将VUE组件转换为微信小程序组件

作者头像
极乐君
发布2019-12-31 18:35:48
4.2K0
发布2019-12-31 18:35:48
举报
文章被收录于专栏:极乐技术社区

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

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

本文通过对 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
输入数据
代码语言:javascript
复制
function square(n) {
return n * n;
}复制代码1
处理数据
代码语言:javascript
复制
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 代码
  }复制代码
输出数据
代码语言:javascript
复制
function square(x) {return x * x;}复制代码

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

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

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

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

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

代码语言:javascript
复制
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个文件。

代码语言:javascript
复制
//文件
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
代码语言:javascript
复制
export default {
//组件的对外属性
              props: {
max: {
type: Number,
value: 99
                  }
              },
//组件的内部数据
              data(){
return {
num:10000
                  }
              },
//组件的方法
              methods: {
                 textFn() {
this.num = 2
                },
onMyButtonTap: function(){
this.num = 666
                },
              }
            }复制代码
处理后我们得到的微信小程序组件 JavaScript 部分代码
代码语言: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插件):
代码语言:javascript
复制
//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 树与代码
代码语言:javascript
复制
export default {// VUE 组件的惯用写法用于导出对象模块
    data(){
    },
    methods:{
    },
    props:{
    }
}复制代码
转换之后的 AST 树与代码
代码语言:javascript
复制
components({//小程序组件的构造器
data(){
},
methods:{
},
props:{
}
})复制代码

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

我们做了什么(在转换中进入到 ExportDefault 中做对应的处理):
代码语言:javascript
复制
//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

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

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

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

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

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

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

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

转换之前的 JavaScript 代码
代码语言:javascript
复制
export default {
data(){//函数表达式
return {
num: 10000,
arr: [1, 2, 3],
obj: {
d1: "val1",
d2: "val2"
}
}
}
}
复制代码
处理后我们得到的
代码语言:javascript
复制
export default {
data: {//对象表达式
num: 10000,
arr: [1, 2, 3],
obj: {
d1: "val1",
d2: "val2"
}
}
};
复制代码

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

转换前后 AST 树对比图明确转换目标:
我们对 JavaScript 动了什么手脚(亦可封装成babel插件):
代码语言:javascript
复制
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 树遍历的性能消耗
代码语言:javascript
复制
//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样本
代码语言:javascript
复制
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;
}`;
复制代码
处理后我们得到的
代码语言:javascript
复制
.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代码的一个简单的转换。

代码语言:javascript
复制
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等等的转换与考虑不同的处理场景哦!

八、总结:

通过以上我们的介绍,我们大概对抽象语法树有了初步的了解。总体思路是:我们用Babel的解析器 把 JavaScript 源码转化为抽象语法树,

再通过 Babel 的遍历器遍历 AST (抽象语法树),替换、移除和添加节点,得到一个新的 AST 树。最后, 使用,Babel 的代码生成器 Babel Generator 模块 读取 处理后的 AST 并将其转换为代码。任务就完成了!

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

  1. VUE 组件 JavaScript模块 对外属性转换为小程序对外属性的处理
  2. VUE 组件 JavaScript模块 内部数据的转换为小程序内部数据的处理
  3. VUE 组件 JavaScript模块 methods 中的赋值语句转换为小程序赋值语句的处理
  4. VUE 组件 JavaScript模块 外层对象,生命周期钩子函数的处理与 CSS 模块的简易处理
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-12-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 极乐技术社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、 一探究竟 AST
    • 国际惯例,先来一个小demo
      • 输入数据
        • 处理数据
          • 输出数据
          • 二、 简单粗暴的版本(VUE 组件转换为微信小程序组件)
          • 三、 抽象语法树三大法宝
            • 总结核心三步:
              • 我们到底是如何通过 AST 将 VUE 组件转换为微信小程序组件的呢?
                • 转换之前的 VUE 组件代码 Demo
                • 处理后我们得到的微信小程序组件 JavaScript 部分代码
                • 我们对js动了什么手脚(亦可封装成babel插件):
                • 转换之前的js代码的部分 AST 树:
                • 具体的 API 使用,童鞋们看一下 babel 相关的文档了解一下。
                • 转换之前的 AST 树与代码
                • 转换之后的 AST 树与代码
                • 我们做了什么(在转换中进入到 ExportDefault 中做对应的处理):
                • 转换之前的 JavaScript 代码
                • 处理后我们得到的
                • 转换前后 AST 树对比图明确转换目标:
                • 我们对 JavaScript 动了什么手脚(亦可封装成babel插件):
                • 我们的转换部分都尽量在一个 Traverse 中处理,减少 AST 树遍历的性能消耗
                • 以下是我们要处理的css样本
                • 处理后我们得到的
                • CSS 又做了哪些处理呢?
            • 四、 VUE 组件转换为微信小程序组件中 组件的对外属性、赋值语句的转换处理
            • 五, VUE 组件转换为微信小程序组件中 Export Default 到 Component 构造器的转换 与 生命周期钩子函数,事件函数的处理
            • 六,VUE 组件转换为微信小程序组件中 的 Data 部分的处理:
            • 七,VUE 组件转换为微信小程序组件中 CSS 部分的处理:
            • 八、总结:
            相关产品与服务
            云开发 CloudBase
            云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档