Server 端的认证——拥抱 JWT(一)

What is Json Web Token

根据官网的定义,JWT 是一套开放的标准(RFC 7519),它定义了一套简洁的(compact)、自包含的(self-contained)方案,来让我们安全地在客户端和服务器之间传递 JSON 格式的信息。

Advantages

  • 体积小,因而传输速度快
  • 传输方式多样,可以通过 URL/POST 参数/HTTP 头部 等方式传输
  • 严谨的结构化。它自身(在 payload 中)就包含了所有与用户相关的验证消息,如用户可访问路由、访问有效期等信息,服务器无需再去连接数据库验证信息的有效性,并且 payload 支持为你的应用而定制化
  • 支持跨域验证,多应用于单点登录。

单点登录(Single Sign On):在多个应用系统中,用户只需登陆一次,就可以访问所有相互信任的应用。

WHY JWT

除了上面说到的优点之外,相比传统的服务端验证, JWT 还有以下优点。

  • 充分依赖无状态 API ,契合 RESTful 设计原则
  • 易于实现 CDN,将静态资源分布式管理
  • 验证解耦,无需使用特定的身份验证方案, token 可以在任何地方生成
  • 比 cookie 更支持原生移动端应用
  1. 关于状态
    首先我们先看一下,什么是状态,什么是有状态与无状态。

状态:请求的状态是 client 与 server 交互过程中,保存下来的相关信息,客户端的保存在 page/request/session/application 或者全局作用域中,而 server 的一般存在 session 中。

有状态 API:server 保存了 client 的请求状态, server 会通过 client 传递的 sessionID 在其 session 作用域内找到之前交互的信息并应答。

无状态 API:无状态是 RESTful 架构设计的一个非常主要的原则。无状态 API 的每一个请求都是独立的,它要求由客户端保存所有需要的认证信息,每次发请求都要带上自己的状态,以 url 的形式提交包含了 cookies 等状态的数据。

JWT 就很好地体现了无状态原则。用户登陆之后,服务器会返回给他一个 token,由他保存在本地,在这之后的对服务器的访问都要带上这串 JWT ,来获得访问服务器相关路由、服务及资源的权限。比如单点登录就比较多地使用了 JWT,因为它的体积小,并且简单处理(使用 HTTP 头带上 Bearer 属性 + token )就可以支持跨域操作。

  1. 分布式管理
    在传统的 session 验证中,服务端必须保存 session ID,用于与用户传过来的 cookie 验证。而在一开始保存 session ID 时, 只会保存在一台服务器上,所以只能由一个 server 应答,就算其他服务器有空闲也无法应答,因此也利用不到分布式服务器的优点。
    而 JWT 依赖的是在客户端本地保存验证信息,不需要利用服务器保存的信息来验证,所以任意一台服务器都可以应答,服务器的资源也被较好地利用。

  2. 验证解耦
    只要拥有生成 token 所需的验证信息,在何处都可以调用 token 生成接口,无需繁琐的耦合的验证操作,可谓是一次生成,永久使用。

  3. 对原生应用的支持(我对移动端开发不够深入,这点不是很清楚)
    原生的移动应用对 cookie 与 session 的支持不够好,而对 token 的方式支持较好。

除此之外,JWT 的可靠的结构化的标准,也是我们选择它的一大原因。特别是使用 nodejs 开发时,嗯,node 大法好!

client 使用 JWT 与 server 交互的过程

image

首先,拥有某网站账号的某 client 使用自己的账号密码发送 post 请求 login,由于这是首次接触,server 会校验账号与密码是否合法,如果一致,则根据密钥生成一个 token 并返回,client 收到这个 token 并保存在本地的 localStorage。在这之后,需要访问一个受保护的路由或资源时,而只要附加上你保存在本地的 token(通常使用 Bearer 属性放在 Header 的 Authorization 属性中),server 会检查这个 token 是否仍有效,以及其中的校验信息是否正确,再做出相应的响应。

JWT 由三部分组成:Header/Payload/Signature

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Header
{
"alg": "HS256",
"type": "JWT"
}
// Payload
{
// reserved claims
"iss": "a.com",
"exp": "1d",
// public claims
"http://a.com": true,
// private claims
"company": "A",
"awesome": true
}
// $Signature
HS256(Base64(Header) + "." + Base64(Payload), secretKey)
JWT = {Base64(Header), Base64(Payload), $Signature}

第一部分是 Header。首先声明一个 JSON 对象,对象里有一个 type 属性,值为 JWT ,以及 alg 属性,值为 HS256,表明最终使用的加密算法是 HS256。

第二部分是 Payload Claim。这一部分被定义为实体的状态,就像 token 自身附加元数据一样,claim 包含我们想要传输的信息,以及用于服务器验证的信息,一般有 reserved/public/private 三类。

第三部分是 Signature。它由前面在 Header 指定的算法 HS256 加密两个参数构成,第一个参数是经过编码的 Header 与经过编码的 Payload 通过 . 连接之后的字符串,第二个参数是生成的密钥,会由服务器保存。每次服务器接收到 token 之后,也是先解密出用于验证的用户信息以及密钥,再与自己保存的密钥对比是否相同,以此来验证用户的身份。

