[内部]前端技术浅谈

前端技术浅谈

content.png

1、前端框架发展

前端技术演进.png

以铜为鉴,可以正衣冠;以人为鉴,可以明得失;以史为鉴,可以知兴替。

本文主题:前端开发如何借助对HTMLJavascript不同的使用方式,来一步步演进前端开发框架,从而优化前端开发过程。

想要全面了解前端技术发展,最好的方法就是向后看,看一路走来前端开发是如何从服务端主导的静态网站一步步发展到现在由客户端主导的单页应用。只有了解了过去前端分别在不同的阶段解决了怎样的问题,才能更好地看清楚未来要向哪里去。所以,我们先简单的来看看企业信息化产品在前端技术发展过程中的几个重要迭代过程。

1.1、静态页面

1991年,Tim作为布道者在Internet上广泛推广Web的理念,与此同时,美国国家超算应用中心对此表现出了浓厚的兴趣,并开发了名为Mosaic的浏览器,于1993年4月进行了发布。此时的网页以HTML为主,是纯静态的网页,网页是“只读”的,信息流只能通过服务器到客户端单向流通,由此世界进入了Web 1.0时代。

90年代,流行的一些新闻网站、公司官方宣传网页,都属于这个范畴。

解决问题:信息的展示和传播过程。

1.2、动态页面

静态页面因为内容是固定的,不能读取后台数据库中的数据,为了使得Web内容更加灵活,以PHPJSPASP.NET为代表的动态页面技术相继诞生。

随着动态页面技术的不断发展,后台代码变得庞大臃肿,后端逻辑也越来越复杂,逐渐难以维护,此时,后端的各种MVC框架逐渐发展起来,以JSP为例,StrutsSpring MVC等框架层出不穷,这些框架把请求处理逻辑,分层了多层,比如典型的三层结构,就起源于此。

JSP(JavaServer Pages)原理

jsp.png

JSP其实就是HTML+Java语言组成的一个html模板文件,通过JSP Engine 把它最终转换为HTML页面。

公司内部产品:

软件公司SIP平台,基于ASP.NET 开发,采用aspx模板语法,进行动态页面渲染。并且还采用流行一时的前端服务器组件:Express Developer.Net

技术公司Supplant 4.x 产品,基于Struts,采用freemark模板,进行动态页面渲染。

// 现在比较流行的spring 前端模板 Thymeleaf 渲染一个用户信息块
<h2>
    <p>Name: <span th:text="${user.name}">Jack</span>.</p>
    <p>Age: <span th:text="${user.age}">21</span>.</p>
    <p>friend: <span th:text="${user.friend.name}">Rose</span>.</p>
</h2>

从Web诞生至2005年,一直处于后端重、前端轻的状态。这个阶段,浏览器只是负责渲染页面,具体页面的一些逻辑大部分都是在后端执行,包括按钮点击,下拉框数据改变等等前端交互事件,甚至在ASP.NET WebForm框架还有服务器端控件一说。

解决的问题:实现html内容动态拼装的过程,可以让服务器端的数据动态的生成到页面,同时采用页面模板,可以复用很多公共页面逻辑。web产品更加灵活了,涉足的领域也更加广。

1.3、AJAX

在Web最初发展的阶段(动态页面),因为页面内容和页面交互事件都依托于服务器端,前端页面要想获取后台信息需要刷新整个页面,这是很糟糕的用户体验。

Google分别在2004年和2005年先后发布了两款重量级的Web产品:GmailGoogle Map。这两款Web产品都大量使用AJAX技术,不需要刷新页面就可以使得前端和服务器进行网络通信,这虽然在当今看来是理所应当的,但是在十几年前AJAX却是一项革命性的技术,颠覆了用户体验。

Asynchronous Javascript And XML

浏览器BOM实现原理 XMLHttpRequst API展示:

function sendAjax() {
  //构造表单数据
  var formData = new FormData();
  formData.append('username', 'tom');
  formData.append('id', 123456);
  
  //创建xhr对象 
  var xhr = new XMLHttpRequest();
  //设置xhr请求的超时时间
  xhr.timeout = 3000;
  //设置响应返回的数据格式
  xhr.responseType = "text";
  //创建一个 post 请求,采用异步
  xhr.open('POST', '/server', true);
  //注册相关事件回调处理函数
  xhr.onload = function(e) { 
    if(this.status == 200||this.status == 304){
        alert(this.responseText);
    }
  };
  xhr.ontimeout = function(e) { ... };
  xhr.onerror = function(e) { ... };
  xhr.upload.onprogress = function(e) { ... };
  
  //发送数据
  xhr.send(formData);
}

AJAX使得浏览器客户端可以更方便地向服务器发送数据信息,这促进了Web 2.0的发展。

