0%

Prettier和ESLint

在前端工程中,统一的代码风格规划更容易写出合理的、易于阅读和维护的代码。
Prettier是用来格式化代码的工具,使项目中的代码风格一致。
ESLint 是一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。
ps:ESLint 主要功能还是监测语法是否错误,格式化功能还是交给Prettier吧。

Vue中使用Prettier和ESLint

1
2
npm install --save-dev eslint eslint-plugin-vue
npm install --save-dev eslint-config-prettier

项目根目录创建.eslintrc.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = {
extends: [
/* vue2 配置 */
// 'plugin:vue/recommended',
// 'plugin:vue/essential',
// 'plugin:vue/strongly-recommended',
/* vue3 配置 */
'plugin:vue/vue3-recommended',
'plugin:vue/vue3-essential',
'plugin:vue/vue3-strongly-recommended',
// 解决ESLint和Prettier之间的冲突
'prettier',
],
rules: {
/* off,0不启用 warn,1警告 eror,2报错 */
'no-var': 2, //禁用var,用let和const代替
'no-unused-vars': 1, //不能有声明后未被使用的变量或参数
'no-empty': 1, // 块语句中的内容不能为空
eqeqeq: 1, // 是否使用全等
},
};

.vscode下创建settings.json

1
2
3
4
5
6
7
8
9
10
11
12
{
// 保存时格式化
"editor.formatOnSave": true,
// 所有文件使用 prettier 格式化
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
// eslint校验哪些类型的文件
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "Vue"],
// Vetur默认使用 eslint-plugin-vue@beta 来检测 <template>
"vetur.validation.template": true
}

项目根目录创建.prettierrc.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
//.prettierrc.js
module.exports = {
// 一行最多 120 字符
printWidth: 120,
// 使用 2 个空格缩进
tabWidth: 2,
// 用制表符代替空格
useTabs: false,
// 行尾需要有分号
semi: true,
// 使用单引号代替双引号
singleQuote: false,
// jsx中是否使用单引号替代双引号
jsxSingleQuote: false,
// 末尾使用逗号
trailingComma: "none",
// 大括号内的首尾需要空格 { foo: bar }
bracketSpacing: true,
//把多行HTML (HTML, JSX, Vue, Angular)元素的>放在最后一行的末尾,而不是单独放在下一行(不适用于自关闭元素)。
bracketSameLine: false,
// 箭头函数,只有一个参数的时候,也需要括号
arrowParens: "always",
// 是否使用根目录下的EditorConfig配置文件
useEditorConfig: false
};

修改.prettierrc.js配置后,重启生效!
配置ESLint和Prettier忽略文件
根目录下创建.eslintrc.js和.prettierignore

写在最后

这里的代码风格规范,其他框架只需要稍加修改,同样适用

前言

TypeScript已经成为前端必备技能了,以下是TypeScript的优势

  1. 静态类型检查:TypeScript是JavaScript的超集,它引入了静态类型检查,可以在编译时发现类型错误,减少在运行时出现的错误。这可以提高代码的可靠性和维护性。
  2. 更好的开发工具支持:TypeScript提供了更好的开发工具支持,如代码自动补全、代码导航、重构和调试等功能。这些工具可以提高开发效率,并减少调试和排错的时间。
  3. 更好的代码组织和重用:TypeScript支持面向对象的编程风格,可以使用类、接口、模块等概念来组织和重用代码。这有助于提高代码的可读性、可维护性和可扩展性。
  4. 更好的团队协作:TypeScript的静态类型检查可以帮助团队成员更好地理解和使用代码,减少因为类型错误导致的沟通和协作问题。此外,TypeScript还支持定义类型声明文件,可以提供给其他团队成员使用,减少对代码的依赖和理解成本。
  5. 兼容性和迁移性:TypeScript兼容JavaScript的语法和库,可以无缝地集成到现有的JavaScript项目中。此外,TypeScript还提供了一些工具和指南,可以帮助开发者将现有的JavaScript代码逐步迁移到TypeScript。

开始使用TypeScript

在项目中安装TypeScript

1
npm i typescript -g

在TypeScript官网打开演练场
TypeScript官网

类型推断

1
2
3
4
// bad
let str = 'abc'
str = 10
// typescript会根据你给变量存放的初始值来进行变量类型限定,这在我们开发中很有意义,因为变量一旦分配了字符串值,那意味着后期可能会书写一些字符串相关功能,比如方法等相关的操作,如果过程中突然分配了一个数值,那意味着这些针对字符串的操作都是会出现错误的,为了避免这些情况,我们才需要使用TypeScript

类型注解

1
let str:string = 'abc' || let str:string  str = 'abc'

类型断言

1
2
3
4
5
6
7
8
9
10
11
// bad
let numArr = [1,2,3]
// find() 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。
const result = numArr.find(item => 2)
result * 5

