基于小程序的AST实践

背景

如今前端对移动端和小程序的开发热情很高,各类多端解决方案百花齐放。例如很火的Taro和mpvue,还有后来居上的uni-app等等。php

因公司业务须要,本人最近也在忙活各类小程序,例如:以前开发的小程序的业务逻辑须要在其余平台复用,咱们不可能把业务再重写一遍,因此须要研究下小程序之间的差别和转换,所以花了很多精力,有点心得体会,写点东西和你们交流交流。css

这篇总结文章主要是对转换工具 github.com/xujie-phper… 的介绍,想进一步研究的同窗能够带着问题看看代码,这样你就会更疑惑了~~html

小程序的比较

类型 微信小程序 百度小程序 支付宝小程序
api wx.* swan.* my.*
视图模版 循环: wx:for条件: wx:if 循环: s-for 条件:s-if条件判断中不须要使用插值语法 循环: a:for条件: a:if
事件处理 bindtap bindtap onTap
过滤器 wxs语法 filter语法
原生组件 除canvas,基本一致 除canvas,基本一致 更强大组件库系统
登录流程 指定scope受权 指定scope,获取code,换token 受权获取code,获取token
支付 微信支付 聚合收银台 支付宝
配置信息 project.config.json project.swan.json和pkginfo.json
生命周期和样式 一致(理论) 一致(理论) 一致(理论)

设计图

图片

流程图

图片

架构图

图片

生成项目基本文件目录

使用recursive-copy库,完成文件的总体拷贝,替换文件名后缀,例:.wxml===> .swan前端

转换json文件,去掉组件驼峰

一、找到josn文件里“usingComponents“包含的值,将组件引用中的驼峰改成kebabCase 二、若包含抽象节点“componentGenerics“字段,手百中不支持,存放在错误日志中vue

⚠️将修改后的组件名的映射关系记录在全局的contextStore中,属性值为“renamedComponents“,视图层中转换中须要使用新的组件名node

AST实战讲解

如下的转换逻辑会大量依赖babel,进行AST的代码转换,因此咱们先巩固下抽象语法树相关的知识。git

可能刚接触AST的人会感受无从下手,毕竟ast相关的知识点确实比较繁杂,并且相关的入门指导比较少。这里咱们以一个完整的例子,过一下AST经常使用基本语法,方便你们入门,虽然说是入门,但若是熟练掌握,已经能够应用于实际开发了。github

  1. 打开在线AST工具,发现新大陆长这样

高亮的是对应的代码段,左边是一个对象的属性,右边对应ast中的节点信息。

注意:js中不一样的数据类型,对应的ast节点信息也不竟相同。以图中为例,externalClasses对象的节点信息中类型(type)是ObjectProperty,包含key ,value等关键属性(其余类型节点可能就没有)json

  1. 打开transform开关,选择转换引擎,又发现了新大陆

这里咱们选择babel和配套的acorn,能够根据实际须要本身选择,这只是推荐。

注意选择最新的babel7版本,否则下面例子中的类型会匹配不上,canvas

  1. 如今的界面结构展现以下图,接下来就开始进行转换逻辑的代码编写

假设咱们的目标是要把properties属性中key为‘current’的属性改成myCurrent。let's go!

原始代码:

/*eslint-disable*/
/*globals Page, getApp, App, wx,Component,getCurrentPages*/
Component({
  externalClasses: ['u-class'],

  relations: {
    '../tab/index': {
      type: 'child',
      linked() {
        this.changeCurrent();
      },
      linkChanged() {
        this.changeCurrent();
      },
      unlinked() {
        this.changeCurrent();
      }
    }
  },

  properties: {
    current: {
      type: String,
      value: '',
      observer: 'changeCurrent'
    }
  },

  methods: {
    changeCurrent(val = this.data.current) {
      let items = this.getRelationNodes('../tab/index');
      const len = items.length;

      if (len > 0) {
        items.forEach(item => {
          item.changeScroll(this.data.scroll);
          item.changeCurrent(item.data.key === val);
          item.changeCurrentColor(this.data.color);
        });
      }
    },
    emitEvent(key) {
      this.triggerEvent('change', { key });
    }
  }
});

复制代码

首先在原始代码中选中'current',查看右边ast的节点结构,如图:

这是一个对象属性(ObjectProperty),关键节点信息为key和value,key自己也是一个ast节点,类型为Identifier(准确的应该是StringIdentifer,经常使用的还有NumberIdentifer等),'curent'是里面的name属性。因此咱们的第一步就是找到改节点,而后修改它。

