[AST實戰]從零開始寫一個wepy轉VUE的工具

Think.發表於2019-03-15

為什麼需要wepy轉VUE

“轉轉二手”是我司用wepy開發的功能與APP相似度非常高的小程式,實現了大量的功能性頁面,而新業務H5專案在開發過程中有時也經常需要一些公共頁面和功能,但新專案又有自己的獨特點,這些頁面需求重新開發成本很高,但如果把小程式程式碼轉換成VUE就會容易的多,因此需要這樣一個轉換工具。

本文將通過實戰帶你體驗HTML、css、JavaScript的AST解析和轉換過程

如果你看完覺得有用,請點個贊~

AST概覽

AST全稱是叫抽象語法樹,網路上有很多對AST的概念闡述和demo,其實可以跟XML類比,目前很多流行的語言都可以通過AST解析成一顆語法樹,也可以認為是一個JSON,這些語言包括且不限於:CSS、HTML、JavaScript、PHP、Java、SQL等,舉一個簡單的例子:

var a = 1;
複製程式碼

這句簡單的JavaScript程式碼通過AST將被解析成一顆“有點複雜”的語法樹:

[AST實戰]從零開始寫一個wepy轉VUE的工具

這句話從語法層面分析是一次變數宣告和賦值,所以父節點是一個type為VariableDeclaration(變數宣告)的型別節點,宣告的內容又包括兩部分,識別符號:a 和 初始值:1

[AST實戰]從零開始寫一個wepy轉VUE的工具

這就是一個簡單的AST轉換,你可以通過 astexplorer視覺化的測試更多程式碼。

AST有什麼用

AST可以將程式碼轉換成JSON語法樹,基於語法樹可以進行程式碼轉換、替換等很多操作,其實AST應用非常廣泛,我們開發當中使用的less/sass、eslint、TypeScript等很多外掛都是基於AST實現的。

本文的需求如果用文字替換的方式也可能可以實現,不過需要用到大量正則,且出錯風險很高,如果用AST就能輕鬆完成這件事。

AST原理

AST處理程式碼一版分為以下兩個步驟:

詞法分析

詞法分析會把你的程式碼進行大拆分,會根據你寫的每一個字元進行拆分(會捨去註釋、空白符等無用內容),然後把有效程式碼拆分成一個個token。

語法分析

接下來AST會根據特定的“規則”把這些token加以處理和包裝,這些規則每個解析器都不同,但做的事情大體相同,包括:

  • 把每個token對應到解析器內建的語法規則中,比如上文提到的var a = 1;這段程式碼將被解析成VariableDeclaration型別。
  • 根據程式碼本身的語法結構,將tokens組裝成樹狀結構。

各種AST解析器

每種語言都有很多解析器,使用方式和生成的結果各不相同,開發者可以根據需要選擇合適的解析器。

JavaScript

  • 最知名的當屬babylon,因為他是babel的御用解析器,一般JavaScript的AST這個庫比較常用
  • acron:babylon就是從這個庫fork來的

HTML

  • htmlparser2:比較常用
  • parse5:不太好用,還需要配合jsdom這個類庫

CSS

  • cssom、csstree等
  • less/sass

XML

  • Xmldom

wepy轉VUE工具

接下來我們開始實戰了,這個需求我們用到的技術有:

  • node
  • commander:用來寫命令列相關命令呼叫
  • fs-extra:fs類庫的升級版,主要提高了node檔案操作的便利性,並且提供了Promise封裝
  • Xmldom:解析XML
  • htmlparser2:解析HTML
  • less:解析css(我們所有專案統一都是less,所以直接解析less就可以了)
  • babylon:解析JavaScript
  • @babel/types:js的型別庫,用於查詢、校驗、生成相應的程式碼樹節點
  • @babel/traverse:方便對JavaScript的語法樹進行各種形式的遍歷
  • @babel/template:將你處理好的語法樹列印到一個固定模板裡
  • @babel/generator:生成處理好的JavaScript文字內容

轉換目標

