You are my JavaScript Queen


  • 首页

  • 标签

  • 归档

一个移动端项目的工程化部署总结

发表于 2020-12-13

多环境部署

@vue/cli-service 安装了一个名为 vue-cli-service 的命令。你可以在 npm scripts 中以vue-cli-service、或者从终端中以 ./node_modules/.bin/vue-cli-service 访问这个命令。

npm run serve启动本地开发环境。

在项目根目录中新建env.*可以设置环境变量。

变量我们统一在 src/config/env.*.js 里进行管理。

config下新建对应的文件进行管理的目的是修改起来方便,不需 要重启项目,符合开发习惯。

解决1px问题:vw布局

安装postcss-px-to-viewport;
新建.postcssrc.js文件,参考文档进行配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = {
plugins: {
autoprefixer: {
overrideBrowserslist: ['Android 4.1', 'iOS 7.1', 'Chrome > 31', 'ff > 31', 'ie >= 8']
},
'postcss-px-to-viewport': {
unitToConvert: "px", // 要转化的单位
viewportWidth: 375, // 视窗的宽度,对应的是我们设计稿的宽度
unitPrecision: 6, // 指定`px`转换为视窗单位值的小数位数(很多时候无法整除)
propList: ["*"], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
viewportUnit: 'vw', // 指定需要转换成的视窗单位,建议使用vw
fontViewportUnit: "vw", // 指定字体需要转换成的视窗单位,默认vw
selectorBlackList: ['.ignore', '.hairlines'], // 指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名
minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值
mediaQuery: false, // 是否在媒体查询的css代码中也进行转换,默认false
exclude: [/node_modules/], // 设置忽略文件,用正则做目录名匹配
}
}
}

Sass全局样式

安装node-sass和sass-loader。
参考vue-cli官网针对css的配置文档,把全局的scss文件注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV)
const defaultSettings = require('./src/config/index.js')
module.exports = {
css: {
extract: IS_PROD,
sourceMap: false,
loaderOptions: {
scss: {
additionalData: `
@import "assets/css/mixin.scss";
@import "assets/css/variables.scss";
`,
},
},
},
}

移动端组件库

选择了有赞出品的vant,按需引入参考官网文档。

为了避免每个页面都还要import组件的写法,最好是在一个文件里统一做引入,挂载全局,这样任何组件都可以直接用了。
可以在 src/plugins/vant.js 下统一管理。

vant主题定制

首先安装sass-loader和sass包。
在babel.config.js的plugins里做修改:

1
2
3
4
5
6
7
const plugins = [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
style: (name) => `${name}/style/less`,
}, 'vant']
];

最后在vue.config.js里加入lessOptions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module.exports = {
css: {
loaderOptions: {
less: {
// 若 less-loader 版本小于 6.0,请移除 lessOptions 这一级,直接配置选项。
lessOptions: {
modifyVars: {
// 直接覆盖变量
'text-color': '#111',
'border-color': '#eee',
// 或者可以通过 less 文件覆盖(文件路径为绝对路径)
hack: `true; @import "your-less-file-path.less";`,
},
},
},
},
},
};

注意,如果选择直接覆盖变量,修改以后需要重启服务。
鉴于要修改的主题太多,全写在配置文件里看着很难受,所以我用hack引入文件,看着比较舒服,而且不需要重启服务,一举两得。
样式修改的主题就参考vant文档每一个组件下边的列表即可。

打包分析

使用webpack-bundle-analyzer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
chainWebpack: config => {
// 打包分析
if (IS_PROD) {
config.plugin('webpack-report').use(BundleAnalyzerPlugin, [
{
analyzerMode: 'static'
}
])
}
}
}

去掉console.log

