Giter VIP home page Giter VIP logo

blog's Introduction

blog's People

Contributors

brunoyang avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

blog's Issues

中文输入法的救星 composition event

最近在工作中碰到搜索的需求,需要实时获取输入框中的内容并请求搜索结果,一开始想到的就是用input事件监听,在英文输入的情况下一切 ok,但在使用中文输入法时就会出现问题了。比如输入 a、s、d三个字符,我们想要的情况是输入时不触发 input 事件,在选择好对应的中文字符后再触发。而实际情况是每按下一次键盘后都会触发一次 input 事件然后去请求了接口,这显然不是我想要的,一番查找之后,终于找到了解决方案。

人民大救星 composition event

composition event可以观察用户在输入法中组字选词的状态。composition event提供三个事件,分别为compositionstart compositionupdate compositionend

  • compositionstart:开始通过输入法输入法输入字符时触发,当实现选中了某些字符才开始输入时,事件对象的data属性就是这些字符,否则为空字符串;
  • compositionupdate:输入字符时触发,事件对象的data属性的值为输入的字符,该事件用得比较少;
  • compositionend:结束选词时触发,事件对象的data属性的值为选中的词;

compositionend事件会在input事件之后触发,所以需要在compositionend事件中手动执行input事件的处理逻辑。

实现需求

结合 compositionstartcompositionendinput事件就可以实现我们的需求

const input = document.querySelector('input');
let inputLocked = false;

input.addEventListender('compositionstart', () => {
  inputLocked = true;
});

input.addEventListener('compositionend', () => {
  inputLocked = false;
});

input.addEventListener('input', () => {
  if (!inputLocked) {
    console.log(input.value);
  }
});

Async就要来了,来不及解释了,快上车!

据说,async 将在2016年 Q2,也就是今年的 4~6 月份于 V8 上推出,到那时,我们就可以欢快地写上『同步代码』啦!所以,本文的目的就是教大家如何使用 async。


配置 webpack + babel 使用 async

如果已配置过,请跳过本节

首先,安装babel-corebabel-loader,这两项分别是 babel 的核心和 babel 能够在 webpack 中运行的保障。

npm install babel-core babel-loader --save-dev

接着,安装babel-preset-stage-3插件,就可以直接使用 async 了。

npm install babel-preset-stage-3 --save-dev

若你希望你的代码可以在浏览器,或是 node v4 以下的环境上运行,就需要加上babel-preset-es2015,因为 babel 转码 async 的原理是将其转为 generator,低版本浏览器若要使用 generator,还需要再转成 es5 的代码。其外,若要在 react 中使用,需另加babel-preset-react

附一份 webpack.config.js 的 loader部分

module: {
  loaders: [{
    test: /\.jsx?$/,
    include: [
      path.resolve(__dirname, 'src')
    ],
    loader: 'babel-loader',
    query: {
      presets: ['es2015', 'stage-3', 'react'],
    }
  }]
}

正文

我们先来写一个最简单的 async 函数

function getDataFromGithub() {
  return fetch('https://api.github.com/whatever')
    .then((data) => {
      return data.status;
    });
}

async function githubPage() {
  const data = await getDataFromGithub();
  console.log(data); // 200
  return data;
}

async function wrap() {
  const data = await githubPage();
  console.log(data); // 200
}

wrap();

done!

只需要在 function 前面加上 async 关键字,以及在被调用的函数前面加上 await 关键字,就是这么简单。
来讲讲需要注意的几点

1.首先,请谨记,async的基础是 promise,我们可以试着改写githubPage这个函数

function githubPage() {
  getDataFromGithub()
    .then((v) => {
      console.log(v);
    });
}

对比两个版本的githubPage函数,就能发现,await 相当于是调用了 then 方法,并拿到返回值(当然这只是打个比方,实际上 then 函数是用来注册回调的)。


2.await 关键字不允许出现在普通函数中。map((x) => await x * x),类似这样的代码是会报错的。map(async (x) => await x * x),这样的代码不会报错,但是不符合我们的预期,其中的 await 不是顺序执行而是并行地执行,至于原因,请看这里。没有办法,目前看来,只能在 for 循环中使用

async function foo() {
  const arr = [bar, baz];
  for (const func of arr) {
    await func();
  }
}

async 函数的返回值是一个 promise,也就是说,我们可以写这样的代码:

async function foo() {
  return await getJSON();
}

foo.then((json) => {
  console.log(json);
});

若 await 后的函数 reject 了,或是抛出了一个错,上面的代码是没有办法处理的。当然应对方法也很简单,就是把业务代码都包裹在 try/catch 中,在 catch 中对错误进行统一处理。

async function wrap() {
  try {
    const data = await githubPage();
    console.log(data); // 200
  } catch (e) {
    // ...handle error
  }
}

上面的代码都是顺序执行,那怎么让两个 await 同时执行呢

const [foo, bar] = await* [getFoo(), getBar];

很简单吧,不过,上面的写法已经被废弃了,得用下面这个写法:

const [foo, bar] = await Promise.all([getFoo(), getBar()]);

虽然也很好理解,但不得不吐槽这样写实在是太丑了。
当然,有 Promise.all 自然也可以用 Promise.race

const foo = await Promise.race([getFoo(), getBar()]);

async 也可以在 class 中使用:

class Foo {
  constructor() {
    this.index = 0;
  }

  async test(v) {
    console.log('class A');
    return await Promise.resolve(this.index++);
  }
}

class Bar {
  async test(foo) {
    console.log('class B');
    return await foo.test();
  }
}

const foo = new Foo();
const bar = new Bar();

foo.test().then(v => console.log(v));
bar.test(foo).then(v => console.log(v));
// class A
// class B
// class A
// ----- next tick -----
// 0
// 1

若是像上面这样直接调用,并不会得到预期的结果,因为这相当于是不加 await 调用 async 函数。我们需要将函数调用包装一下~~(快去抢注 co-async)~~:

(async function wrapper() {
  await foo.test().then(v => console.log(v));
  // class A
  // 0
  await bar.test(foo).then(v => console.log(v));
  // class B
  // class A
  // 1
})()

JS 与迭代器

JavaScript 的历史上,先有两种集合,ArrayObject,ES6 中新加了两种集合,MapSet。遍历 Array 可以用 for 循环或 forEach 等 ES5 中提供的遍历方法,Object 使用 for...inMapSet 使用 for...of。可以预见,如果不添加一个统一的遍历接口,数据结构改变就需要不同的遍历方法,势必会越来越混乱。所以,ES6 中添加了迭代器(Iterator)这一机制,为不同的数据结构提供统一的访问接口。

什么是迭代器

所谓迭代器,就是一种封装,封装了获取集合中某个值的操作,而屏蔽了『如何拿到某个值』的细节。举个例子,如果想获取对象和数组的第一个值,数组的取值方法是 arr[0], 对象的取值方法是 for (const v in obj) obj[v]。为了屏蔽差异,我们需要设计一种通用的方法。一种做法就是往这些集合上增加个方法,该方法可以拿到集合中的值,取值时只需要调用这个方法就可以了。

使用迭代器

ES6 中数组部署了迭代器,我们来看看如何使用迭代器遍历数组:

const arr = [1,2,3]
const iterator = arr[Symbol.iterator]()
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: undefined, done: true }
iterator.next() // { value: undefined, done: true }

上面短短几行中,有几点是需要注意的:

  • Symbol 是 ES6 中引入的新全局变量,想详细了解的戳这里
  • Symbol.iterator 是 Symbol 的一个属性,每个可遍历的集合上都有这个键;
  • arr[Symbol.iterator] 返回的是一个函数,arrSymbol.iterator 返回一个对象,对象中包含 next 方法,用来遍历集合;
  • next() 方法从头遍历集合,返回的不是值,而是一个有 value 和 done 字段的对象,done 表示遍历是否结束,结束后无论调用多少次 next,返回的永远是 { value: undefined, done: true }

迭代器与 for...of

一遍一遍地写 .next() 相当低效,所以 ES6 引入了 for...of 帮我们自动遍历。for...of 会自动调用迭代器接口,类似这样:

const arr = [1,2,3]
for (const v of arr) {
  console.log(v) // 1, 2, 3
}

自己实现个迭代器

四大集合,ArraySetMap 都有迭代器接口,独独少了 Object,据说是为了以后考虑。不过虽说官方没有默认提供,我们可以手动添加,自己实现个迭代器:

const foo = {
  a: 1,
  b: 2,
  c: 3,
}

Object.defineProperty(foo, Symbol.iterator, {
  configurable: false,
  enumerable: false,
  writable: false,
  get: () => {
    let index = -1
    const values = Object.entries(foo)
    const len = values.length
    return () => ({
      next: () => {
        index++
        return {
          value: values[index] ? values[index][1] : undefined,
          done: index >= len ? true : false,
        }
      }
    })
  }
})

for (const v of foo) {
  console.log(v) // 1, 2, 3
}

done, for...of 识别出了我们自定义的迭代器!

迭代器与 generator

{ value: xx, done: false } 这样的对象感觉很眼熟... 我们来对比一下数组和 generator function:

const arr = [1,2,3]
const iterator = arr[Symbol.iterator]()
iterator.next() // { value: 1, done: false }

// ----------------------

function * iterFunc () {
  let i = 1
  while (true) {
    yield i++
  }
}
const gen = iterFunc()
gen.next() // { value: 1, done: false }

简直一毛一样。

而且,generator 函数还可以用来部署迭代器

function* makeIterator(obj){
  var nextIndex = 0;
  var values = Object.entries(obj)

  while(nextIndex < values.length){
    yield values[nextIndex++][1];
  }
}

var gen = makeIterator({
  a: 1, 
  b: 2,
});

for (const v of gen) {
  console.log(v); // 1, 2
}

迭代器的其他用途

其实 ES6 中迭代器无处不在,随便列举几个

rest 参数:

function rest (...args) {
  for (const v of args) {
    console.log(args)
  }
}

解构赋值:

const [a, b] = 'a.b'.split('.')

const [c, d] = 'a.b.c'.split('.')
// c: 'a', d: ['b', 'c']

还可以对类数组比如字符串等操作:

[...'abc'] // 'a', 'b', 'c'

Koa 在 Macaca 中的实践

macaca-clireliable-master 中,都使用了 koa。koa 是一款优秀的,面向未来的 web 框架,若你还在使用 express,一定要试试 koa。

[email protected]

要想了解 koa 的运行原理,最好看看它的源码,只有4个文件,每个文件也不长,但却可以支撑起一个 web 应用,真正的麻雀虽小,五脏俱全

koa 同 express 一样,都继承自 events。暴露出的 Application 上有7个属性,envsubdomainOffsetmiddlewareproxycontextrequestresponse。我们分别来看这几个属性。

1. env

为了区分生产环境和开发环境,一般会在环境变量里加上export NODE_ENV=productionexport NODE_ENV=development,这样在调用process.env.NODE_ENV时就能拿到当前环境了。这是一个约定俗成的『关键字』。或者,在命令行输入NODE_ENV=production node index.js也有上面的效果,只不过这是一次性,局部的。

2. subdomainOffset

这个参数是为了拿到子域名所设置。如域名为china.asia.news.bbc.com,subdomainOffset 为2时,调用 request 上的 subdomains,返回的是['news', 'asia', 'china'],因为 com 是顶级域名,bbc 是二级域名,越往前越低。subdomainOffset 为3时,返回的是['asia', 'china']。个人感觉没啥用,只是为了和 express 统一……

3. middleware

middleware 数组中存放着多个 generator 函数,middleware 往 koa-compose 模块中传。compose 模块是个典型的 one-function module,作用是顺序执行数组中的函数。

4. proxy

供 request 上的 protocol、host、ips 方法调用。当本应用不是直面用户,而是经过了代理服务器的转发(大部分情况下都是这样的),协议,host,ip 就不能从进入应用的请求中直接获取,只能间接地从一些自定义请求头里取。

5. context

在 context 上挂载了 request 和 response 的方法和少量 context 本身的方法。

6. request & 7. response

request 和 response 上的方法基本上是直接操作http请求本身,比较底层。


我们通过向 koa 发起一条请求来看 koa 是如何工作的,建议对着源码一起读。

先写一个玩具式最简单的 koa 应用。

const app = require('koa')();

app.use(function *(next) {
  console.log(1);
  yield next;
  console.log(4);
});

app.use(function *(next) {
  console.log(2);
  yield next;
  console.log(3);
});

app.use(function *() {
  this.body = 'Hello World';
});

app.listen(3000);

