The World Best ST.

React学习

字数统计: 9.9k阅读时长: 40 min
2023/09/16 Share

站在巨人的肩膀上开发前端

React的优点

  • 组件化: 提高开发效率
  • 虚拟dom: 提升用户体验
  • 函数式编程: 提高可维护性和可阅读性
  • react-native: 开发原生移动应用

Web开发

web安全

xxs

反射型

攻击者通过电子邮件等方式将包含注入脚本的恶意链接发送给受害者,当受害者点击该链接的时候,注入脚本被传输到目标服务器上,然后服务器将注入脚本 “反射”到受害者的浏览器上,从而浏览器就执行了该脚本

存储型

存储型XSS的原理是:主要是将恶意代码上传或存储到服务器中,下次只要受害者浏览包含此恶意代码的页面就会执行恶意代码

基于DOM

因此我们需要对HTML进行编码,对JS进行编码来防止这些问题产生

SQL注入

SQL注入是通过客户端的输入把SQL命令注入到一个应用的数据库中,从而执行恶意的SQL语句

csrf

中文名:跨站请求伪造。其原理是攻击者构造网站后台某个功能接口的请求地址,诱导用户去点击或者用特殊方法让该请求地址自动加载。用户在登录状态下这个请求被服务端接收后会被误以为是用户合法的操作。对于 GET 形式的接口地址可轻易被攻击,对于 POST 形式的接口地址也不是百分百安全,攻击者可诱导用户进入带 Form 表单可用POST方式提交参数的页面。

文件上传漏洞

  • 限制文件后缀
  • 不执行用户上传的文件

性能相关

页面加载过程

  1. 浏览器会从服务器中获取到 HTML 内容
  2. 浏览器获取到 HTML 内容后,就开始从上到下解析 HTML 的元素
  3. <head>元素内容会先被解析,此时浏览器还没开始渲染页面。当遇到’head’ 标签内部一般会有
    3.1. 页面元数据的<meta>元素
    3.2. 还有一些<link>元素涉及外部资源(如图片、CSS 样式等),此时浏览器会去获取这些外部资源。
    3.3. <script>元素通过src属性指向外部资源。
  4. 当浏览器解析<script>,会暂停解析并下载 JavaScript 脚本。
  5. 当 JavaScript 脚本下载完成后,浏览器的控制权转交给 JavaScript 引擎。当脚本执行完成后,
    控制权会交回给渲染引擎,渲染引擎继续往下解析 HTML 页面。
  6. <body>元素内容开始被解析,浏览器开始渲染页面。

1)输入URL
2)检查缓存
3)DNS域名解析,获取IP地址
4)浏览器与服务器建立连接(TCP三次握手)
4)向服务器发送HTTP请求,服务器返回response
5)浏览器对response进行解析渲染页面
6)断开连接(四次挥手)

页面渲染及页面加载资源的顺序

  1. 一个页面的加载顺序是从上到下顺序加载的,并且加载与渲染同时进行。
  2. 引用外部js文件时,当在加载过程中遇到<script>标签时,浏览器会向服务器发送一个reques并等待该request的返回。
    因为浏览器需要1个稳定的DOM树结构,而JS中很有可能有代码直接改变了DOM树结构,比如使用document.write 或 appendChild,甚至是直接使用的location.href进行跳转,浏览器为了防止出现JS修改DOM树,需要重新构建DOM树的情况,所以加载js就会阻塞其后内容的下载和呈现。
  3. 使用嵌入式js时,会阻塞所有内容的呈现。
  4. 当在加载过程中遇到<style>标签时,浏览器会发1个request去请求CSS或image,然后继续执行下面的转换,而不需要等待request的返回,当request返回后,只需要把返回的内容放入到DOM树中对应的位置就OK,所以正常来说CSS并不会诸塞页面(不阻塞DOM的解析,会阻塞Dom的渲染)。

但是也有例外:当CSS后面跟着嵌入的JS的时候,该CSS就会出现阻塞后面资源下载的情况。

原因:因为浏览器会维持html中css和js的顺序,样式表必须在嵌入的JS执行前先加载、解析完。而嵌入的JS会阻塞后面的资源加载,所以就会出现上面CSS阻塞下载的情况。

浏览器加载页面渲染流程

  1. 渲染引擎首先通过网络获取所请求文档内容
  2. 解析HTML文件生成DOM Tree
  3. 解析CSS文件生成CSSOM(CSS对象模型)
  4. 整合DOM Tree和CSSOM,生成Render Tree(渲染树)
  5. 根据Render Tree渲染绘制,将像素渲染到屏幕上。
  6. layout布局(重排在这一步,确定大小位置等,可以想象成将页面切成一块一块的)
  7. 渲染每一个节点(重绘在这一步,这也是为什么重排一定重绘,重绘不一定重排)

iframe

contentwindow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 父级网页 -->
<iframe src="http://child.domain.com" id="myframe"></iframe>
<script>
var iframe = window.document.getElementById("myframe");
var childWindow = iframe.contentWindow; // 获取子窗口对象
childWindow.postMessage("Hello, Child Window!", "http://child.domain.com"); // 向子窗口发送消息
</script>

<!-- 子级网页 -->
<script>
window.addEventListener("message", function(event) {
console.log(event.data); // 输出从父级窗口接收到的消息
event.source.postMessage("Hello, Parent Window!", event.origin); // 向父级窗口发送消息
}, false);
</script>

