Koa

  |   0 评论   |   0 浏览

KOA 是什么

后端框架(以 nodejs 作为运行时环境的 js 框架),处理 http 请求并响应的框架,注意,学完 KOA,一定要放弃 Express

学 koa 之前

要掌握 es7 知识,会查 nodejs 的 api,了解 http 是什么

入门

创建项目

mkdir stukoa
cd stukoa
# 初始化node项目
yarn init
# 加入koa依赖
yarn add koa2

Hello world

# 新建一个js文件
touch app.js

写入代码

const Koa = require('Koa')
const app = new Koa()

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

app.listen(8080)
console.log('the server is running on 8080')

启动服务

node app.js

服务启动后查看 hello world页面

ctx

const Koa = require('Koa')
const app = new Koa()

app.use(async(ctx)=>{
    // 打印一下ctx
    // 打上断点
    console.dir(ctx);
    ctx.body = 'hello koa2'
})

app.listen(8080)
console.log('the server is running on 8080')

看看 ctx 中有什么


我们可以看到 ip,query,request,response,request.header,response.body 等属性

而 koa 的核心就是处理 http 请求并返回数据

ctx = context:应用上下文,里面直接封装部分 request.js 和 response.js 的方法

路由

那我们只有一个 ctx,如何应对各种格式路径的请求呢


// 我们现在访问localhost:8080/userinfo
// 打印出了/userinfo
console.log(ctx.request.url)

现在我们明白如何去区分 url 了

//我们可以手动区分
app.use(async(ctx)=>{
    if(ctx.request.url === '/userinfo'){
        ctx.body = {name:'王二牛',age:15,sex:0}
    }
    if(ctx.request.url === '/userinfo/1'){
        ctx.body = {name:'王三牛',age:15,set:0}
    }
})

一个一个区分太麻烦了代码越写越乱,我们可以引入 koa 的插件,按照插件的配置写就好了

(中间件)插件

为什么叫做中间件,这里我们提出 koa 的思想

最左边是 httpRequest,经过一堆中间件(过滤器?管道?就是这个意思),然后 response 出去

犹豫插件加载在 request 和 response 中间,所以我们叫插件为中间件

koa-router

yarn add koa-router@7
const Koa = require('koa')
const fs = require('fs')
const app = new Koa()

const Router = require('koa-router')

let home = new Router()

// 子路由1
// 我们访问/会获取到html
// 两个链接
// /page/helloworld
// /page/404
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()
// 404 路由,对应的response
// helloworld路由
page.get('/404', async ( ctx )=>{
  ctx.body = '404 page!'
}).get('/helloworld', async ( ctx )=>{
  ctx.body = 'helloworld page!'
})

// 装载所有路由
let router = new Router()
router.use('/', home.routes(), home.allowedMethods())
// 这里为/page装载了 /404 /helloworld
// 拼起来就是 /page/404 /page/helloworld
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')
})

koa-bodyparser

获取请求参数

get

 // get请求?a=b&c=d类似形式
 // 从上下文中直接获取
  let ctx_query = ctx.query
  let ctx_querystring = ctx.querystring

JSON

    // 如果是json格式的直接转换为对象就可以用了
    let obj = JSON.parse(ctx.request.body);

post

那既不是 get 也不是 JSON 的 post 请求呢,我们需要用到转换中间件

yarn add 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')
})

koa-static

koa-static 为我们提供了静态资源解析的功能

const Koa = require('koa')
const path = require('path')
const static = require('koa-static')

const app = new Koa()

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

// __dirname为nodejs全局变量 
// 将静态资源文件按照文件夹路径解析出来
app.use(static(
  path.join( __dirname,  staticPath)
))


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

app.listen(3000)

koa-session

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: '',
  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)

cookie 保存在客户端

const Koa = require('koa')
const app = new Koa()

