前端技术概览
作者:兰峰
Web 时代与浏览器的发展
Web 时代
1990 年,英国计算机工程师蒂姆·伯纳斯·李(Tim Berners-Lee)在瑞士的欧洲核子研究组织(CERN)工作时,开发出首个 Web 服务器与图形化 Web 浏览器。他将这个进入互联网世界的新窗口,称为“WorldWideWeb”(即“万维网”)。这是一款为 NeXT 计算机开发的易于使用的图形化界面浏览器,超文本第一次通过公开网络被链接起来——即我们现在所熟知的 Web。
浏览器大战
网景崛起
要说浏览器的历史,要从 1994 年开始,那一年网景通信公司推出了代号为“网景导航者”的网景浏览器 1.0(Mozilla Firefox 的早期版本),随后迭代版本迅速占领浏览器大部分份额。
IE 崛起
微软意识到网景通讯公司对其操作系统和应用市场的威胁,立马收购另外一家浏览器公司,在其基础上开发了 Internet Explorer。在 Windows 浏览器中捆绑了 IE 浏览器,用户无需再掏钱买网景公司的浏览器,致使网景公司的浏览器市场份额大降。
IE 不思进取
1998 年 1 月,网景与微软 IE 浏览器竞争失利以后,为了挽回市场,网景通信公司公布旗下所有软件以后的版本皆为免费,并开放网景浏览器的源代码,成立了非正式组织 Mozilla,自此 Mozilla 浏览器开始登上舞台。可惜的是尽管 Mozilla(2002 年发布 Firefox)、Opera 浏览器很好用,可微软操作系统的市场占有率很大,造成其他浏览器的市场份额一直不变。IE 坐在份额第一的头把交椅后,却一直不思进取,自己制作一套 Web 标准,也不怎么支持 HTML,Javascript,CSS 这些 Web 技术的新版本特性,微软从 IE6 开始到 IE8 七八年间几乎没对浏览器做什么革新,大家都适应了 IE,什么补丁、不安全、崩溃也不在意,也觉得浏览器就该如此。
Chrome 崛起
2008 年 Chrome 横空出世。界面简洁、加载快速、数据安全等这些特点让 Chrome 的市场份额逐步攀升。当微软意识到 Chrome 开始逐步侵蚀自己的市场时,开始频繁更新 IE,2011 年 IE9 发布,2012 年 IE10 发布,2013 年 IE11 发布,最后 IE 的代码实在适应不了新的要求的 web 技术,就重新开发了一个名为”edge"的浏览器用来取代 IE,但还是挡不住 Chrome 成为市场份额第一的命运。
在 IE 横行的那一段时间为了适应 IE 中国的大多数常用网站也不大符合互联网标准,也就是说如果用符合互联网标准的浏览器去解析这些网站,反而会不正常显示,可见 IE 坐头把交椅的这几年,却一直在误导和阻挠互联网的发展。在此要向那些不断创新、不断完善、不断接纳新 Web 技术的浏览器公司致敬,面对 IE 他们的市场份额不高,却仍然坚持着不断前进。
国产浏览器起源
其实国产浏览器的起源于 IE,一位网名为 changyou(畅游)的程序员于 1999 年在论坛上发布一款叫”MyIE"的浏览器,基于 IE,但采用多窗口浏览,占用系统资源比 IE6 少很多,且有鼠标手势、视觉化书签等功能,后来的中国浏览器 MyIE2(后改名 Maxthon)、网际畅游(后改名 GreenBrowser)与 TheWorld(世界之窗)等都是用 MyIE 的源代码改写完成。遨游成立公司独自运营,TheWorld 被 360 收购变成了 360 安全浏览器。
一些国内互联网公司认识到浏览器是互联网的入口,是推广自家产品的最佳工具,如果大家都用了我的浏览器,那我在浏览器的显要位置放上自己的产品岂不是很容易,其次 google 创建了一个开源浏览器引擎 Chromium 项目,这样一来做一个简单、快速、安全的浏览器就很容易,为什么不做呢!同时为了适应国内环境还推出了双内核版本的浏览器,Chromium 内核为极速内核,打开网页速度快,IE 内核为兼容内核,为了兼容银行、政府部门的老网站访问。例如搜狗、360、QQ 浏览器等等,无一不是套着不同的外壳用着相同的内核。
Google 在 2008 年 9 月决定开源整个 Chrome 项目作为其 Chromium 项目的一部分。
Chromium 是 Google 的 Chrome 浏览器背后的引擎,其目的是为了创建一个安全、稳定和快速的通用浏览器,使用 Chromium 开源代码(基于 webkit 内核)的浏览器有 360 极速浏览器、枫树浏览器、太阳花浏览器、世界之窗极速版、UC 浏览器电脑版、搜狗高速浏览器和 qq 浏览器等。 google 一直坚持开源这个态度,Chromium 和 android 一样开源的同时快速迭代产品,从而混乱现有格局,态度是好的,结果也不赖,一举两得。
微软放弃自有内核,拥抱 Chrominum
2020 年 1 月 16 日微软发布基于 Chrominum 的 edge 浏览器性能提升很大,可与 Chrome 媲美,使得 Edge 浏览器份额逐渐上涨。
PC 浏览器内核
1997 年 Trident(IE 内核,在 1997 年的 IE4 中首次被采用并沿用到 IE11)
1998 年 KHTML(由 KDE 所开发的 HTML 排版引擎)
2000 年 Gecko(Netscape6 开始采用的内核,后来的 Mozilla FireFox(火狐浏览器) 也采用了该内核,Gecko 的特点是代码完全公开,因此,其可开发程度很高,全世界的程序员都可以为其编写代码,增加功能)
2001 年 WebKit(KHTML 的分支,苹果 Safari)
2003 年 Presto(挪威产浏览器 opera 的 "前任" 内核,为何说是 "前任",因为最新的 opera 浏览器早已将之抛弃从而投入到了谷歌大本营)2008 年 Chromium(早期基于 WebKit)2010 年 混合引擎(双核)2013 年 Blink(WebKit 分支)
2015 年 EdgeHTML(Edge 浏览器)
Blink 其实是 WebKit 的分支,如同 WebKit 是 KHTML 的分支。Google 的 Chromium 项目此前一直使用 WebKit(WebCore) 作为渲染引擎,但出于某种原因,并没有将其多进程架构移植入 Webkit。后来,由于苹果推出的 WebKit2 与 Chromium 的沙箱设计存在冲突,所以 Chromium 一直停留在 WebKit,并使用移植的方式来实现和主线 WebKit2 的对接。这增加了 Chromium 的复杂性,且在一定程度上影响了 Chromium 的架构移植工作。基于以上原因,Google 决定从 WebKit 衍生出自己的 Blink 引擎(后由 Google 和 Opera Software 共同研发),将在 WebKit 代码的基础上研发更加快速和简约的渲染引擎,并逐步脱离 WebKit 的影响,创造一个完全独立的 Blink 引擎。这样以来,唯一一条维系 Google 和苹果之间技术关系的纽带就这样被切断了。
网络
在浏览器输入网址到浏览器展示页面经过了什么?
1.输入网址+回车
2.DNS 解析得到 IP 地址(DNS 查询过程:浏览器缓存、系统缓存、hosts 文件、野生 DNS 服务器(本地 DNS 服务器)、根 DNS、顶级 DNS、权威 DNS、本地(附近)CDN、源站)
3.得到后 IP,通过 IP 地址去找目标服务器,路由器根据路由表路由转发
4.找到后进行 TCP 三次握手建立连接
5.连接成功发起 HTTP 请求
6.目标服务器响应 HTTP 请求(返回请求的 HTML 文件)
7.浏览器收到 HTML 文件后解析 HTML 代码,并请求 HTML 代码中的资源(如 JS、CSS、图片等)
8.浏览器对页面进行渲染呈现给用户
TCP 三次握手、四次挥手
为什么握手要三次,挥手要四次?因为 TCP 是基于全双工通信的,通信的两方都可以向对方发送消息和接收消息,在一方发起挥手后接收方有可能还有数据没有发送完成,只能先返回已经收到了对方的挥手,等数据发送完成之后再发起自己要关闭的请求,等到对方应答收到了就关闭连接。
内容分发网络加速静态资源——CDN(Content Delivery Network)
CDN 是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。
A 记录
域名到 IP
CNAME
域名到域名
CDN 的作用位置
1.本地 DNS 向权威 DNS 发送 DNS 查询报文时;
2.权威 DNS 查找到一条 NAME 字段为“join.xx.com”的 CNAME 记录(由服务提供者配置),该记录的 Value 字段为“join.xx.cdn.com”;并且还找到另一条 NAME 字段为“join.xx.cdn.com”的 A 记录,该记录的 value 字段为 GSLB(全局负载均衡系统)的 IP 地址;
3.本地 DNS 向 GSLB 发送 DNS 查询报文;
4.GSLB 根据本地 DNS 的 IP 地址判断用户的大致位置用户的大致位置为广州,筛选出位于华南地区且综合考量最优的 SLB(本地负载均衡系统)的 IP 地址填入 DNS 回应报文,作为 DNS 查询的最终结果;
5.本地 DNS 回复客户端的 DNS 请求,将上一步的 IP 地址作为最终结果回复给客户端;
6.客户端根据 IP 地址向 SLB 发送 HTTP 请求:“join.xx.com/video.php”;
7.SLB 综合考虑缓存服务器集群中各个节点的资源限制条件、健康度、负载情况等因素,筛选出最优的节点后回应客户端的 HTTP 请求(状态码为 302,重定向地址为最优缓存节点的 IP 地址);
8.客户端接收到 SLB 的 HTTP 回复后,重定向到该缓存节点上;
9.缓存节点判断请求的资源是否存在、过期,将缓存的资源直接回复给客户端,否则到源站进行数据更新再回复。
与普通 DNS 过程不同的是,这里需要服务提供者(源站)配置它在其权威 DNS 中的记录,将直接指向源站的 A 记录修改为一条 CNAME 记录以及对应的 A 记录,CNAME 记录将目标域名转换为 GSLB 的别名,A 记录又将该别名转换为 GSLB 的 IP 地址。通过这一系列的操作,将解析源站的目标域名的权利交给了 GSLB,以至于 GSLB 可以根据地理位置等信息将用户的请求引导至距离其最近的“缓存节点”,减缓了源站的负载压力和网络拥塞。
为什么使用 CDN 需要 CNAME 记录?
举个例子:在某个图片云平台创建加速域名后,平台会给域名分配一个'CNAME 域名'(例:cdn-example-com.test.com),我们需要在域名服务商中配置一条 CNAME 记录,将访问加速域名的请求指向这个 cdn-example-com.test.com 域名记录,生效后访问加速域名时解析将会指向加速的图片云平台 CDN 地址,之后由图片云平台的 CDN 完成调度,使得该域名所有请求都开始享有 CDN 图片加速效果。
为什么要有 CNAME 记录?
如果服务商给你一个 ip,假如哪天服务商想把 ip 地址换一个,很多人域名上对应的 ip 地址就要跟着变化,要让所有人都一起改完是一件成本很高的事情,换成 CNAME 就没事了,你用你的 CDN,他改他的 ip 地址。唯一的坏处就是,第一次 DNS 解析域名的时候会多解析一次。总体来看,好处远大于坏处。
常见的服务器响应状态码
200:OK(没有问题)
206:Partial Content(客户端使用 Content-Range 指定了需要的实体数据的范围,然后服务端处理请求成功之后返回用户需要的这一部分数据而不是全部)
301:Moved Permanently(代表永久性定向。该状态码表示请求的资源已经被分配了新的 URL,以后应该使用资源现在指定的 URL)
302:Found(代表临时重定向。该状态码表示请求的资源已经被分配了新的 URL,但是和 301 的区别是 302 代表的不是永久性的移动,只是临时的。)
304:Not Modifie(你要去拿缓存)
307:Temporary Redirect(临时重定向,与 302 相同,但是 302 会把 POST 改成 GET,而 307 就不会。)400:Bad Request(400 表示请求报文中存在语法错误。需要修改后再次发送)
403:Forbidden(有表明请求访问的资源被拒绝了。没有获得服务器的访问权限,IP 被禁止等。)404:Not Found(服务器没有这个资源)
500:Internal Server Error(服务器内部错误,表明服务器端在执行请求时发生了错误,很有可能是服务端程序的 Bug 或故障)
503:Service Unavailable (它表示服务器尚未处于可以接受请求的状态)
HTTP 和 HTTPS 的区别
HTTP
80 端口
只需要 TCP 三次握手
HTTPS
443 端口
在 TCP 三次握手基础上还要四个阶段的 SSL / TLS 的握手
加密 HTTPS 采用的是对称加密和非对称加密结合的混合加密方式:
在通信建立前,采用非对称加密的方式交换会话密钥,后续就不再使用非对称加密
在通信过中全部使用对称加密的会话密钥的方式加密明文数据
浏览器
浏览器的缓存机制
浏览器缓存(Browser Caching)是为了节约网络的资源加速浏览,浏览器在用户磁盘上对最近请求过的文档进行存储,当访问者再次请求这个页面时,浏览器就可以从本地磁盘显示文档,这样就可以加速页面的阅览。
通常浏览器缓存策略分为两种:强缓存(Expires,cache-control)和协商缓存(Last-modified ,Etag),并且缓存策略都是通过设置 HTTP Header (Expires,cache-control,Last-modified ,Etag)来实现的。
基本流程:
浏览器在加载资源时,根据请求头的 expires 和 cache-control 判断是否命中强缓存,是则直接从缓存读取资源,不会发请求到服务器;
如果没有命中强缓存,浏览器一定会发送一个请求到服务器,通过 last-modified 和 etag 验证资源是否命中协商缓存,如果命中,服务器会将这个请求返回,但是不会返回这个资源的数据,依然是从缓存中读取资源;
如果前面两者都没有命中,直接从服务器加载资源。
强缓存
1、Expires
Expires 是 http1.0 提出的一个表示资源过期时间的 header,它描述的是一个绝对时间,由服务器返回。
Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效。
2、Cache-Control
Cache-Control 出现于 HTTP / 1.1,优先级高于 Expires ,表示的是相对时间。
协商缓存
当浏览器对某个资源的请求没有命中强缓存,就会发一个请求到服务器,验证协商缓存是否命中,如果协商缓存命中,请求响应返回的 http 状态为 304 并且会显示一个 Not Modified 的字符串。
1、Last-Modified,If-Modified-Since
Last-Modified 表示本地文件最后修改日期,浏览器会在 request header 加上 If-Modified-Since(上次返回的 Last-Modified 的值),询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来。
但是如果在本地打开缓存文件,就会造成 Last-Modified 被修改,所以在 HTTP / 1.1 出现了 ETag。
2、ETag、If-None-Match
Etag 就像一个指纹,资源变化都会导致 ETag 变化,跟最后修改时间没有关系,ETag 可以保证每一个资源是唯一的。
If-None-Match 的 header 会将上次返回的 Etag 发送给服务器,询问该资源的 Etag 是否有更新,有变动就会发送新的资源回来。
整体流程:
存储
cookie(最大存储 4k):可以设置生效的域名、路径、过期时间、过期秒数等,一般用于存储用户状态信息,cookie 中的内容会根据设置域名随着 HTTP 请求一起发送到服务器,服务器可以获取 cookie 中存储的用户标记信息,判断该请求来自于哪个用户;seesion 机制中的用户信息存放在 cookie 中;登录账号成功之后记录登录状态可以记录到 cookie 中,做到下次打开网站免登录的效果。
Storage(sessionStorage/localStorage)最大存储 5M:localStorage 为持久存储,除非手动清除,否则一直存在;sessionStorage 为会话存储,即关掉当前页面 sessionStorage 中存储的数据就被浏览器清除了,再重新通过链接地址打开关闭页面,sessionStorage 中的之前存储的数据已经不在,而 localStorage 中存储的还在。localStorage 中也可以存储 token 信息,做到免登陆的效果(JWT)。
IndexedDB:是浏览器提供的本地数据库, 允许储存大量数据,提供查找接口,还能建立索引。这些都是 LocalStorage 所不具备的。就数据库类型而言,IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。
跨域
跨域是什么?
浏览器的同源策略导致了跨域,其目的是隔离潜在恶意文件。
什么是同源策略?
同源策略可以防止 JavaScript 发起跨域请求。源被定义为协议、主机名和端口号的组合。此策略可以防止页面上的恶意脚本通过该页面的文档对象模型,访问另一个网页上的敏感数据。
解决跨域的方法:
1.jsonp,浏览器允许 script 标签加载不同源的资源
2.反向代理(NGINX 服务器内部配置)
3.配置 CORS(跨域资源共享)前后端协作设置请求头部,Access-Control-Allow-Origin 等头部信息 4.iframe 嵌套通信,postMessage
JSONP 是什么?
<script>标签的 src 属性是可以跨域的,并且还会立即执行,把跨域服务器写成调用本地的函数,即后端返回一个定制的 js 文件,回调数据就能作为调用的参数从而本地获得到了。
// 后端返回的 js 文件 xxx.js// callback 的名字可以根据前端的回调名字自定义 callback({a:1, b:2}) // 后端需要传递的数据直接作为调用参数
前端只需要定义 callback 函数并新建 script 标签,就可以了。
<script src="https://xx.com/api/xxx.js?callbackName=callback"></script>CORS 简单请求+预检请求
当一个资源与该资源本身所在的服务器不同的域、协议、端口请求一个资源时,资源会发起一个跨域 HTTP 请求。对于浏览器限制这个词:不一定是浏览器限制了发起跨站请求,也可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了。
CORS 跨域资源共享,该标准新增了一组 HTTP 首部字段,允许服务器声明哪些资源站通过浏览器有权限访问哪些资源。规范要求,对那些可能对服务器产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务端允许后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。
什么是简单请求?
不会触发 CORS 预检的请求称为简单请求,满足以下所有的条件才会被视为简单请求,基本上我们日常开发只会关注前面两点:
1.使用 GET POST HEAD 其中一种方法
2.只使用了如下的安全首部字段,不得人为设置其他首部字段
o Accept
o Accept-Language
o Content-Language
o Content-Type 仅限以下三种
text/plain
mutipart/form-data
application/x-www-form-urlencoded
o HTML 头部 header field 字段:DPR、Download、Save-Data、Viewport-Width、Width
3.请求中的任意 XMLHttpRequestUpload 对象均为没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问
4.请求中没有使用 ReadableStream 对象
前端三剑客
HTML+CSS+JavaScript
HTML——网页的骨架(DOM 树)
页面结构(DOM 节点,DOM 树的节点)
为什么移动站点叫 H5?
H5 是 HTML5 的简称。
现阶段的网页展示技术无论是移动端还是 PC 端都用到了 HTML5,HTML5 是相对于之前的 HTML4 的超文本标记语言规范。
之所以说移动站是 H5 站 H5 版本是因为在 HTML5 技术没有成熟之前,Web 的移动端表现并不是很好,比如加载速度、响应速度、对手机内存要求高等,与原生的安卓 iOS 嵌套性能也不好,而 HTML5 技术成熟之后,性能和表现都很出色,加上现阶段的机动设备处理系统、手机内存都比之前好了很多,所以 HTML5 可以放肆的用在移动端,很多移动页面也就称呼为“H5”页面。
外行人一般会将移动端 Web 站点叫成 H5,专业前端一般称其为 Web 移动端。
CSS——骨架(树节点)的大小、涂料、状态(动画)
美化+动画
JavaScript——让骨架移动,修改骨架的 CSS,修改骨架显示的内容
元素移动动画+动作
写逻辑当什么时候操作 DOM 节点、让 DOM 节点怎样运动,修改 DOM 节点的属性如 class,style
JavaScript 版本
从 2015 年起,ECMAScript 按年命名,ECMAScript 通常缩写为 ES,ES6 即为 ECMAScript 的第六个版本(ECMAScript 2015=ES6)。
JavaScript 版本
浏览器支持
所有浏览器都完全支持 ECMAScript 3。
所有现代浏览器(Chrome、Firefox、Safari、Edge)都完全支持 ECMAScript 5。
JavaScript 库的发展
jQuery 时期(2009~2012 年 石器时代)
jQuery 的语法设计使得许多操作变得容易,如操作文档对象(document)、选择文档对象模型(DOM)元素、创建动画效果、处理事件、以及开发 Ajax 程序。
更方便地操作 DOM
原生 JavaScript 操作 DOM
// 通过 css id 获取 dom 元素
var oEl = document.getElementById('idName')
// 通过 css 类名获取 dom 元素
var oEls = document.getElementsByClassName('className')
// 通过标签名获取 dom 元素
var oEls = document.getElementsByTagName('tagName')
// 查找元素并为该元素添加一个类
document.getElementById("myDIV").classList.add("myclass");
jQuery 操作
// 通过 css id 获取 dom 元素
$('#idName')
// 通过 css 类名获取 dom 元素
$('.className')
// 操作 dom 元素,支持链式调用
$('#idName').css({...}).find(...).css({})
jQuery 的出现促使了以下两个 JavaScript 原生 api 的诞生
document.querySelector('#id')
document.querySelectorAll('.className')
更方便地发请求
原生 JavaScript 发起 ajax 请求
// get 请求
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/user?id=333', true);
xhr.send();
xhr.onreadystatechange = function (e) {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
}
};
// post 请求
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/user', true);
// POST 请求需要设置此参数
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
xhr.send('name=33&ks=334');
xhr.onreadystatechange = function (e) {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
}
};
jQuery 发起 ajax 请求
$.ajax({
url: "/greet",
data: {name: 'jenny'},
type: "POST",
dataType: "json",
success: function(data) {
// data = jQuery.parseJSON(data); //dataType 指明了返回数据为 json 类型,故不需要再反序列化
...
}
});
$.post(url, data, func, dataType);
$.get(url, data, func, dataType);
/*
可选参数:
1)url:链接地址,字符串表示
2)data:需要发送到服务器的数据,格式为{A: '...', B: '...'}
3)func:请求成功后,服务器回调的函数;function(data, status, xhr),其中 data 为服务器回传的数据,status 为响应状态,xhr 为 XMLHttpRequest 对象,个人感觉关注 data 参数即可
4)dataType:服务器返回数据的格式
*/
jQuery 已经足够了吗?
虽然使用 jQuery 已经大大增加了操作 DOM 的效率,但是频繁地操作 DOM 会导致性能问题。另外在改变多个节点时需要选取多次(或者必须通过一个特殊的统一命名选取多个需要修改的元素),非常之麻烦。
<h1 id="h1">Hello <span id="message">World</span></h1>
<p id="message2">World</p>
<input type="text" id="message-input">
<button id="test-btn">click</button>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script>
$(document).ready(function(){
// 选择 id 为 message 的 span 标签
var oMessage = $('#message')
// 选择 id 为 message2 的 p 标签
var oMessage2 = $('#message2')
// 选择 id 为 test-btn 的 button 标签
var oBtn = $('#test-btn')
// 选择 id 为 message-input 的 input 标签
var oInput = $('#message-input')
oBtn.on('click', function(e) {
var text = 'jQuery'
oMessage.text(text)
oMessage2.text(text)
});
oInput.on('input', function(e) {
oMessage.text(e.target.value)
oMessage2.text(e.target.value)
})
});
</script>
AngularJS 的诞生 2009(铁器时代)
一位 Google 工程师开源了他的业余项目 AngularJS,这个全新的框架给当时被 jQuery 统治的前端行业带来了革命性的进步。当时这位工程师只用 1,500 行 AngularJS 代码在 2 周内实现了 3 人 花了 6 个月开发的 17,000 行代码的内部项目。由此可见这个框架的强大。AngularJS 的主要特点是 HTML 视图(HTML View)与数据模型(Data Model)的双向绑定(Two-way Binding),意味着前端界面会响应式的随着数据而自动发生改变。这个特性对于习惯写 jQuery 的前端工程师来说是既陌生又期待的,因为这种编写模式不再要求主动更新 DOM,从而将节省大量主动操作 DOM 的代码和逻辑,这些 AngularJS 全部帮你自动完成了。
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
<div ng-app="">
<label>Name:</label>
<input type="text" ng-model="yourName" placeholder="Enter a name here">
<hr>
<h1>Hello {{yourName}}!</h1>
</div>
Angular 的亮点:
双向绑定:当修改一个输入框的值时,会修改绑定到这个输入框的变量。当修改这个输入框绑定的变量会修改输入框里面的值
虚拟 DOM:虚拟 DOM 是对页面元素真实 DOM 的一个抽象,在修改一个变量时,先修改虚拟 DOM,经过 Angular 的脏检查机制,会判断哪个真实的 DOM 需要更新,最终只更新需要更新的真实 DOM。对比原生 JavaScript 和 jQuery,每次更新都需要开发人员手动去选择需要更新的真实 DOM 节点,Angular 对开发的效率有很大的提升。
React 的诞生 2013 年
支持双向绑定,通过 diff 算法计算出需要修改的虚拟 DOM,再更新真实 DOM
<div id="root"></div>
class App extends React.Component {
constructor(props) {
super(props);
this.state = {message: 'World'}
this.handleChange = this.handleChange.bind(this)
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
this.setState({message: 'React'})
}
handleChange(e) {
this.setState({message: e.target.value})
}
render() {
return (
<div>
<h1>Hello {this.state.message}</h1>
<p>{this.state.message}</p>
<input onChange={this.handleChange} value={this.state.message}/>
<button onClick={this.handleClick}>test</button>
</div>
);
}
}
ReactDOM.render(<App/>, window.root);
Vue 的诞生 2014 年 2 月
吸取 Angular 和 React 的优点,同时实现了模板语法,增加了开发效率。
<div id="root">
<h1>Hello {{ message }}</h1>
<p>{{ message }}</p>
<input type="text" v-model="message">
<button @click="handleClick">click</button>
</div>
const app = new Vue({
el: '#root',
data: {
message: 'World'
},
methods: {
handleClick() {
this.message = 'Vue'
}
}
});
组件
组件是 Vue 最为强大的特性之一。为了更好地管理一个大型的应用程序,往往需要将应用切割为小而独立、具有复用性的组件。在 Vue 中,组件是基础 HTML 元素的拓展,可方便地自定义其数据与行为。下方的代码是 Vue 组件的一个示例,渲染为一个能计算鼠标点击次数的按钮。
// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
模板
Vue 使用基于 HTML 的模板语法,允许开发者将 DOM 元素与底层 Vue 实例中的数据相绑定。所有 Vue 的模板都是合法的 HTML,所以能被遵循规范的浏览器和 HTML 解析器解析。在底层的实现上,Vue 将模板编译成虚拟 DOM 渲染函数。结合响应式系统,在应用状态改变时,Vue 能够智能地计算出重新渲染组件的最小代价并应用到 DOM 操作上。
<template>
<div class="container">
<p v-if="isShowValue">{{value}}</p>
</div>
</template>
此外,Vue 允许开发者直接使用 JSX 语言作为组件的渲染函数,以代替模板语法。[13]以下为可计算点击次数的按钮的 JSX 渲染版本(需配置相应 Babel 编译器):
Vue.component('buttonclicked', {
props: ["initial_count"],
data: function() {var q = {"count": 0}; return q;} ,
render: function (h) {
return (<button vOn:click={this.onclick}>Clicked {this.count} times</button>)
},
methods: {
"onclick": function() {
this.count = this.count + 1;
}
},
mounted: function() {
this.count = this.initial_count;
}
});
响应式设计(双向绑定)
响应式是指 MVC 模型中的视图随着模型变化而变化。在 Vue 中,开发者只需将视图与对应的模型进行绑定,Vue 便能自动观测模型的变动,并重绘视图。这一特性使得 Vue 的状态管理变得相当简单直观。
单文件组件为了更好地适应复杂的项目,Vue 支持以.vue 为扩展名的文件来定义一个完整组件,用以替代使用 Vue.component 注册组件的方式。开发者可以使用 Webpack 或 Browserify 等构建工具来打包单文件组件。
核心插件
vue-route 前端路由
vuex 前端状态管理
vue-loader 将.vue 文件转换成 js 文件
vue-cli 创建 vue 项目命令行工具
vite 开发工具
前端工程化
Nodejs 的诞生 2009
Java 有 JRE,JRE 中有 JVM,NodeJS 中也有 JavaScript 虚拟机,这个虚拟机就是 Chrome 的开源 JavaScript 解析器——V8 引擎。
NodeJS 可以称之为服务器端的 JavaScript,可以操作文件(修改、和生成新的文件),这为前端的工程化带来了革命性的进步。
NPM
npm 为 NodeJS 包管理器,类似 Java 的 Maven。
package.json 文件
{
"name": "xxx",
"version": "2.4.2",
"description": "xxxxx",
"authors": [
"xxxxx"
],
"private": true,
"engines": {
"node": ">= 14",
"npm": ">= 5.2.0"
},
"scripts": {
"dev": "nuxt",
"dev-mock": "nuxt --mock",
"dev-joint": "nuxt --joint",
"build": "nuxt build",
"start": "nuxt start",
"generate": "nuxt generate",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
"lintfix": "eslint --fix --ext .js,.vue --ignore-path .gitignore ."
},
"dependencies": {
"@nuxtjs/axios": "5.12.2",
"@nuxtjs/composition-api": "0.15.1",
"@nuxtjs/google-analytics": "2.4.0",
"@nuxtjs/proxy": "^2.1.0",
"@nuxtjs/pwa": "3.2.2",
"@nuxtjs/style-resources": "0.1.2",
"@vue/babel-preset-jsx": "1.2.4",
"dayjs": "1.8.18",
"js-base64": "^3.6.0",
"js-cookie": "^2.2.1",
"less": "3.9.0",
"less-loader": "4.1.0",
"nuxt": "2.14.7",
"qs": "^6.9.4"
},
"devDependencies": {
"@babel/core": "7.8.3",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.8.3",
"@babel/plugin-proposal-optional-chaining": "7.8.3",
"@babel/preset-env": "7.12.1",
"@commitlint/cli": "8.2.0",
"@commitlint/config-conventional": "8.2.0",
"babel-eslint": "10.1.0",
"babel-jest": "24.9.0",
"core-js": "3.7.0",
"cross-env": "latest",
"eslint": "7.13.0",
"eslint-config-prettier": "6.15.0",
"eslint-friendly-formatter": "4.0.1",
"eslint-plugin-jest": "24.1.3",
"eslint-plugin-nuxt": "1.0.0",
"eslint-plugin-prettier": "3.1.4",
"github-release-notes": "0.17.1",
"husky": "1.3.1",
"jest": "24.9.0",
"lint-staged": "8.2.1",
"prettier": "1.18.2",
"standard-version": "6.0.1",
"stylelint": "9.10.1",
"stylelint-config-standard": "18.3.0",
"svg-sprite-loader": "^4.1.6"
}
}
dependencies:记录这个前端项目中所需要使用的外部模块
devDependencies:记录这个前端项目构建阶段所使用的模块
JavaScript 模块化
模块化一些专业的定义为:模块化是软件系统的属性,这个系统被分解为一组高内聚,低耦合的模块。
模块化是前端工程化的基础,在没有模块化之前前端的项目文件夹大概是这样的
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="./style/common.css">
<link rel="stylesheet" href="./style/main.css">
</head>
<body>
main
<script src="./js/common.js"></script>
<script src="./js/main.js"></script>
</body>
</html>
我们会将公共的逻辑抽取到一个 common.js 中,在每个 page-n.html 子页面手动引入,这样会导致的问题:
一个逻辑可能会在很多个页面使用,当项目非常大的时候,如果你把所有逻辑都放到 common.js 文件中会造成这个文件变得很大,如果某个页面只需要使用这个文件中的一两个方法时,就必须得全量引入 common.js 文件,会造成前端加载性能问题。
在不依赖后台服务器端渲染的情况下,没办法提取出一个既带有 JS 逻辑又带有 HTML 页面的子模块进行复用,例如:网站的顶部导航模块、菜单模块等。
CommonJS
CommonJS 是服务器端模块的规范,Node.js 采用了这个规范。根据 CommonJS 规范,一个单独的文件就是一个模块。加载模块使用 require 方法,该方法读取一个文件并执行,最后返回文件内部的 exports 对象。
// foobar.js
//私有变量
var test = 123;
//公有方法
function foobar () {
this.foo = function () {
// do someing ...
}
this.bar = function () {
//do someing ...
}
}
//exports 对象上的方法和变量是公有的
var foobar = new foobar();
exports.foobar = foobar;
//require 方法默认读取 js 文件,所以可以省略 js 后缀
var test = require('./boobar').foobar;
test.bar();
CommonJS 加载模块是同步的,所以只有加载完成才能执行后面的操作。像 Node.js 主要用于服务器的编程,加载的模块文件一般都已经存在本地硬盘,所以加载起来比较快,不用考虑异步加载的方式,所以 CommonJS 规范比较适用。但如果是浏览器环境,要从服务器加载模块,这是就必须采用异步模式。所以就有了 AMD CMD 解决方案。
AMD 和 RequireJS
AMD 是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。
AMD 设计出一个简洁的写模块 API:
define(id?, dependencies?, factory);
第一个参数 id 为字符串类型,表示了模块标识,为可选参数。若不存在则模块标识应该默认定义为在加载器中被请求脚本的标识。如果存在,那么模块标识必须为顶层的或者一个绝对的标识。
第二个参数,dependencies ,是一个当前模块依赖的,已被模块定义的模块标识的数组字面量。
第三个参数,factory,是一个需要进行实例化的函数或者一个对象。
定义 AMD 模块
define("alpha", [ "require", "exports", "beta" ], function( require, exports, beta ){
export.verb = function(){
return beta.verb();
// or:
return require("beta").verb();
}
});
模块加载
require([module], callback)
AMD 模块化规范中使用全局或局部的 require 函数实现加载一个或多个模块,所有模块加载完成之后的回调函数。
[module]:是一个数组,里面的成员就是要加载的模块;
callback:是模块加载完成之后的回调函数。
// 加载一个 math 模块,然后调用方法 math.add(2, 3);
require(['math'], function(math) {
math.add(2, 3);
});
define 和 require 这两个定义模块,调用模块的方法合称为 AMD 模式,定义模块清晰,不会污染全局变量,清楚的显示依赖关系。AMD 模式可以用于浏览器环境并且允许非同步加载模块,也可以按需动态加载模块。
CMD 和 SeaJS
CMD 是 SeaJS 在推广过程中对模块定义的规范化产出,即 SeaJS 是 CMD 的实现。
对于依赖的模块 AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不通过)。
CMD 推崇依赖就近,AMD 推崇依赖前置。
AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹。
UMD
UMD 是 AMD 和 CommonJS 的糅合,AMD 浏览器第一的原则发展异步加载模块。
CommonJS 模块以服务器第一原则发展,选择同步加载,它的模块无需包装(unwrapped modules)。
这迫使人们又想出另一个更通用的模式 UMD (Universal Module Definition)。希望解决跨平台的解决方案。
UMD 先判断是否支持 Node.js 的模块(exports)是否存在,存在则使用 Node.js 模块模式。再判断是否支持 AMD(define 是否存在),存在则使用 AMD 方式加载模块。
打包构建
自从有了 node 之后,单个的 js 文件离开了 html 以后也可以在终端 run 起来了,我们前端可以和别的语言一样在命令行里玩编程!模块化标准实施之后,js 就有了“引入”和“导出”的概念,这带来的革命性变化便是:当我们写业务的时候再也不用很麻烦地去在 html 里写 15 个<script src="">去运行 js 了。我只要插入一个作为入口的总的 script 标签,另外的 14 个 js 文件都作为模块导出,并导入到这个入口的 js(通常叫 main.js,这 15 个 js 文件也可以互相导入导出,在 node 环境下,每个 js 文件都是一个单独的模块)。
但问题在于,一旦 js 文件以<script src="">的形式插入 html,那么 require、export、import 之类的模块语法就会报错,因为浏览器不支持模块化,模块语法是建立在 node 的环境下才有的。webpack 等打包工具的一个作用就是让我们插入一个 script 标签的同时,还允许我们在 js 文件之间使用 export、import、require 这些语法,并且非常智能地把这些 js 模块合并压缩成 1 个(或 2 个或以上)大大的紧实的 js 文件。
打包工具可以让我们在开发的时候使用 import export require,像后端程序员那样进行模块化开发。
编译
在一个以 Webpack 为构建工具的工程化的前端项目中,我们可能会用到一些预处理器(sass、less)、有静态检测的语言(TypeScript)、更版本的 JavaScript(ES6)方便我们的开发和维护。但是浏览器并不认识这些.scss(sass 预处理文件,需要转成 css)文件、.ts(TypeScript 文件,需要转成 js)文件,还有一些浏览器不支持 ES6(JavaScript 版本太高)的语法(一般支持到 ES5),所以我们需要通过一个一个 loader 将这些文件转换成浏览器认识的文件,将 JS 语法降级(需要用到 babel)到大多数浏览器认识的版本(ES5),这个过程就叫编译。
打包工具
Browserify
将 NodeJS 模块打包成浏览器可以加载的模块,Browserify 允许您在浏览器中使用 require,就像在 Node 中使用 require 一样。
grunt
Grunt 是 JavaScript 任务运行器,是一种用于自动执行常见任务的工具,例如压缩(删除文件多余空格),混淆(修改变量名),编译,单元测试等。
gulp
gulp 是由 Eric Schoffstall 创建的开源 JavaScript 工具包,用作前端 Web 开发中的流构建系统。它是基于 Node.js 和 npm 构建的任务运行器,用于自动化 Web 开发中涉及的耗时且重复的任务,例如缩小,串联,缓存清除,单元测试,linting,优化等。
Webpack
Webpack 是一个开源的前端打包工具。Webpack 提供了前端开发缺乏的模块化开发方式,将各种静态资源视为模块,并从它生成优化过的代码。 Webpack 可以从终端、或是更改 webpack.config.js 来设置各项功能。
rollup
Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。
现在的工程化前端项目
有了以上的基础,现在的前端项目长这样:
现在的组织页面的方式:
通过一个个可以复用的模块组合成一个页面。
其他
SPA(单页应用)与 SSR(服务器端渲染)
前后端不分离时期
前端写好页面模板,提供给后台人员,后台在需要动态展示的地方将模板改成相应的占位变量,或循环(JSP),这个时期前端的页面结构(HTML 的完整 DOM 结构)都是由服务器生成好的,我们称之为服务器端渲染。
一个服务器端渲染的网页:
查看其源代码:
前后端分离
前后端分离之后,前端和后台只需要通过接口进行数据交互。一般情况下,如果没有特殊的要求,使用 React 或者 Vue 开发的 Web 应用都是 SPA。
一个 SPA 网页:
查看其页面源码:
我们看到这个页面上是有链接和社区这些文字和链接的,但是我们在检查它的页面代码时却看不到这些内容,这是因为链接、社区的这些文字都是这个浏览器通过解析这个 HTML 文件中的 JS,通过运行 JS 代码生成的(生成 DOM 的逻辑都写在 JS 文件中),即浏览器生成 DOM,而不是服务器端生成 DOM 结构。跳转页面,都是通过前端路由控制,所有的操作都在这张页面上完成,都由 JavaScript 来控制,而不通过远程的 Web 服务器,这就是 SPA——单页应用。我们可以将 SPA 理解成一个半成品,因为它还需要浏览器执行 JS 脚本生成 DOM,才能显示出页面。
一些企业需要自己的网站在搜索引擎的搜索结果中排名靠前,但是 SPA 对搜索引擎不友好,因为 HTML 中只有 head 标签的有网站信息,而 body 里面基本上都是 script 标签,无法为搜索引擎的爬虫提供更多信息(现在大多数搜索引擎爬虫并不是一个浏览器环境,无法通过 JS 生成 DOM 结构)。这时候我们又需要让浏览器能拿到一个拥有完整 DOM 结构的 HTML 文件这时候应该怎么办呢?
又需要用到服务器端渲染(SSR)了,只不过这个渲染不是靠 Java 等传统后台来做了,而是通过 NodeJS 的 Web 服务器来做。以 Vue 的 SSR 框架 Nuxt 为例,Nuxt 的 NodeJS 服务器在收到前端的页面请求时,会先向后台请求这个页面渲染所需的数据的接口,拿到接口数据后渲染好(生成好完整 DOM 节点的 HTML 文件),再返回给浏览器或爬虫,浏览器或爬虫拿到的就是一个完整的 HTML 文件。这样避免了网络缓慢的情况下浏览器加载 SPA 首页白屏,用户既能更快看到页面,又对爬虫友好,皆大欢喜。但是随之带来了另外一个问题——页面不支持太多人同时访问。因为基于虚拟 DOM 生成真实 DOM 是一项计算量比较大(相对于传统字符串模板方式的服务器端渲染)的任务,而单线程执行 JavaScript 逻辑的 NodeJS 并不适合 CPU 密集型任务,大量的计算将会导致服务器吞吐量的显着下降。在最坏的情况下,服务器将会失去响应,并且无法将任务委派给工作池。
NodeJS 通过 libuv 来处理与操作系统的交互,并且因此具备了异步、非阻塞、事件驱动的能力。因此,NodeJS 能响应大量的并发请求。但在执行 JS 代码的时候是单线程的,所以 NodeJS 适合运用在高并发、I/O 密集、少量业务逻辑的场景。
优化方式——判断如果是爬虫的 user-agent,返回渲染好的 HTML,如果是爬虫返回 SPA。
微前端
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。
微前端架构具备以下几个核心价值:
技术栈无关主框架不限制接入应用的技术栈,微应用具备完全自主权
独立开发、独立部署微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
增量升级在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
独立运行时每个微应用之间状态隔离,运行时状态不共享
微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。
实现方案:
qiankun
Single-SPA
页面效果:
子应用可以是一个老的 jQuery 项目、一个独立的应用,多个子应用通过主应用这个基座合并在一起。
客户端
Android
一般使用 Java 或 kotlin 语言开发,APP 安装包一般为.apk(Android Package)后缀。Google 在推.abb(Android App Bundle)后缀的安装包。
iOS
用 Objective-c、swift 语言开发的 APP,APP 安装包一般为.ipa(iPhoneApplication)文件,ipa 文件实质是一个 zip 压缩包。
MacOS
一般使用 Objective-c、swift 语言开发,应用程序安装包一般为.dmg 文件。可运行应用程序一般为.app。
PC
一般使用 Qt/C++、.Net 等语言开发,应用程序安装包一般为 exe/msi 等。可运行的应用程序一般为.exe。
跨端
小程序(依赖于宿主 APP 的跨端)
ReactNative(Android+iOS)
Weex(Android+iOS)
Flutter(移动跨端解决方案 Android+iOS+Web)
uniapp、Taro(多小程序+APP+Web 移动端解决方案)
快应用(国内 Android 厂商为了应对微信小程序联合提出的方案)
Electron(PC、MacOS、Linux 跨端)
最后
前端发展日新月异,以上只是对前端技术的简单科普,有说错的地方还请斧正。
如果你对以上内容感兴趣且需要帮助的话,可以登录https://www.deepexi.com/product-new/27了解更多产品详情。
版权声明: 本文为 InfoQ 作者【滴普科技2048实验室】的原创文章。
原文链接:【http://xie.infoq.cn/article/826fc05536179bbc2a0f9710e】。文章转载请联系作者。
评论