最近在作需求的時候,常常是,同一個需求是在h5端實現一次,再在小程序實現一次,公司的h5端是用vue寫的,微信小程序則是小程序的原生語言,這就致使了不少很重複的勞動,雖然語言不一樣,但邏輯和設計都是如出一轍的。javascript
而公司也沒想過花點時間統一一下,好比考慮使用一下mpvue之類的,因此,在本着偷懶的心態下,開始想着如何能避免重複性的工做,好比只須要寫一套代碼。可是跟mpvue不同,不須要一個DSL工程化的東西,只須要轉換一下本身想轉換的文件。css
因而就有了這個想法,把所須要單個vue文件的轉換爲小程序原生語言所須要的四個文件(wxml, wxss, json, js)vue
在開始以前,須要瞭解一點AST(抽象語法樹)的相關知識。node
好比JavaScript在執行以前,會通過詞法分析和語法分析兩個步驟以後,獲得一個抽象語法樹。git
好比下面這段代碼github
const foo = (item) => item.id
複製代碼
獲得的抽象語法樹以下圖。 這是在AST Explorer轉換獲得的。express
能夠看到咱們的js代碼已經被轉換成一個json對象,這個json對象的描述了這段代碼。 咱們能夠經過拿到這個json對象去進行樹形遍歷,從而把這一段js代碼進行加工成一段咱們想要的代碼。好比能夠把它轉換成一段ES5的代碼。json
這裏就不描述具體步驟了,在後面的將script -> js中有具體描述。gulp
這是js的部分。而在vue中,也是將template中的代碼轉換成了AST結構的json文件。後面咱們須要使用到的postcss也是把less或者css文件轉換成一個AST結構的json文件,而後再加工,輸出成所須要的文件。
另外還有一個須要瞭解的是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文件的結構以下:
能夠看到單個的vue文件已經被解析成了三個部分,styles是一個數組,由於在vue文件中能夠寫多個style標籤。 咱們拿到解析後的json文件以後,就能夠正式開始了。
首先從最簡單的開始。將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好像會失效。
在進行這個步驟以前,先得講一個很重要的工具,就是 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運行原理,而後舉了一個簡單的例子,整個過程基本是這樣的,複雜的部分主要是對每個須要攔截的節點進行處理。
若是想多瞭解一點能夠參考一下這裏
如今能夠正式開始了。
首先來看一下vue文件中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 文件中
複製代碼
接下來處理 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
在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) // 替換掉當前節點
}
},
}
複製代碼
往 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.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)
}
},
}
複製代碼
// ......
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代碼:
轉換後的小程序代碼:
接下來是 將 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樹的結果以下:
能夠看出對咱們有用的屬性就幾個
還有一些特殊的屬性
拿到這個結構以後要怎麼轉換呢。
個人思路是,由於這是一個樹形結構,因此能夠採用深度優先遍歷,廣度優先遍歷或者遞歸遍歷。
經過遍歷給每個節點加上一個開始標籤 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}`
}
複製代碼
轉換完的格式仍是須要本身調整一下。
轉換前的vue代碼:
轉換後的小程序代碼:
留下的坑其實還蠻多,之後慢慢完善。作這個不是想作一個工程化的東西,工程化的東西已經有mpvue等框架了。
就是想偷點懶...哈哈。歡迎一塊兒交流。
完整代碼在 ast-h5-wp