Koa2框架学习笔记

摘录自:https://chenshenhai.github.io/koa2-note/

框架提出的背景

ES6/7带来的变革

自ES6确定和ES7中async/await开始普及,Node的发展变得更加迅速,可以预见“多层嵌套回调”,将会被Promise+async/await方式逐步取代。(但多层嵌套回调也有其特殊的应用场景)

Koa2大势所趋的前景

基于async/await实现中间体系的koa2框架将会是Node Web开发方向大势所趋的普及框架。基于generator/yield的koa1框架将会逐步被koa2框架取代,毕竟使用co.js来处理generator是一种过渡的方式,虽然有特定的应用场景,但是使用async/await会更加优雅的实现同步写法。

koa2快速开始

环境准备

快速开始

安装koa2
//  初始化package.json
npm init

//  安装koa2
npm install koa
hello world代码
const Koa = require('koa');
const app = new Koa();

app.use(async(ctx) => {
  ctx.body = 'hello koa2'
});

app.listen(300);
console.log('[demo] start-quick is starting at port 3000');
启动demo

由于koa2是基于async/await操作中间件,目前node.js 7.x的harmony模式下才能使用,所以启动的时的脚本如下:

node index.js

浏览器访问http:localhost:3000](http://localhost:3000/

async/await使用

快速上手理解

在chrome浏览器的console控制台中,运行以下代码:

function getSyncTime() {
  return new Promise(resolve, reject) => {
    try {
        let startTime = new Date().getTime();
        setTimeout(() => {
            let endTime = new Date().getTime(),
                data = endTime - startTime;
            resolve(data);    
        }, 500);
    } catch(err) {
      reject(err);
    }
  }
}

async function getSyncData() {
  let time = await getSyncTime(),
      data = `endTime - startTime = ${time}`;
  return data;
}

async function getDat() {
  let data = await getSyncData();
  console.log(data);
}

getData();

运行结果如下:

Promise {[[PromiseStatus]]: "pending", [PromiseValue]]: undefined}
endTime - startTime = 501
async/await语法特点:
  • 可以让异步逻辑用同步写法实现
  • 最底层的await返回需要是Promise对象
  • 可以通过多层async function的同步写法来代替传统的callback嵌套

koa2简析结构

源码文件
├── lib
│   ├── application.js
│   ├── context.js
│   ├── request.js
│   └── response.js
└── package.json

Github: https://github.com/koajs/koa
koa2源码的源文件结构,核心是lib目录下的4个文件:

  • application.js是整个koa2的入口文件,封装了context,request,response,以及最核心的中间件处理流程。
  • context.js处理应用上下文,里面直接封装部分request.js和response.js的方法。
  • request.js处理http请求。
  • response.js处理http响应。
koa2特性
  • 只提供封装好的http上下文、请求、响应,以及基于async/await的中间件容器。
  • 利用ES7的async/await模式来处理传统的回调嵌套问题和代替koa@1中的generator,但是需要在Node 7.x版本上的harmony模式下才能支持async/await。
  • 中间件只支持async/await形式的封装,如果需要使用koa@1中基于generator的中间件,需要通过中间件koa-convert封装一下才能使用。

koa中间件开发和使用

  • koa v1和v2中使用的中间件开发和使用
  • generator中间件在 koa v1和v2中的使用
  • async/await中间件开发和只能在koa v2使用的限制
generator中间件开发
generator中间件开发
  generator中间件返回的函数应该是function *()函数
/* ./middleware/logger-generator.js */
function log(ctx) {
  console.log(ctx.method, ctx.header.host + ctx.url);
}

module.exports = function() {
  return function *(next) {
    //  执行中间件操作
    log(this);

    if(next) {
      yield next
    }
  }
}
generator中间件在koa@1中的使用
  generator中间件在koa v1中可以直接use使用
const koa = require('koa'); // koa v1
const loggerGenerator = require('./middleware/loggerGenerator');
const app = koa();

app.use(loggerGenerator());

app.use(function *() {
  this.body = "hello world!";
});

app.listen(3000);

console.log('the server is starting at port 3000');
generator中间件在koa@2中的使用
  generator中间件在koa v2中需要koa-convert封装一下才能使用
const Koa = require('koa'); //  koa v2
const convert = require('koa-convert');
const loggerGenerator = require('./middleware/loggerGenerator');
const app = new koa();

app.use(convert(loggerGenerator()));

app.use((ctx) => {
  ctx.body = 'hello world!';
});

app.listen(3000);

console.log('the server is starting at port 3000');
async中间件开发
async中间件开发
/* ./middleware/logger-async.js */

function log(ctx) {
  console.log(ctx.method, ctx.header.host + ctx.url);
}

module.exports = function() {
  return async function(ctx, next) {
    log(ctx);
    await next();
  }
}
async中间件在koa@2中使用
async中间件只能在koa v2中使用
const  Koa = require('koa');  // koa v2
const loggerAsync = require('./middleware/logger-async');
const app = new Koa();

app.use(loggerAsync());

app.use((ctx) => {
  ctx.body = 'hello world!';
});

app.listen(3000);
console.log('the server is starting at port 3000');

路由

koa2原生路由实现

简单例子
const Koa = require('koa');
const app = new Koa();

app.use(async(ctx) => {
  let url = ctx.request.url,
      ctx.body = url;
});

app.listen(3000);

访问http://localhost:3000/hello/world页面会输出 /hello/world,也就是说上下文的请求request对象中url之就是当前访问的路径名称,可以根据ctx.request.url 通过一定的判断或者正则匹配就可以定制出所需要的路由。

定制化路由
源码文件目录
.
├── index.js
├── package.json
└── view
    ├── 404.html
    ├── index.html
    └── todo.html
demo源码
const Koa = require('koa');
const fs = require('fs');
const app = new Koa();

/**
 * 用Promise封装异步读取文件方法
 * @param {string} page html文件名称
 * @return {promise}
 */
function render(page) {
  return new Promise((reslove, rejcet) => {
    let viewUrl = `./view/${page})`;
    fs.readFile(viewUrl, "binary", (err, data) => {
      if(err) {
        reject(err);
      } else {
        reslove(data);
      }
    });
  });
}

/**
 * 根据URL获取HTML内容
 * @param {string} url koa2上下的url,ctx.url
 * @return {string} 获取HTML文件内容
 */
async function route(url) {
  let view = '404.html';
  switch(url) {
    case '/':
      view = 'index.html';
      break;

    case '/index':
      view = 'index.html'
      break;

    case '/todo':
      view = 'todo.html'
      break;

    case '/404':
      view = '404.html'
      break;

    default:
      break;
  }
  let html = await render(view);
  return html;
}

app.use(aync(ctx) => {
  let url = ctx.request.url,
      html = await router(url);
  ctx.body = html;    
});

app.listen(3000);
console.log('[demo] route-simple is starting at port 3000');
运行demo
执行运行脚本(harmony模式):
  node -harmony index.js
运行结果:

访问http://localhost:3000/index

koa-router中间件

如果依靠ctx.request.url去手动处理路由,将会处理很多处代码,这时候就需要对应路由的中间件对路由进行控制,这里介绍一个比较好用的中间件koa-router。

安装koa-router中间件
//  koa2对应的版本是7.x
npm install --save koa-router@7
快速使用koa-router
const Koa = require('koa');
const fs = require('fs');
const app = new Koa();