但是有个问题,各个浏览器对AjaxDom操作有差异性。为了解决浏览器兼容性问题,DojojQueryYUIExtJS等前端Framework相继诞生。前端开发人员用这些Framework频繁发送AJAX请求到后台,在得到数据后,再用这些Framework更新DOM树。

前端库时代.png

Jquery Ajax的封装:

 $.ajax({
     //请求方式
     type : "POST",
     //请求的媒体类型
     contentType: "application/json;charset=UTF-8",
     //请求地址
     url : "http://127.0.0.1/admin/list/",
     //数据,json字符串
     data : JSON.stringify(list),
     //请求成功
     success : function(result) {
         console.log(result);
     },
     //请求失败,包含具体的错误信息
     error : function(e){
         console.log(e.status);
         console.log(e.responseText);
     }
 });

于此同时,服务器框架也慢慢的由处理对html页面的请求(Jsp+servlet)和模板渲染,转变为对前端Ajax请求的处理(Spring MVC \ Asp.NET MVC),前后端一般通过xml或者Json格式进行数据传输交互。当然后期,Json已经成为前后端数据格式的标准。

解决问题:Ajax 解决了动态页面获取内容或者提交数据都需要整页刷新的问题,可以让页面局部更新,异步提交数据,提升了Web的系统用户体验。同时后端也出现了,纯粹处理数据的web服务技术来支撑异步请求的过程。另外,页面的展示和页面的数据也开始慢慢有种分离的趋势。

公司内部产品:

软件公司SIP4.X产品,大量使用Jquery\Easyui等前端组件,并且服务器端也从Asp.Net升级为Asp.Net MVC。每个页面都是纯粹的HTML+Javascript, 开发方式也慢慢的前后端分离。

1.4、MV* 架构

伴随着Ajax出现和流行,大量的交互逻辑都从后端移到了前端,前端的代码越来越多,于此同时,javascript语言本身也重新开始迭代。

1.4.1、ES6的发展

2015年6月,ECMAScript 6.0发布。该版本增加了许多新的语法,包括支持let、const、Arrow function、Class、Module、Promise、Iterator、Generator、Set、Map、async、Symbol、Proxy、Reflect、Decorator等。这些语法使得前端开发人员,可以不用Jquery这种第三方的框架,也能够很优雅的编写前端代码。

ECMAScript发展很快,以至于许多浏览器都只能支持部分ES6中的新特性。随之,BabelTypeScript逐渐流行起来,编写ES6代码,然后用Babel或TypeScript将其编译为ES5等浏览器支持的JavaScript。

同时随着HTML5的流行,前端不再是人们眼中的小玩意,以前在C/S中实现的桌面软件的功能逐步迁移到了前端,前端的代码逻辑逐渐变得复杂起来。解决办法,和后端代码一样:分层。当然,分层一般都是基于比较通用的一些设计模式,比如MVC、MVP、MVVM。伴随着,分层的出现,一些现在流行的框架也同时出现了:

MVVM.png

随着这些MV*框架的出现,网页逐渐由Web Site演变成了Web App,最终导致了复杂的单页应用( Single Page Application)的出现。同时也伴随出现了类似前端路由虚拟DOM等一些前端概念。

三剑客.jpg

1.4.2、Node.js发展

一个基于 Chrome V8 引擎的 JavaScript 运行时

Node.js的发展,使得Javascript不在局限于浏览器端运行,服务器软件、桌面软件到看到了Javascript的身影。当然对前端领域而言,Node.js的产生使得前端开发基于Javascript实现工程化得到了可能。后文会详细介绍。

解决问题:前端代码复杂后,Dom操作变的频繁,需要用更合理、更优雅的方式来完成这个过程,分层和用更高层次的设计模式来组织,由此大量前端MV*框架出现。

1.5、移动应用兴起

随着iOSAndroid等智能手机的广泛使用,移动浏览器也逐步加强了对HTML5特性的支持力度。

移动浏览器的发展,导致了流量入口逐渐从PC分流到移动平台,这是Web发展的新机遇。移动Web面临着更大的碎片化和兼容性问题,jQuery Mobile、Sencha Touch、Framework7、Ionic等移动Web框架也随之出现。

相比于原生应用(Native App),移动Web开发成本低、跨平台、发布周期短的优势愈发明显,但是原生应用(Native App)的性能和UI体验要远胜于移动Web。移动Web与Native App孰优孰劣的争论愈演愈烈,在无数开发者的实践中,人们发现两者不是替代关系,而是应该将两者结合起来,取长补短,Hybrid技术逐渐得到认同。

