预备知识
AST
在开始之前,需要了解一点AST(抽象语法树)的相关知识。
比如JavaScript在执行之前,会经过词法分析和语法分析两个步骤之后,得到一个抽象语法树。
比如下面这段代码
代码语言: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文件。
具体步骤如下:
代码语言:javascript复制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)。
具体步骤如下
代码语言:javascript复制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语法的函数。
代码语言:javascript复制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的基本结构。
script的基本结构
可以看到在export default中有directives和components两个属性与import导入的文件有关
小程序中,directives不需要,需要删除这个节点,同时也要删除import进来的这个文件;components也不需要,但是components 中的文件需要放到小程序的json文件中的usingComponents中。
所以下面先处理import部分:
代码语言:javascript复制// ......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}] } // 配置一个 parseImportVisitorconst result = babel.transform(scriptContent, babelOptions)fs.writeFileSync('./dist/res-js.js', result.code.trim());
下面是在parseImportVisitor中拦截ImportSpecifier,ImportDefaultSpecifier具体处理,ImportDefaultSpecifier是从node_modules中导入的文件,ImportSpecifier是从自己写的文件。 要对两个type进行相同的处理可以用一个管道符号 | ,像这样ImportSpecifier|ImportDefaultSpecifier
代码语言:javascript复制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 文件中。
代码语言:javascript复制// 生成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
代码语言:javascript复制const scriptContent = sfc.script.contentconst 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
代码语言:javascript复制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等其它生命周期或者其它属性需要映射的话,以后慢慢改进。
代码语言:javascript复制// ......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 进行处理
代码语言:javascript复制// ......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中,留点坑,以后填...
代码语言:javascript复制// ......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
代码语言:javascript复制// ......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生命周期中。
代码语言:javascript复制// 以下两个函数实现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转换为这样子的形式的写法就行了。比如:
代码语言:javascript复制watch: { test(newVal, oldVal) { if (newVal === 1) { return 123; } }},
需要转换成
代码语言:javascript复制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,比如:
代码语言:javascript复制this.$router.push({ name: 'ProductList', params: { countryId: this.product.visa_country_id},});
需要转换成
代码语言:javascript复制wx.navigateTo({ url: `ProductList?countryId=${this.data.product.visa_country_id}`});
下面是具体转换过程:
代码语言:javascript复制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
代码语言:javascript复制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。这里采用递归遍历。
代码如下:
代码语言:javascript复制const parseHtml = function(tagsTree) { return handleTagsTree(tagsTree)}
代码语言:javascript复制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) //生成结束标签}
下面是生成开始标签的代码:
代码语言:javascript复制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}
结束标签很简单。 这里是生成结束标签的代码:
代码语言:javascript复制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}
将开始标签和结束标签合并
拿到开始标签和结束标签之后,接下来就是重组代码了。
代码语言:javascript复制const handleTagsTree = function (topTreeNode) {
// 为每一个节点生成开始标签和结束标签 generateTag(topTreeNode)
return createWxml(topTreeNode)};
代码语言:javascript复制// 递归生成 所需要的文本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}`}