const Router = require('koa-router');

let home = new Router();

//  子路由1
home.get('/', async(ctx) => {
  let html = `
    <ul>
      <li>
        <a href="/page/helloworld">
          /page/helloworld
        </a>
      </li>
      <li>
        <a href="/page/404">
          /page/404
        </a>
      </li>
    </ul>
  `;

  ctx.body = html;
});

// 子路由2
let page = new Router();
page.get('/404', async(ctx) => {
  ctx.body = '404.page!';
}).get('/helloworld', async(ctx) => {
  ctx.body = "helloworld page!";
});

//  装载所有子路由
let router = new Router();
router.use('/', home.routers(), home.allowedMethods()); 
router.use('/page', page.routes(), page.allowedMethods());

//  加载路由中间件
app.use(router.routes()).use(router.allowedMethods());

app.listen(3000, () => {
  console.log('[demo] route-use-middleware is starting at port 3000');
});

请求数据获取

GET请求数据获取

使用方法

在koa中,获取GET请求数据的源头是koa中request对象中的query方法或querystring方法。其中:query返回的是格式化好的参数对象,而与之对应querystring返回的是请求字符串,由于ctx对request的API有直接引用的方式,所以获取GET请求数据由两个途径:

    1. 是从上下文中直接获取:
    • 请求对象为ctx.query,返回如:{a:1, b:2}
    • 请求字符串ctx.querystring,返回如a=1&b=2
    1. 是从上下文的request对象中获取:
    • 请求对象ctx.request.query,返回如:{a:1, b:2}
    • 请求字符串ctx.request.querystring,返回如:a=1&b=2
实例代码
const Koa = require('koa');
const app = new Koa();

app.use(async(ctx) => {
  let url = ctx.url;
  //  从上细纹的request对象中获取
  let request = ctx.reuqet;
  let req_query = request.query;
  let req_querystring = request.querystring;
  //  从上下文中直接获取
  let request = ctx.request;
  let req_query = request.query;
  let req_querystring = request.querystring;
  //  从上下文直接获取
  let ctx_query = ctx.query;
  let ctx_querystring = ctx.querystring;

  ctx.body = {
    url,
    req_query,
    req_querystring,
    ctx_query,
    ctx_querystring
  }

  app.listen(3000, () => {
    console.log('[demo] request get is starting at port 3000');
  });
});
执行程序
node get.js

执行程序后,用chrome浏览器访问 http://localhost:3000/page/user?a=1&b=2会出现以下情况:
注意:chrome的json格式化插件,显示json的格式化形式。

{
  'url' : "/page/user?a=1&b=2",
  'req_query' : {
    "a" : "1",
    "b" : "2"
  },
  "req_querystring" : "a=1&b=2",
  "ctx_query" : {
    "a" : "1",
    "b" : "2"
  },
  "ctx_querystring" : "a=1&b=2"
}

POST请求数据获取

原理

对于POST请求的处理,koa2没有封装获取参数的方法,需要通过解析上下文context中的原生node.js请求对象req,将POST表单数据解析为query string(形式:a=1&b=2&c=3),再将query string解析成JSON格式(形式:{"a" : "1", "b" : "2", "c" : "3"})。
注意:ctx.request是context经过封装的请求对象,ctx.req是context提供的node.js原生HTTP请求对象,同理ctx.response是context经过封装的响应对象,ctx.res是context提供的node.js原生HTTP请求对象。

解析出POST请求上下文中的表单数据
//  解析上下文里node原声请求的POST参数
function parsePostData(ctx) {
  return new Promise((resolve, reject) => {
    try {
      let postdata = "";
      ctx.req.addListener('data', (data) => {
        postdata += data;
      });
      ctx.req.addListener('end', function() {
        let parseData = parseQueryStr(postdata);
        resolve(parseData);
      });
    } catch(err) {
      reject(err);
    }
  })
}

//  将POST请求参数字符串解析为JSON
function parseQueryStr(queryStr) {
  let queryData = {};
  let queryStrList = queryStr.split('&');
  console.log(queryStrList);
  for(let [index, queryStr] of queryStrList.entries()) {
    let itemList = queryStr.split('=');
    queryData[itemList[0]] = decodeURIComponent(itemList[1]);
  }
  return queryData;
}
实例代码
const Koa = require('koa');
const app = new Koa();

app.use(async(ctx) => {
  if(ctx.url === '/' && ctx.method === 'GET') {
    //  当GET请求时返回表单页面
    let html = `
      <h1>koa2 request post demo</h1>
      <form method="POST" action="/">
        <P>userName</P> 
        <input name="userName" /><br/>
        <P>nickName</P> 
        <input name="nickName" /><br/>
        <P>email</P> 
        <input name="email" /><br/>
        <button type="submit">submit</button> 
      </form>
    `;
    ctx.body = html;
  } else if (ctx.url === '/' && ctx.method === 'POST') {
    //  当POST请求的时候,解析POST表单里的数据,并显示出来
    let postData = await paresePostData('ctx'),
        ctx.body = postData;
  } else {
    //  其他请求显示404
    ctx.body = '<h1>404!!! o(╯□╰)o</h1>';
  }
});

//  解析上下文里的node原生请求的POST参数
function paresePostData(ctx) {
  return new Promise((resolve, reject) => {
    try {
      let postData = "";
      ctx.req.addListener('data', (data) => {
        postData += data;
      });
      ctx.req.addListener('end', function(){
        let parseData = pareseQueryStr(postData);
        resolve(parseData);
      });
    } catch(err) {
      reject(err);
    }
  });
}

// 将POST请求参数字符串解析为JSON
function parseQueryStr(queryStr) {
  let queryData = {};
  let queryStrList = queryStr.split('&');
  console.log(queryStrList);
  for(let [index, queryStr] of queryStrList.entries()) {
    let itemList = queryStr.split('=');
    queryData[itemList[0]] = decodeURIComponent(itemList[1]);
  }
  return queryData;
}

app.listen(3000, () => {
  console.log('[demo] request post is starting at port 3000');
});
启动例子
node post.js

访问页面http://localhost:3000

提交页面
UserName
koajs
nickName
noder
email
123@example.com
按钮submit
提交表单发起POST请求结果显示
{
    "userName": "koajs",
    "nickName": "noder",
    "email": "123@example.com"
}

koa-bodyparser中间件

原理

对于POST请求的处理,koa-bodyparser中间件可以把koa2上下文的formData数据解析到ctx.request.body中。

安装koa2版本的koa-bodyparse@3中间件
npm install --save koa-bodyparser@3
代码实例
const Koa = require('koa');
const app = new Koa();
const bodyParser = require('koa-bodyparser');

//  使用ctx.body解析中间件
app.use(bodyParser());

app.use(async(ctx) => {
  if(ctx.url === '/' && ctx.method === 'GET') {
    //  当GET请求时候返回表单页面
    let html = `
      <h1>koa2 request post demo</h1>
      <form method="POST" action="/">
        <p>userName</p>
        <input name="userName" /><br/>
        <p>nickName</p>
        <input name="nickName" /><br/>
        <p>email</p>
        <input name="email" /><br/>
        <button type="submit">submit</button>
      </form>
    `
    
    ctx.body = html;
  } else if(ctx.url === '/' && ctx.method === 'POST') {
    //  当POST请求的时候,中间件koa-bodyparser解析POST表单里的数据,并显示出来
    let postData = ctx.request.body,
        ctx.body = postData;
  } else {
    // 其他请求显示404
    ctx.body = '<h1>404!!! o(╯□╰)o</h1>';
  }
});

