0%

这篇文章主要讲怎么写一个 typescript 的描述文件(以 d.ts 结尾的文件名,比如 xxx.d.ts)。

总结一下:
从类型 type 角度分为:基本类型(string、number、boolean 等)及其混合;复杂类型(class、function、object)及其混合(比如说又是 class 又是 function)。
从代码有效范围分为:全局变量、模块变量和又是全局变量又是模块变量的。
从定义文件来说:自己写的.d.ts 文件和扩展别人写的.d.ts 文件。
以上三个角度,应该覆盖了描述文件的各个方面了。

2019.09.12 更新说明:

1
2
1.增加了用interface的方式声明函数。
2.增加了在使用模块化导入的情况下如何声明全局变量。

2018.12.18 更新说明:

1
2
3
1.增加了全局声明的原理说明。
2.增加了es6的import、export对应的d.ts文件写法。
3.增加了d.ts文件放置位置的说明。

发现了一个关于 typescript 比较好的入门教程:https://ts.xcatliu.com/basics...,这是其中的关于描述文件的文档。

最近开始从 js 转 ts 了。但是要用到一些描述文件(d.ts),常用的比如 jquery 等都可以通过 npm 下载到别人已经写好的npm install @types/jquery。但是还是有一些小众的或者公司内部的公共库或者以前写过的公用 js 代码需要自己手动写描述文件。

之前也从网上也找了一些资料,但还是看的云里雾里模糊不清,经过一段摸索,将摸索的结果记录下来,也希望可以给别人一个参考。

如果你只写 js,d.ts 对你来说也是有用的,大部分编辑器能识别 d.ts 文件,当你写 js 代码的时候给你智能提示。效果像这样:

image

详情可以看我以前写过的一些文章:https://segmentfault.com/a/11...

通常,我们写 js 的时候有两种引入 js 的方式:

1
2
1,在html文件中通过<script>标签全局引入全局变量。
2,通过模块加载器require其他js文件:比如这样var j=require('jquery')。

全局类型

首先以第一种方式举例。

变量

比如现在有一个全局变量,那对应的 d.ts 文件里面这样写。

1
declare var aaa:number

其中关键字 declare 表示声明的意思。在 d.ts 文件里面,在最外层声明变量或者函数或者类要在前面加上这个关键字。在 typescript 的规则里面,如果一个.ts、.d.ts 文件如果没有用到 import 或者 export 语法的话,那么最顶层声明的变量就是全局变量。

所以我们在这里声明了一个全局变量 aaa,类型是数字类型(number)。当然了也可以是 string 类型或者其他或者:

1
declare var aaa:number|string //注意这里用的是一个竖线表示"或"的意思

如果是常量的话用关键字 const 表示:

1
declare const max:200

函数

由上面的全局变量的写法我们很自然的推断出一个全局函数的写法如下:

1
2
/** id是用户的id,可以是number或者string */
declare function getName(id:number|string):string

最后的那个 string 表示的是函数的返回值的类型。如果函数没有返回值可以用 void 表示。
在 js 里面调用的时候就会提示:

image

我们上面写的注释,写 js 的时候还可以提示。

有时候同一个函数有若干种写法:

image

1
2
get(1234)
get("zhangsan",18)

那么 d.ts 对应的写法:

1
2
declare function get(id: string | number): string
declare function get(name:string,age:number): string

如果有些参数可有可无,可以加个?表示非必须。

1
declare function render(callback?:()=>void): string

js 中调用的时候,回调传不传都可以:

1
2
3
4
5
render()

render(function () {
alert('finish.')
})

用 interface 声明函数

也可以用 interface 去声明函数类型:

image

1
2
3
4
5
6
7
8
//Get是一种类型
declare interface Get{
(id: string): string
(name:string,age:number):string
}

//get是Get类型的
declare var get:Get

用起来长这个样子:

image

class

当然除了变量和函数外,我们还有类(class)。

1
2
3
4
5
6
7
8
declare class Person {

static maxAge: number //静态变量
static getMaxAge(): number //静态方法

constructor(name: string, age: number) //构造函数
getName(id: number): string
}

constructor 表示的是构造方法:

image

image

其中 static 表示静态的意思,用来表示静态变量和静态方法:

image

image

对象

1
2
3
declare namespace OOO{

}

当然了这个对象上面可能有变量,可能有函数可能有类。

1
2
3
4
5
6
7
8
9
10
11
12
declare namespace OOO{
var aaa: number | string
function getName(id: number | string): string
class Person {

static maxAge: number //静态变量
static getMaxAge(): number //静态方法

constructor(name: string, age: number) //构造函数
getName(id: number): string //实例方法
}
}

其实就是把上面的那些写法放到这个 namespace 包起来的大括号里面,注意括号里面就不需要 declare 关键字了。
效果:

image

image

image

对象里面套对象也是可以的:

1
2
3
4
5
6
7
declare namespace OOO{
var aaa: number | string
// ...
namespace O2{
let b:number
}
}

效果:

image

混合类型

有时候有些值既是函数又是 class 又是对象的复杂对象。比如我们常用的 jquery 有各种用法:

1
2
3
new $()
$.ajax()
$()

既是函数又是对象

1
2
3
4
5
declare function $2(s:string): void

declare namespace $2{
let aaa:number
}

效果:

作为函数用:

image

作为对象用:

image

也就是 ts 会自动把同名的 namespace 和 function 合并到一起。

既是函数,又是类(可以 new 出来),又是对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 实例方法
interface People{
name: string
age: number
getName(): string
getAge():number
}
interface People_Static{
/** 构造函数 */
new (name: string, age: number): People
new (id:number): People

/** 作为对象,调用对象上的方法或者变量 */
staticA():number
aaa:string

/** 作为函数使用 */
(w:number):number
(w:string):number
}
declare var People:People_Static

ts3.6 增加了新功能,function 声明和 class 声明可以合并了,所以又有了新的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/** 作为函数使用 */
declare function People(w: number): number
declare function People(w: string): number

declare class People {
/** 构造函数 */
constructor(name: string, age: number)
constructor(id: number)

// 实例属性和实例方法
name: string
age: number
getName(): string
getAge(): number

/** 作为对象,调用对象上的方法或者变量 */
static staticA(): number
static aaa: string
}

/** 作为对象,调用对象上的方法或者变量 */
declare namespace People {
export var abc: number
}

函数用 function,类用 class 声明,复杂对象就用 namespace,这样的对应关系简洁明了。

效果:

作为函数使用:

image

类的静态方法:

image

类的构造函数:

image

类的实例方法:

image

模块化的全局变量

这个是怎么回事呢,就是有时候我们定义全局变量的时候需要引入(别人写的)文件,比如这样的,我想声明个全局变量 req:

image

由于我们当前的 d.ts 文件使用了 import/export 语法,那么 ts 编译器就不把我们通过 declare var xxx:yyy 当成了全局变量了,那么我们就需要通过以下的方式声明全局变量:

1
2
3
4
5
6
7
8
9
10
import { Request,Response} from 'express'

declare global {
var req: Request
var res: Response

namespace OOO {
var a:number
}
}

用起来长这个样子:

image

其他类型(number、string blabla)就不一一举例了,参照上面的例子去掉 declare 填到 global 的大括号下就行了。

在 Ts 中定义 window 对象

1
2
3
4
5
declare global {
interface Window {
_czc: any
}
}

模块化(CommonJS)

除了上面的全局的方式,我们有时候还是通过 require 的方式引入模块化的代码。

比如这样的效果:

image

对应的写法是这样的:

1
2
3
4
5
6
7
declare module "abcde" {
export let a: number
export function b(): number
export namespace c{
let cd: string
}
}

其实就是外面套了一层 module "xxx",里面的写法和之前其实差不多,把declare换成了export

此外,有时候我们导出去的是一个函数本身,比如这样的:

image

对应的写法很简单,长这个样子:

1
2
3
4
declare module "app" {
function aaa(some:number):number
export=aaa
}

以此类推,导出一个变量或常量的话这么写:

1
2
3
4
declare module "ccc" {
const c:400
export=c
}

效果:

image

ES6 的模块化方式(import export)

1
2
3
4
5
declare var aaa: 1
declare var bbb: 2
declare var ccc: 3 //因为这个文件里我们使用了import或者export语法,所以bbb和ccc在其他代码里不能访问到,即不是全局变量

export { aaa }

使用:

1
2
3
4
import { a1, a2 } from "./A"

console.log(a1)
console.log(a2)

那么对应的 A.d.ts 文件是这样写的:

1
2
3
4
declare var a1: 1
declare var a2: 2

export { a1,a2 }

当然了也能这样写:

1
2
export declare var a1: 1
export declare var a2: 2

不过建议之前的第一种写法,原因看这里https://segmentfault.com/a/11...

当然了还有人经常问 default 导出的写法:

1
2
declare var a1: 1
export default a1

使用的时候当然就是这样用了:

1
2
3
import a1 from "./A";

console.log(a1)

UMD