// 假如我们确定result的最终的结果是必然有值大于2的,那我们就可以通过类型断言的方式来设置
// good
let numArr = [1,2,3]
const result = numArr.find(item => 2) as number
result * 5

基础类型和联合类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let v1: string = 'abc'
let v2:number = 10
let v3:boolean = true
let nu:null = null
let un:undefined = undefined
// 大家可能比较好奇,null和undefined不常用,为什么还要有,那是因为TypeScript还支持一种类型:联合类型
let v4:string | null = null
// null默认是可以分配给其他任意类型的,当我们勾选strictNullChecks时,我们就不能随意将null分配给其他类型

// 我们除了可以限制一些类型以外,还可以做一些具体值的限定
// bad
let v5: 1 | 2 | 3 = 5
// good
let v5: 1 | 2 | 3 = 2

数组、元组、枚举

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
// 数组
// bad
let arr:number[] = [1,2,3,'a']
// good
let arr:number[] = [1,2,3]
// good
let arr1: Array<string> = ['a','b','c']

// 元组
let t1:[number,string,number] = [1,'a',2]
// bad
t1[0] = 'a'
// 其实在操作的时候,这个元组并不是说完全必须得是三个值,如果希望某个值可选得话,你也可以采用一个'?'的方式来写一个可选参数
// good
let t1:[number,string,number?] = [1,'a']

// 枚举
// 枚举需要使用一个新的关键字叫 enum[ɪˌnjuːm],一般枚举首字母大写
enum MyEnum {
A,
B,
C
}
// 这里我们可以使用两种方式来操作,第一种方式我们可以使用:'MyEnum.A',或者我们可以利用值的方式来访问枚举类型:'MyEnum[0]',枚举类型当我们声明好以后它是会自动做值的分配,默认是从0分配,a是0,b是1,c是2
console.log(MyEnum.A) //0
console.log(MyEnum[0]) //A

函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//void类型,void其实就是空的意思,void只能被分配undefined(严格模式),一般我们很少会给变量去赋值void,通常是一个函数,比如一个函数没有返回值的时候,默认返回undefined
function myfn(a:number,b:string):void {
console.log(a+b)
}
// 除了可以这样写以外,还有一些其他用法,比如可以把这些参数设置成可选的参数,这和我们使用的ES6是比较类似的,所有的可选参数应该在必选参数的后面
function myfn(a:number,b?:string):void {
if(b) {
console.log(a+b)
} else {
console.log(a)
}
}
// 注意'?'这种写法只能在TypeScript使用

// 除此之外,也可以将参数设置默认值,设置剩余参数
function myfn(a:number = 10,b:string,c?:boolean,...rest:number[]):void {
console.log(a+b)
}
// bad
myfn(5,6,7,1,2,3)
// good
myfn(5,'6',true,1,2,3)

接口

1
2
3
4
5
6
7
8
9
10
// 接口的关键字是'interface',通常用来去进行一个对象的定义
interface Obj {
name:string,
age:number
}
const obj:Obj = {
name:'a',
age:10
}
// 这种方式就避免在声明数据的时候出现一些偏差

类型别名

1
2
3
4
5
6
7
8
9
// 比如需要定义几个用户名变量,可能是联合类型
let a: string | number = 10
let b: string | number = 20
let c: string | number = 'abc'
//使用类型别名,类型别名关键字'type'
type MyUserName = string | number
let a: MyUserName = 10
let b: MyUserName = 20
let c: MyUserName = 'abc'

泛型

1
2
3
4
5
6
7
8
9
function myFn(a:number,b:number):number[] {
return [a,b]
}
// 假设这个函数是一个比较通用的函数,既可以处理数字值,又可以处理字符串值,布尔值等等,这种情况下,想用它来传其他类型数据,就不可以了,因为它规定的是number,那有没有可能写number | string呢?那样的话会出现交叉使用的情况,为了避免这个问题,就可以采用泛型的概念
function myFn<T>(a:T,b:T):T[] {
return [a,b]
}
myFn<number>(1,2)
myFn<string>('a','b') || myFn('a','b')

JS异步的概念

我们的JS程序几乎是由多个块(函数)构成的,这些块中只有一个是现在执行,其余的则会在将来执行

1
2
3
4
5
// A
ajax("..",function(..) {
//C
})
// B

// A 和 // B 表示程序的前半部分(也就是现在的部分),而// C 标识了程序的后半部分(也就是将来的部分)。前半部分立刻执行,然后是一段时间不确定的停顿。在未来的某个时刻,如果Ajax调用完成,程序就会从停下的位置继续执行后半部分,这个ajax函数就是异步函数。

事件循环

