小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

從零開(kāi)始寫(xiě)一個(gè) wepy 轉(zhuǎn) Vue 的工具

 ws8637 2019-03-24


為什么需要 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)單的例子:

  1. var a = 1;

這句簡(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è)解析器都不同,但做的事情大體相同,包括:

  • 把每個(gè) token 對(duì)應(yīng)到解析器內(nèi)置的語(yǔ)法規(guī)則中,比如上文提到的 var a = 1;這段代碼將被解析成 VariableDeclaration 類(lèi)型。

  • 根據(jù)代碼本身的語(yǔ)法結(jié)構(gòu),將 tokens 組裝成樹(shù)狀結(jié)構(gòu)。

各種 AST 解析器

每種語(yǔ)言都有很多解析器,使用方式和生成的結(jié)果各不相同,開(kāi)發(fā)者可以根據(jù)需要選擇合適的解析器。

JavaScript

  • 最知名的當(dāng)屬 babylon,因?yàn)樗?babel 的御用解析器,一般 JavaScript 的 AST 這個(gè)庫(kù)比較常用

  • acron:babylon 就是從這個(gè)庫(kù) fork 來(lái)的

HTML

  • htmlparser2:比較常用

  • parse5:不太好用,還需要配合 jsdom 這個(gè)類(lèi)庫(kù)

CSS

  • cssom、csstree 等

  • less/sass

XML

  • XmlParser

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ì)比:

  1. //wepy版

  2. <template>

  3. <view class='userCard'>

  4. <view class='basic'>

  5. <view class='avatar'>

  6. <image src='{{info.portrait}}'></image>

  7. </view>

  8. <view class='info'>

  9. <view class='name'>{{info.nickName}}</view>

  10. <view class='label' wx:if='{{info.label}}'>

  11. <view class='label-text' wx:for='{{info.label}}'>{{item}}</view>

  12. </view>

  13. <view class='onsale'>在售寶貝{{sellingCount}}</view>

  14. <view class='follow ' @tap='follow'>{{isFollow ? '取消關(guān)注' : '關(guān)注'}}</view>

  15. </view>

  16. </view>

  17. </view>

  18. </template>

  19. <style lang='less' rel='stylesheet/less' scoped>

  20. .userCard {

  21. position:relative;

  22. background: #FFFFFF;

  23. box-shadow: 0 0 10rpx 0 rgba(162,167,182,0.31);

  24. border-radius: 3rpx;

  25. padding:20rpx;

  26. position: relative;

  27. }

  28. /* css太多了,省略其他內(nèi)容 */

  29. </style>

  30. <script>

  31. import wepy from 'wepy'

  32. export default class UserCard extends wepy.component {

  33. props = {

  34. info:{

  35. type:Object,

  36. default:{}

  37. }

  38. }

  39. data = {

  40. isFollow: false,

  41. }

  42. methods = {

  43. async follow() {

  44. await someHttpRequest() //請(qǐng)求某個(gè)接口

  45. this.isFollow = !this.isFollow

  46. this.$apply()

  47. }

  48. }

  49. computed = {

  50. sellingCount(){

  51. return this.info.sellingCount || 1

  52. }

  53. }

  54. onLoad(){

  55. this.$log('view')

  56. }

  57. }

  58. </script>

  1. //VUE版

  2. <template>

  3. <div class='userCard'>

  4. <div class='basic'>

  5. <div class='avatar'>

  6. <img src='info.portrait'></img>

  7. </view>

  8. <view class='info'>

  9. <view class='name'>{{info.nickName}}</view>

  10. <view class='label' v-if='info.label'>

  11. <view class='label-text' v-for='(item,key) in info.label'>{{item}}</view>

  12. </view>

  13. <view class='onsale'>在售寶貝{{sellingCount}}</view>

  14. <view class='follow ' @click='follow'>{{isFollow ? '取消關(guān)注' : '關(guān)注'}}</view>

  15. </view>

  16. </view>

  17. </view>

  18. </template>

  19. <style lang='less' rel='stylesheet/less' scoped>

  20. .userCard {

  21. position:relative;

  22. background: #FFFFFF;

  23. box-shadow: 0 0 10rpx 0 rgba(162,167,182,0.31);

  24. border-radius: 3*@px;

  25. padding:20*@px;

  26. position: relative;

  27. }

  28. /* css太多了,省略其他內(nèi)容 */

  29. </style>

  30. <script>

  31. export default {

  32. props : {

  33. info:{

  34. type:Object,

  35. default:{}

  36. }

  37. }

  38. data(){

  39. return {

  40. isFollow: false,

  41. }

  42. }


  43. methods : {

  44. async follow() {

  45. await someHttpRequest() //請(qǐng)求某個(gè)接口

  46. this.isFollow = !this.isFollow

  47. }

  48. }

  49. computed : {

  50. sellingCount(){

  51. return this.info.sellingCount || 1

  52. }

  53. }

  54. created() {

  55. this.$log('view')

  56. }

  57. }

  58. </script>