有一种代码,既可以通过全局变量访问到,也可以通过 require 的方式访问到。比如我们最常见的 jquery:

image

image

其实就是按照全局的方式写 d.ts,写完后在最后加上declare module "xxx"的描述:

1
2
3
4
5
6
7
declare namespace UUU{
let a:number
}

declare module "UUU" {
export =UUU
}

效果这样:

作为全局变量使用:

image

作为模块加载使用:

image

其他

有时候我们扩展了一些内置对象。比如我们给 Date 增加了一个 format 的实例方法:

image

对应的 d.ts 描述文件这样写:

1
2
3
interface Date {
format(f: string): string
}

.d.ts 文件放到哪里

经常有人问写出来的 d.ts 文件(A.d.ts)文件放到哪个目录里,如果是模块化的话那就放到和源码(A.js)文件同一个目录下,如果是全局变量的话理论上放到哪里都可以————当然除非你在 tsconfig.json 文件里面特殊配置过。

什么是泛型呢?我们可以理解为泛型就是在编译期间不确定方法的类型(广泛之意思),在方法调用时,由程序员指定泛型具体指向什么类型。泛型在传统面向对象编程语言中是极为常见的,ts 中当然也执行泛型,如果你理解 c#或 java 中的泛型,相信本篇理解起来会很容易。

泛型函数、泛型类、泛型接口。

generic.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
/*
* ts中泛型
* 泛型就是在编译期间不确定方法的类型(广泛之意思),在方法调用时,由程序员指定泛型具体指向什么类型
*/

//1 泛型函数

/**
* 获取数组中最小值 (数字)
* @param {number[]} arr
* @returns {number}
*/
function getMinNumber(arr:number[]):number{
var min=arr[0];
arr.forEach((value)=>{
if(value<min){
min=value;
}
});
return min;
}

/**
* 获取数组中最小值 (字符串)
* @param {number[]} arr
* @returns {number}
*/
function getMinStr(arr:string[]):string{
var min=arr[0];
arr.forEach((value)=>{
if(value<min){
min=value;
}
});
return min;
}

console.log(getMinNumber([1, 3, 5, 7, 8]));//1
console.log(getMinStr(["tom","jerry","jack","sunny"]));//jack

/**
* 获取数组中最小值 (T泛型通用)
* @param {T[]} arr
* @returns {T}
*/
function getMin<T>(arr:T[]):T{
var min=arr[0];
arr.forEach((value)=>{
if(value<min){
min=value;
}
});
return min;
}

console.log(getMin([1, 3, 5, 7, 8]));
console.log(getMin(["tom","jerry","jack","sunny"]));


//2 泛型类
class GetMin<T>{
arr:T[]=[];
add(ele:T){
this.arr.push(ele);
}
min():T{
var min=this.arr[0];
this.arr.forEach(function (value) {
if(value<min){
min=value;
}
});
return min;
}
}
var gm1= new GetMin<number>();
gm1.add(5);
gm1.add(3);
gm1.add(2);
gm1.add(9);
console.log(gm1.min());

var gm2= new GetMin<string>();
gm2.add("tom");
gm2.add("jerry");
gm2.add("jack");
gm2.add("sunny");
console.log(gm2.min());



/**
* 3 泛型函数接口
*/
interface ConfigFn{
<T>(value:T):T;
}

var getData:ConfigFn=function<T>(value:T):T{
return value;
}
getData<string>('张三');
// getData<string>(1243); //错误


// 类似 Map<String,Object> Param 接口
interface Param{
[index:string]:any
}



//4 泛型类接口

/**
* page分页对象
*/
class Page{
private currentPage:number=1; //当前页码 默认1
private pageSize:number=10;//每页条数 默认为10
private sortName:string; //排序字段
private sortOrder:string="asc"; // 排序规则 asc | desc 默认为asc正序


constructor(param:Param){
if(param["currentPage"]){
this.currentPage=param["currentPage"];
}
if(param["pageSize"]){
this.pageSize=param["pageSize"];
}
if(param["sortName"]){
this.sortName=param["sortName"];
}
if(param["sortOrder"]){
this.sortOrder=param["sortOrder"];
}
}

public getStartNum():number{
return (this.currentPage-1)*this.pageSize;
}
}


class User{
id:number;//id主键自增
name:string;//姓名
sex:number;//性别 1男 2女
age:number;//年龄
city:string;//城市
describe:string;//描述

}

//泛型接口
interface BaseDao<T> {
findById(id:number):T;//根据主键id查询一个实体
findPageList(param:Param,page:Page):T[];//查询分页列表
findPageCount(param:Param):number;//查询分页count
save(o:T):void;//保存一个实体
update(o:T):void;//更新一个实体
deleteById(id:number);//删除一个实体
}

/**
* 接口实现类
*/
class UserDao<User> implements BaseDao<User>{
findById(id:number):User{

return null;
}
findPageList(param:Param,page:Page):User[]{
return [];
}
findPageCount(param:Param):number{
return 0;
}
save(o:User):void{

}
update(o:User):void{

}
deleteById(id:number){

}
}

文章结构:

  • React 中的虚拟 DOM 是什么?
  • 虚拟 DOM 的简单实现(diff 算法)
  • 虚拟 DOM 的内部工作原理
  • React 中的虚拟 DOM 与 Vue 中的虚拟 DOM 比较

React 中的虚拟 DOM 是什么?

虽然 React 中的虚拟 DOM 很好用,但是这是一个无心插柳的结果。

React 的核心思想:一个 Component 拯救世界,忘掉烦恼,从此不再操心界面

1. Virtual Dom 快,有两个前提

1.1 Javascript 很快

Chrome 刚出来的时候,在 Chrome 里跑 Javascript 非常快,给了其它浏览器很大压力。而现在经过几轮你追我赶,各主流浏览器的 Javascript 执行速度都很快了。

https://julialang.org/benchmarks/ 这个网站上,我们可以看到,JavaScript 语言已经非常快了,和 C 就是几倍的关系,和 java 在同一个量级。所以说,单纯的 JavaScript 还是很快的。

1.2 Dom 很慢

当创建一个元素比如 div,有以下几项内容需要实现: HTML elementElementGlobalEventHandler。简单的说,就是插入一个 Dom 元素的时候,这个元素上本身或者继承很多属性如 width、height、offsetHeight、style、title,另外还需要注册这个元素的诸多方法,比如 onfocus、onclick 等等。 这还只是一个元素,如果元素比较多的时候,还涉及到嵌套,那么元素的属性和方法等等就会很多,效率很低。

比如,我们在一个空白网页的 body 中添加一个 div 元素,如下所示:

image

这个元素会挂载默认的 styles、得到这个元素的 computed 属性、注册相应的 Event Listener、DOM Breakpoints 以及大量的 properties,这些属性、方法的注册肯定是需要耗费大量时间的

尤其是在 js 操作 DOM 的过程中,不仅有 dom 本身的繁重,js 的操作也需要浪费时间,我们认为 js 和 DOM 之间有一座桥,如果你频繁的在桥两边走动,显然效率是很低的,如果你的 JavaScript 操作 DOM 的方式还非常不合理,那么显然就会更糟糕了

而 React 的虚拟 DOM 就是解决这个问题的! 虽然它解决不了 DOM 自身的繁重,但是虚拟 DOM 可以对 JavaScript 操作 DOM 这一部分内容进行优化

比如说,现在你的 list 是这样:

1
2
3
4
5
6
<ul>
<li>0</li>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>

你希望把它变成下面这样:

1
2
3
4
5
6
7
<ul>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
<li>10</li>
</ul>

通常的操作是什么?

先把 0, 1,2,3 这些 Element 删掉,然后加几个新的 Element 6,7,8,9,10 进去,这里面就有 4 次 Element 删除,5 次 Element 添加。共计 9 次 DOM 操作。

那 React 的虚拟 DOM 可以怎么做呢?

而 React 会把这两个做一下 Diff,然后发现其实不用删除 0,1,2,3,而是可以直接改 innerHTML,然后只需要添加一个 Element(10)就行了,这样就是 4 次 innerHTML 操作加 1 个 Element 添加。共计 5 次操作,这样效率的提升是非常可观的。

2、 关于 React

2.1 接口和设计

在 React 的设计中,是完全不需要你来操作 DOM 的。我们也可以认为,在 React 中根本就没有 DOM 这个概念,有的只是 Component。

当你写好一个 Component 以后,Component 会完全负责 UI,你不需要也不应该去也不能够指挥 Component 怎么显示,你只能告诉它你想要显示一个香蕉还是两个梨。

隔离 DOM 并不仅仅是因为 DOM 慢,而也是为了把界面和业务完全隔离,操作数据的只关心数据,操作界面的只关心界面。比如在 websocket 聊天室的创建房间时,我们可以首先把 Component 写好,然后当获取到数据的时候,只要把数据放在 redux 中就好,然后 Component 就自动把房间添加到页面中去,而不是你先拿到数据,然后使用 js 操作 DOM 把数据显示在页面上。