app.listen(3000, () => {
  console.log('[demo] request post is starting at port 3000');
});
启动例子
node post-middleware.js

访问页面http://localhost:3000

提交页面
UserName
koajs
nickName
noder
email
123@example.com
按钮submit
提交表单发起POST请求结果显示
{
    "userName": "koajs",
    "nickName": "noder",
    "email": "123@example.com"
}

静态资源加载

原生koa2实现静态资源服务器

前言

一个http请求访问web服务静态资源,一般响应结果有3种情况:

  • 访问文本,例如:js, css. png, jpg, gif
  • 访问静态目录
  • 找不到资源,抛出404错误
原生koa2实现静态资源服务器代码实例
代码目录
├── static # 静态资源目录
│   ├── css/
│   ├── image/
│   ├── js/
│   └── index.html
├── util # 工具代码
│   ├── content.js # 读取请求内容
│   ├── dir.js # 读取目录内容
│   ├── file.js # 读取文件内容
│   ├── mimes.js # 文件类型列表
│   └── walk.js # 遍历目录内容
└── index.js # 启动入口文件
代码解析
index.js
const Koa = require('koa');
const path = require('path');
const content = require('./util/content');
const mimes = require('..util.mimes');

//  静态资源目录相对于入口文件index.js的路径
const staticPath = './static';

//  解析资源类型
function parseMime(url) {
  let extName = path.extname(url),
      extName = extName ? extName.slice(1) : 'unknown';
  return  mimes[extName];
}

app.use(async(ctx) => {
  //  静态资源目录在本地的绝对路径
  let fullStaticPath = path.join(__dirname, staticPath);

  //  获取静态资源内容,有可能是文件内容,目录,或404
  let _content = await content(ctx, fullStaticPath);

  //  解析请求内容的类型
  let _mime = parseMime(ctx.url);

  //  如果有对应的文件类型,就配置上下文的类型
  if(_mime) {
    ctx.type = _mime;
  }

  // 输出静态资源内容
  if(_mime && _mime.indexOf('image/') >= 0) {
    //  如果是图片,则用node原生res,输出二进制数据
    ctx.res.writeHead(200);
    ctx.res.write(_content, 'binary');
    ctx.res.end();
  } else {
    //  其他输出文本
    ctx.body = _content;
  }
});

app.listen(3000);
console.log('[demo] static-server is starting at port 3000');
util/content.js
const path = require('path');
const fs = require('fs');

//  封装读取目录内容方法
const dir = require('./dir');

//  封装读取文件内容方法
const file = require('./file');

/**
 * 获取静态资源内容
 * @param {object} ctx koa上下文
 * @param {string} 静态资源目录在本地的绝对路径
 * @return {string} 请求获取到的本地内容
 */
async function content(ctx, fullStaticPath) {
  //  封装请求资源的绝对路径
  let reqPath = path.join(fullStaticPath, ctx.url);

  //  判断请求路径是否是存在的目录或文件
  let exist = fs.existsSync(reqPath);

  //  返回请求内容,默认为空
  let content = '';

  if(!exist) {
    //  如果请求路径不存在,返回404
    content = '404 Not Found! o(╯□╰)o!';
  } else {
    //  判断访问地址是文件夹还是文件
    let stat = fs.statSync(reqPath);

    if(stat.isDirectory()) {
      //  如果是目录,则读取目录内容  
      content = dir(ctx.url, reqPath);
    } else {
      //  如果请求内容为文件,则读取文件内容
      content = await file(reqpath);
    }
  }

  return content;
}

module.exports = content
util/dir.js
const url = require('url');
const fs = require('fs');
const path = require('path');

//  遍历读取目录内容方法
const walk = require('./walk');

/**
 * 封装目录内容
 * @param {string} url 当前请求的上下文中的url,即:ctx.url
 * @param {string} reqPath 请求静态资源的完整本地路径
 * @return {string} 返回目录内容,封装成HTML
 */
function dir(url, reqPath) {
  //  遍历读取当前目录下的文件,子目录
  let contentList = walk(reqPath);

  let html = `<ul>`;
  for(let [index, item] of contentList.entries()) {
    html = `${html}<li><a href="${url === '/' ? '' : url}">${item}</a></li>`
  }
  html = `${html}</ul>`;

  return html;
}

module.exports = dir;
util/file.js
const fs = require('fs');

/**
 * 读取文件方法
 * @param {string} 文件本地的绝对路径
 * @return {string|binary} 
 */
function file(filePath) {
  let content = fs.readFileSync(filePath, 'binary');
  return content;
};

module.exports = file;
util/walk.js
const fs = require('fs');
const mimes = require('./mimes');

/**
 * 遍历读取目录内容(子目录,文件名)
 * @param {string} reqPath请求资源的绝对路径
 * @return {array} 目录内容列表
 */

  function walk(reqPath) {
    let files = fs.readdirSync(reqPath);

    let dirList = [], 
        fileList = [];

    for(let i = 0, len = files.length; i < len; i++) {
      let item = files[i];
      let itemArr = items.split("\.");
      let itemMime = (itemArr.length > 1) ? itemArr[itemArr.length - 1] : "undefined";
    }    

    if(typeof mime[itemMime] === "undefined") {
      dirList.push(files[i]);
    } else {
      fileList.push(files[i]);
    }
  }

  let result = dirList.contact(fileList);

  return result;
};

module.exports = walk;
util.mime.js
let mimes = {
  'css' : 'text/css',
  'less' : 'text/css',
  'gif': 'image/gif',
  'html' : 'text/html',
  'ico' : 'image/x-icon',
  'jpeg' : 'image/jpeg',
  'jpg' : 'image/jpeg',
  'js' : 'text/javascript',
  'json' : 'application/json',
  'pdf' : 'application/pdf',
  'png' : 'image/png',
  'svg' : 'images/svg+xml',
  'swf' : 'application/x-shockwave-flash',
  'tiff' : 'image/tiff',
  'txt' : 'text/plain',
  'wav' : 'audio/x-wav',
  'wma' : 'audio/x-ms-wma',
  'wmv' : 'video/x-wmv',
  'xml' : 'text/xml'
}

module.exports = mimes;
运行效果
启动服务
node index.js
效果

访问http://localhost:3000

+ css
+ image
+ js
+ index.html

访问http://localhost:3000/index.html
页面加载静态资源:显示有样式的文字和图片。
访问http://localhost:3000/js/index.js

(function() {
 alert('hello koa2 static server');
 console.log('hello koa2 static server');
})
koa-static中间件使用
代码实例
const Koa = require('koa');
const path = require('path');
const static = require('koa-static');

const app = new Koa();

//  静态资源目录相对于入口文件index.js的路径
const staticPath = './static';

app.use(static(
  path.join(__dirname, staticPath);
));

app.use(async(ctx) => {
  ctx.body = "hello wrold"
});

app.use(async(ctx) => {
  console.log('[demo] static-use-middleware is starting at port 3000');
});
效果

访问http://localhost:3000

+ css
+ image
+ js
+ index.html