轉(zhuǎn)換代碼實(shí)現(xiàn)

我們先寫(xiě)個(gè)讀取文件的入口方法

  1. const cwdPath = process.cwd()

  2. const fse = require('fs-extra')


  3. const convert = async function(filepath){

  4. let fileText = await fse.readFile(filepath, 'utf-8');

  5. fileHandle(fileText.toString(),filepath)

  6. }

  7. const fileHandle = async function(fileText,filepath){

  8. //dosth...

  9. }

  10. 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 的意義。

  1. //首先需要完成Xml解析及路徑定義:


  2. //初始化一個(gè)Xml解析器

  3. let xmlParser = new XmlParser(),

  4. //解析代碼內(nèi)容

  5. xmlParserObj = xmlParser.parse(fileText),

  6. //正則匹配產(chǎn)生文件名

  7. filenameMatch = filepath.match(/([^\.|\/|\\]+)\.\w+$/),

  8. //如果沒(méi)有名字默認(rèn)為blank

  9. filename = filenameMatch.length > 1 ? filenameMatch[1] : 'blank',

  10. //計(jì)算出模板文件存放目錄dist的絕對(duì)地址

  11. filedir = utils.createDistPath(filepath),

  12. //最終產(chǎn)出文件地址

  13. targetFilePath = `${filedir}/${filename}.vue`


  14. //接下來(lái)創(chuàng)建目標(biāo)目錄

  15. try {

  16. fse.ensureDirSync(filedir)

  17. }catch (e){

  18. throw new Error(e)

  19. }


  20. //最后根據(jù)xml解析出來(lái)的節(jié)點(diǎn)類(lèi)型進(jìn)行不同處理

  21. for(let i = 0 ;i < xmlParserObj.childNodes.length;i++){

  22. let v = xmlParserObj.childNodes[i]

  23. if(v.nodeName === 'style'){

  24. typesHandler.style(v,filedir,filename,targetFilePath)

  25. }

  26. if(v.nodeName === 'template'){

  27. typesHandler.template(v,filedir,filename,targetFilePath)

  28. }

  29. if(v.nodeName === 'script'){

  30. typesHandler.script(v,filedir,filename,targetFilePath)

  31. }

  32. }

