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

爲何須要 wepy 轉 VUE

「轉轉二手」是我司用 wepy 開發的功能與 APP 類似度很是高的小程序,實現了大量的功能性頁面,而新業務 H5 項目在開發過程當中有時也常常須要一些公共頁面和功能,但新項目又有本身的獨特色,這些頁面需求從新開發成本很高,但若是把小程序代碼轉換成 VUE 就會容易的多,所以須要這樣一個轉換工具。javascript

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

若是你看完以爲有用,請點個贊~html

AST 概覽

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

var a = 1;

這句簡單的 JavaScript 代碼經過 AST 將被解析成一顆「有點複雜」的語法樹:vue

這句話從語法層面分析是一次變量聲明和賦值,因此父節點是一個 type 爲 VariableDeclaration(變量聲明)的類型節點,聲明的內容又包括兩部分,標識符:a 和 初始值:1java

這就是一個簡單的 AST 轉換,你能夠經過 astexplorer可視化的測試更多代碼。node

AST 有什麼用

AST 能夠將代碼轉換成 JSON 語法樹,基於語法樹能夠進行代碼轉換、替換等不少操做,其實 AST 應用很是普遍,咱們開發當中使用的 less/sass、eslint、TypeScript 等不少插件都是基於 AST 實現的。npm

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

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

  • XmlParser

wepy 轉 VUE 工具

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

  • node
  • commander:用來寫命令行相關命令調用
  • fs-extra:fs 類庫的升級版,主要提升了 node 文件操做的便利性,而且提供了 Promise 封裝
  • XmlParser:解析 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)
  }
}

不一樣節點的處理邏輯,定義在一個叫作 typesHandler 的對象裏面存放,接下來咱們看下不一樣類型代碼片斷的處理邏輯

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

接下來咱們對代碼進行轉換:

模板處理

轉換目標

  • 模板標籤轉換:把 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
  • 三、接下來咱們看下具體替換過程:
//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

核心過程

  • 一、咱們要先對拿到的 css 文本代碼進行反轉義處理,由於在解析 xml 過程當中,css 中的特殊符號已經被轉義了,這個處理邏輯很簡單,只是字符串替換邏輯,所以封裝在 utils 工具方法裏,本文不贅述。
let styleText = utils.deEscape(v.childNodes.toString())
  • 二、根據節點屬性中的 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
    }
  }
}
  • 三、less 內容的處理:使用 less.render()方法能夠將 less 轉換成 css;若是是 css,直接對 styleText 進行處理就能夠了
less.render(styleText).then((output)=>{
  //output是css內容對象
})
  • 四、將 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}
}
  • 五、將 rpx 替換爲*@px
replacedStyleText = replacedStyleText.replace(/([\d\s]+)rpx/g,'$1*@px')
  • 六、將轉換好的代碼寫入文件
replacedStyleText = `<style scoped>\r\n${replacedStyleText}\r\n</style>\r\n`

fs.writeFile(targetFilePath,replacedStyleText,{
  flag: 'a'
},()=>{
  resolve()
});

JavaScript 轉換

轉換目標

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

核心過程

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

借用其餘做者一張圖片,能夠看出轉換過程分爲解析->轉換->生成 這三個步驟。

具體以下:

  • 一、先把 xml 節點經過 toString 轉換成文本
v.childNodes.toString()
  • 二、再進行反轉義(不然會報錯的哦)
let javascriptContent = utils.deEscape(v.childNodes.toString())
  • 三、接下來初始化一個解析器
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 配置有不少,如何配置取決於你的代碼裏面使用了哪些高級語法,具體能夠參見文檔或者根據報錯提示處理

  • 四、在解析以前能夠先經過 beforeParse 方法去除掉一些無用代碼(這些代碼一般比較固定,直接經過字符串替換掉更方便)
javascriptContent = javascriptParser.beforeParse(javascriptContent)
  • 五、再把文本解析成 AST
javascriptParser.parse(javascriptContent)
  • 六、經過 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 寫法的代碼

  • 七、把轉換好的 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,

}
`

<u>*生命週期須要進行對應關係處理,略複雜,本文不贅述</u>

  • 八、把模板轉換成文本內容並寫入到文件中
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 的方式進行精準的代碼處理或者語法轉換

如何作成命令行工具

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

註冊命令

  • 一、在項目的 package.json 裏面配置 bin 部分
{
  "name": "@zz-vc/fancy-cli",
  "bin": {
    "fancy": "bin/fancy"
  },
  //其餘配置
}
  • 二、寫好代碼後,npm publish 上去
  • 三、開發者安裝了你的插件後就能夠在命令行以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 的命令行工具,但願對你有所收穫。

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

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

相關文章
相關標籤/搜索