访问http://localhost:3000/index.html
页面加载静态资源:显示有样式的文字和图片。
访问http://localhost:3000/js/index.js

(function() {
 alert('hello koa2 static server');
 console.log('hello koa2 static server');
})

cookie/session

koa2使用cookie
使用方法

koa提供了从上下文直接读取,写入cookie的方法

  • ctx.cookies.get(name, [options])读取上下文请求中的cookie
  • ctx,cookies.set(name, value, [options])在上下文中写入cookie
    koa2中操作的cookies是使用了npm的cookies模块,源码在:https://github.com/pillarjs/cookies,所以在读写cookie的使用参数与该模块的使用一致。
实例代码
const Koa = require('koa');
const app = new Koa();

app.use(async(ctx) => {
  if(ctx.url === '/index') {
    ctx.cookies.set(
      'cid',
      'hello world',
      {
        domian: 'localhost',  //  写cookie所在的域名
        path: '/index', //  写cookie所在的路径
        maxAge: 10*60*1000, //  cookie有效时长
        expires: new Date('2017-02-15'),  //  cookie失效时间
        httpOnly: false,  //  是否只用于http请求获取
        overwrite: false  //  是否允许重写
      }
    )
    ctx.body = 'cookie is ok';
  } else {
    ctx.body = 'hello world';
  }
});


app.listen(3000, () => {
  console.log('[demo] cookie is starting at port 3000');
});
运行实例
执行脚本
node index.js
运行结果

访问http://localhost:3000/index

  • 可以在控制台的cookie列表中看到写在页面上的cookie
  • 在控制台的console中使用document.cookie可以打印出页面的所有cookie(需要是httpOnly设置false才能显示)
    浏览器显示:
cookie is ok

console控制台显示:

document.cookie
"cid=hello world"
koa2实现session
前言

koa2原生功能只提供了cookie的操作,但是没有提供session操作。session就只能用自己实现或者通过第三方中间件实现。在koa2中实现session的方案有以下几种:

  • 如果session数据量很小,可以直接存在内存中。
  • 如果session数据量很大,则需要存储介质存放session数据。
数据库存储方案
  • 将session存放在MYSQL数据库中
  • 需要用到中间件
    • koa-session-minimal适用于koa2的session中间件,提供存储介质的读写接口。
    • koa-mysql-session为koa-session-minimal中间件提供MYSQL数据库的session数据读写操作。
    • 将sessionId和对应的数据存到数据库。
  • 将数据库存储的sessionId存在页面的cookie中。
  • 根据cookie的sessionId去获取对应的session信息。
实例代码
const Koa = require('koa');
const session = require('koa-session-minimal');
const MysqlSession = require('koa-mysql-session');

const app = new Koa();

//  配置存储session信息的mysql
let store = new MysqlSession({
  user: 'root',
  password: 'abc123',
  database: 'koa_demo'
  host: '127.0.0.1'
});

// 存放sessionId的cookie配置
let cookie = {
  maxAge: '', //  cookie有效时长
  expires: '',  //  cookie失效时间
  path: '', //  写cookie所在的路径
  domain: '', //  写cookie所在的域名
  httpOnly: '', //  是否只用于http请求中获取
  overwrite: '',  //  是否允许重写
  secure: '',
  samSite: '',
  signed: ''
}

//  使用session中间件
app.use(session({
  key: 'SESSION_ID',
  store: store,
  cookie: cookie
}));

app.use(async(ctx) => {
  //  设置session
  if(ctx.url === '/set') {
    ctx.session = {
      user_id: Math.random().toString(36).substr(2),
      count: 0
    }
    ctx.body = ctx.session;
  } else if(ctx.url === '/') {
    //  读取session信息
    ctx.session.count = ctx.session.count + 1;
    ctx.body = ctx.session;
  }
});

app.listen(3000);
console.log('[demo] session is starting at port 3000');
运行实例
执行命令
node index.js
访问连接设置session

浏览器访问:http://localhost:3000/set
浏览器显示:

{
  "user_id": "eejxr0uwwoa0f9tyjjjbkv5cdi",
  "count": 0
}
查看数据库session是否存储
mysql> use koa_demo;
mysql> show tables;
mysql> select * from _mysql_session_store;
查看cookie中是否种下了sessionId

浏览器访问: http://localhost:3000

{
  "user_id": "eejxr0uwwoa0f9tyjjjbkv5cdi",
  "count": 3
}

在chrome开发者模式的Application下查看Storage栏下的Cookies下的http://localhost:3000数据,可以看到:

Name       Value       Domain     Path   Expires/Max-length  Size 
SESSION_ID rj1cBC...   localhost    /         Session                    42

模板引擎

koa2加载模板引擎

安装模块
//  安装koa模板使用中间件
npm install --save koa-views

//  安装ejs模板引擎
npm install --save ejs
使用模板引擎
文件目录
├── package.json
├── index.js
└── view
    └── index.ejs
./index.js文件

const Koa = require('koa');
const views = require('koa-views');
const path = require('path');
const app = new Koa();

// 加载模板引擎
app.use(views(path.join(__dirname, './view'), {
extension: 'ejs'
}));

app.use(async(ctx) => {
let title = 'hello koa2';
await ctx.render('index', {
title
});
});

app.listen(3000);

ejs模板引擎

<!DOCTYPE html>
<html>
<head>
    <title><%= title %></title>
</head>
<body>
    <h1><%= title %></h1>
    <p>EJS Welcome to <%= title %></p>
</body>
</html>
ejs模板引擎

ejs官方文档:https://github.com/mde/ejs

文件上传

busboy模块
安装
npm install --save busboy
模块简介

busboy模块是用来解析POST请求中node原生req中的文件流的模块。

代码实例
const inspect = require('util').inspect;
const path = require('path');
const fs = require('fs');
const Busboy = require('busboy');

//  req为node原生请求
const busboy = new Busboy({headers: req.headers});

//  ...

//  监听文件解析事件
busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
  console.log(`File [${fieldname}]: filename : ${filename}`);

  //  文件保存在特定路径
  file.pipe(fs.createWriteStream('./upload'));

  //  开始解析文件流
  file.on('data', function(data){
    console.log(`File [${fieldname}] got ${data.length} bytes`);
  });

  //  解析文件结束
  file.on('end', function() {
    console.log(`File [${fieldname}] Finished`);
  });
});

//  监听请求中的字段
busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated) {
  console.log(`Field [${fieldname}]: value: ${inspect(val)}`);
});

//  监听结束事件  
busboy.on('finish', function() {
  console.log('Done parsing form!');
  res.writeHead(303, {Connection: 'close', Location: '/'});
  res.end();
});

req.pipe(busboy);
官方文档

busboy API:https://www.npmjs.com/package/busboy

上传文件简单实现

依赖模块
安装依赖
npm install --save busboy
代码实例

#######封装上传文件到写入服务的方法

const inspect = require('util').inspect;
const path = require('path');
const os = require('os');
const fs = require('fs');
const Busboy = require('busboy');

/**
 * 同步创建文件目录
 * @param {string} dirname 目录绝对地址
 * @return {boolean}  创建目录结果
 */
function mkdirsSync(dirname) {
  if(fs.existsSync(dirname)) {
    return true;
  } else {
    if(mkdirsSync(path.dirname(dirname))) {
      fs.mkdirSync(dirname);
      return true;
    }
  }
}