写完这么一大篇真是好累…还想继续深挖一下 JWT 具体的应用、服务端如何验证、JWT 使用 cookie 存储好还是 HTML5 Web Storage 好的,剩下的只能留在下回分解啦。。

感谢以下参考文章:
Json Web Token Introduction
使用Json Web Token设计Passport系统
深入RESTful无状态原则

相信自己的能力,喜欢自己的人生

image
(图片来自硅谷 IT 女神朱赟( yun )的公众号:嘀嗒嘀嗒,本文也启发自她)

近几个月自己的情绪总是不稳定,容易煞有其事地闹点小脾气。也不太喜欢主动和人说话,还出‘高价’买了防噪音耳塞,想躲进世界的角落里。

就在九月底,我给自己做了许多个测试,结合自己的表现,‘诊断’出自己患了抑郁症。需要‘积极地进行自我治疗’,也的确在最初的几天里,在学习和锻炼中找到了生活的充实感。但是过没几天,又变回了老样子。觉得自己一天下来,并学不到什么,所以干脆自暴自弃,寄情于买买买之中。夜里睡前也能看着购物 APP 上自己并不缺但是在搞活动的商品,睡意全无,完全忘了前一天还信誓旦旦地说要早睡早期不让癌症接近自己。

也会忧心,其实上班真是一件好累人的事呢,怪不得学校要给我们一年的时间,来习惯从学生到上班族的转变。但是再怎么样,都是要迈入职场的呀。

今天读了安姐的一篇《每座城市都是一道风景》,里面有一句话让我久久深深触动。

我们四年前从德州搬来加州的时候,房子价格贵了一倍,面积却变成了原先的一半。好在这辈子已经住过大房子了,也没有什么遗憾了。

再想一想自己来到广州这个城市一晃也已三年有余。我喜欢这座城市,她让我遇见了许多可爱的人事物,让我走出了自命不凡的小宇宙,让我喜欢上人与人之间思想的交流与碰撞——让我有能力一步步过上自己想要的生活。但是安姐这句话让我重新审视当下的自己——这个不知足不懂发现美的自己。

仔细想想,我一切的一切不开心与愤懑,其实一开始就是自己带给自己的,连那抑郁症也是。换做是我,搬了家房租贵了,房子还小了,我一定又会生气,并企图找到个条件和之前差不多的房子。那样肯定又会花很多时间在这个上面,自然学习、休息、锻炼等等有趣的事情的时间就这么被压缩了,生活只剩下了一大片碎片时间。而自身的实践证明,零零碎碎的两个小时,还比不上连续的二十分钟来得有效率。更自然而然地,我也就什么事都做不好了。周而复始,陷入死循环。其实生活中这样的死循环比比皆是。

记得高中那时,和舍友说过:我觉得自己现在看事物都会先看到 TA 美好的地方,就比如刚刚你和我说的刚刚迎面走来的女生胖胖的,我却是先看到她那双半月形的会说话的眼睛,真灵动;纵然这几天都阴雨绵绵,我也会因为早上一抹稍纵即逝的阳光而心怀雀跃。

喜欢广州地铁的那句公益广告词:人的每一步,都在书写自己的历史。

历史不会重演,但是我却想做一次历史回退。

我二话不说,打开这二十年自己的版本管理器查看 history ,找到上面说的高中那时的 commit id (版本号)并拷贝,敲入 git reset --hard <commit id>,回车。收到 HEAD is now at <commit id> ... 的响应。

- 这么巧,刚好回到你在的这里。
- 不巧,我在等你。

Welcome back to 1023, Sweety..

javascript 原生 bind() 的 ES6 + ES5 实现

前言: 本来只是想写一下简单的 bind 函数实现,没想到写着写着还能牵出 js 中继承的知识,其实研究原生函数的实现总是能学到很多新东西

在实现之前呢,我们首先要知道 bind 是做什么的。JS MDN 给出的定义是 The bind() methods creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called. ,简单来说就是 bind() 函数创建了一个新函数(原函数的拷贝),这个函数接受一个提供新的 this 上下文的参数,以及之后任意可选的其他参数。当这个新函数被调用时,它的 this 关键字指向第一个参数的新上下文。而第二个之后的参数会与原函数的参数组成新参数(原函数的参数在后),传递给函数。

弄清楚这个之后,我们再来分析看看要怎么做。

