摘录自: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快速开始
环境准备
- Node环境在版本v7.6.0以上开始完全支持async/await
- Node安装资源:
直接安装node.js 7.6:node.js官网地址https://nodejs.org
-
nvm管理多版本node.js:可以用nvm 进行node版本进行管理
- Mac系统安装nvm
https://github.com/creationix/nvm#manual-install
- windows系统安装nvm
https://github.com/coreybutler/nvm-windows
- Ubuntu系统安装nvm
- npm版本3.x以上
快速开始
安装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
运行结果:
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请求数据由两个途径:
-
- 是从上下文中直接获取:
- 请求对象为ctx.query,返回如:{a:1, b:2}
- 请求字符串ctx.querystring,返回如a=1&b=2
-
- 是从上下文的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
提交页面
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
提交页面
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
效果
+ 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');
});
效果
+ 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
运行结果
- 可以在控制台的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/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
注意:
- 如果是全局安装了mocha,可以直接在当前项目目录下执行mocha --harmony命令
- 如果当前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中打调试断点了。