浏览器访问 http://localhost:3000, 就能看到 Hello World,在终端会看到1 2 3 4。

调用 use 方法,将 generator 函数 push 入 middleware 数组。随后,调用 listen 方法启动 http 服务器。

var server = http.createServer(this.callback());

callback 只要返回一个function(req, res) {}函数即可,这就是高阶函数的应用。深入研究 callback 函数,可以看到里面有这么一句:

if (!this.listeners('error').length) this.on('error', this.onerror);

继承自 events 的好处,便于捕捉错误。

return function(req, res){
  res.statusCode = 404;
  var ctx = self.createContext(req, res);
  onFinished(res, ctx.onerror);
  fn.call(ctx).then(function() {
    respond.call(ctx);
  }).catch(ctx.onerror);
}

如果你对一个请求什么都不做,就会拿到一个404的结果。createContext 函数很简单,把 context,request, response,req(原生请求对象),req(原生响应对象)互相挂载(乱)。在执行完中间件后,再执行 respond 函数返回响应。这就是一次完整的请求和响应。但这当中有很大的一块我们没细讲:koa 的中间件。


koa 中的中间件

中间件类似于 java 中的切面编程,下图能够很好地解释 koa 控制流:

一个请求进来,进入第一个中间件,执行完 yield next 上面的内容后,执行到 yield next ,就会执行第二个中间件,重复上述步骤直至最后一个中间件。最后一个中间件中没有 yield next,就会开始执行倒数第二个 yield next 后的内容,然后再回溯,直到第一个中间件。

我的天哪,这么神奇 (ฅ◑ω◑ฅ)

这么神奇的效果是通过 co + koa-compose 共同完成的,我们先来看 koa-compose(有改动):

function compose(middleware) {
  return function *(next) {
    if (!next) next = (function *() {})();

    var i = middleware.length;

    while (i--) {
      next = middleware[i].call(this, next);
    }

    return yield *next;
  }
}

这是 koa 控制流的核心,代码很短,也很精辟。
0. 逆序 while,从最后一个中间件开始,next 就是空函数 noop;

  1. (function *() {})()的结果不是开始执行 generator,而是返回一个对象,称为 next 对象,包含 next、throw 方法。将这个对象传入上一个中间件,上一个中间件执行到 yield next 时,koa-compose 外包着的 co 将会自动执行 next 对象上的 next 方法,哈哈是不是有点绕;
  2. while 执行完后,next 指向第一个中间件;
  3. 执行到return yield *next;,koa-compose 把yield *next交给 co,co 就会开始启动中间件,完成链式调用;
  4. 何时停止呢,当 co 执行 next 返回的 done 为 true 时,是时候结束了。

扩展阅读:我的blog

macaca-cli 中的 koa

webdriver-server 模块是 macaca-cli 中重要的一部分,起到承上启下,类似于代理服务器的作用。如果你还尚不清楚 macaca-cli 是怎么工作的,我们先通过一个最简单的例子了解一下。

首先我们先来写一个测试用例(取自编写移动端 Macaca 测试用例 [单步调试]):

const wd = require('macaca-wd');
const driver = wd.promiseChainRemote({
  host: 'localhost',
  port: 3456
});

driver.init({
  platformName: 'ios',
  platformVersion: '9.3',
  deviceName: 'iPhone 6s',
  app: '/Users/XXX/Code/macaca/macaca-ios-test-sample/app/ios-app-bootstrap.zip'
});

driver
  .waitForElementByXPath('//UIATextField[1]')
  .sendKeys('loginName')
  .waitForElementByXPath('//UIASecureTextField[1]')
  .sendKeys('123456')
  .sleep(1000)
  .sendKeys('\n')
  .waitForElementByName('Login')
  .click();

然后,在终端输入macaca run --verbose,启动监听3456端口的 koa,接收从 macaca-wd 模块发来的 http 请求,转发至 driver 层(如macaca-iosmacaca-android)上,由 driver 层将 http 请求『翻译』成具体的操作并取得结果后返回给 macaca-wd。