举例来说,如果你的JavaScript程序发出一个Ajax请求,从服务器获取一些数据,那你就在一个函数(通常称为回调函数)中设置好响应代码,然后JavaScript引擎会通知宿主环境(对多数开发者来说通常就是Web浏览器):“嘿,现在我要暂停执行,你一旦完成网络请求,拿到了数据,就请调用这个函数”。然后浏览器就会设置侦听来自网络的响应,拿到要给你的数据之后,就会把回调函数插入到事件循环,以此实现对这个回调的调度执行
那么,什么是事件循环?
先通过一段伪代码了解一下这个概念:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// eventLoop是一个用作队列的数组
// (先进,先出)
let eventLoop = [];
let event;

// “永远”执行
while(true) {
// 一次tick
if(eventLoop.length > 0) {
// 拿到队列中的下一个事件
event = eventLoop.shift();
// 现在,执行下一个事件
try {
event();
}
catch (err) {
reportError(err);
}
}
}

一定要清楚,setTimeout(..)并没有把你的回调函数挂在事件循环队列中。它所做的是设定一个定时器。当定时器到时后,环境(对多数开发者来说通常就是Web浏览器)会把你的回调函数放在事件循环中,这样,在未来某个时刻的tick会摘下并执行这个回调。

回调地狱

现在来看一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function drive(fn, timer) {
setTimeout(fn, timer);
}
function play(fn, timer) {
setTimeout(fn, timer);
}
function dinner(fn, timer) {
setTimeout(fn, timer);
}
(function () {
drive(() => {
console.log("正在开车去商场"); // A
}, 3000);
play(() => {
console.log("正在商场玩"); // B
}, 2000);
dinner(() => {
console.log("正在商场吃晚餐"); // C
}, 1000);
})();

// 我们希望代码依次按照A,B,C的顺序执行
// 实际上输出顺序C,B,A

上述代码输出结果与预期不符是因为setTimeout是异步函数,根据事件循环队列先进先出的原则,它会依次执行C,B,A函数的回调函数

使用回调函数调整异步函数执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function drive(fn, timer) {
setTimeout(fn, timer);
}
function play(fn, timer) {
setTimeout(fn, timer);
}
function dinner(fn, timer) {
setTimeout(fn, timer);
}

(function () {
drive(() => {
console.log("正在开车去商场"); // A
play(() => {
console.log("正在商场玩"); // B
dinner(() => {
console.log("正在商场吃晚餐"); // C
}, 1000);
}, 2000);
}, 3000);
})();
// 此时输出顺序为A,B,C

这里我们得到了三个函数嵌套在一起构成的链,其中每个函数代表异步序列(任务,”进程”)中的一个步骤
这种代码常常被称为回调地狱(callback hell),有时也被称为毁灭金字塔(pyramid of doom,得名于嵌套缩进产生的横向三角形状)
很显然,这种代码是复杂和难以阅读维护的,于是Promise出现了

Promise

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise是一个对象(广义),从它可以获取异步操作的消息。Promise提供统一的API,各种异步操作都可以用同样的方法进行处理
Promiese对象有以下两个特点
(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
ES6规定,Promise对象是一个构造函数,用来生成Promise实例
现在来看一段Promise代码

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
console.log("我是同步代码A");
const drive = new Promise((resolve, reject) => {
console.log("我是drive Promise里的代码");
setTimeout(() => {
resolve("正在开车去商场");
}, 3000);
});
const play = new Promise((resolve, reject) => {
console.log("我是play Promise里的代码");
setTimeout(() => {
resolve("正在商场玩");
}, 2000);
});
const dinner = new Promise((resolve, reject) => {
console.log("我是dinner Promise里的代码");
setTimeout(() => {
resolve("正在商场吃晚餐");
}, 1000);
});
drive
.then((data) => {
console.log(data);
return play;
})
.then((data) => {
console.log(data);
return dinner;
})
.then((data) => {
console.log(data);
});
console.log("我是同步代码B");
// 输出结果:
// 我是同步代码A
// 我是drive Promise里的代码
// 我是play Promise里的代码
// 我是dinner Promise里的代码
// 我是同步代码B
// 正在开车去商场
// 正在商场玩
// 正在商场吃晚餐

上面代码中,Promise新建后立即执行,所以首先输出的是 Promise里的代码。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以 正在开车去商场…最后输出

有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve(异步操作成功时调用)和reject(异步操作失败时调用)。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
了解:
Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)

async await

ES2017标准引入了async函数,使得异步操作变得更加方便。
async函数是什么?一句话,它就是Generator函数的语法糖
async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function drive() {
setTimeout(() => {
console.log("正在开车去商场");
}, 3000);
}
function play() {
setTimeout(() => {
console.log("正在商场玩");
}, 2000);
}
function dinner() {
setTimeout(() => {
console.log("正在商场吃晚餐");
}, 1000);
}
(async function () {
await drive();
await play();
await dinner();
})();

这两天在b站刷视频,偶然看到了一位阿里P8的程序员,叫徐赫老师。他分享了一些程序员职业规划的话题,我觉得这个话题很有意义,无论是什么行业,职业规划都是很有必要的。在这里大概记录一下徐赫老师所说的话题。
徐赫老师主要分享的两个话题,一:程序员如何更好的工作;二:程序员晋升需要注意什么