查找

export default function (babel) {
  const { types: t } = babel;
  
  return {
    name: "ast-transform", // not required
    visitor: {
      Identifier(path) {
        //path.node.name = path.node.name.split('').reverse().join('');
      },
       ObjectProperty(path) {
         if (path.node.key.type === 'StringIdentifier' && 
             path.node.key.name === 'current') {
         	console.log(path,'StringIdentifier')
         }
  	   }
    }
  };
}

复制代码

这里须要用到@babel/typesbabeljs.io/docs/en/bab…来辅助咱们进行类型判断,开发中会很是依赖这个字典进行查找

在控制台会看见,path下面的节点信息不少,关键字段为node和parentPath,node记录了该节点下数据信息,例如以前提到过的key和value。parentPath表明父级节点,此例中表示ObjectExpression中properties节点信息,有时咱们须要修改父节点的数据,例如常见的节点移除操做。接下来咱们修改该节点信息。

修改

@babel/types中找到该ObjectProperty的节点信息以下,咱们须要须要构造一个新的同类型节点(ObjectProperty)来替换它。

能够看到关键信息是key和value,其余使用默认就好。value里面的信息咱们能够照搬,从原有的path里面获取,咱们更改的只是key里面的标识符'current'。由于key自己也是一个ast节点,因此咱们还须要查看字典,看看生成Identifier节点须要什么参数,步骤同样。修改代码以下:

ObjectProperty(path) {
         console.log(path,'ObjectProperty--')
         if (path.node.key.type === 'Identifier' && 
             path.node.key.name === 'current') {
            //替换节点
           path.replaceWith(t.objectProperty(t.identifier('myCurrent'), path.node.value));
         }
  	   }
复制代码

其中咱们用到了replaceWith方法,这个方法表示用一个ast节点来替换当前节点。 还有一个经常使用的replaceWithSourceString方法,表示用一个字符串来代替该ast节点,参数为一串代码字符串,如:'current : {type:String};',感兴趣的,能够本身试试。

最后查看转换后的代码,发现'current'已经被咱们替换成了'myCurrent'。

到这里,一个完整的例子就演示完了。这里补充说明一下,在实际中可能会遇到嵌套结构比较深的ast结构。咱们须要嵌套类型判断,好比:

ObjectProperty(path) {
     console.log(path,'ObjectProperty--')
      MemberExpression(memberPath) {
          console.log(path,'memberPath--')
      }
 }
复制代码

由于遍历中的path指定的是当前匹配的节点信息。因此能够为不一样的类型遍历指定不一样的path参数,来获取当前遍历的节点信息,避免path覆盖,例如上面的path和memberPath。

到这里,babel的基本用法就差很少介绍完了,想要熟练掌握,还须要你在项目中反复练习和实践。想系统学习babel,并在实际项目中使用的同窗能够先看看这篇babel的介绍文档,边写边查,巩固学习

逻辑层转换

借助babel的三剑客:@babel/parser@babel/traverse@babel/generator

js的转换规则较复杂,会大量依赖babel/types作类型判断,并借助在线AST工具辅助测试。

+--------+                     +----------+
Input ->- | Parser | ->- Syntax Tree ->- | Compiler | ->- Output
          +--------+          |          +----------+
                              X
                              |
                       +--------------+
                       | Transformers |
                       +--------------+
复制代码
  1. 名称不一样,功能相同的api,须要作映射,例: navigateToMiniProgram ===> navigateToSmartProgram

  2. 自定义组件的处理: 百度小程序构造器不支持的属性: moved,relations, observers 内置behaviors的处理:

    `wx://form-field` ===>  `swan://form-field`
    `wx://component-export` ===>  `swan://component-export`
    复制代码

    relations中如有使用link回调函数,则对应到百度的attached生命周期中执行, 配套使用的getRelationNodes,则对应百度的selectComponent方法。

    为解决页面多组件实例的问题,引入swanId作为惟一标识,咱们会为有依赖关系的组件添加swanId属性,同一组的父子组件共用一个swanId。

    全部的父子组件的依赖关系存在在全局的contextStore中,供视图层添加swanId时使用

  3. 独有api没法自动匹配,存放到转换日志中,需手动删除或替换对应逻辑

  4. 关键词替换:wx ===> swan