旁的不看,单看.click()这一句。当 macaca-wd 执行到这句时,会发出一条请求(对应列表请看 macaca-wd 的 api 文档http://localhost:3456/session/:sessionId/element/:id/click。在 webdriver-server 中我们写了一长串的协议路由,当 koa 接收到请求后,就会执行对应路由上的 controller,驱动 driver 去试图点击某个元素,等待从 driver 层返回的结果(点击成功/没有该元素……等等),随后将结果包装成一个对象,形如:

{
  sessionId, // session,从请求中取得
  status, // wd 定义的状态码,0为正常,其他数字均为报错
  value // 返回的结果
}

返回响应给 macaca-wd,由 macaca-wd 判断响应的结果。这就是一句.click()的完整历程,而将各种命令串联起来,便可组成一个完整的测试用例。

所以,macaca-wd 和 macaca-cli 是完全解耦的,只通过 http 进行沟通。只要你实现了符合 wd 标准的行为,就可以任意调戏 macaca了。

Reliable-master 中的 koa

koa 是一款优秀的 web 框架,优秀在哪儿呢,我们可以把 koa 和它的前辈 express 做一下对比。它们之间最重要的区别在于中间件的调用,这也直接导致了两个框架的风格大相径庭。在 express 中很容易一不小心就写出包含大量回调的代码,不好看不说,调试也费劲,错误捕捉也是『node式』的。[email protected] 中依托于 co 和 yield,每个异步操作都可以封装在 generator 函数中,从视觉上避免了回调的杂乱,同时,可以方便地捕获异常。

koa 也是一个非常轻量级的框架,轻量到连路由,静态文件,session 管理都没有,非常底层,提供的 api 基本是操作 http 本身。所以,想要运行起一个 koa 应用,必须要依赖其他模块, koa-routerkoa-generic-sessionkoa-static 等。

reliable-master 中,koa 的作用就是 web 框架,为用户提供访问入口和可视化的管理平台。

用户权限校验

一般来说,网站会有不同的用户等级,每个等级拥有不同的权限,如超级管理员、管理员、普通用户等。而区别各个用户权限最简单的做法,就是在用户信息里加上某个特定字段,标识该用户的等级。如普通用户的 role 为1,管理员为10,超级管理员为100,这都是可以随意定的。

reliable-master 的用户权限校验中间件中,有三级校验,游客,注册用户和管理员,分别对应不同的操作。如游客因为没有 session 信息,会被引导至登录页;而管理员可以访问一些普通用户不能访问的页面。这都是在 router 里结合 koa-router 做的,将校验中间件放在路由对应的 controller 之前,就可以方便地进行校验。

i18n

i18n(internationalization),国际化,因 i 和 n 之间有18个字母而得名。一个面向国际的网站,至少要同时给出中文版和英文版,方便弘扬民族文化精神(雾)。要实现国际化,思路很简单:
0. 在服务器上放一个配置文件,内有默认的语言信息(默认中文),并在 footer 或 header 上加个选择语言的按钮,再准备两个版本的网页;
0. 增加一个 i18n 中间件,依此按照查询串、cookie 和网站配置返回某个语言版本的网页;
0. 用户第一次访问时,往响应的 cookie 带上中文语言版本信息;
0. 用户是英语用户,手动选择英语版本时,会向网站发起请求,查询串里带lang=en
0. i18n 中间件得到查询串信息,给 cookie 打上lang=en,这样用户下一次的访问就是英语版本的网页了。

但这样的解决方案灵活性肯定是不够的,要是想增加俄语版本,就要多弄一套俄语的网页,浪费生产力。所以,网页里的文字应该是动态获取的,可以根据语言版本信息自动替换文字。原先<p>登录</p><p>Login</p>统一成<p>{this.gettext('login')}</p>(React 实现,其他同理)。再在资源文件里放多个语言文件{login: '登录'}{login: 'login'}。若是想增加一个语言版本,增加一个语言文件即可{login: 'войти'}。reliable-master 中的实现可以看这里

持续集成服务

自动化测试服务与持续集成,可谓天生一对,持续集成可以提前发现问题,减少开发成本。 我们来看看 reliable-master 是如何做持续集成的

我们先准备一个 task 表,里面存放测试完成和还未开始测试的任务。还要再准备一个定时任务,每隔几秒扫描 task 表,观察是否有新增任务。若有新增任务,就把任务信息发送给空闲的机器,在该台机器上进行自动化测试。我们为 push 至 repo 的操作增加一个钩子,钩子函数的作用就是将提交代码的仓库和分支放入 task 表(假设跑任务只需要这两个信息)。当代码提交至 repo 时,调用钩子函数,自动触发自动化测试任务。

这当中所涉及到的模块解读,鉴于篇幅原因不展开,可以看这里⬇️

在不久的两三个月后,v8 将会有 async/await,届时 koa 也会推出 [email protected][email protected] 基于 async/await,提供更好的异步编程体验。现在借助 babel 也可以运行,但不推荐运行在生产环境中。

总结

除了 Koa,在 Macaca 中,还有很多有意思的技术细节。本文抛砖引玉,感兴趣的同学可以向我们提交 pr,或者点点 Star 哦~

if-modified-since vs if-none-match

if-modified-since 和 if-none-match是两个用来控制缓存的http头。

last-modified/if-modified-since: 这两个字段是一对,秤不离砣。浏览器第一次访问资源时,浏览器会将资源的最后修改时间(last-modified)和资源一起给浏览器。浏览器将这两个的东西都缓存下来后,第二次请求该资源时,会带上if-modified-since字段,字段值是上次缓存下来的last-modified值,服务器对比一下要返回的资源的最后修改时间,若发现是相同的,就会返回304。

if-none-match/etag:这是另外一组用来控制缓存的头。浏览器第一次访问资源时,服务器给该条响应加上etag字段,至于etag的值是什么是随意的,最好是保证唯一性,防止该资源与其他的资源etag重复。一般来说,可以是MD5值,版本号等等。浏览器接收到该资源后,会将etag和资源一起缓存下来。在第二次需要获取这个资源时,会向服务器发起一次请求,请求头里面会有if-none-match字段,字段值就是上次请求到的etag。服务器将if-none-match和本次响应里的etag相比较,若相等,则说明无需修改,直接返回304。

那什么时候使用if-modified-since,什么时候使用if-none-match呢,分情况。

  • 对实时性要求不高的静态资源可以使用if-modified-since;
  • 资源是动态生成,如通过查数据库拼出来的html(数据库自带最后修改时间字段可以用if-modified-since)可以使用etag,因为这时候该文件的last-modified值永远是文件生成时间,缓存就无从谈起了。

koa源码分析(一) - application.js

本文分为四个部分,分别对应源码的四个文件。

仓库:https://github.com/koajs/koa

官网:http://koajs.com/

官方介绍:Koa is a new web framework designed by the team behind Express, which aims to be a smaller, more expressive, and more robust foundation for web applications and APIs. Through leveraging generators Koa allows you to ditch callbacks and greatly increase error-handling. Koa does not bundle any middleware within core, and provides an elegant suite of methods that make writing servers fast and enjoyable.

本文根据[email protected]撰写。

依赖

  • debug:debug源码分析
  • compose_es7:与co.wrap相似,不过能够接受 async/await 函数;
  • onFinished:捕捉finish或error事件,并根据第二个参数执行回调;
  • compose:compose_es7的简化版,只接受generator;
  • isJSON:顾名思义,判断传入参数是否为JSON;
  • statuses:将http码分为三类,redirect、empty以及retry,然后根据传入的http来判断属于哪一类;
  • accepts:根据req的内容判断(content negotiation)字段,如accept,Accept-Encoding,Accept-Language等字段对这些字段进行格式化,方便后续处理;
  • only:只返回在传入参数里所包含的字段;
  • co:co源码解析

源码解析

Application

koi暴露了Application对象作为入口,Application的几个属性挑重要的讲:middlewarecontextrequestresponse会在后续文章里讲到。

middleware

middleware是一个数组,在这里特称为中间件数组,数组元素要求是generator函数。当然开启了experimental就可以用async/await了。

middleware数组是如何工作的可以参考下图

middleware 数组

context

http请求和相应进行包装,得到包含requestresponse的对象,对象所具体包含的内容在下面会有写到。

Application.prototype

Applicationn的prototype继承自event.Emitter的prototype,并挂载了多个方法。

listen

这是http.createServer(app.callback()).listen(...)的简写,重要的是当中的callback函数。

callback函数的第一行就是将response函数作为了middleware数组的第一个元素,所以我们再来看一下response函数。

response函数的第一句,就是个yield *next,也就是说在req进入中间件数组时response函数不作处理。那什么时候处理呢?看一下上面这张图,可以看到,response函数在之后的所有中间件都执行完成后,对res做最后一步的处理。

middleware数组放入compose后,会得到一个generator函数,这个generator函数的效果相当于是将多个中间件组合成一个,来让co可以按顺序执行。最后,传入ctx,完成一次http请求。

createContext

ctx从哪来的呢,是由createContext函数创建的。返回的结果形如下面的代码。requestresponsekoa生成的对象,resreqnode原生的http对象,当需要访问原生对象时,可以通过this.res来访问到,但并不推荐这样做,推荐直接访问request对象。

{
    request: {
        url: '/',
        method: 'GET',
        req: <origin node >
        ...
    },
    response: {

    }
    app: {
        subdomainOffset: 2,
        env: 'development'
    },
    originalUrl: '/',
    req: '<original node req>',
    res: '<original node res>',
    socket: '<original node socket>'
}

inspect/toJSON

返回配置的参数subdomainOffsetenv

Context,React中隐藏的秘密!

好吧我承认我是标题党,新人为了导流量蛮拼的。今天我想给大家介绍一下React中的context,本文基于[email protected]

重要声明:context 相关内容在将来仍有可能会发生变动,所以不建议使用在生产环境中。

什么是context

context是一个将被收录进React1.0的特性,但目前还没有正式地出现在官方文档中。

那什么是context呢,context就是一组属性的集合,并被_隐式_地传递给后代组件。

大家可能会有疑惑,我们不是已经有了props了吗,为什么还需要用context来传递属性呢。可以考虑这么一个情景,有一个层级很深的组件,最里层组件的行为将影响最外层的表现,一般来说我们就会在最外层组件上绑定回调,再一级一级地传入至最内层组件上,通过触发回调来进行上述行为(这里不考虑用flux)。再考虑另外一个情景,在服务端有一个组件需要根据session来渲染,当内部组件需要获取session信息时,就需要从最上层节点一层一层地往下传。但这样的实现不得不说实在有些丑陋,有没有更好的实现方式呢?有,就是使用context

实例

import ReactDOM from 'react-dom';
import React from 'react'

// Children component
class Children extends React.Component {
  constructor(props, context) {
    super(props, context);

    this.state = {
      name: this.context.name
    };
  }

  render() {
    return(
      <ul>
        <li>
          {`child context is: ${this.context.age}`} // child context is: 18
        </li>
        <li>
          {`state from context: ${this.state.name}`} // state from context: mars
        </li>
        <li>
          {`print age: ${this.context.print(this.context.age)}`} // print age: 18
        </li>
      </ul>
    );
  }
}

Children.contextTypes = {
  name: React.PropTypes.string,
  age: React.PropTypes.number,
  print: React.PropTypes.func
};


// Parent component
class Parent extends React.Component {
  getChildContext() {
    return {
      name: 'mars',
      age: 18
    };
  }

  render() {
    return (
      <div>
        {`from App component: ${this.context.name}`} // from App component: bruno
        <div>
          {this.props.children}
        </div>
      </div>
    );
  }
}

Parent.contextTypes = {
  name: React.PropTypes.string
};
Parent.childContextTypes = {
  age: React.PropTypes.number,
  name: React.PropTypes.string
};

// App component
class App extends React.Component {
  getChildContext() {
    return { 
        name: 'mars',
        print: (m) => m
     };
  }

  render() {
    return (
      <Parent>
        <Children />
      </Parent>
    );
  }
}

App.childContextTypes = {
  name: React.PropTypes.string,
  print: React.PropTypes.func
};

ReactDOM.render(<App />, document.getElementById('app'));

在上面的例子中,我们可以看到在App组价中声明的print方法并没有通过Parent传递,而是通过context直接传递给了Children,这大大方便了我们传值的操作,不再需要一遍一遍地写print={this.props.print}。但同时,我们也需要注意,不要将所有东西都绑定在context上,而是只在必要时使用context,毕竟全局变量很危险。

使用方法

使用getChildContext方法将属性传递给子组件,并使用childContextTypes声明传递数据类型,子组件中需要显式地使用contextTypes声明需要用到的属性的数据类型。

需要传递进context参数才可以在constructor方法中使用context,要不然React将会报错。

在组件中,通过this.context访问context中的属性或方法。

相关api

contextTypes

当需要在当前组件使用从上级组件传入的context的属性时,需要为用到的属性声明数据类型

childContextTypes

声明传递给子组件的属性的数据类型。

getChildContext

设置传递给子组件的属性,可以覆盖,也可以新增。

使用 IntersectionObserver 和 registerElement 打造 Lazyload

Lazyload 是应用非常广泛的网页效果,可以有效加快用户访问速度并减少流量消耗,实现思路就是判断图片元素是否可见来决定是否加载图片。所以把大象装进冰箱只用两步:

  1. 判断是否可见
  2. 可见时加载图片

老浏览器要是想获取一个元素位于 viewport 中的位置,那可是相当费劲,滚动过的距离,窗口大小,元素大小,元素位置等等,监听滚动事件,然后把几个数字加加减减,还要处理兼容性问题,反正我写过一遍之后就再也不想写了。

现代一些的方法是使用getBoundingClientRect

const img = document.querySelector('img');
const rect = img.getBoundingClientRect();

// 第一步
if (
  rect.top >= 0 &&
  rect.left >= 0 &&
  rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && 
  rect.right <= (window.innerWidth || document.documentElement.clientWidth)
) {
  // 第二步
  img.src = 'https://t.alipayobjects.com/images/T11rdgXbFkXXXXXXXX.png';
}

获取该 img 相对于浏览器 viewport 的位置,若用户可见,就更改该图片元素的 src 属性,让它自动开始加载图片。

看着貌似不错,但有些问题:调用getBoundingClientRect方法会触发页面重排,使页面响应变慢,并且上面这一大串判断看着都累,更不用说写了,此外,通过getBoundingClientRect获取 iframe 里的元素,它是将 iframe 当做 viewport,而不是父窗口,这显然不是我们想要的。有没有更好的,更直观的方法呢?从 Chrome 51 开始,IntersectionObserver拯救你。

IntersectionObserver

第一个参数,callback

直接上代码:

// 1. 获取 img
const img = document.querySelector('img');
// 2. 实例化 IntersectionObserver,添加 img 出现在 viewport 瞬间的回调
const observer =  new IntersectionObserver((changes) => { 
  console.log('我出现了!') 
});
// 3. 开始监听 img
observer.observe(img);

done! 就是这么简单,几行代码就可以监听某个元素是否出现在 viewport 内,这大大减少了代码量和思考成本。

回调里的change参数,接收一个数组,数组元素是 IntersectionObserverEntry 对象。IntersectionObserverEntry 对象上有5个属性,

IntersectionObserverEntry: {
  boundingClientRect, // 对 observe 的元素执行 getBoundingClientRect 的结果
  rootBounds, 
  // 对根视图(默认是viewport,可以指定根元素,后面会讲到)
  // 执行 getBoundingClientRect 的结果
  intersectionRect,
  target: // observe 的对象,如上述代码就是 img
  time: 过了多久才出现在 viewport 
}

intersectionRect 是个很有意思的属性,我们单拎出来说。intersection 是交汇的意思,intersectionRect 就是 observe 元素和 viewport 相交汇的那个矩形,这个矩形会随着元素出现在 viewport 内的大小而改变。

盗用谷歌一张图

完全进入 viewport 的元素 intersectionRect.height 就是元素的高度,部分进入的元素 intersectionRect.height 就是留在 viewport 内的高度。

默认情况下,查看这个属性,会发现 top 是1,bottom 是1,也就是当某个被监听元素随着页面滚动刚冒出来 1px 时,浏览器就会调用 IntersectionObserver 的回调,表示该对象已经出现在 viewport 内了。

在该元素离开页面时,浏览器也会调用一次 IntersectionObserver 的回调,这次 intersectionRect 的所有属性都变成0了。所以我们可以这么干:

const observer =  new IntersectionObserver((changes) => { 
  for (const change of changes) {
    const intersectionRect = change.intersectionRect;
    if (intersectionRect.height * intersectionRect.width > 0) {
      console.log('本宝宝来了!');
    } else {
      console.log('本宝宝又走了!');
    }
  } 
});

我们也可以一次性监听多个元素:

const observer =  new IntersectionObserver((changes) => { 
  console.log(changes.length); // 2
});
observer.observe(img);
observer.observe(div);

有 observe 自然也有unobserve。调用observer.unobserve(img);就可以停止监听。

第二个参数,option

在默认的情况下,回调只会在元素出现于 viewport 内和消失在 viewport 时被触发,但有时候我们会想要在元素进入一半时触发回调(如图片进入一半时才进行加载),这时候该怎么办呢?

其实 IntersectionObserver 除了回调,还可以传入第二个参数 option 对象,option 对象有个很有意思的属性:threshold,也就是所谓的阈值。threshold 是一个范围为0到1数组,默认值是[0],也就是在元素可见高度变为0时就会触发。如果赋值为 [0, 0.5, 1],那回调就会在元素可见高度是0%,50%,100%时,各触发一次回调。

你要是想提前预加载呢,threshold 属性就做不到了。不要方,还有另外一个属性可以使用:rootMargin。rootMargin 是一个字符串,和 css 的 margin 写法一样,如rootMargin: '1px 2px 3px 3px'rootMargin: '5px'。表现出的效果就是给被监听的元素加上 margin,如rootMargin: '20px'时,回调会在元素出现前 20px 提前调用,消失后延迟 20px 调用回调。也可以设置成负值,rootMargin: '-20px',效果就是元素出现后/消失前 20px 调用回调。

还有另外一个配置项是 root,默认为 null,取 rootBounds 时会按照 viewport 的尺寸来计算。也可以自行定义为某个 dom。

const observer =  new IntersectionObserver((changes) => { 
  console.log(changes.length); 
}, {
  root: null, 
  rootMargin: '20px', 
  threshold: [0, 0.25, 0.5, 0.75, 1]
});

在 iframe 中大显神威

在上面说到,getBoundingClientRect方法不能拿到位于 iframe 中元素相对于父窗口的位置。但 IntersectionObserver 可以。一个很常见的例子,个人博客下面的评论框,很多时候是一个嵌在 iframe 中的第三方评论组件。评论组件若是使用了 IntersectionObserver,就能很好地检测自己在父窗口中的位置,然后做相应的动作。

polyfill

虽然 IntersectionObserver 目前只有 Chrome 51+ 上有,但马上就可以通过 polyfill 实现了,但性能上不如原生(因为是通过 getBoundingClientRect 实现)。

registerElement

registerElement 是一个在 Chrome 27 上出现的技术,用于自定义元素。使用 registerElement 注册的元素拥有生命周期,还可以继承并增强现有元素,十分强大,polymer 框架就是建立在此 api 之上。但因为兼容性原因,没有推广开来。当然该 api 也有polyfill。在 html5rocks 上有篇非常棒的文章,感兴趣的同学可以前去观看。

实现个 Lazyload

setp 1:注册 Lazyload 组件

首先,我们先来利用 registerElement 实现个自定义 img:

const FALLBACK_IMAGE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAEElEQVR42gEFAPr/AP///wAI/AL+Sr4t6gAAAABJRU5ErkJggg==';

class LazyloadImage extends HTMLImageElement {
  createdCallback() {}

  attachedCallback() {}

  detachedCallback() {}
}

然后,注册 lazyload-image:

document.registerElement('lazyload-image', {
  prototype: LazyloadImage.prototype,
  extends: 'img'
})

step 2:实现 Lazyload 组件

接着,实现各个生命周期:

createdCallback() {
  this.original = this.currentSrc || this.src;
  this.src = FALLBACK_IMAGE;
  this.observer = new IntersectionObserver(this.visibleChanged.bind(this));
}

attachedCallback() {
  this.observer.observe(this);
}

detachedCallback() {
  this.observer.unobserve(this);
}

和最重要的 visibleChanged 方法:

visibleChanged(changes) {
  for (const change of changes) {
    const intersectionRect = change.intersectionRect;
    if (intersectionRect.height * intersectionRect.width > 0) {
      this.addEventListener('load', () => {
        this.observer.unobserve(this);
      });

      this.addEventListener('error', () => {
        this.removeAttribute('srcset');
        this.src = FALLBACK_IMAGE;
        this.observer.unobserve(this);
      });

      this.src = this.original;
    }
  }
}

step 3:添加至页面

在 html 中插入<img is="lazyload-image" src="https://t.alipayobjects.com/images/T11rdgXbFkXXXXXXXX.png" /> ,is 属性表示该元素是自定义的加强版 img,同时,在不支持 registerElement 的浏览器上也能自然回退成普通的 img。

更为具体的实现可以围观 lazyload-image

倒霉蛋李建国

李建国是个倒霉蛋,小时候爬树摔断腿,考试抄错题;长大了骑车被碰瓷,吃饭吃出钢丝球。

一天,李建国打开了某网银要转1000块给王红霞,转账当然是要登录的。转完账后,李建国关闭了 tab 页。随后,李建国打开了不可描述的网站,半分钟后关闭了这个网站。突然,李建国的手机收到银行发来的两条短信,一条是转给王红霞的1000,另一条是不知道转给谁的10000。李建国慌了,他知道网银被盗了。他很疑惑,我啥也没干,咋就被盗了捏,难道见鬼了。

当然,李建国没见鬼,他只是碰上了 CSRF 漏洞。

所谓 CSRF(Cross-site request forgery),跨站攻击,指的是攻击者盗用你的身份,向某个网站发起恶意请求。

我们看李建国的例子,被盗分这么几步:

  1. 浏览并登录网银,在网银的域名下留下 cookie 信息;
  2. 发起转账,假设银行转账接口是 GET 请求的http://bank.com/transfer/1000/to/wang-hong-xia
  3. 浏览恶意网站,恶意网站上有张图<img src="http://bank.com/transfer/10000/to/huai-yin" />;
  4. 该请求带上还未过期的 cookie 信息,将10000转给了坏银。

这个漏洞能够成立,基于以下事实:

  1. 服务器是通过请求中的 cookie 信息辨认用户的;
  2. 浏览器关闭tab页,并不会立即清除保存在本地的 cookie, 同时服务器上保留的会话信息在过期之前不会清除,除非用户关闭tab之前主动登出;
  3. 在B页面上发起A页面上的请求,该请求会带上A域名下的 cookie。

有了以上的信息,CSRF 漏洞就能成立了。


银行知道了该信息后,紧急组织专家堵漏洞。

银行的转账接口使用 GET 请求,这是严重的错误,因为 GET 请求应该是幂等的,不管调用多少次都是一个结果。于是把银行把接口升级为 POST 请求。

恶意网站也升级了,每次访问都会发起 POST 请求。李建国又被盗了10000。

银行知道了该信息后,又紧急组织专家堵漏洞。

这次他们开始校验请求头中的 referer 信息,因为 referer 信息记录了该请求从哪个域名发出。

恶意网站又升级了,这次他们加了一个代理服务器,在请求发给银行之前先通过代理服务器修改referer信息。李建国再次被盗了10000。

银行知道了该信息后,再次紧急组织专家堵漏洞。

这次他们给表单上增加一个隐藏域,<input type="hidden" value="23kh4acsdudesfr45hoiad" name="ctoken" />,在每次表单提交时都带上 ctoken。

恶意网站发现升级也没用了,就去寻找下一个漏洞了……

防范 CSRF

防范 CSRF 最有效的方式,就是每次提交都要求手动输入验证码,但这样的用户体验很差。现在应用最广泛的就是为提交的表单增加伪随机字段。在服务器上生成一串随机字符串,带到页面上并把随机码保存起来。在用户提交回的表单中取出随机码并与服务器上保存的作对比,如果匹配,那就是合法的请求;要是不匹配,就可以认为这是一个非法的请求。

koa-csrf

基于 [email protected]。以下 koa-csrf 简称为kcsrf。

先来看最基本用法

const koa = require('koa');
const csrf = require('koa-csrf');
const session = require('koa-generic-session');

const app = koa();

app.keys = ['session secret'];
app.use(session());
csrf(app);
app.use(csrf.middleware);

app.use(function* () {
  if (this.method === 'GET') {
    this.body = this.csrf;
  } else if (this.method === 'POST') {
    this.status = 204;
  }
});

app.listen(3000);

kcsrf 依托于 session,所以我们引入了koa-generic-session。kcsrf 接受一个配置项,可以传入 saltLength 和 secretLength,分别为盐长度和token长度。这两个参数被透传给 csrf 模块,用于生成 token。

该模块很简单,通过定义一个 getter 方法,通过如下形式传给模板,用于生成html页面。

app.use(function *() {
  this.render({
    csrf: this.csrf
  });
});

而在 getter 方法内部,生成并返回token的同时,还往 session 上增加了一个 secret 字段,用以保存生成的token。

在第二次请求过来时,就会将 token 带回来,位置可以在表单、查询串或自定义头上,将请求中的 token 取出与 session.secret 作对比,就可以判断是否为跨站攻击。

co源码分析

仓库:github.com/tj/co

co是一款将generatorpromise结合起来的流程控制库。

本文根据[email protected]版本所撰写。

什么是generator

使用方法

1.将promise作为返回值

'use strict';
const fs = require('fs');
const co =require('co');

function read(filename) {
  return new Promise(function(resolve, reject) {
    fs.readFile(filename, 'utf8', function(err, res) {
      if (err) {
        return reject(err);
      }
      return resolve(res);
    });
  });
}

co(function *() {
  return yield read('./a.js');
}).then(function(res){
  console.log(res);
});

2.将数组作为返回值

'use strict';
const fs = require('fs');
const co =require('co');

function read(filename) {
  return new Promise(function(resolve, reject) {
    fs.readFile(filename, 'utf8', function(err, res) {
      if (err) {
        return reject(err);
      }
      return resolve(res);
    });
  });
}

co(function *() {
  const resA = read('./a.js');
  const resB = read('./b.js');
  const resC = read('./c.js');

  return yield [resA, resB, resC];
}).then(function(res){
  console.log(res);
  // [a file content, b file content, c file content]
});

3.将对象作为返回值

'use strict';
const fs = require('fs');
const co =require('co');

function read(filename) {
  return new Promise(function(resolve, reject) {
    fs.readFile(filename, 'utf8', function(err, res) {
      if (err) {
        return reject(err);
      }
      return resolve(res);
    });
  });
}

co(function *() {
  const resA = read('./a.js');
  const resB = read('./b.js');
  const resC = read('./c.js');

  return yield {a: resA, b: resB, c: resC};
}).then(function(res){
  console.log(res);
  // {
  //    a: a file content, 
  //    b: b file content, 
  //    c: c file content
  // }
});

4.将generator做回返回值

'use strict';
const fs = require('fs');
const co =require('co');

function* read(filename) {
  return yield function(fn) {
    return fs.readFile(filename, 'utf8', fn);
  }
}

co(function *() {
  return yield read('./a.js');
}).then(function(res){
  console.log(res);
})

源码分析

promise

由使用方法可以看出来,co接受一个generator函数作为参数(以下简称G函数),并自动化地执行异步操作后返回promise对象。yield值可以是一个generatorpromise,数组,对象或函数。

我们先来看co函数。var args = slice.call(arguments, 1); 收集除了第一个参数外的所有参数。接着co返回一个Promise对象,在这个对象内,第一步先判断是否为一个函数,G函数当然也算是函数,于是便将参数applyG函数。接着调用onFulfilled函数。onFulfilled函数和onRejected函数就相当于是这个大Promise对象resolverejectonFulfilled先执行一次next,随后进入next函数。

为了便于分析,我们现在先假定是上面的第一种情况,也就是yield的值是一个promise。当在上一步执行完G函数自带next后,会得到一个形如{ value: Promise { <pending> }, done: false }的对象,也就是read函数return出来的处于pending状态的promise对象。将这个对象传给next函数,next函数先判断是否已完成(done),若未完成,则继续执行。接着调用toPromise函数,将value值转成一个promise,可以看到该方法对promise对象,generator函数,普通函数,对象和数组都做了相应的封装将其转化为promise

接下来看下一行。紧接着这个promise被调用了then方法,而resolvereject正是onFulfilledonRejected。这就形成了一个迭代,直至yielddone值为true时,这个迭代就结束了,返回resolvereject,被co外的then方法取得。

array和object

看完了promise是如何工作的,再来看arrayobject。其实这两个都差不多,array是通过Promise.all被调用,而object是通过调用objectToPromise方法包装成一个数组,最后仍然通过Promise.all被调用。

arrayToPromise方法调用了Promise.all方法,Promise.all方法用于将多个promise实例重新包装成一个。

'use strict';
let promise1 = Promise.resolve(1);
let promise2 = Promise.resolve(2);

Promise.all([promise1, promise2])
        .then(val => {
            console.log(val); // [1, 2]
        });

arrayToPromise做的就是then前面这一步。

objectToPromise首先通过new obj.constructor()来构造一个和传入对象有相同构造器的对象,results对象里会放Promise.all之后的结果,其中是promise对象的通过defer函数取得resolve之后的结果,不是promise对象的,如布尔值,常量等,原样返回。我们来看一下defer函数,这个函数会给每个传递来的promise对象绑定resolve方法(也就是then的第一个参数),而有些resolve是没有返回值的,所以就先将对应key的值置为undefined。最后,通过Promise.allresults对象中的promise对象都通过绑定的resolve对象变成了执行后的结果。

generator

thunkToPromise就不讲了,因为在[email protected]之前是通过thunk实现,之后都转成用promise实现,tj也似乎想放弃这一实现,只是为了向前兼容而写了个thunkToPromise,毕竟马上就要有async/await了。根据tj在readme里写的,co是一块迈向async/await的垫脚石。

co.wrap

最后讲一讲co.wrapco.wrap的作用是将一个generator函数转成一般的函数以方便调用。先看一个实例。

co(function *() {
  return yield read('./a.js');
}).then(function(res){
  console.log(res);
})

这个函数是一次性的,不能被复用,若想在别处使用,只能再复制一次,这显然是不能忍的,于是就有了co.wrap

var readFile = co.wrap(function *(filename) {
  return yield read(filename);
});

readFile('./a.js').then(function(res) {
  console.log(res);
});

readFile('./b.js').then(function(res) {
  console.log(res);
});

看一下源码,只有短短的几行,功能也很简单,就是在原来的generator函数外包了一层函数用来接收参数,再将这些参数一并置入co中。这个外部函数一般被称为高阶函数或偏函数,推荐阅读兔哥的这篇文章

debug包源码分析

仓库:github.com/visionmedia/debug

debug是由tj开发的一款精巧的node debug工具。

本文根据[email protected]版本所撰写。

使用方法

官方示例

源码分析

在require进debug时,会再跟上一个参数,也就是所谓的namespace,以区分这条debug日志是从哪一个模块打出来。调用debug函数时,自动带上namespace前缀。

根据package.json,入口为node.js。所以我们先来看node.js。

exports = module.exports = debug;

可能有的读者会感到奇怪,为什么要将module.exports等于exports?

其实这是一个小技巧,如果我们不写上面这一句,要暴露多个变量至外部时,就需要像下面这样写:

module.exports = debug;

function createApplication() {
    // ...
}

module.exports.color = color;
module.exports.enabled = false;

而当我们加上exports = module.exports时,就可以省下一些字符:

exports = module.exports = debug;

function debug() {
    // ...
}

exports.color = color;
exports.enabled = false;

这两种写法的结果是一样的,都是一个debug函数带着几个属性:

{
    [Function: debug]
    color: [object],
    enabled: false
}

我们接下去看,直接看最后一行,exports.enable(load())load函数取得环境变量DEBUG并传递给enable函数,如输入DEBUG=http,worker node index.js,DEBUG的值就是http,worker。接着传递给enable函数,enable函数先做一步非空校验,再通过逗号或空格分割成数组后,根据前缀是否带有-分别传递给skipsnames数组,skips数组里存有不在命令行里输出的namespace。

再来看debug.js中的debug函数,该函数是这个debug包的核心。该函数内有两个函数,分别为enabledisable,当这个namaspace在names数组中时,会返回enable函数;而遇到其他情况,如命令行中DEBUG参数和程序中的namespace不相符,或DEBUG参数前带有-号,则返回enable函数(见该函数最后几行)。

disable函数没什么好讲的,只是带了个enable属性为false的空函数。enable函数带有几个属性,包括为了统计操作耗时的diffprevcurr,是否使用颜色的useColor,和使用的颜色coloruseColor通过判断命令行是否输入DEBUG_COLOR和输入的值来返回一个布尔值,color则是通过selectColor函数为每个namespace分配一个颜色,colors数组里面的值是ansi color的的值。

再接下去看对传入参数的处理。通过slice函数将arguments转成一个真正的数组,然后通过coerce函数判断传入的是或否为一个Error对象,是的话则需要打印出stackmessage。接着通过正则将第一个参数中的占位符替换成后面跟着的参数。若参数为函数,则先取得返回值再replace。随后,被传递进去的参数用splice方法删掉。重复上述步骤就可以将一段带有占位符字符串替换为格式化后的字符串,和console的功能相同。

debug('my name is %s, %d years old', 'bruno', 20); // my name is bruno, 20 years old    

大家可能会有疑问,既然有console为什么还需要多此一举?其实是因为console出来的字符串不够美观,信息量也不够大,需要进行进一步处理,调用formatArgs函数为上面这段字符串加上颜色和运行时间。

到这一步,需要显示出来的字符串已经处理完成,接下来就该处理显示到哪里了。debug除了可以将日志打印到控制台,也可以输出到文件。log函数就是来完成这件事的。

log函数的后半段就是一个console,但console到哪里呢,由stream决定。stream是由fd决定的,fd又是一个从命令行输入的参数DEBUG_FD,默认为2,比较有意思的是createWritableStdioStream函数,其中process.binding是没有在官方api文档里提及的api,是一个比较底层的api。通过这个函数可以决定输出的位置。

至于browser.js中的内容,是将日志输出值到浏览器的 console tab,差别并不大,不再赘述。

cors和koa-cors

近期在工作中用到了cors,所以写下这篇分享,本文基于koa-cors 1.0.1

啥是cors呢

Cross-Origin Resource Sharing(cors),顾名思义,跨域资源共享,也就是一种实现跨域的手段,想要的解决的问题是跟使用jsonp一样的。要想知道跨域是为了什么,得先知道什么是跨域。跨域(cross-orgin)是因为有同源策略(same-origin policy)的存在。浏览器为了保证加载的脚本等资源都是可控安全的,就加了一个强制性的规定,非同域名下的资源不得加载。同域名是指两个域名之间,端口,协议,以及子域都应相同

这是与http://www.alipay.com的通信结果

域名 结果 原因
http://www.alipay.com/dir/a.js 成功 同一域名
http://news.alipay.com/a.js 失败 非同一子域
https://www.alipay.com/a.js 失败 协议不同
http://www.alipay.com:1234/a.js 失败 端口不同

在chrome的network面板里可以看到,发给非同域的请求其实是发出去了的,只是浏览器在拿到对方服务器返回的时候数据时,看到这个响应并不是从当前域名返回的,就302了这个响应,同时报了个错。这样看来,跨域都是浏览器的锅,jsonp、iframe等手段都是为了绕过这一障碍才发明的。浏览器也不愿天天背这个锅,码农何苦为难码农,于是出现了cors这一手段,让浏览器天生支持跨域。

cors的实现非常简单,前端甚至都不需要做修改,只需要在对方服务端的响应头里加个字段Access-Control-Allow-Origin就可以了。Access-Control-Allow-Origin的值可以是*,也就是代表任何域名都可以来拿我的资源,也可以指定域名,只有指定的域名可以拉取资源。

cors还有些特性也需要详说的:

预请求(Preflighted)

预校验是指某些特殊情况下,需要在发送真正的ajax请求之前发送一个options方法的请求,来『探测』一下我们的请求服务端接不接受,当然这一步是浏览器自动的,无需手动。『特殊情况』就是指下面的两种情况:

  • 使用了getheadpost之外的方法,或post请求的Content-Typeapplication/x-www-form-urlencodedmultipart/form-datatext/plain以外的,如application/xml等时;
  • ajax请求带有自定义请求头时。

在预请求的请求头里会额外再带上两个字段,分别是Access-Control-Request-Method(必带),Access-Control-Request-Headers(如果设置了自定义请求头),Access-Control-Request-Method的值就是该ajax请求所用的方法。

带上cookie的ajax跨域请求(credentials)

可能有的同学会说,这根本不给力啊,如果我们做的是安全性比较高的服务,cors过来的请求根本没法做校验,很可能会出安全事故啊~ w3c的同学都不是吃素的,早就为你考虑到啦!一般来说,跨域的ajax请求是不带cookie的,而cors的ajax请求就不一样了,人家可以带cookie去服务端,这样就可以让服务端来判断这个请求是否合法。而且让ajax带cookie很简单,一句话的事儿:xhr.withCredentials = true;。若服务端认为这个请求是合法的,返回的响应头里必须带上Access-Control-Allow-Credentials: true,若为其他值或不带这个字段,浏览器会把withCredentials发出的请求的响应通通拒绝掉。

这项技术的浏览器兼容情况还是不错的,IE8 ~ IE9使用XDomainRequest,其他浏览器都使用XMLHttpRequest原生支持。

koa-cors

说完了cors和cors的原理,我们接着来讲koa-cors。

使用

var koa = require('koa');
var cors = require('kcors');

var app = koa();
app.use(cors());

cors方法可以传入一个对象options

{
    origin:允许发来请求的域名,对应响应的`Access-Control-Allow-Origin`,
    allowMethods:允许的方法,默认'GET,HEAD,PUT,POST,DELETE',对应`Access-Control-Allow-Methods`,
    exposeHeaders: 允许客户端从响应头里读取的字段,对应`Access-Control-Expose-Headers`,
    allowHeaders:这个字段只会在预请求的时候才会返回给客户端,标示了哪些请求头是可以带过来的,对应`Access-Control-Allow-Headers`,
    maxAge:也是在预请求的时候才会返回,标明了这个预请求的响应所返回信息的最长有效期,对应`Access-Control-Max-Age`
    credentials:标示该响应是合法的,对应`Access-Control-Allow-Credentials`
}
var requestOrigin = this.get('Origin');
if (!requestOrigin) {
  return yield* next;
}

如请求头不带Origin字段,说明根本不是cors请求,直接忽略。

if (this.method !== 'OPTIONS') {
  ...
} else {
  ...
  if (!this.get('Access-Control-Request-Method')) {
    // this not preflight request, ignore it
    return yield* next;
  }
  ...
}

若该请求不是OPTIONS方法,说明不是预请求,则根据options中的字段设置响应头,若该方法是OPTIONS方法,但不带Access-Control-Request-Method,说明不是预请求,仍然直接忽略。

this.status = 204;

在设置完需要设置的字段后,由于不需要返回具体内容,到这里就可以直接返回个204了。204表示不带响应体的成功响应。

koa源码分析(三) - request.js

本文分为四个部分,分别对应源码的四个文件。

依赖

  • content-type:获取http请求头的content-type并装换成小写
  • stringify:url模块的format方法,用于将一个对象整合成url
  • parserutl: 顾名思义,解析url
  • type-is: 判断req的content-type
  • fresh: 模仿服务器304的判断过程,既对比请求中的if-modified-since和etag与相应中的last-modified来判断是否需要带上响应体(body)

源码分析

request里的方法都是对request头里的参数进行一个简单的封装或仅仅仅仅是一个缩写,大多数很简单,我只挑我喜欢的讲,没有为什么,任性~

idempotent

idempotent的中文是幂等,什么意思呢,就是你进行了一项操作,比如拉取资源,上传文件,提交订单等,若是做了100次跟做1次的结果是一样的,那这项操作就是幂等的。比如我拉取一个资源,拉取100次跟拉1次没什么区别。但用post提交数据就不是一个幂等操作,若不做幂等校验,每一次的post都将会落入数据库(比如是一个落库操作),所以都会对提交上来的数据进行幂等校验。

说完什么是幂等,再来看idempotent,就两句,第一句里的数组元素都是幂等操作,第二句是看this.method是否是一个幂等操作,操作符是NOT操作符,属于对二进制数据的操作,前端用的很少。有一个简单的公式可以计算之后的结果,a => -a - 1,所以当indexOf的结果是-1时,-1就等于0,两次取反就得到false。

ee-first源码分析

仓库:https://github.com/jonathanong/ee-first

ee-first是一个竞争事件收集器,可以为多个事件对象绑定多个事件,并在某个事件触发后移除该对象的所有事件。

本文根据[email protected]版本所撰写。

使用方法

'use strict';
const first = require('ee-first');

const event = require('events').EventEmitter;
const ee1 = new event();
const ee2 = new event();

const handle1 = first([[ee1, 'error', 'a', 'b']], function(err, ee, event, args) {
 console.log(`err: ${err}\nee: ${ee}\nevent: ${event}\nargs: ${args}`);
})

const handle2 = first([[ee2, 'error', 'a', 'b']], function(err, ee, event, args) {
 console.log(`err: ${err}\nee: ${ee}\nevent: ${event}\nargs: ${args}`);
})

ee1.emit('error', 'err msg'); 
// err: err msg
// ee: [object Object]
// event: error
// args: err msg


ee1.emit('b', 'whatever'); // nothing

// 移除所有绑定在ee2事件对象上的事件
handle2.cancel();

ee2.emit('b', 'sucks'); // also nothing

源码分析

这个模块暴露了一个first方法,这个方法接受2个参数,事件数组和回调。事件数组是指一个二维数组,可以包含若干个数组,每个数组的第一个是元素是事件对象,如httpnet等继承自EventEmitter的对象。剩余的元素是所有需要绑定在该事件对象上的事件。第二个参数是事件绑定的回调,在这里为了便于区分,特称为事件回调

cleanups数组是一个包含所有事件对象所绑定事件的数组。

进入循环后,变量fnlistener函数的返回值,listener的作用是收集事件回调的参数列表,并传入callback函数,等待事件触发(emit)。callback函数做了两件事,执行cleanup方法移除所有cleanups数组中事件对象中的事件,并执行回调,这就表现竞争现象

cancel方法直接调用cleanup,移除所有事件。

no-cache与max-age=0

今天又复习了一遍http协议,在看文档时,发现cache-control有很多指令,其中就包括一个max-age。
打开控制台,查看网络连接,当中有很多max-age都被设置成了0,我很疑惑,这和no-cacha有啥区别。进过一番谷歌,终于知晓其中区别。

相同之处

no-cache顾名思义是指该资源不会被缓存,而max-age会被缓存到本地,只是在下次重新访问该页面时又会强制地从服务器拉取资源。所以大部分情况下,这俩其实是一样的。

不同之处

当点击浏览器的前进后退按钮时,被no-cache的资源会重新加载;而被设置成max-age的读取则会从本地读取资源。当然这也需要根据浏览器实现的情况来看,某些浏览器如IE9之前的IE,并没有遵循http协议,直接统一了这两个字段的行为为no-cache.

static file & koa-static

用户访问我们的网站,得到的结果有两种,想看的和不想看的。

不想看的分两种,网站报错和地址输错。

想看的也分两种,动态生成和静态存在。今天,我们就来聊聊静态文件。

所谓静态文件

所谓的静态文件,就是静态的,static的文件,无需经过查找数据库、模板渲染等步骤(以上步骤为充分不必要)就可以直接给用户看的,如js、css文件,图片等。

一般来说,访问静态文件的url和其本身的文件路径是一样,如 http://a.com/public/img/a.png, 那目录下就会有个public文件夹。这是因为静态文件数量很可能非常庞大,不能给每个文件都加上路由,所以其文件路径就是url。

每个http响应头里都有Content-Type字段,该字段告诉浏览器返回的文件是什么类型的,便于浏览器处理。如一个html文件的Content-Type是text/html,而一般的文本文件Content-Type是text/plain,当把html文件的Content-Type改成text/plain,浏览器就不会解析html文件,而是直接输出html代码。我们可以通过文件的后缀识别该文件的MIME。

koa-static

有了上面的知识,我们再来看koa-static。基本用法如下代码:

'use strict';

const app = require('koa')();
const server = require('koa-static');

const opts = {
  maxage: 1000 * 60 * 60 * 24 * 365, // 1年,默认为0
  hidden: false, // 能否返回隐藏文件(以`.`打头),默认false不返回
  index: 'index.js', // 默认文件名
  defer: true, // 在yield* next之后返回静态文件,默认在之前
  gzip: true 
  // 允许传输gzip,如静态文件夹下有两个文件,index.js和index.js.gz,
  // 会优先传输index.js.gz,默认开启
};

app.use(server('/public', opts));

koa-static所做的,仅仅是简单地处理选项,真正干事的是koa-send模块,我们再来看看这个模块。

前面一段是选项处理,略过,看如下代码

if (encoding === 'gzip' && gzip && (yield fs.exists(path + '.gz'))) {
  path = path + '.gz';
  ctx.set('Content-Encoding', 'gzip');
  ctx.res.removeHeader('Content-Length');
}

如果开启了gzip选项,就会优先传输.gz文件。

if (stats.isDirectory()) {
  if (format && index) {
    path += '/' + index;
    stats = yield fs.stat(path);
  } else {
    return;
  }
}

若为目录,并且找不到index配置项的文件,直接return,如无意外就是404,不过这样情况是应该是403。另外,是否为隐藏文件的判断居然只是根据文件名前的.,完全不考虑windows机器。勇敢的少年快去提pr吧。

核心代码就是下面的几行

ctx.set('Content-Length', stats.size);
if (!ctx.response.get('Last-Modified')) {
  ctx.set('Last-Modified', stats.mtime.toUTCString());
}
if (!ctx.response.get('Cache-Control')) {
  ctx.set('Cache-Control', 'max-age=' + (maxage / 1000 | 0));
}
ctx.type = type(path);
ctx.body = fs.createReadStream(path);

LRU(近期最少使用算法)及实现

任何缓存的容量都是有限的,当缓存已被填满,新的数据进来时,必定有其他数据被清除出去。那我们如何选择什么数据该被剔除呢?我们可以做一个假设,新数据被使用的概率要高于老数据,所以最后一次使用时间离现在最远的数据将被清除。举个例子,A,B,C 三个数据,依次创建于9点18,10点20,11点10分,并占满了缓存空间。此时有个新的数据 D 加入缓存,A 就会从缓存里剔除出去。而如果我们在11点30分调用了 A,此时 D 加入缓存,B 就会被剔除,因为 A 此时变成了最新的数据,而 B 的最后一次使用时间离 D 进入的时间最长。实际上,V8 也是用『新数据被使用的概率要高于老数据』这个假设来进行垃圾回收的。

上一段所述内容,就是 LRU(Least Recently Used),近期最少使用算法,一种按照访问时间排序的数据结构。一般来说,我们会采用双向链表来实现。

|A(head): { |    |B: {       |    |C(tail): { |
|  key,     |    |  key,     |    |  key,     |
|  value,   | => |  value,   | => |  value,   |
|  newer: B,|    |  older: A |    |  older: B,|
|           |    |  newer: C,|    |           |
|}          |    |}          |    |}          |
将被删除 <---------------------- 刚加入的数据

一个完整的缓存起码有增(add)删(remove)改(set)查(get)的能力,为了便于删除最老的数据,我们再加个 shift 函数。

class LRUCache {
  constructor(limit) {
    this.size = 0
    this.limit = limit
    this._keymap = {}
  }

  add () {}
  shift () {}
  get () {}
  set () {}
  remove () {}
}

我们用 size 记录存储的数据个数,limit 记录存储上限,_keymap 存放所有数据。

先来实现 add 函数

class LRUCache {
  ...
  add (key, value) {
    const entry = {
      key,
      value
    }
    this._keymap[key] = entry

    if (this.tail) {
      // 新数据加入时,老的 tail 增加 newer 字段指向新数据
      this.tail.newer = entry;
      entry.older = this.tail
    } else {
      this.head = entry
    }
    // tail 永远指向最新的数据
    this.tail = entry

    if (this.size === this.limit) {
      return this.shift()
    } else {
      this.size++
    }
  }
}

tail 记录了最新加入的数据,head 记录了最老的数据,而每个新加入的数据都会加上 newer 和 older 指向前一个和后一个活动数据(最新的没有 newer,最旧的没有 older),相当于双向链表的 next 和 prev。同时,当 size 超过 limit 时,删掉最老的数据。

接着我们实现上一步提到的 shift,shift 函数应该能够删掉最旧的数据,并将 head 指向倒数第二旧的数据

class LRUCache {
  ...
  shift () {
    const entry = this.head
    if (entry) {
      // 当数据不止一个时
      if (this.head.newer) {
        // 把倒数第二个数据设为 head
        this.head = this.head.newer;
        this.head.older = undefined;
      } else {
      // 只有一个数据时调用shift,直接设置为 undefined
        this.head = undefined
      }

      // 返回的数据不应该有 newer 和 older 的数据
      entry.newer = entry.older = undefined;
      delete this._keymap[entry.key]
    }
    return entry
  }
}

然后再来实现 get 的逻辑。要注意 get 后该数据会被提升为 tail

class LRUCache {
  ...
  get (key) {
    const entry = this._keymap[key]

    if (!entry) return
    if (entry === this.tail) {
      return entry.value
    }

    // 双向列表的删除操作,older 连接到 newer
    if (entry.newer) {
      if (entry === this.head) {
        this.head = entry.newer
      }
      entry.newer.older = entry.older
    }
    if (entry.older) {
      entry.older.newer = entry.newer
    }

    // 并把该数据提到tail的位置,tail变为第二位
    entry.newer = undefined
    entry.older = this.tail
    if (this.tail) {
      this.tail.newer = entry
    }
    this.tail = entry

    return entry.value
  }
}

get 函数所做的就是将该数据从链表中删除,随后放到第一位,原来的 tail 自然变成了第二位。

set 的逻辑很简单,先通过 get 拿到数据后修改,若没有该数据,调用 add 新增。

class LRUCache {
  set () {
    let oldvalue
    const entry = this.get(key, true)
    if (entry) {
      oldvalue = entry.value
      entry.value = value
    } else {
      oldvalue = this.add(key, value)
      if (oldvalue) oldvalue = oldvalue.value
    }
    return oldvalue
  }
}

好了,最后一步,实现 remove

class LRUCache {
  ...
  remove (key) {
    var entry = this._keymap[key]
    if (!entry) return
    delete this._keymap[entry.key]
    if (entry.newer && entry.older) {
      // 既不是 head 也不是 tail
      entry.older.newer = entry.newer;
      entry.newer.older = entry.older;
    } else if (entry.newer) {
      // 是 head,并重新设置 head
      entry.newer.older = undefined;
      this.head = entry.newer;
    } else if (entry.older) {
      // 是 tail,并重新设置 tail
      entry.older.newer = undefined;
      this.tail = entry.older;
    } else {
      // 独苗,一删全完蛋
      this.head = this.tail = undefined;
    }

    this.size--;
    return entry.value;
  };
}

根据数据所在位置和数量不同,有不同的处理方法。夹在中间的数据,安心删掉自己就可以;在 head 和 tail 上的数据,需要删完重新设置;而最惨的只有一个数据时,一删除全完蛋。

参考:js-lru

cookie & session & koa

产品经理之死

http是无状态的,你不能通过观察一条http请求来猜想前一条或后一条请求可能的样子。这为网站管理登录用户带来了困难,因为无状态,所以你无法知道这条请求从何而来。在1994年时,网景浏览器弄出了可以存储在用户本地的一小段文本信息,被称为cookie。

这个名字还有一段小故事,美国有一款叫Fortune cookie的饼干,里面藏有一张写着有趣句子的纸条。网景借鉴了这个寓意,把http内的隐藏信息称为cookie。

用户在访问某个网站时,服务器的响应头里会带有Set-Cookie字段,告诉浏览器存储这里面的信息,于是用户的浏览器里就带有cookie了,发第二条请求时就会带上Set-Cookie字段里的值,这就是客户端能做的所有。

这时来了个产品经理,说我们网站要支持登录。所谓登录,就是用户带上凭证,server允许其进行不带凭证的用户不能进行的操作。用户登录后,server在response里加上Set-Cookie字段,并在server里(内存/文件/数据库)存储该值。下一次用户再发来请求,就会带上cookie,然后server取出cookie与存储内的值作对比,若匹配,我们就会认为改用户是已登录的,从而放行操作。以上的行为,就可以称为维持了一段会话(session)。

这时产品经理说,我们网站要支持20分钟超时登出。也就是说如果用户20分钟内没有发来任何请求,我们就认为用户死了,也就没有维持这段会话的必要了,可以删除掉存储的cookie值了。但在未过期之前,用户每发来一个请求,就给该用户 +20m。

永不满足的产品经理说,我们网站要提高安全性,不管你们干嘛,就是要提高安全性。经过一番安全检查,我们发现我们的cookie有被篡改的风险。针对这种情况,我们可以给cookie加签。我们选择把cookie增加个时间戳字段,然后进行加密后提取摘要,再把这个摘要值放在cookie和server的存储中。在下一次请求时,我们就会对比这两个值是否相等,若相等,重新更新时间戳并计算,保证每条请求都是独一无二的;若不相等,说明该cookie被篡改。

后来,产品经理说,我们要支持单点登录。后来他坟头草两丈高了吧。

koa-generic-session

上面说了这么多,我们再结合代码来看看,下面是根据 [email protected]做的源码解读。

来看/lib/session.js。

const session = require('koa-generic-session');
const app = require('koa')();

app.use(session());

这是最基本的用法,session可以传入配置项,如下:

{
  key, // session id的键名
  store, 
  // 如何存储session,一般会选择redis。
  // 若不传入,会默认存储在内存中,在生产环境下会产生一个警告
  ttl, // session过期时间
  prefix, // 存储sessoin时的前缀
  cookie, // cookie配置项
  defer, 
  // 默认情况下,每次app.use(session());后都会去生成存储session,但静态文件是不需要session的。
  // 所该选项为true,调用yield this.session;才会去生成session。
  genSid, // 可以自定义生成session的方法
  errorHanlder, // 自定义处理session存储和取出失败时的错误处理
  valid, // 自定义校验session,
  beforeSave, // 钩子函数
  sessionIdStore, // object,内部需要有set,get,reset三个方法用来往ctx.cookies上设置cookie
}

选项中有个defer字段,作用如下

return options.defer ? deferSession : session;

在设置defer为true后只有在调用yield this.session时才会生成session,节省资源。我们来看deferSession函数。

代码太长,只挑要紧的讲。在这个函数内有getter/setter,和regenerateSession方法。getter方法取得session,若已被setter方法设置过,就直接返回设置的session。若没设置过,会调用getSession获取session。

getSession的流程就是先检查sessionId(cookie中),若没有说明是新连接,调用generateSession生成session,若不是,则从存储中取出的对应的session(可能为空,因为访问间隔超过超时时间),随后返回一个object。

在做完上面的步骤后,就会调用refreshSession,当session变为空,就从存储中删掉该条session,若发生改变,则更新cookie和存储中的值。

session方法就是deferSession的简化版,就不重复了。

koa源码分析(四) - response.js

本文分为四个部分,分别对应源码的四个文件。

依赖

大多数在之前有介绍到过,这里只讲之前没讲到的

  • vary: 用于设置vary字段

源码分析

首先,推荐大家去看http roc文档,这还是中文版的,不用怕看不懂。作为前端工程师,又跟node打交道,http知识是必不可少的,有兴趣的还应该再去了解TCP/IP协议,了解一点后就会对网络通信有比较深入的了解了,推荐一个大部头,《TCP-IP详解》,内容很多,调重要的看,绝对能学到很多。

照例,有些一眼看得懂的我又不讲啦。

set body

响应体(body)若为空,设状态码为204,并将Content-Type,Content-Length,Transfer-Encoding移除掉,因为这三个字段在body有值时才会被设置。
接着内容是不是由‘<’打头的字符串,若是的话就肯定是个html,若不是的话,就是个单纯的文本。
再判断是否为一个buffer也就是二进制文件,图片视频那些东西在传输时全是二进制,所以就不做啥操作,设置个length就行了。
然后看是否是stream,依据是是否有pipe这个函数,这个函数就跟管道符一样,用于传输数据,然后我就不太懂了。
最后就认为body是个json, json没有length

redirect

用法:

this.redirect('back');

this.redirect('back', '/index.html');

this.redirect('/login');

this.redirect('http://google.com');

若第一个参数为back,该方法将从referrer中取值或取第二个参数,没有就直接跳'/'。接着在看req中的accept是什么值,要是个html的话,body是个链接,若不是的话,直接返回串字符就行了。

react-redux 的 hooks

在v7.1中,react-redux加上了hooks,接下来我们就来看看新hooks,以及需要如何修改组件。
如果你还不清楚什么是hooks,建议阅读此文档

hooks

  • useSelector,接受传入一个函数,此函数的入参是store,可以理解为原来的mapStateToProps函数;
  • useDispatch,没有参数,返回值就是dispatch,可以理解为原来的mapDispatchToProps;
  • useStore,获得store,但尽量不要用这个方法,因为获得的store不是响应式的,只是一个快照。

useSelector

签名:

const result : any = useSelector(selector : Function, equalityFn? : Function)

举例来说:

import { shallowEqual, useSelector } from 'react-redux'
const count = useSelector(stoer => store.count, shallowEqual); // 第二个参数可选,也可以换成自定义的比较函数

useDispatch

用法很简单

const dispatch = useDispatch;
dispatch({ type: 'action1' })

使用connect的写法

import React from 'react';
import { connect } from 'react-redux';

function componentWithConnect({ count, addCount }) {
  return (
    <>
      <span>{count}</span>
      <button onClick={addCount}>点击</button>
    </>
  );
}

const mapStateToProps = (store) => ({
  count: store.count,
});
const mapDispatch = (dispatch) => ({
  addCount: () => dispatch({ type: 'addCount' }),
});
export default connect(mapStateToProps, mapDispatch)(componentWithConnect);

换成hooks的写法

import React from 'react';
import { useSelector, useDispatch } from 'react-redux';

export default function componentWithHooks() {
  const { count } = useSelector(store => ({
    count: store.count,
  }));
  const dispatch = useDispatch();
  const addCount = () => dispatch({ type: 'addCount' });

  return (
    <>
      <span>{count}</span>
      <button onClick={addCount}>点击</button>
    </>
  );
}

在新组件中,我们去掉了connect,使用了useSelectoruseDispatch两个hooks。这样就不用再从props传入,让逻辑更为内聚。(另外还有一个好处就是在React Devtools里也少了一层嵌套)

消失的useActions?

可能有的同学会有疑问,useDispatch并没有完全代替mapDispatchToProps,为什么不加上useActions呢?
实际上在beta版本中,react-redux是带有useActions的,但在dan的建议下,去掉了这个api。为什么呢?其实只要自己模拟一下,你就能知道为什么了。

function someComponet() {
  // !!!!没有 useActions 这个api!!!!
  const actions = useActions((dispatch) => ({
    action1: () => dispatch({ type: 'action1' }),
    action2: () => dispatch({ type: 'action2' }),
  }));

  useEffect(() => {
    actions.action1();
    actions.action2();
  }, [actions.action1, actions.action2]);
}

看看为了发送一个dispatch,写了多少模板代码,要是有更多的action,useEffect的依赖列表就会变得更长。此外,在得到actions后,几乎是立即就又被解包了,在使用connect的情况下可能还是可以接受的,但在使用了hooks后,无疑是画蛇添足。
其实在过去,通过mapDispatchToProps() => dispatch({ type: 'action1' })包装成一个actionCreator,是不是有过分迷恋这种『一行缩写』的嫌疑呢?更不要说这种缩写会让阅读代码的人搞不清数据流。
我们再来看如果直接使用useDispatch是怎么样的

function someComponet() {
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch({ type: 'action1' });
    dispatch({ type: 'action2' });
  }, [dispatch]);
}