根据实现原理,Hybrid技术可以分为两大类:

  • HTML5的代码放到Native App的WebView(可以理解为移动端轻量浏览器)组件中运行,WebView为Web提供宿主环境,JavaScript代码通过WebView调用Native API。

    最新公司上线一个沙特健康码项目,就是一个混合的App。其优点就是改动H5部分的功能,App不需要重新安装。

    技术公司信息化产品e-mobile其实也是个混合App,比如我们之前的每日健康申报。

健康码.jpg
  • HTML5代码针对不同平台编译成不同的原生应用,实现了Web开发,Native部署。这一类的典型代表有React Native

    React Native : 使用JavaScript和React编写原生移动应用。

解决问题:移动端的兴趣,让H5在移动端发展迅速,由此也让混合应用成为了趋势。混合应用解决了更新应用内容,但是App不需要重新下载更新的问题,同时也因为JS的跨平台特性,简化了移动应用的开发过程。

2、主流框架和技术介绍

目前国内,前端单页应用开发相对流行的Javascript框架主要有两个(Jquery不算的话):VueReact,他们核心都是解决一个数据视图之间关联的问题。

一般我们进行表单开发过程中,往往需要经历两个过程:

1)、页面加载完成后,脚本向服务器异步请求数据,数据返回后,通过脚本给表单设置初始值。

2)、表单操作完成后,脚本获取表单的值,组织格式后,向后台服务器提交数据。如下图:

表单常用过程.png

Jquery时代,其为我们提供了操作DOM元素的常用API,帮我们封装了提交和获取数据的Ajax请求API,使得我们可以轻松实现上面介绍的两个过程。

<input type="submit" value="Jquery" id = "id"/>

//页面加载请求数据并赋值
$(document).ready(function(){
    // $.ajax("Get")
    $("#id").val("Hello Jquery")
});
//页面操作完成,收集数据,并提交
$("button").click(function(){
    var id = $("#id").val();
    // $.ajax("Post","/save",{id:id})
});

但是这种DOM操作往往过于繁琐,每个页面元素都需要进行赋值、收集数据等等一些列重复的过程。伴随着框架设计模式的引入,出现了类似MVVM这样的框架,比如Vue,就是从更高层次解决数据视图双向同步更新的过程。

解决问题:主流前端架构都在解决一个核心问题:数据和视图的关联。

2.1、Vue 的MVVM

vue-mvvm.png
// View
<template>
    <div id="app">
      <p>{{ message }}</p>
      <input v-model="message">
    </div>
</template>

<script>
// Model
var data = {
    message:"Hello Vue"
}  
// ModelAndView
var app = new Vue({
  el: '#app',
  data: data,
  methods:{
      getData(){
          //请求数据
          this.message = 'Hello World'
      },
      saveDate(){
          //发送数据 this.message 已经是最新值
      }
  },
  mounted(){
      this.getData()
  }
})
</script>

<style>
    #app {
      padding: 0px;
      margin: 0px;
    }
</style>

Vue底层帮我们实现了数据视图的双重绑定,大概过程:

监听数据更新视图:

//遍历数据的每一项,设置数据监听
Object.keys(data).forEach(function (key) {
    defineReactive(vm, key, data[key]);
});

function defineReactive(obj, key, val) {
  // 内部监听器
  var dep = new Dep();
  Object.defineProperty(obj, key, {
    get: function () {
      if (Dep.target) dep.addSub(Dep.target);
      return val;
    },
    set: function (newVal) {
      if (newVal === val) return;
      val = newVal;
      // 触发数据更新通知,重新根据模板的渲染页面(过程相对复杂,代码略)
      dep.notify();
    },
  });
}

操作视图更新数据:

SomeInput.addEventListener("input", function(event) {
     vm["SomeInput"] = event.target.value;
})

当然底层比这个复杂很多,会采用一些观察者模式等设计模式,而且Vue 将模板编译成虚拟 DOM 渲染函数Render。并且通过算法能够智能地计算出最少需要重新渲染多少组件,并把 DOM 操作次数减到最少。

2.2、React 的MVC

React 同样也是来解决数据视图之间关联的框架,不过其并不像Vue一样采用双向的数据绑定,而只处理数据到视图的更新,也就是单向过程。它的处理更加简洁,所有数据的更新统一走一个API:setState

// root 容器
<body>
    <div id="root"></div>
</body>

// react 
import React, { PureComponent } from 'react'
import { render } from "react-dom";

class SimpleDemo extends PureComponent {
    constructor(props) {
        super(props)
        this.state = {
            message:'react'
        }
    }
    
    componentDidMount(){
        //通过ajax请求获取message数据
        this.setState({message:'hello'})
    }

