用戶
 找回密碼
 立即注冊

QQ登錄

只需一步,快速開始

掃一掃,登錄網站

小程序社區 首頁 教程 查看內容

如何將單個vue文件轉換為小程序所需的四個文件(wxml, wxss, json, js) ...

Rolan 2019-8-29 00:38

最近在做需求的時候,經常是,同一個需求是在h5端實現一次,再在小程序實現一次,公司的h5端是用vue寫的,微信小程序則是小程序的原生語言,這就導致了很多很重復的勞動,雖然語言不同,但邏輯和設計都是一模一樣的 ...

最近在做需求的時候,經常是,同一個需求是在h5端實現一次,再在小程序實現一次,公司的h5端是用vue寫的,微信小程序則是小程序的原生語言,這就導致了很多很重復的勞動,雖然語言不同,但邏輯和設計都是一模一樣的。

而公司也沒想過花點時間統一一下,比如考慮使用一下mpvue之類的,所以,在本著偷懶的心態下,開始想著如何能避免重復性的工作,比如只需要寫一套代碼。但是跟mpvue不一樣,不需要一個DSL工程化的東西,只需要轉換一下自己想轉換的文件。

于是就有了這個想法,把所需要單個vue文件的轉換為小程序原生語言所需要的四個文件(wxml, wxss, json, js)

有點長,需要耐心讀一下。

預備知識

AST

在開始之前,需要了解一點AST(抽象語法樹)的相關知識。

比如JavaScript在執行之前,會經過詞法分析和語法分析兩個步驟之后,得到一個抽象語法樹。

比如下面這段代碼

const foo = (item) => item.id
復制代碼

得到的抽象語法樹如下圖。 這是在AST Explorer轉換得到的。

抽象語法樹

可以看到我們的js代碼已經被轉換成一個json對象,這個json對象的描述了這段代碼。 我們可以通過拿到這個json對象去進行樹形遍歷,從而把這一段js代碼進行加工成一段我們想要的代碼。比如可以把它轉換成一段ES5的代碼。

這里就不描述具體步驟了,在后面的將script -> js中有具體描述。

這是js的部分。而在vue中,也是將template中的代碼轉換成了AST結構的json文件。后面我們需要使用到的postcss也是把less或者css文件轉換成一個AST結構的json文件,然后再加工,輸出成所需要的文件。

vue-template-compiler

另外還有一個需要了解的是vue-template-compiler。 我們寫的單個vue文件叫做SFC(Single File Components)。 vue-template-compiler 就是解析SFC文件,提取每個語言塊,將單個VUE文件的template、script、styles分別解析,得到一個json文件。

具體步驟如下。

const fs = require('fs');
const compiler = require('vue-template-compiler')

// 讀取vue文件
const vueFileContent = fs.readFileSync('./target/target.vue', 'utf8');
const sfc = compiler.parseComponent(vueFileContent)

復制代碼

得到的sfc的json文件的結構如下:

SFC

可以看到單個的vue文件已經被解析成了三個部分,styles是一個數組,因為在vue文件中可以寫多個style標簽。 我們拿到解析后的json文件之后,就可以正式開始了。

style -> wxss文件

首先從最簡單的開始。將styles部分轉換成wxss文件。

因為在vue中我們使用的是less的語法,所以解析出來的styles中content的代碼是less語法。但是小程序需要的是css的語法。所以我們需要將less轉換成css。另外在h5端我們less的單位是rem,所以還需要將rem轉換成rpx。

將less換成css,將rem轉換成rpx的方案有很多,這里采用的是postcss。另外還有gulp的方案也可以試試。

postcss已經有插件可以將less轉換成css,rem轉換成rpx。所以我們直接用postcss以及postcss的插件(postcss-less-engine, postcss-clean, postcss-rem2rpx)。

具體步驟如下:

const compiler = require('vue-template-compiler')

const postcss = require('postcss');
const less = require('postcss-less-engine');
const clean = require('postcss-clean');
const rem2rpx = require('postcss-rem2rpx');