程序员如何更好的工作

这里主要分为三个阶段
1-3年阶段
关键词:折腾
1~3年是打基础的阶段,要不断折腾。主动学习市面主流技术,了解同样的语言和工具,多刨根问底,了解一下原理,提高自身的核心竞争力。如果只是一知半解,那么永远只是一个初级水平。

3-5年
关键词:沉淀
3~5年要对自己工作做一些提效,多思考。如何更快速和更好的去完成crud,提高工作效率。

5-10年
关键词:优秀
5~10年要以终为始,让自己成为一个优秀的人。优秀不是标准,它是一个比例,不是说跑步要跑进多少秒,而是要比别人跑得更快。如果团队别人都不写周报,那么就要尝试去写周报,因为你比别人做得更好。如果大家都写周报,那就把周报写得更好。代码如何写得更好,文档如何写得更好,沟通如何做到更好。要知止,要知道自己的目标是什么,只有知道自己的目标是什么,才能比别人做得更好。

程序员晋升需要注意什么

一:始终要保持积累沉淀和终身学习的一个状态
二:练能力,涨本事
能力和本事是两回事,能力是具备的基本素质,本事是解决问题的能力。 以能力来看,十个刘邦都不一定打得过项羽,所以项羽是一个有能力的人,但是刘邦是有本事的人。所以技术能力极强,但是不能给企业和业务和公司带来效益,带来价值,那其实用处不大。
三:坚持
坚持学习,坚持优秀,坚持终身学习的这种心态
四:拓宽自己的人脉

数据类型概述

基本的数据类型

数值(number),字符串(string),布尔值(boolean)
至于undefined和null,一般将它们看成两个特殊值

复杂的数据类型

对象是最复杂的数据类型,又可以分为三个子类型

  • 狭义的对象(object)
  • 数组(array)
  • 函数(function)

基本数据类型的拷贝

基本数据类型的值是存入栈内存中的。值与值之间是相互独立存在的,当我们改变了a的值,对b是没有影响的,所以b输出的还是10

1
2
3
4
5
let a = 10;
let b = a;
a = 100;
console.log(a); // 100
console.log(b); // 10

复杂的数据类型拷贝(引用数据类型)

引用数据类型的值是存入堆内存中的,变量a存的是一个对象的内存地址(对象的引用),所以只是把a的内存地址赋值给b,如果两个变量保存的是同一个内存地址(对象的引用),每当通过修改任意一个变量修改属性时,另一个也会受到影响

1
2
3
4
5
6
7
let a = {
age: 10,
};
let b = a;
a.age = 100;
console.log(a.age); // 100
console.log(b.age); // 100
1
2
3
4
5
let a = [1,2,3]
let b = a
a[0] = 100
console.log(a) // [100, 2, 3]
console.log(b) // [100, 2, 3]

深拷贝

如果是基本的数据类型,那么它们之间的拷贝都是深拷贝。如果是引用数据类型,只要把内部的引用类型数据全部都独立开一块内存空间,让它们之间完全不要有共用,数据完全相互不干扰就是深拷贝

浅拷贝

只要内部的引用类型数据有共用,数据会相互干扰就是浅拷贝

Git

git分支命名规范

1
2
3
4
5
6
// feat: 新功能
// perf: 功能优化(performance)
// fix: bug修复
// config: 配置修改,策略调整
// day: 日常提交
// merge: 代码合并: 比如分支同步master最新代码

master分支与其他分支

主分支master分支用来发布新版本
平时代码提交到自己的分支

下载分支

1
2
git clone https://github.com/... .git	// 下载所有分支代码到本地
git clone -b <branchName> https://github.com/... .git // 下载指定分支到本地

查看分支

1
2
3
git branch		// 查看本地分支
git pull // 更新
git branch -r // 查看远程所有分支

创建分支

1
git branch <branchName>		// 保存在本地,要在远程创建分支最终需要push

切换分支

1
git checkout <branchName>

创建+切换分支

1
git checkout -b <branchName>

本地分支关联远程分支

1
git branch --set-upstream-to=origin/<branch>

提交分支

1
git push origin <branchName>	// 如果远程没有这个分支,它会自动创建这个分支

取消git add

1
2
3
// git reset命令可以用来重置暂存区,也可以用来撤销提交
git reset <file> // 取消单个文件
git reset . // 取消所有文件的暂存

删除分支

1
2
git branch -d <branchName>		// 删除本地分支
git push origin --delete <branchName> // 删除远程分支

下载远程指定分支到本地指定分支

1
2
3
git checkout -b <本地分支名称> origin/<远程分支名称>
// 例如
git checkout -b my-branch origin/feature-branch

更改分支名称

