# 前端框架的出现

# 前端的飞速发展

如果你也有经历从 jQuery 一把梭的年代,或许你也会惊讶于短短的几年时间,前端竟然经历了这么多的改变。曾经前端大多数代表着切图和重构,重页面样式而轻逻辑。

那是 node.js 刚出现还不稳定的年代,前端常常拼接 JSP 模板、拼 PHP 模板,工作大多数用来调节浏览器兼容。所以 jQuery 的出现,链式调用的编程方式使得代码如行云流水般流畅,同时提供了极容易使用的异步请求 ajax,超方便的 Sizzle 引擎元素选择器,提供的便捷几乎满足了当时前端的大部分工作,所以说 jQuery 一把梭不是毫无道理的。

相信这也是当年被迫写过 HTML/CSS/Javascript 三剑客的各种开发对前端保留的印象:只需要 jQuery 就可以了。当然毫无疑问 jQuery 是个极其优秀的库,但 node.js 的发展、npm 包管理的完善,加上活跃的开源社区,前端已经获得了千千万万开发者的支援,从页面开发到工具库开发、框架开发、脚本开发、到服务端开发,单线程的 Javascript 正在不断进行自我革新,从而将领域不断拓宽,形成了如今你所能看到的、获得赋能的前端。

# 前端工程化

我们在代码中会使用到很多的资源,图片、样式、代码,还有各式各样的依赖包,而打包的时候怎么实现按需分块、处理依赖关系、不包含多余的文件或内容,同时提供开发和生产环境,包括本地调试自动更新到浏览器这些能力,都是由工程化的工具整合起来的。在现在,这样的工具更多的表现在 gulp、webpack 这种工具上。

要实现工程化,前端开发几乎都离不开 node.js 的包管理器 npm,比如前端在搭建本地开发服务以及打包编译前端代码等都会用到。在前端开发过程中,经常用到npm install来安装所需的依赖。

同时,我们可以结合一些 Tree-shaking 的能力,在本地构建的时候,把使用的别人的依赖包里没用到的部分去掉,减小代码包的大小等。

依赖管理工具、自动化工具、代码规范工具、测试工具等等,层出不穷的新工具加快了前端工程化的步伐。而各种框架也各自推出了脚手架,可以让你快速地生成示例代码、搭建本地环境,也可以更新依赖的版本等,避免了每个开发者自行调整开发环境、打包逻辑等配置。

至此,开发者可以专注于业务开发,不再需要花很多事件处理一些低效而重复性的事情,开发效率得到质的提升。

# 前端框架的出现

最初是 AngularJS 开始占领了比较多的地位,后面 React 迎面赶上,在 Angular 断崖升级的帮助下,Vue 结合了各种框架的优势,以及非常容易入门的文档,成功成为了那一匹黑马。

那么这些框架它们都做了什么事情呢?为什么到现在几乎所有的前端项目都脱离不了框架开发呢?首先还是得从浏览器讲起。

# 浏览器如何渲染页面

我们知道一个页面的代码里,主要包括了 HTML、CSS、Javascript 三大块内容,那么浏览器是怎么解析和加载这些内容的呢?

一次浏览器的页面渲染过程中,浏览器会解析三种文件:

  • 解析 HTML/SVG/XHTML,会生成一个 DOM 结构树
  • 解析 CSS,会生成一个 CSS 规则树
  • 解析 JS,可通过 DOM API 和 CSS API 来操作 DOM 结构树和 CSS 规则树

CSS 规则树与 DOM 结构树结合,最终生成一个 Render 树(即最终呈现的页面,例如其中会移除 DOM 结构树中匹配到 CSS 里面display:none的 DOM 节点)。一般来说浏览器绘制页面的过程是:
(1) 计算 CSS 规则树。
(2) 生成 Render 树。
(3) 计算各个节点的大小/position/z-index。
(4) 绘制。

由于篇幅的原因,这里不做过多的说明,大家感兴趣可以去细细阅读下《How browsers work》 (opens new window)这篇文章。

# 页面的局部刷新

一般看来,浏览器生成了最终的 Render 树,页面也已经渲染完毕,似乎浏览器已经完成了它的工作了。但现实中我们的页面更多的不只是静态的页面,还会包括点击、拖拽等事件操作,以及接口请求、数据渲染到页面等动态的交互逻辑,这时候我们会需要更新页面的信息。

我们的业务代码中情况会复杂得多,除了插入内容,还包括内容更新、删除元素节点等。不管是哪种情况,目前来说前端一般分为两种方式:
(1) 字符串模版:使用拼接的方式生成 DOM 字符串,直接通过innerHTML()插入页面。
(2) 节点模版:使用createElement()/appendChild()/textContent等方法,动态地插入 DOM 节点。

代码人当然要用代码来说话啦,假设页面中存在<div id="div"></div>这样一个元素,我们需要插入一些内容如<p>测试<a>test</a></p>

var div = document.getElementById("div");

/** 1. 字符串模版 **/
div.innerHTML = "<p>测试<a>test</a></p>";

/** 2. 节点模版 **/
const p = document.createElement("p");
p.textContent = "测试";
const a = document.createElement("a");
a.textContent = "test";
p.appendChild(a);
div.appendChild(p);

当然这个世界不是非黑即白的,真实应用中可能也会存在两种方式都同时使用,甚至也有夹杂着使用的情况。同时,我们使用 DOM API 和 CSS API 的时候,通常会触发浏览器的两种操作:Repaint(重绘)和 Reflow(回流):

  • Repaint:页面部分重画,通常不涉及尺寸的改变,常见于颜色的变化。
  • Reflow:意味着节点需要重新计算和绘制,常见于尺寸的改变。

