├── .babelrc ├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── assets └── stylesheets │ └── style.css ├── bin └── www ├── config ├── db.config.js ├── logger.config.js ├── proxy.config.js └── session.config.js ├── package.json ├── process.config.js ├── publicKey.pub └── src ├── app.js ├── controllers └── usersController.js ├── files └── test.json ├── middleware └── errorRouteCatch.js ├── routes ├── home.js ├── index.js ├── proxy.js └── users.js ├── services └── usersService.js ├── utils ├── bufferHelper.js ├── crypto.js ├── helper.js ├── proxyHelper.js ├── request.js └── sqlHelper.js └── views ├── error.hbs ├── index.html └── layout.hbs /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "transform-runtime", 5 | { 6 | "helpers": false, 7 | "polyfill": false, 8 | "regenerator": true, 9 | "moduleName": "babel-runtime" 10 | } 11 | ] 12 | ] 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | yarn.lock 3 | /logs/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "启动程序", 8 | "program": "${workspaceRoot}/bin/www", 9 | "sourceMaps": true 10 | // "preLaunchTask": "dev" // 等于下面`label`值 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Allen Zhang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #基于webpack构建的 Koa2 RESTful API 服务器脚手架 2 | 这是一个基于 Koa2 的轻量级 RESTful API Server 脚手架,支持 ES6, 支持使用TypeScript编写。 3 | 4 | 此脚手架只安装了一些配合koa2使用的必要插件,不仅提供RESTful API实现,同时也集成了对静态资源的处理,支持跨越,代理转发请求等基础功能。基本上您仅需要关注您的业务开发即可。 5 | 6 | 脚手架可以根据不同的环境配置不同的信息运行价值,支持开发,测试,生产环境的不同参数配置。 7 | 8 | #数据库选型MySQL 9 | 当然你也可以根据需要配置其他的关系型数据库,可扩展 sequelize.js 作为 PostgreSQL, MySQL, MariaDB, SQLite, MSSQL 关系型数据库的 ORM,本框架使用MVC分成模式实现,事例上通过SQL去实现对数据库的增、删、查、改操作。 10 | 11 | ## 目录结构说明 12 | 13 | ```bash 14 | . 15 | ├── README.md 16 | ├── .babelrc # Babel 配置文件 17 | ├── .gitignore # Git 忽略文件列表 18 | ├── package.json # 描述文件 19 | ├── process.config.js # pm2 部署示例文件 20 | ├── bin # bin入口目录 21 | │   └── www # 启动文件入口 22 | ├── .vscode # VS CODE 调式目录 23 | │   └── launch.json # 调试配置 24 | ├── config # 配置文件 25 | │   ├── db.config.js # 数据库配置文件 26 | │   ├── logger.config.js # 日志配置文件 27 | │   ├── proxy.config.js # 代理配置文件 28 | │   └── session.config.js # session配置文件 29 | ├── src # 源代码目录,编译后目标源代码位于 dist 目录 30 | │   ├── app.js # 入口文件 31 | │   ├── files # 存放文件目录 32 | │   ├── middleware # 中间件目录 33 | │   └── errorRouteCatch.js # 示例插件 - router异常处理 34 | │   ├── utils # 工具类目录 35 | │   ├── controllers # 控制器 36 | │   └── usersController.js # 示例users控制器 37 | │   ├── models # 模型层 38 | │   ├── routes # 路由层 39 | │   └── users.js # 示例users路由 40 | │   └── services # 服务层 41 | │   └── usersService.js # 示例users服务层 42 | ├── public # 静态资源目录 43 | └── logs # 日志目录 44 | ``` 45 | 46 | ## 开发使用说明 47 | 48 | ```bash 49 | git clone https://github.com/Allenzihan/koa2-mysql-framework.git 50 | 51 | cd mv koa2-mysql-framework 52 | npm install 53 | npm run dev 54 | 55 | 访问: http://127.0.0.1:3000/home 56 | ``` 57 | ## 开发调试说明 58 | 59 | 支持VSCODE调试 Node.js功能,已经配置好, 启动VSCODE IDE 上的Debug按钮即可调试 60 | 61 | 62 | ## 开发环境 63 | 64 | npm run dev 65 | 66 | ## PM2 部署说明 67 | 提供了 PM2 部署 RESTful API Server 的示例配置,位于“process.config.js”文件中。 68 | 69 | process.config.js 文件提供了两套环境的配置,分别是测试环境和生产环境的配置 70 | 71 | 启动测试环境: 72 | npm run uat 73 | 74 | 如何启动失败,使用pm2 直接启动 75 | pm2 start process.config.js --only uat 76 | 77 | 启动生产环境: 78 | npm run prod 79 | 80 | 如何启动失败,使用pm2 直接启动 81 | pm2 start process.config.js --only prod 82 | 83 | 以上使用pm2启动,需提前安装好pm2模块 84 | PM2 配合 Docker 部署说明: http://pm2.keymetrics.io/docs/usage/docker-pm2-nodejs/ 85 | 86 | ### 关于 Token 使用的特别说明(JWT 身份认证) 87 | 88 | app.use(jwt({ 89 | secret: publicKey.toString() 90 | }).unless({ 91 | path: [ 92 | /^\/users\/login/, 93 | /^\/home/, 94 | /^\/assets/ 95 | ] 96 | })) 97 | 98 | 在 path 里面的开头路径则不进行身份认证,否则都将进行  鉴权。 99 | 100 | 前端处理方案: 101 | 102 | ```javascript 103 | import axios from 'axios' 104 | import { getToken } from './tool' 105 | 106 | const DevBaseUrl = 'http://127.0.0.1:8080' 107 | const ProdBashUrl = 'https://xxx.xxx' 108 | 109 | let config = { 110 | baseURL: process.env.NODE_ENV !== 'production' ? DevBaseUrl : ProdBashUrl // 配置API接口地址 111 | } 112 | 113 | let token = getToken() 114 | if (token) { 115 | config.headers = { Authorization: 'Bearer ' + token } 116 | } 117 | 118 | let request = axios.create(config) 119 | 120 | // http request 拦截器 121 | axios.interceptors.request.use( 122 | config => { 123 | if (window) { 124 | let token = getToken() 125 | if (token) { 126 | // 判断是否存在token,如果存在的话,则每个http header都加上token 127 | config.headers.Authorization = `Bearer ${token}` 128 | } 129 | } 130 | // if (config.method === 'get') { 131 | // config.url = config.url + 'timestamp=' + Date.now().toString() 132 | // } 133 | return config 134 | }, 135 | err => { 136 | return Promise.reject(err) 137 | } 138 | ) 139 | 140 | export default request 141 | ``` 142 | 143 | `tool.js`文件 144 | 145 | ```javascript 146 | // 写 cookies 147 | export let setCookie = function setCookie(name, value, time) { 148 | if (time) { 149 | let strsec = getsec(time) 150 | let exp = new Date() 151 | exp.setTime(exp.getTime() + parseInt(strsec)) 152 | document.cookie = 153 | name + '=' + escape(value) + ';expires=' + exp.toGMTString() 154 | } else { 155 | document.cookie = name + '=' + escape(value) 156 | } 157 | } 158 | 159 | // 读 cookies 160 | export let getCookie = function(name) { 161 | let reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)') 162 | let arr = document.cookie.match(reg) 163 | return arr ? unescape(arr[2]) : null 164 | } 165 | 166 | // 删 cookies 167 | export let delCookie = function(name) { 168 | var exp = new Date() 169 | exp.setTime(exp.getTime() - 1) 170 | var cval = getCookie(name) 171 | if (cval != null) { 172 | document.cookie = name + '=' + cval + ';expires=' + exp.toGMTString() 173 | } 174 | } 175 | 176 | // 获取Token 177 | export let getToken = function() { 178 | if (window.sessionStorage && window.sessionStorage.Bearer) { 179 | return window.sessionStorage.Bearer 180 | } else if (window.localStorage && window.localStorage.Bearer) { 181 | return window.localStorage.Bearer 182 | } else if (window.document.cookie) { 183 | return getCookie('Bearer') 184 | } 185 | } 186 | 187 | // 设置Token 188 | export let setToken = function(token, rememberTime) { 189 | if (window.sessionStorage) { 190 | window.sessionStorage.Bearer = token 191 | } 192 | 193 | if ((rememberTime && window.localStorage) || !window.sessionStorage) { 194 | window.localStorage.Bearer = token 195 | } 196 | 197 | if ( 198 | window.document.cookie && 199 | !window.sessionStorage && 200 | !window.localStorage 201 | ) { 202 | if (rememberTime) { 203 | setCookie('Bearer', token, rememberTime) 204 | } else { 205 | setCookie('Bearer', token) 206 | } 207 | } 208 | } 209 | 210 | // 删除Token 211 | export let delToken = function() { 212 | if (window.sessionStorage && window.sessionStorage.Bearer) { 213 | window.sessionStorage.removeItem('Bearer') 214 | } 215 | 216 | if (window.localStorage && window.localStorage.Bearer) { 217 | window.localStorage.removeItem('Bearer') 218 | } 219 | 220 | if (window.document.cookie) { 221 | delCookie('Bearer') 222 | } 223 | } 224 | ``` 225 | 226 | 大概原理: 227 | 通过某个 API(通常是登录 API)获取成功后的 Token,存于本地,然后每次请求的时候在 Header 带上`Authorization: "Bearer " + token`,通常情况下无需担心本地 Token 被破解。 -------------------------------------------------------------------------------- /assets/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('babel-register') ({ 3 | presets: [ 'env' ] 4 | }) 5 | let env = process.argv[2] ? process.argv[2].slice(2) : 'dev' 6 | process.env.NODE_ENV = process.env.NODE_ENV ? process.env.NODE_ENV : env 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var app = require('../src/app'); 12 | var debug = require('debug')('demo:server'); 13 | var http = require('http'); 14 | 15 | /** 16 | * Get port from environment and store in Express. 17 | */ 18 | 19 | var port = normalizePort(process.env.PORT || '3000'); 20 | // app.set('port', port); 21 | 22 | /** 23 | * Create HTTP server. 24 | */ 25 | 26 | var server = http.createServer(app.callback()); 27 | 28 | /** 29 | * Listen on provided port, on all network interfaces. 30 | */ 31 | 32 | server.listen(port); 33 | server.on('error', onError); 34 | server.on('listening', onListening); 35 | 36 | /** 37 | * Normalize a port into a number, string, or false. 38 | */ 39 | 40 | function normalizePort(val) { 41 | var port = parseInt(val, 10); 42 | 43 | if (isNaN(port)) { 44 | // named pipe 45 | return val; 46 | } 47 | 48 | if (port >= 0) { 49 | // port number 50 | return port; 51 | } 52 | 53 | return false; 54 | } 55 | 56 | /** 57 | * Event listener for HTTP server "error" event. 58 | */ 59 | 60 | function onError(error) { 61 | if (error.syscall !== 'listen') { 62 | throw error; 63 | } 64 | 65 | var bind = typeof port === 'string' 66 | ? 'Pipe ' + port 67 | : 'Port ' + port; 68 | 69 | // handle specific listen errors with friendly messages 70 | switch (error.code) { 71 | case 'EACCES': 72 | console.error(bind + ' requires elevated privileges'); 73 | process.exit(1); 74 | break; 75 | case 'EADDRINUSE': 76 | console.error(bind + ' is already in use'); 77 | process.exit(1); 78 | break; 79 | default: 80 | throw error; 81 | } 82 | } 83 | 84 | /** 85 | * Event listener for HTTP server "listening" event. 86 | */ 87 | 88 | function onListening() { 89 | var addr = server.address(); 90 | var bind = typeof addr === 'string' 91 | ? 'pipe ' + addr 92 | : 'port ' + addr.port; 93 | debug('Listening on ' + bind); 94 | if(process.env.NODE_ENV === 'dev'){ 95 | console.log('Please visit the address: http://localhost:' + port) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /config/db.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dev: { 3 | host: 'localhost', 4 | user: 'root', 5 | password: '123456', 6 | database: 'testdb', 7 | connectionLimit: 20 8 | }, 9 | uat: { 10 | host: 'localhost', 11 | user: 'allen', 12 | password: '123456', 13 | database: 'testdb', 14 | connectionLimit: 20 15 | }, 16 | prod: { 17 | host: 'localhost', 18 | user: 'allen', 19 | password: '123456', 20 | database: 'testdb', 21 | connectionLimit: 20 22 | } 23 | } -------------------------------------------------------------------------------- /config/logger.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import koaLogjs from 'koa-log4' 3 | 4 | koaLogjs.configure({ 5 | appenders: { 6 | access: { 7 | type: 'dateFile', 8 | pattern: '-yyyy-MM-dd.log', //生成文件的规则 9 | alwaysIncludePattern: true, //文件名始终以日期区分 10 | encoding:"utf-8", 11 | filename: path.join(__dirname, '../logs/access') //生成文件名 12 | }, 13 | trace: { 14 | type: 'dateFile', 15 | pattern: '-yyyy-MM-dd.log', 16 | alwaysIncludePattern: true, //文件名始终以日期区分 17 | encoding:"utf-8", 18 | filename: path.join(__dirname, '../logs/trace') 19 | }, 20 | out: { 21 | type: 'console' 22 | } 23 | }, 24 | categories: { 25 | default: { appenders: [ 'out' ], level: 'info' }, 26 | access: { appenders: [ 'access' ], level: 'info' }, 27 | trace: { appenders: [ 'trace' ], level: 'all'} 28 | } 29 | }) 30 | 31 | 32 | //记录所有访问级别的日志 33 | export const accessLogger = () => koaLogjs.koaLogger(koaLogjs.getLogger('access')) 34 | 35 | //记录所有应用级别的日志 36 | export const logger = koaLogjs.getLogger('trace') -------------------------------------------------------------------------------- /config/proxy.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dev: { 3 | target: 'http://apis.juhe.cn', 4 | changeOrigin: true, 5 | pathRewrite:{ 6 | '^/old/api/*': '' 7 | } 8 | }, 9 | uat: { 10 | target: 'http://localhost:3000', 11 | changeOrigin: true, 12 | pathRewrite:{ 13 | '^/old/api/*': '' 14 | } 15 | }, 16 | prod: { 17 | target: 'http://localhost:3000', 18 | changeOrigin: true, 19 | pathRewrite:{ 20 | '^/old/api/*': '' 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /config/session.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | key: 'koa:sess', /** (string) cookie key (default is koa:sess) */ 3 | maxAge: 86400000, 4 | overwrite: true, /** (boolean) can overwrite or not (default true) */ 5 | httpOnly: true, /** (boolean) httpOnly or not (default true) */ 6 | signed: true, /** (boolean) signed or not (default true) */ 7 | rolling: false, /** (boolean) Force a session identifier cookie to be set on every response. The expiration is reset to the original maxAge, resetting the expiration countdown. (default is false) */ 8 | renew: false, /** (boolean) renew session when session is nearly expired, so we can always keep user logged in. (default is false)*/ 9 | } 10 | 11 | export default config -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-master", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node bin/www", 7 | "dev": "./node_modules/.bin/nodemon bin/www", 8 | "uat": "pm2 start ./process.config.js --only uat", 9 | "prod": "pm2 start ./process.config.js --only prod" 10 | }, 11 | "dependencies": { 12 | "babel-preset-env": "^1.7.0", 13 | "babel-preset-stage-0": "^6.24.1", 14 | "crypto": "^1.0.1", 15 | "debug": "^4.1.1", 16 | "handlebars": "~4.0.5", 17 | "http-proxy-middleware": "^0.19.1", 18 | "https-proxy-agent": "^2.2.2", 19 | "jsonwebtoken": "^8.5.1", 20 | "koa": "^2.7.0", 21 | "koa-bodyparser": "^4.2.1", 22 | "koa-convert": "^1.2.0", 23 | "koa-json": "^2.0.2", 24 | "koa-jwt": "^3.6.0", 25 | "koa-log4": "^2.3.2", 26 | "koa-logger": "^3.2.0", 27 | "koa-mysql-session": "^0.0.2", 28 | "koa-onerror": "^4.1.0", 29 | "koa-router": "^7.4.0", 30 | "koa-session": "^5.12.2", 31 | "koa-static": "^5.0.0", 32 | "koa-views": "^6.2.0", 33 | "koa2-connect": "^1.0.2", 34 | "koa2-cors": "^2.0.6", 35 | "uuid": "^3.3.2" 36 | }, 37 | "devDependencies": { 38 | "babel-plugin-transform-runtime": "^6.23.0", 39 | "babel-register": "^6.26.0", 40 | "mysql": "^2.17.1", 41 | "nodemon": "^1.19.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /process.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | // 测试环境 5 | name: "uat", 6 | script: "./bin/www", 7 | cwd: './', 8 | watch: true, // 默认关闭watch 可替换为 ['src'] 9 | ignore_watch: ['node_modules', 'build', 'logs'], 10 | log_date_format: "YYYY-MM-DD", 11 | out_file: './logs/out.log', // 日志输出 12 | error_file: './logs/error.log', // 错误日志 13 | max_memory_restart: '2G', // 超过多大内存自动重启,仅防止内存泄露有意义,需要根据自己的业务设置 14 | exec_mode: 'cluster', // 开启多线程模式,用于负载均衡 15 | instances: 'max', // 启用多少个实例,可用于负载均衡 16 | autorestart: true, // 程序崩溃后自动重启 17 | env: { 18 | "NODE_ENV": "uat", 19 | "PORT": 3000 20 | } 21 | }, 22 | { 23 | // 生产环境 24 | name: "prod", 25 | script: "./bin/www", 26 | cwd: './', 27 | watch: true, // 默认关闭watch 可替换为 ['src'] 28 | ignore_watch: ['node_modules', 'build', 'logs'], 29 | log_date_format: "YYYY-MM-DD", 30 | out_file: './logs/out.log', // 日志输出 31 | error_file: './logs/error.log', // 错误日志 32 | max_memory_restart: '2G', // 超过多大内存自动重启,仅防止内存泄露有意义,需要根据自己的业务设置 33 | exec_mode: 'cluster', // 开启多线程模式,用于负载均衡 34 | instances: 'max', // 启用多少个实例,可用于负载均衡 35 | autorestart: true, // 程序崩溃后自动重启 36 | // 项目环境变量 37 | env: { 38 | "NODE_ENV": "prod", 39 | "PORT": 3000 40 | } 41 | } 42 | ] 43 | } -------------------------------------------------------------------------------- /publicKey.pub: -------------------------------------------------------------------------------- 1 | wqdjkwl1e21FQlk1j2 -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import Koa from 'koa' 2 | import session from 'koa-session' 3 | import views from 'koa-views' 4 | import json from 'koa-json' 5 | import onerror from 'koa-onerror' 6 | import bodyparser from 'koa-bodyparser' 7 | import koaLogger from 'koa-logger' 8 | import path from 'path' 9 | import jwt from 'koa-jwt' 10 | import fs from 'fs' 11 | import {errorRoute, notFoundRoute} from './middleware/errorRouteCatch' 12 | import {accessLogger, logger} from '../config/logger.config' 13 | import sessConfig from '../config/session.config' 14 | import routers from './routes/index' 15 | 16 | // error handler 17 | const app = new Koa() 18 | const publicKey = fs.readFileSync(path.join(__dirname, '../publicKey.pub')) 19 | onerror(app) 20 | logger.error('start app', 'env:', process.env.NODE_ENV) 21 | // middlewares 22 | app.use(bodyparser({ 23 | enableTypes:['json', 'form', 'text'] 24 | })) 25 | app.use(json()) 26 | app.use(accessLogger()) 27 | app.use(koaLogger()) 28 | 29 | // router error 30 | app.use(errorRoute()) 31 | 32 | app.use(require('koa-static')(__dirname + '../assets')) 33 | 34 | // set the signature, control api to be accessed 35 | app.use(jwt({ 36 | secret: publicKey.toString() 37 | }).unless({ 38 | path: [ 39 | /^\/users\/login/, 40 | /^\/home/, 41 | /^\/assets/ 42 | ] 43 | })) 44 | 45 | // set template type, if use hbs, only change "extension: 'hbs'" and "hbs: 'handlebars'" 46 | app.use(views(__dirname + '/views', { 47 | extension: 'html', 48 | map: { html: 'handlebars' } 49 | })) 50 | 51 | // extends request header info 52 | app.use(async (ctx, next) => { 53 | ctx.request.header.publicKey = publicKey.toString() 54 | // test authorization 55 | // ctx.request.header = {'authorization': "Bearer " + (ctx.request.headers.)} 56 | await next() 57 | }) 58 | 59 | // if donot use session, you can remove the session middleware here. 60 | app.keys = [publicKey.toString()] 61 | app.use(session(sessConfig, app)) 62 | 63 | // logger 64 | app.use(async (ctx, next) => { 65 | const start = new Date() 66 | await next() 67 | const ms = new Date() - start 68 | console.log(`${ctx.method} ${ctx.url} - ${ms}ms`) 69 | }) 70 | 71 | // routes 72 | routers.forEach(router => { 73 | app.use(router.routes(), router.allowedMethods()) 74 | }) 75 | 76 | // not found router 77 | app.use(notFoundRoute()) 78 | 79 | // error-handling 80 | app.on('error', (err, ctx) => { 81 | console.error('server error', err, ctx) 82 | logger.error('server error', err, ctx) 83 | }) 84 | 85 | module.exports = app 86 | -------------------------------------------------------------------------------- /src/controllers/usersController.js: -------------------------------------------------------------------------------- 1 | 2 | import jwt from 'jsonwebtoken' 3 | import usersService from '../services/usersService' 4 | import request from '../utils/request' 5 | import helper from'../utils/helper' 6 | import md5 from '../utils/crypto' 7 | import { logger } from '../../config/logger.config' 8 | 9 | class usersController { 10 | /** 11 | * 测试登录接口,没有被jwt拦截 12 | * 登录成功返回加签的token信息 13 | */ 14 | static async login(ctx) { 15 | logger.error('users/login', 'response:', process.env.NODE_ENV) 16 | let params = ctx.request.body || ctx.request.query 17 | let username = escape(params.username) 18 | let password = escape(params.password) 19 | if (username && password) { 20 | try { 21 | let data = await usersService.getUserInfoByUserId(username) 22 | if (data[0] && data[0].password === md5(password)){ 23 | let userInfo = { 24 | userId: data[0].userId, 25 | username: data[0].username, 26 | status: data[0].status 27 | } 28 | let token = jwt.sign(userInfo, ctx.request.header.publicKey, { expiresIn: '2h' }) 29 | ctx.session.userInfo = userInfo 30 | helper.responseFormat(ctx, 200, '登录成功', token) 31 | logger.info('users/login', 'response:', token) 32 | } else { 33 | helper.responseFormat(ctx, 410, '用户名或者密码错误') 34 | logger.error('users/login', 'response:', '用户名或者密码错误') 35 | } 36 | } catch (err) { 37 | helper.responseFormat(ctx, 412, '用户信息查询失败', err) 38 | logger.error('users/login', 'response:', err) 39 | } 40 | } else { 41 | helper.responseFormat(ctx, 416, '查询参数缺失') 42 | logger.error('users/login', 'params:', params) 43 | } 44 | } 45 | 46 | /** 47 | * 测试获取用户信息接口。 48 | * 如果没有获得授信将会被jwt拦截,测试时优先访问以上的login接口完成授信操作 49 | */ 50 | static async getUserInfoByUserId (ctx) { 51 | let params =Object.assign({}, ctx.request.query, ctx.request.body) 52 | let username = escape(params.username) 53 | // let token = jwt.verify(ctx.headers.authorization.split(' ')[1], ctx.request.header.publicKey) 54 | console.log('session', ctx.session.userInfo) 55 | if (username) { 56 | try { 57 | let data = await usersService.getUserInfoByUserId(username) 58 | helper.responseFormat(ctx, 200, '查询成功', data) 59 | logger.info('users/getUserInfoByUserId', 'response:', data) 60 | } catch (err) { 61 | helper.responseFormat(ctx, 412, '查询失败', err) 62 | logger.error('users/getUserInfoByUserId', 'response:', err) 63 | } 64 | } else { 65 | helper.responseFormat(ctx, 416, '查询参数缺失') 66 | logger.error('users/getUserInfoByUserId', 'response:', '查询参数缺失') 67 | } 68 | } 69 | 70 | /** 71 | * 测试从远端请求数据 72 | * request中有proxy设置 73 | */ 74 | static async getRemoteData (ctx) { 75 | const options = { 76 | method: 'GET', 77 | path: '/simpleWeather/query' 78 | } 79 | let data = await request(options,{city:'苏州', key:'2343423'}) 80 | helper.responseFormat(ctx, 200, '查询成功', data) 81 | } 82 | 83 | /** 84 | * 测试从本地读取数据,读取方式采取异步读取 85 | */ 86 | static async readFiles (ctx) { 87 | var path = require('path') 88 | let filepath =path.join(__dirname, '../files/test.json') 89 | let data = await helper.asyncReadFile (filepath) 90 | helper.responseFormat(ctx, 200, '查询成功', JSON.parse(data)) 91 | } 92 | } 93 | 94 | export default usersController -------------------------------------------------------------------------------- /src/files/test.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "next": "b.json", 3 | "msg": "This is a!" 4 | }, 5 | { 6 | "next": "c.json", 7 | "msg": "This is b!" 8 | }, 9 | { 10 | "next": null, 11 | "msg": "This is c!" 12 | }] -------------------------------------------------------------------------------- /src/middleware/errorRouteCatch.js: -------------------------------------------------------------------------------- 1 | export const errorRoute = ()=> { 2 | return (ctx, next) => { 3 | return next().catch((err) => { 4 | switch (err.status) { 5 | case 401: 6 | ctx.status = 401 7 | ctx.body = { 8 | code: 200, 9 | msg: 'Authentication Error.Protected resource, use authorization header to get access.' 10 | } 11 | break 12 | default: 13 | throw err 14 | } 15 | }) 16 | } 17 | } 18 | 19 | export const notFoundRoute = () => { 20 | return (ctx, next) => { 21 | switch (ctx.status) { 22 | case 404: 23 | ctx.body = '没有找到内容 - 404' 24 | break 25 | } 26 | next() 27 | } 28 | } -------------------------------------------------------------------------------- /src/routes/home.js: -------------------------------------------------------------------------------- 1 | import koaRouter from 'koa-router' 2 | const router = koaRouter() 3 | 4 | router.prefix('/home') 5 | 6 | router.get('/', async (ctx, next) => { 7 | await ctx.render('index', { 8 | title: 'Hello Koa 2!' 9 | }) 10 | }) 11 | 12 | router.get('/string', async (ctx, next) => { 13 | ctx.body = 'koa2 string' 14 | }) 15 | 16 | router.get('/json', async (ctx, next) => { 17 | ctx.body = { 18 | title: 'koa2 json' 19 | } 20 | }) 21 | 22 | export default router 23 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import proxyRouter from './proxy' 2 | import homeRouter from './home' 3 | import usersRouter from './users' 4 | 5 | export default [ 6 | proxyRouter, 7 | homeRouter, 8 | usersRouter 9 | ] -------------------------------------------------------------------------------- /src/routes/proxy.js: -------------------------------------------------------------------------------- 1 | import koaRouter from 'koa-router' 2 | import proxyHelper from '../utils/proxyHelper' 3 | 4 | const router = koaRouter() 5 | /** 6 | * 需要代理的接口配置类,直接转发至第三方去获取请求数据 7 | * 测试访问:http://localhost:3000/old/api/loginStatus 8 | */ 9 | 10 | const proxyApi = { 11 | get: ['/old/api/**'], 12 | post: ['/post/old/api/**'] 13 | } 14 | 15 | router.get(proxyApi.get, async(ctx, next) => { 16 | ctx.respond = false 17 |   await proxyHelper(ctx, next) 18 | }) 19 | 20 | router.post(proxyApi.post, async(ctx, next) => { 21 | ctx.respond = false 22 |   await proxyHelper(ctx, next) 23 | }) 24 | 25 | export default router -------------------------------------------------------------------------------- /src/routes/users.js: -------------------------------------------------------------------------------- 1 | import koaRouter from 'koa-router' 2 | import usersController from '../controllers/usersController' 3 | 4 | const router = koaRouter() 5 | /** 6 | * 接口路径加前缀,如访问时使用 http://localhost:3000/users/getUserInfoByUserId 7 | */ 8 | router.prefix('/users') 9 | 10 | // 测试登录 11 | router.post('/login', usersController.login) 12 | 13 | // 测试读取数据库 14 | router.get('/getUserInfoByUserId', usersController.getUserInfoByUserId) 15 | 16 | // 测试访第三方接口 17 | router.get('/getRemoteData', usersController.getRemoteData) 18 | 19 | // 测试本地读取文件数据 20 | router.get('/readFiles', usersController.readFiles) 21 | 22 | export default router 23 | -------------------------------------------------------------------------------- /src/services/usersService.js: -------------------------------------------------------------------------------- 1 | import sqlHelper from '../utils/sqlHelper.js' 2 | 3 | class usersService extends sqlHelper { 4 | 5 | async getUserInfoByUserId (username) { 6 | return await this.query('SELECT * FROM users where username = ?', username) 7 | } 8 | } 9 | 10 | export default new usersService() -------------------------------------------------------------------------------- /src/utils/bufferHelper.js: -------------------------------------------------------------------------------- 1 | class bufferHelper { 2 | constructor() { 3 | this.buffers = [] 4 | this.size = 0 5 | } 6 | get length() { 7 | return this.size 8 | } 9 | clean () { 10 | this.buffers = [] 11 | this.size = 0 12 | return this 13 | } 14 | concat (buffer) { 15 | this.buffers.push(buffer) 16 | this.size += buffer.length 17 | return this 18 | } 19 | toBuffer () { 20 | return Buffer.concat(this.buffers, this.size) 21 | } 22 | toString (encoding) { 23 | return this.toBuffer().toString(encoding) 24 | } 25 | } 26 | 27 | export default bufferHelper -------------------------------------------------------------------------------- /src/utils/crypto.js: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | 3 | /** 4 | * 使用MD5方式加密数据 5 | * @param {} data 6 | */ 7 | const md5 = data => { 8 | return crypto.createHash('md5').update(data, 'utf-8').digest('hex') 9 | } 10 | 11 | export default md5 -------------------------------------------------------------------------------- /src/utils/helper.js: -------------------------------------------------------------------------------- 1 | class helper { 2 | /** 3 | * 响应统一格式化输出对象 4 | * @param {*} ctx 5 | * @param {*} status 6 | * @param {*} msg 7 | * @param {*} data 8 | */ 9 | static responseFormat (ctx, code, msg, data = []){ 10 | ctx.response.status = 200 11 | ctx.body = { 12 | code: code, 13 | msg: msg, 14 | data: data 15 | } 16 | } 17 | 18 | /** 19 | * 异步读取文件 20 | * @param {*} filepath 21 | */ 22 | static asyncReadFile (filepath) { 23 | var fs = require('fs') 24 | var path = require('path') 25 | let buf = Buffer.allocUnsafe(0) 26 | return new Promise((resolve, reject) => { 27 | var readerStream = fs.createReadStream(filepath, {highWaterMark :1}) 28 | readerStream.on('data', function(chunk) { 29 | buf = Buffer.concat([buf, chunk], buf.length + chunk.length) 30 | }) 31 | readerStream.on('end',function(){ 32 | resolve(buf.toString()) 33 | }) 34 | }) 35 | } 36 | } 37 | 38 | export default helper -------------------------------------------------------------------------------- /src/utils/proxyHelper.js: -------------------------------------------------------------------------------- 1 | import httpProxy from 'http-proxy-middleware' 2 | import k2c from 'koa2-connect' 3 | import config from '../../config/proxy.config' 4 | 5 | /** 6 | * 根据不同的环境配置代理路径,处理代理请求 7 | * @param {*} ctx 8 | * @param {*} next 9 | */ 10 | const proxyHelper = (ctx, next) => { 11 | return k2c(httpProxy(config[process.env.NODE_ENV]))(ctx, next) 12 | } 13 | 14 | export default proxyHelper -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import url from 'url' 2 | import http from 'http' 3 | import bufferHelper from '../utils/bufferHelper' 4 | 5 | const config = url.parse(require('../../config/proxy.config')[process.env.NODE_ENV].target) 6 | 7 | /** 8 | * http请求方法封装 9 | * @param {} options 10 | * @param {*} requestData 11 | */ 12 | const request = (options, requestData) => { 13 | return new Promise((resolve, reject) => { 14 | // let responseData = '' 15 | const buffer = new bufferHelper() 16 | Object.assign(options, { 17 | host: config.hostname, 18 | port: config.port || '80', 19 | method: options.method || 'POST', 20 | path: options.path 21 | }) 22 | options.headers = options.headers || {} 23 | 24 | let req = http.request(options, proxyRes => { 25 | proxyRes.setEncoding('utf8') 26 | proxyRes.on('data', chunk => { 27 | // responseData += chunk 28 | buffer.concat(Buffer.from(chunk)) 29 | }) 30 | proxyRes.on('end', () => { 31 | try { 32 | // resolve ({ 33 | // body: responseData, 34 | // headers: proxyRes.headers 35 | // }) 36 | // resolve(responseData) 37 | let res = JSON.parse(buffer.toString('utf-8')) 38 | resolve(res) 39 | } catch (err) { 40 | console.log('err', err) 41 | reject(err) 42 | } 43 | }) 44 | }) 45 | 46 | req.on('error', err => { 47 | reject(err) 48 | }) 49 | req.write(JSON.stringify(requestData)) 50 | req.end() 51 | }) 52 | } 53 | 54 | export default request -------------------------------------------------------------------------------- /src/utils/sqlHelper.js: -------------------------------------------------------------------------------- 1 | import mysql from 'mysql' 2 | import config from '../../config/db.config' 3 | 4 | /** 5 | * 数据库连接帮助类 6 | */ 7 | export default class sqlHelper { 8 | static getPool () { 9 | return mysql.createPool(config[process.env.NODE_ENV]) 10 | } 11 | static execute (sql, values) { 12 | return new Promise((resolve, reject) => { 13 | sqlHelper.getPool().getConnection((err, conn) => { 14 | if (err) return reject(err) 15 | conn.query(sql, values, (err, rows) => { 16 | if (err) reject(err) 17 | else resolve(rows) 18 | conn.release() 19 | }) 20 | }) 21 | }) 22 | } 23 | /** 24 | * 查询 25 | * @param {*} sql 26 | * @param {*} values 27 | */ 28 | query (sql, values) { 29 | return sqlHelper.execute(sql, values) 30 | } 31 | /** 32 | * 插入 33 | * @param {*} sql 34 | * @param {*} values 35 | */ 36 | insert (sql, values) { 37 | return sqlHelper.execute(sql, values) 38 | } 39 | /** 40 | * 更新 41 | * @param {*} sql 42 | * @param {*} values 43 | */ 44 | update (sql, values) { 45 | return sqlHelper.execute(sql, values) 46 | } 47 | /** 48 | * 删除 49 | * @param {*} sql 50 | * @param {*} values 51 | */ 52 | delete (sql, values) { 53 | return sqlHelper.execute(sql, values) 54 | } 55 | /** 56 | *从数据库中查询一条数据,返回值是对象,而非数组 57 | *最好在sql语句中加一个唯一的限制条件 58 | */ 59 | // get (sql, values) { 60 | // try { 61 | // return q(sql, values).then(rows => { 62 | // if (rows.length >= 1) { 63 | // return rows[0] 64 | // } 65 | // }) 66 | // } catch (err) { 67 | // return new Promise((resolve, reject) => { 68 | // reject(err) 69 | // }) 70 | // } 71 | // } 72 | } -------------------------------------------------------------------------------- /src/views/error.hbs: -------------------------------------------------------------------------------- 1 |

{{message}}

2 |

{{error.status}}

3 |
{{error.stack}}
4 | -------------------------------------------------------------------------------- /src/views/index.html: -------------------------------------------------------------------------------- 1 |

{{title}}

2 |

Welcome to {{title}}

3 | -------------------------------------------------------------------------------- /src/views/layout.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{title}} 5 | 6 | 7 | 8 | {{{body}}} 9 | 10 | 11 | --------------------------------------------------------------------------------