因为开发过程中经常需要打印一些值来调试,但是生产环境的版本不应该打印出来。又不想一点一点删除,所以需要一个工具自动删除线上环境的console。
使用babel-plugin-transform-remove-console
在 babel.config.js 中配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 获取 VUE_APP_ENV 非 NODE_ENV,测试环境依然 console
const IS_PROD = ['production', 'prod'].includes(process.env.VUE_APP_ENV)
const plugins = [
[
'import',
{
libraryName: 'vant',
libraryDirectory: 'es',
style: true
},
'vant'
]
]
// 去除 console.log
if (IS_PROD) {
plugins.push('transform-remove-console')
}
module.exports = {
presets: [['@vue/cli-plugin-babel/preset', {useBuiltIns: 'entry'}]],
plugins
}

splitChunks 单独打包第三方模块

使用script-ext-html-webpack-plugin

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
module.exports = {
chainWebpack: config => {
config.when(IS_PROD, config => {
config
.plugin('ScriptExtHtmlWebpackPlugin')
.after('html')
.use('script-ext-html-webpack-plugin', [
{
// 将 runtime 作为内联引入不单独存在
inline: /runtime\..*\.js$/
}
])
.end()
config.optimization.splitChunks({
chunks: 'all',
cacheGroups: {
// cacheGroups 下可以可以配置多个组,每个组根据test设置条件,符合test条件的模块
commons: {
name: 'chunk-commons',
test: resolve('src/components'),
minChunks: 3, // 被至少用三次以上打包分离
priority: 5, // 优先级
reuseExistingChunk: true // 表示是否使用已有的 chunk,如果为 true 则表示如果当前的 chunk 包含的模块已经被抽取出去了,那么将不会重新生成新的。
},
node_vendors: {
name: 'chunk-libs',
chunks: 'initial', // 只打包初始时依赖的第三方
test: /[\\/]node_modules[\\/]/,
priority: 10
},
vantUI: {
name: 'chunk-vantUI', // 单独将 vantUI 拆包
priority: 20, // 数字大权重到,满足多个 cacheGroups 的条件时候分到权重高的
test: /[\\/]node_modules[\\/]_?vant(.*)/
}
}
})
config.optimization.runtimeChunk('single')
})
}
}

用JS实现一个单链表和常用操作方法

发表于 2020-10-12

链表

用一组任意存储的单元来存储线性表的数据元素。一个对象存储着本身的值和下一个元素的地址。

  • 需要遍历才能查询到元素,查询慢。
  • 插入元素只需断开连接重新赋值,插入快。

构造函数

链表的元素都是节点,首先要构建一个节点Node类,每个节点都是Node的实例。
Node类有两个属性,一个是节点的值,一个是对下一个节点的指向(next)。
链表需要一个头节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Node{
constructor(element){
this.element = element;
this.next = null;
}
}
class LinkList{
//构造出head节点
constructor(){
this.head = new Node('head');
}
}

