|
為什么需要 wepy 轉(zhuǎn) VUE“轉(zhuǎn)轉(zhuǎn)二手”是我司用 wepy 開(kāi)發(fā)的功能與 APP 相似度非常高的小程序,實(shí)現(xiàn)了大量的功能性頁(yè)面,而新業(yè)務(wù) H5 項(xiàng)目在開(kāi)發(fā)過(guò)程中有時(shí)也經(jīng)常需要一些公共頁(yè)面和功能,但新項(xiàng)目又有自己的獨(dú)特點(diǎn),這些頁(yè)面需求重新開(kāi)發(fā)成本很高,但如果把小程序代碼轉(zhuǎn)換成 VUE 就會(huì)容易的多,因此需要這樣一個(gè)轉(zhuǎn)換工具。 本文將通過(guò)實(shí)戰(zhàn)帶你體驗(yàn) HTML、css、JavaScript 的 AST 解析和轉(zhuǎn)換過(guò)程 如果你看完覺(jué)得有用,請(qǐng)點(diǎn)個(gè)贊~ AST 概覽AST 全稱(chēng)是叫抽象語(yǔ)法樹(shù),網(wǎng)絡(luò)上有很多對(duì) AST 的概念闡述和 demo,其實(shí)可以跟 XML 類(lèi)比,目前很多流行的語(yǔ)言都可以通過(guò) AST 解析成一顆語(yǔ)法樹(shù),也可以認(rèn)為是一個(gè) JSON,這些語(yǔ)言包括且不限于:CSS、HTML、JavaScript、PHP、Java、SQL 等,舉一個(gè)簡(jiǎn)單的例子: 這句簡(jiǎn)單的 JavaScript 代碼通過(guò) AST 將被解析成一顆“有點(diǎn)復(fù)雜”的語(yǔ)法樹(shù): 
這句話(huà)從語(yǔ)法層面分析是一次變量聲明和賦值,所以父節(jié)點(diǎn)是一個(gè) type 為 VariableDeclaration(變量聲明)的類(lèi)型節(jié)點(diǎn),聲明的內(nèi)容又包括兩部分,標(biāo)識(shí)符:a 和 初始值:1 
這就是一個(gè)簡(jiǎn)單的 AST 轉(zhuǎn)換,你可以通過(guò) astexplorer(https:///)可視化的測(cè)試更多代碼。 AST 有什么用AST 可以將代碼轉(zhuǎn)換成 JSON 語(yǔ)法樹(shù),基于語(yǔ)法樹(shù)可以進(jìn)行代碼轉(zhuǎn)換、替換等很多操作,其實(shí) AST 應(yīng)用非常廣泛,我們開(kāi)發(fā)當(dāng)中使用的 less/sass、eslint、TypeScript 等很多插件都是基于 AST 實(shí)現(xiàn)的。 本文的需求如果用文本替換的方式也可能可以實(shí)現(xiàn),不過(guò)需要用到大量正則,且出錯(cuò)風(fēng)險(xiǎn)很高,如果用 AST 就能輕松完成這件事。 AST 原理AST 處理代碼一版分為以下兩個(gè)步驟: 詞法分析詞法分析會(huì)把你的代碼進(jìn)行大拆分,會(huì)根據(jù)你寫(xiě)的每一個(gè)字符進(jìn)行拆分(會(huì)舍去注釋、空白符等無(wú)用內(nèi)容),然后把有效代碼拆分成一個(gè)個(gè) token。 語(yǔ)法分析接下來(lái) AST 會(huì)根據(jù)特定的“規(guī)則”把這些 token 加以處理和包裝,這些規(guī)則每個(gè)解析器都不同,但做的事情大體相同,包括: 各種 AST 解析器每種語(yǔ)言都有很多解析器,使用方式和生成的結(jié)果各不相同,開(kāi)發(fā)者可以根據(jù)需要選擇合適的解析器。 JavaScript HTML CSS XML wepy 轉(zhuǎn) VUE 工具接下來(lái)我們開(kāi)始實(shí)戰(zhàn)了,這個(gè)需求我們用到的技術(shù)有: node commander:用來(lái)寫(xiě)命令行相關(guān)命令調(diào)用 fs-extra:fs 類(lèi)庫(kù)的升級(jí)版,主要提高了 node 文件操作的便利性,并且提供了 Promise 封裝 XmlParser:解析 XML htmlparser2:解析 HTML less:解析 css(我們所有項(xiàng)目統(tǒng)一都是 less,所以直接解析 less 就可以了) babylon:解析 JavaScript @babel/types:js 的類(lèi)型庫(kù),用于查找、校驗(yàn)、生成相應(yīng)的代碼樹(shù)節(jié)點(diǎn) @babel/traverse:方便對(duì) JavaScript 的語(yǔ)法樹(shù)進(jìn)行各種形式的遍歷 @babel/template:將你處理好的語(yǔ)法樹(shù)打印到一個(gè)固定模板里 @babel/generator:生成處理好的 JavaScript 文本內(nèi)容
轉(zhuǎn)換目標(biāo)我們先看一段簡(jiǎn)單的 wepy 和 VUE 的代碼對(duì)比: //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 ? '取消關(guān)注' : '關(guān)注'}}</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太多了,省略其他內(nèi)容 */
</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() //請(qǐng)求某個(gè)接口
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 ? '取消關(guān)注' : '關(guān)注'}}</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太多了,省略其他內(nèi)容 */
</style>
<script>
export default {
props : {
info:{
type:Object,
default:{}
}
}
data(){
return {
isFollow: false,
}
}
methods : {
async follow() {
await someHttpRequest() //請(qǐng)求某個(gè)接口
this.isFollow = !this.isFollow
}
}
computed : {
sellingCount(){
return this.info.sellingCount || 1
}
}
created() {
this.$log('view')
}
}
</script>
轉(zhuǎn)換代碼實(shí)現(xiàn)我們先寫(xiě)個(gè)讀取文件的入口方法 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 函數(shù)中,我們可以得到代碼的文本內(nèi)容,首先我們將對(duì)其進(jìn)行 XML 解析,把 template、css、JavaScript 拆分成三部分。 有同學(xué)可能問(wèn)為什么不直接正則匹配出來(lái),因?yàn)殚_(kāi)發(fā)者的代碼可能有很多風(fēng)格,比如有兩部分 style,可能有很多意外情況是使用正則考慮不到的,這也是使用 AST 的意義。 //首先需要完成Xml解析及路徑定義:
//初始化一個(gè)Xml解析器
let xmlParser = new XmlParser(),
//解析代碼內(nèi)容
xmlParserObj = xmlParser.parse(fileText),
//正則匹配產(chǎn)生文件名
filenameMatch = filepath.match(/([^\.|\/|\\]+)\.\w+$/),
//如果沒(méi)有名字默認(rèn)為blank
filename = filenameMatch.length > 1 ? filenameMatch[1] : 'blank',
//計(jì)算出模板文件存放目錄dist的絕對(duì)地址
filedir = utils.createDistPath(filepath),
//最終產(chǎn)出文件地址
targetFilePath = `${filedir}/${filename}.vue`
//接下來(lái)創(chuàng)建目標(biāo)目錄
try {
fse.ensureDirSync(filedir)
}catch (e){
throw new Error(e)
}
//最后根據(jù)xml解析出來(lái)的節(jié)點(diǎn)類(lèi)型進(jìn)行不同處理
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)
}
}
不同節(jié)點(diǎn)的處理邏輯,定義在一個(gè)叫做 typesHandler 的對(duì)象里面存放,接下來(lái)我們看下不同類(lèi)型代碼片段的處理邏輯 因篇幅有限,本文只列舉一部分代碼轉(zhuǎn)換的目標(biāo),實(shí)際上要比這些更復(fù)雜 接下來(lái)我們對(duì)代碼進(jìn)行轉(zhuǎn)換: 模板處理轉(zhuǎn)換目標(biāo) 模板標(biāo)簽轉(zhuǎn)換:把 view 轉(zhuǎn)換成 div,把 image 標(biāo)簽轉(zhuǎn)換成 img 模板邏輯判斷:wx:if='{{info.label}}' 轉(zhuǎn)換成 v-if='info.label' 模板循環(huán):wx:for='{{info.label}}' 轉(zhuǎn)換成 v-for='(item,key) in info.label' 事件綁定:@tap='follow' 轉(zhuǎn)換成 @click='follow'
核心流程 let templateContent = v.childNodes.toString(),
//初始化一個(gè)解析器
templateParser = new TemplateParser()
//生成語(yǔ)法樹(shù)
templateParser.parse(templateContent).then((templateAst)=>{
//進(jìn)行上述目標(biāo)的轉(zhuǎn)換
let convertedTemplate = templateConverter(templateAst)
//把語(yǔ)法樹(shù)轉(zhuǎn)成文本
templateConvertedString = templateParser.astToString(convertedTemplate)
templateConvertedString = `<template>\r\n${templateConvertedString}\r\n</template>\r\n`
fs.writeFile(targetFilePath,templateConvertedString, ()=>{
resolve()
});
}).catch((e)=>{
reject(e)
})
const Parser = require('./Parser') //基類(lèi)
const htmlparser = require('htmlparser2') //html的AST類(lèi)庫(kù)
class TemplateParser extends Parser {
constructor(){
super()
}
/**
* HTML文本轉(zhuǎn)AST方法
* @param scriptText
* @returns {Promise}
*/
parse(scriptText){
return new Promise((resolve, reject) => {
//先初始化一個(gè)domHandler
const handler = new htmlparser.DomHandler((error, dom)=>{
if (error) {
reject(error);
} else {
//在回調(diào)里拿到AST對(duì)象
resolve(dom);
}
});
//再初始化一個(gè)解析器
const parser = new htmlparser.Parser(handler);
//再通過(guò)write方法進(jìn)行解析
parser.write(scriptText);
parser.end();
});
}
/**
* AST轉(zhuǎn)文本方法
* @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標(biāo)簽替換規(guī)則,可以添加更多
const tagConverterConfig = {
'view':'div',
'image':'img'
}
//屬性替換規(guī)則,也可以加入更多
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]
//檢測(cè)到是html節(jié)點(diǎn)
if(node.type === 'tag'){
//進(jìn)行標(biāo)簽替換
if(tagConverterConfig[node.name]){
node.name = tagConverterConfig[node.name]
}
//進(jìn)行屬性替換
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
}
//因?yàn)槭菢?shù)狀結(jié)構(gòu),所以需要進(jìn)行遞歸
if(node.children){
templateConverter(node.children)
}
}
return ast
}
css 處理轉(zhuǎn)換目標(biāo) 將 image 替換為 img 將單位 rpx 轉(zhuǎn)換成 *@px
核心過(guò)程 let styleText = utils.deEscape(v.childNodes.toString())
if(v.attributes){
//檢測(cè)css是哪種類(lèi)型
for(let i in v.attributes){
let attr = v.attributes[i]
if(attr.name === 'lang'){
type = attr.value
}
}
}
less.render(styleText).then((output)=>{
//output是css內(nèi)容對(duì)象
})
const CSSOM = require('cssom') //css的AST解析器
const replaceTagClassName = function(replacedStyleText){
const replaceConfig = {}
//匹配標(biāo)簽選擇器
const tagReg = /[^\.|#|\-|_](\b\w+\b)/g
//將css文本轉(zhuǎn)換為語(yǔ)法樹(shù)
const ast = CSSOM.parse(replacedStyleText),
styleRules = ast.cssRules
if(styleRules && styleRules.length){
//找到包含tag的className
styleRules.forEach(function(item){
//可能會(huì)有 view image {...}這多級(jí)選擇器
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方法可以把語(yǔ)法樹(shù)轉(zhuǎn)換為字符串
replacedStyleText = ast.toString()
}
return {replacedStyleText,replaceConfig}
}
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 轉(zhuǎn)換轉(zhuǎn)換目標(biāo) 核心過(guò)程 在了解如何轉(zhuǎn)換之前,我們先簡(jiǎn)單了解下 JavaScript 轉(zhuǎn)換的基本流程: 
借用其他作者一張圖片,可以看出轉(zhuǎn)換過(guò)程分為解析->轉(zhuǎn)換->生成 這三個(gè)步驟。 具體如下: let javascriptContent = utils.deEscape(v.childNodes.toString())
let javascriptParser = new JavascriptParser()
這個(gè)解析器里封裝了什么呢,看代碼: const Parser = require('./Parser') //基類(lèi)
const babylon = require('babylon') //AST解析器
const generate = require('@babel/generator').default
const traverse = require('@babel/traverse').default
class JavascriptParser extends Parser {
constructor(){
super()
}
/**
* 解析前替換掉無(wú)用字符
* @param code
* @returns
*/
beforeParse(code){
return code.replace(/this\.\$apply\(\);?/gm,'').replace(/import\s+wepy\s+from\s+['']wepy['']/gm,'')
}
/**
* 文本內(nèi)容解析成AST
* @param scriptText
* @returns {Promise}
*/
parse(scriptText){
return new Promise((resolve,reject)=>{
try {
const scriptParsed = babylon.parse(scriptText,{
sourceType:'module',
plugins: [
// 'estree', //這個(gè)插件會(huì)導(dǎo)致解析的結(jié)果發(fā)生變化,因此去除,這本來(lái)是acron的插件
'jsx',
'flow',
'doExpressions',
'objectRestSpread',
'exportExtensions',
'classProperties',
'decorators',
'objectRestSpread',
'asyncGenerators',
'functionBind',
'functionSent',
'throwExpressions',
'templateInvalidEscapes'
]
})
resolve(scriptParsed)
}catch (e){
reject(e)
}
})
}
/**
* AST樹(shù)遍歷方法
* @param astObject
* @returns {*}
*/
traverse(astObject){
return traverse(astObject)
}
/**
* 模板或AST對(duì)象轉(zhuǎn)文本方法
* @param astObject
* @param code
* @returns {*}
*/
generate(astObject,code){
const newScript = generate(astObject, {}, code)
return newScript
}
}
module.exports = JavascriptParser
值得注意的是:babylon 的 plugins 配置有很多,如何配置取決于你的代碼里面使用了哪些高級(jí)語(yǔ)法,具體可以參見(jiàn)文檔或者根據(jù)報(bào)錯(cuò)提示處理 javascriptContent = javascriptParser.beforeParse(javascriptContent)
javascriptParser.parse(javascriptContent)
let {convertedJavascript,vistors} = componentConverter(javascriptAst)
componentConverter 是轉(zhuǎn)換的方法封裝,轉(zhuǎn)換過(guò)程略復(fù)雜,我們先了解幾個(gè)概念。 假如我們拿到了 AST 對(duì)象,我們需要先對(duì)他進(jìn)行遍歷,如何遍歷呢,這樣一個(gè)復(fù)雜的 JSON 結(jié)構(gòu)如果我們用循環(huán)或者遞歸的方式去遍歷,那無(wú)疑會(huì)非常復(fù)雜,所以我們就借助了 babel 里的traverse這個(gè)工具,文檔:babel-traverse(https:///docs/en/babel-traverse)。 traverse 接受兩個(gè)參數(shù):AST 對(duì)象和 vistor 對(duì)象 vistor 就是配置遍歷方式的對(duì)象 主要有兩種: 樹(shù)狀遍歷:主要通過(guò)在節(jié)點(diǎn)的進(jìn)入時(shí)機(jī) enter 和離開(kāi) exit 時(shí)機(jī)進(jìn)行遍歷處理,進(jìn)入節(jié)點(diǎn)之后再判斷是什么類(lèi)型的節(jié)點(diǎn)做對(duì)應(yīng)的處理
const componentVistor = {
enter(path) {
if (path.isIdentifier({ name: 'n' })) {
path.node.name = 'x';
}
},
exit(path){
//do sth
}
}
const componentVistor = {
FunctionDeclaration(path) {
path.node.id.name = 'x';
}
}
本文代碼主要使用了樹(shù)狀遍歷的方式,代碼如下: const componentVistor = {
enter(path) {
//判斷如果是類(lèi)屬性
if (t.isClassProperty(path)) {
//根據(jù)不同類(lèi)屬性進(jìn)行不同處理,把wepy的類(lèi)屬性寫(xiě)法提取出來(lái),放到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;
}
}
//判斷如果是類(lèi)方法
if(t.isClassMethod(path)){
if(vistors.lifeCycle.is(path)){
vistors.lifeCycle.handle(path.node)
}else {
vistors.methods.handle(path.node)
}
}
}
}
本文的各種 vistor 主要做一個(gè)事,把各種類(lèi)屬性和方法收集起來(lái),基類(lèi)代碼: class Vistor {
constructor() {
this.data = []
}
handle(path){
this.save(path)
}
save(path){
this.data.push(path)
}
getData(){
return this.data
}
}
module.exports = Vistor
這里還需要補(bǔ)充講下@babel/types這個(gè)類(lèi)庫(kù),它主要是提供了 JavaScript 的 AST 中各種節(jié)點(diǎn)類(lèi)型的檢測(cè)、改造、生成方法,舉例: //類(lèi)型檢測(cè)
if(t.isClassMethod(path)){
//如果是類(lèi)方法
}
//創(chuàng)造一個(gè)對(duì)象節(jié)點(diǎn)
t.objectExpression(...)
通過(guò)上面的處理,我們已經(jīng)把 wepy 里面的各種類(lèi)屬性和方法收集好了,接下來(lái)我們看如何生成 vue 寫(xiě)法的代碼 convertedJavascript = componentTemplateBuilder(convertedJavascript,vistors)
看下 componentTemplateBuilder 這個(gè)方法如何定義: 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這個(gè)類(lèi)庫(kù),主要作用是可以把你的代碼數(shù)據(jù)組裝到一個(gè)新的模板里,模板如下: const componentTemplate = `
export default {
data() {
return DATA
},
props:PROPS,
methods: METHODS,
computed: COMPUTED,
watch:WATCH,
}
`
*生命周期需要進(jìn)行對(duì)應(yīng)關(guān)系處理,略復(fù)雜,本文不贅述 let codeText = `<script>\r\n${generate(convertedJavascript).code}\r\n</script>\r\n`
fs.writeFile(targetFilePath,codeText, ()=>{
resolve()
});
這里用到了@babel/generate類(lèi)庫(kù),主要作用是把 AST 語(yǔ)法樹(shù)生成文本格式 上述過(guò)程的代碼實(shí)現(xiàn)總體流程 const JavascriptParser = require('./lib/parser/JavascriptParser')
//先反轉(zhuǎn)義
let javascriptContent = utils.deEscape(v.childNodes.toString()),
//初始化一個(gè)解析器
javascriptParser = new JavascriptParser()
//去除無(wú)用代碼
javascriptContent = javascriptParser.beforeParse(javascriptContent)
//解析成AST
javascriptParser.parse(javascriptContent).then((javascriptAst)=>{
//進(jìn)行代碼轉(zhuǎn)換
let {convertedJavascript,vistors} = componentConverter(javascriptAst)
//放到預(yù)先定義好的模板中
convertedJavascript = componentTemplateBuilder(convertedJavascript,vistors)
//生成文本并寫(xiě)入到文件
let codeText = `<script>\r\n${generate(convertedJavascript).code}\r\n</script>\r\n`
fs.writeFile(targetFilePath,codeText, ()=>{
resolve()
});
}).catch((e)=>{
reject(e)
})
上面就是 wepy 轉(zhuǎn) VUE 工具的核心代碼實(shí)現(xiàn)流程了 通過(guò)這個(gè)例子希望大家能了解到如何通過(guò) AST 的方式進(jìn)行精準(zhǔn)的代碼處理或者語(yǔ)法轉(zhuǎn)換 如何做成命令行工具既然我們已經(jīng)實(shí)現(xiàn)了這個(gè)轉(zhuǎn)換工具,那接下來(lái)我們希望給開(kāi)發(fā)者提供一個(gè)命令行工具,主要有兩個(gè)部分: 注冊(cè)命令{
'name': '@zz-vc/fancy-cli',
'bin': {
'fancy': 'bin/fancy'
},
//其他配置
}
編寫(xiě)命令調(diào)用代碼#!/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>')
//注冊(cè)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 命令對(duì)應(yīng)的代碼: 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 這塊的代碼最開(kāi)始已經(jīng)講過(guò)了,忘記的同學(xué)可以從頭再看一遍,你就可以整個(gè)串起來(lái)這個(gè)工具的整體實(shí)現(xiàn)邏輯了 結(jié)語(yǔ)至此本文就講完了如何通過(guò) AST 寫(xiě)一個(gè) wepy 轉(zhuǎn) VUE 的命令行工具,希望對(duì)你有所收獲。
|