1
2
3
// 更改本地分支名称
// -M 强制重命名
git branch -M <old_branch_name> <new_branch_name>
1
2
3
4
// 更改远程分支名称
git push origin :refs/heads/old_branch refs/heads/new_branch
// 例如
git push origin :old_branch new_branch

使用merge命令合并分支

1.1、多人开发,需要将远程代码pull下来

1
git pull  origin <branchName>

1.2、合并某分支到当前分支

1
2
Administrator	/d/code/...	(master)	// 当前分支
git merge dev // 被合并的分支

Git 无法自动合并分支时,必须先解决冲突,再提交,合并完成
1.3、查看状态

1
git status

1.4、提交

1
git push origin

查看分支合并图

1
git log --graph

git回退

当我们出现错误提交的情况时,我们希望能撤销提交操作,让程序回到提交前的样子,这里我们使用回退(reset)
git回退版本

1
git reset --hard <目标版本号>

git 强制推送
如果用git push 会报错,因为我们本地库HEAD指向的版本比远程库的要旧

1
git push -f

git错误信息

1
husky - pre-commit hook exited with code 1 (error)

chatgpt给出报错原因:
这个错误通常与 pre-commit hook 脚本中的问题有关。pre-commit hook 是在提交之前运行的脚本,用于执行一些代码检查、格式化或测试等操作。当 pre-commit hook 脚本返回非零的退出代码时,会触发此错误。
要解决这个问题,您可以按照以下步骤进行排查:

  1. 检查错误消息:查看错误消息中是否有关于错误原因的具体信息。这可能包括脚本的名称、路径、行号等。这些信息可以帮助您定位问题。
  2. 检查 pre-commit hook 脚本:打开 pre-commit hook 脚本文件,查看其中是否有错误。可能是脚本语法错误、缺少依赖项或其他问题导致脚本执行失败。确保脚本是可执行的,并且没有任何语法错误。可以尝试在命令行中手动执行该脚本,看是否会出现任何错误。
  3. 检查 pre-commit hook 的依赖项:如果 pre-commit hook 脚本依赖于其他工具或库,请确保这些依赖项已正确安装,并且在执行脚本时可以访问到。
  4. 检查 Husky 配置:检查您的 Husky 配置文件,如 package.json 或 .husky 目录中的配置文件。确保 pre-commit hook 的配置正确无误。检查 hooks 部分是否正确设置了 pre-commit hook,并且指向了正确的脚本文件或命令。
  5. 检查 Git hooks 目录:检查 Git 仓库中的 hooks 目录,通常是 .git/hooks。确保 pre-commit hook 脚本位于这个目录中,并且具有正确的权限。如果您使用了 Husky,它会自动将 pre-commit hook 脚本复制到 .git/hooks 目录中。确保脚本存在于正确的位置,并且可以执行。

绕过钩子脚本

在 commit时加上 –no-verify参数

1
git commit -m "xxx" --no-verif

git 合并多条commit记录

如图所示,想要合并前面三条记录为一条记录

1
git rebase -i ...	// 版本号

1

输入i进入插入模式,将需要被合并的两条记录改为s或squash
2

Esc退出编辑模式,Shift+:,wq保存退出

自定义commit message注释
3
输入i进入插入模式,将不需要的提交信息前面加#号注释
4
Esc退出编辑模式,Shift+:,wq保存退出

成功合并
5

强制推送到远程分支仓库
6

Delete ‘␍’ eslint(prettier/prettier) 错误的解决方案

1
git config --global core.autocrlf false

项目优化

项目打包以及本地预览

1
2
3
4
5
6
7
// 打包
yarn build
// 本地预览
// 安装全局安装本地服务包
npm i -g serve
// 或 直接使用
npx serve -s ./build

打包体积分析

1
2
3
4
5
6
7
8
9
10
11
12
13
// 安装分析打包体积的包
yarn add source-map-explorer
// 在package.json中添加一条命令 "anylyze": "source-map-explorer 'build/static/js/*.js"
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject",
"anylyze": "source-map-explorer 'build/static/js/*.js"
}
// 使用
yarn anylyze

路由懒加载

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
// 导入 lazy 和 Suspense 需要配合使用
import { lazy, Suspense } from 'react'
// 正常导入页面
import Layout from '@/pages/Layout'
import Login from './pages/Login'
import NotFound from './pages/NotFound'

// 懒加载导入,
const Layout = lazy(() => import('@/pages/Layout'))
const Login = lazy(() => import('./pages/Login'))
const NotFound = lazy(() => import('./pages/NotFound'))

// Suspense组件需要将把懒加载的组件包裹在里面
export default function App() {
return (
<Router history={customHistroy}>
{/* 使用Suspense 包裹所有内容 */}
<Suspense fallback="loading..."> //fallback可以写jsx代码,可以使用组件制作loding效果
<div className="app">
<Switch>
<Route path="/" exact render={() => <Redirect to="/home" />} />
<AuthRoute path="/home" component={Layout} />
<Route path="/login" component={Login} />
{/* 404 */}
<Route>
<NotFound />
</Route>
</Switch>
</div>
</Suspense>
</Router>
)
}