// 讀取vue文件
const vueFileContent = fs.readFileSync('./target/target.vue', 'utf8');
const sfc = compiler.parseComponent(vueFileContent)

// 將styles數組中的content合并成一個字符串
const stylesSting = sfc.styles.reduce((pre, cur) => {
  return pre + cur.content.trim() + '\n'
}, '')

postcss([
  less({ strictMath: true }),
  rem2rpx({ rootFontSize: 50 }),
  clean()
])
.process(stylesSting, { parser: less.parser, from: 'res-styles-ast.less' })
.then((result) =>{
  fs.writeFileSync('./dist/res-style.wxss', result.css);
}, (err) =>{
  console.log(err);
});

復制代碼

這里有幾個需要注意的點。

1.由于styles是一個數組,postcss需要處理的是一個字符串,所以我們需要事先使用reduce把styles數組中的content合并成一個字符串。

2.在rem2rpx中,需要設置一個rootFontSize,這就需要根據自己的項目情況來。

3.如果style中有@import "./assets/styles/mixin.less";這樣的import代碼,則需要把這個文件copy到本地來。

4.這里安裝的less包版本為"less": "2.7.1",版本3以上好像postcss-less-engine好像會失效。

script -> js文件

babel

在進行這個步驟之前,先得講一個很重要的工具,就是Babel

在將vue中的script部分轉換成小程序需要的js文件過程中,最重要的就是Babel。

比如需要把created方法轉換為小程序的 onLoad 或者 組件中的 attached方法, 我們需要使用Babel把script部分的代碼解析成一個AST抽象語法樹,再用Babel的api去轉換和修改這顆抽象語法樹,最后再生成所需要的代碼。

bable在這里就像一把帶有魔法的手術刀, 可以把現有代碼轉換成任意代碼。這一點有點lisp的感覺。

總結一下 Babel 的三個主要步驟是:

1.解析(parse)

利用 babylon 對源代碼字符串進行解析并生成初始 AST 抽象語法樹

2.轉換(transform)

遍歷初始的 AST 抽象語法樹,babel 中有個babel-core,它向外暴露出babel.transform接口。

3.生成(generate)

生成部分 babel 會利用 babel-generator 將轉換后的 AST 樹轉換為新的代碼字符串。

以上是理論,下面我們來實踐一下。還是那上面AST的箭頭函數來練手,將它變成一個ES5語法的函數。

const babel = require('babel-core')
const types = require('babel-types'); // types就是用來構造一個新的node節點的

const visitor = {
  ArrowFunctionExpression(path) { // 在visitor中攔截箭頭函數
    let params = path.node.params // 獲取函數參數
    const returnStatement = types.returnStatement(path.node.body) //構建一個return表達式
    const blockStatement = types.blockStatement([returnStatement]) // 構建一個blockStatement
    // babel-types的functionExpression構造成一個新的ES function語法的函數
    let func = types.functionExpression(null, params, blockStatement, false, false)
    //替換當前箭頭函數節點
    path.replaceWith(func)
  },
  VariableDeclaration(path) { // 在visitor中變量聲明
    path.node.kind = 'var'
  }
}

const scriptContent = 'const foo = (item) => item.id' // 源代碼
const result = babel.transform(scriptContent, {
  plugins: [
      { visitor }
  ]
})

console.log(result.code.trim())
// 結果為:
// var foo = function (item) {
//   return item.id;
// };
復制代碼

以上只是簡單地講解了下babel運行原理,然后舉了一個簡單的例子,整個過程基本是這樣的,復雜的部分主要是對每一個需要攔截的節點進行處理。

如果想多了解一點可以參考一下這里

Babel 插件手冊

babel-types的使用手冊

處理import導入文件

現在可以正式開始了。

首先來看一下vue文件中script的基本結構。

script的基本結構

可以看到在export default中有directives和components兩個屬性與import導入的文件有關

小程序中,directives不需要,需要刪除這個節點,同時也要刪除import進來的這個文件;components也不需要,但是components 中的文件需要放到小程序的json文件中的usingComponents中。

