- 探索-如何將單個vue檔案轉換為小程式所需的四個檔案(wxml, wxss, json, js)
探索-如何將單個vue檔案轉換為小程式所需的四個檔案(wxml, wxss, json, js)
最近在做需求的時候,經常是,同一個需求是在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檔案的結構如下:
可以看到單個的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執行原理,然後舉了一個簡單的例子,整個過程基本是這樣的,複雜的部分主要是對每一個需要攔截的節點進行處理。
如果想多瞭解一點可以參考一下這裡
處理import匯入檔案
現在可以正式開始了。
首先來看一下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 檔案中
複製程式碼
處理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程式碼:
轉換後的小程式程式碼:
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樹的結果如下:
可以看出對我們有用的屬性就幾個
- 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}`
}
複製程式碼
轉換結果
轉換完的格式還是需要自己調整一下。
轉換前的vue程式碼:
轉換後的小程式程式碼:
總結
留下的坑其實還蠻多,以後慢慢完善。做這個不是想做一個工程化的東西,工程化的東西已經有mpvue等框架了。
就是想偷點懶...哈哈。歡迎一起交流。
完整程式碼在 ast-h5-wp