/**
 * 获取上传文件的后缀名
 * @param {string} fileName 获取上传文件的后缀名
 * @return {string} 文件后缀名
 */
function getStuffixName(fileName) {
  let nameList = fileName.split('.');
  return nameList[nameList.length - 1];
}

/**
 * 上传文件
 * @param {object} ctx koa上下文
 * @param {object} options 文件上传参数,fileType文件类型。path文件存放路径
 * @return {promise}
 */
function uploadFile(ctx, options) {
  let req = ctx.req;
  let res = ctx.res;
  let busboy = new Busyboy({headers: req.headers});

  //  获取类型
  let fileType = options.fileType || 'common';
  let filePath = path.join(options.path, fileType);
  let mkdirResult = mkdirsSync(filePath);

  return new Promise((resolve, reject) => {
    console.log('文件上传中...');
    let result = {
      success: false,
      formData: {}
    };

    //  解析请求文件事件
    busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
      let fileName = Math.random().toString(16).substr(2) + '.' + getStuffixName(filename);
      let _uploadFilePath = path.join(filePath, fileName);
      let saveTo = path.join(_uploadFilePath);

      //  文件保存到指定路径
      file.pipe(fs.createWriteStream(saveTo));
      
      //  文件写入事件结束
      file.on('end', function() {
        result.success = true;
        result.message = '文件上传成功';

        console.log('文件上传成功!');
        resolve(result);
      });
    });  
    //  解析表单中其他字段信息
    busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) {
      console.log('表单字段数据 [' + fieldname + ']: value: ' + inspect(val));
      result.formData[fieldname] = inspect(val);
    });
    //  解析结束事件
    busyboy.on('finish', function() {
      console.log('文件上传结束');
      resolve(result);
    });
    //  解析错误事件
    busboy.on('error', function(err) {
      console.log('文件上传出错');
      reject(result);
    });

    req.pipe(busboy);
  });
}

module.exports = {
  uploadFile
}
入口文件
const Koa = require('koa');
const path = require('path');
const app = new Koa();
//  const bodyParser = require('koa-bodyparser');

const {uploadFile} = require('./util/upload');

//  app.use(bodyParser());

app.use(async(ctx) => {
  if(ctx.url === '/' && ctx.method === 'GET') {
    //  当GET请求时候返回表单页面
    let html = `
      <h1>koa2 upload demo</h1>
      <form method="POST" action="/upload.json" enctype="multipart/form-data">
        <p>file upload</p>
        <span>picName:</span><input name="picName" type="text" /><br/>
        <input name="file" type="file" /><br/><br/>
        <button type="submit">submit</button>
      </form>
    `;
    ctx.body = html;
  } else if(ctx.url === '/upload.json' && ctx.method === 'POST') {
    // 上传文件请求处理
    let result = {success: false};
    let serverFilePath = path.join(__dirname, 'upload-files');

    //  上传文件事件
    result = await uploadFile(ctx, {
      fileType: 'album',  //  common or album
      path: serverFilePath
    });

    ctx.body = result;
  } else {
    //  其他请求显示404
    ctx.body = '<h1>404!!! o(╯□╰)o</h1>'
  }
});

app.listen(3000, ()=>{
  console.log('[demo] upload-simple is starting at port 3000');
});
运行结果

浏览器访问http://localhost:3000

http://localhost:3000/upload.json
{
  'success': true,
  'formData': {
    'picName': "'hello world'";
  },
  'message': '文件上传成功';
}

异步上传图片实现

源码实例
├── index.js # 后端启动文件
├── node_modules
├── package.json
├── static # 静态资源目录
│   ├── image # 异步上传图片存储目录
│   └── js
│       └── index.js # 上传图片前端js操作
├── util
│   └── upload.js # 后端处理图片流操作
└── view
    └── index.ejs # ejs后端渲染模板
后端代码

入口文件:demo/upload-async/index.js

const Koa = require('koa');
const views = require('koa-views');
const path = require('path');
const convert = require('koa-convert');
const static = require('koa-static');
const {uploadFile} = require('./util.upload');

const app = new Koa();

/**
 * 使用第三方中间件 start
 */
app.use(views(path.join(__dirname, './view'), {
  extension: 'ejs'
}));

// 静态资源目录对于相对于入口文件index.js的路径
const staticPath = './static';
// 由于koa-static目前不支持koa2
// 所以只能用koa-convert封装一下
app.use(convert(static(
  path.join(__dirname, staticPath);
)));

/**
 * 使用第三方中间件 end
 */
app.use(async(ctx) => {
  if(ctx.method === 'GET') {
    let title = 'upload pic async';
    await ctx.render('index', {
      title;
    });
  } else if(ctx.url === '/api/picture/upload.json' && ctx.method === 'POST') {
    // 上传文件请求处理
    let result = {success: false};
    let serverFilePath = path.join(__dirname, 'static/image');

    // 上传文件事件
    result = await uploadFile(ctx, {
      fileType: 'album',
      path: serverFilePath;
    });
    ctx.body = result;
  } else {
    // 其他请求显示404
    ctx.body = '<h1>404!!! o(╯□╰)o</h1>';
  }
});

app.listen(3000, () => {
  console.log('[demo] upload-pic-async is starting at port 3000');
});

后端上传图片流写操作 入口文件 demo/upload-async/util/upload.js

const inspect = reuqire('util').insepect;
const path = requie('path');
const os = require('os');
const fs = require('fs');
const Busboy = require('busboy');

/**
 * 同步创建文件目录
 * @param {string} dirname 目录绝对地址
 * @return {boolean} 创建目录结果
 */
function mkdirSync(dirname) {
  if(fs.existsSync(dirname)) {
    return true;
  } else {
    if(mkdirsSync(path.dirname(dirname))) {
      fs.mkdirSync(dirname);
      return true;
    }
  }
}

/**
 * 获取上传文件的后缀名
 * @param {string} filename 获取上传文件的后缀名
 * @return {string} 文件后缀名
 */
function getSuffixName(fileName) {
  let nameList = fileName.split('.');
  return nameList[nameList.length - 1];
}

/**
 * 上传文件
 * @param {object} ctx koa上下文
 * @param {object} options 文件上传参数: fileType文件类型,path文件存放路径
 * @return {promise} 创建目录结果
 */
function mkdirsSync(dirname) {
  if(fs.existsSync(dirname)) {
    return true;
  } else {
    if(mkdirsSync(path.dirname(dirname))) {
      fs.mkdirSync(dirname);
      return true;
    }
  }
}

/**
 * 获取上传文件的后缀名
 * @param {string} fileName 获取上传文件的后缀名
 * @return {string} 文件后缀名
 */
function getSuffixName(fileName) {
  let nameList = fileName.split('.');
  return nameList[nameList.length - 1];
}

/**
 * 上传文件
 * @param {object} ctx  koa上下文
 * @param {object} options 文件上传参数 fileType文件类型,path文件存放路径
 * @return {promise} 
 */