    onChangeHandle(event){
        this.setState({message:event.target.value})
    }
    // 每次 state更新,从新执行该渲染函数
    render() {
        return (
            <div>
                <span>{this.state.message}</span>
                <input onChange={this.onChangeHandle} type='text' ></input>
            </div>
        )
    }
}

render(<SimpleDemo />, document.getElementById("root"));

数据和视图的变更过程:

React.png

2.3、多页和单页应用 Spa&Mpa

2.3.1、MPA多页面

多页面应用:每次页面跳转,后台都会返回一个新的html文档,就是多页面应用。
在以往传统开发的应用(网站)大多都是多页面应用,路由由后端来写。


MPA.jpg

首屏时间快?访问页面,服务器只需要返回一个html文件,这个过程就经历了一个HTTP请求,请求响应回来,页面就能被展示出来。

SEO(搜索引擎排名)效果好?搜索引擎能识别html的内容,根据内容进行排名。

页面切换慢:每一次切换页面都需要发起一个HTTP请求,假设网络较慢就会出现卡顿情况。

2.3.2、SPA单页面

单页应用:用一个页面搞定整个应用的所有功能。刷新页面会请求一个html文件,切换页面的时候,并不会发起新的请求一个html文件,只是页面内容发生了变化。

路由原理:JS监听URL变化,当URL发生变化后,使用JS动态把当前的页面内容清除掉,再把下一个页面的内容挂载到页面上。此时的路由就不是后端来做了,而是前端来做,判断页面到底显示哪一个组件,再把以前的组件清除掉使用新的组件。就不会每一次跳转都请求html文件。


SPA.jpg

首屏时间慢?请求html还有js的请求。js的内容相对比多页应用多,页面元素由前端脚本动态组装。
页面切换快?页面跳转不需要去做HTML文件的请求,节约HTTP请求发送的时延。
SEO差?搜索引擎只认识HTML内容不认识js内容。单页应用的渲染都是靠JavaScript渲染出来的。搜索引擎不好识别排名。

公司未来平台产品Supfusion 一个模块是一个单页应用

解决问题:单页应用把前端开发过程组织的更像是一个桌面软件开发,因为对于后端来说,就是一个html页面,内部所有的功能都是通过前端来组织,在这个时候,广义上的一个页面,对于单页应用来说就是一个组件,而之前页面的概念对于用户来说,最好是通过浏览器的地址来区分,于是乎,前端的静态路由的概念出现了,核心还是根据URL来区分如何加载前端组件。

示例:公司邮箱展示 Link

2.4、静态路由

在现代前端开发中,路由是非常重要的一环。但路由到底是什么呢?有些说:路由就是指随着浏览器地址栏的变化,展示给用户的页面也变化的过程。这是从路由的用途上来解释路由是什么的,还有一种说法是:路由就是URL到函数的映射。这是从路由的实现原理上来解释路由是什么的。这两种说法都很有道理,但我个人认为还是第二种比较切合自己对路由的理解吧。

而路由本身也经历了不同的发展阶段:

  • 后端路由

  • 前端路由

后端路由又可称之为服务器端路由,因为对于服务器来说,当接收到客户端发来的HTTP请求,就会根据所请求的相应URL,来找到相应的映射函数(MVC部分的Controller),然后执行该函数,并将函数的返回值(html/json/xml)发送给客户端。对于最简单的静态资源服务器,可以认为,所有URL的映射函数就是一个文件读取操作。对于动态资源,映射函数可能是一个数据库读取操作,也可能是进行一些数据的处理,等等。然后根据这些读取的数据,在服务器端就使用相应的模板来对页面进行渲染后,再返回渲染完毕的页面。

SPA的出现,也伴随着前端路由的出现。对于前端路由来说,路由的映射函数通常是进行一些DOM的显示和隐藏操作。这样,当访问不同的路径的时候,会显示不同的页面组件。前端路由主要有以下两种实现方案:

  • hash
  • history

我们常用的诸如react-router等前端框架的路由控制都是基于前端路由进行开发的,因此将前端路由进行一个了解还是很有必要的。下面就两种方式,分别介绍下基本的原理。

2.4.1、hash 的历史

最开始的网页是多页面的,后来出现了 Ajax 之后,才慢慢有了 SPA。然而,那时候的 SPA 有两个弊端:

  • 用户在使用的过程中,url 不会发生任何改变。当用户操作了几步之后,一不小心刷新了页面,又会回到最开始的状态。那个年代这种仅仅靠Ajax更新内容的方式叫局部刷新
  • 由于缺乏 url,不方便搜索引擎进行收录。怎么办呢? hash

https://developer.mozilla.org/en-US/docs/Web/API/URL/href#Examples

通过哈希字符串:#Exaples ,定位到页面具体元素。