所以下面先處理import部分:

// ......
const compiler = require('vue-template-compiler')

const babelrc = path.resolve('./.babelrc') //拿到本地的 babelrc 的配置

const vueFileContent = fs.readFileSync('./target/target.vue', 'utf8');
const sfc = compiler.parseComponent(vueFileContent)

const scriptContent = sfc.script.content // 拿到解析后的sfc中的script部分的源代碼
const babelOptions = { extends: babelrc, plugins: [{visitor: parseImportVisitor}] } // 配置一個 parseImportVisitor
const result = babel.transform(scriptContent, babelOptions)
fs.writeFileSync('./dist/res-js.js', result.code.trim());

復制代碼

下面是在parseImportVisitor中攔截ImportSpecifier,ImportDefaultSpecifier具體處理,ImportDefaultSpecifier是從node_modules中導入的文件,ImportSpecifier是從自己寫的文件。 要對兩個type進行相同的處理可以用一個管道符號 | ,像這樣ImportSpecifier|ImportDefaultSpecifier

const parseImportVisitor = {
  "ImportSpecifier|ImportDefaultSpecifier"(path) {
    const currentName = path.node.local.name // 獲取import進來的名稱,比如上圖中script的基本結構的 TransferDom, XDialog, stars

    const parentPath = path.findParent((path) => path.isImportDeclaration()); //找到當前節點的 ImportDeclaration 類型父節點
    const [ ExportDefaultDeclaration ] = parentPath.container.filter(item => item.type === 'ExportDefaultDeclaration') //通過父節點去找到 ExportDefaultDeclaration 類型的節點,就是export default中代碼
    const { properties } = ExportDefaultDeclaration.declaration // 獲取 export default 中所有屬性

    const [ directivesProperty ] = properties.filter(item => item.key.name === 'directives')
    if (directivesProperty) {
      const { properties } = directivesProperty.value // directives中的屬性值
      // 遍歷 directives 中的屬性值
      properties.forEach(p => {
        const value = p.value.name || p.value.value
        if (value === currentName) {
          // 如果在 directives中找到了和當前import進來的名字一樣的,就需要把當前的節點刪除
          // 比如 import { TransferDom, XDialog } from 'vux'; 刪除后會變成 import { XDialog } from 'vux';
          path.remove() 
          if (!parentPath.node.specifiers.length) { //如果父節點為空,需要把父節點也完全刪除
            path.parentPath.remove()
          }
        }
      })
    }
    
    // 上面對 directives 的處理是直接刪除
    // 下面對 components 的處理則需要保存起來,主要是保存在 path.hub.file 中的 metadata 中
    const { metadata } = path.hub.file
    const [ componentsProperty ] = properties.filter(item => item.key.name === 'components')
    const usingComponents = {...metadata.usingComponents} //創建一個 usingComponents 對象
    if (componentsProperty) {
      const { properties } = componentsProperty.value // 獲取 components 中的屬性值
      // 遍歷 components 中的屬性值
      properties.forEach(p => {
        const value = p.value.name || p.value.value
        if (value === currentName) {
          // 如果在 components 中找到了和當前import進來的名字一樣的,就需要把當前的節點放入 usingComponents 中,然后刪除
          usingComponents[value] = parentPath.node.source.value
          path.remove()
          if (!parentPath.node.specifiers.length) { //如果父節點為空,需要把父節點也完全刪除
            path.parentPath.remove()
          }
        }
      })

    }
    metadata.usingComponents = usingComponents

  },
}
復制代碼

上面的代碼將 components 中的組件放到了 path.hub.file.metadata中,這樣可便于在最后拿到結果的時候把 usingComponents 直接寫到 json 文件中。

// 生成json文件
// ......
const result = babel.transform(scriptContent, babelOptions)