添加方法

  1. 尾部添加节点
    添加节点非常简单,从头节点开始,用next属性做遍历,当某个节点的next属性为null,则说明到达尾部,将尾部节点的next指向要添加的节点。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //添加节点
    append(newVal){
    let newNode = new Node(newVal);
    //从头结点开始走
    let currentNode = this.head;
    //结点next不为null,可往后走
    while(currentNode.next!=null){
    currentNode = currentNode.next
    }
    currentNode.next = newNode;
    }
  2. 根据节点值查找节点
    链表和数组不同,并没有indexOf方法,只能逐个遍历判断。
    由于头节点没有值,将从头节点后的第一个节点开始进行。
    循环条件:节点不为空且节点值和目标值不等,则循环下一个节点。
    循环完毕没找到节点,返回-1

    1
    2
    3
    4
    5
    6
    7
    8
    //根据节点值查找节点
    findByValue(item){
    let currentNode = this.head.next;
    while(currentNode != null && currentNode.element != item){
    currentNode = currentNode.next;
    }
    return currentNode===null? -1 : currentNode
    }
  3. 根据索引值获取节点
    链表和数组不同,内存地址不连续,无法直接通过索引下标(链表根本不存在下标的概念)查找。
    头节点之后的节点是索引0号节点,用pos变量来记录节点的索引,来和传入的index值作比较。
    循环条件:节点不为空且pos和索引值不等,则循环下一个节点。
    循环完毕没找到节点(index超出节点数),返回-1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //根据index索引查找节点,下标从0开始
    findByIndex(index){
    //头节点之后的节点是索引0号节点
    let currentNode = this.head.next;
    let pos = 0;
    while(currentNode != null && pos != index){
    currentNode = currentNode.next;
    pos++
    }
    return currentNode===null ? -1: currentNode
    }
  4. 指定元素向后插入
    通过findByValue方法找到要插入的节点位置,在其后方插入新元素。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //指定元素向后插入
    insert(newElement, element){
    let currentNode = this.findByValue(element);
    if(currentNode === -1){
    console.log('未找到插入位置')
    return
    }
    let newNode = new Node(newElement)
    newNode.next = currentNode.next;
    currentNode.next = newNode;
    }
  5. 删除元素
    删除链表元素的方法很简单,找到要删除的元素,把它上一个节点的next指向它后一个节点即可。
    那么我们需要先写一个方法来找目标节点的前一个节点。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //查找前一个节点
    findPrev(item){
    let currentNode = this.head;
    while(currentNode.next != null && currentNode.next.element!=item){
    currentNode = currentNode.next
    }
    if(currentNode.next === null){
    return -1
    }
    return currentNode
    }

之后只要让把prevNode.next指向prevNode.next.next就完成了删除。

1
2
3
4
5
6
7
8
remove(item){
let prevNode = this.findPrev(item)
if(prevNode===-1){
console.log('未找到元素')
return
}
prevNode.next = prevNode.next.next
}

完整代码

为了方便测试,需要写一个打印链表元素的方法。非常简单,从头节点后的节点开始遍历,将节点的element打印出来。

1
2
3
4
5
6
7
8
9
10
//展示链表
display(){
let currentNode = this.head.next;
let linkedListElementChain = [];
while(currentNode!=null){
linkedListElementChain.push(currentNode.element)
currentNode = currentNode.next;
}
console.log(linkedListElementChain.join(' -> '))
}

完整示例

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
class Node{
constructor(element){
this.element = element;
this.next = null;
}
}
class LinkList{
//构造出head节点
constructor(){
this.head = new Node('head');
}
//添加节点
append(newVal){
let newNode = new Node(newVal);
//从头结点开始走
let currentNode = this.head;
//结点next不为null,可往后走
while(currentNode.next!=null){
currentNode = currentNode.next
}
currentNode.next = newNode;
}
//根据value查找节点
findByValue(item){
let currentNode = this.head.next;
while(currentNode != null && currentNode.element != item){
currentNode = currentNode.next;
}
return currentNode===null? -1 : currentNode
}
//根据index索引查找节点,下标从0开始
findByIndex(index){
//头节点之后的节点是索引0号节点
let currentNode = this.head.next;
let pos = 0;
while(currentNode != null && pos != index){
currentNode = currentNode.next;
pos++
}
return currentNode===null ? -1: currentNode
}
//指定元素向后插入
insert(newElement, element){
let currentNode = this.findByValue(element);
if(currentNode === -1){
console.log('未找到插入位置')
return
}
let newNode = new Node(newElement)
newNode.next = currentNode.next;
currentNode.next = newNode;
}
//查找前一个
findPrev(item){
let currentNode = this.head;
while(currentNode.next != null && currentNode.next.element!=item){
currentNode = currentNode.next
}
if(currentNode.next === null){
return -1
}
return currentNode
}
//根据值做删除
remove(item){
let prevNode = this.findPrev(item)
if(prevNode===-1){
console.log('未找到元素')
return
}
prevNode.next = prevNode.next.next
}
//展示链表
display(){
let currentNode = this.head.next;
let linkedListElementChain = [];
while(currentNode!=null){
linkedListElementChain.push(currentNode.element)
currentNode = currentNode.next;
}
console.log(linkedListElementChain.join(' -> '))
}
}
const LList = new LinkList();
LList.append('1');
LList.append('2');
LList.append('3');
LList.display(); // 1 -> 2 -> 3
LList.insert('0', '1')
LList.remove('3')
LList.display(); // 1 -> 0 -> 2