我提供一个 Component,然后你只管给我数据,界面的事情完全不用你操心,我保证会把界面变成你想要的样子。所以说 React 的着力点就在于 View 层,即 React 专注于 View 层。你可以把一个 React 的 Component 想象成一个 Pure Function,只要你给的数据是[1, 2, 3],我保证显示的是[1, 2, 3]。没有什么删除一个 Element,添加一个 Element 这样的事情。NO。你要我显示什么就给我一个完整的列表。

另外,Flux 虽然说的是单向的 Data Flow(redux 也是),但是实际上就是单向的 Observer,Store->View->Action->Store(箭头是数据流向,实现上可以理解为 View 监听 Store,View 直接 trigger action,然后 Store 监听 Action)。

2.2 实现

那么 react 如何实现呢? 最简单的方法就是当数据变化时,我直接把原先的 DOM 卸载,然后把最新数据的 DOM 替换上去。 但是,虚拟 DOM 哪去了? 这样做的效率显然是极低的。

所以虚拟 DOM 就来救场了。

那么虚拟 DOM 和 DOM 之间的关系是什么呢?

首先,Virtual DOM 并没有完全实现 DOM,即虚拟 DOM 和真正地 DOM 是不一样的,Virtual DOM 最主要的还是保留了 Element 之间的层次关系和一些基本属性。因为真实 DOM 实在是太复杂,一个空的 Element 都复杂得能让你崩溃,并且几乎所有内容我根本不关心好吗。所以 Virtual DOM 里每一个 Element 实际上只有几个属性,即最重要的,最为有用的,并且没有那么多乱七八糟的引用,比如一些注册的属性和函数啊,这些都是默认的,创建虚拟 DOM 进行 diff 的过程中大家都一致,是不需要进行比对的。所以哪怕是直接把 Virtual DOM 删了,根据新传进来的数据重新创建一个新的 Virtual DOM 出来都非常非常非常快。(每一个 component 的 render 函数就是在做这个事情,给新的 virtual dom 提供 input)。

所以,引入了 Virtual DOM 之后,React 是这么干的:你给我一个数据,我根据这个数据生成一个全新的 Virtual DOM,然后跟我上一次生成的 Virtual DOM 去 diff,得到一个 Patch,然后把这个 Patch 打到浏览器的 DOM 上去。完事。并且这里的 patch 显然不是完整的虚拟 DOM,而是新的虚拟 DOM 和上一次的虚拟 DOM 经过 diff 后的差异化的部分。

假设在任意时候有,VirtualDom1 == DOM1 (组织结构相同, 显然虚拟 DOM 和真实 DOM 是不可能完全相等的,这里的==是 js 中非完全相等)。当有新数据来的时候,我生成 VirtualDom2,然后去和 VirtualDom1 做 diff,得到一个 Patch(差异化的结果)。然后将这个 Patch 去应用到 DOM1 上,得到 DOM2。如果一切正常,那么有 VirtualDom2 == DOM2(同样是结构上的相等)。

这里你可以做一些小实验,去破坏 VirtualDom1 == DOM1 这个假设(手动在 DOM 里删除一些 Element,这时候 VirtualDom 里的 Element 没有被删除,所以两边不一样了)。
然后给新的数据,你会发现生成的界面就不是你想要的那个界面了。

最后,回到为什么 Virtual Dom 快这个问题上。
其实是由于每次生成 virtual dom 很快,diff 生成 patch 也比较快,而在对 DOM 进行 patch 的时候,虽然 DOM 的变更比较慢,但是 React 能够根据 Patch 的内容,优化一部分 DOM 操作,比如之前的那个例子。

重点就在最后,哪怕是我生成了 virtual dom(需要耗费时间),哪怕是我跑了 diff(还需要花时间),但是我根据 patch 简化了那些 DOM 操作省下来的时间依然很可观(这个就是时间差的问题了,即节省下来的时间 > 生成 virtual dom 的时间 + diff 时间)。所以总体上来说,还是比较快。

简单发散一下思路,如果哪一天,DOM 本身的操作已经非常非常非常快了,并且我们手动对于 DOM 的操作都是精心设计优化过后的,那么加上了 VirtualDom 还会快吗?
当然不行了,毕竟你多做了这么多额外的工作。

    但是那一天会来到吗?
    诶,大不了到时候不用Virtual DOM。

注: 此部分内容整理自:https://www.zhihu.com/question/29504639/answer/44680878

虚拟 DOM 的简单实现(diff 算法)

目录

  • 1 前言
  • 2 对前端应用状态管理思考
  • 3 Virtual DOM 算法
  • 4 算法实现
    • 4.1 步骤一:用 JS 对象模拟 DOM 树
    • 4.2 步骤二:比较两棵虚拟 DOM 树的差异
    • 4.3 步骤三:把差异应用到真正的 DOM 树上
  • 5 结语

前言

在上面一部分中,我们已经简单介绍了虚拟 DOM 的答题思路和好处,这里我们将通过自己写一个虚拟 DOM 来加深对其的理解,有一些自己的思考。

对前端应用状态管理思考

维护状态,更新视图。

虚拟 DOM 算法

DOM 是很慢的,如果我们创建一个简单的 div,然后把他的所有的属性都打印出来,你会看到:

1
2
3
4
5
6
var div = document.createElement('div'),
str = '';
for (var key in div) {
str = str + ' ' + key;
}
console.log(str);

image

可以看到,这些属性还是非常惊人的,包括样式的修饰特性、一般的特性、方法等等,如果我们打印出其长度,可以得到惊人的 227 个。
而这仅仅是一层,真正的 DOM 元素是非常庞大的,这是因为标准就是这么设计的,而且操作他们的时候你要小心翼翼,轻微的触碰就有可能导致页面发生重排,这是杀死性能的罪魁祸首。

而相对于 DOM 对象,原生的 JavaScript 对象处理起来更快,而且更简单,DOM 树上的结构信息我们都可以使用 JavaScript 对象很容易的表示出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var element = {
tagName: 'ul',
props: {
id: 'list'
},
children: {
{
tagName: 'li',
props: {
class: 'item'
},
children: ['Item1']
},
{
tagName: 'li',
props: {
class: 'item'
},
children: ['Item1']
},
{
tagName: 'li',
props: {
class: 'item'
},
children: ['Item1']
}
}
}

如上所示,对于一个元素,我们只需要一个 JavaScript 对象就可以很容易的表示出来,这个对象中有三个属性:

  1. tagName: 用来表示这个元素的标签名。
  2. props: 用来表示这元素所包含的属性。
  3. children: 用来表示这元素的 children。

而上面的这个对象使用 HTML 表示就是:

1
2
3
4
5
<ul id='list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ul>

OK! 既然原来的 DOM 信息可以使用 JavaScript 来表示,那么反过来,我们就可以用这个 JavaScript 对象来构建一个真正的 DOM 树。

所以之前所说的状态变更的时候会重新构建这个 JavaScript 对象,然后呢,用新渲染的对象和旧的对象去对比, 记录两棵树的差异,记录下来的就是我们需要改变的地方。 这就是所谓的虚拟 DOM,包括下面的几个步骤:

  1. 用 JavaScript 对象来表示 DOM 树的结构; 然后用这个树构建一个真正的 DOM 树,插入到文档中。
  2. 当状态变更的时候,重新构造一个新的对象树,然后用这个新的树和旧的树作对比,记录两个树的差异。
  3. 把 2 所记录的差异应用在步骤一所构建的真正的 DOM 树上,视图就更新了。

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

算法实现

1、 用 JavaScript 对象模拟 DOM 树

用 JavaScript 对象来模拟一个 DOM 节点并不难,你只需要记录他的节点类型(tagName)、属性(props)、子节点(children)。

element.js

1
2
3
4
5
6
7
8
function Element(tagName, props, children) {
this.tagName = tagName;
this.props = props;
this.children = children;
}
module.exports = function (tagName, props, children) {
return new Element(tagName, props, children);
}

通过这个构造函数,我们就可以传入标签名、属性以及子节点了,tagName 可以在我们 render 的时候直接根据它来创建真实的元素,这里的 props 使用一个对象传入,可以方便我们遍历。

基本使用方法如下:

1
2
3
4
5
6
7
var el = require('./element');

var ul = el('ul', {id: 'list'}, [
el('li', {class: 'item'}, ['item1']),
el('li', {class: 'item'}, ['item2']),
el('li', {class: 'item'}, ['item3'])
]);

然而,现在的 ul 只是 JavaScript 表示的一个 DOM 结构,页面上并没有这个结构,所有我们可以根据 ul 构建一个真正的<ul>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Element.prototype.render = function () {
// 根据tagName创建一个真实的元素
var el = document.createElement(this.tagName);
// 得到这个元素的属性对象,方便我们遍历。
var props = this.props;

for (var propName in props) {
// 获取到这个元素值
var propValue = props[propName];

// 通过setAttribute设置元素属性。
el.setAttribute(propName, propValue);
}

// 注意: 这里的children,我们传入的是一个数组,所以,children不存在时我们用【】来替代。
var children = this.children || [];

//遍历children
children.forEach(function (child) {
var childEl = (child instanceof Element)
? child.render()
: document.createTextNode(child);
// 无论childEl是元素还是文字节点,都需要添加到这个元素中。
el.appendChild(childEl);
});

return el;
}