const jsonFile = {
  component: result.metadata.isComponent ? true : undefined,
  usingComponents: result.metadata.usingComponents // 取出 metadata中的usingComponents
}
fs.writeFileSync('./dist/res-json.json', circularJSON.stringify(jsonFile, null, 2)); // 寫到 json 文件中
復制代碼

處理ExportDefaultDeclaration

接下來處理 export default 中的代碼。所以需要加一個 visitor

const scriptContent = sfc.script.content
const babelOptions = { extends: babelrc, plugins: [{visitor: parseImportVisitor}, { visitor: parseExportDefaultVisitor }] } // 這里添加了 一個 parseExportDefaultVisitor的方法
const result = babel.transform(scriptContent, babelOptions)
fs.writeFileSync('./dist/res-js.js', result.code.trim());

復制代碼

下面是 parseExportDefaultVisitor

const parseExportDefaultVisitor = {
  ExportDefaultDeclaration: function (path) { // 這里攔截 ExportDefaultDeclaration
    // 這里只處理 ExportDefaultDeclaration, 就是把export default 替換成 Page 或者 Component
    // 其它都交給 traverseJsVisitor 處理
    path.traverse(traverseJsVisitor)

    // 把export default 替換成 Page 或者 Component
    const { metadata } = path.hub.file
    const { declaration } = path.node
    const newArguments = [declaration]
    const name = metadata.isComponent ? 'Component' : 'Page'
    const newCallee = types.identifier(name)
    const newCallExpression = types.CallExpression(newCallee, newArguments)
    path.replaceWith(newCallExpression)
  }
}

復制代碼

這里需要注意的點是,export default如何替換成Page或者Component,在traverseJsVisitor會判斷當前文件是否是一個組件, 然后把isComponent保存到metadata中,在ExportDefaultDeclaration就可以取到 isComponent 的值,從而決定是生成 Page還是Component。

而在小程序Page({})或者Component({})是一個CallExpression, 所以需要構造一個CallExpression來替換掉ExportDefaultDeclaration

處理props, created, mounted, destroyed

在traverseJsVisitor來處理props, created, mounted, destroyed

props => properties

created => attached || onLoad

mounted => ready || onReady

destroyed => detached || onUnload

這里只是做了一下簡單映射,如果onShow或者active等其它生命周期或者其它屬性需要映射的話,以后慢慢改進。

// ......
const traverseJsVisitor = {
  
  Identifier(path) {
    const { metadata } = path.hub.file
    // 替換 props
    if (path.node.name === 'props') {
      metadata.isComponent = true //在這里判斷當前文件是否是一個組件

      const name = types.identifier('properties') //創建一個標識符
      path.replaceWith(name) // 替換掉當前節點
    }
    
    if (path && path.node.name === 'created'){
      let name
      if (metadata.isComponent) { //判斷是否是組件
        name = types.identifier('attached') //創建一個標識符
      } else {
        name = types.identifier('onLoad') //創建一個標識符
      }
      path.replaceWith(name) // 替換掉當前節點
    }
    if (path && path.node.name === 'mounted'){
      let name
      if (metadata.isComponent) { //判斷是否是組件
        name = types.identifier('ready') //創建一個標識符
      } else {
        name = types.identifier('onReady') //創建一個標識符
      }
      path.replaceWith(name) // 替換掉當前節點
    }
    if (path && path.node.name === 'destroyed'){
      let name
      if (metadata.isComponent) { //判斷是否是組件
        name = types.identifier('detached') //創建一個標識符
      } else {
        name = types.identifier('onUnload') //創建一個標識符
      }
      path.replaceWith(name) // 替換掉當前節點
    }
  },
}
復制代碼

處理 methods

往 traverseJsVisitor 中 再加入一個 ObjectProperty的攔截器,因為小程序中,組件文件的方法都是寫在 methods 屬性中, 而在非組件文件中 方法是直接和生命周期一個層級的,所以需要對 methods 進行處理