TypeScript里对象的类型——接口

发表于 2020-07-28

接口的意义

在java这种面向对象的编程语言里,接口(Interface)是非常重要的概念,是对行为的抽象,由类(class)去实现(implement)。
在TS里,接口还可以用来规范对象的描述。

例1

1
2
3
4
5
6
7
8
9
interface Person {
name: string;
age: number;
}
let john: Person = {
name: 'John',
age: 29
};

john的属性必须有name和age(且value类型也一致),不可多不可少不可增不可改。

例2

如果对象的某些属性处于可用可不用的状态,那就需要设定可选属性,在属性后加一个问号即可。

1
2
3
4
5
6
7
8
interface Person {
name: string;
age?: number;
}
let john: Person = {
name: 'John',
};

这样,john的age属性就可以不设定。
但是仍然不允许添加新的属性!

例3

如果对象的属性还不能够确定,有可能添加新的属性,则可以使用任意属性。

1
2
3
4
5
6
7
8
9
10
interface Person {
name: string;
age?: number;
[propName: string]: any;
}
let tom: Person = {
name: 'Tom',
gender: 'male'
};

任意属性的数值类型最好命为any,因为一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集

例4

如果对象的某个属性需要确定,避免更改,则可使用只读属性。
只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Person {
readonly id: number;
name: string;
age?: number;
[propName: string]: any;
}
let tom: Person = {
id: 13579,
name: 'John',
gender: 'male'
};
tom.id = 24680;
// index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

公司某个nuxt项目的经验总结

发表于 2020-04-01

最近花了四个月完成了一个nuxt项目的开发,组件库为iview。现在对项目中累积的经验做一个总结和记录。

1.开工前准备

本项目功能众多且复杂,需要先理顺整个业务逻辑。
先花了一整天的时间画出了思维导图,确定开发模块的顺序。
然后找出需要做的共用组件,用工厂模式开发组件。

2.nuxt的配置

已经不是第一次用nuxt了,现在对于nuxt的配置已经很娴熟。现把本次项目用到的配置和含义记录如下:

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
const isProdMode = Object.is(process.env.NODE_ENV, 'production');
export default {
mode: 'universal', //默认是universal,如果不需要服务端渲染则设置spa,但不需要服务端渲染又何必用nuxt,所以这一项配置基本不用关心
head: {
title: '', //网站标签页的标题
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'keywords', name: 'keywords', content: '' },
{ hid: 'description', name: 'description', content: '' }
], //常用的meta设置,方便SEO
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' } //favicon放置在static文件夹里
],
script: [
{ src: '' }, //引入需要的脚本文件,如百度统计文件
]
},
loading: { color: '#fff' }, //每个页面加载的进度条的颜色
css: [
{ src:'@/static/css/reset.scss', lang:'scss'}, //公用初始化设置css文件
],
plugins: [
{ src: '~/plugins/iview', ssr: true }, //引入的plugin,如组件库,iconfont,axios等,ssr为true的时候表示服务端渲染,建议涉及到window和document的部分ssr设置为false
],
modules: ["@nuxtjs/axios", "@nuxtjs/proxy"], //需要引入的模块,前者是网络请求axios必须,后者为开发模式下代理请求的必须
axios: { prefix: '/api/', proxy: true }, //请求的前缀
proxy: [
[
"/api/",
{
target: "http://xxx", // 后端服务地址
changeOrigin: true,
pathRewrite: { "^/api/": "/" }
}
]
],
dev: isProdMode,
env:{
BASE_API: '/api' //请求前缀,可根据开发环境和生产环境的不同做不同的设置
},
build: {
extend(config, ctx) {
config.resolve.extensions.push('.less') //对less类型的文件做处理
}
}
}