是不是更简单,也更清晰了呢。

自定义hooks

在很多情况下,useDispatch已经够用,但在一些复杂情况下,还是需要自定义hooks的。如要在多个组件间共享逻辑,自定义hooks是个不错的选择。

// complexActions.js
const complexActions = () => {
  const dispatch = useDispatch();
  dispatch({ type: 'actions1' });
  dispatch({ type: 'actions2' });
  someRequest().then(() => {
  	dispatch({ type: 'actions3' });	
  });
  // balbala...
};

// componetA.js
function componentA() {
  useEffect(() => {
  	complexActions();
  }, [complexActions]);
  
  return (...);
}

// componetB.js
function componentB() {
  useEffect(() => {
  	complexActions();
  }, [complexActions]);
  
  return (...);
}

性能优化建议

触发dispatch后,useSelector被触发(因为useSelector是使用useEffect实现,useEffect的依赖就有store),当发现前后两次state不一样时,会触发重渲染。此时的问题是当不相关的值被修改,本组件仍然会重渲染。可以使用reselect来减少这种情况,具体怎么做,可以参考官方文档
另外,connect也会对props做一次浅比较,防止重渲染。所以去掉connect之后,就需要使用React.memo去做了。

import React from 'react';
function componentA(props) { return <>...</> }
export default React.memo(componentA);