配置CDN(内容分发网络)

通过 craco 来修改webpack配置,从而实现CDN优化

craco文档地址

外部扩展(Externals)

安装 craco

1
2
3
yarn add  @craco/craco
//或者
npm install @craco/craco --save

在项目 根目录新建craco.config.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
const { whenProd, getPlugin, pluginByName } = require('@craco/craco')
// whenProd => when production 针对生产环境做处理
// whenProd()第一个参数,在生产环境中,第二个参数,默认值
// whenProd(() => {},[])
const path = require('path')
module.exports = {
// webpack配置
webpack: {
// 配置别名
alias: {
// 约定:使用 @ 表示 src 文件所在路径
'@': path.resolve(__dirname, 'src'),
},
configure: (webpackConfig) => {
// 生产环境下,配置 externals
webpackConfig.externals = whenProd(
() => ({
react: 'React',
'react-dom': 'ReactDOM',
redux: 'Redux',
'react-router-dom': 'ReactRouterDOM',
}),
{}
)
// 拿到 HtmlWebpackPlugin 插件
const { isFound, match } = getPlugin(
webpackConfig,
pluginByName('HtmlWebpackPlugin')
)
// 生产环境下,暴露 CDN 链接到模板页面中
if (isFound) {
// match 表示当前匹配的插件 HtmlWebpackPlugin
// 通过 options 将额外的数据添加给 HtmlWebpackPlugin 插件
// options可以传递任意数据,比如,此处我们自己写的 cdn 就是要额外传递的数据
match.options.cdn = {
// js 链接
js: whenProd(
() => [
'https://cdn.bootcdn.net/ajax/libs/react/18.2.0/cjs/react-jsx-dev-runtime.production.min.js',
'https://cdn.bootcdn.net/ajax/libs/react-dom/18.2.0/cjs/react-dom-server-legacy.browser.production.min.js',
'https://cdn.bootcdn.net/ajax/libs/redux/4.2.1/redux.min.js',
'https://cdn.bootcdn.net/ajax/libs/react-router-dom/6.10.0/react-router-dom.production.min.js',
],
[]
),
// css 链接
css: [],
}
}
return webpackConfig
},
},
}

public/index.html中:

1
2
3
4
5
6
7
	
<div id="root"></div>
<!-- 加载第三方包的 CDN链接 -->
<% htmlWebpackPlugin.options.cdn.js.forEach(cdnURL => { %>
<script src="<%= cdnURL %>"></script>
<% }) %>
</body>

查找项目中对应的CDN

unpkg
bootcdn

组件复用

antd 组件复用示例

如图所示,在内容管理路由组件和发布文章组件都有Select选择器

1

2

将该功能单独封装成组件,复用的组件应当完善和健壮。
这个组件是From表单的受控组件。
antd的自定义表单控件的说明From
说明:
自定义或第三方的表单控件,也可以与 Form 组件一起使用。只要该组件遵循以下的约定:
1.提供受控属性 value 或其它与 valuePropName 的值同名的属性
2.提供 onChange 事件或 trigger 的值同名的事件。

From表单

1
2
3
4
5
6
7
8
9
10
11
12
// pages/Article/index.js
import { Channels } from '@/components/Channels'
export default function Article() {
...
return(
<Form>
<Form.Item label="频道:" name="channel_id">
<Channels/>
</Form.Item>
</Form>
)
}

复用的组件

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
// components/Channels
import { Select } from 'antd'
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { getChannels } from '@/store/actions'

//不是受控组件
export const Channels = ({ ...rest }) => { // 接收到表单的参数
console.log(rest) // {value: undefined, id: 'channel_id', onChange: ƒ} 更换频道也拿不到当前值
const dispatch = useDispatch()
const { channels } = useSelector((value) => value.article)
useEffect(() => {
dispatch(getChannels()) //分发获取参数的状态
}, [dispatch])
return (
<Select
placeholder="请选择文章频道"
>
{channels.map((items) => (
<Select.Option key={items.id} value={items.id}>
{items.name}
</Select.Option>
))}
</Select>
)
}

// 改为受控组件,给Select组件添加value,onChange属性,width是自定义参数,可以自定义样式
export const Channels = ({ value,onChange,width }) => {
const dispatch = useDispatch()
const { channels } = useSelector((value) => value.article)
useEffect(() => {
dispatch(getChannels())
}, [dispatch])
return (
<Select
style={
{
width,
}
}
placeholder="请选择文章频道"
value={value} // 指定当前选中的条目
onChange={onChange} //选中 option,或 inputvalue 变化时,调用此函数
>
{channels.map((items) => (
<Select.Option key={items.id} value={items.id}>
{items.name}
</Select.Option>
))}
</Select>
)
}

最后在两个路由组件引入组件就行了