在 Reflow 的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成 Reflow 后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为 Repaint。

回流的花销跟 render tree 有多少节点需要重新构建有关系,所以使用innerHTML()可能会导致更多的开销。

# 前端框架做了什么

前端框架为什么变得如此重要,我们来看看在框架出现前,我们是如何和用户进行交互的。以一个常见的表单提交作为例子:

# (1) 编写静态页面。

<form>
  Name:
  <p id="name-value"></p>
  <input type="text" name="name" id="name-input" />
  Email:
  <p id="email-value"></p>
  <input type="email" name="email" id="email-input" />
  <input type="submit" />
</form>

# (2) 给对应的元素绑定对应的事件。

例如给 input 输入框绑定输入事件:

var nameInputEl = document.getElementById("name-input");
var emailInputEl = document.getElementById("email-input");
// 监听输入事件,此时 updateValue 函数未定义
nameInputEl.addEventListener("input", updateNameValue);
emailInputEl.addEventListener("input", updateEmailValue);

# (3) 事件触发时,更新页面内容。

var nameValueEl = document.getElementById("name-value");
var emailValueEl = document.getElementById("email-value");
// 定义 updateValue 函数,用来更新页面内容
function updateNameValue(e) {
  nameValueEl.innerText = e.srcElement.value;
}
function updateEmailValue(e) {
  emailValueEl.innerText = e.srcElement.value;
}

上面这是最简单的例子,结合前面说过的页面更新的方式,如果我们页面中有很多的内容需要更新,光拼接字符串我们可能就有一大堆代码。以下的例子,为了不占用大量的篇幅,使用了 jQuery,否则代码量会更多。

例如我们新增一个卡片,卡片内需要填写一些内容:

var index = 0;
// 用来新增一个卡片,卡片内需要填写一些内容
function addCard() {
  // 获取一个id为the-dom的元素
  var body = $("#the-dom");
  // 从该元素内获取class为the-class的元素
  var addDom = body.find(".the-class");
  // 在the-class元素前方插入一个div
  addDom.before('<div class="col-lg-4" data-index="' + index + '"></div>');
  // 同时保存下来该DOM节点,方便更新内容
  var theDom = body.find('[data-index="' + index + '"]');
  theDom.innerHTML(
    `<input type="text" class="form-control question" placeholder="你的问题">
         <input type="text" class="form-control option-a" placeholder="回答1">
         <input type="text" class="form-control option-b" placeholder="回答2">
        `
  );
  // 做完上面这堆之后index自增
  index++;
  return theDom;
}

而当我们需要对输入框进行某些字数限制的时候:

// theDom使用上面代码保存下来的引用哈
// 问题绑定值
theDom
  .on("keyup", ".question", function(ev) {
    ev.target.value = ev.target.value.substr(0, 20);
  })
  // 答案a绑定值
  .on("keyup", ".option-a", function(ev) {
    ev.target.value = ev.target.value.substr(0, 10);
  })
  // 答案b绑定值
  .on("keyup", ".option-b", function(ev) {
    ev.target.value = ev.target.value.substr(0, 10);
  });

而当我们需要获取某些输入框内容的时候:

// 获取卡片的输入值
// theDom使用上面代码保存下来的引用哈
function getCardValue(index) {
  var body = $("#the-dom");
  var theDom = body.find('[data-index="' + index + '"]');
  var questionName = theDom.find(".question").val();
  var optionA = theDom.find(".option-a").val();
  var optionB = theDom.find(".option-b").val();
  return { questionName, optionA, optionB };
}

而当我们使用 Vue 的时候,我们可以这么写:

<template>
  <div v-for="card in cards">
    <input
      type="text"
      class="form-control question"
      v-model="card.questionName"
      placeholder="你的问题"
    />
    <input
      type="text"
      class="form-control option-a"
      v-model="card.optionA"
      placeholder="回答1"
    />
    <input
      type="text"
      class="form-control option-b"
      v-model="card.optionB"
      placeholder="回答2"
    />
  </div>
</template>

<script>
  export default {
    name: "Cards",
    data() {
      return {
        cards: []
      };
    },
    methods: {
      // 添加一个卡片
      addCard() {
        this.cards.push({
          questionName: "",
          optionA: "",
          optionB: ""
        });
      },
      // 获取卡片的输入值
      getCardValue(index) {
        return this.cards[index];
      }
    }
  };
</script>

数据绑定、界面更新、事件监听等都用最简单的方式提供给开发者,开发效率和代码可维护性也是直线上升。前端框架中很重要的一个功能——模板引擎,通过 AST 它能做到的事情还包括:

  • 排除无效 DOM 元素,并在构建过程可进行报错
  • 使用自定义组件的时候,可匹配出来
  • 可方便地实现数据绑定、事件绑定等,具备自动更新页面的功能
  • 为虚拟 DOM Diff 过程打下铺垫
  • HTML 转义(预防 XSS 漏洞)

配合前面提到的前端工程化,前端的开发模式有很大的转变。我们来看看,Vue 框架具体做了什么,我们要怎么快速入门和上手,有哪些的方式可以高效提升开发效率、避免一些容易发生的问题,或者是脑洞大开的一些编码方式,都可以从这本书里看到,希望你们会喜欢。