[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 和 初始值:1 java

这就是一个简单的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

  • 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

复制代码
  • 三、接下来咱们看下具体替换过程:
//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,
  
}
`
复制代码

*生命周期须要进行对应关系处理,略复杂,本文不赘述

  • 八、把模板转换成文本内容并写入到文件中
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的命令行工具,但愿对你有所收获。

最重要的事我司 转转 正在招聘前端高级开发工程师数名,有兴趣来转转跟我一块儿搞事情的,请发简历到zhangsuoyong@zhuanzhuan.com

转载请注明来源及做者:张所勇@转转

相关文章
相关标签/搜索