function uploadFile(ctx, options) {
  let req = ctx.req;
  let res = ctx.res;
  let busboy = new Busboy({headers: req.headers});

  //  获取类型
  let fileType = options.fileType || 'common';
  let filePath = path.join(options.path, fileType);
  let mkdirResult = mkdirsSync(filePath);

  return new Promise((reslove, reject) => {
    console.log('文件上传中...');
    let result = {
      success: false,
      message: '',
      data: null
    }

    //  解析请求文件事件
    busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
      let fileName = Math.random().toString(16).substr(2) + '.' + getSuffixName(filename);
      let _uploadFilePath = path.join(filePath, fileName);
      let saveTo = path.join(_uploadFilePath);

      //  文件保存到制定路径
      file.pipe(fs.createWriteStream(saveTo));

      //  文件写入事件结束
      file.on('end', function() {
        result.success = true;
        result.message = '文件上传成功';
        result.data = {
          pictureUrl: `//${ctx.host}/image/${fileType}/${fileName}`;
        }
        console.log('文件上传成功!');
        resolve(result);
      });
    });

    //  解析结束事件
    busboy.on('finish', function() {
      console.log('文件上结束');
      resolve(result);
    });

    //  解析错误事件
    busboy.on('error', function(err) {
      console.log('文件上出错');
      reject(result);
    });

    req.pipe(busboy);
  });
}

module.exports = {
  uploadFile
}
前端代码

页面代码:

<buttton class="btn" id="J_UploadPictureBtn">上传图片</buttton>
<hr/>
<p>上传进度<span id="J_UploadProgress">0</span>%</p>
<p>上传结果图片</p>
<div id="J_PicturePreview" class="preview-picture">
</div>
<script src="/js/index.js"></script>

上传操作代码:

(function() {
  let btn = document.getElementById('J_UploadPictureBtn');
  let progressElem - document.getElementById('J_UploadProgress');
  let previewElem = document.getElementById('J_PicturePreview');

  btn.addEventListener('click', function() {
    uploadAction({
      success: function(result) {
        console.log(result)
        if(result && result.success && result.data && result.data.pictureUrl) {
          previewElem.innerHTML = '<img src="'+ result.data.pictureUrl +'" style="max-width: 100%">';
        }
      },
      progress: function(data) {
        if(data && data * 1 > 0) {
          progressElem.innerText = data;
        }
      }
    });
  });

  /**
   * 类型判断
   * @tyoe {Object}
   */
  let UtilType = {
    isPrototype: function(data) {
      return Object.prototype.toString.call(data).toLowerCase();
    },
    isJSON: function(data) {
      return this.isPrototype(data) === '[object object]';
    },
    isFunction: function(data) {
      return this.isPrototype(data) === '[object function]';
    }
  }

  /**
   * form表单上传请求事件
   * @param {object} options 请求参数
   */
  function requestEvent(options) {
    try {
      let formData = options.formData;
      let xhr = new XMLHttpRequest();
      xhr.onreadystatechange = function() {
        if(xhr.readyState === 4 && xhr.status === 200) {
          option.success(JSON.parse(xhr.responseText));
        }
      }
      xhr.upload.onprogress = function(evt) {
        let loaded = evt.loaded;
        let tot = evt.total;
        let per = Math.floor(100 * loaded/tot);
        options.progress(per);
      }
      xhr.open('post', '/api/picture/upload.json');
      xhr.send(formData);
    } catch(err) {
      options.fail(err);
    }
  }

  /**
   * 上传事件
   * @param {object} options 上传参数
   */
  function uploadEvent(options) {
    let file;
    let formData = new formData();
    let input = document.createElement('input');
    input.setAttribute('type', 'file');
    input.setAttribute('name', 'files');

    input.click();
    input.onchange = function() {
      file = input.files[0];
      formData.append('files', file);

      requestEvent({
        formData,
        success: options.success,
        fail: options.fail,
        progress: options.progress
      });
    }
  }

  /**
   * 上传操作
   * @param {object} options 上传参数
   */
  function uploadAction(options) {
    if(!UtilType.isJSON(options)) {
      console.log('upload options is null');
    };
    let _options = {};
    _options.success = UtilType.isFunction(options.success) ? options.success : function() {};
    _options.fail = UtilType.isFunction(options.fail) ? options.fail :function() {};
    _options.progress = UtilType.isFunction(options.progress) ? options.progress : function() {};

    uploadEvent(_options);
  }
})();
运行结果

访问浏览器http://localhost:3000
获得上传图片页面

mysql模块

安装MySQL数据库

https://www.mysql.com/downloads/

安装node.js的mysql模块
npm install --save mysql
模块介绍

mysql模块是node操作MYSQL的引擎,可以再node,js环境下对MySQL数据库进行建表,增,删,改,查等操作。

创建数据库会话
const mysql = require('mysql');
const connection = mysql.createConnection({
  host: '127.0.0.1',  // 数据库地址
  user: 'root', //  数据库用户
  password: '123456'. //  数据库密码
  database: 'my_database' //  选中数据库
});

//  执行sql脚本对数据库进行读写
connection.query('SELECT * FROM my_table', (error, result, fields) => {
  if(error) throw error
  //  connected!

  //  结束会话
  connection.release();
});

注意:一个事件就有一个从开始到结束的过程,数据库会话操作执行完成后,就需要关闭掉,以免占用连接资源。

创建数据连接池

一般情况下操作数据库是很复杂的读写过程,不只是一个会话,如果直接用会话操作,就需要每次会话都配置连接参数。所以这时候就需要连接池管理会话。

const mysql = require('mysql');

//  创建数据池
const pool = mysql.createPool({
  host: '127.0.0.1',  // 数据库地址
  user: 'root', //  数据库用户
  password:'123456',  // 数据库密码
  database: 'my_database' //  选中数据库
});

// 在数据池中进行会话操作
pool.getConnection(function(err, connection) {
  connection.query('SELECT * FROM my_table', (error, results, fields) => {
    //  结束会话
    connection.release();

    //  如果有错误就抛出
    if(error) throw error;
  });
});
更多模块信息

关于mysql的详细API可以访问官方文档:
https://www.npmjs.com/package/mysql

async/await封装使用mysql

前言

由于mysql模块的操作都是异步操作,每次操作的结果都是在回调函数中执行的,现在有了async/await,就可以用同步的写法去操作数据库。

Promise封装mysql模块
Promise封装./async-db
const mysql = require('mysql');
const pool = mysql.createPool({
  host: '127.0.0.1',
  user: 'root',
  password: '123456'
  database: 'my_database'
});

let query = function(sql, values) {
  return new Promise((resolve, reject) => {
    pool.getConnection(function(err, connection) {
      if(err) {
        reject(err);
      } else {
        connection.query(sql, values, {err, rows} => {
          if(err) {
            reject(err);
          } else {
            resolve(rows);
          }
          connection.release();
        });
      }
    });
  });
}

module.exports = {query};
async/await
const {query} = require('./async-db');
async function selectAllData() {
  let sql = 'SELECT * FROM my_table';
  let dataList = await query(sql);
  return dataList;
}

async function getData() {
  let dataList = await selectAllData();
  console.log(dataList);
}

getData();

建表初始化

前言

通常初始化数据库要建立很多表,特别是在项目开发的时候,表的格式可能有些变动,这时就需要封装对数据库初始化的方法,保留项目的sql脚本文件,然后每次需要重新建表,则执行建表初始化程序就行。

代码实例

源码目录
├── index.js # 程序入口文件
├── node_modules/
├── package.json
├── sql   # sql脚本文件目录
│   ├── data.sql
│   └── user.sql
└── util    # 工具代码
    ├── db.js # 封装的mysql模块方法
    ├── get-sql-content-map.js # 获取sql脚本文件内容
    ├── get-sql-map.js # 获取所有sql脚本文件
    └── walk-file.js # 遍历sql脚本文件