// ......
const traverseJsVisitor = {
  
  ObjectProperty: function (path) {
    const { metadata } = path.hub.file

     //是否是組件,如果是則不動, 如果不是,則用 methods 中的多個方法一起來替換掉當前的 methods節點
    if (path && path.node && path.node.key.name === 'methods' && !metadata.isComponent) {
      path.replaceWithMultiple(path.node.value.properties );
      return;
    }
    // 刪除 name directives components
    if (path.node.key.name === 'name' || path.node.key.name === 'directives' || path.node.key.name === 'components') {
      path.remove();
      return;
    }
  },
}
復制代碼

將this.xxx 轉換成 this.data.xxx, 將 this.xx = xx 轉換成 this.setData

這里其實是留了坑的,因為如果有多個this.xx = xx,我這里并沒有將他們合并到一個this.setData中,留點坑,以后填...

// ......
const traverseJsVisitor = {
  // 將this.xxx 轉換成 this.data.xxx
  MemberExpression(path) { // 攔截 MemberExpression
    const { object, property} = path.node
    if (object.type === 'ThisExpression' && property.name !== 'data') {
      const container = path.container
      if (container.type === 'CallExpression') {
        return;
      }
      if (property.name === '$router') {
        return;
      }
      // 將 this.xx 轉換成 this.data.xx
      const dataProperty = types.identifier('data')
      const newObject = types.memberExpression(object, dataProperty, false)
      const newMember = types.memberExpression(newObject, property, false)
      path.replaceWith(newMember)
    }
  },
  // 將 this.xx == xx 轉換成 this.setData
  AssignmentExpression(path) {  // 攔截 AssignmentExpression
    const leftNode = path.node.left
    const { object, property } = leftNode

    if (leftNode.type === 'MemberExpression' && leftNode.object.type === 'ThisExpression') {
      
      const properties = [types.objectProperty(property, path.node.right, false, false, null)]
      const arguments = [types.objectExpression(properties)]

      const object = types.thisExpression()
      const setDataProperty = types.identifier('setData')
      const callee = types.memberExpression(object, setDataProperty, false)

      const newCallExpression = types.CallExpression(callee, arguments)

      path.replaceWith(newCallExpression)
    }
  },
}
復制代碼

處理 props中的default;把 data 函數轉換為 data 屬性;處理watch

// ......
const traverseJsVisitor = {
  ObjectMethod: function(path) {
    // 替換 props 中 的defalut
    if (path && path.node && path.node.key.name === 'default') {
      
      const parentPath = path.findParent((path) => path.isObjectProperty());
      const propsNode = parentPath.findParent((findParent) => findParent.isObjectExpression()).container
      if (propsNode.key.name === 'properties') {
        const key = types.identifier('value')
        const value = path.node.body.body[0].argument
        const newNode = types.objectProperty(key, value, false, false, null)
        path.replaceWith(newNode)
      }
    }
    if (path && path.node.key.name === 'data') {
      const key = types.identifier('data')
      const value = path.node.body.body[0].argument
      const newNode = types.objectProperty(key, value, false, false, null)

      path.replaceWith(newNode)
    }

    if (path && path.node && path.node.key.name === 'created') {
      const watchIndex = path.container.findIndex(item => item.key.name === 'watch')
      const watchItemPath = path.getSibling(watchIndex)
      if (watchItemPath) {
        const { value } = watchItemPath.node
        const arguments = [types.thisExpression(), value]
        const callee = types.identifier('Watch')
  
        const newCallExpression = types.CallExpression(callee, arguments)
        path.get('body').pushContainer('body', newCallExpression);
        watchItemPath.remove()
      }

      return;
    }
  },
}
復制代碼

這里有一點需要注意的是watch的處理,因為小程序沒有watch,所以我在小程序手寫了一個簡單watch

而且小程序中的watch需要放在onLoad或者attached生命周期中。

// 以下兩個函數實現watch 未實現deep功能
const Watch = (ctx, obj) => {
  Object.keys(obj).forEach((key) => {
    defineProperty(ctx.data, key, ctx.data[key], (value) => {
      obj[key].call(ctx, value);
    });
  });
};