我們先看一段簡單的wepy和VUE的程式碼對比:

//wepy版
<template>
  <view class="userCard">
    <view class="basic">
      <view class="avatar">
        <image src="{{info.portrait}}"></image>
      </view>
      <view class="info">
        <view class="name">{{info.nickName}}</view>
        <view class="label" wx:if="{{info.label}}">
          <view class="label-text" wx:for="{{info.label}}">{{item}}</view>
        </view>
        <view class="onsale">在售寶貝{{sellingCount}}</view>
        <view class="follow " @tap="follow">{{isFollow ? '取消關注' : '關注'}}</view>
      </view>
    </view>
  </view>
</template>
<style lang="less" rel="stylesheet/less" scoped>
.userCard {
    position:relative;
    background: #FFFFFF;
    box-shadow: 0 0 10rpx 0 rgba(162,167,182,0.31);
    border-radius: 3rpx;
    padding:20rpx;
    position: relative;
}
/* css太多了,省略其他內容 */
</style>
<script>
  import wepy from 'wepy'
  export default class UserCard extends wepy.component {
      props = {
        info:{
          type:Object,
          default:{}
        }
      }
      data = {
          isFollow: false,
      }
      methods = {
        async follow() {
          await someHttpRequest()  //請求某個介面
          this.isFollow = !this.isFollow
          this.$apply()
        }
      }
      computed = {
        sellingCount(){
            return this.info.sellingCount || 1
        }
      }
      onLoad(){
        this.$log('view')
      }
  }
</script>
複製程式碼
//VUE版
<template>
  <div class="userCard">
    <div class="basic">
      <div class="avatar">
        <img src="info.portrait"></img>
      </view>
      <view class="info">
        <view class="name">{{info.nickName}}</view>
        <view class="label" v-if="info.label">
          <view class="label-text" v-for="(item,key) in info.label">{{item}}</view>
        </view>
        <view class="onsale">在售寶貝{{sellingCount}}</view>
        <view class="follow " @click="follow">{{isFollow ? '取消關注' : '關注'}}</view>
      </view>
    </view>
  </view>
</template>
<style lang="less" rel="stylesheet/less" scoped>
.userCard {
    position:relative;
    background: #FFFFFF;
    box-shadow: 0 0 10rpx 0 rgba(162,167,182,0.31);
    border-radius: 3*@px;
    padding:20*@px;
    position: relative;
}
/* css太多了,省略其他內容 */
</style>
<script>
  export default {
      props : {
        info:{
          type:Object,
          default:{}
        }
      }
      data(){
          return {
            isFollow: false,
          }
      }
      
      methods : {
        async follow() {
          await someHttpRequest()  //請求某個介面
          this.isFollow = !this.isFollow
        }
      }
      computed : {
        sellingCount(){
            return this.info.sellingCount || 1
        }
      }
      created() {
        this.$log('view')
      }
  }
</script>
複製程式碼

轉換程式碼實現

我們先寫個讀取檔案的入口方法

const cwdPath = process.cwd()
const fse = require('fs-extra')

const convert = async function(filepath){
	let fileText = await fse.readFile(filepath, 'utf-8');
	fileHandle(fileText.toString(),filepath)
}
const fileHandle = async function(fileText,filepath){
    //dosth...
}
convert(`${cwdPath}/demo.wpy`)
複製程式碼