参考

抛弃难用的stream

什么是 stream

简单来说,将一个资源分成一个个小块(chunk)传输,而不是一次性传输所有数据,这种技术就可以被称为 stream。
数据源被称为 readable stream,接受数据方被称为 writeable stream,而在两者之间可以对数据做一些处理的中间步骤被称为 transform stream。
Stream 的优点就是不会对内存产生压力,并且可以让数据尽可能快地到达,而不必等待所有数据加载到内存。

难用的 stream

对我来说 stream 可能是 nodejs 中最难以掌握的部分,每次使用到 stream 时,都需要翻开文档查 api。另外,也很容易忘记处理错误,如当要从 readable stream pipe 到 writeable stream 时,要同时处理两个 stream 的错误,但往往会遗漏。

让我们看个例子,从一个 url 下载图片到本地

function download(url: string, filePath: string): Promise<void> {
  return new Promise((resolve, reject) => {
    const file = createWriteStream(filePath);
    file.on('finish', resolve);

    http.get(url, response => {
      response.pipe(file);
    }); 
  });
}

Emm,好像忘了处理错误,让我们来加上:

function download(url: string, filePath: string): Promise<void> {
  return new Promise((resolve, reject) => {
    const file = createWriteStream(filePath);

    const request = network.get(url, response => {
      if (response.statusCode === 200) {
        response.pipe(file);
      } else {
        file.close();
        unlinkSync(filePath);
        reject(`Server responded with ${response.statusCode}: ${response.statusMessage}`);
      }
    });

    request.on('error', err => {
      file.close();
      unlinkSync(filePath);
      reject(err.message);
    });

    file.on('finish', resolve);

    file.on('error', (err: FileErr) => {
      file.close();

      if (err.code === 'EEXIST') {
        reject('File already exists');
      } else {
        unlinkSync(filePath);
        reject(err.message);
      }
    });
  });
}