首先,调用 bind() 会返回一个闭包,这个闭包中创建了一个新函数,这个函数首先包含原函数的属性与方法,并且这个函数的 this 值是传给 bind() 函数第一个参数,所以自然而然我们想到用 call 或者 apply 来改变原函数的 this ,这里我们选择 apply, 理由是我们新函数的第一个之后的参数是由传给 bind() 的第二个及之后的参数(代码中的 formerArgs )再加上原函数的参数(代码中的 laterArgs )构成的,我们把它们拼接成一个数组就完事儿了~具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Function.prototype.bind = function (ctx) {
// 保存原函数的 this 至 _this
var _this = this
// slice 使用了两次,保存到变量中
// slice 主要用于类数组对象 arguments 的浅复制
var slice = Array.prototype.slice
// 传给 bind() 函数的第二个至之后的参数,从 arguments 的第二位开始
var formerArgs = slice.call(arguments, 1)
// bind() 本身就是一个函数,返回
return function (){
// 传给原函数的参数
let laterArgs = slice.call(arguments, 0)
// 返回一个函数,这个函数调用了原函数,并且 this 指向 bind 的第一个参数,
// 第二个参数由 formerArgs 与 laterArgs组成
return _this.apply(ctx, formerArgs.concat(laterArgs))
}
}

ES6 实现

上面的代码是基于 ES5 实现的,当时对于不确定的参数的一般处理方法都是利用类数组对象 arguments (其中包含了传递给函数的所有参数),也就免不了使用 call 或者是 apply 对其进行数组操作。代码也就显得比较冗长。但是 ES6 不一样了呀~我们有了不定参数这个神器。无论有无参数,有几个参数都可以简单地处理。

不定参数: 传递给函数的最后一个参数可以被标记为不定参数,当函数被调用时,不定参数之前的参数都可正常被填充,剩下的参数会被放进一个数组中,并被赋值给不定参数。而当没有剩下的参数时,不定参数会是一个空数组,而不会被填充为 undefined

同样的功能,只要几行代码就可以实现:

1
2
3
4
5
6
7
8
9
10
// formerArgs 为传递给 bind 函数的第二个到之后的参数
Function.prototype.bind = function (ctx, ...formerArgs) {
let _this = this
// laterArgs 为传递给原函数的参数
return (...laterArgs) => {
// bind 函数的不定参数在原函数参数之前,formerArgs 本身就是数组,可以直接调用数组的 concat 方法,无需借助 call 或 apply
return _this.apply(ctx, formerArgs.concat(laterArgs))
}
}

至此,我们就实现了简单的 bind() 函数的功能,接下来我们给它做点优化。

优化 upupup..

  • 当 Function 的原型链上没有 bind 函数时,才加上此函数
1
2
3
if (!Function.prototype.bind) {
// add bind() to Function.prototype
}
  • 只有函数才能调用 bind 函数,其他的对象不行。即判断 this 是否为函数。
1
2
3
if (typeof this !== 'function') {
// throw NOT_A_FUNCTION error
}
  • 压轴戏: 关于继承
    我们上面的代码使用了借用 apply 继承的方式。用了 apply 来改变 this 的指向,继承了原函数的基本属性和引用属性,并且保留了可传参优点,但是新函数无法实现函数复用,每个新函数都会复制出一份新的原函数的函数,并且也无法继承到原函数通过 prototype 方式定义的方法或属性。

    为解决以上问题,我们选用 组合继承 方式,在使用 apply 继承的基础上,加上了原型链继承。
    所以我们可以这么改。

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
if (!Function.prototype.binds) {
Function.prototype.binds = function (ctx) {
if (typeof this !== 'function') {
throw new TypeError("NOT_A_FUNCTION -- this is not callable")
}
var _this = this
var slice = Array.prototype.slice
var formerArgs = slice.call(arguments, 1)
// 定义一个中间函数,用于作为继承的中间值
var fun = function () {}
var fBound = function (){
let laterArgs = slice.call(arguments, 0)
return _this.apply(ctx, formerArgs.concat(laterArgs))
}
// 先让 fun 的原型方法指向 _this 即原函数的原型方法,继承 _this 的属性
fun.prototype = _this.prototype
// 再将 fBound 即要返回的新函数的原型方法指向 fun 的实例化对象
// 这样,既能让 fBound 继承 _this 的属性,在修改其原型链时,又不会影响到 _this 的原型链
fBound.prototype = new fun()
return fBound
}
}

在上面的代码中,我们引入了一个新的函数 fun,用于继承原函数的原型,并通过 new 操作符实例化出它的实例对象,供 fBound 的原型继承,至此,我们既让新函数继承了原函数的所有属性与方法,又保证了不会因为其对原型链的操作影响到原函数。用图来表示应该是下面这样的:

image

这样我们对新函数的 prototype 修改只会应用在它自己身上,而不会影响到原函数。

  • 其他
    MDN 中还提到,若是将 bind 绑定之后的函数当作构造函数,通过 new 操作符使用,则不绑定传入的 this,而是将 this 指向实例化出来的对象。所以我们最终的代码为:
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
if (!Function.prototype.binds) {
Function.prototype.binds = function (ctx) {
if (typeof this !== 'function') {
throw new TypeError("NOT_A_FUNCTION -- this is not callable")
}
var _this = this
var slice = Array.prototype.slice
var formerArgs = slice.call(arguments, 1)
var fun = function () {}
var fBound = function (){
let laterArgs = slice.call(arguments, 0)
// 若通过 new 调用 bind() 之后的函数,则这时候 fBound 的 this 指向的是 fBound 实例,
// 而下面又定义了 fBound 是 fun 的派生类(其 prototype 指向 fun 的实例),
// 所以 this instanceof fun === true ,这时 this 指向了 fBound 实例,不另外绑定!
return _this.apply(this instanceof fun ? this : ctx || this, formerArgs.concat(laterArgs))
}
fun.prototype = _this.prototype
fBound.prototype = new fun()
return fBound
}
}

