如何实现 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 才会结束