这下看起来不错了,已经补上了所有错误处理的逻辑。但是一个如此简单的需求我们却写了这么多代码,说明 stream 太难用了,需要我们手动处理太多逻辑。

那么,「难用」体现在哪呢?
第一,pipe 方法倾向于让用户使用链式调用,非常容易忘记写错误处理逻辑;
第二,http.get 回调参数 和 createWriteStream 都基于 stream,需要事无巨细地处理各种事件;
第三,逻辑被分割在各个回调里,无法清晰地阅读及调试。

新的工具

node 基础库里的 http 模块非常底层,所以 node 社区为 http client 出了一个又一个轮子,如axios、request、urllib等等。官方显然也是看到了这个问题,于是写了一个轮子Undici,同样基于 stream,但封装了很多细节,支持 promise,未来有望合并到 Node 基础库内。贴一段示例代码

import { request } from 'undici'

const {
  statusCode,
  headers,
  body
} = await request('http://localhost:3000/foo')

console.log('response received', statusCode)
console.log('headers', headers)

for await (const data of body) {
  console.log('data', data)
}

可以看到 api 设计已经和流行的 http client 比较接近了。

Pipe 方法也有了一个替代方案,在 Node 10 内新增了一个 pipeline 方法,注意是pipeline不是pipe。再贴一段官方示例:

