如何实现 Vue 转小程序 (1)
引
利用小程序框架,我们可以用包括 react、vue 在内的各种姿势开发小程序,本文不是教你使用上手这些小程序框架,而是从 0 开始,编码实现 Vue 代码转小程序代码这一奇妙过程
开始实现
我们知道,小程序的 page 和 component 都是由 wxml、js、wxss、json 四部分组成,实际上这种多文件的开发方式体验并不是很好,而 vue 采用的是单文件开发。这就需要我们解析 Vue 代码,将它们分为几部分,以便之后将它转为小程序所需要的四个文件
Vue 的代码是由各个标签组成,tempalte
包裹模版,其中又有各种标签,script
则是 js 代码,style
内定义样式,标签也可能是<img/>
这种自闭合标签;这些标签上又会有各式属性,属性有<div class="wrapper"></div>
、<style scoped></style>
两种定义方式。在以上条件的前提下,我们先写出几个正则用来匹配这些规则:
const tagName = "([a-zA-Z_][\\w\\-\\.]*)"; //标签名
const startTag = new RegExp("^<" + tagName); //开始标签的开头
const startTagClose = /^\s*(\/?)>/; //开始标签的结束,用(\/?)捕获/,得以判断是否为自闭合标签
const endTag = new RegExp("^</(" + tagName + ")[^>]*>"); // 结束标签
const attribute = /\s*([^\s<>=]+)(?:\s*=\s*)?([^\s<>=]*)/; //标签上的属性
有了这些正则,我们很轻松就知道这个标签是开始标签还是结束标签,抑或者自闭合标签;而通过捕获括号,可以拿到标签名和标签上的各式属性
接下来,我们定义一个 parseHtml 方法:
const parseHtml = (
source,
startCb = () => {},
endCb = () => {},
charsCb = () => {}
) => {};
其中 source 是 vue 单文件的文本,startCb、endCb、charsCb 分别是遇上开始标签、结束标签、文本内容相应的回调,由外部传入,我们先不管这三个回调的具体实现,先完成 parseHtml 函数的实现:
let index = 0;
const stack = [];
while (source) {
let tagStartIndex = source.indexOf("<");
if (tagStartIndex === 0) {
const endTagMatch = source.match(endTag);
if (endTagMatch) {
const curIndex = index;
advance(endTagMatch[0].length);
parseEndTag(endTagMatch[1], curIndex, index);
continue;
}
const startTagMatch = parseStartTag();
if (startTagMatch) {
handleStartTag(startTagMatch);
continue;
}
}
}
由于标签是有嵌套关系的,所以我们先定义一个栈 stack 保存标签的嵌套关系;
我们知道,不管是开始标签还是结束标签肯定是由<
开头,我们先获取一个<
的 index 值,如果为 0,则去匹配闭合标签和开始标签
闭合标签
若匹配闭合标签成功,则利用advance
方法向前进闭合标签字符串的长度,advance
实现如下:
function advance(n) {
index += n;
source = source.slice(n);
}
然后调用praseEndTag
方法,传入标签名 tagName,闭合标签的开始 index 和结束 index:
function parseEndTag(tagName, start, end) {
const pop = stack.pop();
if (pop.tag === tagName) {
endCb(tagName, start, end);
} else {
console.error("请检查标签嵌套关系");
}
}
在parseEndTag
方法中,先从 stack 中弹出顶部元素,与 tagName 对比,如果相同,则调用 endCb 方法并传入 tagName,start,end;
开始标签
若匹配闭合标签失败,再调用 parseStartTag 方法匹配开始标签:
function parseStartTag() {
const start = source.match(startTag);
if (start) {
const match = {
tag: start[1],
attrs: [],
start: index
};
advance(start[0].length);
let end, attr;
while (
!(end = source.match(startTagClose)) &&
(attr = source.match(attribute))
) {
advance(attr[0].length);
if (attr[2] === "") {
attr[2] = true;
}
const [, name, value] = attr;
match.attrs.push({ name, value });
}
if (end) {
advance(end[0].length);
match.end = index;
match.selfClose = !!end[1];
return match;
}
}
}
若匹配成功,创建 match 对象,传入标签名 tag,attrs 用来保存属性;
再利用attribute
正则匹配属性,需要注意,如果source.match(attribute)
第三项为空,则该属性是<script scoped></script>
中scoped
这种形式的属性,我们将它的置为true
,并将属性名和属性值 push 至 attrs 中
还有一个地方需要注意,由于有<img/>
这种自闭合标签的存在,我们需要对捕获括号(\/?)
进行判断,若成功捕获,则为自闭合标签
若parseStartTag
返回值存在,调用handleStartTag
函数:
function handleStartTag(match) {
const { tag, selfClose, attrs, start, end } = match;
if (!selfClose) {
stack.push({
tag,
attrs
});
}
startCb(tag, attrs, selfClose, start, end);
}
如果不是自闭合标签,则向 stack 中 push 标签名 tag 和属性数组 attrs;
最后调用 startCb 函数
字符内容
以上是if (tagStartIndex === 0)
为 true 的情况下,而如果tagStartIndex>=0
(等于 0 是前面匹配开始标签和结束标签失败的情况)则是像content</tag>
这种形式,需要处理content
这种文本内容:
if (tagStartIndex >= 0) {
let rest = source.slice(tagStartIndex);
while (!endTag.test(rest) && !startTag.test(rest)) {
const next = rest.indexOf("<");
if (next < 0) {
break;
}
tagStartIndex += next;
}
const text = source.slice(0, tagStartIndex);
charsCb(text, index, index + text.length - 1);
advance(tagStartIndex);
}
文本内容遇上 startTag 或 endTag 才会结束