不同節(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'

核心流程

  • 首先把拿到的目標(biāo)文本解析成語(yǔ)法樹(shù),然后進(jìn)行各項(xiàng)轉(zhuǎn)換,最后把語(yǔ)法樹(shù)轉(zhuǎn)換成文本寫(xiě)入到文件

  1. let templateContent = v.childNodes.toString(),

  2. //初始化一個(gè)解析器

  3. templateParser = new TemplateParser()


  4. //生成語(yǔ)法樹(shù)

  5. templateParser.parse(templateContent).then((templateAst)=>{

  6. //進(jìn)行上述目標(biāo)的轉(zhuǎn)換

  7. let convertedTemplate = templateConverter(templateAst)

  8. //把語(yǔ)法樹(shù)轉(zhuǎn)成文本

  9. templateConvertedString = templateParser.astToString(convertedTemplate)


  10. templateConvertedString = `<template>\r\n${templateConvertedString}\r\n</template>\r\n`

  11. fs.writeFile(targetFilePath,templateConvertedString, ()=>{

  12. resolve()

  13. });

  14. }).catch((e)=>{

  15. reject(e)

  16. })

  • TemplateParser 是我封裝的一個(gè)簡(jiǎn)單的模板 AST 處理類(lèi)庫(kù),(因?yàn)槭褂昧?htmlparser2 類(lèi)庫(kù),該類(lèi)庫(kù)的調(diào)用方式有點(diǎn)麻煩),我們看下代碼:

  1. const Parser = require('./Parser') //基類(lèi)

  2. const htmlparser = require('htmlparser2') //html的AST類(lèi)庫(kù)

  3. class TemplateParser extends Parser {

  4. constructor(){

  5. super()

  6. }


  7. /**

  8. * HTML文本轉(zhuǎn)AST方法

  9. * @param scriptText

  10. * @returns {Promise}

  11. */

  12. parse(scriptText){

  13. return new Promise((resolve, reject) => {

  14. //先初始化一個(gè)domHandler

  15. const handler = new htmlparser.DomHandler((error, dom)=>{

  16. if (error) {

  17. reject(error);

  18. } else {

  19. //在回調(diào)里拿到AST對(duì)象

  20. resolve(dom);

  21. }

  22. });

  23. //再初始化一個(gè)解析器

  24. const parser = new htmlparser.Parser(handler);

  25. //再通過(guò)write方法進(jìn)行解析

  26. parser.write(scriptText);

  27. parser.end();

  28. });

  29. }

  30. /**

  31. * AST轉(zhuǎn)文本方法

  32. * @param ast

  33. * @returns {string}

  34. */

  35. astToString (ast) {

  36. let str = '';

  37. ast.forEach(item => {

  38. if (item.type === 'text') {

  39. str += item.data;

  40. } else if (item.type === 'tag') {

  41. str += '<' + item.name;

  42. if (item.attribs) {

  43. Object.keys(item.attribs).forEach(attr => {

  44. str += ` ${attr}='${item.attribs[attr]}'`;

  45. });

  46. }

  47. str += '>';

  48. if (item.children && item.children.length) {

  49. str += this.astToString(item.children);

  50. }

  51. str += `</${item.name}>`;

  52. }

  53. });

  54. return str;

  55. }

  56. }


  57. module.exports = TemplateParser

  • 3、接下來(lái)我們看下具體替換過(guò)程:

  1. //html標(biāo)簽替換規(guī)則,可以添加更多

  2. const tagConverterConfig = {

  3. 'view':'div',

  4. 'image':'img'

  5. }

  6. //屬性替換規(guī)則,也可以加入更多

  7. const attrConverterConfig = {

  8. 'wx:for':{

  9. key:'v-for',

  10. value:(str)=>{

  11. return str.replace(/{{(.*)}}/,'(item,key) in $1')

  12. }

  13. },

  14. 'wx:if':{

  15. key:'v-if',

  16. value:(str)=>{

  17. return str.replace(/{{(.*)}}/,'$1')

  18. }

  19. },

  20. '@tap':{

  21. key:'@click'

  22. },

  23. }

  24. //替換入口方法

  25. const templateConverter = function(ast){

  26. for(let i = 0;i<ast.length;i++){

  27. let node = ast[i]

  28. //檢測(cè)到是html節(jié)點(diǎn)

  29. if(node.type === 'tag'){

  30. //進(jìn)行標(biāo)簽替換

  31. if(tagConverterConfig[node.name]){

  32. node.name = tagConverterConfig[node.name]

  33. }

  34. //進(jìn)行屬性替換

  35. let attrs = {}

  36. for(let k in node.attribs){

  37. let target = attrConverterConfig[k]

  38. if(target){

  39. //分別替換屬性名和屬性值

  40. attrs[target['key']] = target['value'] ? target['value'](node.attribs[k]) : node.attribs[k]

  41. }else {

  42. attrs[k] = node.attribs[k]

  43. }

  44. }

  45. node.attribs = attrs

  46. }

  47. //因?yàn)槭菢?shù)狀結(jié)構(gòu),所以需要進(jìn)行遞歸

  48. if(node.children){

  49. templateConverter(node.children)

  50. }

  51. }

  52. return ast

  53. }