url 上的 hash 本意是用来作锚点的,方便用户在一个很长的文档里进行上下的导航,用来做 SPA 的路由控制并非它的本意。然而,hash 满足这么一种特性:改变 url 的同时,不刷新页面,再加上浏览器也提供 onhashchange 这样的事件监听,因此,hash 能用来做路由控制。后来,这种模式大行其道,onhashchange 也就被写进了 HTML5 规范当中去了。

下面举个例子,演示“通过改变 hash 值,对页面进行局部刷新”:

<ul>
    <li><a href="#/">turn white</a></li>
    <li><a href="#/blue">turn blue</a></li>
    <li><a href="#/green">turn green</a></li>
</ul>
function Router() {
    this.routes = {};
    this.currentUrl = '';
}
Router.prototype.route = function (path, callback) {
    this.routes[path] = callback || function () {};
};
Router.prototype.refresh = function () {
    console.log('触发一次 hashchange,hash 值为', location.hash);
    this.currentUrl = location.hash.slice(1) || '/';
    this.routes[this.currentUrl]();
};
Router.prototype.init = function () {
    window.addEventListener('load', this.refresh.bind(this), false);
    window.addEventListener('hashchange', this.refresh.bind(this), false);
};

window.Router = new Router();
window.Router.init();
var content = document.querySelector('body');
// change Page anything
function changeBgColor(color) {
    content.style.backgroundColor = color;
}
Router.route('/', function () {
    changeBgColor('white');
});
Router.route('/blue', function () {
    changeBgColor('blue');
});
Router.route('/green', function () {
    changeBgColor('green');
});

hash-demo.gif

通过 hash 的改变来对页面进行局部刷新。尤其需要注意的是:在第一次进入页面的时候,如果 url 上已经带有 hash,那么也会触发一次onhashchange 事件,这保证了一开始的 hash 就能被识别。

问题:虽然 hash 解决了 SPA 路由控制的问题,但是它又引入了新的问题 :url 上会有一个 # 号,很不美观,解决方案:抛弃 hash,使用 history

2.4.2、history 的演进

很早以前,浏览器便实现了 history。然而,早期的 history 只能用于多页面进行跳转,比如:

// 这部分可参考红宝书 P215
history.go(-1);       // 后退一页
history.go(2);        // 前进两页
history.forward();    // 前进一页
history.back();       // 后退一页

HTML5 规范中,history 新增了以下几个 API

history.pushState();         // 添加新的状态到历史状态栈
history.replaceState();     // 用新的状态代替当前状态
history.state             // 返回当前状态对象

通过history.pushState或者history.replaceState,也能做到:改变 url 的同时,不会刷新页面。所以 history 也具备实现路由控制的潜力。然而,还缺一点:hash 的改变会触发 onhashchange 事件,history 的改变会触发什么事件呢很遗憾,没有
怎么办呢?虽然我们无法监听到 history 的改变事件,然而,如果我们能罗列出所有可能改变 history 的途径,然后在这些途径一一进行拦截,不也一样相当于监听了 history 的改变吗?
对于一个应用而言,url 的改变只能由以下 3 种途径引起:

  • 1、点击浏览器的前进或者后退按钮;

  • 2、点击 a 标签;

  • 3、在 JS 代码中直接修改路由。

第 2 和第 3 种途径可以看成是一种,因为 a 标签的默认事件可以被禁止,进而调用 JS 方法。关键是第 1 种,HTML5 规范中新增了一个 onpopstate 事件,通过它便可以监听到前进或者后退按钮的点击。
要特别注意的是:调用history.pushStatehistory.replaceState并不会触发 onpopstate 事件。

window.history.pushState(state, title, url) 

window.history.replaceState(state, title, url)
// 与 pushState 基本相同,但她是修改当前历史记录,而 pushState 是创建新的历史记录

window.addEventListener("popstate", function() {
    // 监听浏览器前进后退事件,pushState 与 replaceState 方法不会触发              
});

window.history.back() // 后退
window.history.forward() // 前进
window.history.go(1) // 前进一步,-2为后退两步,window.history.lengthk可以查看当前历史堆栈中页面的数量

总结:经过上面的分析,history 是可以用来进行路由控制的,只不过需要从 3 方面进行着手。当然后端需要支持,任何前端的404请求都引导index.html的机制,因为前端无法控制用户手工刷新当前页面。

React路由简单示例
// 开源Route项目截取的实现逻辑
let instances = [];  // 用来存储页面中的 Router
const register = (comp) => instances.push(comp);
const unRegister = (comp) => instances.splice(instances.indexOf(comp), 1);

const historyPush = (path) => {
    window.history.pushState({}, null, path);
    instances.forEach(instance => instance.forceUpdate())
};

window.addEventListener('popstate', () => {
    // 遍历所有 Route,强制重新渲染所有 Route
    instances.forEach(instance => instance.forceUpdate());
});

