一种在线预览Vue组件的思路

最近在研究一些低代码平台,设想了一种面向开发者的低代码编辑器,本文是印证这个想法的第一步,即在线预览单个组件文件。本文以Vue技术栈为例,尝试实现一个可以在线预览Vue组件的功能。

<!--more-->

1. 背景

拿低代码比较常用的一个场景:活动页面来举例。如果是纯手工编码,需要经历

  • 启动本地开发环境
  • 编写代码、调试代码
  • 提交代码、CI打包、部署

不论页面的复杂程度,都需要经历这几个步骤,哪怕仅仅只是修改几个文案...

按照我的想法,开发者只需要编写一个页面组件文件,可以直接预览页面,在开发完毕后,点击一下保存就行了,最后的产出是一个完全用于生产环境的页面链接。

由于是代码编写,可以绕开低代码平台最大的缺点:不够灵活、很难扩展;也可以使用低代码平台的一些优点:开发迅速,所见及所得。

要解决这个问题,第一步就是要实现在开发时能够预览组件。

先从最简单的开始,下面是一个不依赖于外部模块的基础SFC文件

<template>
  <div>
    <h1>{{ msg }}</h1>
  </div>
</template>

<script>
export default {
  data() {
    return {
      msg: 'Hello world!'
    }
  }
}
</script>

<style lang='scss'>
h1 {
  color: red;
}
</style>

如何在浏览器中直接预览这个组件呢?实际上vue-cli本身是提供了打包单个文件的功能的,参考:构建目标

使用下面命令,将test1.vue文件打包成commonjs和umd模块文件,css文件会单独打包成一个文件

vue build --target lib test1.vue

最后还会生成一个demo.html文件,可以直接预览改组件内容

![image-20211207213835916](/Users/bcz/Library/Application Support/typora-user-images/image-20211207213835916.png)

对于单个sfc文件,这种方式看起来还是比较简便的,打包后直接使用对应的umd模块就可以了。

我们接下来要研究的是:直接在浏览器预览sfc组件。

2. 一些实现思路参考

2.1. ElementUI组件库文档

ElementUI的文档网站是放在源码仓库的,因此可以看一下这个文档的源码,了解它是如何实现在线展示组件的。

Button组件为例,找到其文档源码。从element/examples/route.config.js文件开始,在registerRoute找到页面路由的注册方式,最后找到button组件的文档,可以看见其页面内容是用markdown组件编写的,红框标注的代码块最后会被渲染成可交互的页面组件

在注册路由组建时,通过loadDocs方法引入markdown文档,并将其作为页面组件。

我们知道md文件是不能直接被js模块识别的,这里是使用了element/bin/md-loader实现的,将markdown内容进行解析,获取code标签的代码,并转换成一个SFC组件的内容,包含scripttemplate标签

上面展示了一个组件库文档实现markdowdn中代码直接渲染成组件的思路,但在ElementUI文档中的组件示例,并没有提供直接编辑修改的功能,取而代之的是提供了一个跳转到codepen在线运行的方法

我们甚至可以看看这种动态拼接代码然后跳转到codepen实现代码编辑的方法

实际上是构建了一个post form表单提交。

2.2. codepen在线JS工具

打开codepen的控制台,可以发现他的页面结构

可以看见编写的HTML、CSS、JS等代码都会被写到iframe对应的位置,由于js依赖于Vue库,还通过cdn script的形式插入到用户代码前面。

启动iframe的好处是可以获得一个隔离的沙盒,并且可以通过父子window通信实现代码的动态更新。

这种方式实际上是绕开了SFC的限制,在Vue runtime编译template并渲染render函数。跟下面这种写法实际上差不多

<style>
  h1 {
    color: red;
  }
</style>
<script>
  new Vue({
    el: "#app",
    template: `
<div>
  <h1>{{ msg }}</h1>
</div>
            `,
    data() {
      return {
        msg: "Hello Vue",
      };
    },
  });
</script>

那么对于SFC组件,看起来分别编写templatestylescript然后通过iframe运行是没有问题的,那么问题就变成了:如何在浏览器中解析SFC组件?

2.3. stroybook

storybook是一个UI组件的开发环境,官网上的文档介绍确实比较简陋,可以看看Introduction to Storybook这篇博客介绍。

3. 使用@vue/compile-sfc编译单个文件

前面提到Vue本身是支持直接传入template配置项的,因此只需要将SFC中的templatescriptstyle标签解析出来,然后重新构建一个Vue组件就可以

当然,从头解析sfc不是很合理,不要重复造轮子,最简单的做法是使用@vue/compile-sfc,先看看在node环境下如何实现。

const compiler = require("@vue/compiler-sfc");

const sourceCode = `
<template>
  <div>
    <h1>{{ msg }}</h1>
  </div>
</template>
<script>
export default {
  name: "test",
  el: "#app",
  data() {
    return {
      msg: "Hello Vue",
    };
  },
};
<\/script>
<style scoped lang="scss">
h1 {
  color: red;
  span {
      color:blue;
  }
}
</style>
`;

const ans = compiler.parse(sourceCode, {sourceMap:false});

const { template, script, styles } = ans.descriptor;

3.1. 编译script和template

其中template和script都主要稍作修改就行,即将template中的内容添加到script组件的template配置项

类似于下面的样子

// 拼接一个浏览器可以运行的组件
const output = `
const config = ${script.content.replace(/export\s+default/, "")}
new Vue({
    el:"#app",
    template: \`${template.content}\`,
    ...config
})
`;

如果需要考虑运行环境,需要再加一个babel编译成ES5,甚至还可以加uglyJS

jsResult = babel.transformSync(jsResult, {
    presets: ["@babel/preset-env"],
  }).code;

除了template配置项这一种方式外,还可以直接将templte编译成render方法,这里先不展开了。

3.2. 编译styles

如果是单纯的css内容,直接创建一个style标签,将文件中的内容作为textContent赋值即可。但由于SFC的style标签通过lang='xxx'属性,支持多种CSS预编译语法,下面以lang='scss'为例

[
  {
    type: 'style',
    content: '\nh1 {\n  color: red;\n  span {\n      color:blue;\n  }\n}\n',
    loc: {
      source: '\nh1 {\n  color: red;\n  span {\n      color:blue;\n  }\n}\n',
      start: [Object],
      end: [Object]
    },
    attrs: { scoped: true, lang: 'scss' },
    scoped: true,
    lang: 'scss'
  }
]

对于这段style标签解析出来的scss代码,还需要单独编译一下,这里使用sass

for (const style of styles) {
    console.log(style.content);
    const result = sass.renderSync({ data: style.content });
    console.log(result.css.toString());
}

可以看见scss代码已经转换成css代码了,

// 转换前
h1 {
  color: red;
  span {
      color:blue;
  }
}

// 转换后
h1 {
  color: red;
}
h1 span {
  color: blue;
}

如果需要SFC的scoped等特性,,可以使用@vue/compile-sfc提供的compileStyleAsync

const id = +new Date();

for (const style of styles) {
    compiler
        .compileStyleAsync({
            filename: descriptor.filename,
            id: `data-v-${id}`,
            isProd: false,
            source: style.content,
            scoped: style.scoped,
        })
        .then((res) => {
            console.log(res.code);
        });
}

最终输出结果

h1[data-v-1638798252945] {
  color: red;
}
h1 span[data-v-1638798252945] {
  color: blue;
}

3.3. 使用HTTP接口代替浏览器编译

上面的@vue/compile-sfcbabelsass等模块不能完全用在浏览器环境中,因此可以提供一个HTTP接口,用来将sfc的代码转换成可以直接在浏览器运行的内容

router.post("/transform_vue_sfc", async (ctx) => {
    const { code } = ctx.request.body;
      // 将上面sfc的编译结果返回
    const result = await parse(code);
      ctx.body = {
        code: 200,
        msg: "success",
        data: result,
      };
});

这样就可以在线预览了,

4. 在浏览器端编译

尽管最后看起来跟vue build的结果差别不是很大,但却是我们解析了SFC内容之后手动处理的,只要我们把在浏览器中执行这些逻辑的功能实现就大功告成了。

@vue/compile-sfc的替代品

首先是如何直接在浏览器端解析模板内容,看了下@vue/compile-sfc应该是不支持浏览器端的。由于*.vue文件内容是通过三种标签来区分的,因此可以通过手动即系标签内容来,当然用jQuery看起来也可以

const $data = $(`<div>${sourceCode}</div>`);
const template = $data.find("template").html();
const script = $data.find("script").html();
const styles = $data.find("style").html();
console.log({
  template,
  script,
  styles
});

上面展示了如何使用jQuery快速解析sfc中的内容,当然还需要更完善的处理,比如标签不存在时兼容、多个style、style标签的lang属性等。

@babel/standalone

@babel/standalone是babel在浏览器和其他非nodeJS环境下工作的版本,比如直接在浏览器中运行JSX,就可以直接使用。

sass.js

sass.js提供了在浏览器端编译sass的功能,缺点是这个包的体积太大了(4M多)...

此外可能还需要考虑其他CSS预编译语言的支持。

看起来最省事的还是直接利用@vue/compile-sfc和一系列node工具链完成相关的工作。

5. 小结

回到开头的设想,如果不需要预览,也就不需要在实现在浏览器预览组件的功能,只要在最后保存的时候触发一下打包,生成对应的页面链接就可以了...

但这跟直接在本地开发有什么区别呢?当我敲下这行字的时候,我愣住了...

GG,重新想想我到底要解决什么问题?也许只是为了快速开发、打包和上线,那么直接本地开发环境编辑,然后上传一个.vue文件?试试vite?或者直接用vue serve xxx.vue启动原型开发?

等后面再补充。