css 處理

轉(zhuǎn)換目標(biāo)

  • 將 image 替換為 img

  • 將單位 rpx 轉(zhuǎn)換成 *@px

核心過(guò)程

  • 1、我們要先對(duì)拿到的 css 文本代碼進(jìn)行反轉(zhuǎn)義處理,因?yàn)樵诮馕?xml 過(guò)程中,css 中的特殊符號(hào)已經(jīng)被轉(zhuǎn)義了,這個(gè)處理邏輯很簡(jiǎn)單,只是字符串替換邏輯,因此封裝在 utils 工具方法里,本文不贅述。

  1. let styleText = utils.deEscape(v.childNodes.toString())

  • 2、根據(jù)節(jié)點(diǎn)屬性中的 type 來(lái)判斷是 less 還是普通 css

  1. if(v.attributes){

  2. //檢測(cè)css是哪種類(lèi)型

  3. for(let i in v.attributes){

  4. let attr = v.attributes[i]

  5. if(attr.name === 'lang'){

  6. type = attr.value

  7. }

  8. }

  9. }

  • 3、less 內(nèi)容的處理:使用 less.render()方法可以將 less 轉(zhuǎn)換成 css;如果是 css,直接對(duì) styleText 進(jìn)行處理就可以了

  1. less.render(styleText).then((output)=>{

  2. //output是css內(nèi)容對(duì)象

  3. })

  • 4、將 image 選擇器換成 img,這里也需要替換更多標(biāo)簽,比如 text、icon、scroll-view 等,篇幅原因不贅述

  1. const CSSOM = require('cssom') //css的AST解析器

  2. const replaceTagClassName = function(replacedStyleText){

  3. const replaceConfig = {}

  4. //匹配標(biāo)簽選擇器

  5. const tagReg = /[^\.|#|\-|_](\b\w+\b)/g

  6. //將css文本轉(zhuǎn)換為語(yǔ)法樹(shù)

  7. const ast = CSSOM.parse(replacedStyleText),

  8. styleRules = ast.cssRules


  9. if(styleRules && styleRules.length){

  10. //找到包含tag的className

  11. styleRules.forEach(function(item){

  12. //可能會(huì)有 view image {...}這多級(jí)選擇器

  13. let tags = item.selectorText.match(tagReg)

  14. if(tags && tags.length){

  15. let newName = ''

  16. tags = tags.map((tag)=>{

  17. tag = tag.trim()

  18. if(tag === 'image')tag = 'img'

  19. return tag

  20. })

  21. item.selectorText = tags.join(' ')

  22. }

  23. })

  24. //使用toString方法可以把語(yǔ)法樹(shù)轉(zhuǎn)換為字符串

  25. replacedStyleText = ast.toString()

  26. }

  27. return {replacedStyleText,replaceConfig}

  28. }

  • 5、將 rpx 替換為*@px

  1. replacedStyleText = replacedStyleText.replace(/([\d\s]+)rpx/g,'$1*@px')

  • 6、將轉(zhuǎn)換好的代碼寫(xiě)入文件

  1. replacedStyleText = `<style scoped>\r\n${replacedStyleText}\r\n</style>\r\n`


  2. fs.writeFile(targetFilePath,replacedStyleText,{

  3. flag: 'a'

  4. },()=>{

  5. resolve()

  6. });

JavaScript 轉(zhuǎn)換

轉(zhuǎn)換目標(biāo)

  • 去除 wepy 引用

  • 轉(zhuǎn)換成 vue 的對(duì)象寫(xiě)法

  • 去除無(wú)用代碼:this.\$apply()

  • 生命周期對(duì)應(yīng)

核心過(guò)程

在了解如何轉(zhuǎn)換之前,我們先簡(jiǎn)單了解下 JavaScript 轉(zhuǎn)換的基本流程:

借用其他作者一張圖片,可以看出轉(zhuǎn)換過(guò)程分為解析->轉(zhuǎn)換->生成 這三個(gè)步驟。

具體如下:

  • 1、先把 xml 節(jié)點(diǎn)通過(guò) toString 轉(zhuǎn)換成文本

  1. v.childNodes.toString()

  • 2、再進(jìn)行反轉(zhuǎn)義(否則會(huì)報(bào)錯(cuò)的哦)

  1. let javascriptContent = utils.deEscape(v.childNodes.toString())

  • 3、接下來(lái)初始化一個(gè)解析器

  1. let javascriptParser = new JavascriptParser()

這個(gè)解析器里封裝了什么呢,看代碼:

  1. const Parser = require('./Parser') //基類(lèi)

  2. const babylon = require('babylon') //AST解析器

  3. const generate = require('@babel/generator').default

  4. const traverse = require('@babel/traverse').default


  5. class JavascriptParser extends Parser {

  6. constructor(){

  7. super()

  8. }

  9. /**

  10. * 解析前替換掉無(wú)用字符

  11. * @param code

  12. * @returns

  13. */

  14. beforeParse(code){

  15. return code.replace(/this\.\$apply\(\);?/gm,'').replace(/import\s+wepy\s+from\s+['']wepy['']/gm,'')

  16. }

  17. /**

  18. * 文本內(nèi)容解析成AST

  19. * @param scriptText

  20. * @returns {Promise}

  21. */

  22. parse(scriptText){

  23. return new Promise((resolve,reject)=>{

  24. try {

  25. const scriptParsed = babylon.parse(scriptText,{

  26. sourceType:'module',

  27. plugins: [

  28. // 'estree', //這個(gè)插件會(huì)導(dǎo)致解析的結(jié)果發(fā)生變化,因此去除,這本來(lái)是acron的插件

  29. 'jsx',

  30. 'flow',

  31. 'doExpressions',

  32. 'objectRestSpread',

  33. 'exportExtensions',

  34. 'classProperties',

  35. 'decorators',

  36. 'objectRestSpread',

  37. 'asyncGenerators',

  38. 'functionBind',

  39. 'functionSent',

  40. 'throwExpressions',

  41. 'templateInvalidEscapes'

  42. ]

  43. })

  44. resolve(scriptParsed)

  45. }catch (e){

  46. reject(e)

  47. }

  48. })

  49. }


  50. /**

  51. * AST樹(shù)遍歷方法

  52. * @param astObject

  53. * @returns {*}

  54. */

  55. traverse(astObject){

  56. return traverse(astObject)

  57. }


  58. /**

  59. * 模板或AST對(duì)象轉(zhuǎn)文本方法

  60. * @param astObject

  61. * @param code

  62. * @returns {*}

  63. */

  64. generate(astObject,code){

  65. const newScript = generate(astObject, {}, code)

  66. return newScript

  67. }

  68. }

  69. module.exports = JavascriptParser

值得注意的是:babylon 的 plugins 配置有很多,如何配置取決于你的代碼里面使用了哪些高級(jí)語(yǔ)法,具體可以參見(jiàn)文檔或者根據(jù)報(bào)錯(cuò)提示處理

  • 4、在解析之前可以先通過(guò) beforeParse 方法去除掉一些無(wú)用代碼(這些代碼通常比較固定,直接通過(guò)字符串替換掉更方便)

  1. javascriptContent = javascriptParser.beforeParse(javascriptContent)

  • 5、再把文本解析成 AST

  1. javascriptParser.parse(javascriptContent)

  • 6、通過(guò) AST 遍歷整個(gè)樹(shù),進(jìn)行各種代碼轉(zhuǎn)換

  1. 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)的處理

  1. const componentVistor = {

  2. enter(path) {

  3. if (path.isIdentifier({ name: 'n' })) {

  4. path.node.name = 'x';

  5. }

  6. },

  7. exit(path){

  8. //do sth

  9. }

  10. }

  • 按類(lèi)型遍歷:traverse 幫你找到對(duì)應(yīng)類(lèi)型的所有節(jié)點(diǎn)

  1. const componentVistor = {

  2. FunctionDeclaration(path) {

  3. path.node.id.name = 'x';

  4. }

  5. }

