由 Express 原班人马打造的 koa,致力于成为一个更小、更健壮、更富有表现力的 Web 框架。使用 koa 编写 web 应用,通过async function,可以免除重复繁琐的回调函数嵌套,并极大地提升常用错误处理效率。
安装
npm install koa
官网:https://koajs.com/
Hello world
const Koa = require('koa');
const app = new Koa();
// logger
app.use(async (ctx, next) => {
const start = new Date;
await next();
const ms = new Date - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});
// response
app.use(ctx => {
ctx.body = 'Hello Koa';
});
app.listen(3000);
浏览器输入:http://localhost:3000/
可以看到屏幕显示 Hello Koa
把数据库挂载到context上
app.context.db = db(假设这里是mysql);
app.use(async ctx => {
console.log(ctx.db);
});
原生路由
在koa中可以通过ctx.request.path获取用户请求的路径,我们可以将我们的body指向一个网页模板,通过fs.createReadStream()获取文件流
const Koa = require('koa');
const fs = require('fs');
const app = new Koa();
const main = ctx => {
ctx.response.type = 'html';
if(ctx.request.path === '/') {
ctx.response.body = fs.createReadStream('./index.html');
}
else {
ctx.response.body = '页面未找到';
}
}
app.use(main);
app.listen(3000);
静态资源
如果是一个静态网站,比如它在static目录下
const Koa = require('koa');
const app = new Koa();
const path = require('path');
const static = require('koa-static');
const main = static(path.join(__dirname, 'static'));
app.use(main);
app.listen(3000);
错误ctx.throw()
const Koa = require('koa');
const app = new Koa();
const main = async function (ctx, next) {
ctx.throw(500)
// ctx.throw(400, 'name required');
};
app.use(main);
app.listen(3000);
const main = ctx => {
ctx.response.status = 404;// 等同于ctx.throw(404)
ctx.response.body = 'Page Not Found';
};
全局错误监听
app.on('error', err => {
console.error('server error', err)
});
设置
app.env默认值为"development"
app.proxy设为true开启代理
cookie
ctx.cookies.set('name', 'tobi')
console.log('ccc:',ctx.cookies.get('name'))
也可以设置过期时间,签名等(ctx.cookies.set(‘name’, ‘tobi’, { signed: true });),见官网:https://koajs.com/
const app = new Koa();
// key用于cookie签名,尽量设置复杂些
app.keys = ['token1','token2'];
...
ctx.cookies.set("jt", "abcd", {
signed: true,
});
router.get('/', async (ctx) => {
ctx.cookies.set('name', 'txj', {
// 设置过期时间
maxAge: 60*1000*60,
// 配置可以访问的页面
path: '/news',
// 当有多个子域名的时候使用,也可以不写,默认是请求的域名
domain: '.xxx.com',
// true 表示这个 cookie 只有服务器端可以方位,false 表示客户端也可以访问
httpOnly: true
})
})
在设置 jt 这个cookie的时候,koa会以 jt 的值 abcd 加上设置的密钥,生成校验值,并写入至 jt.sig 这个cookie中。后续的请求中,获取 jt 这个cookie时,则会根据 jt.sig 的值判断是否合法,安全性上又明显提升。
当生成cookie时,使用keys中的第一个元素来生成,而校验的时候,是从第一个至最后一个,一个个的校验,直到通过为止,所以在更新密钥的时候,只需要把新的密钥加到数组第一位则可以。
中间件链式调用
中间件决定响应请求,并希望绕过下游中间件可以简单地省略 next()
。通常这将在路由中间件中,但这也可以任意执行。例如,以下内容将以 “two” 进行响应,但是所有三个都将被执行,从而使下游的 “three” 中间件有机会操纵响应。
app.use(async function (ctx, next) {
console.log('>> one');
await next();
console.log('<< one');
});
app.use(async function (ctx, next) {
console.log('>> two');
ctx.body = 'two';
await next();
console.log('<< two');
});
app.use(async function (ctx, next) {
console.log('>> three');
await next();
console.log('<< three');
});
以下配置在第二个中间件中省略了next()
,并且仍然会以 “two” 进行响应,然而,第三个(以及任何其他下游中间件)将被忽略:
app.use(async function (ctx, next) {
console.log('>> one');
await next();
console.log('<< one');
});
app.use(async function (ctx, next) {
console.log('>> two');
ctx.body = 'two';
console.log('<< two');
});
app.use(async function (ctx, next) {
console.log('>> three');
await next();
console.log('<< three');
});
当最远的下游中间件执行 next();
时,它实际上是一个 noop 函数,允许中间件在堆栈中的任意位置正确组合。
从整体上看,就像一个洋葱,一层层调用,使用此特性,可以在核心业务的前面或者后面做一些公共处理逻辑。
将多个中间件与 koa-compose 相结合
有时您想要将多个中间件 “组合” 成一个单一的中间件,便于重用或导出。你可以使用 koa-compose
npm install koa-compose --save
const compose = require('koa-compose');
async function random(ctx, next) {
if ('/random' == ctx.path) {
ctx.body = Math.floor(Math.random() * 10);
} else {
await next();
}
};
async function backwards(ctx, next) {
if ('/backwards' == ctx.path) {
ctx.body = 'sdrawkcab';
} else {
await next();
}
}
async function pi(ctx, next) {
if ('/pi' == ctx.path) {
ctx.body = String(Math.PI);
} else {
await next();
}
}
const all = compose([random, backwards, pi]);
app.use(all);
安装路由
参考:https://github.com/koajs/router/blob/master/API.md
# npm ..
npm i @koa/router
# yarn ..
yarn add @koa/router
基本用法
const Koa = require('koa');
const Router = require('@koa/router');
const app = new Koa();
const router = new Router();
router.get('/', (ctx, next) => {
ctx.body = 'Hello router';
});
// 调用router.routes()来组装匹配好的路由,返回一个合并好的中间件
// 调用router.allowedMethods()获得一个中间件,当发送了不符合的请求时,会返回 `405 Method Not Allowed` 或 `501 Not Implemented`
app
.use(router.routes())
.use(router.allowedMethods());
统一加前缀
const router = new Router({
prefix: '/users'
});
// router.prefix('/users')
其他method
router.get("/users", async (ctx) => {
console.log('查询参数', ctx.query);
ctx.body = '获取用户列表';
})
.get("/:id", async (ctx) => {
const { id } = ctx.params
ctx.body = `获取id为${id}的用户`;
})
.post("/", async (ctx) => {
ctx.body = `创建用户`;
})
.put("/:id", async (ctx) => {
const { id } = ctx.params
ctx.body = `修改id为${id}的用户`;
})
.del("/:id", async (ctx) => {
const { id } = ctx.params
ctx.body = `删除id为${id}的用户`;
})
// 所有类型的请求,如get/post
.all("/users/:id", async (ctx) => {
ctx.body = ctx.params;
});
前端使用axios的get请求的参数较多时,使用$axios.get(‘indexData’, { params: { sex: 1, name:‘txj’ …} }),后端使用ctx.query.sex / ctx.query.name获取传参
传参
router.get('/:category/:title', (ctx, next) => {
console.log(ctx.params);
// => { category: 'programming', title: 'how-to-node' }
});
router.get("/users", async (ctx) => {
console.log('查询参数', ctx.query);
ctx.body = '获取用户列表';
})
路由嵌套
const forums = new Router();
const posts = new Router();
posts.get('/', (ctx, next) => {...});
posts.get('/:pid', (ctx, next) => {...});
forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods());
// responds to "/forums/123/posts" and "/forums/123/posts/123"
app.use(forums.routes());
可以使用这个功能把大路由拆分为多个子路由,比如:
const router = new Router();
const router1 = new Router();
...
const router2 = new Router();
...
const router3 = new Router();
...
router.get('/', (ctx, next) => {...});
router.use('/router1', router1.routes(), router1.allowedMethods());
router.use('/router2', router2.routes(), router2.allowedMethods());
router.use('/router3', router3.routes(), router3.allowedMethods());
app.use(router.routes());
上面的router1,router2,router3位于不同 的文件中
重定向
由center重定向至login
router.redirect('/center', 'login');
生成URL
// 给路由取个名字
router.get('user', '/users/:id', (ctx, next) => {
// ...
});
// 调用url方法使用路由名字生成链接
router.url('user', 3);
// => "/users/3"
router.url('user', { id: 3 });
// => "/users/3"
router.use((ctx, next) => {
// redirect to named route
ctx.redirect(ctx.router.url('sign-in'));
})
router.url('user', { id: 3 }, { query: { limit: 1 } });
// => "/users/3?limit=1"
router.url('user', { id: 3 }, { query: "limit=1" });
// => "/users/3?limit=1"
解析body json参数
koa无法解析http请求体中的数据,这时我们需要引入另外一个模块叫做koa-bodyparser。获取前端传的json参数或者form提交的参数
npm install koa-bodyparser
eg:
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const app = new Koa();
app.use(bodyParser());
app.use(async ctx => {
// the parsed body will store in ctx.request.body
// if nothing was parsed, body will be an empty object {}
ctx.body = ctx.request.body;
});
解析post请求
router.post('/', async (ctx, next)
// 通过 ctx.request.body 获取表单提交的数据
// 获取的 post 的 body 已经被转为对象
console.log(ctx.request.body)
})
session
浏览器访问服务器并发送第一次请求时,服务器端会创建一个 session 对象,生成一个类似于 key,value 的键值对, 然后将 key(cookie)返回到浏览器(客户)端,浏览器下次再访问时,携带 key(cookie),找到对应的 session(value)。 客户的信息都保存在 session 中。
npm install koa-session --save
参考:https://github.com/koajs/session
// 引入
const session = require('koa-session')
// 设置中间件
app.keys = ['some secret hurr']
const CONFIG = {
key: 'koa:sess', //cookie key (default is koa:sess)
maxAge: 86400000, // cookie 的过期时间 maxAge in ms (default is 1 days)
httpOnly: true, //cookie 是否只有服务器端可以访问 httpOnly or not (default true) signed: true, //签名默认 true
rolling: false, //在每次请求时强行设置cookie,这将重置cookie过期时间(默认:false)
renew: true, // 快过期的时候重新设置
}
app.use(session(CONFIG, app))
// 设置
router.get('/login', async (ctx) => {
// 设置 session
ctx.session.userInfo = 'zachary'
ctx.body = '登录成功'
})
// 获取
router.get('/', async (ctx) => {
console.log(ctx.session.userInfo)
})
原理就是在内存中生成一个 session 的散列表。每次生成一个随机的字符串,并将其作为 cookie 下发给客户端
更多koa中间件:https://github.com/koajs/koa/wiki#middleware
其他
node热更新
开发的时候每次修改都要手动重新 run 一次 node 会非常的麻烦。因此需要一个能自动重启的库的帮助。这个库就是 nodemon:
npm install -g nodemon
全局安装nodemon,然后启动的时候不要通过 node 了,而是 nodemon:
nodemon app.js
发送请求
使用koa 接收客户端请求。但是很多时候,我们需要对请求做转发。因此,需要使用 nodejs 发送请求。
发送请求可以使用库 request-promise。它是 request 的 promise 版本:
npm install --save request
npm install --save request-promise
# request 是 request-promise 的依赖项,需要自己手动安装
app.use(async (ctx, next) => {
// 先让路由响应
await next()
// 如果没有一个路由相应
if (ctx.status === 404) {
const url = `https://xxx.cn${ctx.url}`
try {
// 尝试请求转发
let obj = await request.get(url)
ctx.body = obj
} catch (err) {
// 转发请求发送失败
console.log('发生了error')
console.log(err.statusCode)
}
}
})
await
中发生的 error 需要通过 try…catch 捕获。
通过package配置环境变量
在scripts中添加start项,后面的值为启动app.js的命令
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "set NODE_ENV='production'&& node app.js"
},
在Mac和Linux上使用export, 在windows上export要换成set;如果想要同时兼容多种操作系统可以安装cross-env
这样我们在运行npm run start
的时候,就会自动设置NODE_ENV的值为production
console.log(process.env.NODE_ENV); // production
如果是多个变量:set NODE_ENV=‘development’ && set NODE_PORT=‘3000’ && node app.js
**敲黑板:**字符串比较的时候
// 这里一定要去空格,否则是有时相等有时又不相等,因为执行命令时后面的空格也会带上
const isProduction = process.env.NODE_ENV.trim() === 'production'
如果不想代码里去空格,则要如下配置(或者&&的前方不要有空格)
"scripts": {
"start": "set \"NODE_ENV=production\" && node main.js"
}
设置环境变量
node中的环境变量在 process.env
中保存,使用库 cross-env
可以方便设置兼容 windows 的环境变量:
npm install --save-dev cross-env
然后在package.json配置
"scripts": {
"start": "cross-env NODE_ENV='production' ttt='txj' node main.js",
"dev": "nodemon main.js"
}
......
const app = new Koa();
......
console.log('-----env:',app.env) // production
console.log(process.env.NODE_ENV) // production
console.log(process.env.ttt) // txj
环境配置文件dotenv
Dotenv是一个零依赖模块,它将环境变量从.env文件加载到process.env中
npm install dotenv --save
这个.env文件不要提交至git/svn,放在根目录
# .env file
#
# Add environment-specific variables on new lines in the form of NAME=VALUE
#
DB_HOST=localhost
DB_USER=root
DB_PASS=s1mpl3
读取环境变量
// index.js
require('dotenv').config()
console.log(process.env) // remove this after you've confirmed it working
如果需要指定目录:require(‘dotenv’).config({ path: ‘.env’ })
允许跨域
需要借助第三方的库 koa-cors
进行允许跨域设置:
npm install koa-cors
app.use(cors({
origin: function (ctx) {
if (ctx.url === '/cors') {
return '*' // 允许来自所有域名请求
}
return 'http://localhost:3000'
},
exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],
credentials: true,
allowMethods: ['GET', 'POST', 'DELETE'], // 设置允许的HTTP请求类型
allowHeaders: ['Content-Type', 'Authorization', 'Accept']
}))
其中origin字段对应于 access-control-allow-origin
,通过它设置哪些站点发起哪些请求可以进行跨域。请求路径为 /cors
的这个请求可以进行跨域,但是不能获取 cookie 信息,而 http://localhost:3000
这个域名下的所有请求都可以进行跨域,且可以获取 cookie 信息,是受信的站点。
如果设置了 access-control-allow-origin
为 *
,那么就是允许跨域了。但是跨域的客户端请求,无法携带该域名下的 cookie 信息给服务端。
必须设置 access-control-allow-origin
为一个特定的域名,而不是 *
。这样 Access-Control-Allow-Credentials
才会被默认置位 true,才可以跨域使用 cookie
redis
npm install redis --save
pm2
让node应用在后台运行
进程守护,系统奔溃自动重启;启动多进程,充分利用 CPU 和内存;自带日志记录
npm install pm2 -g
启动
pm2 start app.js
开发环境下使用 nodemon
,线上环境可以使用 pm2
;
常用命令:
# 查看进程列表
pm2 list
# 重启进程
pm2 restart <AppName>/<id>
# 停止进程
pm2 stop <AppName>/<id>
# 删除进程
pm2 delete <AppName>/<id>
# 查看进程信息
pm2 info <AppName>/<id>
# 查看日志(console.log/error/warn)
pm2 log <AppName>/<id>
# 监控
pm2 monit <AppName>/<id>
npm命令:
npm run dev 等同于 pm2 start npm -- run dev
npm start 等同于 pm2 start npm -- start
# 命名进程名
pm2 start npm --name test -- run dev
pm2 start npm --name test -- start
监听:
语法:pm2 start npm --watch --name – run ;
#其中 – watch监听代码变化,-- name重命名任务名称,-- run后面跟脚本名字
执行命令安装完pm2之后可能会报错【-bash: pm2: command not found】
这个时候需要手动创建软链接
先看下环境变量path在哪
# echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/usr/local/java/jdk1.8.0_40/bin:/usr/local/java/jdk1.8.0_40/jre/bin:/usr/local/ffmpeg/:/usr/local/apache-maven-3.3.9//bin:/root/bin
然后执行ln -s 【pm2的安装目录]】 【path目录,比如:/usr/local/sbin】
# 要注意path环境变量是在后面
ln -s /usr/local/node-v10.15.3-linux-x64/bin/pm2 /usr/local/sbin
这个时候在执行 pm2 list
pm2 list
-------------
__/\\\\\\\\\\\\\____/\\\\____________/\\\\____/\\\\\\\\\_____
_\/\\\/\\\_\/\\\\\\________/\\\\\\__/\\\///\\\___
_\/\\\_______\/\\\_\/\\\//\\\____/\\\//\\\_\///______\//\\\__
_\/\\\\\\\\\\\\\/__\/\\\\///\\\/\\\/_\/\\\___________/\\\/___
_\/\\\/____\/\\\__\///\\\/___\/\\\________/\\\//_____
_\/\\\_____________\/\\\____\///_____\/\\\_____/\\\//________
_\/\\\_____________\/\\\_____________\/\\\___/\\\/___________
_\/\\\_____________\/\\\_____________\/\\\__/\\\\\\\\\\\\\\\_
_\///______________\///______________\///__\///__