• [AST实战]从零开始写一个wepy转VUE的工具


    为什么需要 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 将被解析成一颗“有点复杂”的语法树:

    这句话从语法层面分析是一次变量声明和赋值,所以父节点是一个 type 为 VariableDeclaration(变量声明)的类型节点,声明的内容又包括两部分,标识符:a 和 初始值:1

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

    • 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 的对象里面存放,接下来我们看下不同类型代码片段的处理逻辑

    因篇幅有限,本文只列举一部分代码转换的目标,实际上要比这些更复杂

    接下来我们对代码进行转换:

    模板处理

    转换目标

    • 模板标签转换:把 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>
    ${templateConvertedString}
    </template>
    `
      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 = /[^.|#|-|_](w+)/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(/([ds]+)rpx/g,'$1*@px')
    
    • 6、将转换好的代码写入文件
    replacedStyleText = `<style scoped>
    ${replacedStyleText}
    </style>
    `
    
    fs.writeFile(targetFilePath,replacedStyleText,{
      flag: 'a'
    },()=>{
      resolve()
    });
    

    JavaScript 转换

    转换目标

    • 去除 wepy 引用
    • 转换成 vue 的对象写法
    • 去除无用代码:this.$apply()
    • 生命周期对应

    核心过程

    在了解如何转换之前,我们先简单了解下 JavaScript 转换的基本流程:

    借用其他作者一张图片,可以看出转换过程分为解析->转换->生成 这三个步骤。

    具体如下:

    • 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(/imports+wepys+froms+['"]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>
    ${generate(convertedJavascript).code}
    </script>
    `
    
    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>
    ${generate(convertedJavascript).code}
    </script>
    `
    
      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

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

  • 相关阅读:
    小白的基金理财课
    Spring Security 入门原理及实战
    spring-data-rest的魔力 10分钟实现增删改查
    redis单点、redis主从、redis哨兵sentinel,redis集群cluster配置搭建与使用
    Netty开发redis客户端,Netty发送redis命令,netty解析redis消息
    使用Netty实现HTTP服务器
    Netty实现心跳机制
    SpringMVC是怎么工作的,SpringMVC的工作原理
    Netty 学习系列
    Mybatis 源码学习系列
  • 原文地址:https://www.cnblogs.com/zhuanzhuanfe/p/10550502.html
Copyright © 2020-2023  润新知