具体流程
    +---------------------------------------------------+
       |                                                   |
       |   +-----------+   +-----------+   +-----------+   |
       |   |           |   |           |   |           |   |
       |   |           |   |           |   |           |   |
       |   |           |   |           |   |           |   |
       |   |           |   |           |   |           |   |
+----------+  遍历sql  +---+ 解析所有sql +---+  执行sql  +------------>
       |   |  目录下的  |   |  文件脚本  |   |   脚本     |   |
+----------+  sql文件   +---+   内容    +---+           +------------>
       |   |           |   |           |   |           |   |
       |   |           |   |           |   |           |   |
       |   |           |   |           |   |           |   |
       |   |           |   |           |   |           |   |
       |   +-----------+   +-----------+   +-----------+   |
       |                                                   |
       +---------------------------------------------------+

源码详解

数据库操作文件 ./util/db.js

···javascript
const mysql = require('mysql');

const pool = mysql.createPool({
host: '127.0.0.1',
user: 'root',
password: 'abc123',
database: 'koa_demo'
});

let query = function(sql, values) {
return new promise((resolve, reject) => {
pool.getConnection(function(err, connection) {
if(err) {
reject(err);
} else {
connection.query(sql, values, (err, rows) => {
if(err) {
reject(err);
} else {
resolve(rows);
}
connection.release();
});
}
});
});
}

module.exports = {
query
};

######获取所有sql脚本内容 ./util/get-sql-content-map.js
```javascript
const fs = require('fs');
const getSqlMap = require('./get-sql-map');

let sqlContentMap = {};

/**
 * 读取sql文件内容
 * @param {string} filename 文件名称
 * @param {string} path 文件所在的目录
 * @return {string} 脚本文件内容
 */
function getSqlContent(fileName, path) {
  let content = fs.readFileSync(path, 'binary');
  sqlContentMap[fileName] = content; 
}

/**
 * 封装所有sql文件脚本内容
 * @return {object}
 */
function getSqlContentMap() {
  let sqlMap = getSqlMap();
  for(let key in sqlMap) {
    getSqlContent(key, sqlMap[key]);
  }

  return sqlContentMap;
}

module.exports = getSqlContentMap;
获取sql目录详情 ./util/get-sql-map.js
const fs = require('fs');
const walkFile = require('./walk-file');

/**
 * 获取sql目录下的文件目录数据
 * @return {object}
 */
function getSqlMap() {
  let basePath = __dirname;
  basePath - basePath.replace(/\\/g, '\/');

  let pathArr = basePath.split('\/');
  pathArr = pathArr.splice(0, pathArr.length - 1);
  basePath = pathArr.join('/') + '/sql/';

  let fileList = walkFile(basePath, 'sql');
  return fileList;
}

module.exports = getSqlMap;
遍历目录操作 ./util/walk-file.js
const fs = require('fs');

/**
 * 遍历目录下的文件目录
 * @param {string} pathResolve 需要进行便利的目录路径
 * @param {string} mime 建立文件的后缀名
 * @param {object} 返回便利后的目录结果
 */
const fs = require('fs');

/**
 * 遍历目录下的文件目录
 * @param {string} pathResolve 需要进行遍历的目录路径
 * @param {string} mime 便利文件的后缀名
 * @param {string} 返回遍历后的目录结果
 */
const walkFile = function(pathResolve, mime) {
  let files = fs.readdirSync(pathResolve); 
  let fileList = {};

  for(let [i, item] of files.entries()) {
    let itemArr = item.split('\.');

    let itemMime = (itemArr.length > 1) ? itemArr[itemArr.length - 1] : 'undefined';
    let keyName = item + '';
    if(mime === itemMime) {
      fileList[item] = pathResolve + item;
    }
  }

  return fileList;
}

module.exports = walkFile;
入口文件 ./index.js
const fs = require('fs');
const getSqlContentMap = require('./util/get-sql-content-map');
const {query} = require('./util/db');

// 打印脚本执行日志
const eventLog = function(err, sqlFile, index) {
  if(err) {
    console.log(`[ERROR] sql脚本文件: ${sqlFile} 第${index + 1}条脚本 执行失败 o(╯□╰)o !`);
  } else {
    console.log(`[SUCCESS] sql脚本文件: ${sqlFile} 第${index + 1}条脚本 执行成功 O(∩_∩)O !`);    
  }
}

//  获取所有sql脚本内容
let sqlContentMap = getSqlContentMap();

//  执行建表sql脚本
const createAllTables = async () => {
  for(let key in sqlContentMap) {
    let sqlShell = sqlContentMap[key];
    let sqlShellList = sqlShell.split(';');

    for(let [i, shell] of sqlShellList.entries()) {
      if(shell.trim()) {
        let result = await query(shell);
        if(result.serverStatus * 1 === 2) {
          eventLog(null, key, i);
        } else {
          eventLog(true, key, i);
        }
      }
    }
  }
  console.log('sql脚本执行结束!');
  console.log('请按ctrl + c键退出!');
}

createAllTables();
sql脚本文件 ./sql/data.sql
CREATE TABLE IF NOT EXISTS 'data' {
  'id': int(11) NOT NULL AUTO_INCREMENT,
  'data_info': json DEFAULT NULL,
  'create_time': varchar(20) DEFAULT NULL;
  'modified_time': varchar(20) DEFAULT NULL;
  'level': int(11) DEFAULT NULL,
  PRIMARY KEY ('id')
} ENGINE=InnoDB DEFAULT CHARSET=utf8
sql脚本文件 ./sql/user.sql
CREATE TABLE IF NOT EXISTS `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `email` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  `nick` varchar(255) DEFAULT NULL,
  `detail_info` json DEFAULT NULL,
  `create_time` varchar(20) DEFAULT NULL,
  `modified_time` varchar(20) DEFAULT NULL,
  `level` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf-8;

INSERT INTO 'user' set email=`1@example.com`, password=`123456`;
INSERT INTO 'user' set email=`2@example.com`, password=`123456`;
INSERT INTO 'user' set email=`3@example.com`, password=`123456`;
执行脚本
node index.js
执行结果

在终端上sql脚本依次执行成功的消息,逐条显示。

查看数据库写入数据
use koa_demo;
show tables;
select * from user;

原生koa2实现jsonp

前言

在项目复杂的业务场景下,有时候需要在前端跨域获取数据,这时候提供数据的服务就需要提供跨域请求的接口,通常是JSONP方式提供跨域接口。

实现JSONP
具体原理
//  判断是否为JSONP的请求
if(ctx.method === 'GET' && ctx.url.split('?')[0] === '/getData.jsonp') {
  //  获取jsonp的callback
  let callbackName = ctx.query.callback || 'callback';
  let returnData = {
    success: true,
    data: {
      text: 'this is a jsonp api',
      time: new Date().getTime()
    }
  }

  // jsonp的script字符串
  let jsonpStr = `;${callbackName}(${JSON.stringify(returnData)})`;
  
  // 用text/javascript,让请求支持跨域获取
  ctx.type = 'text/javascript';

  // 输出jsonp字符串
  ctx.body = jsonStr;
}
解析原理
  • JSONP跨域输出的数据是可执行的JavaScript代码
    • ctx输出的类型应该是'text/javascript'
    • ctx输出的内容为可执行的返回数据JavaScript代码字符串
  • 需要有回调函数名callbackName,前端获取后会通过动态执行JavaScript代码字符,获取里面的数据。