const { pipeline } = require('stream');
const fs = require('fs');
const zlib = require('zlib');

pipeline(
  fs.createReadStream('archive.tar'),
  zlib.createGzip(),
  fs.createWriteStream('archive.tar.gz'),
  (err) => {
    if (err) {
      console.error('Pipeline failed.', err);
    } else {
      console.log('Pipeline succeeded.');
    }
  }
);

抛弃了链式写法,更加清爽,强烈建议换用。并且还有 promise 版本:

const { pipeline } = require('stream/promises');

async function run() {
  await pipeline(
    fs.createReadStream('archive.tar'),
    zlib.createGzip(),
    fs.createWriteStream('archive.tar.gz')
  );
  console.log('Pipeline succeeded.');
}

run().catch(console.error);

重构

让我们使用上面的工具重构最初的需求

async function download(url: string, filePath: string): Promise<void> {
  try {
    const { body } = await request(url);
    await pipeline(
      body,
      createWriteStream(filePath),
    );
  } catch (e) {
    console.log('下载失败', url);
    unlinkSync(filePath);
  }
}

这个版本的代码,清晰易调试,不会忘记处理错误,你有什么理由不用呢~

总结

又做了一回标题党,我们其实并不需要抛弃 stream,反而应该大力拥抱,WHATWG已经制定了web streams 标准,node 也实现了这套标准,stream 也早已脱离 node,来到了浏览器。通过高层级封装的 api,我们可以更加方便地使用,或许2022年就将是 the year of web streams :)

Babel 全家桶

15 年 11 月,Babel 发布了 6.0 版本。相较于前一代 Babel 5,新一代 Babel 更加模块化, 将所有的转码功能以插件的形式分离出去,默认只提供 babel-core。原本只需要装一个 babel ,现在必须按照自己的需求配置,灵活性提高的同时也提高了使用者的学习成本。下面就来讲讲 Babel 全家桶中的各个部分。

npm i babel

已经弃用,你能下载到的仅仅是一段 console.warn,告诉你 babel 6 不再以大杂烩的形式提供转码功能了。

npm i babel-core

babel-core 的作用是把 js 代码分析成 ast ,方便各个插件分析语法进行相应的处理。有些新语法在低版本 js 中是不存在的,如箭头函数,rest 参数,函数默认值等,这种语言层面的不兼容只能通过将代码转为 ast,分析其语法后再转为低版本 js。

以下代码转自阮一峰老师博客

const babel = require('babel-core');

// 字符串转码
babel.transform('console.log', options);
// => { code, map, ast }

// 文件转码(异步)
babel.transformFile('filename.js', options, (err, result) => {
  result; // => { code, map, ast }
});

// 文件转码(同步)
babel.transformFileSync('filename.js', options);
// => { code, map, ast }

// Babel AST转码
babel.transformFromAst(ast, code, options);
// => { code, map, ast }

npm i babel-register

该模块给 require 加了个钩子,.jsjsx.eses6 后缀的模块都会先转码。此外有几点需要注意:

  1. 当前文件不会被转码;
  2. 需首先加载 babel-register
  3. 由于是实时转码,只适合开发环境。
require('babel-register')
const index = require('./index.jsx')

npm i babel-cli -g

babel 命令行工具,可以转码文件或目录并输出至指定文件,直接看用法:

#1. 直接在终端输出
$ babel script.js
# output...

#2. 输出到文件
$ babel script.js --out-file script-compiled.js

#3. 支持 source maps
$ babel script.js --out-file script-compiled.js --source-maps

#4. watch 变动并输出到文件
$ babel script.js --watch --out-file script-compiled.js

#5. 可以编译文件夹
$ babel src --out-dir lib

#6. 也可以编译成一个文件
$ babel src --out-file script-compiled.js

npm i babel-plugin-*

babel-plugin-* 代表了一系列的转码插件,如babel-plugin-transform-es2015-arrow-functions 用于转码 es6 中的箭头函数,babel-plugin-transform-async-to-generator 用于将 es7 中的 async 转成 generator。

.babelrc中的配置:

{
  plugins: [
    'transform-es2015-arrow-functions',
    'transform-async-to-generator',
  ]
}

npm i babel-preset-*

我们现在有了 babel-plugin 系列,可以按需配置自己想要的特性。但若是想搭个 es6 环境,一个个地配置各个插件,我猜你会疯掉。babel-preset 系列就可以满足我们的需求,babel-preset 系列打包了一组插件,类似于餐厅的套餐。如 babel-preset-es2015 打包了 es6 的特性,babel-preset-stage-0 打包处于 strawman 阶段的语法(关于 ECMAScript 制定流程可以看这里),

.babelrc中可以这样配置:

{
  presets: [
    'es2015',
    'stage-3',
    'react'
  ]
}

npm i babel-runtime / babel-polyfill

上面提到的插件可以将语法从 es6 转成 es5,但没有提供 api 的转码功能,如 Promise、Set、Map 等新增对象,Object.assign、Object.entries 等全局对象上的新增方法都不会转码。而 babel-runtimebabel-polyfill 就是为此而生。

这两个模块功能几乎相同,就是转码新增 api,模拟 es6 环境,但实现方法完全不同。babel-polyfill 的做法是将全局对象通通污染一遍,比如想在 node 0.10 上用 Promise,调用 babel-polyfill 就会往 global 对象挂上 Promise 对象。对于普通的业务代码没有关系,但如果用在模块上就有问题了,会把模块使用者的环境污染掉。

babel-runtime 的做法是自己手动引入 helper 函数,还是上面的例子,const Promise = require('babel-runtime/core-js/promise') 就可以引入 Promise。

但 babel-runtime 也有问题,第一,很不方便,第二,在代码中中直接引入 helper 函数,意味着不能共享,造成最终打包出来的文件里有很多重复的 helper 代码。所以,babel 又开发了 babel-plugin-transform-runtime,这个模块会将我们的代码重写,如将 Promise 重写成 _Promise(只是打比方),然后引入_Promise helper 函数。这样就避免了重复打包代码和手动引入模块的痛苦。此外,babel-runtime 不能转码实例方法,比如这样的代码:

