node+koa中转层开发实践总结
node中转层的意义:
1.能解决前后端代码部署在不同服务器下时的跨域问题。(实现)
2.合并请求,业务逻辑处理。(实现)
3.单页应用的首屏服务端渲染。(暂未实现)
环境准备:
node: ^8.11.2
koa: ^2.6.1
koa-router: ^7.4.0
koa-bodyparser: ^4.2.1
在项目目录下新建server目录,新建app.js
const Koa = require('koa'); const bodyParser = require('koa-bodyparser'); const apiControler = require('./apiControler'); let app = new Koa(); global.hostname = "172.16.16.113"; global.port = 8070; app.use(async (ctx, next) => { await next(); console.log(`Process ${ctx.request.method} ${ctx.request.url}...`); }); //bodyParser必须在router之前注册到app上 app.use(bodyParser()); //使用路由处理中间件 app.use(apiControler()); app.listen(8899);
在server目录下新建业务接口目录,本例目录名为apiControlers,拿登录模块为例,新建一个login.js,里面包含登录模块所需要的所有接口。(获取验证码、登录、获取菜单权限)
login.js
const http = require('http'); const hostname = global.hostname; const port = global.port; let tokenStr = ""; /*获取图形验证码*/ let getAuthCoedFn = async (ctx, next) => { let data = await asyncGetAuthCode(); ctx.set("Content-Type", "application/json"); ctx.body = JSON.parse(data); await next(); }; function asyncGetAuthCode() { return new Promise((resolve, reject)=> { let authCodedData = ""; let req = http.request({ path: '/api/backstage/authCode', port: port, method: 'GET', hostname: hostname },(res)=> { res.on('data', (chunk)=> { authCodedData += chunk }); res.on('end', ()=> { authCodedData = JSON.stringify(authCodedData) resolve(authCodedData) }) }); req.on("error", (e)=> { console.log("api:/backstage/authCode error") reject(e.message) }); req.end(); }) } /*登录*/ let loginFn = async (ctx, next) => { let param = ctx.request.body; let authcodekey = ctx.request.header.authcodekey; let postData = { userName: param.userName, authCode: param.authCode, password: param.password }; let loginData = await asyncPostLogin(authcodekey, JSON.stringify(postData)); ctx.set("Content-Type", "application/json"); ctx.set("Connection", "keep-alive"); ctx.body = JSON.parse(loginData); next() }; function asyncPostLogin(authcodekey, postData) { return new Promise((resolve, reject)=> { let loginData = ""; let req = http.request({ path: '/api/backstage/login', port: port, method: 'POST', hostname: hostname, headers: { 'Content-Type': 'application/json', 'authCodeKey': authcodekey } },(res)=> { res.on('data', (chunk)=> { loginData += chunk }).on('end', ()=> { loginData = JSON.stringify(loginData); tokenStr = res.headers['set-cookie']; resolve(loginData) }) }); req.on('error', (e)=> { console.log("api:/backstage/login error"); reject(e.message) }); req.write(postData); req.end(); }) } /*获取菜单及权限列表*/ let getPowerListFn = async (ctx, next) => { let menuList = await asyncGetPowerList(); ctx.body = JSON.parse(menuList); next() }; function asyncGetPowerList() { return new Promise((resolve, reject)=> { let listData = ""; let req = http.request({ path: '/api/backstage/getPowerList', method: 'get', port: port, hostname: hostname, headers: { 'Cookie': tokenStr.toString() } },(res)=> { res.on('data', (chunk)=> { listData += chunk; }).on('end', ()=> { listData = JSON.stringify(listData); resolve(listData) }) }); req.on("error", (e)=> { console.log("api: /backstage/getPowerList error"); reject(e.message) }); req.end() }) } module.exports = { 'GET/api/backstage/authCode': getAuthCoedFn, 'POST/api/backstage/login': loginFn, 'GET/api/backstage/getPowerList': getPowerListFn }
以接口功能声明一个函数,在此函数中通过node的http模块发送请求。需要注意的是http.request请求获取响应头cookie的方式是tokenStr = res.headers['set-cookie']。
每一个业务功能js最后暴露出内部所有以接口请求方式+接口地址为key,以对应功能函数为value的对象。
在server目录下新建一个apiControler.js中间件(有返回值的函数)。此中间件的功能一是读取apiControlers目录下的所有业务js,并引入;二是设置接口请求方式与执行函数的映射关系。
最后暴露出一个函数返回所有请求接口路径的集合。
apiControler.js
const fs = require("fs"); function readApiFiles(router, dir = '/apiControlers') { fs.readdirSync(__dirname + dir).filter((f)=> { return f.endsWith('.js') }).forEach(f => { console.log(`process controller: ${f}...`); let mapping = require(__dirname + dir + '/' + f); addMapping(router, mapping) }); } function addMapping(router, mapping) { for(let url in mapping) { if(url.startsWith('GET')) { let path = url.substring(3); router.get(path, mapping[url]); }else if(url.startsWith('POST')) { let path = url.substring(4); router.post(path, mapping[url]); }else{ router.get(url, mapping[url]); console.log(`无效的URL: ${url}`); } } } module.exports = function (dir) { let controllers_dir = dir || '/apiControlers'; let router = require('koa-router')(); readApiFiles(router, controllers_dir); return router.routes(); };
最后回到app.js,引入apiControler.js中间件并注册到app上。需要注意的是bodyParser中间件必须在router之前注册到app上。
后续
此例目前只能用作接口转发、合并请求和解决跨域问题,终极目标是能解决SPA(单页应用的)首屏服务端渲染问题。
持续折腾中...