const defineProperty = (data, key, val, fn) => {
  Object.defineProperty(data, key, {
    configurable: true,
    enumerable: true,
    get() {
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      if (fn) fn(newVal);
      val = newVal;
    },
  });
};
復制代碼

所以只需要將vue中的watch轉換為這樣子的形式的寫法就行了。比如:

watch: {
  test(newVal, oldVal) {
    if (newVal === 1) {
      return 123;
    }
  }
},
復制代碼

需要轉換成

Watch(this, {
  test(newVal, oldVal) {
    if (newVal === 1) {
      return 123;
    }
  }
})
復制代碼

處理路由跳轉

處理路由跳轉有點復雜,需要將this.$router.push或者this.$router.replace轉換為wx.navigateTo或者wx.redirectTo

把this.$router的params參數和query參數合并到一起

并合成一個字符串url,比如:

this.$router.push({
  name: 'ProductList',
  params: { countryId: this.product.visa_country_id},
});
復制代碼

需要轉換成

wx.navigateTo({
  url: `ProductList?countryId=${this.data.product.visa_country_id}`
});
復制代碼

下面是具體轉換過程:

const traverseJsVisitor = {
    CallExpression(path) {
    // 處理 router 路由跳轉
    const { arguments, callee } = path.node
    
    const { object, property } = callee
    if (object && object.type === 'MemberExpression' && object.property.name === '$router') { //攔截到$router
      const properties = arguments[0].properties
      // vue里面這里只能獲取到 路由名稱,但是小程序需要的是page頁面的路徑,這里就沒有做轉換了,直接拿了路由名稱充當小程序跳轉的url,到時候手動改
      const [ nameInfo ] = properties.filter(item => item.key.name === 'name')
      const [ paramsInfo ] = properties.filter(item => item.key.name === 'params') //拿到router的params參數
      const [ queryInfo ] = properties.filter(item => item.key.name === 'query') //拿到router的query參數

      // 把params和query的參數都合并到一個數組當中去,然后 map 出 key 和 value
      const paramsValue = paramsInfo && paramsInfo.value
      const queryValue = queryInfo && queryInfo.value
      const paramsValueList = paramsValue && paramsValue.properties ? paramsValue.properties : []
      const queryValueList = queryValue && queryValue.properties ? queryValue.properties : []
      const paramsItems = [].concat(paramsValueList, queryValueList).map(item => ({ key: item.key, value: item.value }))

      const url = types.identifier('url') // 創建一個 叫做 url 的標識符
      const routeName = nameInfo.value.value // 跳轉的路由名稱
      
      let expressions, quasis
      if (paramsItems.some(item => types.isCallExpression(item.value) || types.isMemberExpression(item.value))) {
        const expressionList = paramsItems.filter(item => types.isCallExpression(item.value) || types.isMemberExpression(item.value))
        const literalList = paramsItems.filter(item => types.isLiteral(item.value))

        // 把參數都合并成一個字符串
        const templateElementLastItem = literalList.reduce((finalString, cur) => {
          return `${finalString}&${cur.key.name}=${cur.value.value}`
        }, '')

        const templateElementItemList = expressionList.map((item, index) => {
          if (index === 0) {
            return `${routeName}?${item.key.name}=`
          }
          return `&${item.key.name}=`
        })
        
        expressions = expressionList.map(item => item.value)
        quasis = [ ...templateElementItemList, templateElementLastItem ].map(item => {
          return types.templateElement({ raw: item, cooked: item }, false)
        })
      }
      const newTemplateLiteral = types.templateLiteral(quasis, expressions) //創建一個 templateLiteral
      const objectProperty = types.objectProperty(url, newTemplateLiteral, false, false, null)

      // 構造一個CallExpression
      let newPoperty
      if (property.name === 'replace') {
        newPoperty = types.identifier('redirectTo')
      }
      if (property.name === 'push') {
        newPoperty = types.identifier('navigateTo')
      }
      const newArguments = [types.objectExpression([objectProperty])]

      const newObject = types.identifier('wx')
      const newCallee = types.memberExpression(newObject, newPoperty, false)

      const newCallExpression = types.CallExpression(newCallee, newArguments)
      path.replaceWith(newCallExpression)
    }
  }
}
復制代碼

