Nodejs学习记录: koa2
koa list in github : https://github.com/topics/koa
异常处理
并发
async / await
特点
- 让异步逻辑用同步写法实现
- 最底层的await返回需要是Promise对象
- 可以通过多层 async function 的同步写法代替传统的callback嵌套
function getSyncTime() { return new Promise((resolve, reject) => { try { let startTime = new Date().getTime() setTimeout(() => { let endTime = new Date().getTime() let data = endTime - startTime resolve( data ) }, 500) } catch ( err ) { reject( err ) } }) } async function getSyncData() { let time = await getSyncTime() let data = `endTime - startTime = ${time}` return data } async function getData() { let data = await getSyncData() console.log( data ) } getData()
了解更多异步编程,可以戳鲸鱼之前的笔记Nodejs学习记录:异步编程
cookie
HTTP 请求都是无状态的,但是我们的 Web 应用通常都需要知道发起请求的人是谁。为了解决这个问题,HTTP 协议设计了一个特殊的请求头:Cookie。服务端可以通过响应头(set-cookie)将少量数据响应给客户端,浏览器会遵循协议将数据保存,并在下次请求同一个服务的时候带上(浏览器也会遵循协议,只在访问符合 Cookie 指定规则的网站时带上对应的 Cookie 来保证安全性)。
cookie经常用于做登录信息的储存,当然我们在后端经常喜欢用它,在前端的单页应用一般喜欢用localstorage
通过 ctx.cookies
,我们可以在 controller 中便捷、安全的设置和读取 Cookie。
来个简单的案例,看看如何写入cookie
koa提供了从上下文直接读取、写入cookie的方法
- ctx.cookies.get(name, [options])
读取上下文请求中的cookie
- ctx.cookies.set(name, value, [options])
在上下文中写入cookie
设置Cookie其实是通过在HTTP响应中设置set-cookie头完成,每个set-cookie都会让浏览器在Cookie中存一个键值对。在设置Cookie值同时,协议还支持许多参数来配置这个Cookie的传输、储存和权限
{Number} maxAge:
设置这个键值对在浏览器的最长保存时间。是一个从服务器当前时刻开始的毫秒数。{Date} expires:
设置这个键值对的失效时间,如果设置了 maxAge,expires 将会被覆盖。如果 maxAge 和 expires 都没设置,Cookie 将会在浏览器的会话失效(一般是关闭浏览器时)的时候失效。{String} path:
设置键值对生效的 URL 路径,默认设置在根路径上(/),也就是当前域名下的所有 URL 都可以访问这个 Cookie。{String} domain:
设置键值对生效的域名,默认没有配置,可以配置成只在指定域名才能访问。{Boolean} httpOnly:
设置键值对是否可以被 js 访问,默认为 true,不允许被 js 访问。{Boolean} secure:
设置键值对只在 HTTPS 连接上传输,框架会帮我们判断当前是否在 HTTPS 连接上自动设置 secure 的值。{Boolean} overwrite
感兴趣的可以看看 cookie的实现源码
koa2 中操作的cookies是使用了npm的cookies模块,所以在读写cookie的使用参数与该模块的使用一致。
源码在:https://github.com/pillarjs/c...
const Koa = require('koa') const app = new Koa() app.use(async(ctx) => { if(ctx.url === '/index'){ ctx.cookies.set( 'cid', 'hello world', { domain: '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') })
在设置 Cookie 时我们需要思考清楚这个 Cookie 的作用,它需要被浏览器保存多久?是否可以被 js 获取到?是否可以被前端修改?
访问http://localhost:3000/index
- 可以在控制台的cookie列表中中看到写在页面上的cookie
- 在控制台的console中使用document.cookie可以打印出在页面的所有cookie(需要是httpOnly设置false才能显示)
更改下代码
const Koa = require('koa') const app = new Koa() app.use(async(ctx) => { if(ctx.url === '/index'){ ctx.cookies.set( 'cid', 'hello world', { domain: '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 { if(ctx.cookies.get('cid')){ ctx.body= ctx.cookies.get('cid'); }else { ctx.body = 'cookie is none'; } } }) app.listen(3000, () => { console.log('[demo] cookie is starting at port 3000') })
重启服务器node cookie.js
,浏览器分别输入http://localhost:3000/index/
http://localhost:3000
http://localhost:3000/index/aa
因为我们配置了
{path:'/index'}
session
Cookie 在 Web 应用中经常承担标识请求方身份的功能,
但是cookie信息会被储存在浏览器本地或硬盘中,这样会有安全问题,如果有人能够访问你的电脑就能分析出你的敏感信息,用户名、密码等等。为了解决这个隐患,所以 Web 应用在 Cookie 的基础上封装了 Session 的概念,专门用做用户身份识别。
既然服务器渲染又需要用户登录功能,那么用session去记录用户登录态是必要的
koa2原生功能只提供了cookie的操作,但是没有提供session操作。session就只能自己实现或者通过第三方中间件实现。
但是基于koa的egg.js框架内置了 Session 插件,给我们提供了 ctx.session 来访问或者修改当前用户 Session 。----> cookie & session
在koa2中实现session的方案有:
- 如果session数据量很小,可以直接存在内存中
- 如果session数据量很大,则需要存储介质存放session数据
数据库存储方案
储存在MySQL
需要用到中间件:
- koa-session
- koa-session-minimal
适用于koa2 的session中间件,提供存储介质的读写接口 。
- koa-mysql-session
为koa-session-minimal中间件提供MySQL数据库的session数据读写操作。
然后,我们将将sessionId和对应的数据存到数据库
将数据库的存储的sessionId存到页面的cookie中
根据cookie的sessionId去获取对于的session信息
在mysql数据库创建 Koa_session_demo数据库
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: 'wyc2016', database: 'koa_session_demo', host: '127.0.0.1', }) //存放sessionId的cookie配置 let cookie = { maxAge: '',// cookie有效时长 expires: '',// cookie失效时间 path: '', // 写cookie所在的路径 domain: '', // 写cookie所在的域名 httpOnly: '', // 是否只用于http请求中获取 overwrite: '', // 是否允许重写 secure: '', sameSite: '', 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'); })
储存在redis
在express中我们用的是express-session,那么在koa2中用的是哪些模块:
注意:一旦选择了将 Session 存入到外部存储中,就意味着系统将强依赖于这个外部存储,当它挂了的时候,我们就完全无法使用 Session 相关的功能了。因此我们更推荐大家只将必要的信息存储在 Session 中,保持 Session 的精简并使用默认的 Cookie 存储,用户级别的缓存不要存储在 Session 中。
模板引擎
文件上传(upload)
busboy模块
npm install --save busboy
busboy 模块是用来解析POST请求,node原生req中的文件流。
更多详细API可以访问npm官方文档 https://www.npmjs.com/package...
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)
上传文件简单实现
#index.js const Koa = require('koa'); const path = require('path'); const app = new Koa(); const {uploadFile} = require('./util/upload') 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', 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'); })
# util/upload.js const inspect = require('util').inspect const path = require('path') 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 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((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) + '.' + 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 = '文件上传成功' console.log('文件上传成功!') }) }) // 解析表单中其他字段信息 busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) { console.log('表单字段数据 [' + fieldname + ']: value: ' + inspect(val)); result.formData[fieldname] = inspect(val); }); // 解析结束事件 busboy.on('finish', function( ) { console.log('文件上结束') resolve(result) }) // 解析错误事件 busboy.on('error', function(err) { console.log('文件上出错') reject(result) }) req.pipe(busboy) }) } module.exports = { uploadFile }
异步上传图片实现
源码:https://github.com/JXtreehous...
通过stream的方式上传文件
参考中间件-> aliyun-oss-upload-stream
为什么使用``stream?
- 使用
stream
的方式上传文件可以很大程度上降低服务器内存开销。Aliyun官方SDK并没有对stream进行一个完美的封装,所以通常上传文件(Put Object)
的流程是客户端上传文件到服务器,服务器把文件数据缓存到内存,等文件全部上传完毕后,一次性上传到Aliyun Oss服务。这样做一旦瞬间上传文件的请求过多,服务器的内存开销会直线上升。而使用stream的方式上传文件的流程是客户端在上传文件数据到服务器的过程中,服务器同时也在把文件数据往Aliyun Oss服务传送,而不需要在服务器上缓存文件数据。 - 可以上传大文件,根据上传数据方式不同而不同, Put Object 方式 文件最大不能超过 5GB,而使用stream的方式,文件大小不能超过 48.8TB
- 更快的速度,由于传统方式(Put Object方式)是客户端上传完毕文件后,统一上传到Aliyun Oss,而stream的方式基本上客户端上传完毕后,服务器已经把一大半的文件数据上传到Aliyun了,所以速度要快很多
- 使用更简单,经过封装后,stream的方式使用起来非常的方便,1分钟就可以学会如何使用
数据库
mysql
npm install --save mysql
mysql模块是node操作MySQL的引擎,可以在node.js环境下对MySQL数据库进行建表,增、删、改、查等操作。
创建数据库会话
onst 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, results, 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; }) })
更多详细API可以访问npm官方文档 https://www.npmjs.com/package...
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脚本文件
https://chenshenhai.github.io...
https://github.com/ChenShenha...
JSONP
原生koa2实现jsonp
在项目复杂的业务场景,有时候需要在前端跨域获取数据,这时候提供数据的服务就需要提供跨域请求的接口,通常是使用JSONP的方式提供跨域接口。
https://chenshenhai.github.io...
https://github.com/ChenShenha...
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') })
前端部分
$.ajax({ url: 'http://localhost:3000/getData.jsonp', type: 'GET', dataType: 'JSONP', success: function(res) { console.log(res) } })
koa-jsonp中间件
npm install --save koa-jsonp
https://www.npmjs.com/package...
https://github.com/chenshenha...
OAuth 2.0
OAuth 2.0深入了解:以微信开放平台统一登录为例
微信开放平台开发——网页微信扫码登录(OAuth2.0)
常用中间件
Koa Static Cache: https://www.npmjs.com/package...
常用脚手架
koa-generator
yi-ge/Koa2-API-Scaffold
- Express-style - Support koa 1.x(supported) - Support koa 2.x(koa middleware supported,need Node.js 7.6+ ,babel optional)
参考项目
egg-commerce
koajs/examples
johndatserakis/koa-vue-notes-api
使用koa2+wechaty打造个人微信小秘书
常见问题
koa
与express
区别
koa原理
- 中间件(Middleware)
- 上下文(Context)
参考
Koa.js 设计模式-学习笔记
koa2进阶学习笔记
koa2 源码分析
npm koa-session-minimal
koa2中的session及redis
egg.js Cookie and Session
《HTTP权威指南》
七天学会NodeJS
Koa2 之文件上传下载
node消息队列
快速搭建可用于实战的koa2+mongodb框架
https://chenshenhai.github.io...
KOA2框架原理解析和实现
koa 介绍 ppt