效果截图
同域访问JSON请求

浏览器访问:http://localhost:3000/getData.jsonp

;callback({"success":true,"data":{"text": "this is a jsonp api", "time": 1488203282385}});
跨域访问JSON请求

浏览器访问:www.sina.com.cn
chrome浏览器终端,键入:

$.ajax({
  url: 'http://localhost:3000/getData.jsonp',
  type: 'GET',
  dataType: 'JSONP',
  success: function(res) {
    console.log(res);
  }
});

结果:

Object {readyState: 1}
Object
    data: Object
      text: "this is a jsonp api"
      time: 1488204285023
      __proto__ : Object
    success: true
    __proto__ : Object
实例代码
const Koa = require('koa');
const app = new Koa();

app.use(async(ctx)) => {
  //  jsonp的请求为GET
  if(ctx.method === 'GET' && ctx.url.split('?')[0] === '/getData.jsonp') {
    //  获取jsonp的callback
    let callbackName = ctx.query.callback || 'callback';
    let returnData = {
      success: true,
      data: {
        text: 'this is a jsonp api',
        time: new Date().getTime()
      }
    }

    // jsonp的script字符串 
    let jsonpStr = `;${callbackName}(${JSON.stringify(returnData)})`;
    
    // 用text/javascript,让请求支持跨域获取
    ctx.type = 'text/javascript';
  
    // 输出jsonp字符串
    ctx.body = jsonpStr;

  } else {

    ctx.body = 'hello jsonp';
    
  }
});    

app.listen(3000, () => {
  console.log(`[demo] jsonp is starting at port 3000`);
});

koa-jsonp中间件

koa.js官方wiki中介绍了koa-jsonp中间件,它支持koa2,使用方式简单。

安装
npm install --save koa-jsonp
代码实例
const Koa = require('koa');
const jsonp = require('koa-jsonp');
const app = new Koa();

//  使用中间件
app.use(jsonp());

app.use(async (ctx) => {
  let returnData = {
    success: true,
    data: {
      text: 'this is a jsonp api',
      time: new Date().getTime()
    }
  }

  // 直接输出JSON
  ctx.body = returnData;
});

app.listen(3000, () => {
  console.log('[demo] jsonp is starting at port 3000');
});

测试

单元测试

前言

测试是一个项目周期里必不可少的环节,开发者在开发过程中也是无时无刻不在进行“人工测试”,如果每次修改一点代码,都要牵一发动全身来手动测试关联接口,这样会禁锢生产力。为了解放大部分测试生产力,相关的测试框架应运而生,比较出名的有mocha,karma,jasmine等。虽然框架繁多,但是使用起来都是大同小异。

准备工作
安装测试相关框架
npm install --save-dev mocha chai supertest
  • mocha模块是测试框架
  • chai模块是用来进行测试结果断言的库
  • supertest模块是http请求测试库,用来请求API接口

测试实例

例子目录
├── index.js # api文件
├── package.json
└── test # 测试目录
    └── index.test.js # 测试用例
``
#####所需测试demo
```javascript
const Koa = require('koa');
const app = new Koa();

const server = async (ctx, next) => {
  let result = {
    success: true,
    data: null
  };

  if(ctx.method === 'GET') {
    if(ctx.url === '/getString.json') {
      result.data = 'this is string data';
    } else if(ctx.url === '/getNumber.json') {
      result.data = 123456;
    } else {
      result.success = false
    }
    ctx.body = result;
    next && next();
  } else if(ctx.method === 'POST') {
    if(ctx.url === '/postData.json') {
      result.data = 'ok';
    } else {
      result.success = false;
    }
    ctx.body = result;
    next && next();
  } else {
    ctx.body = 'hello world';
    next && next();
  }
};

app.use(server);

module.exports = app;

app.listen(3000, () => {
  console.log('[demo] test-unit is starting at port 3000');
});

启动服务后访问接口,看到以下数据:
http://localhost:3000/getString.json

{
  "success": true,
  "data": "this is string data"
}
开始写测试用例

demo/test-unit/test/index.test.js

const supertest = require('supertest');
const chai = require('chai');
const app = require('./../index');

const expect = chai.expect
const request = supertest(app.listen());

//  测试套件/组
describe('开始测试demo的GET请求', () => {
  //  测试用例
  it('开始测试demo的GET请求', (done) => {
    request
      .get('/getString.json')
      .expect(200)
      .end((err, res) => {
        //  断言判断结果是否是object类型
        expect(res.body).to.be.an('object');
        expect(res.body.success).to.be,an('boolean');
        expect(req.body.data).to.be.an('string');
        done();
      });
  });
});
执行测试用例
//  node.js <= 7.5.x
./node_modules/.bin/mocha --harmony

//  node.js = 7.6.0
./node_modules/.bin/mocha

注意:

  1. 如果是全局安装了mocha,可以直接在当前项目目录下执行mocha --harmony命令
  2. 如果当前node.js版本低于7.6,由于7.5.x以下还直接不支持async/await就需要加上--harmony
    会自动读取执行命令./test目录下的测试用例文件index.test.js,并执行,测试结果如下:
[demo] test-unit is starting at port 3000
  开始测试demo的GET请求
  测试/getString,json请求
  测试/getNumber,json请求

  开始测试demo的POST请求
  测试/postData.json请求
  
  3 passing (36ms)

用例详解

服务入口加载

如果要对一个服务的API接口,进行单元测试,要用supertest加载服务的入口文件

const supertest = require('supertest'); 
const request = supertest(app.listen());
测试套件、用例
  • describe()描述的是一个测试套件
  • 嵌套在describe()的it()是对接口进行自动化测试的测试用例
  • 一个describe()可以包含多个it()
describe('开始测试demo的GET请求', () => {
  it('测试/getString.json请求', () => {
    //TODO ...
  });
});
  • supertest封装服务request,是用来请求接口
  • chai.expect使用来判断测试结果是否与预期一样
    • chai断言有很多种方法,这里只是使用了数据型断言

开发debug

环境
  • node环境 8.x+
  • chrome 60+
启动脚本
node --inspect index.js
指令框显示

指令框就会出现以下字样:

Debugger listening on ws://127.0.0.1:9229/4c23c723-5197-4d23-9b90-d473f1164abe
For help see https://nodejs.org/en/docs/inspector

使用chrome浏览器调试server
访问:http://localhost:3000
打开浏览器调试窗口可以看到一个node.js的小logo

打开chrome浏览器的node调试窗口

在Sources中的node_modules文件夹下的index.js文件可以看到如下代码:

(function(exports, require, module, __filename, __dirname) {
  const Koa = require('koa');
  const app = new Koa();

  app.use(async (ctx) => {
    ctx.body = 'hello koa2';
  });

  app.listen(3000, () => {
    console.log('[demo] start-quick is starting at port 3000');
  });
});

打开了node的调试窗口后,原来绿色的node按钮变为灰色,同时调试框会显示debug状态

Debugger attached
可以自定义打断电了

可以在浏览器开发者模式下的Sources资源中的index.js中打调试断点了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容