打完收工~~欧耶 ( •̀ ω •́ )y

P.S: ES7 中已经淘汰了 .bind 的写法,而是使用两个冒号的方式 :: 来代替,(要不要这么可爱!不过呢,人家还只是个提案而已,到时候会不会被采用还不一定的呢

用法有两种,第一种 ::对象.方法名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let obj = {
val: 'value',
method: function () {
console.log(this.val)
}
};
// ::对象.方法名
// 等价于 obj.method.bind(obj)
// 将 method 的 this 绑定为 obj
// 这里的 method 必须是 obj 的方法
::obj.method;
// call
obj.method(); // value

第二种, 对象::方法名()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let obj = {
val: 'value'
};
function method () {
console.log(this.val);
}
// 对象::方法名()
// 等价于 method.call(obj) 或 method.apply(obj)
// 会直接调用并输出 'value'
// obj::method();
// 对象::方法 (无括号
// 等价于 method.bind(obj)
// 不会自动执行,必须赋值给 method 才能实现绑定
// 手动调用 method ,输出 vaue
method = obj::method;
method();

嘛~ 有说的不对的地方,欢迎指正..

Vuex 入门

Vuex 是一个专门为 Vue.js 应用所设计的集中式状态管理架构 .

背景:小型应用里的每个组件维护着自有的状态,即当前应用的状态的一部分,所以整个应用的状态被分散在了各个角落,但是我们经常遇到要把状态的一部分共享给多个组件的情况。

状态其实可以形象地想成我们的 data 里面的各个属性。


State

Vuex 使用了单状态树(single state tree),一个 store 对象就存储了整个应用层的状态。它让我们可以更方便地定位某一具体的状态,并且在调试时能简单地获取到当前整个应用的快照。

  • 先埋个伏笔。Vuex 使用的这种 single state tree 与 modularity 模块化是不冲突的,问题是,如何将 state 与 mutation 分到子模块中?
  • 要使用 store ,首先必须Vue.use(Vuex),然后将 store const store = new Vuex.store() inject 定义到 Vue 实例 app 中new Vue({store}),实现从根组件注入到所有子组件中,接着就可以在子组件中使用 this.$store 调用了。
  • 当一个组件需要使用多个某 store 的状态属性或 getters ,可以使用 shared helper —— 共享帮手 mapState,它会返回一个对象 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
it('helper: mapState (object)', () => {
const store = new Vuex.Store({
state: {
a: 1
},
getters: {
b: () => 2
}
})
const vm = new Vue({
store,
computed: mapState({
// 在 mapState 里面我们既可以调用 store 的 state ,也可以调用 store 的 getters
a: (state, getters) => {
return state.a + getters.b
}
})
})
expect(vm.a).toBe(3)
store.state.a++
expect(vm.a).toBe(4)
})

那么如何将它与本地的计算属性结合使用呢?一般我们会使用一个工具,将多个对象合而为一,再把这个最终的对象传递给 computed。但是这里我们可以直接使用 es6 的 stage 3 的 object spread operator —— 对象扩展操作符,来超简洁地实现这一功能。

1
2
3
4
5
6
7
computed: {
localComputed () {}
// 将其中的属性与本地的计算属性合并在一起
...mapState({
message: state => state.obj.message
})
}

Getters

有时候我们需要从 store 的状态派生出其他状态,然后对这个状态(的方法)在多个组件中加以利用。通常我们的做法是复制这个方法,或者将它封装为一个公用的方法,然后在需要的时候导入,但是两者其实都不甚理想。Vuex 提供了 getters 属性,用途类似 stores 中的计算属性。
getters 中的方法接受两个参数,分别为 state 以及 getters(其他 getters),用法如下。

1
2
3
4
5
6
getters: {
// ...
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}

那么我们在其他组件内部使用 getters 也变得十分简单

1
2
3
4
5
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}
  • mapGetters
    可以将 store 的 getters 映射到本地的计算属性中来,除了可以使用数组之外,还可以使用对象起别名。
1
2
3
4
5
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])

Mutations

能改变 Vuex store 中的 state 状态的唯一方法是提交 mutation 变更。mutation 和事件很像:都有字符串类型的 type 以及 handler 句柄。我们在 handler 中实际修改 state,state 为每个 mutation 的第一个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// mutate state
state.count++
}
}
})
// call, 只有在使用 type increment 调用 mutation 时才能称为 handler
store.commit('increment')