所以,render 方法会根据 tagName 构建一个真正的 DOM 节点,然后设置这个节点的属性,最后递归的把自己的子节点也构建起来,所以只需要调用 ul 的 render 方法,通过 document.body.appendChild 就可以挂载到真实的页面上了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>div</title>
</head>
<body>
<script>

function Element(tagName, props, children) {
this.tagName = tagName;
this.props = props;
this.children = children;
}


var ul = new Element('ul', {id: 'list'}, [
new Element('li', {class: 'item'}, ['item1']),
new Element('li', {class: 'item'}, ['item2']),
new Element('li', {class: 'item'}, ['item3'])
]);

Element.prototype.render = function () {
// 根据tagName创建一个真实的元素
var el = document.createElement(this.tagName);
// 得到这个元素的属性对象,方便我们遍历。
var props = this.props;

for (var propName in props) {
// 获取到这个元素值
var propValue = props[propName];

// 通过setAttribute设置元素属性。
el.setAttribute(propName, propValue);
}

// 注意: 这里的children,我们传入的是一个数组,所以,children不存在时我们用【】来替代。
var children = this.children || [];

//遍历children
children.forEach(function (child) {
var childEl = (child instanceof Element)
? child.render()
: document.createTextNode(child);
// 无论childEl是元素还是文字节点,都需要添加到这个元素中。
el.appendChild(childEl);
});

return el;
}

var ulRoot = ul.render();
document.body.appendChild(ulRoot);
</script>
</body>
</html>

上面的这段代码,就可以渲染出下面的结果了:

image

2、比较两颗虚拟 DOM 树的差异

比较两颗 DOM 树的差异是 Virtual DOM 算法中最为核心的部分,这也就是所谓的 Virtual DOM 的 diff 算法。 两个树的完全的 diff 算法是一个时间复杂度为 O(n3) 的问题。 但是在前端中,你会很少跨层地移动 DOM 元素,所以真实的 DOM 算法会对同一个层级的元素进行对比。

image

上图中,div 只会和同一层级的 div 对比,第二层级的只会和第二层级对比。 这样算法复杂度就可以达到 O(n)。

(1)深度遍历优先,记录差异

在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每一个节点就会有一个唯一的标记:

image

上面的这个遍历过程就是深度优先,即深度完全完成之后,再转移位置。 在深度优先遍历的时候,每遍历到一个节点就把该节点和新的树进行对比,如果有差异的话就记录到一个对象里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// diff函数,对比两颗树
function diff(oldTree, newTree) {
// 当前的节点的标志。因为在深度优先遍历的过程中,每个节点都有一个index。
var index = 0;

// 在遍历到每个节点的时候,都需要进行对比,找到差异,并记录在下面的对象中。
var pathches = {};

// 开始进行深度优先遍历
dfsWalk(oldTree, newTree, index, pathches);

// 最终diff算法返回的是一个两棵树的差异。
return pathches;
}

// 对两棵树进行深度优先遍历。
function dfsWalk(oldNode, newNode, index, pathches) {
// 对比oldNode和newNode的不同,记录下来
pathches[index] = [...];

diffChildren(oldNode.children, newNode.children, index, pathches);
}

// 遍历子节点
function diffChildren(oldChildren, newChildren, index, pathches) {
var leftNode = null;
var currentNodeIndex = index;
oldChildren.forEach(function (child, i) {
var newChild = newChildren[i];
currentNodeIndex = (leftNode && leftNode.count)
? currentNodeIndex + leftNode.count + 1
: currentNodeIndex + 1

// 深度遍历子节点
dfsWalk(child, newChild, currentNodeIndex, pathches);
leftNode = child;
});
}

例如,上面的 div 和新的 div 有差异,当前的标记是 0, 那么我们可以使用数组来存储新旧节点的不同:

1
patches[0] = [{difference}, {difference}, ...]

同理使用 patches[1]来记录 p,使用 patches[3]来记录 ul,以此类推。

(2)差异类型

上面说的节点的差异指的是什么呢? 对 DOM 操作可能会:

  1. 替换原来的节点,如把上面的 div 换成了 section。
  2. 移动、删除、新增子节点, 例如上面 div 的子节点,把 p 和 ul 顺序互换。
  3. 修改了节点的属性。
  4. 对于文本节点,文本内容可能会改变。 例如修改上面的文本内容 2 内容为 Virtual DOM2.
      所以,我们可以定义下面的几种类型:
1
2
3
4
var REPLACE = 0;
var REORDER = 1;
var PROPS = 2;
var TEXT = 3;

对于节点替换,很简单,判断新旧节点的 tagName 是不是一样的,如果不一样的说明需要替换掉。 如 div 换成了 section,就记录下:

1
2
3
4
patches[0] = [{
type: REPALCE,
node: newNode // el('section', props, children)
}]

除此之外,如果给 div 新增了属性 id 为 container,就记录下:

1
2
3
4
5
6
7
8
9
10
11
12
pathches[0] = [
{
type: REPLACE,
node: newNode
},
{
type: PROPS,
props: {
id: 'container'
}
}
]

如果是文本节点发生了变化,那么就记录下:

1
2
3
4
5
6
pathches[2] = [
{
type: TEXT,
content: 'virtual DOM2'
}
]

那么如果我们把 div 的子节点重新排序下了呢? 比如 p、ul、div 的顺序换成了 div、p、ul,那么这个该怎么对比呢? 如果按照同级进行顺序对比的话,他们就会被替换掉,如 p 和 div 的 tagName 不同,p 就会被 div 所代替,最终,三个节点就都会被替换,这样 DOM 开销就会非常大,而实际上是不需要替换节点的,只需要移动就可以了, 我们只需要知道怎么去移动。这里牵扯到了两个列表的对比算法,如下。

(3)列表对比算法

假设现在可以用英文字母唯一地标识每一个子节点:

旧的节点顺序:

1
a b c d e f g h i

现在对节点进行了删除、插入、移动的操作。新增 j 节点,删除 e 节点,移动 h 节点:

新的节点顺序:

1
a b c h d f g i j