轉換結果

這里有一個例子。

轉換前的vue代碼:

轉換前的vue代碼

轉換后的小程序代碼:

轉換后的小程序代碼

template -> wxml文件

將 template 代碼轉換為 AST樹

接下來是 將 template 部分 轉換為 wxml 文件。這里要先用 vue-template-compiler 的 compiler 將 template 代碼轉換為 AST樹。

然后再實現一個解析這個 AST樹的函數parseHtml

const compiler = require('vue-template-compiler')
// 讀取vue文件
const vueFileContent = fs.readFileSync('./target/target.vue', 'utf8');
const sfc = compiler.parseComponent(vueFileContent)

const astTplRes = compiler.compile(sfc.template.content, {
  comments: true,
  preserveWhitespace: false,
  shouldDecodeNewlines: true
}).ast

const wxmlResult = parseHtml(astTplRes)

復制代碼

解析出來的 AST樹的結果如下:

template AST樹

可以看出對我們有用的屬性就幾個

  • tag: 標簽
  • type: 類型,1-標簽;2-表達式節點(Mustache);3-純文本節點和comment節點
  • attrsMap: 標簽上的屬性集合
  • children: 元素的子元素,需要遞歸遍歷處理

還有一些特殊的屬性

  • classBinding、styleBinding: 動態綁定的class、style
  • if、elseif、else: 條件語句中的條件
  • ifConditions: 條件語句的else、elseif的節點信息都放在ifConditions的block里了
  • isComment:是否是注釋

給AST樹的每個節點加上開始標簽和結束標簽

拿到這個結構之后要怎么轉換呢。

我的思路是,因為這是一個樹形結構,所以可以采用深度優先遍歷,廣度優先遍歷或者遞歸遍歷。

通過遍歷給每一個節點加上一個開始標簽 startTag,和一個 結束標簽 endTag。這里采用遞歸遍歷。

代碼如下:

const parseHtml = function(tagsTree) {
  return handleTagsTree(tagsTree)
}
復制代碼
const handleTagsTree = function (topTreeNode) {

  // 為每一個節點生成開始標簽和結束標簽
  generateTag(topTreeNode)

};

// 遞歸生成 首尾標簽
const generateTag = function (node) {
  let children = node.children
  // 如果是if表達式 需要做如下處理
  if (children && children.length) {
    let ifChildren
    const ifChild = children.find(subNode => subNode.ifConditions && subNode.ifConditions.length)
    if (ifChild) {
      const ifChildIndex = children.findIndex(subNode => subNode.ifConditions && subNode.ifConditions.length)
      ifChildren = ifChild.ifConditions.map(item => item.block)
      delete ifChild.ifConditions
      children.splice(ifChildIndex, 1, ...ifChildren)
    }
    children.forEach(function (subNode) {
      generateTag(subNode)
    })
  }
  node.startTag = generateStartTag(node) // 生成開始標簽
  node.endTag = generateEndTag(node) //生成結束標簽
}
復制代碼

下面是生成開始標簽的代碼:

const generateStartTag = function (node) {
  let startTag
  const { tag, attrsMap, type, isComment, text } = node
  // 如果是注釋
  if (type === 3) {
    startTag = isComment ? `<!-- ${text} -->` : text
    return startTag;
  }
  // 如果是表達式節點
  if (type === 2) {
    startTag = text.trim()
    return startTag;
  }
  switch (tag) {
    case 'div':
    case 'p':
    case 'span':
    case 'em':
      startTag = handleTag({ tag: 'view', attrsMap });
      break;
    case 'img':
      startTag = handleTag({ tag: 'image', attrsMap });
      break;
    case 'template':
      startTag = handleTag({ tag: 'block', attrsMap });
      break;
    default:
      startTag = handleTag({ tag, attrsMap });
  }
  return startTag
}