commit 的第二个可选参数为 payload 有效载荷,可以为普通类型或对象类型等等。
commit 方法还可以通过对象形式调用,这种情况下,这个对象都会被当成 payload 。

1
2
3
4
store.commit({
type: 'increment',
amount: 10
})
  • little tips
  • 建议使用大写命名 Mutation
    将所有大写变量存放在一个文件中,需要的时候引入。使用 es6 的计算属性名新特性来使用常量作为方法名。
1
2
// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutation-types'
const store = new Vuex.Store({
state: { ... },
mutations: {
// we can use the ES2015 computed property name feature
// to use a constant as the function name
[SOME_MUTATION] (state) {
// mutate state
}
}
})

es6 计算属性名

1
2
3
4
5
6
7
8
// e.g: 使用含有空格的变量作为属性名会报错,此时可以将它存为字符串或者存在中括号包裹的变量中
var lastName = "last name";
var person = {
"first name": "Nicholas",
// 中括号包裹的变量
[lastName]: "Zakas"
};
console.log(person["last name"]); // Zakas
  • mutations 必须都是同步的,它的改变必须在调用之后立即执行
    因为它是唯一可以修改 state 的,如果它使用了异步方法,将会使我们的 state 变得无法追踪,定位问题也变得是否困难
  • 在组件中 commit mutation 时
    可以使用 this.$store.commit() 或者使用 mapMutations 方法,后者可以将组件中的方法映射到 store.commit 调用(需要在根组件注入 store)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
// 传入数组
...mapMutations([
'increment' // map this.increment() to this.$store.commit('increment')
]),
// 传入对象,可以使用 alias
...mapMutations({
add: 'increment' // map this.add() to this.$store.commit('increment')
})
}
}

Actions

actions 是提交 mutations 的,它可以有任意的异步操作。
actions 的第一个参数是 context,它向外暴露一组与 store 实例相同的方法/属性,所以可以直接调用 context.commit 或者访问 context.state 或者 context.getters 。我们通常使用 es6 的参数解构来简化我们的代码,直接写成 { commit }

1
2
3
4
5
actions: {
increment ({ commit }) {
commit('increment')
}
}
  • 如何触发 Actions?
    actions 通过store.dispatch('actionName') 触发,其方法体中再触发 mutation,但是 mutations 是可以直接通过 store.commit 触发的,那么为什么不直接使用 store.commit(‘mutationName’) 呢?因为,actions 是可以异步执行的,而 mutations 只可以同步。所以这种 dispatch 调用可以在 action 内执行异步操作,也就是说可以执行异步 mutation。
  • 可以使用 payload 格式或者对象形式触发。二者等价
1
2
3
4
5
6
7
8
9
10
// dispatch with a payload
store.dispatch('incrementAsync', {
amount: 10
})
// dispatch with an object
store.dispatch({
type: 'incrementAsync',
amount: 10
})
  • shopping cart 中的实际应用,既调用了异步 API,又提交了多个 mutation。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
actions: {
checkout ({ commit, state }, payload) {
// save the items currently in the cart
const savedCartItems = [...state.cart.added]
// send out checkout request, and optimistically
// clear the cart
commit(types.CHECKOUT_REQUEST)
// the 异步 shop API accepts a success callback and a failure callback
shop.buyProducts(
products,
// handle success
() => commit(types.CHECKOUT_SUCCESS),
// handle failure
() => commit(types.CHECKOUT_FAILURE, savedCartItems)
)
}
}
  • 在组件中分发 Actions
    可以使用 this.$store.dispatch() 或者 mapActions 映射组件方法到 store.dispatch 中调用(需要注入 root)。同 mapMutations
  • Actions 组合,怎么控制 actions 执行呢?
    由于 actions 是异步的,因此我们就很难知道一个 action 什么时候完成,以及该怎么把多个 action 组合起来,处理复杂的异步工作流?
    好在, store.dispatch() 方法返回了我们定义的 action handler 的返回值,所以我们可以直接返回一个 Promise 呀~
1
2
3
4
5
6
7
8
9
10
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}

可以这么用

1
2
3
store.dispatch('actionA').then(() => {
// ...
})

然后在另一个 action 中

1
2
3
4
5
6
7
8
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}

Modules

由于 Vuex 使用了单状态树,所以随着我们应用的规模逐渐增大, store 也越来越膨胀。为了应对这个问题,Vuex 允许我们将 store 分成多个 modules。每个 module 有着自己的 state, mutations, actions, getters, 甚至可以有嵌套( nested )的 modules。比如说:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
// 注意,调用的时候,多个模块都在 state 对象中,而非 modules 中
store.state.a // -> moduleA's state
store.state.b // -> moduleB's state
  • modules 中的各种 state , local or root?
    • mutations 和 getters 中,接受的第一个参数是 modules 的本地 state
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const moduleA = {
state: { count: 0 },
mutations: {
increment: (state) {
// state is the local module state
state.count++
}
},
getters: {
doubleCount (state) {
return state.count * 2
}
}
}
  • 相似地,在 actions 中,context.state 为本地 state,而 context.rootState 为根 state