在fileHandle函式中,我們可以得到程式碼的文字內容,首先我們將對其進行XML解析,把template、css、JavaScript拆分成三部分。 有同學可能問為什麼不直接正則匹配出來,因為開發者的程式碼可能有很多風格,比如有兩部分style,可能有很多意外情況是使用正則考慮不到的,這也是使用AST的意義。

        //首先需要完成Xml解析及路徑定義:
        //初始化一個Xml解析器
        let xmlParser = new XmlParser(),  
            //解析程式碼內容
	    xmlParserObj = xmlParser.parse(fileText),    
	    //正則匹配產生檔名
	    filenameMatch = filepath.match(/([^\.|\/|\\]+)\.\w+$/),     
	    //如果沒有名字預設為blank
	    filename = filenameMatch.length > 1 ? filenameMatch[1] : 'blank', 
	    //計算出模板檔案存放目錄dist的絕對地址
	    filedir = utils.createDistPath(filepath),      
	    //最終產出檔案地址
	    targetFilePath = `${filedir}/${filename}.vue`
	
        //接下來建立目標目錄
         try {
            fse.ensureDirSync(filedir)
         }catch (e){
            throw new Error(e)
         }

        //最後根據xml解析出來的節點型別進行不同處理
        for(let i = 0 ;i < xmlParserObj.childNodes.length;i++){
            let v = xmlParserObj.childNodes[i]
            if(v.nodeName === 'style'){
                typesHandler.style(v,filedir,filename,targetFilePath)
            }
            if(v.nodeName === 'template'){
            	typesHandler.template(v,filedir,filename,targetFilePath)
            }
            if(v.nodeName === 'script'){
            	typesHandler.script(v,filedir,filename,targetFilePath)
            }
	}
複製程式碼
//XmlParser定義
const Xmldom = require('xmldom')
const utils = require('../utils')
class XmlParser extends Parser {
  constructor(){
    super()
  }
  createParser(){
    return new Xmldom.DOMParser({errorHandler: {
      warning (x) {
        if (x.indexOf('missed value!!') > -1) {
          // ignore warnings
        } else
          console.warn(x);
      },
      error (x) {
        console.error(x);
      }
    }});
  }
  parse(fileText){
    fileText = utils.replaceTagAndEventBind(fileText)
    return this.createParser().parseFromString(fileText);
  }
}

複製程式碼

不同節點的處理邏輯,定義在一個叫做typesHandler的物件裡面存放,接下來我們看下不同型別程式碼片段的處理邏輯

因篇幅有限,本文只列舉一部分程式碼轉換的目標,實際上要比這些更復雜

接下來我們對程式碼進行轉換:

模板處理

轉換目標

  • 模板標籤轉換:把view轉換成div,把image標籤轉換成img
  • 模板邏輯判斷:wx:if="{{info.label}}" 轉換成 v-if="info.label"
  • 模板迴圈:wx:for="{{info.label}}" 轉換成v-for="(item,key) in info.label"
  • 事件繫結:@tap="follow" 轉換成 @click="follow"

核心流程

  • 首先把拿到的目標文字解析成語法樹,然後進行各項轉換,最後把語法樹轉換成文字寫入到檔案
let templateContent = v.childNodes.toString(),
    //初始化一個解析器
    templateParser = new TemplateParser()   
    
//生成語法樹
templateParser.parse(templateContent).then((templateAst)=>{
    //進行上述目標的轉換
    let convertedTemplate = templateConverter(templateAst)  
    //把語法樹轉成文字
    templateConvertedString = templateParser.astToString(convertedTemplate) 
    
    templateConvertedString = `<template>\r\n${templateConvertedString}\r\n</template>\r\n`
    fs.writeFile(targetFilePath,templateConvertedString, ()=>{
    	resolve()
    });
}).catch((e)=>{
	reject(e)
})
複製程式碼
  • TemplateParser是我封裝的一個簡單的模板AST處理類庫,(因為使用了htmlparser2類庫,該類庫的呼叫方式有點麻煩),我們看下程式碼:
const Parser = require('./Parser') //基類
const htmlparser = require('htmlparser2')   //html的AST類庫
class TemplateParser extends Parser {
  constructor(){
    super()
  }
  /**
   * HTML文字轉AST方法
   * @param scriptText
   * @returns {Promise}
   */
  parse(scriptText){
    return new Promise((resolve, reject) => {
      //先初始化一個domHandler
      const handler = new htmlparser.DomHandler((error, dom)=>{
        if (error) {
          reject(error);
        } else {
          //在回撥裡拿到AST物件  
          resolve(dom);
        }
      });
      //再初始化一個解析器
      const parser = new htmlparser.Parser(handler);
      //再通過write方法進行解析
      parser.write(scriptText);
      parser.end();
    });
  }
  /**
   * AST轉文字方法
   * @param ast
   * @returns {string}
   */
  astToString (ast) {
    let str = '';
    ast.forEach(item => {
      if (item.type === 'text') {
        str += item.data;
      } else if (item.type === 'tag') {
        str += '<' + item.name;
        if (item.attribs) {
          Object.keys(item.attribs).forEach(attr => {
            str += ` ${attr}="${item.attribs[attr]}"`;
          });
        }
        str += '>';
        if (item.children && item.children.length) {
          str += this.astToString(item.children);
        }
        str += `</${item.name}>`;
      }
    });
    return str;
  }
}

module.exports = TemplateParser

複製程式碼
  • 3、接下來我們看下具體替換過程:
//html標籤替換規則,可以新增更多
const tagConverterConfig = {
  'view':'div',
  'image':'img'
}
//屬性替換規則,也可以加入更多
const attrConverterConfig = {
  'wx:for':{
    key:'v-for',
    value:(str)=>{
      return str.replace(/{{(.*)}}/,'(item,key) in $1')
    }
  },
  'wx:if':{
    key:'v-if',
    value:(str)=>{
      return str.replace(/{{(.*)}}/,'$1')
    }
  },
  '@tap':{
    key:'@click'
  },
}
//替換入口方法
const templateConverter = function(ast){
  for(let i = 0;i<ast.length;i++){
    let node = ast[i]
    //檢測到是html節點
    if(node.type === 'tag'){
      //進行標籤替換  
      if(tagConverterConfig[node.name]){
        node.name = tagConverterConfig[node.name]
      }
      //進行屬性替換
      let attrs = {}
      for(let k in node.attribs){
        let target = attrConverterConfig[k]
        if(target){
          //分別替換屬性名和屬性值
          attrs[target['key']] = target['value'] ?
                                  target['value'](node.attribs[k]) : 
                                  node.attribs[k]
        }else {
          attrs[k] = node.attribs[k]
        }
      }
      node.attribs = attrs
    }
    //因為是樹狀結構,所以需要進行遞迴
    if(node.children){
      templateConverter(node.children)
    }
  }
  return ast
}
複製程式碼

css處理

轉換目標

  • 將image替換為img
  • 將單位 rpx 轉換成 *@px

核心過程

  • 1、我們要先對拿到的css文字程式碼進行反轉義處理,因為在解析xml過程中,css中的特殊符號已經被轉義了,這個處理邏輯很簡單,只是字串替換邏輯,因此封裝在utils工具方法裡,本文不贅述。
let styleText = utils.deEscape(v.childNodes.toString())
複製程式碼
  • 2、根據節點屬性中的type來判斷是less還是普通css
if(v.attributes){
        //檢測css是哪種型別
	for(let i in v.attributes){
		let attr = v.attributes[i]
		if(attr.name === 'lang'){
			type = attr.value
		}
	}
}
複製程式碼
  • 3、less內容的處理:使用less.render()方法可以將less轉換成css;如果是css,直接對styleText進行處理就可以了
less.render(styleText).then((output)=>{
    //output是css內容物件
})
複製程式碼
  • 4、將image選擇器換成img,這裡也需要替換更多標籤,比如text、icon、scroll-view等,篇幅原因不贅述