'!!!'.repeat(3);
'hello'.includes('h');

这只能通过 babel-polyfill 来转码,因为 babel-polyfill 是直接在原型链上增加方法。

babel-polyfill vs babel-runtime

那什么时候用 babel-polyfill 什么时候用 babel-runtime 呢?如果你不介意污染全局变量(如上面提到的业务代码),放心大胆地用 babel-polyfill ;而如果你在写模块,为了避免污染使用者的环境,没的选,只能用 babel-runtime + babel-plugin-transform-runtime

和 webpack 配合

很少有大型项目仅仅需要 babel,一般都是 babel 配合着 webpack 或 glup 等编译工具一起上的。下面就来介绍下 babel 和 webpack 如何 1 + 1 > 2。

为了显出 babel 的能耐,我们分别配个用 babel-polyfillbabel-runtime 、支持 react 的webpack.config.js

先来配使用 babel-runtime 的:

module: {
  loaders: [{
    loader: 'babel',
    test: /\.jsx?$/,
    include: path.join(__dirname, 'src'),
    query: {
      plugins: ['transform-runtime'],
      presets: [
        'es2015', 
        'stage-0',
        'stage-1',
        'stage-2',
        'stage-3',
        'react',
      ],
    }
  }]
}

需要注意的是,babel-runtime 虽然没有出现在配置里,但仍然需要安装,因为 transform-runtime 依赖它。

再来个 babel-polyfill 的:

entry: [
  'babel-polyfill',
  'src/index.jsx',
],

module: {
  loaders: [{
    loader: 'babel',
    test: /\.jsx?$/,
    include: path.join(__dirname, 'src'),
    query: {
      presets: [
        'es2015', 
        'stage-0',
        'stage-1',
        'stage-2',
        'stage-3',
        'react',
      ],
    }
  }]
}

行文仓促,多有疏漏,理解上可能有偏差,欢迎各位搞个大新闻。

koa源码分析(二) - context.js

本文分为四个部分,分别对应源码的四个文件。

依赖

  • createError: 将小于500的错误往前端报,大于500的内部消化
  • delegate:将某个对象的方法代理至某个对象上

源码分析

context暴露了一个对象,包含了多个方法,并将request和response的方法代理到自己身上。

onerror

this.app继承自event.emitter,通过error事件触发onerror。onerror方法将取得的error code设置为传入err的status,默认为500,在设置完http相应的长度、状态码及body后,发送该响应至用户。

request和response

143~191行,通过delegate方法将response的request的方法绑定至context上。

koa-router

koa-router(以下简称KRouter)是使用很广泛的koa路由中间件,它的路由风格与express一致,使用方便。现7.0版本以上已支持koa2.0,本文基于[email protected]

之所以KRouter的路由风格与express一致,是因为都使用到了path-to-regexp这个模块,该模块可以很方便地将路由转换成正则,方便路由匹配。

Route的代码十分清晰,分为 methodsuseprefixroutesallowedMethodsallredirectregisterrouteurlmatchparam,现在按顺序分析。

Router

this.optsregister方法中的配置项,传给path-to-regexp,可以接收5个参数:

{
  end: 不再往下匹配路由,默认为true,
  name: KRouter实例名,
  sensitive: 大小写敏感,默认为false,
  strict: 请求的路径末尾是否必须得带上`/`,默认为false,
  prefix: 全局前缀
}

this.params中放的是路由参数中需要提前处理的参数,通过param方法置入。

this.stack中放的是Layer实例,Layer实例是通过register方法产生,具体

Router.prototype[method]

将http协议中的各个方法绑定到原型上,并对传入的参数做了下处理,Router的各http方法支持传入两或三个参数,第一个参数可以是命名路由(named route),或不传该参数。随后调用了register注册该中间件。

Router.prototype.use

KRouter也有一个use方法,用来单独对某些路由做前置操作,如可以对所有的路由都进行session校验,也可以对某一条ajax路由做cors,这都是可以的。此外,也可以用来做内嵌路由。啥是内嵌路由呢,举个🌰:

var forums = new Router();
var posts = new Router();

posts.get('/', function (ctx, next) {...});
posts.get('/:pid', function (ctx, next) {...});
forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods());

// responds to "/forums/123/posts" and "/forums/123/posts/123"
app.use(forums.routes());

这是readme里面的一段,仔细看一下,所谓的内嵌路由就是把前面的路由当做后面路由的前缀,这样再来看代码就清楚了。

若第一个参数是路由数组的形式,将其抓换为单路由形式;若为单路由形式,就用该路由覆盖默认的(.*),表示这个中间件是针对某个路由的,否则就是对所有路由都生效。

m.router中的router是在Router.prototype.routes中被挂上的,指向KRouter,单纯的中间件是没有这个属性的。随后,用父路由和全局前缀为该路由增加前缀,并给子路由的param存入父路由的param。这样,就得到了一个『内嵌路由』。

Router.prototype.prefix

KRouter可以使用setPrefix方法为每个已存在的路由设置默认前缀,这个setPrefix方法是Layer实例的方法。Layer是什么具体在register方法里讲。

Router.prototype.routes

该方法提供给koa使用,相当于入口程序。首先,往this.match方法传入路径和方法,看是否匹配。若匹配,则把匹配的Layer实例往ctx上挂,便于下次相同路由使用;否则,return next()。一个路径可能会匹配上多个路由,如/student/11可以与/:type/:id/student/:id两条路由都匹配上,这时就会先处理/:type/:id下的中间件,然后处理/student/:id下的。layerChain就是一个按顺序存放着中间件的数组。数组长度是路由长度的两倍,第N个是对路由中参数的提取包装,第N+1个是该路由对应的中间件(N从0开始)。随后使用compose执行layerChain中的中间件。

|--------------------------------koa 中间件------------------------------------|
|---[前置中间件]---|
                 |[KRouter]=>[获取匹配到的路由参数]=>[对应的中间件]|
                                                             |---[后置中间件]---|

再重新理一下,当一个请求到达KRouter时,先分析有没有匹配的路由,在匹配到的情况下,return出一个中间件数组,这个数组是进过compose包装的,而koa本身就是使用compose来处理中间件的,也就是说,KRouter是通过这个手段,在koa的中间件数组中上动态地插入中间件。

Router.prototype.allowedMethods

当响应为404或没有响应码时,我们可以再细分一点,给客户端返回不同的状态码。当请求的方法是我们没有实现的,返回个501;若这个http方法实现了,且是个options方法,就返回支持的http方法;若这个http方法实现了,但并没有路由使用了这个方法,返回个405

Router.prototype.all

为所有方法都注册该条路由,是Router.prototype[method]的加强版。

Router.prototype.redirect

该方法接受三个参数,从哪儿来,到哪儿去,以及状态码。只不过是调用了url、all方法和koa的redirect,很容易理解。

Router.prototype.register

register应该是最重要的一个方法,该方法4个方法,路径、该路径的http方法、改路径所对应的中间件、配置项。这4个参数在经过一些处理后,传入Layer中。至于Layer怎么翻译我也母鸡啊。存贮在stack中的元素全是Layer的实例,所以所以我们再来看Layer.js。

Layer

首先检查传入的方法,有get方法的话把head方法放第一个(不知道为什么),接着检查stack(其实就是中间件列表)中的元素是否为函数,然后用path-to-regexp模块得到匹配该条路径的正则和参数map(paramNames)。

Layer.prototype.match

检测该条路径与传入的路径是否匹配。

Layer.prototype.params

对从请求路径中得到的参数名和值做处理,返回值是一个键值对,形如{type: stu, id: 1}

Layer.prototype.captures

返回捕获到的参数数组。一个🌰:

const regexp = /^\/([^\/]+?)\/([^\/]+?)(?:\/(?=$))?$/i; // 通过reg-to-regexp解析/:type/:id得到的正则
const path = '/student/1'
path.match(regexp).slice(1); // ['stu', 1]

Layer.prototype.url

可以将字符串,数字,对象转换后传入pathToRegExp.parse后得到解析后的路径,相当于是pathToRegExp.parse的加强版。

Layer.prototype.param

该方法被 Router.prototype.param暴露出来,作用是对路由中的参数做前置操作。两个参数分别是路由的『小名』和对应的处理函数。

Layer.prototype.setPrefix

用传入的字符串为该条路由增加个前缀。


每条路由对应一个Layer实例,这些Layer实例按顺序存入stack中。

Router.prototype.route

每条路由都有一个配置项name,用来给这条路由起个『小名』,可以通过这个小名找到对应的Layer实例。

Router.prototype.url

该方法用来得到解析后的url,用Router.prototype.route得到对应的Layer实例后,调用实例上的url方法得到解析后的url。

Router.prototype.match

将请求路径与stack中的路由作对比,若有匹配上的请求就放入matched.pathAndMethod

Router.prototype.param

该方法的具体逻辑在Layer.prototype.param里。

koa源码分析(五) - koa 2.0

co的README里,tj写道co是async/await的一块垫脚石,没想到好时代这么快就来了,我们已经可以借助babel写出形似同步的异步代码了(需开启experimental模式)。而借着这股春风,koa2.0也来了。

我们还是先来看一下什么是async/await函数吧。

async函数就是generator + promise,只不过是将yield换成了await,但是比yield好理解,并且不用next来显式地执行下一步。async 可以声明一个异步函数,此函数需要返回一个 Promise 对象。await 可以等待一个 Promise 对象 resolve,并拿到结果。我们来看一个例子(引用自阮一峰老师的ECMAScript 6 入门)。

const readFile = function (fileName){
  return new Promise(function (resolve, reject){
    fs.readFile(fileName, function(error, data){
      if (error) reject(error);
      resolve(data);
    });
  });
};

const asyncReadFile = async function (){
  const f1 = await readFile('a.txt');
  const f2 = await readFile('b.txt');
  console.log(f1.toString());
  console.log(f2.toString());
};

可以看到,async和promise是紧密结合在一起的,await的结果就是被修饰函数的resolve值,那似乎没有方法接收reject呀,所以,最好是将await包裹在try/catch中,用来接收reject。

const asyncReadFile = async function (){
  try{
    const f1 = await readFile('a.txt');
    const f2 = await readFile('b.txt');
    console.log(f1.toString());
    console.log(f2.toString());
  } catch(e) {
    console.log('failed!');
  } 
};

有了async,能让我们更好地书写异步代码,就是这么的方便。

说了这么多async,我们再来看koa。koa2.0相对于1.0,转变体现在将var换成了let及const,使用箭头函数简化书写,不再支持generator,以及对于中间件的处理上。

中间件有一个很形象的比喻,就像一个洋葱,一个请求从最外面那一层进入洋葱,一路上进过一层层的中间件,到达洋葱心之后,请求完成了任务,又派了响应出去,带上需要返回的数据,一层层地返回最外面。而在进入出来的过程中,中间件会对请求和响应做『手脚』,比如对请求检验cookie,对响应加etag。那中间件是怎么知道来的到底是请求还是响应呢?其实中间件不需要知道,这是因为请求肯定早于响应,所以koa的中间件的做法就是将函数的上半部分用来处理请求,下半部分用来处理响应(这里只是打比方)。而区分『上半部分』和『下半部分』的分界线,在1.0是yield next(),在2.0中,就是await next()或是return next().then()。

说完原理,再来看koa-compose,最关键的只有一步

return Promise.resolve(fn(context, function next() {
  return dispatch(i + 1)
}))

Promise.resolve的作用是将一个普通的对象转换成promise对象,防止上一步放回一个普通函数,造成中间件链路的断裂。

为了更好地理解如何使用中间件,我们来写一个访客记录的中间件。

首先分析一下需求,我们认为请求中带有visited字段cookie的请求是来自曾经访问过本网站的游客,就在数据库中增加一次访问量,并将该条请求打个标记便于后续操作。若没有visitedcookie,就在响应增加visitedcookie。

module.exports = async function (ctx, next) {
  ctx.countNum = await count();
  const visited = ctx.cookies.get('visited') ? true : false;

  await next();

  if (!visited) {
    setCookie(ctx, 'visited');
  }
}

function count() {
  const Count = new mongoose.model('count');

  Count.add()
  .then(function() {
    return Count.findCountNum();
  })
  .then(function(num) {
    return Promise.reslove(num);
  });
} 

function setCookie(ctx, name) {
  ctx.cookies.set(name, 'what ever');
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.