1
2
3
4
5
6
7
8
9
10
const moduleA = {
// ...
actions: {
incrementIfOdd ({ state, commit }) {
if (state.count % 2 === 1) {
commit('increment')
}
}
}
}
  • getters 的第三个参数才是 root state
1
2
3
4
5
6
7
8
const moduleA = {
// ...
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}
}

Strict Mode & Form Handling

严格模式下,如果在 mutation handler 之外修改了 Vuex 的 state,应用就会抛错。比如我们将 Vuex 中的某个数据,用 Vue 的 v-model 绑定到 input 时,一旦感应到 input 改动,就会尝试去直接修改这个数据,严格模式下就会报错。所以建议是绑定 value 值,然后在 input 时调用 action 。

1
<input :value="message" @input="updateMessage">
1
2
3
4
5
6
7
8
9
10
11
// ...
computed: {
...mapState({
message: state => state.obj.message
})
},
methods: {
updateMessage (e) {
this.$store.commit('updateMessage', e.target.value)
}
}

mutation 可以这么处理

1
2
3
4
5
mutations: {
updateMessage (state, message) {
state.obj.message = message
}
}

诚然,这样做是很仔细明了的,但是我们也不能用 v-model 这么好用的方法了,另外一个方法就是继续使用 v-model ,并配套使用 双向计算属性和 setter 。

1
2
3
4
5
6
7
8
9
10
11
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
// 直接 commit 到 mutation,type 为 updateMessage
this.$store.commit('updateMessage', value)
}
}
}

建议部署到开发环境的时候一定一定要关掉严格模式。

Vue 2.0 制作列表组件,实现分页、搜索、批量操作等

本文仅讲解如何使用 Vue 创建一个实现分页、搜索、批量操作的列表组件,所以只提供此小组件的代码及说明,不提供其之外的代码或配置

github 地址: https://github.com/jothy1023/vue-component

使用到的技术或框架


  • Vue.js (2.0) 一套基于 MVVM 的 progressive framework,可以帮助我们迅速搭建用户界面
  • semantic UI 一套漂亮的 UI 框架
  • localStorage HTML5 的本地存储 API 之一(另一个为 sessionStorage),顾名思义, localStorage 保存在当前设备内存中,除非主动删除,否则一直存在

准备工作


  • 安装 Vue 2.0 (rc.6)
  • 引入 semantic.js 文件
  • 创建 Audit.vue 文件

具体过程


一个简单的 Vue 组件由三部分组成:html 模板 、javascript 脚本 以及 css 样式。下面我们一一来看。
效果图如下:

oops 图裂了.jpg

oops 图裂了.jpg

用到的数据结构是一个简单的 user 对象数组,对象格式为

1
2
3
4
5
user {
id: int
name: String,
audit: int
}

其中 audit 用来标记是否审核成功,共 4 个状态从 0 到 3,分别为 未审核、正在审核、已审核、审核不通过。

html 模板

由一对<template></template>标签作为唯一父标签包裹住模板内容,模板内分为 filter (过滤)、container (主体)两部分。

container

container 主体是一个 table 表格,表格内容 tbody 部分使用 Vue 的 v-for 指令,基于 users 数组渲染出列表。主要代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<tr v-for="user in filteredUsers">
<td class="collapsing">
<div class="ui toggle slider checkbox" v-if="aKey!==''">
<input type="checkbox" :value="user" v-model="selectedUsers"> <label></label>
</div>
</td>
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>
<i class="radio black icon" v-if="user.audit==0"></i>
<i class="minus blue icon" v-if="user.audit==1"></i>
<i class="checkmark green icon" v-if="user.audit==2"></i>
<i class="remove red icon" v-if="user.audit==3"></i>
</td>
<td>
<ui-button css="primary" v-if="user.audit==0" @click="user.audit=1">审核</ui-button>
<ui-button css="green" v-if="user.audit==1" @click="user.audit=2">通过审核</ui-button>
<ui-button css="red" v-if="user.audit==1" @click="user.audit=3">不通过审核</ui-button>
<ui-button css="grey" v-if="user.audit==2 || user.audit==3" @click="user.audit=1">再次审核</ui-button>
</td>
</tr>

其中,filteredUsers 为计算属性 computed 的一个 getter,用于从模板中分理出较复杂的逻辑,此处返回过滤后的 users 。
下来有一个 type 为 checkbox 的 input,当按照某一审核要求过滤时显示,其 v-model 指向 selectedUsers 数组,用来保存批量操作中选中的 user ,之后可以直接对此数组进行操作,改动会同步到 users 。

filter

filter 部分由一个 input 输入框以及一个 select 选择框组成,分别可以通过用户名和审核状态过滤。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div class="filter">
<ui-input css="icon">
<input type="text" v-model="fKey" placeholder="输入姓名搜索.." />
<i class="search icon"></i>
</ui-input>
<!-- dropdown -->
<ui-dropdown :setting="{allowAdditions: true}" css="selection">
<input name="aKey" type="hidden" v-model.lazy="aKey">
<div class="default text">Select a id</div>
<i class="dropdown icon"></i>
<div class="menu">
<div class="item" data-value="">所有</div>
<div class="item" data-value="0">未审核</div>
<div class="item" data-value="1">正在审核</div>
<div class="item" data-value="2">已审核</div>
<div class="item" data-value="3">审核不通过</div>
</div>
</ui-dropdown>
</div>

