前端技术浅谈
content.png1、前端框架发展
前端技术演进.png以铜为鉴,可以正衣冠;以人为鉴,可以明得失;以史为鉴,可以知兴替。
本文主题:前端开发如何借助对
HTML
和Javascript
不同的使用方式,来一步步演进前端开发框架,从而优化前端开发过程。
想要全面了解前端技术发展,最好的方法就是向后看,看一路走来前端开发是如何从服务端主导的静态网站
一步步发展到现在由客户端主导的单页应用
。只有了解了过去前端分别在不同的阶段解决了怎样的问题,才能更好地看清楚未来要向哪里去。所以,我们先简单的来看看企业信息化产品
在前端技术发展过程中的几个重要迭代过程。
1.1、静态页面
1991年,Tim作为布道者在Internet上广泛推广Web的理念,与此同时,美国国家超算应用中心对此表现出了浓厚的兴趣,并开发了名为Mosaic
的浏览器,于1993年4月进行了发布。此时的网页以HTML为主,是纯静态的网页,网页是“只读”的,信息流只能通过服务器到客户端单向流通,由此世界进入了Web 1.0时代。
90年代,流行的一些新闻网站、公司官方宣传网页,都属于这个范畴。
解决问题:信息的展示和传播过程。
1.2、动态页面
静态页面因为内容是固定的,不能读取后台数据库中的数据,为了使得Web内容更加灵活,以PHP
、JSP
、ASP.NET
为代表的动态页面技术相继诞生。
随着动态页面技术的不断发展,后台代码变得庞大臃肿,后端逻辑也越来越复杂,逐渐难以维护,此时,后端的各种MVC
框架逐渐发展起来,以JSP
为例,Struts
、Spring MVC
等框架层出不穷,这些框架把请求处理逻辑,分层了多层,比如典型的三层
结构,就起源于此。
JSP(JavaServer Pages)原理
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产品:Gmail
和Google 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的发展。
但是有个问题,各个浏览器对Ajax
和Dom
操作有差异性。为了解决浏览器兼容性问题,Dojo
、jQuery
、YUI
、ExtJS
等前端Framework相继诞生。前端开发人员用这些Framework频繁发送AJAX请求到后台,在得到数据后,再用这些Framework更新DOM树。
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中的新特性。随之,Babel
和TypeScript
逐渐流行起来,编写ES6代码,然后用Babel或TypeScript
将其编译为ES5
等浏览器支持的JavaScript。
同时随着HTML5
的流行,前端不再是人们眼中的小玩意,以前在C/S中实现的桌面软件的功能逐步迁移到了前端,前端的代码逻辑逐渐变得复杂起来。解决办法,和后端代码一样:分层
。当然,分层一般都是基于比较通用的一些设计模式,比如MVC、MVP、MVVM
。伴随着,分层的出现,一些现在流行的框架也同时出现了:
随着这些MV*
框架的出现,网页逐渐由Web Site演变成了Web App,最终导致了复杂的单页应用( Single Page Application)的出现。同时也伴随出现了类似前端路由
、虚拟DOM
等一些前端概念。
1.4.2、Node.js发展
一个基于 Chrome
V8
引擎的 JavaScript 运行时
Node.js的发展,使得Javascript
不在局限于浏览器端运行,服务器软件、桌面软件到看到了Javascript
的身影。当然对前端领域而言,Node.js的产生使得前端开发基于Javascript
实现工程化得到了可能。后文会详细介绍。
解决问题:前端代码复杂后,Dom操作变的频繁,需要用更合理、更优雅的方式来完成这个过程,分层和用更高层次的设计模式来组织,由此大量前端MV*框架出现。
1.5、移动应用兴起
随着iOS
和Android
等智能手机的广泛使用,移动浏览器也逐步加强了对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,比如我们之前的每日健康申报。
-
将
HTML5
代码针对不同平台编译成不同的原生应用,实现了Web开发,Native部署。这一类的典型代表有React Native
。React Native : 使用JavaScript和React编写原生移动应用。
解决问题:移动端的兴趣,让
H5
在移动端发展迅速,由此也让混合应用成为了趋势。混合应用解决了更新应用内容,但是App不需要重新下载更新的问题,同时也因为JS的跨平台特性,简化了移动应用的开发过程。
2、主流框架和技术介绍
目前国内,前端单页应用
开发相对流行的Javascript
框架主要有两个(Jquery不算的话):Vue
和 React
,他们核心都是解决一个数据
和视图
之间关联的问题。
一般我们进行表单开发过程中,往往需要经历两个过程:
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.png2.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.pushState
和history.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
从Vue
和React
的数据更新到视图更新,两个框架都采用了虚拟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操作上。
上图几个概念加以解释:
渲染函数
:渲染函数是用来生成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.pngVirtual 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
算法的实现过程是怎样的?
React Diff
算法基于三个策略diff2.png
Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结
对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。
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模块管理的两大流行规范:commonJS
和AMD
。
前者定义的是模块的同步加载,主要用于Node.js。同步加载在前端会导致整个页面等待,对前端并不适用,便出现了AMD
。AMD
采用异步加载方式,通过RequireJS
等工具适用于前端。
以RequireJS
为例,这是一种在线“编译”模块的方案,相当于在浏览器中先加载一个AMD
解释器,使浏览器认识define、export、module等相关命令,来实现模块化。后来ES6提供了对模块化的原生支持,它的目标是创建一种CommonJS
和AMD
使用者都愿意接受的方式,即拥有简洁的语法,又支持异步加载和配置模块加载。
AMD示例
:
//<script type="text/javascript" data-main="js/script/main" src="js/lib/require.js"></script>
require(['jquery'], function ($){
alert($);
});
为了让模块化开发的代码,可以打包在一起并且在浏览器上运行,出现了一些打包模块的构建工具,webpack
是一个预编译模块的方案。在发布前预编译好,不需要在浏览器中加载解释器。另外,直接写AMD
或ES6
的模块化代码,它都能编译成浏览器识别的JavaScript代码。甚至CommonJS
规范的模块化,webpack
也可以转换成浏览器使用的形式。
webpack.pngLink : Webpack
npm包和npm仓库
npm2.png伴随着模块化的程度不断加深,前端各种各样的第三方模块层出不穷,前端也出现了类似java
领域的Maven仓库一样的包管理库NPM。已经有相当丰富的第三方开源脚本库上传到了NPM仓库内,这也是前端发展如此快速的原因之一。
总结:模块化来解决脚本如何拆分和组合,让单页面的组件被复用成为了可能。
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
网友评论