那是一九年的春天,

晨光中,我看到傻子朱逸之在爬树。

他站在树下徘徊许久,踱步,泪眼迷离。风吹了几万里,而江河澎湃,彩虹在开放。

傻子朱逸之爬上树的尖稍,他望着古城杭州,眼神中进发出迷离,也诉说着一种蓝。

你如果不注意,还以为是一尊雕塑,孤独的矗立在风里。

手是树和树有了联系,乌儿带去消息,黑夜开始生出蓝。

而古城杭州啊,划船的人此刻在哪里。

最近在做一个小应用,需要用到 vue 实现登录,以及给不同路由设置权限。在网上看了很多文章,讲的是乱七八糟。感叹国内技术类文章实在是差劲,抄来抄去。这篇文章就说说我最后是如何实现的。

前后端分离项目中,后端提供 api 接口给前端,使用 jwt 发放权限。

首先前端提供用户名和密码请求登录接口,后端验证之后返回给前端一个 token,之后前端在请求需要权限的接口时携带这个 token 就可以了。

两个问题

现在面临两个问题,

首先 vue 中不同的路由有不同的权限,比如我要访问后台 /admin, 就需要先登录才行,而有的页面不需要登录。

第二个问题是,vue 组件中使用 axios 请求后台服务时,不同的接口有不同的权限。

Vue 权限认证

先来解决第二个问题。vue 不同组件都要用到 axios,我们在全局为 axios 添加 request 和 response 的拦截器。

也就是,在发起请求之前,先检测 header 是否携带 token 信息。在接收响应之前,先查看后端返回状态码,如果说需要 token 验证就跳转到登录界面。

在 main.js 添加如下,或者新增一个 http.js 文件:

// * http request 拦截器
axios.interceptors.request.use(
    config => {
        // * 判断是否存在 token,如果存在的话,则每个 http header 都加上 token
        // * token 会在登录之后存储在本地
        if (localStorage.token) {
            config.headers["Authorization"]  = `Bearer ${localStorage.token}`;
        }
        return config;
    },
    err => {
        return Promise.reject(err);
    });

// * http response 拦截器
axios.interceptors.response.use(
    response => {
        let data = response.data;
        // * 正常返回数据
        if (data.code === 0) {
            // * 返回 data
            return data
        }
        // * 如果 code 是 20103 表示 token 未认证 (后端定义的错误码)
        // * 跳转到 login
        if (data.code === 20103) {
            router.replace('/login')
        }
        return  Promise.reject(data);
    },
    error => {
        return Promise.reject(error);
    });

Vue.prototype.$http = axios;

现在发起的任何请求之前都会检查是否携带 token,如果没有就跳到 login 界面。

在 login 中,携带用户名和密码获取 token 之后,存放到本地。

login.vue:

axios.post('/api/login', {
                    email: this.email,
                    password: this.password
                }) .then((res) => {
                        // * 存储 token
                        localStorage.setItem('token', res.data.token);
                        console.info("login successful");
                        // * 跳转回登录前页面
                        this.$router.push({path: this.$route.query.redirect || '/admin',})

                    }).catch((error) => {
                    console.error(error)
                });

现在,访问后端接口的权限问题解决了。但是在 vue 中我不同的页面有不同的访问权限该如何处理?vue-router 官方文档给出了例子:

在需要权限的路由添加 meta 信息,表明该路由需要登录才能访问,然后在所有路由跳转之前添加处理函数,如果没有 auth,跳转到登录:

path: '/admin',
name: 'admin',
component: () => import('../views/admin/Admin.vue'),
// * 需要登录才能访问
meta: {requiresAuth: true},
// * 全局钩子
router.beforeEach((to, from, next) => {
    if (to.matched.some(record => record.meta.requiresAuth)) {
        // * 对于需要 auth 的路径
        // * 没有 token 信息,redirect to login
        if (!localStorage.token) {
            next({
                path: '/login',
                query: {redirect: to.fullPath}
            })
        } else {
            next()
        }
    } else {
        next() // 确保一定要调用 next()
    }
})

登出的话,清除 token 信息即可。

localStorage.removeItem("token")

以上,希望有帮助。

那是在四月,朱家的老宅住进了一只小燕子。每天飞来飞去的筑巢,衔来树枝,衔来泥土。巢筑在胡同的拐角,遮风挡雨。这件事吸引了朱逸之。

朱逸之是朱家的老二,这宅子里只住着他一个人,无双镇只有这一个宅子。