export class Route extends Component {
    static propTypes = {
        path: PropTypes.string,
        component: PropTypes.func,
        exact: PropTypes.bool
    };

    componentWillMount() {register(this);}

    render() {
        const {path, component, exact} = this.props;
        const match = matchPath(window.location.pathname, {path, exact});

        // Route 跟当前 url 不匹配,就返回 null
        if (!match) return null;

        if (component) {
            return React.createElement(component);
        }
    }

    componentWillUnMount() {unRegister(this);}
}
React路由.png

总结:前端路由解决了单页应用内部页面的跳转问题,同时有组织了路由和组件的关联关系。

2.5、虚拟DOM

VueReact的数据更新到视图更新,两个框架都采用了虚拟Dom这样一种机制,主要为了差异化更新真实Dom,避免因为DOM更新过多或者频繁引起页面渲染性能问题。

“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”

“Any problem in computer science can be solved by anther layer of indirection.”

一个使用虚拟Dom的简单场景,如果用jquery展示一个表格:

<div id="container"></div>
<button id="btn-change">change</button>
<script type="text/javascript" src="./jquery.js"></script>
<script type="text/javascript">
    var data = [
        {
            name: '张三',
            age: '20',
            address: '北京'
        },
        {
            name: '李四',
            age: '21',
            address: '上海'
        },
        {
            name: '王五',
            age: '22',
            address: '广州'
        }
    ]

    // 渲染函数
    function render(data) {
        var $container = $('#container')

        // 清空容器,重要!!!
        $container.html('')

        // 拼接 table
        var $table = $('<table>')

        $table.append($('<tr><td>name</td><td>age</td><td>address</td>/tr>'))
        data.forEach(function (item) {
            $table.append($('<tr><td>' + item.name + '</td><td>' + item.age + '</td><td>' + item.address + '</td>/tr>'))
        })

        // 渲染到页面
        $container.append($table)
    }

    $('#btn-change').click(function () {
        data[1].age = 30
        data[2].address = '深圳'
        // re-render  再次渲染,整个表格会全部重新渲染
        render(data)
    })

    // 页面加载完立刻执行(初次渲染)
    render(data)
</script>

如果使用虚拟DOM:

// VDom 的 snabbdom库

var vnode = h('ul#list',{},[
    h('li.item',{},'item1'),
    h('li.item',{},'item2')
])
var container = document.getElementById('container');
patch(container,vnode);

//模拟改变

var btnChange = document.getElementById('btnChange');
btnChange.addEventListener('click',function(){
    var newVnode = h('ul#list',{},[
        h('li.item',{},'item1'),
        h('li.item',{},'item222')
    ]) 
    patch(vnode,newVnode); //vnode和newVnode对比,仅仅更新需要更新的部分
})

2.5.1、Vue虚拟DOM原理

  • Vue.js通过编译将template 模板转换成渲染函数(render ) ,执行渲染函数就可以得到一个虚拟节点树。

  • 在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来修改视图。这个过程主要是将新旧虚拟节点进行差异对比,然后根据对比结果进行DOM操作来更新视图。

简单点讲,在Vue的底层实现上,Vue将模板编译成虚拟DOM渲染函数。结合Vue自带的响应系统,在状态改变时,Vue能够智能地计算出重新渲染组件的最小代价并应到DOM操作上。

vDom.png

上图几个概念加以解释:

渲染函数:渲染函数是用来生成Virtual DOM的。Vue推荐使用模板来构建我们的应用界面,在底层实现中Vue会将模板编译成渲染函数,当然我们也可以不写模板,直接写渲染函数,以获得更好的控制。

VNode 虚拟节点:它可以代表一个真实的 DOM节点。通过 createElement 方法能将 VNode 渲染成 DOM节点。简单地说,vnode可以理解成节点描述对象,它描述了应该怎样去创建真实的DOM节点。

patch(也叫做patching算法):虚拟DOM最核心的部分,它可以将vnode渲染成真实的DOM,这个过程是对比新旧虚拟节点之间有哪些不同,然后根据对比结果找出需要更新的的节点进行更新。这点我们从单词含义就可以看出, patch本身就有补丁、修补的意思,其实际作用是在现有DOM上进行修改来实现更新视图的目的。Vue的Virtual DOM Patching算法是基于Snabbdom的实现,并在些基础上作了很多的调整和改进。

Virtual DOM 是什么?

Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。

简单来说,可以把Virtual DOM 理解为一个简单的JS对象,并且最少包含标签名( tag)、属性(attrs)和子元素对象( children)三个属性。不同的框架对这三个属性的命名会有点差别。