现在知道了新旧的顺序,求最小的插入、删除操作(移动可以看成是删除和插入操作的结合)。这个问题抽象出来其实是字符串的最小编辑距离问题(Edition Distance),最常见的解决算法是 Levenshtein Distance,通过动态规划求解,时间复杂度为 O(M * N)。但是我们并不需要真的达到最小的操作,我们只需要优化一些比较常见的移动情况,牺牲一定 DOM 操作,让算法时间复杂度达到线性的(O(max(M, N))。具体算法细节比较多,这里不累述,有兴趣可以参考代码。

我们能够获取到某个父节点的子节点的操作,就可以记录下来:

1
2
3
4
patches[0] = [{
type: REORDER,
moves: [{remove or insert}, {remove or insert}, ...]
}]

但是要注意的是,因为 tagName 是可重复的,不能用这个来进行对比。所以需要给子节点加上唯一标识 key,列表对比的时候,使用 key 进行对比,这样才能复用老的 DOM 树上的节点。

这样,我们就可以通过深度优先遍历两棵树,每层的节点进行对比,记录下每个节点的差异了。完整 diff 算法代码可见 diff.js。

3、把差异引用到真正的 DOM 树上

因为步骤一所构建的 JavaScript 对象树和 render 出来真正的 DOM 树的信息、结构是一样的。所以我们可以对那棵 DOM 树也进行深度优先的遍历,遍历的时候从步骤二生成的 patches 对象中找出当前遍历的节点差异,然后进行 DOM 操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function patch (node, patches) {
var walker = {index: 0}
dfsWalk(node, walker, patches)
}

function dfsWalk (node, walker, patches) {
var currentPatches = patches[walker.index] // 从patches拿出当前节点的差异

var len = node.childNodes
? node.childNodes.length
: 0
for (var i = 0; i < len; i++) { // 深度遍历子节点
var child = node.childNodes[i]
walker.index++
dfsWalk(child, walker, patches)
}

if (currentPatches) {
applyPatches(node, currentPatches) // 对当前节点进行DOM操作
}
}

applyPatches,根据不同类型的差异对当前节点进行 DOM 操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function applyPatches (node, currentPatches) {
currentPatches.forEach(function (currentPatch) {
switch (currentPatch.type) {
case REPLACE:
node.parentNode.replaceChild(currentPatch.node.render(), node)
break
case REORDER:
reorderChildren(node, currentPatch.moves)
break
case PROPS:
setProps(node, currentPatch.props)
break
case TEXT:
node.textContent = currentPatch.content
break
default:
throw new Error('Unknown patch type ' + currentPatch.type)
}
})
}

5、结语

virtual DOM 算法主要实现上面步骤的三个函数: element、diff、patch,然后就可以实际的进行使用了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1. 构建虚拟DOM
var tree = el('div', {'id': 'container'}, [
el('h1', {style: 'color: blue'}, ['simple virtal dom']),
el('p', ['Hello, virtual-dom']),
el('ul', [el('li')])
])

// 2. 通过虚拟DOM构建真正的DOM
var root = tree.render()
document.body.appendChild(root)

// 3. 生成新的虚拟DOM
var newTree = el('div', {'id': 'container'}, [
el('h1', {style: 'color: red'}, ['simple virtal dom']),
el('p', ['Hello, virtual-dom']),
el('ul', [el('li'), el('li')])
])

// 4. 比较两棵虚拟DOM树的不同
var patches = diff(tree, newTree)

// 5. 在真正的DOM元素上应用变更
patch(root, patches)

当然这是非常粗糙的实践,实际中还需要处理事件监听等;生成虚拟 DOM 的时候也可以加入 JSX 语法。这些事情都做了的话,就可以构造一个简单的 ReactJS 了。

数组和链表是两种基本的数据结构,他们在内存存储上的表现不一样,所以也有各自的特点。

大致总结一下特点和区别,拿几个人一起去看电影时坐座位为例。

数组的特点

  • 在内存中,数组是一块连续的区域。 拿上面的看电影来说,这几个人在电影院必须坐在一起。
  • 数组需要预留空间,在使用前要先申请占内存的大小,可能会浪费内存空间。 比如看电影时,为了保证 10 个人能坐在一起,必须提前订好 10 个连续的位置。这样的好处就是能保证 10 个人可以在一起。但是这样的缺点是,如果来的人不够 10 个,那么剩下的位置就浪费了。如果临时又多来了个人,那么 10 个就不够用了,这时可能需要将第 11 个位置上的人挪走,或者是他们 11 个人重新去找一个 11 连坐的位置,效率都很低。如果没有找到符合要求的座位,那么就没法坐了。
  • 插入数据和删除数据效率低,插入数据时,这个位置后面的数据在内存中都要向后移。删除数据时,这个数据后面的数据都要往前移动。 比如原来去了 5 个人,然后后来又去了一个人要坐在第三个位置上,那么第三个到第五个都要往后移动一个位子,将第三个位置留给新来的人。 当这个人走了的时候,因为他们要连在一起的,所以他后面几个人要往前移动一个位置,把这个空位补上。
  • 随机读取效率很高。因为数组是连续的,知道每一个数据的内存地址,可以直接找到该地址的数据。
  • 并且不利于扩展,数组定义的空间不够时要重新定义数组。

链表的特点

  • 在内存中可以存在任何地方,不要求连续。 在电影院几个人可以随便坐。
  • 每一个数据都保存了下一个数据的内存地址,通过这个地址找到下一个数据。 第一个人知道第二个人的座位号,第二个人知道第三个人的座位号……
  • 增加数据和删除数据很容易。 再来个人可以随便坐,比如来了个人要做到第三个位置,那他只需要把自己的位置告诉第二个人,然后问第二个人拿到原来第三个人的位置就行了。其他人都不用动。
  • 查找数据时效率低,因为不具有随机访问性,所以访问某个位置的数据都要从第一个数据开始访问,然后根据第一个数据保存的下一个数据的地址找到第二个数据,以此类推。 要找到第三个人,必须从第一个人开始问起。
  • 不指定大小,扩展方便。链表大小不用定义,数据随意增删。

各自的优缺点

数组的优点

  • 随机访问性强
  • 查找速度快

数组的缺点

  • 插入和删除效率低
  • 可能浪费内存
  • 内存空间要求高,必须有足够的连续内存空间。
  • 数组大小固定,不能动态拓展

链表的优点

  • 插入删除速度快
  • 内存利用率高,不会浪费内存
  • 大小没有固定,拓展很灵活。

链表的缺点

  • 不能随机查找,必须从第一个开始遍历,查找效率低

  • | 数组 | 链表 |
    | —- | —- | —- |
    | 读取 | O(1) | O(n) |
    | 插入 | O(n) | O(1) |
    | 删除 | O(n) | O(1) |

image

html 布局三部分

1
2
3
4
5
6
7
<div class="wrap">
<div class="header">header</div>
<div class="main">
弹性滚动区域
</div>
<div class="footer">footer</div>
</div>

css:

flex 布局方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
*{
margin:0;
padding:0;
}
html,body{
height:100%;
}
.wrap{
display:-webkit-box;
display:-webkit-flex;
display:-ms-flexbox;
display:flex;
-webkit-box-orient:vertical;
-webkit-flex-direction:column;
-ms-flex-direction:column;
flex-direction:column;
width:100%;
height:100%;
}
.header,.footer{
height:40px;
line-height:40px;
text-align: center;
background-color:#D8D8D8;
}
.main{
-webkit-box-flex:1;
-webkit-flex:1;
-ms-flex:1;
flex:1;
width:100%;
padding:10px;
box-sizing: border-box;
}

absolute 布局方式:

1
2
3
4
5
6
7
*{ padding:0; margin:0; }
html,body{height:100%;}
.wrap{width:100%;}
.header,.footer{height:40px;line-height:40px;background-color:#D8D8D8;text-align:center;}
.header{position: absolute;top:0;left:0;width:100%;}
.footer{position: absolute;bottom:0;left:0;width:100%;}
.main{position:absolute;z-index:1;top:40px;left:0;bottom:40px;width:100%;}

一、前端 HTML 部分

1
2
3
4
5
<div class='main'>
<input type='file' class='filebutton' style='display:none' οnchange='fileSelected()' /> <br>
<button class="upload" οnclick='openFileDialog()' > 选择文件上传 </button>
<div class="img"></div>
</div>

二、js 部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
//点击普通按钮,打开文件选择框
function openFileDialog()
{
$(".filebutton").click();
}
//选择一个文件时onchange事件被触发
function fileSelected()
{
var fbutton = $(".filebutton")[0];//dom元素
//读取文件
var reader = new FileReader();
reader.onload = function(e)
{
var dataURL = e.target.result;//'data:image/jpeg;base64,/9j/4AAQSk...(base64编码)...'
//alert(data);
var htmlImg = "<img src = '" + dataURL + "'/>";
$(".img").html(htmlImg);
}
var file = fbutton.files[0];
reader.readAsDataURL(file);

startFileUpload(file);
}
//开始上传
function startFileUpload(file)
{
var uploadURL = "FileUploadServer";

//手工构造一个form对象
var formData = new FormData();
formData.append("file" , file);// 'file' 为HTTP Post里的字段名, file 对浏览器里的File对象
//手工构造一个请求对象,用这个对象发送表单数据
//设置 progress, load, error, abort 4个事件处理器
var request = new XMLHttpRequest();
request.upload.addEventListener("progress" , window.evt_upload_progress , false);
request.addEventListener("load", window.evt_upload_complete, false);
request.addEventListener("error", window.evt_upload_failed, false);
request.addEventListener("abort", window.evt_upload_cancel, false);
request.open("POST", uploadURL ); // 设置服务URL
request.send(formData); // 发送表单数据
}
window.evt_upload_progress = function(evt)
{
if(evt.lengthComputable)
{

var progress = Math.round(evt.loaded * 100 / evt.total);
console.log("上传进度" + progress);
}
};
window.evt_upload_complete = function (evt)
{
if(evt.loaded == 0)
{
console.log ("上传失败!");
}
else
{
console.log ("上传完成!");
var response = JSON.parse(evt.target.responseText);
console.log (response);
}
};
window.evt_upload_failed = function (evt)
{
console.log ("上传出错");
};
window.evt_upload_cancel = function (evt)
{
console.log( "上传中止!");
};

三、后端部分,需要两个 jar 包的支持,他们分别是:commons-fileupload-1.3.1.jar commons-io-2.4.jar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package my.fileUpload;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.util.Streams;
import org.json.JSONObject;

public class FileUploadServer extends HttpServlet {

File tmpDir;//文件保存的临时目录

@Override
public void init() throws ServletException {
System.out.println("初始化");
File webRoot = new File(getServletContext().getRealPath("/"));
tmpDir = new File(webRoot , "upload");
if(!tmpDir.exists()) tmpDir.mkdirs();
}

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doPost(request , response);

}

public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("进入");
int error = 0;
String reason = "OK";
String data = null;
try {
data = doUpload(request , response);
} catch (Exception e) {
error = -1;
reason = e.getMessage();
// TODO Auto-generated catch block
e.printStackTrace();
}

JSONObject jreq = new JSONObject();
jreq.put("error", error);
jreq.put("reason", reason);
if(data != null) jreq.put("data", data);
response.setCharacterEncoding("utf-8");
response.setContentType("text/plain");
PrintWriter out = response.getWriter();
out.write(jreq.toString(2));

}

private String doUpload(HttpServletRequest request, HttpServletResponse response) throws Exception
{
String result = null;
boolean isMultipart = ServletFileUpload.isMultipartContent(request);
if(!isMultipart)
throw new Exception("请求编码必须为: multipart/form-data !");
request.setCharacterEncoding("utf-8");
ServletFileUpload upload = new ServletFileUpload();
FileItemIterator iter = upload.getItemIterator(request);
while(iter.hasNext())
{
//表单域
FileItemStream item = iter.next();
String fieldName = item.getFieldName();
InputStream fieldStream = item.openStream();
if(item.isFormField())
{
//普通表单域直接读取
String fieldValue = Streams.asString(fieldStream , "utf-8");
System.out.println("表单域:" + fieldName + "=" + fieldValue);
}
else
{
String realName = item.getName();//原始文件名
//文件的后缀名
String suffix = realName.substring(realName.lastIndexOf(".")+1);
System.out.println("文件名:" + realName + "....." + "后缀名:" + suffix);

//创建已个临时文件名
String s = UUID.randomUUID().toString();
String s2 = s.substring(0,8)+s.substring(9,13)+s.substring(14,18)+s.substring(19,23)+s.substring(24);
s2 = s2.toUpperCase();
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss");
String dateStr = sdf.format(new Date());
String fileName = dateStr +"-" + s2 + "." + suffix;
result = fileName;
System.out.println("文件名:" + fileName);
File file = new File(tmpDir , fileName);
long fileSsize = 0;//文件大小
System.out.println("===========文件开始上传=============");
//从FieldStream读取数据,保存到目标文件
file.getParentFile().mkdirs();
FileOutputStream fileStream = new FileOutputStream(file);
try
{
byte[] buf = new byte[1024];
while(true)
{
int n = fieldStream.read(buf);
if(n < 0) break;
if(n == 0) continue;
fileStream.write(buf, 0, n);

fileSsize += n;
}
}finally
{
fileStream.close();
fieldStream.close();
}
System.out.println("上传完成!");


}
}
return result;
}
}