1
2
3
4
5
6
7
8
9
10
11
import { Channels } from '@/components/Channels'
export default function Article() {
...
return(
<Form>
<Form.Item label="频道:" name="channel_id">
<Channels width={xxx}/>
</Form.Item>
</Form>
)
}

路由鉴权

路由鉴权是项目中必不可少的操作,目的是确认用户是否有权限访问某个路由,例如,用户未拿到token时,就只能访问到登录路由。
自己封装一个 路由组件实现路由鉴权

1
2
3
4
5
6
7
8
9
10
// App.js

import { AuthRoute } from './components/AuthRoute'
<Router>
<Switch>
{/* <Route path="/home" component={Layout} /> */}
{/* 自己封装路由组件 */}
<AuthRoute path="/home" component={Layout} />
</Switch>
</Router>
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
// components/AuthRoute.js

import { Route, Redirect } from 'react-router-dom'
// 为utils下所有模块统一暴露
import { isTOken } from '@/utils' //isTOken函数返回一个布尔值,确认localStorage是否存有token const isTOken = () => Boolean(getToken())

//接收到路由组件的传参,解构除了component属性的剩余参数, conponent重命名
export const AuthRoute = ({ component: Component, ...rest }) => {
// console.log(rest)
return ( // 返回一个<Route/>
<Route
{...rest} //将接收到的参数再传给<Route/>组件
render={(props) => //render的回调函数,按条件渲染一个路由组件,传入props参数
isTOken() ? ( // 判断是否有token
<Component /> // 有token,正常渲染路由组件
) : (
<Redirect // 无token
to={{
pathname: '/login', // 重定向到login组件
state: { from: props.location.pathname }, // 向路由组件传递state参数,props.location.pathname 为当前路由组件路径
}}
/>
)
}
/>
)
}

未登录时重定向到登录页面后返回原来页面

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
// pages/Login/login.js

import { useHistory, useLocation } from 'react-router-dom'
export default function Login() {
const dispatch = useDispatch()
const history = useHistory()
const location = useLocation()
const onFinish = async (values) => {
try {
await dispatch(login({ mobile: values.mobile, code: values.code }))
message.success('登录成功!', 1.5, () => {
//根据路由地址跳转页面,location?.state?.from的值要么是原来页面的路由,要么为undefined,当左侧操作符为 undefined和null时,?? 执行右侧操作数
history.replace(location?.state?.from ?? '/home')
})
} catch (e) {
message.warning(
e.response
? e.response?.data?.message ?? '出错了~'
: '网络繁忙,请稍后再试',
1.5
)
}
}
return (...)
}

redux

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。官网
安装redux

1
2
3
npm install --save redux

yarn add redux

redux三大核心属性

action

Action 本质上是 JavaScript 普通对象。我们约定,action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作。多数情况下,type 会被定义成字符串常量。

1
2
// 定义了一个add函数,函数返回一个action对象
const add = (payload) => ({ type: 'add', payload })

reducer

reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。
注意 reducer 是纯函数。它仅仅用于计算下一个 state。它应该是完全可预测的:多次传入相同的输入必须产生相同的输出。它不应做有副作用的操作,如 API 调用或路由跳转。这些应该在 dispatch action 前发生。
下面代码就是reducer接收了一个旧的state和action,返回了新的state

1
2
3
4
5
6
7
8
9
10
11
const reducer = (state, action) => {
switch (action.type) {
case 'add':
return (state = state + action.payload)
default:
return state
}
}
reducer(0,add(1))
// 等价于
// reducer(0,{ type: 'add', payload:1 })

store

在前面的代码中,我们学会了使用 action 来描述“发生了什么”,和使用 reducers 来根据 action 更新 state 的用法。
Store 就是把它们联系到一起的对象。Store 有以下职责:
维持应用的 state;
提供 getState() 方法获取 state;
提供 dispatch(action) 方法更新 state;
通过 subscribe(listener) 注册监听器;
通过 subscribe(listener) 返回的函数注销监听器。
再次强调一下 Redux 应用只有一个单一的 store。当需要拆分数据处理逻辑时,你应该使用 reducer 组合 而不是创建多个 store。
根据已有的 reducer 来创建 store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { configureStore } from '@reduxjs/toolkit'
// action
const Add = (payload) => ({ type: 'add', payload })
// reducer
const reducer = (state = 0, action) => {
switch (action.type) {
case 'add':
return state + action.payload
default:
return state
}
}
// 创建一个store实例
const store = configureStore({ reducer })
// 每次state更新时,打印日志,getState,获取最新state
// 返回一个函数来注销监听器
const unsubscribe = store.subscribe(() => console.log('state:',store.getState())) //1 3
// dispatch,更新state
store.dispatch(Add(1))
store.dispatch(Add(2))

// 停止监听state更新
unsubscribe()

configureStore