JS进阶

let 和 const

var 声明变量。
let 代替 var,声明变量。
const 声明常量。

  • 使用 const 声明常量,一旦声明,就必须立即初始化,不能留到以后赋值
  • const 声明的常量,允许在不重新赋值的情况下修改它的值(只适用于引用数据类型)
  • 已经存在的变量或常量,又声明了一遍。
  • var 允许重复声明,let、const 不允许。
  • 只要是已经声明过的变量或常量(不论是用什么方式声明的),let、const 都不能再次声明。
  • var 会提升变量的声明到当前作用域的顶部
  • let、const 存在暂时性死区。
  • 只要作用域内存在 let、const,它们所声明的变量或常量就自动“绑定”这个区域,不再受到外部作用域的影响。
  • 全局作用域中,var 声明的变量和通过 function 声明的函数,会自动变成 window 对象的属性或方法。let、const 则不会。
  • var 没有块级作用域。
  • let/const 有块级作用域。

解构

数组的解构赋值,对象的解构赋值,字符串的解构赋值,数值与布尔值的解构赋值,函数参数的解构赋值。

解构数组

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
var foo = ["one", "two", "three"];

var [one, two, three] = foo;

//如果解构不成功,变量的值为undefined。

//解构可设置默认值
[a = 5, b = 7] = [1];
console.log(a); // 1
console.log(b); // 7

//交换变量
var a = 1;
var b = 3;
[a, b] = [b, a];
console.log(a); // 3
console.log(b); // 1

// 忽略某些返回值
function f() {
return [1, 2, 3];
}
var [a, , b] = f();
console.log(a); // 1
console.log(b); // 3
你也可以忽略全部返回值:
[, ,] = f();

// 将剩余数组赋值给一个变量
var [a, ...b] = [1, 2, 3];
console.log(a); // 1
console.log(b); // [2, 3]

//

解构对象

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
var o = { p: 42, q: true };
var { p, q } = o;
console.log(p); // 42
console.log(q); // true

let { a, b, ...rest } = { a: 10, b: 20, c: 30, d: 40 };
a; // 10
b; // 20
rest; // { c: 30, d: 40 }

// 默认值
var { a = 10, b = 5 } = { a: 3 };
console.log(a); // 3
console.log(b); // 5

// 给新的变量命名并提供默认值
一个属性可以同时 1)从一个对象解构,并分配给一个不同名称的变量 2)分配一个默认值,以防未解构的值是 undefined
var { a: aa = 10, b: bb = 5 } = { a: 3 };
console.log(aa); // 3
console.log(bb); // 5

// 函数参数默认值
function drawES2015Chart({ size = "big", cords = { x: 0, y: 0 }, radius = 25 } = {}) {
console.log(size, cords, radius);
// do some chart drawing
}
drawES2015Chart({
cords: { x: 18, y: 30 },
radius: 30
});

// 解构嵌套对象和数组
const metadata = {
title: "Scratchpad",
translations: [
{
locale: "de",
localization_tags: [],
last_edit: "2014-04-14T08:43:37",
url: "/de/docs/Tools/Scratchpad",
title: "JavaScript-Umgebung"
}
],
url: "/en-US/docs/Tools/Scratchpad"
};
let {
title: englishTitle, // rename
translations: [
{
title: localeTitle // rename
}
]
} = metadata;
console.log(englishTitle); // "Scratchpad"
console.log(localeTitle); // "JavaScript-Umgebung"

//For of 迭代和解构
var people = [
{
name: "Mike Smith",
family: {
mother: "Jane Smith",
father: "Harry Smith",
sister: "Samantha Smith"
},
age: 35
},
{
name: "Tom Jones",
family: {
mother: "Norah Jones",
father: "Richard Jones",
brother: "Howard Jones"
},
age: 25
}
];

for (var {
name: n,
family: { father: f }
} of people) {
console.log("Name: " + n + ", Father: " + f);
}

// "Name: Mike Smith, Father: Harry Smith"
// "Name: Tom Jones, Father: Richard Jones"

// 从作为函数实参的对象中提取数据
function userId({ id }) {
return id;
}

function whois({ displayName: displayName, fullName: { firstName: name } }) {
console.log(displayName + " is " + name);
}

var user = {
id: 42,
displayName: "jdoe",
fullName: {
firstName: "John",
lastName: "Doe"
}
};

console.log("userId: " + userId(user)); // "userId: 42"
whois(user); // "jdoe is John"

// 对象属性计算名和解构
let key = "z";
let { [key]: foo } = { z: "bar" };
console.log(foo); // "bar"

// 解构对象时会查找原型链(如果属性不在对象自身,将从原型链中查找)
// 声明对象 和 自身 self 属性
var obj = { self: "123" };
// 在原型链中定义一个属性 prot
obj.__proto__.prot = "456";
// test
const { self, prot } = obj;
// self "123"
// prot "456"(访问到了原型链)

模板字符串

