用戶
 找回密碼
 立即注冊

QQ登錄

只需一步,快速開始

掃一掃,登錄網站

小程序社區 首頁 教程 查看內容

通過一個需求揭秘多端編譯

Rolan 2020-3-18 00:51

過年在家辦公期間,接到了一個需求,需要將目前的微信小程序自定義組件擴展到支付寶小程序平臺。關于需求的背景和歷史這邊就暫不多說了,就從上面已說明的內容來看待這個需求吧。 接到需求的第一時間,筆者就思考, ...

過年在家辦公期間,接到了一個需求,需要將目前的 微信小程序自定義組件 擴展到 支付寶小程序 平臺。關于需求的背景和歷史這邊就暫不多說了,就從上面已說明的內容來看待這個需求吧。 接到需求的第一時間,筆者就思考,這不就是多端編譯嗎?話不多說,那就開搞吧。

背景介紹

由于筆者的項目是一個單純的微信小程序自定義組件,打包工具是rollup,所以,筆者的技術方案是編寫一個rollup插件,來支持多端編譯。關于rollup和rollup插件的寫法本次不作過多介紹,有興趣的可以看它的 官方文檔 ,這邊只是介紹一下核心的多端編譯流程。

流程介紹

微信小程序組件包含 *.json 、 *.js 、 *.wxml 、 *.wxss 這4個文件,要轉換成支付寶小程序,其中json文件和wxss文件比較簡單,前者原封不動,后者改一下后綴名就好了,主要要修改js和wxml兩個文件。

大致流程基本就是如下

  1. 差異整理
  2. 將代碼轉成AST樹
  3. 替換樹上的節點
  4. 根據新的AST樹生成代碼

acorn

對于js文件,要實現這些功能的話,業界已經有一些出色的工具了。筆者選擇了 babel ,babel內置acron作為javascript解釋器,生成符合estree標準的AST樹(可以在 astexplorer.net/ 中查看效果)。其次babel的封裝很漂亮,除了搭配webpack完成日常的構建工作外,它還提供了 @babel/parser , @babel/generator , @babel/traverse , @babel/types 等優秀的工具包,每個工具包都是單一職責,職責很明確,幫助實現以上的流程(其實rollup內置了acron實例,不過babel會更好用一些)。 其中 @babel/parser 可以將js代碼解釋為AST樹, @babel/generator 將根據AST樹生成js代碼, @babel/traverse 支持高效地操作AST樹的節點, @babel/types 則提供一些判斷函數,幫助開發者快速定位節點。

看一個簡單的示例

function sayHello() {
  console.log('hello')
}

sayHello();
復制代碼

對于以上這段代碼,通過acron轉換后,得出的AST樹如下

{
  "type": "Program",
  "start": 0,
  "end": 58,
  "body": [
    {
      "type": "FunctionDeclaration",
      "start": 0,
      "end": 45,
      "id": {
        "type": "Identifier",
        "start": 9,
        "end": 17,
        "name": "sayHello"
      },
      "expression": false,
      "generator": false,
      "async": false,
      "params": [],
      "body": {
        "type": "BlockStatement",
        "start": 20,
        "end": 45,
        "body": [
          {
            "type": "ExpressionStatement",
            "start": 23,
            "end": 43,
            "expression": {
              "type": "CallExpression",
              "start": 23,
              "end": 43,
              "callee": {
                "type": "MemberExpression",
                "start": 23,
                "end": 34,
                "object": {
                  "type": "Identifier",
                  "start": 23,
                  "end": 30,
                  "name": "console"
                },
                "property": {
                  "type": "Identifier",
                  "start": 31,
                  "end": 34,
                  "name": "log"
                },
                "computed": false
              },
              "arguments": [
                {
                  "type": "Literal",
                  "start": 35,
                  "end": 42,
                  "value": "hello",
                  "raw": "'hello'"
                }
              ]
            }
          }
        ]
      }
    },
    {
      "type": "ExpressionStatement",
      "start": 47,
      "end": 58,
      "expression": {
        "type": "CallExpression",
        "start": 47,
        "end": 57,
        "callee": {
          "type": "Identifier",
          "start": 47,
          "end": 55,
          "name": "sayHello"
        },
        "arguments": []
      }
    }
  ],
  "sourceType": "module"
}
復制代碼

對于這段js代碼,如果要替換它的方法名為 sayHi 、打印出的 hello 替換為 Hi ,通過babel,只需要這樣做就可以了。

import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
import generate from "@babel/generator";
import * as t from "@babel/types";

const code = `
function sayHello() {
  console.log('hello')
}

sayHello();
`;

const transform = code => {
  const ast = parse(code);
  traverse(ast, {
    enter(path) {
      if (t.isIdentifier(path.node, { name: "sayHello" })) {
        path.node.name = "sayHi";
      }
      if (t.isLiteral(path.node, { value: "hello" })) {
        path.node.value = "Hi";
      }
    }
  });
  const output = generate(ast, {}, code);
  return output;
};

console.log(transform(code).code);
復制代碼

也可以在 codeSandbox 中查看效果。

關于包的其它使用,可以查看 官方手冊 。

himalaya

對于wxml文件,筆者選擇了 himalaya-wxml ,它提供了 parse 和 stringify 兩個方法,前者將wxml解釋成AST樹,后者反之(可以在 jew.ski/himalaya/ 中查看效果)。通過 parse 將wxml代碼轉換成AST樹之后,接下去只需要手動遞歸遍歷AST樹去替換節點,再將其轉換回wxml代碼就可以完成工作了。