const CSSOM = require('cssom')  //css的AST解析器
const replaceTagClassName = function(replacedStyleText){
        const replaceConfig = {}
        //匹配標籤選擇器
        const tagReg = /[^\.|#|\-|_](\b\w+\b)/g 
        //將css文字轉換為語法樹
        const ast = CSSOM.parse(replacedStyleText), 
              styleRules = ast.cssRules

	if(styleRules && styleRules.length){
		//找到包含tag的className
	    styleRules.forEach(function(item){
		//可能會有 view image {...}這多級選擇器
        	let tags = item.selectorText.match(tagReg) 
        	if(tags && tags.length){
        		let newName = ''
    			tags = tags.map((tag)=>{
    				tag = tag.trim()
    				if(tag === 'image')tag = 'img'
    				return tag
    			})
    			item.selectorText = tags.join(' ')
        	}
	    })
	    //使用toString方法可以把語法樹轉換為字串
	    replacedStyleText = ast.toString()  
	}
	return {replacedStyleText,replaceConfig}
}
複製程式碼
  • 5、將rpx替換為*@px
replacedStyleText = replacedStyleText.replace(/([\d\s]+)rpx/g,'$1*@px')
複製程式碼
  • 6、將轉換好的程式碼寫入檔案
replacedStyleText = `<style scoped>\r\n${replacedStyleText}\r\n</style>\r\n`
    
fs.writeFile(targetFilePath,replacedStyleText,{
	flag: 'a'
},()=>{
	resolve()
});
複製程式碼

JavaScript轉換

轉換目標

  • 去除wepy引用
  • 轉換成vue的物件寫法
  • 去除無用程式碼:this.$apply()
  • 生命週期對應

核心過程

在瞭解如何轉換之前,我們先簡單瞭解下JavaScript轉換的基本流程:

[AST實戰]從零開始寫一個wepy轉VUE的工具

借用其他作者一張圖片,可以看出轉換過程分為解析->轉換->生成 這三個步驟。

具體如下:

  • 1、先把xml節點通過toString轉換成文字
v.childNodes.toString()
複製程式碼
  • 2、再進行反轉義(否則會報錯的哦)
let javascriptContent = utils.deEscape(v.childNodes.toString())
複製程式碼
  • 3、接下來初始化一個解析器
let javascriptParser = new JavascriptParser()
複製程式碼

這個解析器裡封裝了什麼呢,看程式碼:

const Parser = require('./Parser')  //基類
const babylon = require('babylon')  //AST解析器
const generate = require('@babel/generator').default
const traverse = require('@babel/traverse').default

class JavascriptParser extends Parser {
  constructor(){
    super()
  }
  /**
   * 解析前替換掉無用字元
   * @param code
   * @returns
   */
  beforeParse(code){
    return code.replace(/this\.\$apply\(\);?/gm,'').replace(/import\s+wepy\s+from\s+['"]wepy['"]/gm,'')
  }
  /**
   * 文字內容解析成AST
   * @param scriptText
   * @returns {Promise}
   */
  parse(scriptText){
    return new Promise((resolve,reject)=>{
      try {
        const scriptParsed = babylon.parse(scriptText,{
          sourceType:'module',
          plugins: [
            // "estree", //這個外掛會導致解析的結果發生變化,因此去除,這本來是acron的外掛
            "jsx",
            "flow",
            "doExpressions",
            "objectRestSpread",
            "exportExtensions",
            "classProperties",
            "decorators",
            "objectRestSpread",
            "asyncGenerators",
            "functionBind",
            "functionSent",
            "throwExpressions",
            "templateInvalidEscapes"
          ]
        })
        resolve(scriptParsed)
      }catch (e){
        reject(e)
      }
    })
  }

  /**
   * AST樹遍歷方法
   * @param astObject
   * @returns {*}
   */
  traverse(astObject){
    return traverse(astObject)
  }

  /**
   * 模板或AST物件轉文字方法
   * @param astObject
   * @param code
   * @returns {*}
   */
  generate(astObject,code){
    const newScript = generate(astObject, {}, code)
    return newScript
  }
}
module.exports = JavascriptParser

複製程式碼

值得注意的是:babylon的plugins配置有很多,如何配置取決於你的程式碼裡面使用了哪些高階語法,具體可以參見文件或者根據報錯提示處理

  • 4、在解析之前可以先通過beforeParse方法去除掉一些無用程式碼(這些程式碼通常比較固定,直接通過字串替換掉更方便)
javascriptContent = javascriptParser.beforeParse(javascriptContent)
複製程式碼
  • 5、再把文字解析成AST
javascriptParser.parse(javascriptContent)
複製程式碼
  • 6、通過AST遍歷整個樹,進行各種程式碼轉換
let {convertedJavascript,vistors} = componentConverter(javascriptAst)
複製程式碼

componentConverter是轉換的方法封裝,轉換過程略複雜,我們先了解幾個概念。

假如我們拿到了AST物件,我們需要先對他進行遍歷,如何遍歷呢,這樣一個複雜的JSON結構如果我們用迴圈或者遞迴的方式去遍歷,那無疑會非常複雜,所以我們就藉助了babel裡的traverse這個工具,文件:babel-traverse

  • traverse接受兩個引數:AST物件和vistor物件

  • vistor就是配置遍歷方式的物件

  • 主要有兩種:

    • 樹狀遍歷:主要通過在節點的進入時機enter和離開exit時機進行遍歷處理,進入節點之後再判斷是什麼型別的節點做對應的處理
const componentVistor = {
  enter(path) {
    if (path.isIdentifier({ name: "n" })) {
      path.node.name = "x";
    }
  },
  exit(path){
      //do sth
  }
}
複製程式碼
  • 按型別遍歷:traverse幫你找到對應型別的所有節點
const componentVistor = {
    FunctionDeclaration(path) {
        path.node.id.name = "x";
    }
}
複製程式碼

本文程式碼主要使用了樹狀遍歷的方式,程式碼如下:

const componentVistor = {
  enter(path) {
    //判斷如果是類屬性
    if (t.isClassProperty(path)) {
      //根據不同類屬性進行不同處理,把wepy的類屬性寫法提取出來,放到VUE模板中
      switch (path.node.key.name){
        case 'props':
          vistors.props.handle(path.node.value)
          break;
        case 'data':
          vistors.data.handle(path.node.value)
          break;
        case 'events':
          vistors.events.handle(path.node.value)
          break;
        case 'computed':
          vistors.computed.handle(path.node.value)
          break;
        case 'components':
          vistors.components.handle(path.node.value)
          break;
        case 'watch':
          vistors.watch.handle(path.node.value)
          break;
        case 'methods':
          vistors.methods.handle(path.node.value)
          break;
        default:
          console.info(path.node.key.name)
          break;
      }
    }
    //判斷如果是類方法
    if(t.isClassMethod(path)){
      if(vistors.lifeCycle.is(path)){
        vistors.lifeCycle.handle(path.node)
      }else {
        vistors.methods.handle(path.node)
      }
    }
  }
}
複製程式碼

本文的各種vistor主要做一個事,把各種類屬性和方法收集起來,基類程式碼:

class Vistor {
  constructor() {
    this.data = []
  }
  handle(path){
    this.save(path)
  }
  save(path){
    this.data.push(path)
  }
  getData(){
    return this.data
  }
}
module.exports = Vistor

複製程式碼

這裡還需要補充講下@babel/types這個類庫,它主要是提供了JavaScript的AST中各種節點型別的檢測、改造、生成方法,舉例:

//型別檢測
if(t.isClassMethod(path)){
    //如果是類方法
}
//創造一個物件節點
t.objectExpression(...)
複製程式碼

通過上面的處理,我們已經把wepy裡面的各種類屬性和方法收集好了,接下來我們看如何生成vue寫法的程式碼

  • 7、把轉換好的AST樹放到預先定義好的template模板中
convertedJavascript = componentTemplateBuilder(convertedJavascript,vistors)
複製程式碼

看下componentTemplateBuilder這個方法如何定義:

const componentTemplateBuilder = function(ast,vistors){
  const buildRequire = template(componentTemplate);
  ast = buildRequire({
    PROPS: arrayToObject(vistors.props.getData()),
    LIFECYCLE: arrayToObject(vistors.lifeCycle.getData()),
    DATA: arrayToObject(vistors.data.getData()),
    METHODS: arrayToObject(vistors.methods.getData()),
    COMPUTED: arrayToObject(vistors.computed.getData()),
    WATCH: arrayToObject(vistors.watch.getData()),
  });
  return ast
}
複製程式碼

這裡就用到了@babel/template這個類庫,主要作用是可以把你的程式碼資料組裝到一個新的模板裡,模板如下:

const componentTemplate = `
export default {
  data() {
    return DATA
  },
  
  props:PROPS,
  
  methods: METHODS,
  
  computed: COMPUTED,
  
  watch:WATCH,
  
}
`
複製程式碼

*生命週期需要進行對應關係處理,略複雜,本文不贅述

  • 8、把模板轉換成文字內容並寫入到檔案中
        let codeText =  `<script>\r\n${generate(convertedJavascript).code}\r\n</script>\r\n`
        
        fs.writeFile(targetFilePath,codeText, ()=>{
		resolve()
	});
複製程式碼

這裡用到了@babel/generate類庫,主要作用是把AST語法樹生成文字格式

上述過程的程式碼實現總體流程

const JavascriptParser = require('./lib/parser/JavascriptParser')  

//先反轉義
let javascriptContent = utils.deEscape(v.childNodes.toString()),   
    //初始化一個解析器
    javascriptParser = new JavascriptParser()   
 
//去除無用程式碼   
javascriptContent = javascriptParser.beforeParse(javascriptContent) 
//解析成AST
javascriptParser.parse(javascriptContent).then((javascriptAst)=>{   
	//進行程式碼轉換
	let {convertedJavascript,vistors} = componentConverter(javascriptAst)  
	//放到預先定義好的模板中
	convertedJavascript = componentTemplateBuilder(convertedJavascript,vistors)
	
        //生成文字並寫入到檔案
        let codeText =  `<script>\r\n${generate(convertedJavascript).code}\r\n</script>\r\n`
    
	fs.writeFile(targetFilePath,codeText, ()=>{  
		resolve()
	});
}).catch((e)=>{
	reject(e)
})
複製程式碼

上面就是wepy轉VUE工具的核心程式碼實現流程了

通過這個例子希望大家能瞭解到如何通過AST的方式進行精準的程式碼處理或者語法轉換

如何做成命令列工具

既然我們已經實現了這個轉換工具,那接下來我們希望給開發者提供一個命令列工具,主要有兩個部分:

註冊命令

  • 1、在專案的package.json裡面配置bin部分
{
  "name": "@zz-vc/fancy-cli",
  "bin": {
    "fancy": "bin/fancy"
  },
  //其他配置
}
複製程式碼
  • 2、寫好程式碼後,npm publish上去
  • 3、開發者安裝了你的外掛後就可以在命令列以fancy xxxx的形式直接呼叫命令了

編寫命令呼叫程式碼

#!/usr/bin/env node

process.env.NODE_PATH = __dirname + '/../node_modules/'

const { resolve } = require('path')

const res = command => resolve(__dirname, './commands/', command)

const program = require('commander')

program
  .version(require('../package').version )

program
  .usage('<command>')

//註冊convert命令
program
  .command('convert <componentName>')
  .description('convert a component,eg: fancy convert Tab.vue')
  .alias('c')
  .action((componentName) => {
    let fn = require(res('convert'))
    fn(componentName)
  })


program.parse(process.argv)

if(!program.args.length){
  program.help()
}
複製程式碼

convert命令對應的程式碼:

const cwdPath = process.cwd()
const convert = async function(filepath){
	let fileText = await fse.readFile(filepath, 'utf-8');
	fileHandle(fileText.toString(),filepath)
}

module.exports = function(fileName){
	convert(`${cwdPath}/${fileName}`)
}

複製程式碼

fileHandle這塊的程式碼最開始已經講過了,忘記的同學可以從頭再看一遍,你就可以整個串起來這個工具的整體實現邏輯了

結語

至此本文就講完了如何通過AST寫一個wepy轉VUE的命令列工具,希望對你有所收穫。

最重要的事我司 轉轉 正在招聘前端高階開發工程師數名,有興趣來轉轉跟我一起搞事情的,請發簡歷到zhangsuoyong@zhuanzhuan.com

轉載請註明來源及作者:張所勇@轉轉

相關文章