本文代碼主要使用了樹(shù)狀遍歷的方式,代碼如下:

  1. const componentVistor = {

  2. enter(path) {

  3. //判斷如果是類(lèi)屬性

  4. if (t.isClassProperty(path)) {

  5. //根據(jù)不同類(lèi)屬性進(jìn)行不同處理,把wepy的類(lèi)屬性寫(xiě)法提取出來(lái),放到VUE模板中

  6. switch (path.node.key.name){

  7. case 'props':

  8. vistors.props.handle(path.node.value)

  9. break;

  10. case 'data':

  11. vistors.data.handle(path.node.value)

  12. break;

  13. case 'events':

  14. vistors.events.handle(path.node.value)

  15. break;

  16. case 'computed':

  17. vistors.computed.handle(path.node.value)

  18. break;

  19. case 'components':

  20. vistors.components.handle(path.node.value)

  21. break;

  22. case 'watch':

  23. vistors.watch.handle(path.node.value)

  24. break;

  25. case 'methods':

  26. vistors.methods.handle(path.node.value)

  27. break;

  28. default:

  29. console.info(path.node.key.name)

  30. break;

  31. }

  32. }

  33. //判斷如果是類(lèi)方法

  34. if(t.isClassMethod(path)){

  35. if(vistors.lifeCycle.is(path)){

  36. vistors.lifeCycle.handle(path.node)

  37. }else {

  38. vistors.methods.handle(path.node)

  39. }

  40. }

  41. }

  42. }