同樣,看一個簡單的示例

<div id='main'>
  <span>hello world</span>
</div>
復制代碼

對于以上html代碼,通過 himalaya 轉換后,生成的AST樹如下

[
  {
    "type": "element",
    "tagName": "div",
    "attributes": [],
    "children": [
      {
        "type": "text",
        "content": "\n  "
      },
      {
        "type": "element",
        "tagName": "span",
        "attributes": [],
        "children": [
          {
            "type": "text",
            "content": "hello world"
          }
        ]
      },
      {
        "type": "text",
        "content": "\n"
      }
    ]
  }
]
復制代碼

對于這段代碼html代碼,如果要替換它外層 div 的 id 為 container ,只需要這樣做就可以了。

import { parse, stringify } from "himalaya";

const code = `
<div id='main'>
  <span>hello world</span>
</div>
`;

const traverse = ast => {
  return ast.map(item => {
    if (item.type === "element" && item.attributes) {
      return {
        ...item,
        attributes: item.attributes.map(attr => {
          if (attr.key !== "id") {
            return attr;
          }
          return {
            ...attr,
            value: "container"
          };
        })
      };
    }
    return item;
  });
};

const transform = code => {
  const ast = parse(code);
  const json = traverse(ast);
  return stringify(json);
};

console.log(transform(code));
復制代碼

也可以在 codeSandbox 中查看效果。

核心介紹

流程和工具介紹的差不多了,接下來就開始正題吧。 首先是整理差異,根據筆者的調研,微信小程序組件要轉換成支付寶小程序組件,大致有以下幾個改動(只是符合筆者的需求,如果不完全,歡迎補充):

  1. wxml后綴名要改成axml
  2. wxss后綴名要改成acss
  3. wxml中的屬性wx-xxx要改成a-xxx
  4. wxml中的事件屬性bindxxx要改成onXxx
  5. 生命周期attached要替換成onInit
  6. 生命周期detached要替換成didUnmount
  7. 生命周期pageLifetimes.show要替換成didMount
  8. 生命周期pageLifetimes要刪除

改后綴名的工作相對簡單,交給構建工具,output配置里面指定一下就好了,重點是替換屬性。

轉換js部分代碼如下

import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
import generate from '@babel/generator';
import * as t from '@babel/types';

function transformJs(code: string) {
  const ast = parse(code);
  let pp;

  traverse(ast, {
    enter(path) {
      if (t.isIdentifier(path.node, {name: 'attached'})) {
        path.node.name = 'onInit';
      }
      if (t.isIdentifier(path.node, {name: 'detached'})) {
        path.node.name = 'didUnmount';
        pp = path.parentPath;
      }
      if(t.isIdentifier(path.node.key, {name: 'show'})){
        path.node.key.name = 'didMount';
        pp.insertAfter(path.node);
      }
    },
    exit(path) {
      if(t.isIdentifier(path.node.key, {name: 'pageLifetimes'})){
        path.remove();
      }
    }
  });
  const output = generate(ast, {}, code);
  return output
}

export default transformJs

復制代碼

轉換wxml部分如下:

import { parse, stringify } from 'himalaya-wxml';

const traverseKey = (key: string) => {
  if(key.startsWith('wx:')){
    const postfix = key.slice(3);
    return `a:${postfix}`;
  }
  if(key === 'catchtouchmove'){
    return 'catchTouchMove';
  }
  if(key === 'bindtap'){
    return 'onTap';
  }
  if(key === 'bindload'){
    return 'onLoad';
  }
  if(key === 'binderror'){
    return 'onError';
  }
  if(key === 'bindchange'){
    return 'onChange';
  }
  return key
}

const traverseAst = (ast: any) => {
  return ast.map(item => {
    if(item.type !== 'element'){
      return item;
    }
    let res = item;
    if(item.attributes){
      res = {
        ...item,
        attributes: item.attributes.map(attr => ({
          ...attr,
          key: traverseKey(attr.key)
        }))
      }
    }
    if(item.children){
      res.children = traverseAst(item.children);
    }
    return res
  });
}

const transformWxml = (code: string) => {
  const ast = parse(code);
  const json = traverseAst(ast);
  return stringify(json)
}

export default transformWxml
復制代碼

以上,就擁有了兩個轉換函數,再之后的工作,就是將這兩個函數運行在rollup里,就完成了將微信小程序組件轉換成支付寶小程序組件的功能。

總結

javascript作為前端最常用的語言,我們不僅要熟悉它,更要能操控它,通過javascript解釋器,我們就擁有了操控它的能力。回本碩源,鞏固基礎,才能在寒冬之中保持內心的平靜。

鮮花
鮮花
雞蛋
雞蛋
分享至 : QQ空間
收藏
原作者: leeing 來自: 掘金
河北20选5大星走势图 四川金七乐手机版 广西快3开奖号码查询 pc蛋蛋幸运28计划软件 现在什么理财平台好 pk10cqswlzx 福建快三走势图结果 安徽十一选五开奖结果查询结果 广西快三全天在线计划 配资网 河南快赢481有多少数 2007上证指数最高 四川体育彩票兑奖流程 炒股投资 北京快乐8全包稳赚法 河南快3开奖图 七星彩预测最准确