3.iview的定制主题

很多组件库的UI并不符合我们的设计稿,所以需要对iview的样式做全局修改。
在iview的github地址可以看到iview的全局样式设置,可以看到是less格式的,所以项目必须支持less的解析。
使用npm install less-loader安装less-loader,如果解析失败,可能是less版本问题导致,详细参考本篇文章。
在plugins下建立ui文件夹,目录如下:

1
2
3
ui
├── index.js # 入口文件
├── theme.less # 自定义覆盖变量

然后在nuxt.config.js文件的plugins里引入ui。

4.如何在nuxt项目中添加iconfont

本质上和正常的项目添加一样,主要是需要把css文件里的路径调整一致。
详情可以参考Nuxt使用iconfont矢量图标

5.scss做计算的方法

假定定义变量$a = ‘20px’,若要使用calc,直接calc(100% - a)这么用是不行的,需要按如下使用方法:  calc(100% - #{a})

6.做两个互相关联的datepicker

iview本身的datepicker如果要选择日期范围(即type为daterange),但是设计图稿要求用两个datepicker框来表示,这时候就需要用到data里绑定的属性值来做计算。
因为要用到this,所以disabledDate要用箭头函数表示。

1
2
3
4
<DatePicker type="date" v-model="startDate" :options="startOption" format="yyyy-MM-dd" style="width: 160px"></DatePicker>
<span style="margin:0 3px">至</span>
<DatePicker type="date" v-model="endDate" :options="endOption" format="yyyy-MM-dd" style="width: 160px"></DatePicker>
</div>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default {
    data(){
        return{
startDate:'',
endDate:'',
startOption:{
disabledDate: date => {
return date && date.valueOf() > Date.now() || date.valueOf()>this.endDate;
}
},
endOption:{
disabledDate: date => {
return date && date.valueOf() > Date.now() || date.valueOf()<this.startDate;
}
}
}
}
}

7.封装axios的注意事项

大部分请求都需要token,但对于不涉及用户权限和登录的接口是无需token的,那么在封装axios的时候就需要区别对待。
这次采取的方法是创建白名单,在白名单里的url就不从Cookie里拿token。

刷新token如何做?
比如token的有效期是1小时,用户有长达50分钟的时间内没有进行任何操作,此时用户进行刷新页面等操作需要刷新token来重新计算session时间。
后端接口会返回code为501,告知我需要刷新token进行更新。
对于刷新接口的操作采用“防抖”机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 是否正在刷新的标记
let isRefreshing = false;
if(response.data.code=="501"){
if (!isRefreshing) {
isRefreshing = true
return refreshToken().then(res => {
if (res.code === 200) {
Cookies.set('token', res.data.token);
}
}).finally(() => {
isRefreshing = false
})
}
}

8.组件全局注册

抽象出的组件可能会在许多其他组件里使用,每次使用都引入注册很麻烦,索性全局注册。
全局注册的文件放在plugins目录下,需要特别注意的是在nuxt.config.js里引入的时候ssr必须为true,否则服务端渲染和客户端渲染不一致 会导致组件的生命周期被执行两次。

9.组件网络请求放在mounted里

网络请求应该出现在mounted的生命周期里, 在created中的话, 因为还没有渲染出来, 会导致一个非常奇怪的ERROR connect ECONNREFUSED 127.0.0.1:80 的网络错误。
开发模式下没什么问题, 但是一旦打包 就会有很大的错误。

10.防止document和window对象的报错

防止报document or window is not defined的方法是做一个if(process.client)的判断:

1
2
3
if(process.client){
//写涉及到document和window相关的代码
}

vue源码学习的前期准备工作

发表于 2019-12-26

类型检查工具Flow

Flow是Facebook出品的静态类型检查工具。

Flow的工作方式

通常类型检查分成 2 种方式:

  • 类型推断:通过变量的使用上下文来推断出变量类型,然后根据这些推断来检查类型。

  • 类型注释:事先注释好我们期待的类型,Flow 会基于这些注释来判断。

在vue源码里找Flow

在Vue的主目录下,有一个 .flowconfig 文件,这是Flow的配置文件,其中[libs]按照官方文档是来描述指定库定义的目录,可以看出配置的是flow。
接着我们可以找到flow文件夹,可以看到文件如下:

1
2
3
4
5
6
7
8
flow
├── compiler.js # 编译相关
├── component.js # 组件数据结构
├── global-api.js # Global API 结构
├── modules.js # 第三方库定义
├── options.js # 选项相关
├── ssr.js # 服务端渲染相关
├── vnode.js # 虚拟 node 相关

在阅读源码的过程中,如果对于某个自定义类型的数据结构有疑问,可以翻阅对应的文件部分进行查阅

同时,在查阅Flow的资料时,发现很多同行认为TypeScript比Flow更先进,对于TypeScript可以在以后进行深入学习。

vue源码目录结构

vue的源码都在src的目录下,结构如下:

1
2
3
4
5
6
7
src
├── compiler # 编译相关
├── core # 核心代码
├── platforms # 不同平台的支持
├── server # 服务端渲染
├── sfc # .vue 文件解析
├── shared # 共享代码

compiler

编译相关。具有把模板解析成ast语法树,对ast语法树的优化,代码生成等功能。

cores

vue的核心代码就在这里,包括我们熟知的全局API、Vue实例化、虚拟DOM、工具函数等等。
这里是vue的灵魂。s

platform

我们已经知道阿里开发了weex,用来实现vue跑在原生客户端上。
platform 是 Vue.js 的入口,2 个目录代表 2 个主要入口,分别打包成运行在 web 上和 weex 上的 Vue.js。

server

从vue2.0开始支持了服务端渲染,主要的工作是把组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将静态标记”混合”为客户端上完全交互的应用程序。

sfc

这个目录下的代码逻辑会把 .vue 文件内容解析成一个 JavaScript 的对象。

shared

Vue.js 会定义一些工具方法,这里定义的工具方法都是会被浏览器端的 Vue.js 和服务端的 Vue.js 所共享的。

session, cookie和token的一个总结

发表于 2019-08-10

背景

在公司的多个项目里,有的使用了session做会话管理,前端无需做特殊处理,只要让后端朋友处理即可。有的使用了token,需要后端传来一个token,让前端来传入。
于是想针对会话管理做一个总结。


http是一个无状态协议

什么是无状态呢?就是说这一次请求和上一次请求是没有任何关系的,互不认识的,没有关联的。这种无状态的的好处是快速。坏处是假如我们想要把www.zhihu.com/login.html和www.zhihu.com/index.html关联起来,必须使用某些手段和工具


cookie和session

由于http的无状态性,为了使某个域名下的所有网页能够共享某些数据,session和cookie出现了。客户端访问服务器的流程如下:

  • 首先,客户端会发送一个http请求到服务器端。
  • 服务器端接受客户端请求后,建立一个session,并发送一个http响应到客户端,这个响应头,其中就包含Set-Cookie头部。该头部包含了sessionId。Set-Cookie格式:Set-Cookie: value[; expires=date][; domain=domain][; path=path][; secure]
  • 在客户端发起的第二次请求,假如服务器给了set-Cookie,浏览器会自动在请求头中添加cookie
  • 务器接收请求,分解cookie,验证信息,核对成功后返回response给客户端

注意

  • cookie只是实现session的其中一种方案。虽然是最常用的,但并不是唯一的方法。禁用cookie后还有其他方法存储,比如放在url中
  • 现在大多都是Session + Cookie,但是只用session不用cookie,或是只用cookie,不用session在理论上都可以保持会话状态。可是实际中因为多种原因,一般不会单独使用
  • 用session只需要在客户端保存一个id,实际上大量数据都是保存在服务端。如果全部用cookie,数据量大的时候客户端是没有那么多空间的。
  • 如果只用cookie不用session,那么账户信息全部保存在客户端,一旦被劫持,全部信息都会泄露。并且客户端数据量变大,网络传输的数据量也会变大

小结

简而言之, session 有如用户信息档案表, 里面包含了用户的认证信息和登录状态等信息. 而 cookie 就是用户通行证


token

token 也称作令牌,由uid+time+sign[+固定参数]
token 的认证方式类似于临时的证书签名, 并且是一种服务端无状态的认证方式, 非常适合于 REST API 的场景. 所谓无状态就是服务端并不会保存身份认证相关的数据。

组成

  • uid: 用户唯一身份标识
  • time: 当前时间的时间戳
  • sign: 签名, 使用 hash/encrypt 压缩成定长的十六进制字符串,以防止第三方恶意拼接
  • 固定参数(可选): 将一些常用的固定参数加入到 token 中是为了避免重复查库

存放

token在客户端一般存放于localStorage,cookie,或sessionStorage中。在服务器一般存于数据库中

token认证流程

token 的认证流程与cookie很相似

  • 用户登录,成功后服务器返回Token给客户端。
  • 客户端收到数据后保存在客户端
  • 客户端再次访问服务器,将token放入headers中
  • 服务器端采用filter过滤器校验。校验成功则返回请求数据,校验失败则返回错误码

token可以抵抗csrf,cookie+session不行

假如用户正在登陆银行网页,同时登陆了攻击者的网页,并且银行网页未对csrf攻击进行防护。攻击者就可以在网页放一个表单,该表单提交src为http://www.bank.com/api/transfer,body为count=1000&to=Tom。倘若是session+cookie,用户打开网页的时候就已经转给Tom1000元了.因为form 发起的 POST 请求并不受到浏览器同源策略的限制,因此可以任意地使用其他域的 Cookie 向其他域发送 POST 请求,形成 CSRF 攻击。在post请求的瞬间,cookie会被浏览器自动添加到请求头中。但token不同,token是开发者为了防范csrf而特别设计的令牌,浏览器不会自动添加到headers里,攻击者也无法访问用户的token,所以提交的表单无法通过服务器过滤,也就无法形成攻击。


分布式情况下的session和token

我们已经知道session是有状态的,一般存于服务器内存或硬盘中,当服务器采用分布式或集群时,session就会面对负载均衡问题。

  • 负载均衡多服务器的情况,不好确认当前用户是否登录,因为多服务器不共享session。这个问题也可以将session存在一个服务器中来解决,但是就不能完全达到负载均衡的效果。
    而token是无状态的,token字符串里就保存了所有的用户信息
  • 客户端登陆传递信息给服务端,服务端收到后把用户信息加密(token)传给客户端,客户端将token存放于localStroage等容器中。客户端每次访问都传递token,服务端解密token,就知道这个用户是谁了。通过cpu加解密,服务端就不需要存储session占用存储空间,就很好的解决负载均衡多服务器的问题了。这个方法叫做JWT(Json Web Token)

总结

  • session存储于服务器,可以理解为一个状态列表,拥有一个唯一识别符号sessionId,通常存放于cookie中。服务器收到cookie后解析出sessionId,再去session列表中查找,才能找到相应session。依赖cookie
  • cookie类似一个令牌,装有sessionId,存储在客户端,浏览器通常会自动添加。
  • token也类似一个令牌,无状态,用户信息都被加密到token中,服务器收到token后解密就可知道是哪个用户。需要开发者手动添加。
  • jwt只是一个跨域认证的方案
12…4
Snapline

Snapline

毕业于香港城市大学
苏州码农
热爱摇滚乐

24 日志
9 标签
GitHub 我的邮箱
Links
  • 张鑫旭
  • 阮一峰
  • 廖雪峰
  • witness
  • 慕课网
  • 掘金前端
  • Vuejs
  • 微信小程序
© 2021 Snapline
特别鸣谢 Hexo
|
主题 — NexT.Pisces v5.1.4