select 使用 semanticUI 的 dropdown 实现,将其 class 设置为 selection。 dropdown 中包含一个 input 标签,此标签属性为 <input name="aKey" type="hidden" v-model.lazy="aKey">,其中,name 对应 v-model 的属性名,负责将值从下面的 .item 中的 data-value 取回并传递。
此处有一个地方需要引起注意!!(敲黑板
由于此处的 input 标签其实算是 select 组件的,改动时默认行为是 change 而非 input ,也没有输入框,因此就算添加了 v-model 也无法触发。但是添加 @change 却可以正常触发,百思不得姐,经过千难万苦跋山涉水之后,终于发现!!在 Vue 的源码中有这么一断代码:

oops 图裂了.jpg

我们只关注首末行,噢.. 大意就是,判断有无 lazy 属性(或者在 IE 环境下并且类型为 range ),true 时监听 change 事件,否则监听 input 事件。真相大白啦,机智地给 v-model 加上个小尾巴 .lazy ,hot-loader 马上刷新,一试果然成功了!赞 b( ̄▽ ̄)d
P.S: 后来又翻了翻 Vue 的教程,发现尤大大其实在教程中已经说明过了,惹..附图

oops 图裂了.jpg

并且.. 1.0 版本与 2.0 版本也有点细微差别,1.0 是直接写在 input 标签的末尾,而 2.0 必须紧跟 v-model 之后。

当二者的 v-model 即 fKey 或 aKey 发生改变时,触发 computed 属性重新计算 filteredUsers ,重新渲染 users 列表,还有个 paginate 为分页方法。

1
2
3
4
5
filteredUsers () {
let fUsers = this.queryFilter('name', this.fKey, this.users)
fUsers = this.queryFilter('audit', this.aKey, fUsers)
return this.paginate(fUsers)
}

javascript 脚本

初始数据

定义了 initialUsers 数组,存进去若干个 user 对象,定义 userStorage 对象,存放 fetch 与 save 方法,分别调用 localStorage 的 getItem 与 setItem 方法向 localStorage 获取及存储数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var userStorage
function init() {
var STORAGE_KEY = 'users';
userStorage = {
fetch: function () {
return JSON.parse(localStorage.getItem(STORAGE_KEY)) || initialUsers
},
save: function (users) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(users))
}
};
}
init(window);

接着是 Vue 实例的各属性方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data () {
return {
users: userStorage.fetch(), // users 数据
selectedUsers: [], // 保存选中的 users 数组
fKey: '', // 过滤 name 的关键字
name: '', // 上一次过滤的 name 关键字,初始化为''
aKey: '', // select audit 的关键字
audit: '', // 上一次过滤的 audit关键字,初始化为''
limit: 10, // 每页显示行数
totalPage: 0, // 总页数
currentPage: 0, // 当前页
jPage: 1 // 跳转到某页
}
}

此处的 name 与 audit 看起来有些多余,其实它是有很重要的作用的,它用于保存上一次过滤的 key与 本次的对比。

过滤方法 queryFilter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
queryFilter (prop, key, arr) {
// none query string, return arr
if (!key) {
return arr
}
// filtering
arr = arr.filter((user) => {
if (user[prop].toString().indexOf(key) !== -1) {
return true
}
})
// if it's a new filter query, refilter and turn to page one
if (key !== this[prop]) {
this.currentPage = 0
// save last filter query
this[prop] = key
}
return arr
}

这个设置应用情境是:未设置这个变量时,若用户在一次搜索之后进行翻页,假设停留在了第 n 页,这时候再重新搜索,页面会停留在本次搜索的结果的第 n 页,非常不方便。因此检测如果重新搜索,则充值 currentPage 属性,记录新的 key 。

分页方法 paginate
1
2
3
4
5
6
7
8
paginate (arr) {
this.totalPage = Math.ceil(arr.length / this.limit)
let page = this.currentPage
let curLimit = this.limit
// 返回指定条数的数组
arr = arr.slice(curLimit * page, curLimit * (page + 1))
return arr
}

此处用到了数组的 slice 方法进行浅复制。

翻页方法 turnPage

接受一个数组,1 为向后翻页,-1 为向前翻页

  • HTML
1
2
3
4
5
6
<div class="jtp">
<span>跳转到第 </span>
<ui-input css="icon">
<input type="text" v-model="jPage" @keyup.enter="jumpToPage">
</ui-input><span></span>
</div>
  • Javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
turnPage (num) {
if (num === 1) {
if (this.currentPage === this.totalPage - 1) {
return
} else {
this.currentPage++
}
} else {
if (this.currentPage === 0) {
return
} else {
this.currentPage--
}
}
}
单个操作修改 audit

