【轉】VUE 組件轉換為微信小程序組件的方法


VUE 組件轉換為微信小程序組件的方法

 
這篇文章主要介紹了VUE 組件轉換為微信小程序組件的方法,本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下
 

簡介:

首先我們介紹一下本文的關鍵點:抽象語法樹,它是以樹狀的形式表現編程語言的語法結構,樹上的每個節點都表示源代碼中的一種結構。

通過操作這棵樹,可以精確的定位到聲明、賦值、運算語句,從而實現對代碼的優化、變更等操作。

本文通過對 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

輸入數據

1
2
3
function square(n) {
  return n * n;
}

處理數據

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 代碼
  }

輸出數據

1
2
3
function square(x) {
  return x * x;
}

我們看一下我們得到的 AST 樹

接下來我們插入一段 把 VUE 組件轉換為微信小程序組件正則版本的處理

二、 簡單粗暴的版本(VUE 組件轉換為微信小程序組件)

沒有使用 AST 將 VUE 組件轉換成小程序組件的簡易版本介紹

下方是兩段代碼,簡單的邏輯,實現思路,匹配目標字符串,替換字符,然后生成文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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個文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//文件
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default {
        //組件的對外屬性
        props: {
          max: {
            type: Number,
            value: 99
          }
        },
        //組件的內部數據
        data(){
          return {
           num:10000
          }
        },
        //組件的方法
        methods: {
          textFn() {
           this .num = 2
         },
          onMyButtonTap: function (){
          this .num = 666
         },
        }
       }

處理后我們得到的微信小程序組件 JavaScript 部分代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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插件):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//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 樹與代碼

1
2
3
4
5
6
7
8
export default { // VUE 組件的慣用寫法用於導出對象模塊
   data(){
   },
   methods:{
   },
   props:{
   }
}

轉換之后的 AST 樹與代碼

1
2
3
4
5
6
7
8
components({ //小程序組件的構造器
   data(){
   },
   methods:{
   },
   props:{
   }
})

通過以上轉換之前和轉換之后代碼和 AST 的對比我們明確了轉換目標就是 ExportDefault 到 Component構造器的轉換,下面看一下我們是如何處理的:

我們做了什么(在轉換中進入到 ExportDefault 中做對應的處理):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//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] 第二種思路呢,就是我們上面展示的這種,不過有一個關鍵的地方要注意一下:

1
2
3
4
5
6
7
8
9
10
//我把 ExportDefaultDeclaration 的處理放到最后來執行,拿到 AST 首先進行轉換。然后在創建得到新的小程序組件JS部分的 AST 即可
traverse(ast, {
    enter(path) {},
    ObjectProperty(path) {},
    ObjectMethod(path) {},
    //......
    ExportDefaultDeclaration(path) {
    //...
    }
})

如果你想在 AST 開始處與結尾處插入,可使用 path 操作:

1
2
3
4
5
6
path.insertBefore(
  t.expressionStatement(t.stringLiteral( "start.." ))
);
path.insertAfter(
  t.expressionStatement(t.stringLiteral( "end.." ))
);

注:關於微信小程序不支持 computed , 與 watch,我們具體的初期采用的方案是掛載 computed 和 watch 方法到每一個微信小程序組件,讓小程序組件也支持這兩個功能。

六,VUE 組件轉換為微信小程序組件中 的 Data 部分的處理:

關於 Data 部分的處理實際上就是:函數表達式轉換為對象表達式 (FunctionExpression 轉換為 ObjectExpression)

轉換之前的 JavaScript 代碼

1
2
3
4
5
6
7
8
9
10
11
12
export default {
   data(){ //函數表達式
    return {
     num: 10000,
     arr: [1, 2, 3],
     obj: {
      d1: "val1" ,
      d2: "val2"
     }
    }
   }
}

處理后我們得到的

1
2
3
4
5
6
7
8
9
10
export default {
  data: { //對象表達式
   num: 10000,
   arr: [1, 2, 3],
   obj: {
    d1: "val1" ,
    d2: "val2"
   }
  }
};

通過如上的代碼對比,我們看到了我們的轉換前后代碼的變化:

轉換前后 AST 樹對比圖明確轉換目標:

我們對 JavaScript 動了什么手腳(亦可封裝成babel插件):

1
2
3
4
5
6
7
8
9
10
11
12
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 樹遍歷的性能消耗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//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樣本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
  }`;

處理后我們得到的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.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代碼的一個簡單的轉換。

1
2
3
4
5
6
7
8
9
10
11
12
13
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 組件轉換為微信小程序組件的方法,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對腳本之家網站的支持!
如果你覺得本文對你有幫助,歡迎轉載,煩請注明出處,謝謝!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM