前言
一个Web前端框架最先被使用者感知的大概就是虚拟DOM的标签和语法了,为什么叫虚拟DOM呢,因为通常基于Web前端框架编写的DOM包含很多框架自定义的标签,这些标签能够极大简化DOM的结构,但并不能被浏览器有效处理,这些DOM结构只有经过框架引擎翻译渲染后才能被转换为真正的DOM结构并被浏览器识别。
那么jm-hcj实质设计的第一步,就是设计虚拟DOM标签了。对于这个问题,一直以来我都有一个疯狂的想法,我希望能够像写其它编程语言代码一样,让HTML编写变得有逻辑,而不仅仅是结构定义,甚至,让HTML标签更多的替代JS。例如让HTML标签能够在一定的作用域中定义变量、加载数据甚至进行基本的数据加工。
那么,就把这个想法在jm-hcj上实现吧。
一、总体构想
1.1 DOM树的语法构成
jm-hcj的虚拟DOM部分实际应该分成两个部分,分别是 基于HTML扩展的特殊含义标签 和 标签相关的特殊含义属性。
对于框架而言,无论是特殊含义的标签还是属性,都应当支持持续扩展,而不应当封闭。我们把jm-hcj默认定义的特殊含义标签称为语法标签,把特殊含义属性称为语法属性。语法标签和语法属性应是两个彼此独立的集合,语法标签可以配套定义自身的属性,但这些配套定义的属性应视为语法标签的组成部分,而不是语法属性,而语法属性应当可以广泛应用于绝大部分标签,并具备独立功能。
语法标签和语法属性共同构成了虚拟DOM树的语法(实际叫语法不是很严谨,但为了方便就这么叫吧)。虚拟DOM树的语法应尽可能符合HTML编写规范,这样就能够方便的在各类IDE中开发,且能够被正确的着色。
1.2 基于虚拟DOM树的变量作用域
1.2.1 数据交互是前端一直的痛
对于一个熟练的前端开发人员来说,开发页面最麻烦的部分就是与后端的数据交互,“对接口”头疼的原因是需要不断的处理加载时序、与插值对应等问题。
传统的虚拟DOM树的渲染过程可以简单描述为通过给定的初始数据(data),对DOM模板(一个包含HTML定义的字符串)中的插值进行替换,如果在渲染过程中需要对data的内容进行增补或修改,需要在对应的JS钩子方法中完成。
例如,假设在某网站首页加载时,需要分三步完成个人信息数据的展示:
1.通过 /api/account/summary 接口加载当前登录用户信息,用户信息内包含用户的uid,在页面上显示用户姓名;
接口返回数据示例:
{
uid : "xxxx-xxxx",
name : "张三"
}
2.以uid为参数,通过接口 /api/vip/level?uid= 获取用户的VIP等级信息,展示VIP等级;
接口返回数据示例:
{
level : 3,
expiringTime : "2024-12-31"
}
3. 根据用户uid和vip等级,通过/api/content/individual 拉取面向该用户的推荐内容,并在页面上以列表形式展示。
接口返回数据示例:
[
{"title" : "推荐内容1", "url" : "xxx.com/article/100001"},
{"title" : "推荐内容2", "url" : "xxx.com/article/100002"},
{"title" : "推荐内容3", "url" : "xxx.com/article/100003"},
]
针对这种需求,相当多的Vue开发者会给出两种解决方案,一是定义三个组件,逐层嵌套,在每个组件的created()钩子中完成一次接口数据拉取,把结果传递给子组件,每个组件分别渲染自身的内容;二是定义一个组件,在created中依次调用三个API,并且把三个结果全部放入data中,一次性完成渲染。例如:
<!-- Vue实现示例 -->
<template>
<div class="individual">
<span class="name">{
{userName}}</span>
<span class="vip">VIP(Lv.{
{vipLevel}})</span>
</div>
<div class="articles">
<ol>
<li v-for="article in articles">
<a href="{
{article.url}}">{
{article.title}}</a>
</li>
</ol>
</div>
</template>
<script>
// 简化代码,仅用于示例
export default {
data(){return {}},
mounted(){
this.fetchData();
},
methods:{
async fetchData() {
let individualSummary = await axios.get("/api/individual/summary");
this.name = individualSummary.name;
let uid = individualSummary.uid;
let vipLevel = await axios.get("/api/vip/level?uid=" + uid);
this.vipLevel = vipLevel.level;
let url = "/api/content/indi