本文的各種 vistor 主要做一個(gè)事,把各種類(lèi)屬性和方法收集起來(lái),基類(lèi)代碼:

  1. class Vistor {

  2. constructor() {

  3. this.data = []

  4. }

  5. handle(path){

  6. this.save(path)

  7. }

  8. save(path){

  9. this.data.push(path)

  10. }

  11. getData(){

  12. return this.data

  13. }

  14. }

  15. module.exports = Vistor

這里還需要補(bǔ)充講下@babel/types這個(gè)類(lèi)庫(kù),它主要是提供了 JavaScript 的 AST 中各種節(jié)點(diǎn)類(lèi)型的檢測(cè)、改造、生成方法,舉例:

  1. //類(lèi)型檢測(cè)

  2. if(t.isClassMethod(path)){

  3. //如果是類(lèi)方法

  4. }

  5. //創(chuàng)造一個(gè)對(duì)象節(jié)點(diǎn)

  6. t.objectExpression(...)

通過(guò)上面的處理,我們已經(jīng)把 wepy 里面的各種類(lèi)屬性和方法收集好了,接下來(lái)我們看如何生成 vue 寫(xiě)法的代碼

  • 7、把轉(zhuǎn)換好的 AST 樹(shù)放到預(yù)先定義好的 template 模板中

  1. convertedJavascript = componentTemplateBuilder(convertedJavascript,vistors)

看下 componentTemplateBuilder 這個(gè)方法如何定義:

  1. const componentTemplateBuilder = function(ast,vistors){

  2. const buildRequire = template(componentTemplate);

  3. ast = buildRequire({

  4. PROPS: arrayToObject(vistors.props.getData()),

  5. LIFECYCLE: arrayToObject(vistors.lifeCycle.getData()),

  6. DATA: arrayToObject(vistors.data.getData()),

  7. METHODS: arrayToObject(vistors.methods.getData()),

  8. COMPUTED: arrayToObject(vistors.computed.getData()),

  9. WATCH: arrayToObject(vistors.watch.getData()),

  10. });

  11. return ast

  12. }