模板字符串相当于加强版的字符串,用反引号 `,除了作为普通字符串,还可以用来定义多行字符串,还可以在字符串中加入变量和表达式

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
//字符串插入变量和表达式。变量名写在 ${} 中,${} 中可以放入 JavaScript 表达式。
let name = "lucy"
let age = 20
let info = `My name is ${name} ,I am ${age+1}`
console.log(info)

$('#list').html(`
<ul>
<li>first</li>
<li>second</li>
</ul>
`);

var x = 1, y = 2;
`${x} + ${y} = ${x + y}`;
// "1 + 2 = 3"
`${x} + ${y * 2} = ${x + y * 2}`;
// "1 + 4 = 5"
var obj = {x: 1, y: 2};
`${obj.x + obj.y}`
// "3"

// 模板字符串之中还可以调用函数。
function func() {
return 'Hello';
}
`${func()} World`;
// "Hello World"

字符串方法

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
includes:
var str = "Hello world, welcome to the Runoob。";
var n = str.includes("Runoob");
// true

startsWith
var str = "Hello world, welcome to the Runoob.";
var n = str.startsWith("Hello");
// true

endsWith
let str = "Hello world";
str.endsWith("world") // 返回 true
str.endsWith("World") // 返回 false

padStart
let str = '123';
console.log(str.padStart(5, '0')); // 输出 "00123"
let str = '12345';
console.log(str.padStart(3, '0')); // 如果目标长度小于或等于原始字符串的长度,则返回原始字符串。输出 "12345"

padEnd
let str = '123';
console.log(str.padEnd(5, '0')); // 输出 "12300"
let str = '12345';
console.log(str.padEnd(3, '0')); // 如果目标长度小于或等于原始字符串的长度,则返回原始字符串。输出 "12345"

trimStart
let s = ' foo ';
console.log(s.trimStart());//foo //

trimEnd
let s = ' foo ';
console.log(s.trimEnd()); // foo

match
var str="The rain in SPAIN stays mainly in the plain";
var n=str.match(/ain/g);
// ain,ain,ain
// 如果 regexp 没有标志 g,那么 match() 方法就只能在 stringObject 中执行一次匹配。如果没有找到任何匹配的文本, match() 将返回 null。否则,它将返回一个数组,其中存放了与它找到的匹配文本有关的信息。
//gi代表不区分大小写

replace
var str="Visit Microsoft! Visit Microsoft!";
var n=str.replace("Microsoft","Runoob");
// Visit Runoob!Visit Microsoft!
// 当第一个 "Microsoft" 被找到,它就被替换为 "Runoob"

-- 正则
var str="Mr Blue has a blue house and a blue car";
var n=str.replace(/blue/gi, "red");
// Mr red has a red house and a red car

React

常用的hooks的使用

简单想一下,函数和对象不同,并没有一个实例的对象能够在多次执行之间保存状态,那势必需要一个函数之外的空间来保存这个状态,而且要能够检测其变化,从而能够触发函数组件的重新渲染。

再进一步想,那我们需要这样一个机制,能够把一个外部的数据绑定到函数的执行。当数据变化时,函数能够自动重新执行。这样的话,任何会影响 UI 展现的外部数据,都可以通过这个机制绑定到 React 的函数组件。

Hook 就是“钩子”的意思。在 React 中,Hooks 就是把某个目标结果钩到某个可能会变化的数据源或者事件源上,那么当被钩到的数据或事件发生变化时,产生这个目标结果的代码会重新执行,产生更新后的结果。

React常用的hook主要有八个 分别是 useState, useEffect, useMemo, useCallback, useContext, useRef, useLayoutEffect, useImerativeHandle

hook的好处

最大的好处:逻辑复用
在之前的 React 使用中,要实现逻辑的复用,必须借助于高阶组件等复杂的设计模式,这样会让调试变得困难。而Hooks 有什么好处,最关键的答案就是简化了逻辑复用。

另外一个好处:有助于关注分离
意思是说 Hooks 能够让针对同一个业务逻辑的代码尽可能聚合在一块。这是过去在 Class 组件中很难做到的。因为在Class组件,我们不得不把业务逻辑分散在组件的不同生命周期当中。Hooks 的方式,把业务逻辑清晰地隔离开,能够让代码更加容易理解和维护。

useState

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
// 使用:

// 第一种
const [x, setX] = useState(值)

// 第二种
const [y, setY] = useState(() => {
// 逻辑
// 必须有 return
return
})

// 获取上一次的值
setX(preValue => {
preValue //上一次的值
return
})


// 修改state
import React, { useState } from "react";
export default function Example() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

// 以回调的方式修改state
import React, { useState } from "react";
export default function Example() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count => count + 1)}>{count}</button>;
}

// useState的回调参数:节约state初始化带来的性能损耗
import React, { useState } from "react";
export default function Example() {
const [count, setCount] = useState(() => {
// ... 其他计算
const v = 1 + 1 * 1 - 2;
return v;
});
return <button onClick={() => setCount(count => count + 1)}>{count}</button>;
}

我们有时会遇到这样的需求:在setState完成后执行某项异步回调,但函数组件的set方法是没有第二个参数的,那我们应该怎么处理呢?实际上可以结合useEffect和useRef来实现

useEffect

可监听某个依赖state的变化并异步执行响应函数, 赋值给 useEffect 的函数会在组件渲染到屏幕之后执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useState, useEffect, useRef } from "react";
export default function Example() {
const [count, setCount] = useState(0);
const isMountedRef = useRef(true);
useEffect(() => {
if (isMountedRef.current) {
isMountedRef.current = false;
return;
}
// 下面是count这一state改变后的回调
console.log("count被改变了,当前值为" + count);
}, [count]);

return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
useEffect不同写法的执行差别

(1)不写第二个参数

会在函数组件初次渲染和每次重渲染的时候调用,包括props改变和state改变导致的更新。

效果相当于componentDidMount + componentDidUpdate + componentWillUnmount

1
2
3
useEffect(() => {
// ...
});

(2)第二个参数为空数组

只在入栈的时候运行一次

效果相当于componentDidMount

1
2
3
useEffect(() => {
// ...
},[]);

(3)useEffect使用返回值

返回值是一个函数,将会在组件销毁时候调用

(4)useEffect在第二个参数中写入数据属性

除了初次入栈被以外,将只在数据属性改变的时候才运行useEffect内的函数,如下面代码中useEffect内匿名函数将会伴随count的变化而调用(初次入栈时count也会被识别为是”变化”的)

1
2
3
4
const [count, setCount] = useState(0);
useEffect(() => {
// ...
}, [count]);
函数组件内多个useEffect的执行次序

函数组件内部是可以写入多个useEffect的,如果这几个useEffect内的函数都是同步代码且执行条件相同的话(useEffect第二个参数相同),理论上多个useEffect内部函数是会按照编写时从上到下的次序执行的。

useMemo

  • 不传第二个参数时:每次组件渲染useMemo接收函数都会调用并返回计算值
  • 第二个参数为空数组时:只有组件首次加载时候useMemo接收函数才会调用返回计算值,后续重渲染都返回第一次计算的缓存值
  • 第二个参数为依赖数组时:当依赖发生改变时useMemo调用接收函数并返回值,如果依赖相比前一次渲染没有改变就返回缓存值
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
export default function Example() {
let [text, setText] = useState("默认文本");
let [count, setCount] = useState(0);
useEffect(() => {
// 更新count,使组件每隔1秒就刷新一次
const id = setInterval(() => setCount(count => count + 1), 1000);
// 在入栈1s后修改text
setTimeout(() => setText("修改后文本"), 1000);
return () => clearInterval(id);
}, []);
// 只在text变化的时候才重新运行memo内部函数
let t = useMemo(() => {
console.log("memo调用");
return "当前文本:" + text;
}, [text]);
// 在组件函数中打印
console.log("组件渲染");
return (
<div>
<p>{t}</p>
<p>统计数:{count}</p>
</div>
);
}

// 总共打印2次memo调用

useCallback

  • 不传第二个依赖参数时:每次渲染都把传入的函数原样返回,每次返回的都是新的函数引用
  • 第二个参数为空数组时:每次渲染都返回缓存的第一次传入的函数引用
  • 第二个参数为一个依赖数组时,只有依赖改变时才返回接收到的新函数引用,如果依赖没有改变就返回之前缓存的函数引用

下面我们来看一个例子:

1
2
3
4
5
function Counter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return <button onClick={increment}>+</button>
}

由于增加计数的方法increment在组件内部,这就导致在每次修改count时,都会重新渲染这个组件,increment也就无法进行重用,每次都需要创建一个新的increment方法。

不仅如此,即使count没有发生改变,当组件内部的其他state发生变化时,组件也会进行重新渲染,那这里的increment方法也会因此重新创建。虽然这些都不影响页面的正常使用,但是这增加了系统的开销,并且每次创建新函数的方式会让接收事件处理函数的组件重新渲染。

可以修改成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { useState, useCallback } from "react";
const set = new Set();
export default function Callback() {
const [count, setCount] = useState(1);
const [value, setValue] = useState(1);
const callback = useCallback(() => {
console.log(count);
}, [count]);
set.add(callback);
return (
<div>
<h1>Count: {count}</h1>
<h1>Set.size: {set.size}</h1>
<h1>Value: {value}</h1>
<div>
<button onClick={() => setCount(count + 1)}>Count + 1</button>
<button onClick={() => setValue(value + 2)}>Value + 2</button>
</div>
</div>
);
}

可以看到,当我们点击Count + 1按钮时,Count和Set.size都增加1,说明产生了新的回调函数。当点击Value + 2时,只有Value发生了变化,而Set.size没有发生变化,说明没有产生的新的回调函数,返回的是缓存的旧版本函数。

「使用场景:」 父组件中一个子组件,通过情况下,当父组件发生更新时,它的子组件也会随之更新,在多数情况下,子组件随着父组件更新而更新是没有必要的。这时就可以借助useCallback来返回函数,然后把这个函数作为props传递给子组件,这样,子组件就可以避免不必要的更新。

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
import React, { useState, useCallback, useEffect } from "react";
export default function Parent() {
const [count, setCount] = useState(1);
const [value, setValue] = useState(1);
const callback = useCallback(() => {
return count;
}, [count]);
return (
<div>
<h1>Parent: {count}</h1>
<h1>Value: {value}</h1>
<Child callback={callback} />
<div>
<button onClick={() => setCount(count + 1)}>Count + 1</button>
<button onClick={() => setValue(value + 2)}>Value + 2</button>
</div>
</div>
);
}
function Child({ callback }) {
const [count, setCount] = useState(() => callback());
useEffect(() => {
setCount(callback());
}, [callback]);
return <h2>Child: {count}</h2>;
}

useRef

  • 作为存值对象使用,起到类似class组件中this的作用
  • 读取到当前最新值而非旧的“快照”
  • 获取上一轮次渲染的state或props
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
import React, { useEffect, useState, useRef } from "react";
export default function Example() {
const [count, setCount] = useState(0);
// useEffect的目的是使组件重渲染
useEffect(
() =>
setTimeout(() => {
setCount(1);
}, 1000),
[]
);
// 调用useRef方法
let isMountedRef = useRef(true);
if (isMountedRef.current) {
isMountedRef.current = false;
return <div>首次渲染</div>;
}
return <div>非首次渲染</div>;
}

// 例子2
import React, { useCallback, useEffect, useState, useRef } from "react";
export default function Example() {
const [count, setCount] = useState(0);
const countRef = useRef(0);
function handleAlertClick() {
// 通过ref获取最新值
setTimeout(() => {
alert("你点击了: " + countRef.current);
}, 3000);
}

return (
<div>
<p>你点击了按钮 {count} 次</p>
<button
onClick={() => {
setCount(count + 1);
// 修改ref内存储的数据
countRef.current = count + 1;
}}
>
按钮
</button>
<button onClick={handleAlertClick}>弹框</button>
</div>
);
}

// 获取上一次渲染的state或props
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return <h1>当前: {count}, 上一次: {prevCount}</h1>;
}

function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}

useContext

useContext的作用是用于在组件之间共享数据。它的作用是在组件树中传递数据,避免通过props一层层传递数据,从而简化组件之间的通信。

1
2
3
4
5
6
7
8
const ThemeContext = React.createContext(初始值可选)

<ThemeContext.Provider value={{ count, age, setCount, setAge }}>
<子组件 />
</ThemeContext.Provider>

// 子组件
const theme = useContext(ThemeContext)

useReducer

与useState相似

  • 更好的可读性。
  • state 处理都集中到 reducer,对 state 的变化更有掌控力

当遇到以下场景时,可以优先使用 useReducer :

  • state 变化很复杂,经常一个操作需要修改很多 state。
  • 深层子组件里去修改一些状态。
  • 应用程序比较大,UI 和业务需要分开维护。
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
import React, { useReducer } from "react";
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
default:
throw new Error();
}
}
function App() {
// reducer是定义的函数, initialState是初始化的值会放到state中
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<h1>you click {state.count} times</h1>
<input
type="button"
onClick={() => dispatch({ type: "increment" })}
value="click me"
/>
</div>
);
}
export default App;

Redux的使用

Redux就是一个帮助我们管理State的容器:Redux是 JavaScript的状态容器,提供了可预测的状态管理。

Store

Store 是保存数据的地方,它是一个对象,一个应用只能有一个 Store;
Rudex 提供了一个叫 createStore 的方法用来生成 Store;

1
2
import { createStore } from 'redux';
const store = createStore(fn);

零碎知识

npm、yarn、nvm、镜像配置等概念及使用

npm

npm就是js的包管理器, npm下载的包不仅仅是个单纯的js库,还适配Nodejs的工作标准(能更好地被基于Nodejs的工程使用)

1
2
3
4
5
6
7
8
9
10
# 查看已配置的镜像源
npm config get registry
# 官方
npm config set registry https://registry.npmjs.org
# 淘宝
npm config set registry https://registry.npm.taobao.org

开发依赖:在命令行中使用 npm i -D 包名 或 npm i--save-dev 包名,包信息保存在package.json 中dependencies属性,只在开发阶段有用。

生产依赖:在命令行中使用 npm i -S 包名 或 npm i--save 包名,包信息保存在package.json中devDependencies属性,在开发和生产阶段都有用,默认安装为生产依赖。

yarn

yarn与npm一样,都是js包管理器,都适用于nodejs。不一样的是,yarn与npm的架构和包管理方式方式不同, 安装包的速度比npm和cnpm快。

1
2
3
4
5
6
7
8
9
10
11
npm install -g yarn

yarn add 包名 生产环境

yarn add 包名 --dev

yarn global add 包名

yarn remove 包名

yarn upgrade 包名

nvm

nvm 是 Mac 下的 node 管理工具,可以很方便的切换node的版本

1
2
3
4
5
6
7
8
9
10
11
nvm list available

nvm install 版本号

nvm use 版本号

nvm list

nvm install latest (安装最近的nodejs)

nvm uninstall 版本号

国际化框架选型及使用

i18next

react-i18next 是基于 i18next 的一款强大的国际化框架,可以用于 react 和 react-native 应用,是目前非常主流的国际化解决方案

  • 基于i18next不仅限于react,学一次就可以用在其它地方
  • 提供多种组件在hoc、hook和class的情况下进行国际化操作
  • 适合服务端的渲染
  • 历史悠久,始于2011年比大多数的前端框架都要年长
  • 因为历史悠久所以更成熟,目前还没有i18next解决不了的国际化问题
  • 有许多插件的支持,比如可以用插件检测当前系统的语言环境,从服务器或者文件系统加载翻译资源

React-intl

React-intl通过context api的方式为react项目提供多语言支持,可以对文本、数字、日期等进行翻译。以下代码以组件的方式进行国际化操作。在locales文件夹中创建多语言资源文件,与i18next不同的是,资源文件的类型为js类型,导出的是一个对象, i18next是json文件

通过this.setState({ locale: 'zh' })}切换语言

react-intl只提供了组件(文本组件 FormattedMessage)和api(intl.formatMessage())的方式来进行国际化处理,通过id获取对应的语言文本进行国际化

对比

  • React-i18next初始化的时候需要将初始化配置放置在初始化文件(i18n.js)中,然后将初始化文件(i18n.js)通过import的方式引入到入口文件中即可。当然也可以通过I18nextProvider将i18n往下传递到各子组件。React-intl提供的是context api初始化方案,需要将初始化配置放在IntlProvider组件中,并且将入口文件的组件(如)作为IntlProvider的子组件来使用;
  • React-i18next提供了切换语言的接口(i18n.changeLanguage),react-intl则需要对切换做一些封装的工作;
  • React-i18next提供了三种方式进行国际化操作(render props、hook和hoc), react-intl提供了api(intl.formatMessage())和组件()两种方式进行国际化;
  • React-i18next的语言资源文件为json格式,react-intl为js格式,同时支持变量传值;
  • React-i18next有很多插件可以使用比如检测当前系统语言,从后端获取数据等;
  • React-intl除文本翻译外还提供日期、时间和金额的国际化支持;

webpack

webpack,web是网络的意思,pack是包装的意思。本质上,webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。
简单的说: 它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其打包为合适的格式以供浏览器使用。

核心概念

  • Entry:指示 Webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。Webpack 会从配置的 Entry 开始递归找出依赖的所有模块。
  • Output:告诉 Webpack 在哪里输出它所创建的结果文件,以及如何命名这些文件,默认值为./dist
  • Loader:Webpack 自身只能解析 JavaScript 和 json 文件,Webpack 利用 loader 处理其他文件
  • Plugins:用于执行范围更广的任务。如打包优化、压缩、重新定义环境中的变量等
  • Module:Webpack 里一个模块对应着一个文件,Webpack 会从配置的 Entry 开始递归找出所有依赖的模块

作用

webpack的理念就是一切皆模块化,把一堆的css文件和js文件放在一个总的入口文件,通过require引入,剩下的事情webpack会处理,包括所有模块的前后依赖关系,打包、压缩、合并成一个js文件,公共代码抽离成一个js文件、某些自己指定的js单独打包,模块可以是css/js/imsge/font等等。

下面我们就具体来看webpack的用法

  1. webpack可以根据模板生成HTML,并自动处理上面的css/js引用路径
  2. webpack可以自动处理<img>里面的图片路径,css里面背景图的路径,字体引用
  3. webpack可以开启本地服务器,一边改写代码,一边自动更新页面内容
  4. webpack可以编译jsx es6 sass less coffescript等,并添加md5、sourcemap等辅助
  5. webpack可以异步加载内容,不需要时不加载到DOM
  6. webpack可以配合vue.js和react.js等框架开发。

(1)重新加载编译,将浏览器不认识的语法编译成浏览器认识的语法。less编译成css,ES6语法转换成ES5。
(2)减少io请求。发送请求,会发挥一个html到浏览器,这是,打开控制台会发现html页面通过script,link等标签引用的静态,浏览器会再次发出请求去获取这些资源。如何webpack打包,将所有的静态资源都合并好了,减少了io请求。

devServer

为什么要使用 devServer?

因为在开发调试的过程中不用频繁的去执行型打包命令,等待打包完成才能进行调试。devServer 会自动在内存内打包,并自动打开或刷新浏览器。

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
/*
webpack.config.js webpack的配置文件
作用:指示webpack干哪些活(当你运行webpack指令时,会加载里面的配置)所有构建工具都是基于nodejs平台运行的~模块化默认采用commonjs.
loader: 1.下载 2.使用(配置loader)
plugins: 1.下载2. 引入 3.使用

路径: ./webpack.config.js
*/

// resolve用来拼接绝对路径的方法
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MinicssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
// webpack配置
……

//开发服务器devServer:用来自动化(自动编译, 自动打开浏览器, 自动刷新浏览器)
//特点:只会在内存中编译打包,不会有任何输出
//启动devServer指令为: npx webpack-dev-server
devServer: {
//项目构建后路径
contentBase: resolve(__dirname,'build'),
//启动gzip压缩
compress: true,
//端口号
port: 3000,
//自动打开浏览器
open: true
}
}

# 调整package.json 文件
"scripts": {
"build": "node_modules/.bin/webpack --config webpack.config.js",
"start": "npx webpack-dev-server --config webpack.config.js --open Chrome.exe"
}

# 然后执行
> npm run start


# proxy开启代理
devServer: {
proxy: {
'/api': 'http://localhost:3000',
},
},

# 忽略https证书
devServer: {
proxy: {
'/api': {
target: 'https://other-server.example.com',
secure: false,
},
},
},

# 指定路径代理
devServer: {
proxy: [
{
context: ['/auth', '/api'],
target: 'http://localhost:3000',
},
],
},

craco

react没有内置的vite.config或者vue.config,如果需要自定义配置webpack可以把eject暴漏出来(不建议)推荐通过craco配置额外的自定义webpack配置

craco.config.js配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const path = require('path')
module.exports = {
webpack: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@hooks': path.resolve(__dirname, 'src/hooks'),
'@components': path.resolve(__dirname, 'src/components'),
'@assets': path.resolve(__dirname, 'src/assets'),
'@router': path.resolve(__dirname, 'src/router'),
'@store': path.resolve(__dirname, 'src/store'),
'@utils': path.resolve(__dirname, 'src/utils'),
'@views': path.resolve(__dirname, 'src/views'),
'@api': path.resolve(__dirname, 'src/api'),
},
},

}

tsconfig.json配置路径

1
2
3
4
5
6
7
8
9
10
"paths": {
"@/*": ["src/*"],
"@utils/*": ["src/utils/*"],
"@views/*": ["src/views/*"],
"@store/*": ["src/store/*"],
"@hooks/*": ["src/hooks/*"],
"@components/*": ["src/components/*"],
"@api/*": ["src/api/*"],
"@assets/*": ["src/assets/*"]
}

将craco覆盖package.json中的react-script

1
2
3
4
"scripts": {
"dev": " craco start",
"build": "craco build",
}

配置环境变量

  1. 项目根目录建立.env文件(如.env.development .env.production)
  2. 在.env文件中添加自定义环境变量
1
2
//必须以REACT_APP_开头,就像vue-cli必须以VUE_APP_开头,vite必须以VITE_开头一样,create-app必须以REACT_APP_开头
REACT_APP_BASE_URL=/api
  1. 使用process.env.REACT_APP_BASE_URL使用(项目在启动时会自动根据你的命令start build…分发当前环境)

babel转码器及兼容处理

Babel是如何工作的?

Babel的工作可以分为三个步骤:解析、转换和生成。

解析:Babel将原始JavaScript代码解析为一种叫做抽象语法树(AST)的数据结构。

转换:Babel对AST进行转换,这是通过所谓的Babel插件来实现的。每一个新的JavaScript特性都需要一个对应的插件来转换。

生成:最后,Babel将转换后的AST转换回JavaScript代码。

css module与postcss

css module

解决css命名冲突问题,让你放心优雅的写css。

postcss

一个用 JavaScript 工具和插件转换 CSS 代码的工具。比如可以使用 Autoprefixer 插件自动获取浏览器的流行度和能够支持的属性,并根据这些数据帮我们自动的 为 CSS 规则添加前缀,将最新的 CSS 语法转换成大多数浏览器都能理解的语法。

常用工具

  • Charles:适合查看、控制网络请求、分析数据
  • Fiddler:与Charles类似,适合windows平台
  • spy-debugger:远程调试手机页面、抓包
  • Whistle:基于Node实现的跨平台Web调试代理工具

移动端适配的概念

  • px:像素 相对长度单位,相对于显示器屏幕分辨率(推荐使用)
  • em:相对长度单位 基准点为父节点字体的大小,如果自身定义了font-size按自身来计算(浏览器默认字体是16px),整个页面内1em不是一个固定的值。
  • rem:相对单位 可理解为”root em”, 相对根节点html的字体大小来计算,CSS3新加属性,chrome/firefox/IE9+支持。
  • vw:viewpoint width,视窗宽度,1vw等于视窗宽度的1%。
  • vh:viewpoint height,视窗高度,1vh等于视窗高度的1%。
  • vmin:vw和vh中较小的那个。
  • vmax:vw和vh中较大的那个。
  • %:百分比
  • in:寸
  • cm:厘米
  • mm:毫米
  • pt:point,大约1/72寸
  • pc:pica,大约6pt,1/6寸

布局视口可以看作是CSS布局时的画布(页面全部),视觉视口是当前显示的页面区域,理想视口是页面在设备最佳的呈现。理想的呈现方式是终极目标,可以使用户体验大大提升,特别是在非PC设备上,理想的状态意味着:

  • 布局视口宽度 = 视觉视口宽度 = 设备宽度

如果布局视口宽度 ≠ 视觉视口宽度, 出现的情况就是内容过宽,用户可能就需要缩放来查看内容,缩小后,看起来费劲,放大后需要左右滑动查看。

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">该meta标签的作用是让当前布局视口的宽度等于设备的宽度,同时不允许用户手动缩放。

  • 方案一:百分比设置;
    ✓ 因为不同属性的百分比值,相对的可能是不同参照物,所以百分比往往很难统一;
    ✓ 所以百分比在移动端适配中使用是非常少的;
  • 方案二:rem单位+动态html的font-size;
  • 方案三:vw单位;
  • 方案四:flex的弹性布局;

后端模数据模拟

MOCKJS

优点:

  • 与前端代码分离
  • 可生成随机数据

缺点:

  • 数据都是动态生成的假数据,无法真实模拟增删改查的情况
  • 只支持 ajax,不支持 fetch

接口管理工具

优缺点(接口管理工具)
优点:

  • 配置功能强大,接口管理与 Mock 一体,后端修改接口 Mock 也跟着更改,可靠
  • 有统一的接口管理后台,查找使用方便。

缺点:

  • 配置复杂,依赖后端,可能会出现后端不愿意出手,或者等配置完了,接口也开发出来了的情况。mock数据都由后台控制,有什么异常情况 前端同学基本上使不上力。有背前后台分离的原则。
  • 一般会作为大团队的基础建设而存在, 没有这个条件的话需慎重考虑
  • 增加后台负担,与其让后台处理mock数据相关问题,倒不如加快提供真实接口数据。

Rollup的理解及与webpack的区别

Rollup是下一代JavaScript模块打包工具。开发者可以在你的应用或库中使用ES2015模块,然后高效地将它们打包成一个单一
文件用于浏览器和Node.js使用。 Rollup最令人激动的地方,就是能让打包文件体积很小。因为Rollup基于ES2015模块,比Webpack和Browserify使用的CommonJS模块机制更高效。这也让Rollup从模块中删除无用的代码,即tree-shaking变得更容易。

Webpack对于代码分割和静态资源导入有着“先天优势”,并且支持热模块替换(HMR),而Rollup并不支持,所以当项目需要用到以上,则可以考虑选择Webpack。但是,Rollup对于代码的Tree-shaking和ES6模块有着算法优势上的支持,若你项目只需要打包出一个简单的bundle包,并是基于ES6模块开发的,可以考虑使用Rollup。简单的说: 在开发应用时使用 Webpack,开发库时使用 Rollup

前端微服务框架

一方面功能快速增加导致打包时间成比例上升,而紧急发布时要求是越短越好,这是矛盾的。另一方面当一个代码库集成了所有功能时,日常协作绝对是非常困难的。而且最近十多年,前端技术的发展是非常快的,每隔两年就是一个时代,导致同志们必须升级项目甚至于换一个框架。但如果大家想在一个规模化应用中一个版本做好这件事,基本上是不可能的。

微前端

微前端是什么:微前端是一种类似于微服务的架构,是一种由独立交付的多个前端应用组成整体的架构风格,将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的应用,而在用户看来仍然是内聚的单个产品。有一个基座应用(主应用),来管理各个子应用的加载和卸载。微前端的核心三大原则就是:独立运行、独立部署、独立开发

singlespa、qiankun

相比于single-spa,qiankun他解决了JS沙盒环境,不需要我们自己去进行处理。在single-spa的开发过程中,我们需要自己手动的去写调用子应用JS的方法(如上面的 createScript方法),而qiankun不需要,乾坤只需要你传入响应的apps的配置即可,会帮助我们去加载。

  • 基于 single-spa[1] 封装,提供了更加开箱即用的 API。
  • 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
  • HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
  • 样式隔离,确保微应用之间样式互相不干扰。
  • JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
  • 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。

其他微服务框架: Micro-app, Module Federation

CATALOG
  1. 1. 站在巨人的肩膀上开发前端
    1. 1.1. React的优点
    2. 1.2. Web开发
      1. 1.2.1. web安全
        1. 1.2.1.1. xxs
          1. 1.2.1.1.1. 反射型
          2. 1.2.1.1.2. 存储型
          3. 1.2.1.1.3. 基于DOM
          4. 1.2.1.1.4. SQL注入
        2. 1.2.1.2. csrf
        3. 1.2.1.3. 文件上传漏洞
      2. 1.2.2. 性能相关
        1. 1.2.2.1. 页面加载过程
        2. 1.2.2.2. 页面渲染及页面加载资源的顺序
        3. 1.2.2.3. 浏览器加载页面渲染流程
      3. 1.2.3. iframe
        1. 1.2.3.1. contentwindow
    3. 1.3. JS进阶
      1. 1.3.1. let 和 const
      2. 1.3.2. 解构
        1. 1.3.2.1. 解构数组
        2. 1.3.2.2. 解构对象
      3. 1.3.3. 模板字符串
      4. 1.3.4. 字符串方法
    4. 1.4. React
      1. 1.4.1. 常用的hooks的使用
        1. 1.4.1.1. hook的好处
        2. 1.4.1.2. useState
        3. 1.4.1.3. useEffect
          1. 1.4.1.3.1. useEffect不同写法的执行差别
          2. 1.4.1.3.2. 函数组件内多个useEffect的执行次序
        4. 1.4.1.4. useMemo
        5. 1.4.1.5. useCallback
        6. 1.4.1.6. useRef
        7. 1.4.1.7. useContext
        8. 1.4.1.8. useReducer
      2. 1.4.2. Redux的使用
        1. 1.4.2.1. Store
    5. 1.5. 零碎知识
      1. 1.5.1. npm、yarn、nvm、镜像配置等概念及使用
        1. 1.5.1.1. npm
        2. 1.5.1.2. yarn
        3. 1.5.1.3. nvm
      2. 1.5.2. 国际化框架选型及使用
        1. 1.5.2.1. i18next
        2. 1.5.2.2. React-intl
        3. 1.5.2.3. 对比
      3. 1.5.3. webpack
        1. 1.5.3.1. 核心概念
      4. 1.5.4. 作用
      5. 1.5.5. devServer
        1. 1.5.5.1. 为什么要使用 devServer?
      6. 1.5.6. craco
        1. 1.5.6.1. 配置环境变量
      7. 1.5.7. babel转码器及兼容处理
        1. 1.5.7.1. Babel是如何工作的?
      8. 1.5.8. css module与postcss
        1. 1.5.8.1. css module
        2. 1.5.8.2. postcss
      9. 1.5.9. 常用工具
      10. 1.5.10. 移动端适配的概念
      11. 1.5.11. 后端模数据模拟
        1. 1.5.11.1. MOCKJS
        2. 1.5.11.2. 接口管理工具
      12. 1.5.12. Rollup的理解及与webpack的区别
      13. 1.5.13. 前端微服务框架
        1. 1.5.13.1. 微前端
        2. 1.5.13.2. singlespa、qiankun