为单独操作的 button 添加 @click 事件,直接修改

批量操作方法

由于 selectedUsers 数组保存了被选中 users 的数据,因此只要调用 setAuditId 方法,传入 selectedUsers 以及要设置的 audit ,遍历 selectedUsers 进行设置即可。代码举例如下:

  • HTML
1
2
3
4
5
6
<ui-button css="green" v-if="aKey==='1'" @click="pass">通过审核</ui-button>
<ui-button css="red" v-if="aKey==='1'" @click="reject">不通过审核</ui-button>
<ui-button css="small" v-if="aKey==='0'" @click="approveSel">审核</ui-button>
<ui-button css="small" v-if="aKey==='0'" @click="approveAll">全部审核</ui-button>
<ui-button css="small" v-if="aKey==='2' || aKey==='3'" @click="approveSel">再次审核</ui-button>
<ui-button css="small" v-if="aKey==='2' || aKey==='3'" @click="approveAll">全部再次审核</ui-button>
  • Javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
approveSel () {
this.setAuditId(this.selectedUsers, 1)
},
approveAll () {
this.setAuditId(this.filteredUsers, 1)
},
pass () {
this.setAuditId(this.selectedUsers, 2)
},
reject () {
this.setAuditId(this.selectedUsers, 3)
},
setAuditId (users, aId) {
users.forEach((user) => {
user.audit = aId
})
}

此外,还添加了 watch 对 users 进行检测,每当其改变(例如 修改了 audit)时,将新的 users 存入 localStorage 中。

1
2
3
4
5
6
7
8
9
// watch
watch: {
users: {
handler () {
userStorage.save(this.users)
},
deep: true
}
}
CSS

这部分其实没什么好说的,基本上都用了 semanticUI 的 css ,只在最后的跳转页码输入框的地方重设了 input 的大小而已。

至此,整个组件就完成啦~~撒花★,°:.☆( ̄▽ ̄)/$:.°★
初试 Vue ,对一些 api 的使用可能有不准确的,有写得不好的地方欢迎指正。

javascript中函数的this指向以及apply/call/bind函数的联系与区别

联系

  三者的联系就在于,都可以用来改变函数中 this 指向的值,且第一个参数为要指向的 this 的值,apply的第二个参数(或 bind 与 call 的不定参数)为要传入的参数。这就不得不提及 javascript 中函数的 this 的指向了。this 的指向大概有以下几种。

1.全局作用域下或正常的函数调用的 this
此时 this 指向的是全局对象。这时有两种情况,如果是在浏览器环境下运行,则 this 指向全局的 window 对象;而如果是在 nodejs 环境下执行,命令行中指向的是 global 对象。有一点需要注意的是,严格模式 “use strict” 下的 this 为 undefined 。

1
2
3
// 浏览器
this.a = 10;
console.log(window.a); // 10
1
2
3
// nodejs
this.a = 20;
console.log(global.a); // 20
1
2
3
4
5
6
// 非严格模式
function f1(){
return this;
}
console.log(f1() === window); // 浏览器 true
console.log(f1() === global); // nodejs true
1
2
3
4
5
6
// 严格模式下
function f2() {
"use strict";
return this;
}
console.log(f2() === undefined); // true

2.当函数作为某对象的方法被调用时
this 指向的是调用该函数的对象,以下代码指的是对象 a。

1
2
3
4
5
6
7
var a = {
b: 30,
c: function () {
console.log(this.b);
}
}
a.c(); // 30

3.构造器函数调用
以构造器函数的形式声明函数,再用 new 关键字声明一个新的函数对象,此时函数中的 this 指向这个构造出来的对象。在 jslint 比较严格的要求下,这种构造函数的函数名必须是首字母大写的形式。

1
2
3
4
5
function A() {
this.b = 40;
}
var obj = new A();
console.log(obj.b); // 40

4.利用 apply/call/bind 方法强制改变 this 的指向
这时可以将要指向的对象 O 作为第一个参数传给以上任意三个函数之一,用某 F 函数调用这三个函数,这样 F 函数中的所有 this 的指向就都变成了对象 O,并且此时 O 就算没有声明 F 函数中的方法,也可以正常调用 F 函数中的方法。

1
2
3
4
5
6
7
8
9
10
11
function a(arg) {
console.log(this.b + arg);
}
var obj = {
b : 1
};
a(); // NaN
a.apply(obj, [50]); // 51
a.call(obj, 60); // 61
var c = a.bind(obj, 70);
c(); // 71

区别

其实区别从上面的第四种也可见一斑。

  1. apply 与 call: 第二个参数的形式 apply 为数组形式,函数会自动帮我们把数组展开,而 call 与 bind 为不定参数,需要传递几个就传递几个。
  2. bind 与 (apply 和 call):apply 与 call 都是在被调用的时候就执行函数体内容,而 bind 绑定之后返回绑定完成的函数,需要再显式执行一次此函数才能完成调用。