這里就用到了@babel/template這個(gè)類(lèi)庫(kù),主要作用是可以把你的代碼數(shù)據(jù)組裝到一個(gè)新的模板里,模板如下:

  1. const componentTemplate = `

  2. export default {

  3. data() {

  4. return DATA

  5. },


  6. props:PROPS,


  7. methods: METHODS,


  8. computed: COMPUTED,


  9. watch:WATCH,


  10. }

  11. `

*生命周期需要進(jìn)行對(duì)應(yīng)關(guān)系處理,略復(fù)雜,本文不贅述

  • 8、把模板轉(zhuǎn)換成文本內(nèi)容并寫(xiě)入到文件中

  1. let codeText = `<script>\r\n${generate(convertedJavascript).code}\r\n</script>\r\n`


  2. fs.writeFile(targetFilePath,codeText, ()=>{

  3. resolve()

  4. });

這里用到了@babel/generate類(lèi)庫(kù),主要作用是把 AST 語(yǔ)法樹(shù)生成文本格式

上述過(guò)程的代碼實(shí)現(xiàn)總體流程

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


  2. //先反轉(zhuǎn)義

  3. let javascriptContent = utils.deEscape(v.childNodes.toString()),

  4. //初始化一個(gè)解析器

  5. javascriptParser = new JavascriptParser()


  6. //去除無(wú)用代碼

  7. javascriptContent = javascriptParser.beforeParse(javascriptContent)

  8. //解析成AST

  9. javascriptParser.parse(javascriptContent).then((javascriptAst)=>{

  10. //進(jìn)行代碼轉(zhuǎn)換

  11. let {convertedJavascript,vistors} = componentConverter(javascriptAst)

  12. //放到預(yù)先定義好的模板中

  13. convertedJavascript = componentTemplateBuilder(convertedJavascript,vistors)


  14. //生成文本并寫(xiě)入到文件

  15. let codeText = `<script>\r\n${generate(convertedJavascript).code}\r\n</script>\r\n`


  16. fs.writeFile(targetFilePath,codeText, ()=>{

  17. resolve()

  18. });

  19. }).catch((e)=>{

  20. reject(e)

  21. })

上面就是 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è)命令

  • 1、在項(xiàng)目的 package.json 里面配置 bin 部分

  1. {

  2. 'name': '@zz-vc/fancy-cli',

  3. 'bin': {

  4. 'fancy': 'bin/fancy'

  5. },

  6. //其他配置

  7. }

  • 2、寫(xiě)好代碼后,npm publish 上去

  • 3、開(kāi)發(fā)者安裝了你的插件后就可以在命令行以fancy xxxx的形式直接調(diào)用命令了

編寫(xiě)命令調(diào)用代碼

  1. #!/usr/bin/env node


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


  3. const { resolve } = require('path')


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


  5. const program = require('commander')


  6. program

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


  8. program

  9. .usage('<command>')


  10. //注冊(cè)convert命令

  11. program

  12. .command('convert <componentName>')

  13. .description('convert a component,eg: fancy convert Tab.vue')

  14. .alias('c')

  15. .action((componentName) => {

  16. let fn = require(res('convert'))

  17. fn(componentName)

  18. })



  19. program.parse(process.argv)


  20. if(!program.args.length){

  21. program.help()

  22. }

convert 命令對(duì)應(yīng)的代碼:

  1. const cwdPath = process.cwd()

  2. const convert = async function(filepath){

  3. let fileText = await fse.readFile(filepath, 'utf-8');

  4. fileHandle(fileText.toString(),filepath)

  5. }


  6. module.exports = function(fileName){

  7. convert(`${cwdPath}/${fileName}`)

  8. }

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ì)你有所收獲。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶(hù)發(fā)布,不代表本站觀(guān)點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶(hù) 評(píng)論公約

    類(lèi)似文章 更多