视图层转换

视图层的转换也是使用的AST,借助stricter-htmlparser2将html转化为节点树,遍历,替换指定节点,最后生成新的html结构。

<view wx:='aaa'>test</view>

"parseHtml": {
        "type": "tag",
        "name": "view",
        "attribs": {
            "wx:": "aaa"
        },
        "children": [
            {
                "data": "test",
                "type": "text"
            }
        ],
        "singleQuoteAttribs": {},
        "selfclose": false
    }
复制代码

循环和条件判断

微信

//循环
<view wx:for="{{array}}" wx:for-index="idx" wx:for-item="itemName">
  {{idx}}: {{itemName.message}}
</view>
//条件
<view wx:if="{{view == 'WEBVIEW'}}"> WEBVIEW </view>
<view wx:elif="{{view == 'APP'}}"> APP </view>
<view wx:else="{{view == 'MINA'}}"> MINA </view>
复制代码

百度

//循环
<view>
    <view s-for="p,index in persons">
        {{index}}: {{p.name}}
    </view>
</view>
//条件
<view s-if="is4G">4G</view>
<view s-elif="isWifi">Wifi</view>
<view s-else>Other</view>
复制代码

转换逻辑为:

  1. 将wx:替换为s-,例:wx:if =====> s-if
  2. 去掉插值语法(花括号)
  3. wx:for, wx:for-index,wx:for-item合并为s-for="p,index in persons"

模版的转换

<template name="msgItem">
  <view>
    <text> {{index}}: {{msg}} </text>
    <text> Time: {{time}} </text>
  </view>
</template>

//**微信**:
<template is="msgItem" data="{{...item}}"/>
//**百度**:
<template is="msg-item" data="{{ {...item} }}" />
复制代码

转换逻辑为:

  1. data属性外增长一个大括号
  2. 名称改成小写字母与中划线“-”的组合

forif做用于同一标签

微信可使用,手百禁止, 编译会报错

注意: s-ifs-for 不可在同一标签下同时使用。

将微信中的if标签,借助虚拟组件block,分红父子组件。 例:

<view wx:for="{{list}}" wx:if="{{item}}">test</view>
复制代码

转化为

<view s-for="item, index in list">
     <block s-if="item">test</block>
 </view>
复制代码

双向绑定

//**微信**:
<scroll-view scroll-into-view="{{toView}}" scroll-top="{{scrollTop}}">
    <view id="green" class="scroll-view-item bc_green"></view>
</scroll-view>

//**百度**:
<scroll-view scroll-into-view="{=toView=}" scroll-top="{=scrollTop=}">
    <view id="green" class="scroll-view-item bc_green"></view>
</scroll-view>
复制代码

转换逻辑为:

将插值语法变换为{= * =}

wxs语法

微信使用wxs来进行数据处理,定义共用函数段;对应的百度的filter语法

//**微信**:
<wxs module="test">
    var some_msg = "hello world";
    module.exports = {
        setPosition: function (position) {
            return 'transform: translateX(' + position.pageX + 'px);';
        }
    }
</wxs>

//**百度**:
<filter module="test">
    var some_msg = "hello world";
    export default {
        setPosition: function (position) {
            return 'transform: translateX(' + position.pageX + 'px);';
        }
    }
</filter>
复制代码

转换逻辑为:

将module.exports替换为export default

注:百度的filter中不支持导出变量,可是微信是支持的,全部这部分须要开发者手动处理下逻辑

样式文件的转换

小程序间的样式彻底同样,只是文件后缀名不一样,只须要替换引入的样式文件后缀wxss ===> css

例:

@import "header.wxss";
复制代码

转化为:

@import "header.css";
复制代码

转换日志

转换日志分为'info'、'warning'、'error'三种类型,转换过程当中产生的日志信息都存放在统一logStore中,结束时会借助mkdirpfs 能力把logStore存储的全部信息,写入到日志文件中。

:小程序独有能力和私有能力,没法转化(目前),须要手动进行逻辑替换或删除。转换中不涉及项目依赖文件的替换,例:project.swan.jsonpkginfo.json,可使用百度开发者工具自动生成

后记

以上所讨论的都是最近写的一个微信转百度小程序工具的详细介绍和具体实现,对小程序和babel感兴趣的能够去看看代码,应该会有所收获,并能发现其中还存在的一些问题,欢迎讨论,一块儿学习。

相关文章
相关标签/搜索