如何实现 Vue 转小程序 (2)

小程序
首页

parse

接着,我们来实现传入的 startCb、endCb 和 CharsCb 方法:

parser.parse = source => {
  const res = [];
  const stack = [];
  parseHtml(source, start, end, chars);
  return res;
  function start(tag, attrs, selfClose, start, end) {}
  function end(tag, start, end) {}
  function chars(text, start, end) {}
};

res 是返回的结果,stack 是用来暂存的栈,start、end 和 chars 是我们要实现的方法,我们先来看看 start 方法:

function start(tag, attrs, selfClose, start, end) {
  const element = {
    tag,
    attrs,
    selfClose,
    children: [],
    start,
    content: []
  };
  if (stack.length > 0) {
    stack[stack.length - 1].children.push(element);
    if (!selfClose) {
      stack.push(element);
    } else {
      element.end = end - 1;
    }
  } else {
    res.push(element);
    stack.push(element);
  }
}

start 方法中先声明一个 element 对象,其中包括标签名 tag,属性 attrs,是否为自闭合标签 selfClose,子元素 children,开始 index start,文本内容 content

再对 stack 的长度进行判断,如果大于 0,则说明处于其他标签之中,将 element 传入 stack 最后一个元素的 children 中;然后判断 selfClose,若不是是自闭合标签,则放入栈中;

如果 stack 长度为 0,则是根标签,将 element 直接存入 res 中,并放入栈中

再看看 end 方法,stack 出栈:

function end(tag, start, end) {
  const element = stack.pop();
  element.end = end - 1;
}

然后是 chars 方法,将文本内容传入栈顶元素的 content:

function chars(text, start, end) {
  if (stack.length > 0) {
    const top = stack[stack.length - 1];
    top.content.push({
      text,
      start,
      end
    });
  }
}

至此,传入 source,parse 方法可以将 source 转为一个数组,我们来实验一下:

const source = `<template lang="vue" vue>
    <div>
        test
        <img />
        <text>123</text>
    </div>
</template>`;
const res = parser.parse(source);
console.log(util.inspect(res, false, null));

最后输出: carbon

serialize

vue 单文件可以转化为 js 对象,那 js 对象当然可以转化为 vue 单文件内容,接下来我们就来实现 serialize 方法:

parser.serialize = node => {
  let source = "";
  node.forEach(item => {
    source += serializeNode(item) + "\n";
  });
  return source;
  function serializeNode(node) {}
};

serialize 接受一个数组,遍历 node,将子 node 传入 serializeNode 方法获取序列化结果,serializeNode 方法如下:

function serializeNode(node) {
  const { tag, attrs, selfClose, children, content } = node;
  let attrString = "";
  attrs.forEach(attr => {
    const { name, value } = attr;
    if (value === true) {
      attrString += ` ${name}`;
    } else {
      attrString += ` ${name}=${value}`;
    }
  });
  if (selfClose) {
    return `<${tag}${attrString} />`;
  }
  const childrenLength = children.length;
  const contentLength = content.length;
  let serializedChildNodes = "";
  let childrenIndex = 0;
  let contentIndex = 0;
  while (childrenIndex < childrenLength || contentIndex < contentLength) {
    if (childrenIndex >= childrenLength) {
      serializedChildNodes += content[contentIndex++].text;
      continue;
    }
    if (contentIndex >= contentLength) {
      serializedChildNodes += serializeNode(children[childrenIndex++]);
      continue;
    }
    serializedChildNodes +=
      content[contentIndex].start > children[childrenIndex].start
        ? serializeNode(children[childrenIndex++])
        : content[contentIndex++].text;
  }
  return `<${tag}${attrString} >${serializedChildNodes}</${tag}>`;
}

先将 node 中的 attr 数组转化为字符串的形式,再判断是否为自闭合标签,若为自闭合标签,直接返回<${tag}${attrString} />

再对contentchildren进行遍历,遍历的规则是根据起始 index 数 start 依次遍历,若content[contentIndex].start<children[childrenIndex].start,则拿到content[contentIndex]text,将其加到serializedChildNodes中;反之调用serializeNode()方法,并传入children[childrenIndex++],将返回值加到serializedChildNodes中。

遍历完成后返回<${tag}${attrString} >${serializedChildNodes}</${tag}>

到了这里,serialize 方法也已完成,我们来试一下:

const source = `<template lang="vue" test="11" xx yy>
    <div>
        test
        <img />
        <text>123</text>
    </div>
</template>


<script>
export default {
name: "Test",
components: {},
data: function() {
    return {};
}
};
</script>


<style scoped>
</style>`;
const res = parser.parse(source);
console.log(parser.serialize(res));

可以看到,最后输出的字符与 source 基本一致(有些空白字符没有完美处理,所以会有点出入,但不影响代码结构)