每天清晨,朱逸之会把所有朝南的窗户打开,建一级的阶梯,把西南角的树桩搬到东北角。然后用尽整日的晴天,看一朵云从这头飘到那头。直到四月的那个早上。

有一只发着光的小燕子住了进来。

这让朱逸之变得急躁,欣喜若狂,像个疯子。小燕子是从南方飞过来的,那里应该有一条河,河的源头在哪,这点并不重要。

总之,朱家的老宅热闹了起来。叮叮哐哐,呼哧呼哧,朱逸之做了一个盒子,用了墙角的那棵树桩。他要保护小燕子。

他和它说话,搬一只小板凳,小燕子叽叽喳喳,他也叽叽喳喳。聊它的过去,聊他建的阶梯。说一辈子说不完的话。

那阶梯,他从生下来就开始建了。等到足够高,就可以摘到星星,这是他的使命。每个人都有使命,就像和小燕子的相遇,都是命中注定。他会把星星送给小燕子,这点并不重要。

重要的是,到了冬天,小燕子就会飞走。沿着通惠河,一路向南。河水是蓝色的,和朱逸之的骨头一样。那个时候,所有的河水都会失望。

而朱逸之则继续沉默。他会搬个小板凳,坐在那里,坐成一尊雕塑。通惠河会从这里断流,分为雕塑以南和雕塑以北。

这种事情总会发生,冬天总要来临。他要关上所有朝南的窗户,写下一个笔记本的文字,然后辗转反侧,彻夜难眠。

我是在那天夜里见到朱逸之的,朱家的老宅就像海里的一座孤岛,风沿着通惠河从眼前吹过。有一个人正叮叮哐哐,呼哧呼哧,修着看不尽的阶梯。

就像是一场宿命。

为了提高系统密集型运算的效率,我们常常会使用到多个进程或者是多个线程,python 中的Threading包实现了线程,multiprocessing 包则实现了多进程。而在 3.2 版本的 python 中,将进程与线程进一步封装成concurrent.futures 这个包,使用起来更加方便。我们以请求网络服务为例,来实际测试一下加入多线程之后的效果。

首先来看看不使用多线程花费的时间:

import time
import requests

NUMBERS = range(12)
URL = 'http://httpbin.org/get?a={}'

# 获取网络请求结果
def fetch(a):
    r = requests.get(URL.format(a))
    return r.json()['args']['a']

# 开始时间
start = time.time()

for num in NUMBERS:
    result = fetch(num)
    print('fetch({}) = {}'.format(num, result))
# 计算花费的时间
print('cost time: {}'.format(time.time() - start))

执行结果如下:

fetch(0) = 0
fetch(1) = 1
fetch(2) = 2
fetch(3) = 3
fetch(4) = 4
fetch(5) = 5
fetch(6) = 6
fetch(7) = 7
fetch(8) = 8
fetch(9) = 9
fetch(10) = 10
fetch(11) = 11
cost time: 6.952988862991333

再来看看加入多线程之后的效果:

import time
import requests
from concurrent.futures import ThreadPoolExecutor

NUMBERS = range(12)
URL = 'http://httpbin.org/get?a={}'

def fetch(a):
    r = requests.get(URL.format(a))
    return r.json()['args']['a']

start = time.time()
# 使用线程池(使用 5 个线程)
with ThreadPoolExecutor(max_workers=5) as executor:
  # 此处的 map 操作与原生的 map 函数功能一样
    for num, result in zip(NUMBERS, executor.map(fetch, NUMBERS)):
        print('fetch({}) = {}'.format(num, result))
print('cost time: {}'.format(time.time() - start))

执行结果如下:

fetch(0) = 0
fetch(1) = 1
fetch(2) = 2
fetch(3) = 3
fetch(4) = 4
fetch(5) = 5
fetch(6) = 6
fetch(7) = 7
fetch(8) = 8
fetch(9) = 9
fetch(10) = 10
fetch(11) = 11
cost time: 1.9467740058898926

只用了近 2 秒的时间,如果再多加几个线程时间会更短,而不加入多线程需要接近 7 秒的时间。

不是说 python 中由于全局解释锁的存在,每次只能执行一个线程吗,为什么上面使用多线程还快一些?

确实,由于 python 的解释器(只有 cpython 解释器中存在这个问题)本身不是线程安全的,所以存在着全局解释锁,也就是我们经常听到的 GIL,导致一次只能使用一个线程来执行 Python 的字节码。但是对于上面的 I/O 操作来说,一个线程在等待网络响应时,执行 I/O 操作的函数会释放 GIL,然后再运行一个线程。