const handleTag = function ({
  attrsMap,
  tag
}) {
  let stringExpression = ''
  if (attrsMap) {
    stringExpression = handleAttrsMap(attrsMap)
  }
  return `<${tag} ${stringExpression}>`
}


// 這個函數是處理 AttrsMap,把 AttrsMap 的所有值 合并成一個字符串
const handleAttrsMap = function(attrsMap) {
  let stringExpression = ''
  stringExpression = Object.entries(attrsMap).map(([key, value]) => {
    // 替換 bind 的 :
    if (key.charAt(0) === ':') {
      return `${key.slice(1)}="{{${value}}}"`
    }
    // 統一做成 bindtap
    if (key === '@click') {
      const [ name, params ] = value.split('(')
      let paramsList
      let paramsString = ''
      if (params) {
        paramsList = params.slice(0, params.length - 1).replace(/\'|\"/g, '').split(',')
        paramsString = paramsList.reduce((all, cur) => {
          return `${all} data-${cur.trim()}="${cur.trim()}"`
        }, '')
      }
      return `bindtap="${name}"${paramsString}`
    }
    if (key === 'v-model') {
      return `value="{{${value}}}"`
    }
    if (key === 'v-if') {
      return `wx:if="{{${value}}}"`
    }
    if (key === 'v-else-if') {
      return `wx:elif="{{${value}}}"`
    }
    if (key === 'v-else') {
      return `wx:else`
    }
    if (key === 'v-for') {
      const [ params, list ] = value.split('in ')
      
      const paramsList = params.replace(/\(|\)/g, '').split(',')
      const [item, index] = paramsList
      const indexString = index ? ` wx:for-index="${index.trim()}"` : ''
      return `wx:for="{{${list.trim()}}}" wx:for-item="${item.trim()}"${indexString}`
    }
    return `${key}="${value}"`
  }).join(' ')
  return stringExpression
}

復制代碼

結束標簽很簡單。 這里是生成結束標簽的代碼:

const generateEndTag = function (node) {
  let endTag
  const { tag, attrsMap, type, isComment, text } = node
  // 如果是表達式節點或者注釋
  if (type === 3 || type === 2) {
    endTag = ''
    return endTag;
  }
  switch (tag) {
    case 'div':
    case 'p':
    case 'span':
    case 'em':
      endTag = '</view>'
      break;
    case 'img':
      endTag = '</image>'
      break;
    case 'template':
      endTag = '</block>'
      break;
    default:
      endTag = `</${tag}>`
  }
  return endTag
}

復制代碼

將開始標簽和結束標簽合并

拿到開始標簽和結束標簽之后,接下來就是重組代碼了。

const handleTagsTree = function (topTreeNode) {

  // 為每一個節點生成開始標簽和結束標簽
  generateTag(topTreeNode)

  return createWxml(topTreeNode)
};

復制代碼
// 遞歸生成 所需要的文本
const createWxml = function(node) {
  let templateString = '';
  const { startTag, endTag, children } = node
  let childrenString = ''
  if (children && children.length) {
    childrenString = children.reduce((allString, curentChild) => {
      const curentChildString = createWxml(curentChild)
      return `${allString}\n${curentChildString}\n`
    }, '')
  }
  return `${startTag}${childrenString}${endTag}`
}

復制代碼
鮮花
鮮花
雞蛋
雞蛋
分享至 : QQ空間
收藏
原作者: AidenH 來自: 掘金
不朽的浪漫援彩金
重庆时时彩现场开奖 财经新闻股票行情查 南粤36选7开奖走势 云南山水麻将在哪里下载 微信彩票北京28合法吗 福建体彩31选7开奖公告 国际棋牌手机版app下载 湖南哈哈麻将手机版苹果版 安徽十一选五前三值走势图百度乐彩 黑龙江p62和值走势图 上证指数20年曲线图 明星河北麻将作弊器免费版 海螺水泥股票吧 设有8个码组000000 山西扣点点麻将下载 亚投大发快三app