基於小程序的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感興趣的能夠去看看代碼,應該會有所收穫,並能發現其中還存在的一些問題,歡迎討論,一塊兒學習。

相關文章
相關標籤/搜索