app.use( async ( ctx ) => {
  if ( ctx.url === '/index' ) {
      // 写入cookie返回给浏览器
    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)

koa-views

通常我们有些路径是获取数据的,但是有些路径是获取页面的,页面如果也写在 ctx.body 中代码会很难看,很难 维护,所以引入koa-views

├── package.json
├── index.js
└── view
    └── index.ejs
const Koa = require('koa')
const views = require('koa-views')
const path = require('path')
const app = new Koa()

// 加载模板引擎
// koa支持许多模版引擎
app.use(views(path.join(__dirname, './view'), {
  extension: 'ejs'
}))

app.use( async ( ctx ) => {
  let title = 'hello koa2'
  // 将title渲染到index.ejs模版中
  await ctx.render('index', {title})
})

app.listen(3000)
<!DOCTYPE html>
<html>
<head>
    <title><%= title %></title>
</head>
<body>
    <h1><%= title %></h1>
    <p>EJS Welcome to <%= title %></p>
</body>
</html>

koa-mysql

待 MySQL 结束后再来写

koa-mongodb

待 mogodb 结束后再来写

koa-wechat

这里的项目地址在争渡 GitHub

config :项目的一些配置
controllers :路由
static :静态资源
views :视图
app.js :入口
controller_scan.js :手写路由匹配器

config

// 初始化微信信息
initWechatConfig = () => {
    console.log("init wechat:appid,appsecret,tokening");
    global.wechat_appid = "wx83157d09978bafed";
    global.wechat_appsecret = "aed830780e19a5d6d427eee076558935";
    global.wechat_token = "ferried";
    global.wechat_access_token = null;
    global.wechat_access_token_flush = new Date().getTime();
}

// 获取AccessToken
getAccessToken = (ctx) => {
    var request = require('request-promise');
    var options = {
        url: `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${global.wechat_appid}&secret=${wechat_appsecret}`
    }
    return request(options).then(function (response) {
        if (response) {
            if (JSON.parse(response).access_token) {
                global.wechat_access_token = JSON.parse(response).access_token;
                global.wechat_access_token_flush = new Date().getTime();
            }
        }
    }).catch(function (err) {
        throw (err);
    })
}

// 初始化AccessToken并每7200秒进行一次刷新
initWechatAccessToken = async (ctx, next) => {
    if (!global.wechat_access_token) {
        await getAccessToken(ctx);
    } else {
        var now = new Date().getTime();
        var timeout = now - global.wechat_access_token_flush;
        console.log("wechat_token: "+global.wechat_access_token,"timeout: "+timeout)
        if (timeout >= 7200000) {
            await getAccessToken(ctx);
        }
    }
    await next();
}

// 导出
module.exports = {
    setWechatConfig: initWechatConfig,
    setWechatAccessToken: initWechatAccessToken
};

controller

menu

// 这个函数用来给微信发送请求添加菜单
var menu = async (ctx, next) => {
    if (ctx.request.body.button) {
        var request = require('request-promise');
        var options = {
            method: 'POST',
            uri: `https://api.weixin.qq.com/cgi-bin/menu/create?access_token=${global.wechat_access_token}`,
            body: JSON.stringify(ctx.request.body)
        };
        await request(options).then((response) => {
            ctx.body = response;
        }).catch((error) => {
            ctx.body = error;
        })
    } else {
        ctx.body = {
            'error_code': '-1',
            'error_msg': 'menu is null'
        }
    }
}

module.exports = {
    'POST /wechat/addmenu': menu
}

auth 用户认证

// 获取auth页面
var wechat_oauth_page = async (ctx, next) => {
    await ctx.render('wechat_oauth');
}

//通过nodejs的request发送请求给微信完成用户认证
var wechat_oauth_token = async (ctx, next) => {
    if (ctx.request.body.code) {
        var request = require('request-promise');
        var code = ctx.request.body.code;
        var uri = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${global.wechat_appid}&secret=${global.wechat_appsecret}&code=${code}&grant_type=authorization_code`
        var options = {
            method: 'GET',
            uri: uri
        };
        await request(options).then((response) => {
            var body = JSON.parse(response);
            if(body.access_token){
                ctx.session.user_access_token = body.access_token;
            }
            if(body.refresh_token){
                ctx.session.user_refresh_token = body.refresh_token;
            }
            if(body.openid){
                ctx.seesion.user_openid = body.openid;
            }
            if(body.scope){
                ctx.session.user_scope = body.scope;
            }
        }).catch((error) => {
            ctx.body = error;
        })
    } else {
        ctx.body = "CODE不存在!";
    }
}

// 传入openid 调用其他语言接口
function queryUserByOpenId(openid) {

}

module.exports = {
    'GET /wechat/oauth_page': wechat_oauth_page,
    'POST /wechat/oauth_token': wechat_oauth_token
}

register:认证链接的时候使用,对参数进行字典排序后比对

const sha = require('sha1');
var register = async (ctx, next) => {
    let wechat_register = ctx.query;
    // 字典排序
    let temp_array = [global.wechat_token, wechat_register.nonce, wechat_register.timestamp].sort();
    // sha1加密
    let shastr = sha(temp_array.join(''));
    // 加密后比对进行开发者注册验证
    console.log(`wechat_register:sha:${shastr},signature:${wechat_register.signature}`)
    if (shastr == wechat_register.signature) {
        ctx.body = wechat_register.echostr;
    }
}

module.exports = {
    'GET /wechat/register': register,
};

app

// import koa
const Koa = require('koa');
// import koa middlewares
const router = require('koa-router')();
const session = require('koa-session');
const bodyParser = require('koa-bodyparser');
const static = require('koa-static');
const views = require('koa-views');
const logger = require('koa-logger');
// import controller_scan
const controller = require('./controller_scan')
// import wechat config
const wechatConfiger = require('./config/wechat_config');
const path = require('path');

const app = new Koa();
// add logger
app.use(logger());
// add post body
app.use(bodyParser());
// add session
app.keys = ['some secret hurr'];
app.use(session({
    key: 'koa:sess',
    maxAge: 86400000,
    overwrite: true,
    httpOnly: true,
    signed: true,
    rolling: true,
    renew: false,
}, app));
// add wechat
wechatConfiger.setWechatConfig();
app.use(wechatConfiger.setWechatAccessToken);
// add static resource
app.use(static(path.join(__dirname,'/static')));
// add views resource
app.use(views(__dirname + '/views', {extension: 'html'}));
// scan controller
app.use(controller());
// 8.listening to port
app.listen(8080);

controller scan

const fs = require('fs')

function addMapping(router, mapping) {
    for (var url in mapping) {
        if (url.startsWith('GET ')) {
            var path = url.substring(4);
            router.get(path, mapping[url]);
            console.log(`register URL mapping: GET ${path}`);
        } else if (url.startsWith('POST ')) {
            var path = url.substring(5);
            router.post(path, mapping[url]);
            console.log(`register URL mapping: POST ${path}`);
        } else {
            console.log(`invalid URL: ${url}`);
        }
    }
}

function addControllers(router) {
    console.log(__dirname);
    var files = fs.readdirSync(__dirname + '/controllers');
    var js_files = files.filter((f) => {
        return f.endsWith('.js');
    });

    for (var f of js_files) {
        console.log(`process controller: ${f}...`);
        let mapping = require(__dirname + '/controllers/' + f);
        addMapping(router, mapping);
    }
}

module.exports = function (dir) {
    let
        controllers_dir = dir || '/controllers',
        router = require('koa-router')();
    addControllers(router, controllers_dir);
    return router.routes();
};