try-catch 能抛出 promise 的异常吗?

1
2
3
4
5
try {
throw new Error('1')
} catch(error) {
console.log(error)
}

这是最常见的 try-catch,会 log 下面的内容:

image

注意,这里并不是红色的,因为 js 异常被捕获后,js 是能够正常往下执行的,如果没有被捕获的话,那么 js 将抛出异常,js 执行将会停止!

例子:

1
2
3
4
5
6
7
8
9
// 异步,宏任务
try {
setTimeout(function() {
console.log(b);
}, 0);
} catch (error) {
console.log(error); // 这里是不会执行的
}
console.log('out try catch')

image

此时 js 会抛出异常,catch 后面的代码都不会执行

1
2
3
4
5
6
7
8
// 异步,微任务
try {
new Promise(() => {
throw new Error('new promise throw error');
});
} catch (error) {
console.log(error);
}

image

解释

try-catch 主要用于捕获异常,注意,这里的异常,是指同步函数的异常,如果 try 里面的异步方法出现了异常,此时catch 是无法捕获到异常的,原因是因为:当异步函数抛出异常时,对于宏任务而言,执行函数时已经将该函数推入栈,此时并不在 try-catch 所在的栈,所以 try-catch 并不能捕获到错误。对于微任务而言,比如 promise,promise 的构造函数的异常只能被自带的 reject 也就是.catch 函数捕获到。

解决方案

对于同步函数

放心用 try-catch 即可

对于异步函数-宏任务

window 有全局的错误捕获函数 onerror

1
2
3
4
5
6
7
8
9
10
try {
setTimeout(function() {
console.log(b);
}, 0);
} catch (error) {
console.log(error); // 这里是不会执行的
}
window.onerror = function() {
console.log(...arguments)
}

这时,是可以捕获到比如 setTimeout 的回调函数异常的,这里可以针对全局的异常做一些处理,比如数据上报等

image

对于异步函数-微任务

对于微任务,js 有专门捕获没有写 catch 的 promise,如下:

1
2
3
window.addEventListener('unhandledrejection', function() {
console.log(...arguments)
})

执行结果如下:

image

更多知识点

try-catch 中的异常只会抛出一层,即不会冒泡,也就是如果你有多层的 try-catch 然后异常已经被内层的 catch 捕获了,外层的 catch 是捕获不到异常的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
try {
try {
throw new Error('oops');
}
catch (ex) {
console.error('inner', ex.message);
}
finally {
console.log('finally');
}
}
catch (ex) {
console.error('outer', ex.message);
}

// Output:
// "inner" "oops"
// "finally"

解决方案是可以在内层的 catch 再手动 throw 出异常

一、为什么要处理异常?

异常是不可控的,会影响最终的呈现结果,但是我们有充分的理由去做这样的事情。

  1. 增强用户体验;
  2. 远程定位问题;
  3. 未雨绸缪,及早发现问题;
  4. 无法复现问题,尤其是移动端,机型,系统都是问题;
  5. 完善的前端方案,前端监控系统;

对于 JS 而言,我们面对的仅仅只是异常,异常的出现不会直接导致 JS 引擎崩溃,最多只会使当前执行的任务终止。

二、需要处理哪些异常?

对于前端来说,我们可做的异常捕获还真不少。总结一下,大概如下:

  • JS 语法错误、代码异常
  • AJAX 请求异常
  • 静态资源加载异常
  • Promise 异常
  • Iframe 异常
  • 跨域 Script error
  • 崩溃和卡顿

下面我会针对每种具体情况来说明如何处理这些异常。

三、Try-Catch 的误区

try-catch 只能捕获到同步的运行时错误,对语法和异步错误却无能为力,捕获不到。

  1. 同步运行时错误:
1
2
3
4
5
6
try {
let name = "jartto";
console.log(nam);
} catch (e) {
console.log("捕获到异常:", e);
}

输出:

1
捕获到异常:ReferenceError: nam is not defined    at :3:15
  1. 不能捕获到具体的语法错误,只有一个语法错误提示。我们修改一下代码,删掉一个单引号:
1
2
3
4
5
6
try {
let name = 'jartto
console.log(nam);
} catch (e) {
console.log("捕获到异常:", e);
}

输出:

1
Uncaught SyntaxError: Invalid or unexpected token不过语法错误在我们开发阶段就可以看到,应该不会顺利上到线上环境。
  1. 异步错误
1
2
3
4
5
6
7
8
try {
setTimeout(() => {
undefined.map((v) => v);
}, 1000);
} catch (e) {
console.log("捕获到异常:", e);
}

我们看看日志:

1
Uncaught TypeError: Cannot read property 'map' of undefined    at setTimeout (:3:11)

并没有捕获到异常,这是需要我们特别注意的地方。

四、window.onerror 不是万能的

当 JS 运行时错误发生时,window 会触发一个 ErrorEvent 接口的 error 事件,并执行 window.onerror()。

1
2
3
4
5
6
7
8
9
/**@param {String}  message    错误信息
* @param {String} source 出错文件
* @param {Number} lineno 行号
* @param {Number} colno 列号
* @param {Object} error Error对象(对象)
* */
window.onerror = function (message, source, lineno, colno, error) {
console.log("捕获到异常:", { message, source, lineno, colno, error });
};
  1. 首先试试同步运行时错误