对于虚拟DOM,咱们来看一个简单的实例,就是下图所示的这个,详细的阐述了模板 → 渲染函数 → 虚拟DOM树 → 真实DOM的一个过程:

vdom2.png
Virtual DOM 作用是什么?

虚拟DOM的最终目标是将虚拟节点渲染到视图上。但是如果直接使用虚拟节点覆盖旧节点的话,会有很多不必要的DOM操作。例如,一个ul标签下很多个li标签,其中只有一个li有变化,这种情况下如果使用新的ul去替代旧的ul,因为这些不必要的DOM操作而造成了性能上的浪费。

为了避免不必要的DOM操作,虚拟DOM在虚拟节点映射到视图的过程中,将虚拟节点与上一次渲染视图所使用的旧虚拟节点(oldVnode)做对比,找出真正需要更新的节点来进行DOM操作,从而避免操作其他无需改动的DOM。

其实虚拟DOM在Vue.js主要做了两件事:

  • 提供与真实DOM节点所对应的虚拟节点vnode

  • 将虚拟节点vnode和旧虚拟节点oldVnode进行对比,然后更新视图

为何需要Virtual DOM?
  • 具备跨平台的优势
    由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。

  • 操作 DOM 慢,js运行效率高。我们可以将DOM对比操作放在JS层,提高效率。
    因为DOM操作的执行速度远不如Javascript的运算速度快,因此,把大量的DOM操作搬运到Javascript中,运用patching算法来计算出真正需要更新的节点,最大限度地减少DOM操作,从而显著提高性能。

Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)

  • 提升渲染性能
    Virtual DOM的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。

为了实现高效的DOM操作,一套高效的虚拟DOM diff算法显得很有必要。我们通过patch 的核心----diff算法,找出本次DOM需要更新的节点来更新,其他的不更新。比如修改某个model 100次,从1加到100,那么有了Virtual DOM的缓存之后,只会把最后一次修改patch到view上。那diff 算法的实现过程是怎样的?

diff.png

React Diff 算法基于三个策略

  • Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。

  • 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结

  • 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。

diff2.png

Link : React Diff 算法

总结:虚拟DOM解决了javascript频繁更新Dom的性能问题,在React内部,虚拟DOM甚至可以合并同一个节点的多次更新,更加减少操作DOM的次数,另外React还可以利用浏览器每个刷新贞的空闲时间更新DOM,来解决脚本和UI渲染之间的资源竞争。

3、前端构建演进

通过以上介绍,单页面引用导致了大量页面现在已经成为了一个一个组件,静态路由来组织组件的呈现和隐藏,虚拟DOM形成了组件和真实页面的桥梁。当单页应用的内容更加庞大以后,如何组织这些组件,已经成为了一个难题。于是乎,前端出现了一个概念三化,也就是下文的模块化工程化组件化

示例:supplant-app-template

3.1、模块化

在模块化概念出现以前,我们的代码是这样的:

<html>
    <head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>项目领用台账</title>
    <script type="text/javascript" src="/bap/struts/res/jquery.js"></script>
    <script type="text/javascript" src="/bap/struts/res/zh_cn/core.js"></script>
    <script type="text/javascript" src="/bap/struts/res/zh_cn/main.js"></script>
    <script type="text/javascript" src="/bap/struts/supdatagrid/supdatagrid.js"></script>
    </head>
    <body>
        ....
    </body>
</html>


页面引用了一堆js文件,这些文件内部可能有依赖关系,也有可能同时定义的全局对象,一不小心,就容易相互覆盖。

// core.js
window.some = {}

//main.js
window.some = [1,2,3]

//main.js比core.js 后加载,最后window.some就是数组[1,2,3]

为了实现像服务器端语言一样,按需的使用各个代码块,前端语言也引入了模块化的概念。

一个模块就是一个实现特定功能的文件,有了模块我们就可以更方便的使用别人的代码,要用什么功能就加载什么模块。

模块化开发的好处:

  • 避免变量污染,命名冲突

  • 通过代码分离,提高代码复用率

  • 提高维护性

  • 依赖关系的管理

为了实现模块化,出现了用于JavaScript模块管理的两大流行规范:commonJSAMD

前者定义的是模块的同步加载,主要用于Node.js。同步加载在前端会导致整个页面等待,对前端并不适用,便出现了AMDAMD采用异步加载方式,通过RequireJS等工具适用于前端。

RequireJS为例,这是一种在线“编译”模块的方案,相当于在浏览器中先加载一个AMD解释器,使浏览器认识define、export、module等相关命令,来实现模块化。后来ES6提供了对模块化的原生支持,它的目标是创建一种CommonJSAMD使用者都愿意接受的方式,即拥有简洁的语法,又支持异步加载和配置模块加载。

AMD示例:

//<script type="text/javascript" data-main="js/script/main" src="js/lib/require.js"></script>

require(['jquery'], function ($){
     alert($);
});

为了让模块化开发的代码,可以打包在一起并且在浏览器上运行,出现了一些打包模块的构建工具,webpack是一个预编译模块的方案。在发布前预编译好,不需要在浏览器中加载解释器。另外,直接写AMDES6的模块化代码,它都能编译成浏览器识别的JavaScript代码。甚至CommonJS规范的模块化,webpack也可以转换成浏览器使用的形式。

Link : Webpack

webpack.png

npm包和npm仓库

npm2.png

伴随着模块化的程度不断加深,前端各种各样的第三方模块层出不穷,前端也出现了类似java领域的Maven仓库一样的包管理库NPM。已经有相当丰富的第三方开源脚本库上传到了NPM仓库内,这也是前端发展如此快速的原因之一。

npm.png
npms.png

总结:模块化来解决脚本如何拆分和组合,让单页面的组件被复用成为了可能。

3.2、工程化

将前端项目当成一项系统工程进行分析、组织和构建从而达到项目结构清晰、分工明确、团队配合默契、开发效率提高的目的。

还记得我在最早期写前端代码时,往往一个页面就是一个文件搞定,HTML/CSS/JS全部写在一起,后来知道应该把结构、样式和动作分离,我想这是我接触到最早的前端工程化的思想了,所谓前端工程化我认为就是将前端项目当成一项系统工程进行分析、组织和构建从而达到项目结构清晰、分工明确、团队配合默契、开发效率提高的目的。

工程化是一种思想而不是某种技术(当然为了实现工程化我们会用一些技术),这样说还不够具体,举个例子来说:

要盖一栋大楼,假如我们不进行工程化的考量那就是一上来掂起瓦刀、砖块就开干,直到把大楼垒起来,这样做往往意味着中间会出现错误,要推倒重来或是盖好以后结构有问题但又不知道出现在哪谁的责任甚至会在某一天轰然倒塌,那我们如果用工程化的思想去做,就会先画图纸、确定结构、确定用料和预算以及工期,另外需要用到什么工种多少人等等,我们会先打地基再建框架再填充墙体这样最后建立起来的高楼才是稳固的合规的,什么地方出了问题我们也能找到源头和负责人。

前面我说接触最早的工程化思维就是“结构、样式和动作分离”,在只有若干个页面的小型项目我们只需要用这些简单的做法就能把项目很好的组织起来,但是在一个大型web项目中往往有更加复杂的结构和非常多的页面需要很多人甚至是多个团队配合才能把项目做完,我们需要有更加严谨和复杂的工程化思维去组织结构。从更高层面的项目组织来看我们要做项目的各种规范、技术选型、项目构建优化等等,在代码层面我们还需要用到JS/CSS模块机、UI组件化等开发方式。

分工.png

再用一句通俗的话来概括前端工程化:前端工程化就是用做工程的思维看待和开发自己的项目,而不再是直接撸起袖子一个页面一个页面开写。

package.json

基于现在流行的前端工程,都是由npm包管理的,内部有个package.json,系统的管理着每个项目的依赖、打包过程、版本等等信息,这也类似java里面的pom.xml

总结:工程化让复杂单页面应用的开发、构建和部署成为了一套体系。

3.3、组件化

  • 页面上的每个独立的、可视/可交互区域视为一个组件,每个组件对应一个工程目录,组件所需的各种资源都在这个目录下就近维护;

  • 由于组件具有独立性,因此组件与组件之间可以自由组合;

  • 页面不过是组件的容器,负责组合组件形成功能完整的界面;

组件化将页面视为一个容器,页面上各个独立部分例如:头部、导航、焦点图、侧边栏、底部等视为独立组件,不同的页面根据内容的需要,去盛放相关组件即可组成完整的页面。

组件化.jpg

模块化和组件化一个最直接的好处就是复用,同时我们也应该有一个理念,模块化和组件化除了复用之外还有就是分治,我们能够在不影响其他代码的情况下按需修改某一独立的模块或是组件,因此很多地方我们及时没有很强烈的复用需要也可以根据分治需求进行模块化或组件化开发。

Link :React 组件设计哲学

总结:组件化已经成为设计一个复杂前端应用必备的过程。

4、内部前端技术栈

SIP

Jquery 、Easyui

Supplant PC

React、Ant Design React、Jquery

Supplant Mobile

Vue、Element UI、原生webView

Supfusion

React、Ant Design React

Heath-Code Mobile

React、Ant Design Mobile、原生webView

Heath-Code PC

React、Ant Design React

5、前端技术书籍推荐

book.jpg

6、沟通&交流

timg.jpg
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270