所以,执行 I/O 密集型操作时,多线程是有用的,对于 CPU 密集型操作,则每次只能使用一个线程。那这样说来,想执行 CPU 密集型操作怎么办?

答案是使用多进程,使用 concurrent.futures 包中的ProcessPoolExecutor 。这个模块实现的是真正的并行计算,因为它使用 ProcessPoolExecutor 类把工作分配给多个 Python 进程处理。因此,如果需要做 CPU 密集型处理,使用这个模块能绕开 GIL,利用所有可用的 CPU 核心。

说到这里,对于 I/O 密集型,可以使用多线程或者多进程来提高效率。我们上面的并发请求数只有 5 个,但是如果同时有 1 万个并发操作,像淘宝这类的网站同时并发请求数可以达到千万级以上,服务器每次为一个请求开一个线程,还要进行上下文切换,这样的开销会很大,服务器压根承受不住。一个解决办法是采用分布式,大公司有钱有力,能买很多的服务器,小公司呢。

我们知道系统开进程的个数是有限的,线程的出现就是为了解决这个问题,于是在进程之下又分出多个线程。所以有人就提出了能不能用同一线程来同时处理若干连接,再往下分一级。于是协程就出现了。

协程在实现上试图用一组少量的线程来实现多个任务,一旦某个任务阻塞,则可能用同一线程继续运行其他任务,避免大量上下文的切换,而且,各个协程之间的切换,往往是用户通过代码来显式指定的,不需要系统参与,可以很方便的实现异步。

协程本质上是异步非阻塞技术,它是将事件回调进行了包装,让程序员看不到里面的事件循环。说到这里,什么是异步非阻塞?同步异步,阻塞,非阻塞有什么区别?

借用知乎上的一个例子,假如你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下”,然后开始查啊查,等查好了(可能是 5 秒,也可能是一天)告诉你结果(返回结果)。而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。

而阻塞与非阻塞则是你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了,当然你也要偶尔过几分钟 check 一下老板有没有返回结果。在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。

总之一句话,阻塞和非阻塞,描述的是一种状态,而同步与非同步描述的是行为方式。

回到协程上。

类似于Threading 包是对线程的实现一样,python3.4 之后加入的asyncio 包则是对协程的实现。我们用 asyncio 改写文章开头的代码,看看使用协程之后能花费多少时间。

import asyncio
import aiohttp
import time

NUMBERS = range(12)
URL = 'http://httpbin.org/get?a={}'
# 这里的代码不理解没关系
# 主要是为了证明协程的强大
async def fetch_async(a):
    async with aiohttp.request('GET', URL.format(a)) as r:
        data = await r.json()
    return data['args']['a']

start = time.time()
loop = asyncio.get_event_loop()
tasks = [fetch_async(num) for num in NUMBERS]
results = loop.run_until_complete(asyncio.gather(*tasks))

for num, results in zip(NUMBERS, results):
    print('fetch({}) = ()'.format(num, results))

print('cost time: {}'.format(time.time() - start))

执行结果:

fetch(0) = ()
fetch(1) = ()
fetch(2) = ()
fetch(3) = ()
fetch(4) = ()
fetch(5) = ()
fetch(6) = ()
fetch(7) = ()
fetch(8) = ()
fetch(9) = ()
fetch(10) = ()
fetch(11) = ()
cost time: 0.8582110404968262

不到一秒!感受到协程的威力了吧。

asyncio 的知识说实在的有点难懂,因为它是用异步的方式在编写代码。上面给出的 asyncio 示例不理解也没有关系,之后的文章会详细的介绍一些 asyncio 相关的概念。

等等 中文

想起一个事,彗星撞击系守町的那个晩上,我看到有人骑着羊在来回跑。

在工大西门地铁口,手里拿着鞭子。

驾!像骑着一匹马。

天冷的很,我喊了声”天王盖地虎”,羊就停下来看我,眼神像看一个傻子。

这件事情不该发生。

后来我坐在炕头上跟等等说起,他只是笑,骑上羊,余光看我一眼,便绝尘而去。还没有人像他这样,吃饭,骑羊,吃饭,骑羊。

我问他,你为什么骑羊。

“你不懂,这叫念想”。

我是在吃火锅的时候明白的,土豆放到锅里,这是熵增加的过程。赶羊的人都知道,羊吃草,吃着吃着就呆住了。这也是熵增加的过程。

等等有一百头羊。熵增加的更快,虚无感更少。

我也被虚无感击过。