1
2
3
4
5
// configureStore是用来替代createStore方法
const store = configureStore({
reducer:xxx,
preloadedState:xxx, //初始化状态
})

Redux获取状态默认值的执行过程

只要创建store,那么,Redux就会调用一次 reducer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// action
const Add = (payload) => ({ type: 'add', payload })
// reducer
const reducer = (state, action) => { // 这里没有给 state赋值,所以返回undefined.可以这样给state赋值:state = 0
console.log('reducer',state,action); //reducer undefined {type: '@@redux/INITr.5.x.p.5'}
switch (action.type) {
case 'add':
return state + action.payload
default:
return state
}
}
// store
const store = configureStore({reducer}) // 只要创建了store,redux就会调用一次reducer

这一次调用renucer的目的:获取状态的默认值,
第一次调用reducer时,redux机制会让action.type的value是一个随机的字符串,以确保return的是state
redux内部第一次调用reducer:

1
reducer undefined {type: '@@redux/INITr.5.x.p.5'}

redux数据流

严格的单向数据流是 Redux 架构的设计核心。
这意味着应用中所有的数据都遵循相同的生命周期,这样可以让应用变得更加可预测且容易理解。同时也鼓励做数据范式化,这样可以避免使用多个且独立的无法相互引用的重复数据。

React-redux

Provider

在项目入口文件,一般是src目录下的index.js,为整个项目(App组件)提供redux状态管理

1
2
3
4
5
6
7
8
9
10
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import App from './App'
import store from './store'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<App />
</Provider>
)

完整示例

获取个人信息的示例,完整的展示了redux的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 在pages/Layout/index.js中,分发获取个人信息的异步action
import { useDispatch, useSelector } from 'react-redux' //useDispatch用来分发一个action
import { getUserInfo } from '@/store/actions'
export default function GeekLayout() {
const dispatch = useDispatch()
useEffect(() => {
dispatch(getUserInfo()) // 这里的函数由actions组件提供,根据接口所需要的数据,来决定getUserInfo()要不要填入实参
}, [dispatch])
...
return(
...
{name}
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 2. 在 store/actions/user.js中,创建异步action并获取个人信息
import { http } from '@/utils' // 封装了axios,简写了请求地址
export const getUserInfo = () => { // 异步action可以是一个函数,也可以是一个对象,同步action只能是一个对象
return async (dispatch, getState) => { // dispatch, getState是由thunk中间件提供的参数
const { login: token } = getState() // getState是一个函数,返回redux中存储的状态,这里解构拿到了login对象的值,重命名为token
const res = await http.get('/v1_0/user/profile', {
headers: {
Authorization: `Bearer ${token}`, // 传入个人信息所需要提供的token
},
})
// 3.将接口返回的信息 dispatch到reducer来存储该状态
dispatch({ type: 'user/userInfo', payload: res.data.data })
}
}
1
2
3
4
5
6
7
8
9
10
// 4. 在 store/reducers/user.js中,处理个人信息的action,将状态存储到redux中
const initialState = {} // 因为个人信息状态是一个对象,所以默认状态为一个对象
export const user = (state = initialState, action) => {
switch (action.type) {
case 'user/userInfo':
return action.payload
default:
return state
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 5. 在pages/Layout/index.js中,获取个人信息并展示
import { useDispatch, useSelector } from 'react-redux'
import { getUserInfo } from '@/store/actions'
export default function GeekLayout() {
const dispatch = useDispatch()
useEffect(() => {
dispatch(getUserInfo())
}, [dispatch])
// 拿到redux中的状态
const { name } = useSelector((state) => state.user)
...
return(
...
{name} // 渲染状态
)
}

reducer的分离与合并

根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。

根 reducer 的结构完全由你决定。Redux 原生提供combineReducers()辅助函数,来把根 reducer 拆分成多个函数,用于分别处理 state 树的一个分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {combineReducers} from 'redux'
function todos(state = [], action) {
// 省略处理逻辑...
return nextState
}

function visibleTodoFilter(state = 'SHOW_ALL', action) {
// 省略处理逻辑...
return nextState
}

// 将多个reducer合并根reducer
let todoApp = combineReducers({
todos,
visibleTodoFilter
})

当你触发 action 后,combineReducers 返回的 todoApp 会负责调用两个 reducer:

1
2
let nextTodos = todos(state.todos, action)
let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action)

然后会把两个结果集合并成一个 state 树:

1
2
3
4
return {
todos: nextTodos,
visibleTodoFilter: nextVisibleTodoFilter
}

action type or constants

为了解决action和reducer中字符串误写错和命名冲突,建议单独抽离放在 actiontypes或constants目录中,一般放在store目录下
目录结构

|– store
|– actions 负责对应组件的action
|– actiontypes 抽离字符串
|– reducers 负责对应组件的reducer
|– index.js 入口文件,创建唯一的store

1
2
3
4
// actiontypes目录
const add = 'counter/add'
const sub = 'counter/sub'
export { add, sub }