1
window.onerror = function(message, source, lineno, colno, error) {// message:错误信息(字符串)。// source:发生错误的脚本URL(字符串)// lineno:发生错误的行号(数字)// colno:发生错误的列号(数字)// error:Error对象(对象)console.log('捕获到异常:',{message, source, lineno, colno, error});}Jartto;

可以看到,我们捕获到了异常:

image

  1. 再试试语法错误呢?
1
2
3
window.onerror = function(message, source, lineno, colno, error) {console.log('捕获到异常:',{message, source, lineno, colno, error});}

let name = 'Jartto

控制台打印出了这样的异常:

1
Uncaught SyntaxError: Invalid or unexpected token

什么,竟然没有捕获到语法错误?

  1. 怀着忐忑的心,我们最后来试试异步运行时错误:
1
window.onerror = function(message, source, lineno, colno, error) {    console.log('捕获到异常:',{message, source, lineno, colno, error});}setTimeout(() => {    Jartto;});

控制台输出了:

1
捕获到异常:{message: "Uncaught ReferenceError: Jartto is not defined", source: "http://127.0.0.1:8001/", lineno: 36, colno: 5, error: ReferenceError: Jartto is not defined    at setTimeout (http://127.0.0.1:8001/:36:5)}
  1. 接着,我们试试网络请求异常的情况:

我们发现,不论是静态资源异常,或者接口异常,错误都无法捕获到。

补充一点:window.onerror 函数只有在返回 true 的时候,异常才不会向上抛出,否则即使是知道异常的发生控制台还是会显示 Uncaught Error: xxxxx

1
window.onerror = function(message, source, lineno, colno, error) {    console.log('捕获到异常:',{message, source, lineno, colno, error});    return true;}setTimeout(() => {    Jartto;});

控制台就不会再有这样的错误了:

1
Uncaught ReferenceError: Jartto is not defined    at setTimeout ((index):36)

需要注意:

  • onerror 最好写在所有 JS 脚本的前面,否则有可能捕获不到错误;
  • onerror 无法捕获语法错误;

到这里基本就清晰了:在实际的使用过程中,onerror 主要是来捕获预料之外的错误,而 try-catch 则是用来在可预见情况下监控特定的错误,两者结合使用更加高效。

问题又来了,捕获不到静态资源加载异常怎么办?

五、window.addEventListener

当一项资源(如图片或脚本)加载失败,加载资源的元素会触发一个 Event 接口的 error 事件,并执行该元素上的 onerror() 处理函数。这些 error 事件不会向上冒泡到 window ,不过(至少在 Firefox 中)能被单一的 window.addEventListener 捕获。

1
window.addEventListener('error', (error) => {    console.log('捕获到异常:', error);}, true)

控制台输出:

image

由于网络请求异常不会事件冒泡,因此必须在捕获阶段将其捕捉到才行,但是这种方式虽然可以捕捉到网络请求的异常,但是无法判断 HTTP 的状态是 404 还是其他比如 500 等等,所以还需要配合服务端日志再进行排查分析才可以。

需要注意

  • 不同浏览器下返回的 error 对象可能不同,需要注意兼容处理。
  • 需要注意避免 addEventListener 重复监听。

六、Promise Catch

在 promise 中使用 catch 可以非常方便的捕获到异步 error ,这个很简单。

没有写 catch 的 Promise 中抛出的错误无法被 onerror 或 try-catch 捕获到,所以我们务必要在 Promise 中不要忘记写 catch 处理抛出的异常。

解决方案:为了防止有漏掉的 Promise 异常,建议在全局增加一个对 unhandledrejection 的监听,用来全局监听 Uncaught Promise Error。使用方式:

1
window.addEventListener("unhandledrejection", function(e){  console.log(e);});

我们继续来尝试一下:

1
window.addEventListener("unhandledrejection", function(e){  e.preventDefault()  console.log('捕获到异常:', e);  return true;});Promise.reject('promise error');

可以看到如下输出:

image

那如果对 Promise 不进行 catch 呢?

1
window.addEventListener("unhandledrejection", function(e){  e.preventDefault()  console.log('捕获到异常:', e);  return true;});new Promise((resolve, reject) => {  reject('jartto: promise error');});

嗯,事实证明,也是会被正常捕获到的。

所以,正如我们上面所说,为了防止有漏掉的 Promise 异常,建议在全局增加一个对 unhandledrejection 的监听,用来全局监听 Uncaught Promise Error。

补充一点:如果去掉控制台的异常显示,需要加上:

1
event.preventDefault();

七、VUE errorHandler

1
Vue.config.errorHandler = (err, vm, info) => {  console.error('通过vue errorHandler捕获的错误');  console.error(err);  console.error(vm);  console.error(info);}

八、React 异常捕获 React 16 提供了一个内置函数 componentDidCatch,使用它可以非常简单的获取到 react 下的错误信息

1
componentDidCatch(error, info) {    console.log(error, info);}

除此之外,我们可以了解一下:error boundary UI 的某部分引起的 JS 错误不应该破坏整个程序,为了帮 React 的使用者解决这个问题,React 16 介绍了一种关于错误边界(error boundary)的新观念。

需要注意的是:error boundaries 并不会捕捉下面这些错误。

  1. 事件处理器
  2. 异步代码
  3. 服务端的渲染代码
  4. 在 error boundaries 区域内的错误
    我们来举一个小例子,在下面这个 componentDIdCatch(error,info) 里的类会变成一个 error boundary:
1
class ErrorBoundary extends React.Component {  constructor(props) {    super(props);    this.state = { hasError: false };  }   componentDidCatch(error, info) {    // Display fallback UI    this.setState({ hasError: true });    // You can also log the error to an error reporting service    logErrorToMyService(error, info);  }   render() {    if (this.state.hasError) {      // You can render any custom fallback UI      return

Something went wrong.

1
; } return this.props.children; }}

然后我们像使用普通组件那样使用它:

componentDidCatch() 方法像 JS 的 catch{} 模块一样工作,但是对于组件,只有 class 类型的组件(class component )可以成为一个 error boundaries 。

实际上,大多数情况下我们可以在整个程序中定义一个 error boundary 组件,之后就可以一直使用它了!

九、iframe 异常

对于 iframe 的异常捕获,我们还得借力 window.onerror:

1
window.onerror = function(message, source, lineno, colno, error) {  console.log('捕获到异常:',{message, source, lineno, colno, error});}

一个简单的例子可能如下:

十、Script error

一般情况,如果出现 Script error 这样的错误,基本上可以确定是出现了跨域问题。这时候,是不会有其他太多辅助信息的,但是解决思路无非如下:

跨源资源共享机制( CORS ):我们为 script 标签添加 crossOrigin 属性。

或者动态去添加 js 脚本:

1
const script = document.createElement('script');script.crossOrigin = 'anonymous';script.src = url;document.body.appendChild(script);

特别注意,服务器端需要设置:Access-Control-Allow-Origin

此外,我们也可以试试这个-解决 Script Error 的另类思路:

1
const originAddEventListener = EventTarget.prototype.addEventListener;EventTarget.prototype.addEventListener = function (type, listener, options) {  const wrappedListener = function (...args) {    try {      return listener.apply(this, args);    }    catch (err) {      throw err;    }  }  return originAddEventListener.call(this, type, wrappedListener, options);}

简单解释一下:

改写了 EventTarget 的 addEventListener 方法;对传入的 listener 进行包装,返回包装过的 listener,对其执行进行 try-catch;浏览器不会对 try-catch 起来的异常进行跨域拦截,所以 catch 到的时候,是有堆栈信息的;重新 throw 出来异常的时候,执行的是同域代码,所以 window.onerror 捕获的时候不会丢失堆栈信息;利用包装 addEventListener,我们还可以达到「扩展堆栈」的效果:

1
(() => {   const originAddEventListener = EventTarget.prototype.addEventListener;   EventTarget.prototype.addEventListener = function (type, listener, options) {+    // 捕获添加事件时的堆栈+    const addStack = new Error(`Event (${type})`).stack;     const wrappedListener = function (...args) {       try {         return listener.apply(this, args);       }       catch (err) {+        // 异常发生时,扩展堆栈+        err.stack += '' + addStack;         throw err;       }     }     return originAddEventListener.call(this, type, wrappedListener, options);   } })();

十一、崩溃和卡顿

卡顿也就是网页暂时响应比较慢, JS 可能无法及时执行。但崩溃就不一样了,网页都崩溃了,JS 都不运行了,还有什么办法可以监控网页的崩溃,并将网页崩溃上报呢?

崩溃和卡顿也是不可忽视的,也许会导致你的用户流失。

  1. 利用 window 对象的 load 和 beforeunload 事件实现了网页崩溃的监控。不错的文章,推荐阅读:Logging Information on Browser Crashes。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
window.addEventListener("load", function () {
sessionStorage.setItem("good_exit", "pending");
setInterval(function () {
sessionStorage.setItem("time_before_crash", new Date().toString());
}, 1000);
});
window.addEventListener("beforeunload", function () {
sessionStorage.setItem("good_exit", "true");
});
if (
sessionStorage.getItem("good_exit") &&
sessionStorage.getItem("good_exit") !== "true"
) {
/* insert crash logging code here */

alert(
"Hey, welcome back from your crash, looks like you crashed on: " +
sessionStorage.getItem("time_before_crash")
);
}
  1. 基于以下原因,我们可以使用 Service Worker 来实现网页崩溃的监控:

Service Worker 有自己独立的工作线程,与网页区分开,网页崩溃了,Service Worker 一般情况下不会崩溃;Service Worker 生命周期一般要比网页还要长,可以用来监控网页的状态;网页可以通过 navigator.serviceWorker.controller.postMessage API 向掌管自己的 SW 发送消息。

十二、错误上报

1.通过 Ajax 发送数据 因为 Ajax 请求本身也有可能会发生异常,而且有可能会引发跨域问题,一般情况下更推荐使用动态创建 img 标签的形式进行上报。

2.动态创建 img 标签的形式

1
function report(error) {  let reportUrl = 'http://jartto.wang/report';  new Image().src = `${reportUrl}?logs=${error}`;}

收集异常信息量太多,怎么办?实际中,我们不得不考虑这样一种情况:如果你的网站访问量很大,那么一个必然的错误发送的信息就有很多条,这时候,我们需要设置采集率,从而减缓服务器的压力:

1
Reporter.send = function(data) {  // 只采集 30%  if(Math.random() < 0.3) {    send(data)      // 上报错误信息  }}

采集率应该通过实际情况来设定,随机数,或者某些用户特征都是不错的选择。

十三、总结

回到我们开头提出的那个问题,如何优雅的处理异常呢?

  1. 可疑区域增加 Try-Catch
  2. 全局监控 JS 异常 window.onerror
  3. 全局监控静态资源异常 window.addEventListener
  4. 捕获没有 Catch 的 Promise 异常:unhandledrejection
  5. VUE errorHandler 和 React componentDidCatch
  6. 监控网页崩溃:window 对象的 load 和 beforeunload
  7. 跨域 crossOrigin 解决

其实很简单,正如上文所说:采用组合方案,分类型的去捕获异常,这样基本 80%-90% 的问题都化于无形。

采用两种方式,第一种是标记法,第二种是计数法,两种方法原理是相同的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
 //标记法
function fn(m, n) {
var count = "";
for (i = m; i <= n; i++) {//第一次循环
var flage = true;//设一个标记
for (j = 2; j < i; j++) {
if (i % j === 0) {//第二次循环
flage = false;//不满足条件改变标记,
break;//跳出循环
}
}
if (flage) {//满足条件,也就是为true时
count += i + ","
}
}
return count
}
console.log(fn(100, 200)) //调用函数,输出值

//计数法
function fn(m, n) {
var sun = "";
for (i = m; i <= n; i++) {//第一次循环
var count = 0;//初值为零
for (j = 2; j < i; j++) {//第二次循环
if (i % j === 0) {//不满足条件加1
count+=1
}
}
if (count === 0) {
sun += i + ","
}
}
return sun
}
console.log(fn(100, 200))

在工作中,前端代码打包之后生成的静态资源就要发布到静态服务器上,这时候就要对这些静态资源做一些运维配置,其中,gzip 和设置缓存是必不可少的。这两项是最直接影响到网站性能和用户体验的。

缓存的优点:

  • 减少了不必要的数据传输,节省带宽
  • 减少服务器的负担,提升网站性能
  • 加快了客户端加载网页的速度
  • 用户体验友好

缺点:

  • 资源如果有更改但是客户端不及时更新会造成用户获取信息滞后,如果老版本有 bug 的话,情况会更加糟糕。

所以,为了避免设置缓存错误,掌握缓存的原理对于我们工作中去更加合理的配置缓存是非常重要的。

一、强缓存

到底什么是强缓存?强在哪?其实强是强制的意思。当浏览器去请求某个文件的时候,服务端就在 response header 里面对该文件做了缓存配置。缓存的时间、缓存类型都由服务端控制,具体表现为:
response header 的 cache-control,常见的设置是 max-age public private no-cache no-store 等

如下图,
设置了cache-control:max-age=31536000,public,immutable

image

max-age 表示缓存的时间是 31536000 秒(1 年),public 表示可以被浏览器和代理服务器缓存,代理服务器一般可用 nginx 来做。immutable 表示该资源永远不变,但是实际上该资源并不是永远不变,它这么设置的意思是为了让用户在刷新页面的时候不要去请求服务器!啥意思?就是说,如果你只设置了 cache-control:max-age=31536000,public 这属于强缓存,每次用户正常打开这个页面,浏览器会判断缓存是否过期,没有过期就从缓存中读取数据;但是有一些 “聪明” 的用户会点击浏览器左上角的刷新按钮去刷新页面,这时候就算资源没有过期(1 年没这么快过),浏览器也会直接去请求服务器,这就是额外的请求消耗了,这时候就相当于是走协商缓存的流程了(下面会讲到)。如果 cache-control:max-age=315360000,public 再加个 immutable 的话,就算用户刷新页面,浏览器也不会发起请求去服务器,浏览器会直接从本地磁盘或者内存中读取缓存并返回 200 状态,看上图的红色框(from memory cache)。这是 2015 年 facebook 团队向制定 HTTP 标准的 IETF 工作组提到的建议:他们希望 HTTP 协议能给 Cache-Control 响应头增加一个属性字段表明该资源永不过期,浏览器就没必要再为这些资源发送条件请求了。

强缓存总结

  1. cache-control: max-age=xxxx,public
    客户端和代理服务器都可以缓存该资源;
    客户端在 xxx 秒的有效期内,如果有请求该资源的需求的话就直接读取缓存,status code:200 ,如果用户做了刷新操作,就向服务器发起 http 请求

  2. cache-control: max-age=xxxx,private
    只让客户端可以缓存该资源;代理服务器不缓存
    客户端在 xxx 秒内直接读取缓存,status code:200

  3. cache-control: max-age=xxxx,immutable
    客户端在 xxx 秒的有效期内,如果有请求该资源的需求的话就直接读取缓存,statu code:200 ,即使用户做了刷新操作,也不向服务器发起 http 请求

  4. cache-control: no-cache
    跳过设置强缓存,但是不妨碍设置协商缓存;一般如果你做了强缓存,只有在强缓存失效了才走协商缓存的,设置了 no-cache 就不会走强缓存了,每次请求都会询问服务端。

  5. cache-control: no-store
    不缓存,这个会让客户端、服务器都不缓存,也就没有所谓的强缓存、协商缓存了。

二、协商缓存

上面说到的强缓存就是给资源设置个过期时间,客户端每次请求资源时都会看是否过期;只有在过期才会去询问服务器。所以,强缓存就是为了给客户端自给自足用的。而当某天,客户端请求该资源时发现其过期了,这时就会去请求服务器了,而这时候去请求服务器的这过程就可以设置协商缓存。这时候,协商缓存就是需要客户端和服务器两端进行交互的。

怎么设置协商缓存?

response header 里面的设置

1
2
etag: '5c20abbd-e2e8'
last-modified: Mon, 24 Dec 2018 09:49:49 GMT

etag:每个文件有一个,改动文件了就变了,就是个文件 hash,每个文件唯一,就像用 webpack 打包的时候,每个资源都会有这个东西,如: app.js 打包后变为 app.c20abbde.js,加个唯一 hash,也是为了解决缓存问题。

last-modified:文件的修改时间,精确到秒

也就是说,每次请求返回来 response header 中的 etag 和 last-modified,在下次请求时在 request header 就把这两个带上,服务端把你带过来的标识进行对比,然后判断资源是否更改了,如果更改就直接返回新的资源,和更新对应的 response header 的标识 etag、last-modified。如果资源没有变,那就不变 etag、last-modified,这时候对客户端来说,每次请求都是要进行协商缓存了,即:

发请求–>看资源是否过期–>过期–>请求服务器–>服务器对比资源是否真的过期–>没过期–>返回 304 状态码–>客户端用缓存的老资源。

这就是一条完整的协商缓存的过程。

当然,当服务端发现资源真的过期的时候,会走如下流程:

发请求–>看资源是否过期–>过期–>请求服务器–>服务器对比资源是否真的过期–>过期–>返回 200 状态码–>客户端如第一次接收该资源一样,记下它的 cache-control 中的 max-age、etag、last-modified 等。

所以协商缓存步骤总结:

请求资源时,把用户本地该资源的 etag 同时带到服务端,服务端和最新资源做对比。
如果资源没更改,返回 304,浏览器读取本地缓存。
如果资源有更改,返回 200,返回最新的资源。

补充一点,response header 中的 etag、last-modified 在客户端重新向服务端发起请求时,会在 request header 中换个 key 名:

1
2
3
4
5
6
7
// response header
etag: '5c20abbd-e2e8'
last-modified: Mon, 24 Dec 2018 09:49:49 GMT

// request header 变为
if-none-matched: '5c20abbd-e2e8'
if-modified-since: Mon, 24 Dec 2018 09:49:49 GMT

为什么要有 etag?

你可能会觉得使用 last-modified 已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要 etag 呢?HTTP1.1 中 etag 的出现(也就是说,etag 是新增的,为了解决之前只有 If-Modified 的缺点)主要是为了解决几个 last-modified 比较难解决的问题:

  1. 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新 get;

  2. 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说 1s 内修改了 N 次),if-modified-since 能检查到的粒度是秒级的,这种修改无法判断(或者说 UNIX 记录 MIME 只能精确到秒);

  3. 某些服务器不能精确的得到文件的最后修改时间。

怎么设置强缓存与协商缓存

  1. 后端服务器如 nodejs:
1
2
3
res.setHeader('max-age': '3600 public')
res.setHeader(etag: '5c20abbd-e2e8')
res.setHeader('last-modified': Mon, 24 Dec 2018 09:49:49 GMT)
  1. nginx 配置

image

偶尔自己折腾一番非前端的东西时,若心中有数,自然不会手忙脚乱。

怎么去用?

举个例子,像目前用 vue-cli 打包后生成的单页文件是有一个 html,与及一堆 js css img 资源,怎么去设置这些文件呢,核心需求是

  1. 要有缓存,毋庸置疑
  2. 当发新包的时候,要避免加载老的缓存资源

image

我的做法是:

index.html 文件采用协商缓存,理由就是要用户每次请求 index.html 不拿浏览器缓存,直接请求服务器,这样就保证资源更新了,用户能马上访问到新资源,如果服务端返回 304,这时候再拿浏览器的缓存的 index.html,切记不要设置强缓存!!!

其他资源采用强缓存 + 协商缓存,理由就不多说了。