关于koa2:Koa基本使用

一、koa根本应用依赖装置 npm i koa编码 const Koa = require("koa");const app = new Koa();app.use(async ctx=>{ ctx.body = 'hello Koa';});app.listen('5505',()=>{ console.log('Server running at http://localhost:5505');});二、Koa罕用中间件1.路由依赖装置 npm i @koa/router编码 const Router = require('@koa/router');const router = new Router();router.get('/foo',ctx=>{ ctx.body =' this is foo page';});app.use(router.routes()).use(router.allowedMethods());2.动态资源托管依赖装置 npm i koa-static koa-mount编码 const static = require("koa-static");const path = require("path");const mount = require("koa-mount");// mount 可用于给中间件增加一个路由,这里给托管的动态资源增加一个/public路由前缀app.use(mount("/public", static(path.join(__dirname, "./public"))));3.合并中间件依赖装置 npm i koa-compose编码 const compose = require('koa-compose')const a1 = (ctx,next)=>{ console.log('a1'); next();}const a2 = (ctx,next)=>{ console.log('a2'); next();}const a3 = (ctx,next)=>{ console.log('a3'); next();}app.use(compose([a1,a2,a3]))三、罕用性能1.重定向编码 ...

September 5, 2022 · 1 min · jiezi

关于koa2:koa2洋葱模型的实现

koa2的洋葱模型构造像洋葱 申请过程穿梭洋葱,有个来回外围代码:// 函数集(相当于中间件汇合)let arr = [ (next) => { console.log('a1'); next(); console.log('a2'); }, (next) => { console.log('b1'); next(); console.log('b2'); }, (next) => { console.log('c1'); next(); console.log('c2'); },];// 用递归实现洋葱模型let dispatch = (i) => { let fn = arr[i]; if (typeof fn !== 'function') return let next = () => dispatch(i + 1); fn(next)}dispatch(0)// 运行后果 ==> a1 > b1 > c1 > c2 > b2 > a1包装一下,用中间件形式调用.let a = (next) => { console.log('a1'); next(); console.log('a2');}let b = (next) => { console.log('b1'); next(); console.log('b2');}let c = (next) => { console.log('c1'); next(); console.log('c2');}let compose = (arr) => { return (args) => { console.log(args); let dispatch = (index) => { let fn = arr[index]; if (typeof fn !== 'function') return let next = () => dispatch(index + 1); fn(next); } dispatch(0) }}compose([a, b, c])('内部参数');完

May 15, 2021 · 1 min · jiezi

koa2sequelizemysqlpm2支持node-webpack打包线上部署日志查询

node+koa2+sequelize+mysql+pm2 (欢迎star) 简介koa2 作为主要node service 入口webpack 打包node 环境pm2 服务负载均衡mysql 数据库mysql 强大的事务 sequelizekoa-body,文件上传中间件koa-cors koa 跨域中间件log4 日志输出......项目独立提供服务接口,可作为前后端分类提供良好的解决方案 依赖node -v 8.4.0npm -v 5.3.0npm2 -v 3.5.1目录.├─auto //sequelize-auto 自动生成 models实体类└─src | main.js //入口文件 | router.js // controller 入口 | ├─config //配置文件 ├─controller //api层 ├─models // 实体类 └─utils //工具类 部署 git https://github.com/shanyanwt/koa_vue_blog.git npm install 开发环境 npm run dev localhost:8081 生产环境 npm run build //生成app.js npm run pm2 localhost:8081supervisor nodejs 热加载 开发环境使用supervisor -w src ,添加需要监听的文件,默认是全部但是有时不起作用,加上监听的文件即可 ...

September 10, 2019 · 2 min · jiezi

iijs-一个基于nodejskoa2构建的简单轻量级MVC框架

iijsA simple and lightweight MVC framework built on nodejs+koa2 项目介绍一个基于nodejs+koa2构建的简单轻量级MVC框架,最低依赖仅仅koa和koa-router 官网:js.i-i.me 源码:github 码云 QQ:331406669 使用安装 npm i iijs应用结构├── app //应用目录 (非必需,可更改)│ ├── Controller //控制器目录 (非必需,可更改)│ │ └── index.js //控制器│ ├── view //模板目录 (非必需,可更改)│ │ └── index //index控制器模板目录 (非必需,可更改)│ │ └── index.htm //模板│ ├── model //模型目录 (非必需,可更改)│ ├── logic //逻辑目录 (非必需,可更改)│ └── **** //其他目录 (非必需,可更改)├── app2 //应用2目录 (非必需,可更改)├── common //公共应用目录 (非必需,可更改)├── config //配置目录 (非必需,不可更改)│ ├── app.js //APP配置 (非必需,不可更改)│ ├── route.js //路由配置 (非必需,不可更改)│ └── **** //其他配置 (非必需,可更改)├── public //静态访问目录 (非必需,可更改)│ └── static //css image文件目录 (非必需,可更改)├── node_modules //nodejs模块目录├── server.js //应用入口文件 (必需,可更改)└── package.json //npm package.json应用入口// server.jsconst {app} = require('iijs');app.listen(3000, '127.0.0.1', function(err){ if(!err) console.log('http server is ready on 3000');});Hello world !// app/controller/index.jsclass Index { constructor(ctx, next) { this.ctx = ctx; this.next = next; } async hello() { this.ctx.body = `hello iijs, hello world !`; }}module.exports = Index;访问URL:http://localhost/app/index/hello ...

September 8, 2019 · 2 min · jiezi

koa初探

const router = require('koa-router')()router.get('/', async (ctx, next) => { await ctx.render('index', { title: 'Hello Koa 2!' })})router.get('/string', async (ctx, next) => { ctx.body = 'koa2 string'})router.get('/json', async (ctx, next) => { // http://localhost:3000/json?name=lius&age=26&sex=true ctx.body = { url: ctx.url, ctx_query: ctx.query, ctx_querystring: ctx.querystring } // { // "url": "/json?name=lius&age=26&sex=true", // "ctx_query": { // "name": "lius", // "age": "26", // "sex": "true" // }, // "ctx_querystring": "name=lius&age=26&sex=true" // }})router.post('/json',async (ctx,next)=>{ await ctx.cookies.set('name',ctx.request.body.name) console.log('name',ctx.cookies.get('name')) ctx.body = ctx.request.body})module.exports = router

July 2, 2019 · 1 min · jiezi

Koa源码浅析

Koa源码十分精简,只有不到2k行的代码,总共由4个模块文件组成,非常适合我们来学习。 参考代码: learn-koa2 我们先来看段原生Node实现Server服务器的代码: const http = require('http');const server = http.createServer((req, res) => { res.writeHead(200); res.end('hello world');});server.listen(3000, () => { console.log('server start at 3000');});非常简单的几行代码,就实现了一个服务器Server。createServer方法接收的callback回调函数,可以对每次请求的req res对象进行各种操作,最后返回结果。不过弊端也很明显,callback函数非常容易随着业务逻辑的复杂也变得臃肿,即使把callback函数拆分成各个小函数,也会在繁杂的异步回调中渐渐失去对整个流程的把控。 另外,Node原生提供的一些API,有时也会让开发者疑惑: res.statusCode = 200;res.writeHead(200);修改res的属性或者调用res的方法都可以改变http状态码,这在多人协作的项目中,很容易产生不同的代码风格。 我们再来看段Koa实现Server: const Koa = require('koa');const app = new Koa();app.use(async (ctx, next) => { console.log('1-start'); await next(); console.log('1-end');});app.use(async (ctx, next) => { console.log('2-start'); ctx.status = 200; ctx.body = 'Hello World'; console.log('2-end');});app.listen(3000);// 最后输出内容:// 1-start// 2-start// 2-end// 1-endKoa使用了中间件的概念来完成对一个http请求的处理,同时,Koa采用了async和await的语法使得异步流程可以更好的控制。ctx执行上下文代理了原生的res和req,这让开发者避免接触底层,而是通过代理访问和设置属性。 看完两者的对比后,我们应该会有几个疑惑: ctx.status为什么就可以直接设置状态码了,不是根本没看到res对象吗?中间件中的next到底是啥?为什么执行next就进入了下一个中间件?所有中间件执行完成后,为什么可以再次返回原来的中间件(洋葱模型)?现在让我们带着疑惑,进行源码解读,同时自己实现一个简易版的Koa吧! ...

June 28, 2019 · 7 min · jiezi

分享一个Nodejs-Koa2-MySQL-Vuejs-实战开发一套完整个人博客项目网站

这是个什么的项目?使用 Node.js + Koa2 + MySQL + Vue.js 实战开发一套完整个人博客项目网站。 博客线上地址:www.boblog.comGithub地址:https://github.com/liangfengbo/nodejs-koa-blog解决了什么问题?服务端:使用 Node.js 的 Koa2 框架二次开发 Restful API。前端:Vue.js 打造了前端网站和后台管理系统。项目包含什么功能? Koa2服务端 管理员与权限控制文章文章分类评论文章前端博客网站 Vue.js后台管理系统 Vue.js项目的特点Koa 与 Koa 二次开发API多 koa-router 拆分路由require-directory 自动路由加载异步编程 - async/await异步异常链与全局异常处理Sequelize ORM 管理 MySQLJWT 权限控制中间件参数验证器 Validatornodemon 修改文件自动重启前后端分离使用 Vue.js 搭建前端网站和后台管理系统如何使用和学习?数据库启动项目前一定要在创建好 boblog 数据库。 # 登录数据库$ mysql -uroot -p密码# 创建 wxapp 数据库$ CREATE DATABASE IF NOT EXISTS boblog DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;克隆项目首先使克隆项目,然后进入项目根目录使用命令安装包,最后命令启动项目,代码会根据模型自动创建数据库表的。 根目录都是 Node.js + Koa2 API开发源代码,根目录下的 web 文件夹下都是前端网站项目源代码,根目录下的 admin 文件夹下都是后台管理系统的源代码。 ...

June 27, 2019 · 1 min · jiezi

koa-洋葱模型

分析1、首先这是koa2最简单的入门例子,我将通过这个入门例子来演示koa2的洋葱模型 const Koa = require('koa');const app = new Koa();app.use((ctx,next)=>{ console.log("第一个中间件执行"); next() ;});// 第二个中间件app.use((ctx,next)=>{ console.log("第二个中间件");})let r = app.listen(8080);console.log("server run on 8080");在这里面,app首先是调用了两次use,然后就开始监听端口, listen(...args) { debug('listen'); // 当客户端发送请求将会调用callback const server = http.createServer(this.callback()); return server.listen(...args); }因此use是核心: use(fn) { // ... 一些判断代码 this.middleware.push(fn); return this; }从上面可以看出这里将外部use的函数通过内部的一个middleware变量记录下来,然后就没了。 OK,现在当客户端发送请求的时候,内部会创建上下文对象,然后处理请求: callback() { const fn = compose(this.middleware); if (!this.listenerCount('error')) this.on('error', this.onerror); const handleRequest = (req, res) => { // 创建上下文 const ctx = this.createContext(req, res); // 处理请求 return this.handleRequest(ctx, fn); }; return handleRequest; }处理请求 ...

May 25, 2019 · 2 min · jiezi

koa2mongoose接口开发实战第一枪

初始化项目使用 koa-generator 脚手架工具 npm install koa-generator -g #全局安装koa2 demo #创建demo项目cd demo && npm install #安装依赖默认生成项目结构如下 修改配置用脚手架生成的项目,默认是服务器渲染,即响应的是html视图。而我们要开发接口,响应的是json数据。所以要删除渲染视图的代码。增加响应json的配置。 首先删除views文件夹,接下来就是修改 app.js 1. 删除视图配置以下是要删除的代码 const views = require('koa-views')app.use(views(__dirname + '/views', { extension: 'pug'}))2. 修改路由的注册方式,通过遍历routes文件夹读取文件const fs = require('fs')fs.readdirSync('./routes').forEach(route=> { let api = require(`./routes/${route}`) app.use(api.routes(), api.allowedMethods())})3. 添加jwt认证,同时过滤不需要认证的路由,如获取tokenconst jwt = require('koa-jwt')app.use(jwt({ secret: 'yourstr' }).unless({ path: [ /^\/$/, /\/token/, /\/wechat/, { url: /\/papers/, methods: ['GET'] } ]}));4. 全局错误捕获并响应// errorapp.use(async (ctx, next) => { try { await next() } catch(err) { ctx.status = err.statusCode || err.status || 500; ctx.body = err.message ctx.app.emit('error', err, ctx); }})5. 跨域处理当接口发布到线上,前端通过ajax请求时,会报跨域的错误。koa2使用koa2-cors这个库非常方便的实现了跨域配置,使用起来也很简单 ...

May 13, 2019 · 3 min · jiezi

Graphql实战系列(下)

前情介绍在《Graphql实战系列(上)》中我们已经完成技术选型,并将graphql桥接到凝胶gels项目中,并动态手写了schema,可以通过 http://localhost:5000/graphql 查看效果。这一节,我们根据数据库表来自动生成基本的查询与更新schema,并能方便的扩展schema,实现我们想来的业务逻辑。设计思路对象定义在apollo-server中是用字符串来做的,而Query与Mutation只能有一个,而我们的定义又会分散在多个文件中,因此只能先以一定的形式把它们存入数组中,在生成schema前一刻再组合。业务逻辑模块模板设计:const customDefs = { textDefs: type ReviseResult { id: Int affectedRows: Int status: Int message: String }, queryDefs: [], mutationDefs: []}const customResolvers = { Query: { }, Mutation: { } }export { customDefs, customResolvers }schema合并算法let typeDefs = [] let dirGraphql = requireDir('../../graphql') //从手写schema业务模块目录读入文件 G.L.each(dirGraphql, (item, name) => { if (item && item.customDefs && item.customResolvers) { typeDefs.push(item.customDefs.textDefs || '') //合并文本对象定义 typeDefObj.query = typeDefObj.query.concat(item.customDefs.queryDefs || []) //合并Query typeDefObj.mutation = typeDefObj.mutation.concat(item.customDefs.mutationDefs || []) //合并Matation let { Query, Mutation, ...Other } = item.customResolvers Object.assign(resolvers.Query, Query) //合并resolvers.Query Object.assign(resolvers.Mutation, Mutation) //合并resolvers.Mutation Object.assign(resolvers, Other) //合并其它resolvers } }) //将query与matation查询更新对象由自定义的数组转化成为文本形式 typeDefs.push(Object.entries(typeDefObj).reduce((total, cur) => { return total += type ${G.tools.bigCamelCase(cur[0])} { ${cur[1].join(’’)} } }, ''))从数据库表动态生成schema自动生成内容:一个表一个对象;每个表有两个Query,一是单条查询,二是列表查询;三个Mutation,一是新增,二是更新,三是删除;关联表以上篇中的Book与Author为例,Book中有author_id,会生成一个Author对象;而Author表中会生成一个对象列表[Book]mysql类型 => graphql 类型转化常量定义定义一类型转换,不在定义中的默认为String。const TYPEFROMMYSQLTOGRAPHQL = { int: 'Int', smallint: 'Int', tinyint: 'Int', bigint: 'Int', double: 'Float', float: 'Float', decimal: 'Float',}从数据库中读取数据表信息 let dao = new BaseDao() let tables = await dao.querySql('select TABLE_NAME,TABLE_COMMENT from information_schema.TABLES' + ' where TABLE_SCHEMA = ? and TABLE_TYPE = ? and substr(TABLE_NAME,1,2) <> ? order by ?', [G.CONFIGS.dbconfig.db_name, 'BASE TABLE', 't_', 'TABLE_NAME'])从数据库中读取表字段信息tables.data.forEach((table) => { columnRs.push(dao.querySql('SELECT COLUMNS.COLUMN_NAME,COLUMNS.COLUMN_TYPE,COLUMNS.IS_NULLABLE,' + 'COLUMNS.CHARACTER_SET_NAME,COLUMNS.COLUMN_DEFAULT,COLUMNS.EXTRA,' + 'COLUMNS.COLUMN_KEY,COLUMNS.COLUMN_COMMENT,STATISTICS.TABLE_NAME,' + 'STATISTICS.INDEX_NAME,STATISTICS.SEQ_IN_INDEX,STATISTICS.NON_UNIQUE,' + 'COLUMNS.COLLATION_NAME ' + 'FROM information_schema.COLUMNS ' + 'LEFT JOIN information_schema.STATISTICS ON ' + 'information_schema.COLUMNS.TABLE_NAME = STATISTICS.TABLE_NAME ' + 'AND information_schema.COLUMNS.COLUMN_NAME = information_schema.STATISTICS.COLUMN_NAME ' + 'AND information_schema.STATISTICS.table_schema = ? ' + 'where information_schema.COLUMNS.TABLE_NAME = ? and COLUMNS.table_schema = ?', [G.CONFIGS.dbconfig.db_name, table.TABLE_NAME, G.CONFIGS.dbconfig.db_name])) })几个工具函数取数据库表字段类型,去除圆括号与长度信息 getStartTillBracket(str: string) { return str.indexOf('(') > -1 ? str.substr(0, str.indexOf('(')) : str }下划线分隔的表字段转化为big camel-case bigCamelCase(str: string) { return str.split('_').map((al) => { if (al.length > 0) { return al.substr(0, 1).toUpperCase() + al.substr(1).toLowerCase() } return al }).join('') }下划线分隔的表字段转化为small camel-case smallCamelCase(str: string) { let strs = str.split('_') if (strs.length < 2) { return str } else { let tail = strs.slice(1).map((al) => { if (al.length > 0) { return al.substr(0, 1).toUpperCase() + al.substr(1).toLowerCase() } return al }).join('') return strs[0] + tail } }字段是否以_id结尾,是表关联的标志不以_id结尾,是正常字段,判断是否为null,处理必填typeDefObj[table].unshift(${col[‘COLUMN_NAME’]}: ${typeStr}${col[‘IS_NULLABLE’] === ‘NO’ ? ‘!’ : ‘’}\n)以_id结尾,则需要处理关联关系 //Book表以author_id关联单个Author实体 typeDefObj[table].unshift(“““关联的实体””” ${G.L.trimEnd(col[‘COLUMN_NAME’], ‘_id’)}: ${G.tools.bigCamelCase(G.L.trimEnd(col[‘COLUMN_NAME’], ‘id’))}) resolvers[G.tools.bigCamelCase(table)] = { [G.L.trimEnd(col['COLUMN_NAME'], '_id')]: async (element) => { let rs = await new BaseDao(G.L.trimEnd(col['COLUMN_NAME'], '_id')).retrieve({ id: element[col['COLUMN_NAME']] }) return rs.data[0] } } //Author表关联Book列表 let fTable = G.L.trimEnd(col['COLUMN_NAME'], '_id') if (!typeDefObj[fTable]) { typeDefObj[fTable] = [] } if (typeDefObj[fTable].length >= 2) typeDefObj[fTable].splice(typeDefObj[fTable].length - 2, 0, “““关联实体集合””"${table}s: [${G.tools.bigCamelCase(table)}]\n) else typeDefObj[fTable].push(${table}s: [${G.tools.bigCamelCase(table)}]\n) resolvers[G.tools.bigCamelCase(fTable)] = { [${table}s]: async (element) => { let rs = await new BaseDao(table).retrieve({ [col['COLUMN_NAME']]: element.id}) return rs.data } }生成Query查询单条查询 if (paramId.length > 0) { typeDefObj['query'].push(${G.tools.smallCamelCase(table)}(${paramId}!): ${G.tools.bigCamelCase(table)}\n) resolvers.Query[${G.tools.smallCamelCase(table)}] = async (_, { id }) => { let rs = await new BaseDao(table).retrieve({ id }) return rs.data[0] } } else { G.logger.error(Table [${table}] must have id field.) }列表查询 let complex = table.endsWith('s') ? (table.substr(0, table.length - 1) + 'z') : (table + 's') typeDefObj['query'].push(${G.tools.smallCamelCase(complex)}(${paramStr.join(’, ‘)}): [${G.tools.bigCamelCase(table)}]\n) resolvers.Query[${G.tools.smallCamelCase(complex)}] = async (_, args) => { let rs = await new BaseDao(table).retrieve(args) return rs.data }生成Mutation查询 typeDefObj['mutation'].push( create${G.tools.bigCamelCase(table)}(${paramForMutation.slice(1).join(’, ‘)}):ReviseResult update${G.tools.bigCamelCase(table)}(${paramForMutation.join(’, ‘)}):ReviseResult delete${G.tools.bigCamelCase(table)}(${paramId}!):ReviseResult ) resolvers.Mutation[create${G.tools.bigCamelCase(table)}] = async (_, args) => { let rs = await new BaseDao(table).create(args) return rs } resolvers.Mutation[update${G.tools.bigCamelCase(table)}] = async (_, args) => { let rs = await new BaseDao(table).update(args) return rs } resolvers.Mutation[delete${G.tools.bigCamelCase(table)}`] = async (, { id }) => { let rs = await new BaseDao(table).delete({ id }) return rs }项目地址https://github.com/zhoutk/gels使用方法git clone https://github.com/zhoutk/gelscd gelsyarntsc -wnodemon dist/index.js然后就可以用浏览器打开链接:http://localhost:5000/graphql 查看效果了。小结我只能把大概思路写出来,让大家有个整体的概念,若想很好的理解,得自己把项目跑起来,根据我提供的思想,慢慢的去理解。因为我在编写的过程中还是遇到了不少的难点,这块既要自动化,还要能方便的接受手动编写的schema模块,的确有点难度。 ...

April 13, 2019 · 3 min · jiezi

Graphql实战系列(上)

背景介绍graphql越来越流行,一直想把我的凝胶项目除了支持restful api外,也能同时支持graphql。由于该项目的特点是结合关系数据库的优点,尽量少写重复或雷同的代码。对于rest api,在做完数据库设计后,百分之六十到八十的接口就已经完成了,但还需要配置上api文档。而基于数据库表自动实现graphql,感觉还是有难度的,但若能做好,连文档也就同时提供了。不久前又看到了一句让我深以为然的话:No program is perfect, even the most talented engineers will write a bug or two (or three). By far the best design pattern available is simply writing less code. That’s the opportunity we have today, to accomplish our goals by doing less. so, ready go…基本需求与约定根据数据库表自动生成schema充分利用已经有的支持restful api的底层接口能自动实现一对多的表关系能方便的增加特殊业务,只需要象rest一样,只需在指定目录,增加业务模块即可测试表有两个,book & authorbook表字段有:id, title, author_idauthor表字段有: id, name数据表必须有字段id,类型为整数(自增)或8位字符串(uuid),作为主键或建立unique索引表名为小写字母,使用名词单数,以下划作为单词分隔表关联自动在相关中嵌入相关对象,Book对象增加Author对象,Author对象增加books列表每个表会默认生成两个query,一个是以id为参数进行单条查询,另一个是列表查询;命名规则;单条查询与表名相同,列表查询为表名+s,若表名本身以s结尾,则变s为z桥接库比较与选择我需要在koa2上接入graphql,经过查阅资料,最后聚焦在下面两个库上:kao-graphqlapollo-server-koakao-graphql实现开始是考虑简单为上,试着用kao-graphql,作为中间件可以方便的接入,我指定了/gql路由,可以测试效果,代码如下:import * as Router from ‘koa-router’import BaseDao from ‘../db/baseDao’import { GraphQLString, GraphQLObjectType, GraphQLSchema, GraphQLList, GraphQLInt } from ‘graphql’const graphqlHTTP = require(‘koa-graphql’)let router = new Router()export default (() => { let authorType = new GraphQLObjectType({ name: ‘Author’, fields: { id: { type: GraphQLInt}, name: { type: GraphQLString} } }) let bookType = new GraphQLObjectType({ name: ‘Book’, fields: { id: { type: GraphQLInt}, title: { type: GraphQLString}, author: { type: authorType, resolve: async (book, args) => { let rs = await new BaseDao(‘author’).retrieve({id: book.author_id}) return rs.data[0] } } } }) let queryType = new GraphQLObjectType({ name: ‘Query’, fields: { books: { type: new GraphQLList(bookType), args: { id: { type: GraphQLString }, search: { type: GraphQLString }, title: { type: GraphQLString }, }, resolve: async function (, args) { let rs = await new BaseDao(‘book’).retrieve(args) return rs.data } }, authors: { type: new GraphQLList(authorType), args: { id: { type: GraphQLString }, search: { type: GraphQLString }, name: { type: GraphQLString }, }, resolve: async function (, args) { let rs = await new BaseDao(‘author’).retrieve(args) return rs.data } } } }) let schema = new GraphQLSchema({ query: queryType }) return router.all(’/gql’, graphqlHTTP({ schema: schema, graphiql: true }))})() 这种方式有个问题,前面的变量对象中要引入后面定义的变量对象会出问题,因此投入了apollo-server。但apollo-server 2.0网上资料少,大多是介绍1.0的,而2.0变动又比较大,因此折腾了一段时间,还是要多看英文资料。apollo-server 2.0集成很多东西到里面,包括cors,bodyParse,graphql-tools 等。apollo-server 2.0实现静态schema通过中间件加载,放到rest路由之前,加入顺序及方式请看app.ts,apollo-server-kao接入代码://自动生成数据库表的基础schema,并合并了手写的业务模块import { getInfoFromSql } from ‘./schema_generate’const { ApolloServer } = require(‘apollo-server-koa’)export default async (app) => { //app是koa实例 let { typeDefs, resolvers } = await getInfoFromSql() //数据库查询是异步的,所以导出的是promise函数 if (!G.ApolloServer) { G.ApolloServer = new ApolloServer({ typeDefs, //已经不需要graphql-tools,ApolloServer构造函数已经集成其功能 resolvers, context: ({ ctx }) => ({ //传递ctx等信息,主要供认证、授权使用 …ctx, …app.context }) }) } G.ApolloServer.applyMiddleware({ app })}静态schema试验,schema_generate.tsconst typeDefs = type Author { id: Int! name: String books: [book] } type Book { id: Int! title: String author: Author } # the schema allows the following query: type Query { books: [Post] author(id: Int!): Author }const resolvers = { Query: { books: async function (, args) { let rs = await new BaseDao(‘book’).retrieve(args) return rs.data }, author: async function (, { id }) { let rs = await new BaseDao(‘author’).retrieve({id}) return rs.data[0] }, }, Author: { books: async function (author) { let rs = await new BaseDao(‘book’).retrieve({ author_id: author.id }) return rs.data }, }, Book: { author: async function (book) { let rs = await new BaseDao(‘author’).retrieve({ id: book.author_id }) return rs.data[0] }, },}export { typeDefs, resolvers}项目地址https://github.com/zhoutk/gels使用方法git clone https://github.com/zhoutk/gelscd gelsyarntsc -wnodemon dist/index.js然后就可以用浏览器打开链接:http://localhost:5000/graphql 查看效果了。小结这是第一部分,确定需求,进行了技术选型,实现了接入静态手写schema试验,下篇将实现动态生成与合并特殊业务模型。 ...

April 11, 2019 · 2 min · jiezi

使用koa和socket.io简单搭建多人聊天流程

koa与socket.io简单流程分析:1. 服务端触发初始化io.on(‘connection’, socket => {});2. 客户端发送say会话socket.emit(‘say’, ‘我是客户端’); 3. 服务端监听say会话socket.on(‘say’,data => {}); 4. 服务端发送message会话socket.emit(‘message’, {hello: ‘你是谁’});5. 客户端接收message消息socket.on(‘message’, (data) => {});服务端:const koa = require(‘koa’);const app = new koa();const server = require(‘http’).Server(app.callback())const io = require(‘socket.io’)(server);const port = 8081;server.listen(process.env.PORT || port, () => { console.log(app run at : http://127.0.0.1:${port});})io.on(‘connection’, socket => { console.log(‘socket初始化完成’); socket.on(‘say’, data => { console.log(data, ‘接收到的信息’) socket.emit(‘message’, {hello: ‘你是谁’}) })})客户端<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>Document</title> <style> input { background-color: #fff; background-image: none; border-radius: 4px; border: 1px solid #bfcbd9; box-sizing: border-box; color: #1f2d3d; font-size: inherit; height: 40px; line-height: 1; outline: 0; padding: 3px 10px; } .el-button–primary { color: #fff; background-color: #20a0ff; border-color: #20a0ff; } .el-button { display: inline-block; line-height: 1; white-space: nowrap; cursor: pointer; background: #00aac5; border: 1px solid #c4c4c4; color: #fff; margin: 0; padding: 10px 15px; border-radius: 4px; outline: 0; text-align: center; } </style></head><body> <div> <div id=“content”> </div> </div> <div> <input type=“text” id=“input”> <button class=“el-button el-button–primary el-button–large” type=“button” onclick=“say()"><span>发送消息</span></button> </div> <script src=”./socket.io.js"></script> <script> // 建立连接 var socket = io.connect(‘http://localhost:8081’); // 监听 message 会话 socket.on(‘message’, function (data) { let html = document.createElement(‘p’) html.innerHTML = 系统消息:&lt;span&gt;${data.hello}&lt;/span&gt; document.getElementById(‘content’).appendChild(html) console.log(data); }); // 按钮点击事件 function say() { let t = document.getElementById(‘input’).value if (!t) return let html = document.createElement(‘p’) html.innerHTML = 你细声说:&lt;span&gt;${t}&lt;/span&gt; document.getElementById(‘content’).appendChild(html) socket.emit(‘say’, { my: t}); } // 监听 news 会话 socket.on(’news’, function (data) { console.log(data); let html = document.createElement(‘p’) html.innerHTML = 小皮咖说:&lt;span&gt;我知道了,你说“${data.hello}”&lt;/span&gt; document.getElementById(‘content’).appendChild(html) }); // 监听吃饭会话 socket.on(’eating’, function (data) { console.log(data); let html = document.createElement(‘p’) html.innerHTML = 小皮咖说:${data.hello} document.getElementById(‘content’).appendChild(html) }); </script></body></html> ...

April 5, 2019 · 2 min · jiezi

记一次 React + Koa + Mysql 构建个人博客

前言由于一直在用 vue 写业务,为了熟悉下 react 开发模式,所以选择了 react。数据库一开始用的是 mongodb,后来换成 mysql 了,一套下来感觉 mysql 也挺好上手的。react-router、koa、mysql 都是从0开始接触开发的,期间遇到过很多问题,印象最深的是 react-router 参考官方文档配置的,楞是跑不起来,花费了好几个小时,最后才发现看的文档是v1.0, 而项目中是v4.3, 好在可参考的资料比较多,问题都迎刃而解了。博客介绍前端项目通过 create-react-app 构建,server端通过 koa-generator 构建前后端分离,博客页、后台管理都在 blog-admin 里,对含有 /admin 的路由进行登录拦截前端: react + antd + react-router4 + axiosserver端: koa2 + mysql + sequelize部署:server端 运行在 3000 端口,前端 80 端口,nginx设置代理预览地址web端源码server端源码喜欢或对你有帮助,欢迎 star功能[x] 登录[x] 分页[x] 查询[x] 标签列表[x] 分类列表[x] 收藏列表[x] 文章列表[x] 发布文章时间轴[x] 文章访问次数统计[x] 回到顶部[x] 博客适配移动端[ ] 后台适配移动端[ ] 对文章访问次数进行可视化[ ] 留言评论[ ] 渲染优化、打包优化效果标签分类收藏文章编辑博客页响应式运行项目前端git clone https://github.com/gzwgq222/blog-admin.gitcd blog-adminnpm installlocalhost:2019server 端本地安装 mysql,新建 dev 数据库git clone https://github.com/gzwgq222/blog-server.gitcd blog-servernpm installserver 端前端 react + antd 开发,较为平缓,在此就不再叙述。主要记录下 koa + mysql 相关事宜全局安装 koa-generatornpm install -g koa-generato创建 node-server 项目koa node-server 安装依赖cd node-server npn install运行npm dev出现 Hello Koa 2! 表示运行成功先看routes文件index.jsconst router = require(‘koa-router’)()router.get(’/’, async (ctx, next) => { await ctx.render(‘index’, { title: ‘Hello Koa 2!’ })})router.get(’/string’, async (ctx, next) => { ctx.body = ‘koa2 string’})router.get(’/json’, async (ctx, next) => { ctx.body = { title: ‘koa2 json’ }})module.exports = routerusers.jsconst router = require(‘koa-router’)()router.prefix(’/users’)router.get(’/’, function (ctx, next) { ctx.body = ’this is a users response!’})router.get(’/bar’, function (ctx, next) { ctx.body = ’this is a users/bar response’})module.exports = router分别访问下列路由localhost:3000/stringlocalhost:3000/userslocalhost:3000/bar大概你已经猜到了,koa-router 定义路由访问时返回相应的内容,那我们只需要把相应的 data 返回去就行了,只是我们的数据得从数据库查询出来。本地安装 mysql项目安裝 mysqlnpm install mysql –save项目安裝 sequelizesequelize 是 ORM node框架,对SQL查询语句的封装,让我们可以用OOP的方式操作数据库npm install –save sequelize新建 sequelize.js,建立连接池const Sequelize = require(‘sequelize’);const sequelize = new Sequelize(‘dev’, ‘root’, ‘123456’, { host: ’localhost’, dialect: ‘mysql’, operatorsAliases: false, pool: { max: 5, min: 0, acquire: 30000, idle: 10000 }})sequelize .authenticate() .then(() => { console.log(‘MYSQL 连接成功……’); }) .catch(err => { console.error(‘链接失败:’, err); });// 根据模型自动创建表sequelize.sync()module.exports = sequelize创建 model、controllers 文件夹 定义model:定义表结构;controller:定义对数据库的查询方法以 tag.js 为例model => tag.jsconst sequelize = require(’../sequelize ‘)const Sequelize = require(‘sequelize’)const moment = require(‘moment’) // 日期处理库// 定义表结构const tag = sequelize.define(’tag’, { id: { type: Sequelize.INTEGER(11), // 设置字段类型 primaryKey: true, // 设置为主建 autoIncrement: true // 自增 }, name: { type: Sequelize.STRING, unique: { // 唯一 msg: ‘已添加’ } }, createdAt: { type: Sequelize.DATE, defaultValue: Sequelize.NOW, get() { // this.getDataValue 获取当前字段value return moment(this.getDataValue(‘createdAt’)).format(‘YYYY-MM-DD HH:mm’) } }, updatedAt: { type: Sequelize.DATE, defaultValue: Sequelize.NOW, get() { return moment(this.getDataValue(‘updatedAt’)).format(‘YYYY-MM-DD HH:mm’) } }},{ // sequelize会自动使用传入的模型名(define的第一个参数)的复数做为表名 设置true取消默认设置 freezeTableName: true})module.exports = tagcontroller => tag.s 定义了 create、findAll、findAndCountAll、destroy 方法const Tag = require(’../model/tag’)const Op = require(‘sequelize’).Opconst listAll = async (ctx) => { const data = await Tag.findAll() ctx.body = { code: 1000, data }}const list = async (ctx) => { const query = ctx.query const where = { name: { [Op.like]: %${query.name}% } } const {rows:data, count: total } = await Tag.findAndCountAll({ where, offset: (+query.pageNo - 1) * +query.pageSize, limit: +query.pageSize, order: [ [‘createdAt’, ‘DESC’] ] }) ctx.body = { data, total, code: 1000, desc: ‘success’ }}const create = async (ctx) => { const params = ctx.request.body if (!params.name) { ctx.body = { code: 1003, desc: ‘标签不能为空’ } return false } try { await Tag.create(params) ctx.body = { code: 1000, data: ‘创建成功’ } } catch(err) { const msg = err.errors[0] ctx.body = { code: 300, data: msg.value + msg.message } }}const destroy = async ctx => { await Tag.destroy({where: ctx.request.body}) ctx.body = { code: 1000, desc: ‘删除成功’ }}module.exports = { list, create, listAll, destroy在 routers 文件夹 index.js 中引入定义好的 tag controller ,定义路由const router = require(‘koa-router’)()const Tag = require(’../controllers/tag’)// tagrouter.get(’/tag/list’, Tag.list)router.get(’/tag/list/all’, Tag.listAll)router.post(’/tag/create’, Tag.create)router.post(’/tag/destroy’, Tag.destroy)module.exports = router/* 如每个 route 是单独的文件,可以使用 router.prefix 定义路由前缀router.prefix(’/tag’)router.get(’/list’, Tag.list)router.get(’/list/all’, Tag.listAll)router.post(’/create’, Tag.create)router.post(’/destroy’, Tag.destroy)*/因为 app 中 已经引入 routers 中的 index.js 调用了 app.use了,所以此处不需再引入在浏览器里输入 localhost:3000/tag/list 就可以看到返回的数据结构了,只不过 data 为空数组,因为我们还没添加进去任何数据到这里,model 定义表结构、sequelize操作数据库、koa-router 定义路由 这一套流程算是完成了,其他表结构,接口 都是一样定义的总结之前没有写过 node server 和 react,算是从零搭建该博客,踩了一些坑,也学到了很多东西,譬如react 开发模式、react-router、sequelize 操作mysql的crud、koa、nginx的配置等等。麻雀虽小,也是一次完整的前后端开发体验,脱离了浏览器的限制,像海贼王一样,打开了新世界的大门,寻找 onepiece ……web端源码server端源码详细的 server 端说明后续会在个人博客中添加关于此次部署文章Linksreactreact-router4antdreact-draft-wysiwygkoa2sequelize初尝 react + Node,错误之处还望斧正,欢迎提 issue ...

April 1, 2019 · 3 min · jiezi

# react-router v4 刷新出现找不到页面(NO FOUND)解决方案

react-router v4 刷新页面出现找不到问题解决方案原因### 浏览器被刷新相当于重新请求了服务端的 page 接口,当后端没有这个接口时,就没有document文档返回,这时url 并没有被js 观察处理解决如果是使用webpack-dev-server,请将 historyApiFallback: true 这个配置加入至 devServer 中. 以及在output 中配置 publicPath: ‘/‘如果是使用自定义的node服务器的话,需自己手写一个404接口. 将所有的url 都返回到index.html文档实例koaconst koaWebpack = require(‘koa-webpack’);async startService() {const middleware = await koaWebpack({ config: this.webpackConfig });this.app.use(middleware);app.use(async ctx => { const filename = path.resolve(this.webpackConfig.output.path, ‘index.html’); ctx.response.type = ‘html’; ctx.response.body = middleware.devMiddleware.fileSystem.createReadStream( filename );});this.app.listen(this.port, () => { console.log(当前服务器已启动, http://${this.host}:${this.port});});}[参考地址][1]

March 31, 2019 · 1 min · jiezi

node(koa2) web应用模块介绍

在自己的koa2 web项目中,用到了几个模块,感觉都是不错的,特地来分享下这些模块。一、前言我们都知道可以通过koa2 工程名的方式来初始化koa2项目,官方为我们增加了koa-bodyparser、koa-josn、koa-router等非常不错的模块,但是,仍不够,所以我将搜集到的有用的包介绍下,当然,有好的包仍然会添加到其中。整个项目在koa2-web-engine ,为了方便查看,使用了原生的方式,欢迎查看。二、新的模块将代码克隆到本地并安装依赖后,启动服务器,在3000端口可以看到所有demo。验证码svg-captcha是一个验证码的库,他创建了svg格式的验证码,可以在登录时,验证是否是正常的用户登录。使用十分的简单:const svgCaptcha = require(‘svg-captcha’);captcha = svgCaptcha.create();captcha对象中包含了svg数据和svg上显示的内容,至于是否要大小写强制验证就可以通过配置的方式来增加了。处理代码位于routes/verificationCode.js中。密码加密登录后端主要是利用node-rsa生成公钥和私钥,再将公钥发送给前端,前端利用jsencrypt进行加密后发送给node,node再用私钥解密。为了性能,我只在服务器启动的时候生成公钥和私钥,以后的请求都是用这队公私钥,他位于utils/RSA.js文件中,解密在routes/login.js中。更详细的可以查看我的这篇博客:基于node简单实现RSA加解密。参数类型检测为了服务器的安全性,服务器对前端发送来的数据肯定是要做校验的,我这使用的joi库。校验主要靠Joi.validate()方法,第一个参数是要校验的对象数据,第二个参数是数据内每个键对应的数据类型,第三个则是可选的option,返回值是一个对象,该对象下的error字段用于判断此次校验是否成功。在utils/checkParams.js中,paramsFormat定义了检测类型,当然每个类型都得用joi内置的类型,checkParams()函数就是做检测的地方,将最后的检测结果return出去。回到routes/joi.js中,利用checkParams()方法检测数据类型,这儿的检测是针对单个的请求,如果要针对所有的请求,可以写成中间件的形式,如utils/middleware.js中,并在app.js中加入以下的就行了:const middleware = require(’./utils/middleware’);middleware.use(app);防xss这儿用到的是xss模块,将每次请求到的数据经过xss处理,输出到后端。为此我自己搞了koa2-xss中间件模块,顺带学习了如何发布npm包,感兴趣的可以看下。日志记录我是用的是log4js模块,该模块既可以记录到数据库,也可以记录到log文件中,此处我是写到文件中的。utils/logs.js文件中是log4js的配置,并封装了对外的调用接口,routes/log4js.js中是根据用户发送的请求记录到日志文件中。定时任务利用了node-schedule模块,一个系统总会用到定时任务的,node-schedule提供了较为简单的api,使用比较方便。路由合并koa2初始化的项目中是将每个路由文件require到app.js中的,当路由文件变多时,管理这些路由就是件麻烦的事,于是引入了koa-compose来管理这些路由文件,只对外暴露一个接口。详细的可以查看routes/index.js文件。webSocketwebsocket在实时性要求比较高的场景下也是会用到的,我们可以利用ws模块实现。更为详细的可以查看我的这篇文章:基于node实现websocket通信。三、总结后期用到一些有意思,有用的模块也将加入到koa2-web-engine 中。原文地址:http://www.zhuyuntao.cn/2019/…欢迎关注微信公众号[ 我不会前端 ]或扫描下方二维码!

March 28, 2019 · 1 min · jiezi

koa2+vue+mysql 全栈开发记录

koa2+vue2+mysql 全栈开发记录基于想要自己制作一个个人项目为由,于是有了这么一个开发记录(梳理开发过程也是一个知识巩固的过程)koa2+vue2+mysql 个人的一个通用DEMO(本篇文章的范例)koa2+vue2+mysql GITHUB地址前端工具vuevue-routervuexaxioselement ui 页面UI组件echartsjs 百度强大的图表展示vue-admin-template 花裤衩大佬的一个实用管理后台模版 配套教程vue-i18n 国际化scss后端工具koakoa-bodyparser 解析 PUT / POST 请求中的 bodykoa-convertkoa-jsonkoa-jwt jwt鉴权koa-loggerkoa-mysql-sessionkoa-onerrorkoa-routerkoa-session-minimalkoa-statickoa-viewskoa2-cors 处理跨域md5 加密moment 时间处理mysql前端篇前端这边其实没什么好写的,主要是在vue-admin-template基础上做了一些修改src/utils/request.js的修改request拦截器 // request拦截器 service.interceptors.request.use( config => { if (store.getters.token) { // config.headers[‘X-Token’] = getToken() // 因为是jwt方式鉴权 所以在header头中改为如下写法 config.headers[‘Authorization’] = ‘Bearer ’ + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改 } return config }, error => { // Do something with request error console.log(error) // for debug Promise.reject(error) } )response 拦截器 // response 拦截器 service.interceptors.response.use( response => { /** * code为非0是抛错 可结合自己业务进行修改 / const res = response.data if (res.code !== 0) { // 因为后台返回值为0则是成功,所以将原来的20000改为了0 Message({ message: res.message, type: ’error’, duration: 5 * 1000 }) // 70002:非法的token; 50012:其他客户端登录了; 50014:Token 过期了; if (res.code === 70002 || res.code === 50012 || res.code === 50014) { MessageBox.confirm( ‘你已被登出,可以取消继续留在该页面,或者重新登录’, ‘确定登出’, { confirmButtonText: ‘重新登录’, cancelButtonText: ‘取消’, type: ‘warning’ } ).then(() => { store.dispatch(‘FedLogOut’).then(() => { location.reload() // 为了重新实例化vue-router对象 避免bug }) }) } return Promise.reject(’error’) } else { return response.data } }, error => { console.log(’err’ + error) // for debug Message({ message: error.message, type: ’error’, duration: 5 * 1000 }) return Promise.reject(error) } )封装了一个Echart组件具体参考我的另外一个文章 vue中使用echarts 使用记录后端篇构建项目目录通过项目生成器生成koa-generatornpm install -g koa-generatorkoa2 /server && cd /servernpm install安装组件npm i jsonwebtoken koa-jwt koa-mysql-session koa-session-minimal koa2-cors md5 moment mysql save –save配置app.js const Koa = require(‘koa’) const jwt = require(‘koa-jwt’) const app = new Koa() const views = require(‘koa-views’) const json = require(‘koa-json’) const onerror = require(‘koa-onerror’) const bodyparser = require(‘koa-bodyparser’) const logger = require(‘koa-logger’) const convert = require(‘koa-convert’); var session = require(‘koa-session-minimal’) var MysqlStore = require(‘koa-mysql-session’) var config = require(’./config/default.js’) var cors = require(‘koa2-cors’) const users = require(’./routes/users’) const account = require(’./routes/account’) // error handler onerror(app) // 配置jwt错误返回 app.use(function(ctx, next) { return next().catch(err => { if (401 == err.status) { ctx.status = 401 ctx.body = ApiErrorNames.getErrorInfo(ApiErrorNames.INVALID_TOKEN) // ctx.body = { // // error: err.originalError ? err.originalError.message : err.message // } } else { throw err } }) }) // Unprotected middleware app.use(function(ctx, next) { if (ctx.url.match(/^/public/)) { ctx.body = ‘unprotected\n’ } else { return next() } }) // Middleware below this line is only reached if JWT token is valid app.use( jwt({ secret: config.secret, passthrough: true }).unless({ path: [//register/, //user/login/] }) ) // middlewares app.use(convert(bodyparser({ enableTypes:[‘json’, ‘form’, ’text’] }))) app.use(convert(json())) app.use(convert(logger())) app.use(require(‘koa-static’)(__dirname + ‘/public’)) app.use(views(__dirname + ‘/views’, { extension: ‘pug’ })) // logger app.use(async (ctx, next) => { const start = new Date() await next() const ms = new Date() - start console.log(${ctx.method} ${ctx.url} - ${ms}ms) }) // cors app.use(cors()) // routes app.use(users.routes(), users.allowedMethods()) app.use(account.routes(), account.allowedMethods()) // error-handling app.on(’error’, (err, ctx) => { console.error(‘server error’, err, ctx) }); module.exports = app新建config文件夹用于存放数据库连接等操作default.js// 数据库配置 const config = { port: 3000, database: { DATABASE: ‘xxx’, //数据库 USERNAME: ‘root’, //用户 PASSWORD: ‘xxx’, //密码 PORT: ‘3306’, //端口 HOST: ‘127.0.0.1’ //服务ip地址 }, secret: ‘jwt_secret’ } module.exports = config数据库相关(mysql)createTables.js 一个简单的用户角色权限表 用户、角色、权限表的关系(mysql)// 数据库表格创建const createTable = { users: CREATE TABLE IF NOT EXISTS user_info ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '(自增长)', user_id VARCHAR ( 100 ) NOT NULL COMMENT '账号', user_name VARCHAR ( 100 ) NOT NULL COMMENT '用户名', user_pwd VARCHAR ( 100 ) NOT NULL COMMENT '密码', user_head VARCHAR ( 225 ) COMMENT '头像', user_mobile VARCHAR ( 20 ) COMMENT '手机', user_email VARCHAR ( 64 ) COMMENT '邮箱', user_creatdata TIMESTAMP NOT NULL DEFAULT NOW( ) COMMENT '注册日期', user_login_time TIMESTAMP DEFAULT NOW( ) COMMENT '登录时间', user_count INT COMMENT '登录次数' ) ENGINE = INNODB charset = utf8;, role: CREATE TABLE IF NOT EXISTS role_info ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '(自增长)', role_name VARCHAR ( 20 ) NOT NULL COMMENT '角色名', role_description VARCHAR ( 255 ) DEFAULT NULL COMMENT '描述' ) ENGINE = INNODB charset = utf8;, permission: CREATE TABLE IF NOT EXISTS permission_info ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '(自增长)', permission_name VARCHAR ( 20 ) NOT NULL COMMENT '权限名', permission_description VARCHAR ( 255 ) DEFAULT NULL COMMENT '描述' ) ENGINE = INNODB charset = utf8;, userRole: CREATE TABLE IF NOT EXISTS user_role ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '(自增长)', user_id INT NOT NULL COMMENT '关联用户', role_id INT NOT NULL COMMENT '关联角色', KEY fk_user_role_role_info_1 ( role_id ), KEY fk_user_role_user_info_1 ( user_id ), CONSTRAINT fk_user_role_role_info_1 FOREIGN KEY ( role_id ) REFERENCES role_info ( id ) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT fk_user_role_user_info_1 FOREIGN KEY ( user_id ) REFERENCES user_info ( id ) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE = INNODB charset = utf8;, rolePermission: CREATE TABLE IF NOT EXISTS role_permission ( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '(自增长)', role_id INT NOT NULL COMMENT '关联角色', permission_id INT NOT NULL COMMENT '关联权限', KEY fk_role_permission_role_info_1 ( role_id ), KEY fk_role_permission_permission_info_1 ( permission_id ), CONSTRAINT fk_role_permission_role_info_1 FOREIGN KEY ( role_id ) REFERENCES role_info ( id ) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT fk_role_permission_permission_info_1 FOREIGN KEY ( permission_id ) REFERENCES permission_info ( id ) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE = INNODB charset = utf8;}module.exports = createTable创建lib文件夹 用于存储数据库查询语句mysql.jsconst mysql = require(‘mysql’)const config = require(’../config/default’)const createTables = require(’../config/createTables.js’)var pool = mysql.createPool({ host: config.database.HOST, user: config.database.USERNAME, password: config.database.PASSWORD, database: config.database.DATABASE})let query = function(sql, values) { return new Promise((resolve, reject) => { pool.getConnection(function(err, connection) { if (err) { resolve(err) } else { connection.query(sql, values, (err, rows) => { if (err) { reject(err) } else { resolve(rows) } connection.release() }) } }) })}let createTable = function(sql) { return query(sql, [])}// 建表// createTable(createTables.users)// createTable(createTables.role)// createTable(createTables.permission)// createTable(createTables.userRole)// createTable(createTables.rolePermission)// 查询用户是否存在let findUser = async function(id) { let _sql = SELECT * FROM user_info where user_id="${id}" limit 1; let result = await query(_sql) if (Array.isArray(result) && result.length > 0) { result = result[0] } else { result = null } return result}// 查询用户以及用户角色let findUserAndRole = async function(id) { let _sql = SELECT u.*,r.role_name FROM user_info u,user_role ur,role_info r where u.id=(SELECT id FROM user_info where user_id="${id}" limit 1) and ur.user_id=u.id and r.id=ur.user_id limit 1; let result = await query(_sql) if (Array.isArray(result) && result.length > 0) { result = result[0] } else { result = null } return result}// 更新用户登录次数和登录时间let UpdataUserInfo = async function(value) { let _sql = ‘UPDATE user_info SET user_count = ?, user_login_time = ? WHERE id = ?;’ return query(_sql, value)}module.exports = { //暴露方法 createTable, findUser, findUserAndRole, UpdataUserInfo, getShopAndAccount}koa 路由配置接口报错信息统一方法 创建error文件夹ApiErrorNames.js/* * API错误名称 /var ApiErrorNames = {};ApiErrorNames.UNKNOW_ERROR = “UNKNOW_ERROR”;ApiErrorNames.SUCCESS = “SUCCESS”;/ 参数错误:10001-19999 /ApiErrorNames.PARAM_IS_INVALID = ‘PARAM_IS_INVALID’;ApiErrorNames.PARAM_IS_BLANK = ‘PARAM_IS_BLANK’;ApiErrorNames.PARAM_TYPE_BIND_ERROR = ‘PARAM_TYPE_BIND_ERROR’;ApiErrorNames.PARAM_NOT_COMPLETE = ‘PARAM_NOT_COMPLETE’;/ 用户错误:20001-29999*/ApiErrorNames.USER_NOT_LOGGED_IN = ‘USER_NOT_LOGGED_IN’;ApiErrorNames.USER_LOGIN_ERROR = ‘USER_LOGIN_ERROR’;ApiErrorNames.USER_ACCOUNT_FORBIDDEN = ‘USER_ACCOUNT_FORBIDDEN’;ApiErrorNames.USER_NOT_EXIST = ‘USER_NOT_EXIST’;ApiErrorNames.USER_HAS_EXISTED = ‘USER_HAS_EXISTED’;/* 业务错误:30001-39999 /ApiErrorNames.SPECIFIED_QUESTIONED_USER_NOT_EXIST = ‘SPECIFIED_QUESTIONED_USER_NOT_EXIST’;/ 系统错误:40001-49999 /ApiErrorNames.SYSTEM_INNER_ERROR = ‘SYSTEM_INNER_ERROR’;/ 数据错误:50001-599999 /ApiErrorNames.RESULE_DATA_NONE = ‘RESULE_DATA_NONE’;ApiErrorNames.DATA_IS_WRONG = ‘DATA_IS_WRONG’;ApiErrorNames.DATA_ALREADY_EXISTED = ‘DATA_ALREADY_EXISTED’;/ 接口错误:60001-69999 /ApiErrorNames.INTERFACE_INNER_INVOKE_ERROR = ‘INTERFACE_INNER_INVOKE_ERROR’;ApiErrorNames.INTERFACE_OUTTER_INVOKE_ERROR = ‘INTERFACE_OUTTER_INVOKE_ERROR’;ApiErrorNames.INTERFACE_FORBID_VISIT = ‘INTERFACE_FORBID_VISIT’;ApiErrorNames.INTERFACE_ADDRESS_INVALID = ‘INTERFACE_ADDRESS_INVALID’;ApiErrorNames.INTERFACE_REQUEST_TIMEOUT = ‘INTERFACE_REQUEST_TIMEOUT’;ApiErrorNames.INTERFACE_EXCEED_LOAD = ‘INTERFACE_EXCEED_LOAD’;/ 权限错误:70001-79999 /ApiErrorNames.PERMISSION_NO_ACCESS = ‘PERMISSION_NO_ACCESS’;ApiErrorNames.INVALID_TOKEN = ‘INVALID_TOKEN’;/* * API错误名称对应的错误信息 /const error_map = new Map();error_map.set(ApiErrorNames.SUCCESS, { code: 0, message: ‘成功’ });error_map.set(ApiErrorNames.UNKNOW_ERROR, { code: -1, message: ‘未知错误’ });/ 参数错误:10001-19999 /error_map.set(ApiErrorNames.PARAM_IS_INVALID, { code: 10001, message: ‘参数无效’ });error_map.set(ApiErrorNames.PARAM_IS_BLANK, { code: 10002, message: ‘参数为空’ });error_map.set(ApiErrorNames.PARAM_TYPE_BIND_ERROR, { code: 10003, message: ‘参数类型错误’ });error_map.set(ApiErrorNames.PARAM_NOT_COMPLETE, { code: 10004, message: ‘参数缺失’ });/ 用户错误:20001-29999*/error_map.set(ApiErrorNames.USER_NOT_LOGGED_IN, { code: 20001, message: ‘用户未登录’ });error_map.set(ApiErrorNames.USER_LOGIN_ERROR, { code: 20002, message: ‘账号不存在或密码错误’ });error_map.set(ApiErrorNames.USER_ACCOUNT_FORBIDDEN, { code: 20003, message: ‘账号已被禁用’ });error_map.set(ApiErrorNames.USER_NOT_EXIST, { code: 20004, message: ‘用户不存在’ });error_map.set(ApiErrorNames.USER_HAS_EXISTED, { code: 20005, message: ‘用户已存在’ });/* 业务错误:30001-39999 /error_map.set(ApiErrorNames.SPECIFIED_QUESTIONED_USER_NOT_EXIST, { code: 30001, message: ‘某业务出现问题’ });/ 系统错误:40001-49999 /error_map.set(ApiErrorNames.SYSTEM_INNER_ERROR, { code: 40001, message: ‘系统繁忙,请稍后重试’ });/ 数据错误:50001-599999 /error_map.set(ApiErrorNames.RESULE_DATA_NONE, { code: 50001, message: ‘数据未找到’ });error_map.set(ApiErrorNames.DATA_IS_WRONG, { code: 50002, message: ‘数据有误’ });error_map.set(ApiErrorNames.DATA_ALREADY_EXISTED, { code: 50003, message: ‘数据已存在’ });/ 接口错误:60001-69999 /error_map.set(ApiErrorNames.INTERFACE_INNER_INVOKE_ERROR, { code: 60001, message: ‘内部系统接口调用异常’ });error_map.set(ApiErrorNames.INTERFACE_OUTTER_INVOKE_ERROR, { code: 60002, message: ‘外部系统接口调用异常’ });error_map.set(ApiErrorNames.INTERFACE_FORBID_VISIT, { code: 60003, message: ‘该接口禁止访问’ });error_map.set(ApiErrorNames.INTERFACE_ADDRESS_INVALID, { code: 60004, message: ‘接口地址无效’ });error_map.set(ApiErrorNames.INTERFACE_REQUEST_TIMEOUT, { code: 60005, message: ‘接口请求超时’ });error_map.set(ApiErrorNames.INTERFACE_EXCEED_LOAD, { code: 60006, message: ‘接口负载过高’ });/ 权限错误:70001-79999 /error_map.set(ApiErrorNames.PERMISSION_NO_ACCESS, { code: 70001, message: ‘无访问权限’ });error_map.set(ApiErrorNames.INVALID_TOKEN, { code: 70002, message: ‘无效token’ });//根据错误名称获取错误信息ApiErrorNames.getErrorInfo = (error_name) => { var error_info; if (error_name) { error_info = error_map.get(error_name); } //如果没有对应的错误信息,默认’未知错误’ if (!error_info) { error_name = UNKNOW_ERROR; error_info = error_map.get(error_name); } return error_info;}//返回正确信息ApiErrorNames.getSuccessInfo = (data) => { var success_info; let name = ‘SUCCESS’; success_info = error_map.get(name); if (data) { success_info.data = data } return success_info;}module.exports = ApiErrorNames;创建controller文件夹对路由users进行逻辑编写const mysqlModel = require(’../lib/mysql’) //引入数据库方法const jwt = require(‘jsonwebtoken’)const config = require(’../config/default.js’)const ApiErrorNames = require(’../error/ApiErrorNames.js’)const moment = require(‘moment’)/* * 普通登录 /exports.login = async (ctx, next) => { const { body } = ctx.request try { const user = await mysqlModel.findUser(body.username) if (!user) { // ctx.status = 401 ctx.body = ApiErrorNames.getErrorInfo(ApiErrorNames.USER_NOT_EXIST) return } let bodys = await JSON.parse(JSON.stringify(user)) // 匹配密码是否相等 if ((await user.user_pwd) === body.password) { let data = { user: user.user_id, // 生成 token 返回给客户端 token: jwt.sign( { data: user.user_id, // 设置 token 过期时间 exp: Math.floor(Date.now() / 1000) + 60 * 60 // 60 seconds * 60 minutes = 1 hour }, config.secret ) } ctx.body = ApiErrorNames.getSuccessInfo(data) } else { ctx.body = ApiErrorNames.getErrorInfo(ApiErrorNames.USER_LOGIN_ERROR) } } catch (error) { ctx.throw(500) }}/* * 获取用户信息 /exports.info = async (ctx, next) => { const { body } = ctx.request // console.log(body) try { const token = ctx.header.authorization let payload if (token) { payload = await jwt.verify(token.split(’ ‘)[1], config.secret) // 解密,获取payload const user = await mysqlModel.findUserAndRole(payload.data) if (!user) { ctx.body = ApiErrorNames.getErrorInfo(ApiErrorNames.USER_NOT_EXIST) } else { let cont = user.user_count + 1 let updateInfo = [ cont, moment().format(‘YYYY-MM-DD HH:mm:ss’), user.id ] await mysqlModel .UpdataUserInfo(updateInfo) .then(res => { let data = { avatar: ‘https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', name: user.user_id, // roles: [user.user_admin === 0 ? ‘admin’ : ‘’] roles: [user.role_name] } ctx.body = ApiErrorNames.getSuccessInfo(data) }) .catch(err => { ctx.body = ApiErrorNames.getErrorInfo(ApiErrorNames.DATA_IS_WRONG) }) } } else { ctx.body = ApiErrorNames.getErrorInfo(ApiErrorNames.INVALID_TOKEN) } } catch (error) { ctx.throw(500) }}/* * 退出登录 */exports.logout = async (ctx, next) => { try { // ctx.status = 200 ctx.body = ApiErrorNames.getSuccessInfo() } catch (error) { ctx.throw(500) }}routes中的users.jsconst router = require(‘koa-router’)() //引入路由函数const userControl = require(’../controller/users’) //引入逻辑// const config = require(’../config/default.js’)router.get(’/’, async (ctx, next) => { ‘use strict’ ctx.redirect(’/user/login’)})// 路由中间间,页面路由到/,就是端口号的时候,(网址),页面指引到//user/loginrouter.get(’/user/info’, userControl.info)router.post(’/user/logout’, userControl.logout)router.post(’/user/login’, userControl.login)module.exports = router//将页面暴露出去备注ctx.request 获取post请求中的bodyctx.query.xx 获取get请求中的参数router.prefix(’/account’) 给router实例添加前缀 ...

March 17, 2019 · 8 min · jiezi

koa-rapid-router超越koa-router性能的100多倍

对比如果使用nodejs来搭建Service服务,那么我们首选express或者koa,而fastify告诉我们一个数据:FrameworkVersionRouter?Requests/sechapi18.1.0✓29,998Express4.16.4✓38,510Restify8.0.0✓39,331Koa2.7.0✗50,933Fastify2.0.0✓76,835- http.Server10.15.2✗71,768从数据中可以看出,Koa的性能远大于express。当然,它的测试基于简单的单路由测试。不过由此我们可以看到fastify的性能远大于Koa。相信使用过fastify的小伙伴都会对它的性能速度感到惊讶。其实原理很简单,就是请求的URL快速匹配Callback。如何做到,理论上也很简单,就是找寻它的最短路径来匹配。所以一般能够快速匹配的,都是通过空间换时间的方式来达到效果。这里,我还想告诉大家一点,fastify并不是最快的。主角今天的主角就是koa-rapid-router。为什么我们会以KOA打头呢?因为这篇文章目的其实是与koa-router的比较,而不是fastify。而此路由架构,也是为了在使用KOA的时候能够接近fastify的性能(经过测试,没有超过fastify,KOA本身的性能也有问题)。接下来,我们会抛出一系列的测试数据来告诉大家Koa-router的性能是何其糟糕。我们分别使用这样的原则来测试向每个架构注入10000个静态路由,测试最末尾的那个。使用相同的测试命令 autocannon -c 100 -d 40 -p 10 <url>对比静态路由和动态路由性能上的差距测试代码全部在这里静态路由对比我们写入如下的代码for (let i = 0; i < 10000; i++) { router.get(’/uuid/’ + (i + 1), async (ctx) => ctx.body = ‘ok’); vrouter.get(’/uuid/’ + (i + 1), (res) => res.end(‘ok’)); route_2.get(’/interface/api/uuid/’ + (i + 1), async (ctx) => ctx.body = ‘ok’); fastify.get(’/interface/api/uuid/’ + (i + 1), (request, reply) => reply.send(‘ok’));}接着进行测试 npm run test,得到数据:Preview:ResultscommandarchitectureLatencyReq/SecBytes/Sectest:koakoa + koa-router245.07 ms394.2556 kBtest:fastfastify1.96 ms493247 MBtest:rapidkoa + koa-rapid-router2.17 ms44828.86.37 MBtest:httphttp + koa-rapid-router1.64 ms58911.25.95 MB从数据上得出结论,koa-router在有10000个路由的时候,它的性能超级低下,只能达到平均的394.25,意思就是每秒只能处理394.25个请求,多来就不行。而koa + koa-rapid-router则处理到了44828.8个。同样是使用KOA模型,差距很明显。我做了分析,主要是koa-router内部循环比较多导致的。在10000个请求循环过程中,效率非常低下。而我们如何做到达到44828.8的性能,主要我们在内存中维护了一份静态路由列表,能让程序以最快的速度找到我们需要的callback。对比fastify,可以看出,KOA本身性能的问题很大。大家一定会问,对比静态路由Koa-router肯定没有优势,那么我们来对比动态路由。动态路由对比我们写入如下代码router.get(’/zzz/{a:number}’, async (ctx) => ctx.body = ‘ok’);vrouter.get(’/zzz/{a:number}’, (res) => res.end(‘ok’));route_2.get(’/interface/api/zzz/:a(\d+)’, async (ctx) => ctx.body = ‘ok’);fastify.get(’/interface/api/zzz/:a’, (request, reply) => reply.send(‘ok’));我们将这段代码加入到10000个静态路由代码的后面,修正测试的路径,我们得到如下数据:ResultscommandarchitectureLatencyReq/SecBytes/Sectest:koakoa + koa-router220.29 ms441.7562.7 kBtest:fastfastify1.9 ms50988.657.24 MBtest:rapidkoa + koa-rapid-router2.32 ms41961.65.96 MBtest:httphttp + koa-rapid-router1.82 ms53160.85.37 MB动态路由的对比从一定程度上可以看出koa-router的糟糕之处,不论是静态路由还是动态路由,它都基本稳定在400左右的qps。而koa + koa-rapid-router稍有下降,fastify一如既往的稳定。但是从http + koa-rapid-router模型上看,rapid完全超越fastify。koa + koa-rapid-router与koa + koa-router对比,性能大概是100倍的样子。如果我们可以这样认定,如果我们需要高并发,但是还是使用koa的生态的话,koa + koa-rapid-router是最佳选择。如果我们完全追求性能,不考虑生态的话,那么fastify首选。有人会问,那么为什么http + koa-rapid-router不使用,它可是比fastify更快的路由?那是因为,http + koa-rapid-router需要单独建立生态,暂时无法做到大规模使用,也许到最后,我们可以用上新的基于koa-rapid-router的企业级服务架构。这也是我正在思考的。结尾我们所造的轮子的性能是不可能超越http模块的性能,我们只能无限接近它。这就像光速的道理一样,只能接近,无法等于。高性能架构主要还是在于理念模型,跟数学息息相关。项目开源在 https://github.com/cevio/koa-rapid-router 有兴趣的小伙伴关注下,谢谢。 ...

March 16, 2019 · 1 min · jiezi

基于react+koa的图片验证码

滑动图片验证码基于 react 和 koa2 的一个图片滑动验证码效果图使用git clone https://gitee.com/darcrandex/image-code.git// 前端cd image-code/clientnpm inpm start// 后端cd image-code/servernpm inpm start业务逻辑前端请求数据后台返回主图片,和小滑块图片前端交互,滑动之后,获取滑动的 x 值将用户信息和 x 传给后台后台判断是否正确,返回信息给前端后端这里主要是图片处理的问题,尝试过node-canvas,node-images,node-sharp。但是都存在安装不了或者需要安装很麻烦的依赖库的问题。最后选择node-gm。基本上可以满足需求。不过还是需要安装一个依赖库,GraphicsMagick或者ImageMagick。推荐是GraphicsMagick,但其实ImageMagick也够用了。关于安装ImageMagick。我的环境是windows,除了安装软件之外,还需要配置windows 环境变量。网上查一下好了。前端前端部分没有什么大的问题。只有axios需要配置一下(/src/utils/axios.js),主要是跨越的问题。如果不使用axios,就根据情况解决跨域就可以了。

March 16, 2019 · 1 min · jiezi

KOA2 Restful方式路由初探

前言最近考虑将服务器资源整合一下,作为多端调用的API看到Restful标准和ORM眼前一亮,但是找了不少版本路由写的都比较麻烦,于是自己折腾了半天API库结构考虑到全部对象置于顶层将会造成对象名越来长,同时不便于维护,故采取部分的分层结构如workflow模块内的prototypes,instances等等,分层的深度定义为层级可访问的对象集合(collection)的属性满足Restful设计 – workflow(category) – prototypes(collection) – [method] … – [method] … – instances(collection) – users(collection) –[method] List #get :object/ –[method] Instance #get :object/:id – … – …RESTFUL API 接口将Restful API接口进行标准化命名.get(’/’, ctx=>{ctx.error(‘路径匹配失败’)}) .get(’/:object’, RestfulAPIMethods.List).get(’/:object/:id’, RestfulAPIMethods.Get).post(’/:object’, RestfulAPIMethods.Post).put(’/:object/:id’, RestfulAPIMethods.Replace).patch(’/:object/:id’, RestfulAPIMethods.Patch).delete(’/:object/:id’, RestfulAPIMethods.Delete).get(’/:object/:id/:related’, RestfulAPIMethods.Related).post(’/:object/:id/:related’, RestfulAPIMethods.AddRelated).delete(’/:object/:id/:related/:relatedId’, RestfulAPIMethods.DelRelated)API对象这个文件是来自微信小程序demo,觉得很方便就拿来用了,放于需要引用的根目录,引用后直接获得文件目录结构API对象const _ = require(’lodash’)const fs = require(‘fs’)const path = require(‘path’)/** * 映射 d 文件夹下的文件为模块 */const mapDir = d => { const tree = {} // 获得当前文件夹下的所有的文件夹和文件 const [dirs, files] = _(fs.readdirSync(d)).partition(p => fs.statSync(path.join(d, p)).isDirectory()) // 映射文件夹 dirs.forEach(dir => { tree[dir] = mapDir(path.join(d, dir)) }) // 映射文件 files.forEach(file => { if (path.extname(file) === ‘.js’) { tree[path.basename(file, ‘.js’)] = require(path.join(d, file)) tree[path.basename(file, ‘.js’)].isCollection = true } }) return tree}// 默认导出当前文件夹下的映射module.exports = mapDir(path.join(__dirname))koa-router分层路由的实现创建多层路由及其传递关系执行顺序为 1 – 路径匹配 – 匹配到‘/’结束 – 匹配到对应的RestfulAPI执行并结束 – 继续 2 – 传递中间件 Nest 3 – 下一级路由 4 – 循环 to 1const DefinedRouterDepth = 2let routers = []for (let i = 0; i < DefinedRouterDepth; i++) { let route = require(‘koa-router’)() if (i == DefinedRouterDepth - 1) { // 嵌套路由中间件 route.use(async (ctx, next) => { // 根据版本号选择库 let apiVersion = ctx.headers[‘api-version’] ctx.debug(------- (API版本 [${apiVersion}]) --=-------) if (!apiVersion) { ctx.error(‘版本号未标记’) return } let APIRoot = null try { APIRoot = require(../restful/${apiVersion}) } catch (e) { ctx.error (‘API不存在,请检查Header中的版本号’) return } ctx.debug(APIRoot) ctx.apiRoot = APIRoot ctx.debug(’———————————————’) // for(let i=0;i<) await next() }) } route .get(’/’, ctx=>{ctx.error(‘路径匹配失败’)}) .get(’/:object’, RestfulAPIMethods.List) .get(’/:object/:id’, RestfulAPIMethods.Get) .post(’/:object’, RestfulAPIMethods.Post) .put(’/:object/:id’, RestfulAPIMethods.Replace) .patch(’/:object/:id’, RestfulAPIMethods.Patch) .delete(’/:object/:id’, RestfulAPIMethods.Delete) .get(’/:object/:id/:related’, RestfulAPIMethods.Related) .post(’/:object/:id/:related’, RestfulAPIMethods.AddRelated) .delete(’/:object/:id/:related/:relatedId’, RestfulAPIMethods.DelRelated) if (i != 0) { route.use(’/:object’, Nest, routers[i - 1].routes()) } routers.push(route)}let = router = routers[routers.length - 1]Nest中间件将ctx.apiObject设置为当前层的API对象const Nest= async (ctx, next) => { let object = ctx.params.object let apiObject = ctx.apiObject || ctx.apiRoot if(!apiObject){ ctx.error(‘API装载异常’) return } if (apiObject[object]) { ctx.debug(ctx.apiObject=&gt;ctx.apiObject[object]) ctx.debug(apiObject[object]) ctx.debug(------------------------------------) ctx.apiObject = apiObject[object] } else { ctx.error(API接口${object}不存在) return } await next()}RestfulAPIMethodslet RestfulAPIMethods = {}let Methods = [‘List’, ‘Get’, ‘Post’, ‘Replace’, ‘Patch’, ‘Delete’, ‘Related’, ‘AddRelated’, ‘DelRelated’]for (let i = 0; i < Methods.length; i++) { let v = Methods[i] RestfulAPIMethods[v] = async function (ctx, next) { let apiObject = ctx.apiObject || ctx.apiRoot if (!apiObject) { ctx.error (‘API装载异常’) return } let object = ctx.params.object if (apiObject[object] && apiObject[object].isCollection) { ctx.debug(--- Restful API [${v}] 调用---) if (typeof apiObject[object][v] == ‘function’) { ctx.state.data = await apiObject[object]v ctx.debug(‘路由结束’) return //ctx.debug(ctx.state.data) } else { ctx.error(对象${object}不存在操作${v}) return } } ctx.debug(--- 当前对象${object}并不是可访问对象 ---) await next() }}需要注意的点1、koa-router的调用顺序2、涉及到async注意next()需要加await ...

March 14, 2019 · 2 min · jiezi

一个基于material-ui+react+koa2+mongoose的个人博客系统

前言做这玩意主要是有两个目的,练习平时工作中用不到的技术点,在熟练的基础之上去研究其原理。可能的话,替换掉自己的博客系统。项目地址: https://github.com/2fps/blooog前端前端是基于react的,用到了react-router和redux。UI库主要是material-ui,当然css-in-js的方式还只是会使用,抽空去了解下原理。项目截图就不放了,demo地址:http://132.232.131.250:3000 。用户名和密码都是admin。实现的功能文章的显示、编辑和删除功能。标签的显示、编辑和删除功能。站点信息的配置和显示。登录和修改密码功能。后端后端基于koa2和mongoose。实现的功能加密登录。log4js日志记录功能。joi对数据进行验证。已知问题审美不太好,只觉得别人的界面好,自己搞起来就那样。。后端安全没有做好,没有防xss等。前端代码较乱,还未整理,公共方法未剥离。数据库没有使用事务。没有对数据做缓存。等等。后续待加入菜单。评论。等等。。

March 10, 2019 · 1 min · jiezi

Cookie和Session的区别,Koa2+Mysql+Redis实现登录逻辑

为什么需要登录态?因为需要识别用户是谁,否则怎么在网站上看到个人相关信息呢?为什么需要登录体系?因为HTTP是无状态的,什么是无状态呢?就是说这一次请求和上一次请求是没有任何关系的,互不认识的,没有关联的。我们的网站都是靠HTTP请求服务端获得相关数据,因为HTTP是无状态的,所以我们无法知道用户是谁。所以我们需要其他方式保障我们的用户数据。当然了,这种无状态的的好处是快速。什么叫保持登录状态?比如说我在百度A页面进行了登录,但是不找个地方记录这个登录态的话。那我去B页面,我的登录态怎么保持呢?难道要url携带吗?这肯定是不安全的。你让用户再登录一次?登个鬼,再见???? 用户体验不友好。所以我们需要找个地方,存储用户的登录数据。这样可以给用户良好的用户体验。但是这个状态一般是有保质期的,主要原因也是为了安全。为了解决这个问题,Cookie出现了。CookieCookie的作用就是为了解决HTTP协议无状态的缺陷所作的努力。Cookie是存在浏览器端的。也就是可以存储我们的用户信息。一般Cookie 会根据从服务器端发送的响应的一个叫做Set-Cookie的首部字段信息,通知浏览器保存Cookie。当下次发送请求时,会自动在请求报文中加入Cookie 值后发送出去。当然我们也可以自己操作Cookie。如下图所示(图来源《图解HTTP》)这样我们就可以通过Cookie中的信息来和服务端通信。服务端如何配合?Session!需要看起来Cookie已经达到了保持用户登录态的效果。但是Cookie中存储用户信息,显然不是很安全。所以这个时候我们需要存储一个唯一的标识。这个标识就像一把钥匙一样,比较复杂,看起来没什么规律,也没有用户的信息。只有我们自己的服务器可以知道用户是谁,但是其他人无法模拟。这个时候Session就出现了,Session存储用户会话所需的信息。简单理解主要存储那把钥匙Session_ID,用这个钥匙Session_ID再去查询用户信息。但是这个标识需要存在Cookie中,所以Session机制需要借助于Cookie机制来达到保存标识Session_ID的目的。如下图所示。这个时候你可能会想,那这个Session有啥用?生成了一个复杂的ID,在服务器上存储。那好像我们自己生成一个Session_ID,存在Mysql也可以啊!没错,就是这样!个人认为Session其实已经发展为一个抽象的概念,已经形成了业界的一种解决方案。可能它最开始出现的时候有自己规则,但是现在经过发展。随着业务的复杂,各大公司早就自己实现了方案。Session_id你想搞成什么样,就什么样,想存在哪里就存在哪里。一般服务端会把这个Session_id存在缓存,不会和用户信息表混在一起。一个是为了快速拿到Session_id。第二个是因为前面也讲到过,Session_id是有保质期的,为了安全一段时间就会失效,所以放在缓存里就可以了。常见的就是放在redis、memcached里。也有一些情况放在mysql里的,可能是用户数据比较多。但都不会和用户信息表混在一起。Cookie 和 Session 的区别登录态保持总结浏览器第一次请求网站, 服务端生成 Session ID。把生成的 Session ID 保存到服务端存储中。把生成的 Session ID 返回给浏览器,通过 set-cookie。浏览器收到 Session ID, 在下一次发送请求时就会带上这个 Session ID。服务端收到浏览器发来的 Session ID,从 Session 存储中找到用户状态数据,会话建立。此后的请求都会交换这个 Session ID,进行有状态的会话。登录流程图实现案例(koa2+ Mysql)本案例适合对服务端有一定概念的同学哦,下面仅是核心代码。数据库配置第一步就是进行数据库配置,这里我单独配置了一个文件。因为当项目大起来,需要对开发环境、测试环境、正式的环境的数据库进行区分。let dbConf = null;const DEV = { database: ‘dandelion’, //数据库 user: ‘root’, //用户 password: ‘xxx’, //密码 port: ‘3306’, //端口 host: ‘127.0.0.1’ //服务ip地址}dbConf = DEV;module.exports = dbConf;数据库连接。const mysql = require(‘mysql’);const dbConf = require(’./../config/dbConf’);const pool = mysql.createPool({ host: dbConf.host, user: dbConf.user, password: dbConf.password, database: dbConf.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,}路由配置这里我也是单独抽离出了文件,让路由看起来更舒服,更加好管理。const Router = require(‘koa-router’);const router = new Router();const koaCompose = require(‘koa-compose’);const {login} = require(’../controllers/login’);// 加前缀router.prefix(’/api’);module.exports = () => { // 登录 router.post(’/login’, login); return koaCompose([router.routes(), router.allowedMethods()]);}中间件注册路由。const routers = require(’../routers’);module.exports = (app) => { app.use(routers());}Session_id的生成和存储我的session_id生成用了koa-session2库,存储是存在redis里的,用了一个ioredis库。配置文件。const Redis = require(“ioredis”);const { Store } = require(“koa-session2”); class RedisStore extends Store { constructor() { super(); this.redis = new Redis(); } async get(sid, ctx) { let data = await this.redis.get(SESSION:${sid}); return JSON.parse(data); } async set(session, { sid = this.getID(24), maxAge = 1000 * 60 * 60 } = {}, ctx) { try { console.log(SESSION:${sid}); // Use redis set EX to automatically drop expired sessions await this.redis.set(SESSION:${sid}, JSON.stringify(session), ‘EX’, maxAge / 1000); } catch (e) {} return sid; } async destroy(sid, ctx) { return await this.redis.del(SESSION:${sid}); }} module.exports = RedisStore;入口文件(index.js)const Koa = require(‘koa’);const middleware = require(’./middleware’); //中间件,目前注册了路由const session = require(“koa-session2”); // sessionconst Store = require("./utils/Store.js"); //redisconst body = require(‘koa-body’);const app = new Koa();// session配置app.use(session({ store: new Store(), key: “SESSIONID”,}));// 解析 post 参数app.use(body());// 注册中间件middleware(app);const PORT = 3001;// 启动服务app.listen(PORT);console.log(server is starting at port ${PORT});登录接口实现这里主要是根据用户的账号密码,拿到用户信息。然后将用户uid存储到session中,并将session_id设置到浏览器中。代码很少,因为用了现成的库,人家都帮你做好了。这里我没有把session_id设置过期时间,这样用户关闭浏览器就没了。const UserModel = require(’../model/UserModel’); //用户表相关sql语句const userModel = new UserModel();/** * @description: 登录接口 * @param {account} 账号 * @param {password} 密码 * @return: 登录结果 */async function login(ctx, next) { // 获取用户名密码 get const {account, password} = ctx.request.body; // 根据用户名密码获取用户信息 const userInfo = await userModel.getUserInfoByAccount(account, password); // 生成session_id ctx.session.uid = JSON.stringify(userInfo[0].uid); ctx.body = { mes: ‘登录成功’, data: userInfo[0].uid, success: true, };};module.exports = { login,};登录之后其他的接口就可以通过这个session_id获取到登录态。// 业务接口,获取用户所有的需求const DemandModel = require(’../../model/DemandModel’);const demandModel = new DemandModel();const shortid = require(‘js-shortid’); const Store = require("../../utils/Store.js");const redis = new Store();async function selectUserDemand(ctx, next) { // 判断用户是否登录,获取cookie里的SESSIONID const SESSIONID = ctx.cookies.get(‘SESSIONID’); if (!SESSIONID) { console.log(‘没有携带SESSIONID,去登录吧~’); return false; } // 如果有SESSIONID,就去redis里拿数据 const redisData = await redis.get(SESSIONID); if (!redisData) { console.log(‘SESSIONID已经过期,去登录吧~’); return false; } if (redisData && redisData.uid) { console.log(登录了,uid为${redisData.uid}); } const uid = JSON.parse(redisData.uid); // 根据session里的uid 处理业务逻辑 const data = await demandModel.selectDemandByUid(uid); console.log(data); ctx.body = { mes: ‘’, data, success: true, };};module.exports = { selectUserDemand,}坑点注意注意1、注意跨域问题2、处理OPTIONS多发预检测问题app.use(async (ctx, next) => { ctx.set(‘Access-Control-Allow-Origin’, ‘http://test.xue.com’); ctx.set(‘Access-Control-Allow-Credentials’, true); ctx.set(‘Access-Control-Allow-Headers’, ‘content-type’); ctx.set(‘Access-Control-Allow-Methods’, ‘OPTIONS, GET, HEAD, PUT, POST, DELETE, PATCH’); // 这个响应头的意义在于,设置一个相对时间,在该非简单请求在服务器端通过检验的那一刻起, // 当流逝的时间的毫秒数不足Access-Control-Max-Age时,就不需要再进行预检,可以直接发送一次请求。 ctx.set(‘Access-Control-Max-Age’, 3600 * 24); if (ctx.method == ‘OPTIONS’) { ctx.body = 200; } else { await next(); }});3、允许携带cookie发请求的时候设置这个参数withCredentials: true,请求才能携带cookieaxios({ url: ‘http://test.xue.com:3001/api/login', method: ‘post’, data: { account: this.account, password: this.password, }, withCredentials: true, // 允许设置凭证}).then(res => { console.log(res.data); if (res.data.success) { this.$router.push({ path: ‘/index’ }) }})源码以上的代码只是贴了核心的,源码如下前端 和 后端但是练手的项目还在开发中,网站其他功能还没有全部实现。代码写的比较挫????????????但是登录完全没有问题的~但是你需要提前了解Redis、Mysql、Nginx和基本的服务器操作哦!如有错误,请指教???? ...

March 5, 2019 · 3 min · jiezi

超简单的react服务器渲染(ssr)入坑指南

前言本文是基于react ssr的入门教程,在实际项目中使用还需要做更多的配置和优化,比较适合第一次尝试react ssr的小伙伴们。技术涉及到 koa2 + react,案例使用create-react-app创建SSR 介绍Server Slide Rendering,缩写为 ssr 即服务器端渲染,这个要从SEO说起,目前react单页应用HTML代码是下面这样的<!DOCTYPE html><html lang=“en”> <head> <meta charset=“utf-8” /> <link rel=“shortcut icon” href=“favicon.ico” /> <meta name=“viewport” content=“width=device-width, initial-scale=1, shrink-to-fit=no”/> <meta name=“theme-color” content="#000000" /> <title>React App</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id=“root”></div> <script src="/js/main.js"></script> </body></html>如果main.js 加载比较慢,会出现白屏一闪的现象。传统的搜索引擎爬虫因为不能抓取JS生成后的内容,遇到单页web项目,抓取到的内容啥也没有。在SEO上会吃很多亏,很难排搜索引擎到前面去。React SSR(react服务器渲染)正好解决了这2个问题。React SSR介绍这里通过一个例子来带大家入坑!先使用create-react-app创建一个react项目。因为要修改webpack,这里我们使用react-app-rewired启动项目。根目录创建一个server目录存放服务端代码,服务端代码我们这里使用koa2。目录结构如下:这里先来看看react ssr是怎么工作的。这个业务流程图比较清晰了,服务端只生成HTML代码,实际上前端会生成一份main.js提供给服务端的HTML使用。这就是react ssr的工作流程。有了这个图会更好的理解,如果这个业务没理解清楚,后面的估计很难理解。react提供的SSR方法有两个renderToString 和 renderToStaticMarkup,区别如下:renderToString 方法渲染的时候带有 data-reactid 属性. 在浏览器访问页面的时候,main.js能识别到HTML的内容,不会执行React.createElement二次创建DOM。renderToStaticMarkup 则没有 data-reactid 属性,页面看上去干净点。在浏览器访问页面的时候,main.js不能识别到HTML内容,会执行main.js里面的React.createElement方法重新创建DOM。实现流程好了,我们都知道原理了,可以开始coding了,目录结构如下:create-react-app 的demo我没动过,直接用这个做案例了,前端项目基本上就没改了,等会儿我们服务器端要使用这个模块。代码如下:import “./App.css”;import React, { Component } from “react”;import logo from “./logo.svg”;class App extends Component { componentDidMount() { console.log(‘哈哈哈~ 服务器渲染成功了!’); } render() { return ( <div className=“App”> <header className=“App-header”> <img src={logo} className=“App-logo” alt=“logo” /> <p> Edit <code>src/App.js</code> and save to reload. </p> <a className=“App-link” href=“https://reactjs.org” target="_blank" rel=“noopener noreferrer” > Learn React </a> </header> </div> ); }}export default App;在项目中新建server目录,用于存放服务端代码。为了简化,我这里只有2个文件,项目中我们用的ES6,所以还要配置下.babelrc.babelrc 配置,因为要使用到ES6{ “presets”: [ “env”, “react” ], “plugins”: [ “transform-decorators-legacy”, “transform-runtime”, “react-hot-loader/babel”, “add-module-exports”, “transform-object-rest-spread”, “transform-class-properties”, [ “import”, { “libraryName”: “antd”, “style”: true } ] ]}index.js 项目入口做一些预处理,使用asset-require-hook过滤掉一些类似 import logo from “./logo.svg”; 这样的资源代码。因为我们服务端只需要纯的HTML代码,不过滤掉会报错。这里的name,我们是去掉了hash值的require(“asset-require-hook”)({ extensions: [“svg”, “css”, “less”, “jpg”, “png”, “gif”], name: ‘/static/media/[name].[ext]’});require(“babel-core/register”)();require(“babel-polyfill”);require("./app");public/index.html html模版代码要做个调整,{{root}} 这个可以是任何可以替换的字符串,等下服务端会替换这段字符串。<!DOCTYPE html><html lang=“en”> <head> <meta charset=“utf-8” /> <link rel=“shortcut icon” href="%PUBLIC_URL%/favicon.ico" /> <meta name=“viewport” content=“width=device-width, initial-scale=1, shrink-to-fit=no”/> <meta name=“theme-color” content="#000000" /> <link rel=“manifest” href="%PUBLIC_URL%/manifest.json" /> <title>React App</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id=“root”>{{root}}</div> </body></html>app.js 服务端渲染的主要代码,加载App.js,使用renderToString 生成html代码,去替换掉 index.html 中的 {{root}} 部分import App from ‘../src/App’;import Koa from ‘koa’;import React from ‘react’;import Router from ‘koa-router’;import fs from ‘fs’;import koaStatic from ‘koa-static’;import path from ‘path’;import { renderToString } from ‘react-dom/server’;// 配置文件const config = { port: 3030};// 实例化 koaconst app = new Koa();// 静态资源app.use( koaStatic(path.join(__dirname, ‘../build’), { maxage: 365 * 24 * 60 * 1000, index: ‘root’ // 这里配置不要写成’index’就可以了,因为在访问localhost:3030时,不能让服务默认去加载index.html文件,这里很容易掉进坑。 }));// 设置路由app.use( new Router() .get(’*’, async (ctx, next) => { ctx.response.type = ‘html’; //指定content type let shtml = ‘’; await new Promise((resolve, reject) => { fs.readFile(path.join(__dirname, ‘../build/index.html’), ‘utfa8’, function(err, data) { if (err) { reject(); return console.log(err); } shtml = data; resolve(); }); }); // 替换掉 {{root}} 为我们生成后的HTML ctx.response.body = shtml.replace(’{{root}}’, renderToString(<App />)); }) .routes());app.listen(config.port, function() { console.log(‘服务器启动,监听 port: ’ + config.port + ’ running~’);});config-overrides.js 因为我们用的是create-react-app,这里使用react-app-rewired去改下webpack的配置。因为执行npm run build的时候会自动给资源加了hash值,而这个hash值,我们在asset-require-hook的时候去掉了hash值,配置里面需要改下,不然会出现图片不显示的问题,这里也是一个坑,要注意下。module.exports = { webpack: function(config, env) { // …add your webpack config // console.log(JSON.stringify(config)); // 去掉hash值,解决asset-require-hook资源问题 config.module.rules.forEach(d => { d.oneOf && d.oneOf.forEach(e => { if (e && e.options && e.options.name) { e.options.name = e.options.name.replace(’[hash:8].’, ‘’); } }); }); return config; }};好了,所有的代码就这些了,是不是很简单了?我们koa2读取的静态资源是 build目录下面的。先执行npm run build打包项目,再执行node ./server 启动服务端项目。看下http://localhost:3030页面的HTML代码检查下:没有{{root}}了,服务器渲染成功!总结相信这篇文章是最简单的react服务器渲染案例了,这里交出github地址,如果学会了,记得给个starhttps://github.com/mtsee/reac… ...

February 27, 2019 · 2 min · jiezi

Koa2开发入门

Koa2入门创建Koa2首先,我们创建一个名为koa2的工程目录,然后使用VS Code打开。然后,我们创建app.js,输入以下代码:// 导入koa,和koa 1.x不同,在koa2中,我们导入的是一个class,因此用大写的Koa表示:const Koa = require(‘koa’);// 创建一个Koa对象表示web app本身:const app = new Koa();// 对于任何请求,app将调用该异步函数处理请求:app.use(async (ctx, next) => { await next(); ctx.response.type = ’text/html’; ctx.response.body = ‘<h1>Hello, koa2!</h1>’;});// 在端口3000监听:app.listen(3000);console.log(‘app started at port 3000…’);对于每一个http请求,koa将调用我们传入的异步函数进行处理。例如:async (ctx, next) => { await next(); // 设置response的Content-Type: ctx.response.type = ’text/html’; // 设置response的内容: ctx.response.body = ‘<h1>Hello, koa2!</h1>’;}其中,参数ctx是由koa传入的封装了request和response的变量,我们可以通过它访问request和response,next是koa传入的将要处理的下一个异步函数。那么,怎么启动koa呢?首先,你需要安装koa,可以直接使用npm进行安装,可以参考Koa官网资料。然后在刚才的koa的项目目录中新建一个package.json,这个文件用于管理koa项目运行需要的依赖包,依赖时注意koa版本号。例如:{ “name”: “hello-koa2”, “version”: “1.0.0”, “description”: “Hello Koa 2 example with async”, “main”: “app.js”, “scripts”: { “start”: “node app.js” }, “keywords”: [ “koa”, “async” ], “author”: “xzh”, “license”: “Apache-2.0”, “repository”: { “type”: “git”, “url”: “https://github.com/michaelliao/learn-javascript.git" }, “dependencies”: { “koa”: “2.7.0” }}其中,dependencies是我们的工程依赖的包以及版本号,需要注意版本号的对应。其他字段均用来描述项目信息,可任意填写。然后,在koa目录下执行npm install安装项目所需依赖包。安装完成后,项目的目录结构如下:hello-koa/|+- .vscode/| || +- launch.json //VSCode 配置文件|+- app.js //使用koa的js|+- package.json //项目配置文件|+- node_modules/ //npm安装的所有依赖包然后,使用npm start启动项目,即可看到效果。当然,还可以直接用命令node app.js在命令行启动程序,该命名最终执行的是package.json文件中的start对应命令:“scripts”: { “start”: “node app.js”}接下来,让我们再仔细看看koa的执行逻辑,核心代码如下:app.use(async (ctx, next) => { await next(); ctx.response.type = ’text/html’; ctx.response.body = ‘<h1>Hello, koa2!</h1>’;});每收到一个http请求,koa就会调用通过app.use()注册的async函数,并传入ctx和next参数。那为什么需要调用await next()呢?原因是koa把很多async函数组成一个处理链,每个async函数都可以做一些自己的事情,然后用await next()来调用下一个async函数,此处我们把每个async函数称为中间件。例如,可以用以下3个middleware组成处理链,依次打印日志,记录处理时间,输出HTML。// 导入koa,和koa 1.x不同,在koa2中,我们导入的是一个class,因此用大写的Koa表示:const Koa = require(‘koa’);// 创建一个Koa对象表示web app本身:const app = new Koa();app.use(async (ctx, next) => { console.log(${ctx.request.method} ${ctx.request.url}); // 打印URL await next(); // 调用下一个middleware});app.use(async (ctx, next) => { const start = new Date().getTime(); // 当前时间 await next(); // 调用下一个middleware const ms = new Date().getTime() - start; // 耗费时间 console.log(Time: ${ms}ms); // 打印耗费时间});app.use(async (ctx, next) => { await next(); ctx.response.type = ’text/html’; ctx.response.body = ‘<h1>Hello, koa2!</h1>’;});// 在端口3000监听:app.listen(3000);console.log(‘app started at port 3000…’);koa-router在上面的例子中,我们处理http请求一律返回相同的HTML,这样显得并不是很友好,正常的情况是,我们应该对不同的URL调用不同的处理函数,这样才能返回不同的结果。为了处理URL跳转的问题,我们需要引入koa-router中间件,让它负责处理URL映射。首先在package.json中添加koa-router依赖:“koa-router”: “7.4.0"然后用npm install安装依赖。接下来,我们修改app.js,使用koa-router来处理URL映射。const Koa = require(‘koa’);// 注意require(‘koa-router’)返回的是函数:const router = require(‘koa-router’)();const app = new Koa();app.use(async (ctx, next) => { console.log(Process ${ctx.request.method} ${ctx.request.url}...); await next();});router.get(’/hello/:name’, async (ctx, next) => { var name = ctx.params.name; ctx.response.body = &lt;h1&gt;Hello, ${name}!&lt;/h1&gt;;});router.get(’/’, async (ctx, next) => { ctx.response.body = ‘<h1>Index</h1>’;});app.use(router.routes());app.listen(3000);console.log(‘app started at port 3000…’);需要说明的是,require(‘koa-router’) 返回的是函数,其作用类似于:const fn_router = require(‘koa-router’);const router = fn_router();然后,我们使用router.get(’/path’, async fn)来注册一个GET请求。可以在请求路径中使用带变量的/hello/:name,变量可以通过ctx.params.name来完成访问。当我们在输入首页:http://localhost:3000/当在浏览器中输入:http://localhost:3000/hello/koapost请求用router.get(’/path’, async fn)处理的是get请求。如果要处理post请求,可以用router.post(’/path’, async fn)。用post请求处理URL时,我们会遇到一个问题:post请求通常会发送一个表单、JSON作为request的body发送,但无论是Node.js提供的原始request对象,还是koa提供的request对象,都不提供解析request的body的功能!此时需要借助koa-bodyparser插件。所以,使用koa-router进行post请求时,需要在package.json中添加koa-bodyparser依赖:“koa-bodyparser”: “4.2.1"现在,我们就可以使用koa-bodyparser进行post请求了,例如:const Koa = require(‘koa’);// 注意require(‘koa-router’)返回的是函数:const router = require(‘koa-router’)();const bodyParser = require(‘koa-bodyparser’);const app = new Koa();app.use(async (ctx, next) => { console.log(Process ${ctx.request.method} ${ctx.request.url}...); await next();});router.get(’/hello/:name’, async (ctx, next) => { var name = ctx.params.name; ctx.response.body = &lt;h1&gt;Hello, ${name}!&lt;/h1&gt;;});router.get(’/’, async (ctx, next) => { ctx.response.body = &lt;h1&gt;Index&lt;/h1&gt; &lt;form action="/signin" method="post"&gt; &lt;p&gt;Name: &lt;input name="name" value="koa"&gt;&lt;/p&gt; &lt;p&gt;Password: &lt;input name="password" type="password"&gt;&lt;/p&gt; &lt;p&gt;&lt;input type="submit" value="Submit"&gt;&lt;/p&gt; &lt;/form&gt;;});//POST请求router.post(’/signin’, async (ctx, next) => { var name = ctx.request.body.name || ‘’, password = ctx.request.body.password || ‘’; console.log(signin with name: ${name}, password: ${password}); if (name === ‘koa’ && password === ‘12345’) { ctx.response.body = &lt;h1&gt;Welcome, ${name}!&lt;/h1&gt;; } else { ctx.response.body = &lt;h1&gt;Login failed!&lt;/h1&gt; &lt;p&gt;&lt;a href="/"&gt;Try again&lt;/a&gt;&lt;/p&gt;; }});router.get(’/’, async (ctx, next) => { ctx.response.body = ‘<h1>Index</h1>’;});app.use(bodyParser());app.use(router.routes());app.listen(3000);console.log(‘app started at port 3000…’);然后,当我们使用npm start启动服务,输入koa和12345时,就能通过测试。优化现在,虽然我们可以根据输入处理不同的URL,但是代码的可阅读和扩展性极差。正确的写法是页面和逻辑分离,于是我们把url-koa复制一份,重命名为url2-koa,并重构项目。重构的项目目录结构如下:url2-koa/|+- .vscode/| || +- launch.json |+- controllers/| || +- login.js //处理login相关URL| || +- users.js //处理用户管理相关URL|+- app.js //使用koa的js|+- package.json |+- node_modules/ //npm安装的所有依赖包我们在controllers目录下添加一个index.js文件,并添加如下内容:var fn_index = async (ctx, next) => { ctx.response.body = &lt;h1&gt;Index&lt;/h1&gt; &lt;form action="/signin" method="post"&gt; &lt;p&gt;Name: &lt;input name="name" value="koa"&gt;&lt;/p&gt; &lt;p&gt;Password: &lt;input name="password" type="password"&gt;&lt;/p&gt; &lt;p&gt;&lt;input type="submit" value="Submit"&gt;&lt;/p&gt; &lt;/form&gt;;};var fn_signin = async (ctx, next) => { var name = ctx.request.body.name || ‘’, password = ctx.request.body.password || ‘’; console.log(signin with name: ${name}, password: ${password}); if (name === ‘koa’ && password === ‘12345’) { ctx.response.body = &lt;h1&gt;Welcome, ${name}!&lt;/h1&gt;; } else { ctx.response.body = &lt;h1&gt;Login failed!&lt;/h1&gt; &lt;p&gt;&lt;a href="/"&gt;Try again&lt;/a&gt;&lt;/p&gt;; }};module.exports = { ‘GET /’: fn_index, ‘POST /signin’: fn_signin};上面示例中,index.js通过module.exports把两个URL处理函数暴露出来。然后,我们修改app.js,让它自动扫描controllers目录,找到所有的js文件并注册每个URL。var files = fs.readdirSync(__dirname + ‘/controllers’);// 过滤出.js文件:var js_files = files.filter((f)=>{ return f.endsWith(’.js’);});// 处理每个js文件:for (var f of js_files) { console.log(process controller: ${f}...); // 导入js文件: let mapping = require(__dirname + ‘/controllers/’ + f); for (var url in mapping) { if (url.startsWith(‘GET ‘)) { // 如果url类似"GET xxx”: var path = url.substring(4); router.get(path, mapping[url]); console.log(register URL mapping: GET ${path}); } else if (url.startsWith(‘POST ‘)) { // 如果url类似"POST xxx”: var path = url.substring(5); router.post(path, mapping[url]); console.log(register URL mapping: POST ${path}); } else { // 无效的URL: console.log(invalid URL: ${url}); } }}如果上面的例子看起来有点费劲,可以对上面的功能进行拆分。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) { 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); }}addControllers(router);为了方便,我们把扫描controllers目录和创建router的代码从app.js中提取出来作为一个中间件,并将它命名为:controller.js。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) { 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’, // 如果不传参数,扫描目录默认为’controllers’ router = require(‘koa-router’)(); addControllers(router, controllers_dir); return router.routes();};然后,我们在app.js文件中可以直接使用controller.js。例如:const Koa = require(‘koa’);const bodyParser = require(‘koa-bodyparser’);const app = new Koa();// 导入controller 中间件const controller = require(’./controller’);app.use(bodyParser());app.use(controller());app.listen(3000);console.log(‘app started at port 3000…’);Koa2跨域同源策略所谓同源策略,即浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源,同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策。最初,它的含义是指,A网页设置的 Cookie,B网页不能打开,除非这两个网页"同源”。。所谓"同源"指的是"三个相同",即协议相同、域名相同和端口相同。例如,有下面一个网址:http://www.netease.com/a.html, 协议是http://,域名是 www.netease.com,端口是80(默认端口可以省略),那么同源情况如下:跨域受浏览器同源策略的影响,不是同源的脚本不能操作其他源下面的对象,想要解决同源策略的就需要进行跨域操作。针对浏览器的Ajax请求跨域的主要有两种解决方案JSONP和CORS。AjaxAjax 是一种用于创建快速动态网页的技术,无需重新加载整个网页的情况下即将实现网页的局部更新。下面通过Ajax进行跨域请求的情景,首先通过koa启了两个本地服务:一个port为3200,一个为3201。app1.jsonst koa = require(‘koa’);const app = new koa();const Router = require(‘koa-router’);const router = new Router();const serve = require(‘koa-static’);const path = require(‘path’);const staticPath = path.resolve(__dirname, ‘static’);// 设置静态服务const staticServe = serve(staticPath, { setHeaders: (res, path, stats) => { if (path.indexOf(‘jpg’) > -1) { res.setHeader(‘Cache-Control’, [‘private’, ‘max-age=60’]); } }});app.use(staticServe);router.get(’/ajax’, async (ctx, next) => { console.log(‘get request’, ctx.request.header.referer); ctx.body = ‘received’;});app.use(router.routes());app.listen(3200);console.log(‘koa server is listening port 3200’);app2.jsconst koa = require(‘koa’);const app = new koa();const Router = require(‘koa-router’);const router = new Router();router.get(’/ajax’, async (ctx, next) => { console.log(‘get request’, ctx.request.header.referer); ctx.body = ‘received’;});app.use(router.routes());app.listen(3200);console.log(‘app2 server is listening port 3200’);由于此示例需要使用koa-static插件,所以启动服务前需要安装koa-static插件。然后新增一个origin.html文件,添加如下代码:<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>cross-origin test</title></head><body style=“width: 600px; margin: 200px auto; text-align: center”> <button onclick=“getAjax()">AJAX</button> <button onclick=“getJsonP()">JSONP</button></body><script type=“text/javascript”> var baseUrl = ‘http://localhost:3201’; function getAjax() { var xhr = new XMLHttpRequest(); xhr.open(‘GET’, baseUrl + ‘/ajax’, true); xhr.onreadystatechange = function() { // readyState == 4说明请求已完成 if (xhr.readyState == 4 && xhr.status == 200 || xhr.status == 304) { // 从服务器获得数据 alert(xhr.responseText); } else { console.log(xhr.status); } }; xhr.send(); }</script></html>当ajax发送跨域请求时,控制台报错:Failed to load http://localhost:3201/ajax: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://localhost:3200’ is therefore not allowed access.虽然控制台有报错,但AJAX请求收到了200,这是因为浏览器的CORS机制,后面会具体解释。JSONP虽然浏览器同源策略限制了XMLHttpRequest请求不同域数据的限制。但是,在页面上引入不同域的js脚本是可以的,而且script元素请求的脚本会被浏览器直接运行。在origin.html的脚本文件中添加如下脚本:function getJsonP() { var script = document.createElement(‘script’); script.src = baseUrl + ‘/jsonp?type=json&callback=onBack’; document.head.appendChild(script);}function onBack(res) { alert(‘JSONP CALLBACK: ‘, JSON.stringify(res)); }当点击JSONP按钮时,getJsonP方法会在当前页面添加一个script,src属性指向跨域的GET请求:http://localhost:3201/jsonp?type=json&callback=onBack, 通过query格式带上请求的参数。callback是关键,用于定义跨域请求回调的函数名称,这个值必须后台和脚本保持一致。然后在app2.js中添加jsonp请求的路由代码:router.get(’/jsonp’, async (ctx, next) => { const req = ctx.request.query; console.log(req); const data = { data: req.type } ctx.body = req.callback + ‘(’+ JSON.stringify(data) +’)’;})app.use(router.routes());然后重新刷新即可看的效果。需要说明的是,jquery、zepto这些js第三方库,其提供的ajax 方法都有对jsonp请求进行封装,如jquery发jsonp的ajax请求如下:function getJsonPByJquery() { $.ajax({ url: baseUrl + ‘/jsonp’, type: ‘get’, dataType: ‘jsonp’, jsonpCallback: “onBack”, data: { type: ‘json’ } }); }CORS跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。实现跨域ajax请求的方式有很多,其中一个是利用CORS,而这个方法关键是在服务器端进行配置。CORS将请求分为简单请求和非简单请求。其中,简单请求就是没有加上额外请求头部的get和post请求,并且如果是post请求,请求格式不能是application/json。而其余的,put、post请求,Content-Type为application/json的请求,以及带有自定义的请求头部的请求,就为非简单请求。非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。首先,在origin.html中添加一个post请求,并添加如下代码:function corsWithJson() { $.ajax({ url: baseUrl + ‘/cors’, type: ‘post’, contentType: ‘application/json’, data: { type: ‘json’, }, success: function(data) { console.log(data); } }) }通过设置Content-Type为appliaction/json使其成为非简单请求,“预检"请求的方法为OPTIONS,服务器判断Origin为跨域,所以返回404。除了Origin字段,“预检"请求的头信息包括两个特殊字段:Access-Control-Request-Method该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。Access-Control-Request-Headers该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,例如示例中的content-type。同时,CORS允许服务端在响应头中添加一些头信息来响应跨域请求。然后在app2.js引入koa2-cors,并添加如下代码:app.use(cors({ origin: function (ctx) { if (ctx.url === ‘/cors’) { return “”; } return ‘http://localhost:3201’; }, exposeHeaders: [‘WWW-Authenticate’, ‘Server-Authorization’], maxAge: 5, credentials: true, allowMethods: [‘GET’, ‘POST’, ‘DELETE’], allowHeaders: [‘Content-Type’, ‘Authorization’, ‘Accept’],}));重启服务后,浏览器重新发送POST请求。可以看到浏览器发送了两次请求。OPTIONS的响应头表示服务端设置了Access-Control-Allow-Origin:,于是发送POST请求,得到服务器返回值。除此之外,在OPTIONS的请求响应报文中,头信息里有一些CORS提供的其他字段:Access-Control-Allow-Credentials: trueAccess-Control-Allow-Headers: Content-Type,Authorization,AcceptAccess-Control-Allow-Methods: GET,POST,DELETEAccess-Control-Max-Age: 5Access-Control-Allow-Methods:该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。Access-Control-Allow-Headers:如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。Access-Control-Allow-Credentials:该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。Access-Control-Max-Age:该字段可选,用来指定本次预检请求的有效期,单位为秒。参考:Koa2框架利用CORS完成跨域ajax请求Koa-基于Node.js的下一代Web开发框架 ...

February 21, 2019 · 6 min · jiezi

利用Node中间层,对接讯飞实现h5页面文章tts(自动朗读)功能

很多时候在看文章的时候都会有自动朗读文章内容的功能,那么这种功能如何在h5上是怎么实现的呢,下面就拿我司一个基本需求作为线索,看是怎么一步一步实现的需求提出经过我司产品经理的想法,做出如下功能1.自动朗读当前h5页面文章竞品——》调研发现,竞品h5是app原生实现,而我司都是h5实现文章阅读,所以开始进行h5的调研对接科大讯飞在线语音合成调研发现科大讯飞的在线语音合成可以基本提供相应功能,决定做一个demo来测试效果1.控制台开通权限2.阅读文档具体代码如下import axios from ‘axios’import * as md5 from ‘./md5’axios.defaults.withCredentials = truelet Appid = ‘xxxxx’let apiKey = ‘xxxxxx’let CurTime = Date.parse(new Date()) / 1000let param = { auf: ‘audio/L16;rate=16000’, aue: ’lame’, voice_name: ‘xiaoyan’, speed: ‘50’, volume: ‘50’, pitch: ‘50’, engine_type: ‘intp65’, text_type: ’text’}let Base64 = { encode: (str) => { return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function toSolidBytes(match, p1) { return String.fromCharCode(‘0x’ + p1); })); }, decode: (str) => { // Going backwards: from bytestream, to percent-encoding, to original string. return decodeURIComponent(atob(str).split(’’).map(function (c) { return ‘%’ + (‘00’ + c.charCodeAt(0).toString(16)).slice(-2); }).join(’’)); }}let xp = Base64.encode(JSON.stringify(param))let CheckSum = md5.hex_md5(apiKey + CurTime + xp)let headers = { ‘X-Appid’: Appid, ‘X-CurTime’: CurTime, ‘X-Param’: xp, ‘X-CheckSum’: CheckSum, ‘Content-Type’: ‘application/x-www-form-urlencoded; charset=utf-8’}export function getAloud (text) { // let data = { // text: encodeURI(text) // } var formdata = new FormData() formdata.append(’text’, text) return axios({ baseURL: window.location.href.includes(‘demo’) ? ‘https://api.xfyun.cn’ : ‘/tts’, method: ‘POST’, url: ‘/v1/service/v1/tts’, headers: { …headers }, data: formdata })}经过测试,是返回二进制文件流了但是前端试了各种办法没有实现流的播放node中间层引入node中间层是考虑到文件可以存储,可以放到cdn上进行缓存,可以减少相似文章的请求科大讯飞接口,可以减少流量的产生,所以决定加入node中间层ps:考拉阅读有node服务器作为一些中间层的处理。主要技术栈是node + koa2 + pm2const md5 = require(’../lib/md5.js’)const fs = require(‘fs’)const path = require(‘path’)const marked = require(‘marked’)const request = require(‘request’)let Appid = ‘’let apiKey = ‘’let CurTimelet param = { auf: ‘audio/L16;rate=16000’, aue: ’lame’, voice_name: ‘x_yiping’, speed: ‘40’, volume: ‘50’, pitch: ‘50’, engine_type: ‘intp65’, text_type: ’text’}var b = new Buffer(JSON.stringify(param));let xp = b.toString(‘base64’)let CheckSumlet headersexports.getAloud = async ctx => { CurTime = Date.parse(new Date()) / 1000 CheckSum = md5.hex_md5(apiKey + CurTime + xp) headers = { ‘X-Appid’: Appid, ‘X-CurTime’: CurTime, ‘X-Param’: xp, ‘X-CheckSum’: CheckSum, ‘Content-Type’: ‘application/x-www-form-urlencoded; charset=utf-8’ } let id = ctx.request.body.id let text = ctx.request.body.text console.log(ctx.query) var postData = { text: text } let r = request({ url: ‘http://api.xfyun.cn/v1/service/v1/tts', // 请求的URL method: ‘POST’, // 请求方法 headers: headers, formData: postData }, function (error, response, body) { // console.log(’error:’, error); // Print the error if one occurred // console.log(‘statusCode:’, response && response.statusCode); // Print the response status code if a response was received // console.log(‘body:’, body); // Print the HTML for the Google homepage. }) await new Promise((resolve, reject) => { let filePath = path.join(__dirname, ‘public/’) + /${id}.mp3 const upStream = fs.createWriteStream(filePath) r.pipe(upStream) upStream.on(‘close’, () => { console.log(‘download finished’); resolve() }); }) .then((res) => { ctx.body = { code: 200, message: ‘语音合成成功’, data: { url: ‘https://fe.koalareading.com/file/' + id + ‘.mp3’ } } })}主要运用request的管道流概念把后台返回的二进制文件导入到流里面,在写入到文件里面最后返回一个url给前端播放使用此致,测试//返回url。相同文章唯一id区分,可以缓存使用https://fe.koalareading.com/file/1112.mp3需求demo完成 ...

January 18, 2019 · 2 min · jiezi

vue + koa2 + webpack4 构建ssr项目

什么是服务器端渲染 (SSR)?为什么使用服务器端渲染 (SSR)?看这 Vue SSR 指南技术栈vue、vue-router、vuexkoa2webpack4axiosbabel、eslintcss、stylus、postcsspm2目录层次webpack4-ssr-config├── client # 项目代码目录│ ├── assets # css、images等静态资源目录│ ├── components # 项目自定义组件目录│ ├── plugins # 第三方插件(只能在客户端运行)目录,比如 编辑器│ ├── store # vuex数据存储目录│ ├── utils # 通用Mixins目录│ ├── views # 业务视图.vue和route路由目录│ ├── app.vue # │ ├── config.js # vue组件、mixins注册,http拦截器配置等等│ ├── entry-client.js # 仅运行于浏览器│ ├── entry-server.js # 仅运行于服务器│ ├── index.js # 通用 entry│ ├── router.js # 路由配置和相关钩子配置│ └── routes.js # 汇聚业务模块所有路由route配置├── config # 配置文件目录│ ├── http # axios封装的http请求│ ├── logger # .vue里this.[log,warn,info,error]和koa2里 logger日志输出│ ├── middle # koa2中间件目录│ │ ├── errorMiddleWare.js # 错误处理中间件│ │ ├── proxyMiddleWare.js # 接口代理中间件│ │ └── staticMiddleWare.js # 静态资源中间件│ ├── eslintrc.conf.js # eslint详细配置│ ├── index.js # server入口│ ├── koa.server.js # koa2服务详细配置│ ├── setup.dev.server.js # koa2开发模式实现hot热更新│ ├── vue.koa.ssr.js # vue ssr的koa2中间件。匹配路由、请求接口生成dom,实现SSR│ ├── webpack.base.config.js # 基本配置 (base config) │ ├── webpack.client.config.js # 客户端配置 (client config)│ └── webpack.server.config.js # 服务器配置 (server config)├── dist # 代码打包目录├── log # pm2日志输出目录├── node_modules # node包├── .babelrc # babel配置├── .eslintrc.js # eslint配置├── .gitignore # git配置├── app.config.js # 端口、代理配置、webpack配置等等├── constants.js # 存放常量├── favicon.ico # ico图标├── index.template.ejs # index模板├── package.json # ├── package-lock.json # ├── pm2.config.js # 项目pm2配置├── pm2.md # pm2的api文档├── postcss.config.js # postcss配置文件└── README.md # 文档源码结构构建使用 webpack 来打包我们的 Vue 应用程序,参考官方分成3个配置,这里使用的webpack4和官方的略有区别。├── webpack.base.config.js # 基本配置 (base config) ├── webpack.client.config.js # 客户端配置 (client config)├── webpack.server.config.js # 服务器配置 (server config)具体webpack配置代码这里省略…对于客户端应用程序和服务器应用程序,我们都要使用 webpack 打包 - 服务器需要「服务器 bundle」然后用于服务器端渲染(SSR),而「客户端 bundle」会发送给浏览器,用于混合静态标记。基本流程如下图:项目代码├── entry-client.js # 仅运行于浏览器├── entry-server.js # 仅运行于服务器├── index.js # 通用 entry├── router.js # 路由配置├── routes.js # 汇聚业务模块所有路由route配置index.jsindex.js 是我们应用程序的「通用 entry」,对外导出一个 createApp 函数。这里使用工厂模式为为每个请求创建一个新的根 Vue 实例,从而避免server端单例模式,如果我们在多个请求之间使用一个共享的实例,很容易导致交叉请求状态污染。entry-client.js:客户端 entry 只需创建应用程序,并且将其挂载到 DOM 中:import Vue from ‘vue’import { createApp } from ‘./index’// 引入http请求import http from ‘./../config/http/http’……const { app, router, store } = createApp()if (window.INITIAL_STATE) { store.replaceState(window.INITIAL_STATE) // 客户端和服务端保持一致 store.state.$http = http}router.onReady(() => { …… Promise.all(asyncDataHooks.map(hook => hook({ store, router, route: to }))) .then(() => { bar.finish() next() }) .catch(next) }) // 挂载 app.$mount(’#app’)})entry-server.js:服务器 entry 使用 default export 导出函数,并在每次渲染中重复调用此函数。此时,除了创建和返回应用程序实例之外,还在此执行服务器端路由匹配和数据预取逻辑。import { createApp } from ‘./index’// 引入http请求import http from ‘./../config/http/http’// 处理ssr期间cookies穿透import { setCookies } from ‘./../config/http/http’// 客户端特定引导逻辑……const { app } = createApp()// 这里假定 App.vue 模板中根元素具有 id="app"app.$mount(’#app’)export default context => { return new Promise((resolve, reject) => { const { app, router, store } = createApp() const { url } = context …… // 设置服务器端 router 的位置,路由配置里如果设置过base,url需要把url.replace(base,’’)掉,不然会404 router.push(url) // 等到 router 将可能的异步组件和钩子函数解析完 router.onReady(() => { …… // SSR期间同步cookies setCookies(context.cookies || {}) // http注入到rootState上,方便store里调用 store.state.$http = http // 使用Promise.all执行匹配到的Component的asyncData方法,即预取数据 Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({ store, router, route: router.currentRoute, }))).then(() => { // 在所有预取钩子(preFetch hook) resolve 后, // 我们的 store 现在已经填充入渲染应用程序所需的状态。 // 当我们将状态附加到上下文, // 并且 template 选项用于 renderer 时, // 状态将自动序列化为 window.__INITIAL_STATE__,并注入 HTML。 context.state = store.state resolve(app) }).catch(reject) }, reject) })}router.js、routes.js、store.jsrouter和store也都是工厂模式,routes是业务模块路由配置的集合。routerimport Vue from ‘vue’import Router from ‘vue-router’import routes from ‘./routes’Vue.use(Router)export function createRouter() { const router = new Router({ mode: ‘history’, fallback: false, // base: ‘/ssr’, routes }) router.beforeEach((to, from, next) => { /todo * 做权限验证的时候,服务端和客户端状态同步的时候会执行一次 * 建议vuex里用一个状态值控制,默认false,同步时直接next,因为服务端已经执行过。 * / next() }) router.afterEach((route) => { /todo/ }) return router}routeimport testRoutes from ‘./views/test/routes’import entry from ‘./app.vue’const home = () => import(’./views/home.vue’)const routes = [ { path: ‘/’, component: home }, { path: ‘/test’, component: entry, children: testRoutes },]export default routesstoreimport Vue from ‘vue’import Vuex from ‘vuex’import test from ‘./modules/test’Vue.use(Vuex)export function createStore() { return new Vuex.Store({ modules: { test } })}Http请求http使用Axios库封装/ * Created by zdliuccit on 2019/1/14. * @file axios封装 * export default http 接口请求 * export addRequestInterceptor 请求前拦截器 * export addResponseInterceptor 请求后拦截器 * export setCookies 同步cookie */import axios from ‘axios’const currentIP = require(‘ip’).address()const appConfig = require(’./../../app.config’)const defaultHeaders = { Accept: ‘application/json, text/plain, /; charset=utf-8’, ‘Content-Type’: ‘application/json; charset=utf-8’, Pragma: ’no-cache’, ‘Cache-Control’: ’no-cache’,}Object.assign(axios.defaults.headers.common, defaultHeaders)if (!process.browser) { axios.defaults.baseURL = http://${currentIP}:${appConfig.appPort}}const methods = [‘get’, ‘post’, ‘put’, ‘delete’, ‘patch’, ‘options’, ‘request’, ‘head’]const http = {}methods.forEach(method => { http[method] = axios[method].bind(axios)})export const addRequestInterceptor = (resolve, reject) => { if (axios.interceptors.request.handlers.length === 0) axios.interceptors.request.use(resolve, reject)}export const addResponseInterceptor = (resolve, reject) => { if (axios.interceptors.response.handlers.length === 0) axios.interceptors.response.use(resolve, reject)}export const setCookies = Cookies => axios.defaults.headers.cookie = Cookiesexport default httpstore中已经注入到rootState,使用如下:loading({ commit, rootState: { $http } }) { return $http.get(‘path’).then(res => { … }) }在config.js中,把http注册到vue的原型链和配置request、response的拦截器import Vue from ‘vue’// 引入http请求插件import http from ‘./../config/http’// 引入log日志插件import { addRequestInterceptor, addResponseInterceptor } from ‘./../config/http/http’import titleMixin from ‘./utils/title’// 引入log日志插件import vueLogger from ‘./../config/logger/vue-logger’// 注册插件Vue.use(http)Vue.use(vueLogger)Vue.mixin(titleMixin)// request前自动添加api配置addRequestInterceptor( (config) => { /统一加/api前缀/ config.url = /api${config.url} return config }, (error) => { return Promise.reject(error) })// http 返回response前处理addResponseInterceptor( (response) => { /*todo 在这里统一前置处理请求响应 / return Promise.resolve(response.data) }, (error) => { / * todo 统一处理500、400等错误状态 * 这里reject下,交给entry-server.js的处理 */ const { response, request } = error return Promise.reject({ code: response.status, data: response.data, method: request.method, path: request.path }) })这样,.vue中间中直接调用this.$http.get()、this.$http.post()…cookies穿透在ssr期间我们需要截取客户端的cookie,保持用户会话唯一性。在entry-server.js中使用setCookies方法,传入的参数是从context上获取。…… // SSR期间同步cookies setCookies(context.cookies || {})……在vue.koa.ssr.js代码中往context注入cookie…… const context = { url: ctx.url, title: ‘Vue Koa2 SSR’, cookies: ctx.request.headers.cookie }……其他title处理参考官方用到全局变量的第三方插件、组件如何处理等等流式渲染预渲染……还有很多优化、深坑,看看官方文档、踩踩就知道了Koa官方使用express框架。express虽然现在也支持async、await,不过独爱koa。koa主文件// 引入相关包和中间件等等const Koa = require(‘koa’)…const appConfig = require(’./../app.config’)const uri = http://${currentIP}:${appConfig.appPort}// koa serverconst app = new Koa()// 定义中间件,const middleWares = [ ……]middleWares.forEach((middleware) => { if (!middleware) { return } app.use(middleware)})// vue ssr处理vueKoaSSR(app, uri)// http代理中间件app.use(proxyMiddleWare())console.log(\n&gt; Starting server... ${uri} \n)// 错误处理app.on(’error’, (err) => { // console.error(‘Server error: \n%s\n%s ‘, err.stack || ‘’)})app.listen(appConfig.appPort)vue.koa.ssr.jsvue koa2 ssr中间件开发模式直接使用setup.dev.server.jswebpack hot热更新生产模块直接读取dist目录的文件路由匹配匹配proxy代理配置,接口请求进入proxyMiddleWare.js接口代理中间件非接口进入render(),返回htmlconst fs = require(‘fs’)const path = require(‘path’)const LRU = require(’lru-cache’)const { createBundleRenderer } = require(‘vue-server-renderer’)const isProd = process.env.NODE_ENV === ‘production’const proxyConfig = require(’./../app.config’).proxyconst setUpDevServer = require(’./setup.dev.server’)module.exports = function (app, uri) { const renderData = (ctx, renderer) => { const context = { url: ctx.url, title: ‘Vue Koa2 SSR’, cookies: ctx.request.headers.cookie } return new Promise((resolve, reject) => { renderer.renderToString(context, (err, html) => { if (err) { return reject(err) } resolve(html) }) }) } function createRenderer(bundle, options) { return createBundleRenderer(bundle, Object.assign(options, { cache: LRU({ max: 1000, maxAge: 1000 * 60 * 15 }), runInNewContext: false })) } function resolve(dir) { return path.resolve(process.cwd(), dir) } let renderer if (isProd) { // prod mode const template = fs.readFileSync(resolve(‘dist/index.html’), ‘utf-8’) const bundle = require(resolve(‘dist/vue-ssr-server-bundle.json’)) const clientManifest = require(resolve(‘dist/vue-ssr-client-manifest.json’)) renderer = createRenderer(bundle, { template, clientManifest }) } else { // dev mode setUpDevServer(app, uri, (bundle, options) => { try { renderer = createRenderer(bundle, options) } catch (e) { console.log(’\nbundle error’, e) } } ) } app.use(async (ctx, next) => { if (!renderer) { ctx.type = ‘html’ return ctx.body = ‘waiting for compilation… refresh in a moment.’; } if (Object.keys(proxyConfig).findIndex(vl => ctx.url.startsWith(vl)) > -1) { return next() } let html, status try { status = 200 html = await renderData(ctx, renderer) } catch (e) { console.log(’\ne’, e) if (e.code === 404) { status = 404 html = ‘404 | Not Found’ } else { status = 500 html = ‘500 | Internal Server Error’ } } ctx.type = ‘html’ ctx.status = status ? status : ctx.status ctx.body = html })}setup.dev.server.jskoa2的webpack热更新配置和相关中间件的代码,这里就不贴出来了,和express略有区别。部署Pm2简介PM2是node进程管理工具,可以利用它来简化很多node应用管理的繁琐任务,如性能监控、自动重启、负载均衡等,而且使用非常简单。pm2.config.js配置如下module.exports = { apps: [{ name: ‘ml-app’, // app名称 script: ‘config/index.js’, // 要运行的脚本的路径。 args: ‘’, // 由传递给脚本的参数组成的字符串或字符串数组。 output: ‘./log/out.log’, error: ‘./log/error.log’, log: ‘./log/combined.outerr.log’, merge_logs: true, // 集群的所有实例的日志文件合并 log_date_format: “DD-MM-YYYY”, instances: 4, // 进程数 1、数字 2、‘max’根据cpu内核数 max_memory_restart: ‘1G’, // 当内存超过1024M时自动重启 watching: true, env_test: { NODE_ENV: ‘production’ }, env_production: { NODE_ENV: ‘production’ } }],}构建生产代码npm run build 构建生产代码pm2启动服务初次启动pm2 start pm2.config.js –env production # production 对应 env_productionorpm2 start ml-apppm2的用法和参数说明可以参考pm2.md,也可参考PM2实用入门指南Nginx在pm2基础上,Nginx配置upstream实现负载均衡在http节点下,加入upstream节点。upstream server_name { server 172.16.119.198:8018 max_fails=2 fail_timeout=30s; server 172.16.119.198:8019 max_fails=2 fail_timeout=30s; server 172.16.119.198:8020 max_fails=2 fail_timeout=30s; …..}将server节点下的location节点中的proxy_pass配置为:http:// + server_name,即“ http://server_name”.location / { proxy_pass http://server_name; proxy_set_header Host localhost; proxy_set_header X-Forwarded-For $remote_addr}详细配置参考文档如果应用服务是域名子路径ssr的话,需要注意如下location除了需要设置匹配/ssr规则之外,还需设置接口、资源的前缀比如(/api,/dist) location ~ /(ssr|api|dist) {…}vue的路由也该设置base:’/ssr’entry-server.js里router.push(url)这里,url应该把/ssr去掉,即router.push(url.replace(’/ssr’,’’’))参考文档vue官方文档koanginxpm2Demo地址 服务器带宽垃圾,将就看看。 git仓库地址还有很多不足,后续慢慢折腾….结束语:生命的价值在于瞎折腾 ...

January 17, 2019 · 6 min · jiezi

Koa 系列 — 如何编写属于自己的 Koa 中间件

Koa 是一个由 Express 原班人马打造的新的 web 框架,Koa 本身并没有捆绑任何中间件,只提供了应用(Application)、上下文(Context)、请求(Request)、响应(Response)四个模块。原本 Express 中的路由(Router)模块已经被移除,改为通过中间件的方式实现。相比较 Express,Koa 能让使用者更大程度上构建个性化的应用。1. 中间件简介Koa 是一个中间件框架,本身没有捆绑任何中间件。本身支持的功能并不多,功能都可以通过中间件拓展实现。通过添加不同的中间件,实现不同的需求,从而构建一个 Koa 应用。Koa 的中间件就是函数,可以是 async 函数,或是普通函数,以下是官网的示例:// async 函数app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(${ctx.method} ${ctx.url} - ${ms}ms);});// 普通函数app.use((ctx, next) => { const start = Date.now(); return next().then(() => { const ms = Date.now() - start; console.log(${ctx.method} ${ctx.url} - ${ms}ms); });});中间件可以通过官方维护的仓库查找获取,也可以根据需求编写属于自己的中间件。2. 中间件原理2.1 示例下面是一个的 Koa 应用,简单演示了中间件的执行顺序:const Koa = require(‘Koa’);const app = new Koa();// 最外层的中间件app.use(async (ctx, next) => { await console.log(第 1 个执行); await next(); await console.log(第 8 个执行);});// 第二层中间件app.use(async (ctx, next) => { await console.log(第 2 个执行); await console.log(第 3 个执行); await next(); await console.log(第 6 个执行); await console.log(第 7 个执行);});// 最里层的中间件app.use(async (ctx, next) => { await console.log(第 4 个执行); ctx.body = “Hello world.”; await console.log(第 5 个执行);});app.listen(3000, () => { console.log(Server port is 3000.);})2.2 原理从上面的示例中可以看出,中间件的执行顺序并不是从头到尾,而是类似于前端的事件流。事件流是先进行事件捕获,到达目标,然后进行事件冒泡。中间件的实现过程也是一样的,先从最外面的中间件开始执行,next() 后进入下一个中间件,一路执行到最里面的中间件,然后再从最里面的中间件开始往外执行。Koa 中间件采用的是洋葱圈模型,每次执行下一个中间件传入两个参数 ctx 和 next,参数 ctx 是由 koa 传入的封装了 request 和 response 的变量,可以通过它访问 request 和 response,next 就是进入下一个要执行的中间件。3. 编写属于自己的中间件3.1 token 验证的 middleware前后端分离开发,我们常采用 JWT 来进行身份验证,其中 token 一般放在 HTTP 请求中的 Header Authorization 字段中,每次请求后端都要进行校验,如 Java 的 Spring 框架可以在过滤器中对 token 进行统一验证,而 Koa 则通过编写中间件来实现 token 验证。// token.js// token 中间件module.exports = (options) => async (ctx, next) { try { // 获取 token const token = ctx.header.authorization if (token) { try { // verify 函数验证 token,并获取用户相关信息 await verify(token) } catch (err) { console.log(err) } } // 进入下一个中间件 await next() } catch (err) { console.log(err) }}// app.js// 引入 token 中间件const Koa = require(‘Koa’);const app = new Koa();const token = require(’./token’)app.use(token())app.listen(3000, () => { console.log(Server port is 3000.);})3.2 log 的 middleware日志模块也是线上不可缺少的一部分,完善的日志系统可以帮助我们迅速地排查出线上的问题。通过 Koa 中间件,我们可以实现属于自己的日志模块// logger.js// logger 中间件const fs = require(‘fs’)module.exports = (options) => async (ctx, next) => { const startTime = Date.now() const requestTime = new Date() await next() const ms = Date.now() - startTime; let logout = ${ctx.request.ip} -- ${requestTime} -- ${ctx.method} -- ${ctx.url} -- ${ms}ms; // 输出日志文件 fs.appendFileSync(’./log.txt’, logout + ‘\n’)}// app.js// 引入 logger 中间件const Koa = require(‘Koa’);const app = new Koa();const logger = require(’./logger’)app.use(logger())app.listen(3000, () => { console.log(Server port is 3000.);})可以结合 log4js 等包来记录更详细的日志4. 总结至此,我们已经了解中间件的原理,以及如何实现一个自己的中间件。中间件的代码通常比较简单,我们可以通过阅读官方维护的仓库中优秀中间件的源码,来加深对中间件的理解和运用。本文首发于公众号,更多内容欢迎关注我的公众号:阿夸漫谈 ...

January 15, 2019 · 2 min · jiezi

一台阿里云主机 + one-sys脚手架 秒搭建自己的系统(博客、管理)

项目地址https://github.com/fanshyiis/…本脚手架主要致力于前端工程师的快速开发、一键部署等快捷开发框架,主要目的是想让前端工程师在一个阿里云服务器上可以快速开发部署自己的项目。本着前端后端融合统一的逻辑进行一些轮子的整合、并加入了自己的一些脚手架工具,第一次做脚手架的开发,如有问题,请在issue上提出,如果有帮助到您的地方,请不吝赐个star技术栈选择前端整合:vue-cli3.0、axios、element等命令行工具整合:commander、chalk、figlet、shelljs等后端整合:node、 koa2、koa-mysql-session、mysql等服务器整合:nginx、pm2、node等基本功能模块实现聚合分离所谓聚合分离,首先是‘聚合’,聚合代码,聚合插件,做到一个项目就可完成前端端代码的编写,打包上线等功能的聚合。其后是‘分离’。前后端分离。虽然代码会在同一个项目工程中但是前后端互不干扰,分别上线,区别于常规的ejs等服务端渲染的模式,做到前端完全分离一键部署基于本地的命令行工具,可以快速打包view端的静态文件并上传到阿里云服务器,也可快速上传server端的文件到服务器文件夹,配合pm2的监控功能进行代码的热更新,无缝更新接口逻辑快速迭代提供基本的使用案例,包括前端的view层的容器案例与组件案例,组件的api设定以及集合了axios的中间件逻辑,方便用户快速搭建自己的项目,代码清晰,易于分析与修改,server端对mysql连接池进行简单的封装,完成连接后及时释放,对table表格与函数进行分层,代码分层为路由层、控制器层、sql操作层基本模块举例1.登录页面登录 -正确反馈 错误反馈 登录成功后session的设定注册 -重名检测 正确反馈 错误反馈主要模块功能模块增删查改基本功能的实现后台koa2服务模块配合koa-mysql-session进行session的设定储存checkLogin中间件的实现cors跨域白名单的设定middlewer 中间件的设定mysql连接池的封装等等。。。服务端nginx 的基本配置与前端端分离的配置pm2 多实例构建配置文件的配置文件 pm2config.json使用流程本地调试安装mysql (过程请百度)// 进入sql命令行$ mysql -u root -p// 创建名为nodesql的数据库$ create database nodesql安装pm2 (过程请百度)拉取项目代码git clone https://github.com/fanshyiis/ONE-syscd ONE-sys// 安装插件cnpm i 或 npm i 或者 yarn add// 安装linksudo npm link// 然后就能使用命令行工具了one start// 或者不愿意使用命令行的同学可以yarn run serve主要代码解析代码逻辑serverbinone -h启动效果启动项目yarn run v1.3.2$ pm2 restart ./server/index.js && vue-cli-service serveUse –update-env to update environment variables[PM2] Applying action restartProcessId on app [./server/index.js](ids: 0,1)[PM2] index ✓[PM2] one-sys ✓┌──────────┬────┬─────────┬─────────┬───────┬────────┬─────────┬────────┬─────┬───────────┬───────────┬──────────┐│ App name │ id │ version │ mode │ pid │ status │ restart │ uptime │ cpu │ mem │ user │ watching │├──────────┼────┼─────────┼─────────┼───────┼────────┼─────────┼────────┼─────┼───────────┼───────────┼──────────┤│ index │ 0 │ 0.1.0 │ fork │ 77439 │ online │ 2640 │ 0s │ 0% │ 15.4 MB │ koala_cpx │ disabled ││ one-sys │ 1 │ 0.1.0 │ cluster │ 77438 │ online │ 15 │ 0s │ 0% │ 20.2 MB │ koala_cpx │ disabled │└──────────┴────┴─────────┴─────────┴───────┴────────┴─────────┴────────┴─────┴───────────┴───────────┴──────────┘ Use pm2 show &lt;id|name&gt; to get more details about an app INFO Starting development server… 98% after emitting CopyPlugin DONE Compiled successfully in 10294ms16:31:55 App running at: - Local: http://localhost:8080/ - Network: http://192.168.7.69:8080/ Note that the development build is not optimized. To create a production build, run yarn build.页面展示线上调试阿里云服务器文件存放目录[root@iZm5e6naugml8q0362d8rfZ ]# cd /home/[root@iZm5e6naugml8q0362d8rfZ home]# lsdist server test[root@iZm5e6naugml8q0362d8rfZ home]#阿里云nginx配置 location ^ /api { proxy_pass http://127.0.0.1:3000; } location ^ /redAlert/ { root /home/dist/; try_files $uri $uri/ /index.html =404; } location ^~ /file/ { alias /home/server/controller/public/; } location / { root /home/dist/; index index.html index.htm; }其他方面如同本地配置有问题可以加群联系最后请star一个吧~~~ ...

January 15, 2019 · 2 min · jiezi

ONE-sys 整合前后端脚手架 koa2 + pm2 + vue-cli3.0 + element

项目地址https://github.com/fanshyiis/…本脚手架主要致力于前端工程师的快速开发、一键部署等快捷开发框架,主要目的是想让前端工程师在一个阿里云服务器上可以快速开发部署自己的项目。本着前端后端融合统一的逻辑进行一些轮子的整合、并加入了自己的一些脚手架工具,第一次做脚手架的开发,如有问题,请在issue上提出,如果有帮助到您的地方,请不吝赐个star技术栈选择前端整合:vue-cli3.0、axios、element等命令行工具整合:commander、chalk、figlet、shelljs等后端整合:node、 koa2、koa-mysql-session、mysql等服务器整合:nginx、pm2、node等基本功能模块实现聚合分离所谓聚合分离,首先是‘聚合’,聚合代码,聚合插件,做到一个项目就可完成前端端代码的编写,打包上线等功能的聚合。其后是‘分离’。前后端分离。虽然代码会在同一个项目工程中但是前后端互不干扰,分别上线,区别于常规的ejs等服务端渲染的模式,做到前端完全分离一键部署基于本地的命令行工具,可以快速打包view端的静态文件并上传到阿里云服务器,也可快速上传server端的文件到服务器文件夹,配合pm2的监控功能进行代码的热更新,无缝更新接口逻辑快速迭代提供基本的使用案例,包括前端的view层的容器案例与组件案例,组件的api设定以及集合了axios的中间件逻辑,方便用户快速搭建自己的项目,代码清晰,易于分析与修改,server端对mysql连接池进行简单的封装,完成连接后及时释放,对table表格与函数进行分层,代码分层为路由层、控制器层、sql操作层基本模块举例1.登录页面登录 -正确反馈 错误反馈 登录成功后session的设定注册 -重名检测 正确反馈 错误反馈主要模块功能模块增删查改基本功能的实现后台koa2服务模块配合koa-mysql-session进行session的设定储存checkLogin中间件的实现cors跨域白名单的设定middlewer 中间件的设定mysql连接池的封装等等。。。服务端nginx 的基本配置与前端端分离的配置pm2 多实例构建配置文件的配置文件 pm2config.json使用流程本地调试安装mysql (过程请百度)// 进入sql命令行$ mysql -u root -p// 创建名为nodesql的数据库$ create database nodesql安装pm2 (过程请百度)拉取项目代码git clone https://github.com/fanshyiis/ONE-syscd ONE-sys// 安装插件cnpm i 或 npm i 或者 yarn add// 安装linksudo npm link// 然后就能使用命令行工具了one start// 或者不愿意使用命令行的同学可以yarn run serve主要代码解析代码逻辑serverbinone -h启动效果启动项目yarn run v1.3.2$ pm2 restart ./server/index.js && vue-cli-service serveUse –update-env to update environment variables[PM2] Applying action restartProcessId on app [./server/index.js](ids: 0,1)[PM2] index ✓[PM2] one-sys ✓┌──────────┬────┬─────────┬─────────┬───────┬────────┬─────────┬────────┬─────┬───────────┬───────────┬──────────┐│ App name │ id │ version │ mode │ pid │ status │ restart │ uptime │ cpu │ mem │ user │ watching │├──────────┼────┼─────────┼─────────┼───────┼────────┼─────────┼────────┼─────┼───────────┼───────────┼──────────┤│ index │ 0 │ 0.1.0 │ fork │ 77439 │ online │ 2640 │ 0s │ 0% │ 15.4 MB │ koala_cpx │ disabled ││ one-sys │ 1 │ 0.1.0 │ cluster │ 77438 │ online │ 15 │ 0s │ 0% │ 20.2 MB │ koala_cpx │ disabled │└──────────┴────┴─────────┴─────────┴───────┴────────┴─────────┴────────┴─────┴───────────┴───────────┴──────────┘ Use pm2 show &lt;id|name&gt; to get more details about an app INFO Starting development server… 98% after emitting CopyPlugin DONE Compiled successfully in 10294ms16:31:55 App running at: - Local: http://localhost:8080/ - Network: http://192.168.7.69:8080/ Note that the development build is not optimized. To create a production build, run yarn build.页面展示线上调试阿里云服务器文件存放目录[root@iZm5e6naugml8q0362d8rfZ ]# cd /home/[root@iZm5e6naugml8q0362d8rfZ home]# lsdist server test[root@iZm5e6naugml8q0362d8rfZ home]#阿里云nginx配置 location ^ /api { proxy_pass http://127.0.0.1:3000; } location ^ /redAlert/ { root /home/dist/; try_files $uri $uri/ /index.html =404; } location ^~ /file/ { alias /home/server/controller/public/; } location / { root /home/dist/; index index.html index.htm; }其他方面如同本地配置有问题可以加群联系最后请star一个吧~~~ ...

January 14, 2019 · 2 min · jiezi

typescript 3.2 新编译选项strictBindCallApply

突发错误我的gels项目(https://github.com/zhoutk/gels),几天没动,突然tsc编译出错,信息如下:src/app.ts:28:38 - error TS2345: Argument of type ‘any[]’ is not assignable to parameter of type ‘[Middleware<ParameterizedContext<any, {}>>]’. Property ‘0’ is missing in type ‘any[]’ but required in type ‘[Middleware<ParameterizedContext<any, {}>>]’.28 m && (app.use.apply(app, [].concat(m)))我的源代码,是动态加载Koa的中间件,app是koa2实例 for (let m of [].concat(middleware)) { m && (app.use.apply(app, [].concat(m))) }问题分析几天前还是正常编译、正常运行的项目,突然出错,应该是环境变了。经查找,发现全局typescript已经升级到了最新版本,3.2.2,而项目中的版本是3.0.3。 将全局版本换回3.0.3,编译通过,问题找到。问题定位上typescrpt的github主页,找发布日志,发现3.2的新功能,第一条就是:TypeScript 3.2 introduces a new –strictBindCallApply compiler option (in the –strict family of options) with which the bind, call, and apply methods on function objects are strongly typed and strictly checked.大概意思是:TypeScript 3.2引入一个新的编译选项 –strictBindCallApply,若使用这个选项,函数对象的bind,call和apply方法是强类型的,并进行严格检测。解决方案因为是动态参数,想了半天我没法明确声明类型。因此,我在tsconfig.json配置文件中,设置"strictBindCallApply": false,将这个编译选项关闭,这样就可以将typescript升级到3.2.2了。哪位朋友若有能打开strictBindCallApply选项,又能通过编译的方案,烦请告知一下,谢谢!我若哪天找到方法,会马上更新本文章。 ...

January 2, 2019 · 1 min · jiezi

iKcamp新书上市《Koa与Node.js开发实战》

内容摘要Node.js 10已经进入LTS时代!其应用场景已经从脚手架、辅助前端开发(如SSR、PWA等)扩展到API中间层、代理层及专业的后端开发。Node.js在企业Web开发领域也日渐成熟,无论是在API中间层,还是在微服务中都得到了非常好的落地。本书将通过Web开发框架Koa2,引领你进入Node.js的主战场!本书系统讲解了在实战项目中使用Koa框架开发Web应用的流程和步骤。第1章介绍Node.js的安装、开发工具及调试。第2章和第3章介绍搭建Koa实战项目的雏形。第4章详细介绍HTTP基础知识及其实战应用。第5章介绍MVC、模板引擎和文件上传等实用功能。第6~8章介绍数据库、单元测试及项目的优化与部署。第9~13章介绍从零开始搭建时下火爆的微信小程序前端及后台管理应用的全部过程,以及最终的服务器部署,包括HTTPS、Nginx。本书示例丰富、侧重实战,以完整的实战项目贯穿全部章节,并提供书中涉及的所有源码及部分章节的配套视频教程,将是前端开发人员立足新领域和后端开发人员了解Node.js并使用Koa2开发Web应用的得力助手。前言Node.js诞生于2009年,到本书出版时已经有近10个年头。它扩充了JavaScript的应用范围,使JavaScript也能像其他语言一样操作各种系统资源,因此,前端工程化开发的大量工具都开始运行在Node.js环境中。由于Node.js采用事件驱动、非阻塞I/O和异步输出来提升性能,因此大量I/O密集型的应用也采用Node.js开发。掌握Node.js开发,既能极大地拓宽前端开发者的技术知识面,也能拓展前端开发者的生存空间,从目前前端开发者越来越多的环境中脱颖而出。由于Node.js仅提供基础的类库,开发者需要自主合理地设计应用架构,并调用大量基础类库来进行开发。为了提升开发效率和降低开发门槛,相关技术社区涌现出不少基于Node.js的Web框架。Express框架在Node.js诞生之初出现,并迅速成为主流的Web应用开发框架。在社区中,大量的第三方开发者开发了丰富的Express插件,极大地降低了基于Node.js的Web应用开发成本,同时也带动了大量的开发者选择使用Express框架开发Web应用。但Express框架采用传统的回调方式处理异步调用,对于经验不足的开发者来说,很容易将代码写成“回调地狱”,使开发的应用难以持续维护。在ECMAScript 6的规范中提出了Generator函数,依据该规范,Express的作者TJ Holowaychukhttps://github.com/tj巧妙地开发了co库https://github.com/tj/co,使开发者能够通过yield关键词,像编写同步代码一样开发异步应用,从而解决了“回调地狱”问题。2014年,他基于co库开发了新一代的Web应用开发框架Koa,用官方语言来描述这个框架就是“next generation web framework for Node.js”。社区开发者为Koa开发了大量的插件,与Express相比,两者的处理机制存在根本上的差异。Express的插件是顺序执行的,而Koa的中间件基于“洋葱模型”,可以在中间件中执行请求处理前和请求处理后的代码。ECMAScript 7提供了Async/Await关键词,从语法层面更好地支持了异步调用。TJ Holowaychuk在Koa的基础上,采用Async/Await取代co库处理异步回调,发布了Koa第2版(简称Koa2)。随着Node 8 LTS(Long Term Support,长期支持)的发布,LTS版本正式支持ECMAScript 7规范,选择使用Koa开发框架开发的Node.js Web应用也越来越多,Koa框架逐步取代了Express框架。尽管目前Koa非常流行,但“纯天然”支持ECMAScript 7语法的Node.js 8在2017年10月才正式发布。目前,市面上介绍Koa的书籍几乎没有,大多介绍的是Express框架,本书可以说是第一本介绍Koa的书籍。本书从Node.js基础、HTTP、Koa框架、数据库、单元测试和运维部署等方面全方位地介绍了应用开发所应具备的知识体系。通过阅读本书,读者可以了解Node.js开发的方方面面,减少实际开发中出现的问题。同时,本书的重点章节也提供了线上代码讲解和视频,读者可以在阅读本书的同时,结合线上代码讲解和视频,更容易地理解本书介绍的知识。特别感谢杜珂珂、哈志辉、姜帅、李波、李益、盛瀚钦、田小虎、徐磊、闫萌、赵晨雪(排名不分先后)对线上培训音视频课程资源的开发和支持。本书特色重点章节附带教学视频。为了便于读者理解本书的内容,一些基础、重点的内容配有视频教程。读者可以访问https://ikcamp.com,结合书中内容观看视频。所有源码托管于GitHub。为了降低读者获取源码的难度,本书的所有源码都托管于GitHub(https://github.com/ ikcamp),读者也可通过GitHub直接和本书作者沟通。一线互联网公司Node.js技术栈实战经验总结。本书补充了前端开发者所不具备的后端开发技能和规范,介绍了如何开发Koa应用,如何通过ORM(Object Relational Mapping,对象关系映射)类库读写数据库,如何通过单元测试来保障代码质量,如何通过PM2、CI等方式启动并部署Node.js应用,以及如何采用日志、监控来保障线上应用的稳定运行等内容。典型项目案例解析,实战性强。本书第3篇通过云相册小程序开发项目介绍了目前流行的小程序技术,包括小程序登录流程、扫码登录、文件上传、相册管理等功能。通过学习本书的相关内容,读者可以独立开发时下流行的小程序和其需要的后端服务。本书知识体系第1篇 基础知识(第1~4章)这部分介绍了开发Koa应用需要具备的预备知识,包括Node.js入门、遇见Koa、路由和HTTP共4个章节。在第1章中,介绍了Node.js的历史和发展过程,以及Node.js基础和环境准备。介绍了NPM(Node Package Manager,Node.js的第三方包管理工具),通过该包管理工具,开发者能够方便地使用大量的第三方软件包。本章还介绍了微软公司推出的免费开发工具:Visual Studio Code编辑器,以及如何使用该编辑器调试Node.js应用。在第2章中介绍了Koa的发展历程和作为Koa核心技术的中间件。在第3章中介绍了路由的概念,以及Koa中最流行的路由中间件koa-router。在第4章中介绍了HTTP的基础知识,以及HTTP的后续协议HTTP/2;介绍了在Node.js中如何获取客户端传递来的数据,如何通过koa-bodyparser中间件获取请求中的body数据等。第2篇 应用实战(第5~8章)这部分介绍了应用开发各个环节的知识,包含构建Koa Web应用、数据库、单元测试、优化与部署共4个章节。在第5章中介绍了MVC架构、模板引擎、静态资源,以及如何输出JSON数据,如何通过koa-multer中间件上传文件等。在第6章中介绍了数据库的概念和以MySQL为代表的关系型数据库,以及如何通过ORM类库操作MySQL数据库;介绍了以MongoDB为代表的非关系型数据库,以及如何在Node.js中操作MongoDB;介绍了以Redis为代表的新型缓存数据库,以及如何在Node.js中利用Redis实现Session持久化。在第7章中介绍了Chai断言库,它用来检测单元测试过程中的结果是否符合预期;介绍了Mocha测试框架,使用该框架可以编写和运行单元测试代码;介绍了使用SuperTest工具测试HTTP服务,以及通过Nock库模拟HTTP服务请求响应;最后,介绍了Nyc工具,用以检查单元测试的覆盖率、提升代码质量。在第8章中介绍了如何记录日志和统一捕获异常,以及如何输出自定义错误页;介绍了如何通过PM2、Docker启动应用,如何通过CI集成发布应用,如何通过Nginx提供HTTPS支持;介绍了如何利用日志等途径监控服务器运行情况,以及如何利用PM2提供的Keymetrics监控云服务器。第3篇 项目实战:从零开始搭建微信小程序后台(第9~13章)这部分通过介绍时下最流行的小程序开发,结合具体的相册小程序来说明如何开发一个完整的小程序,以及如何部署小程序。其中,汇总本书前面章节的知识介绍了小程序的功能模块、接口开发、小程序开发、管理后台开发和服务部署。在第9章中介绍了小程序应具备的产品功能及如何开发小程序门户网站。在第10章中介绍了小程序登录流程,扫码登录的逻辑和实现方式,小程序中用到的接口和后台管理系统需要的接口。具体包括如何通过中间件来鉴权,如何统一控制后台管理系统的权限,如何通过Mongoose来定义数据模型和访问、存储数据,如何使用log4js记录日志。在第11章中介绍了开发微信小程序的流程,以及如何借助微信开发者工具开发小程序。在第12章中介绍了开发后台管理系统的整体架构和设计思路,并提供了一套登录与鉴权的技术方案。在第13章中介绍了小程序相关服务的线上部署过程,包括对数据库、Nginx、HTTPS、和Koa服务的部署,具体包括如何通过Nginx实现把多个域名解析到同一台云服务器上,如何通过PM2管理应用。本书适合读者Web前端开发人员对Node.js应用感兴趣的开发人员Node.js开发的自学者大中专院校相关专业的教师和学生相关培训机构的学员本书由陈达孚、金晶、干珺、张利涛、戴亮、周遥、薛淑英编写。本书涉及的技术知识点较多,作者团队成员虽竭力争取奉献好的作品以使技术得到更好的普及,但难免存在疏漏和不足,读者如有问题或建议,可以直接到iKcamp的GitHub上留言。本书源码也可前往GitHub上获取,地址为https://github.com/ikcamp。本书部分内容配有视频,可前往https://camp.qianduan.group/k…。本书已经在各大电商网站开始上架,感谢对iKcamp的支持!

December 27, 2018 · 1 min · jiezi

从koa-session源码解读session本质

前言Session,又称为“会话控制”,存储特定用户会话所需的属性及配置信息。存于服务器,在整个用户会话中一直存在。然而:session 到底是什么?session 是存在服务器内存里,还是web服务器原生支持?http请求是无状态的,为什么每次服务器能取到你的 session 呢?关闭浏览器会过期吗?本文将从 koa-session(koa官方维护的session中间件) 的源码详细解读 session 的机制原理。希望大家读完后,会对 session 的本质,以及 session 和 cookie 的区别有个更清晰的认识。基础知识相信大家都知道一些关于 cookie 和 session 的概念,最通常的解释是 cookie 存于浏览器,session 存于服务器。cookie 是由浏览器支持,并且http请求会在请求头中携带 cookie 给服务器。也就是说,浏览器每次访问页面,服务器都能获取到这次访问者的 cookie 。但对于 session 存在服务器哪里,以及服务器是通过什么对应到本次访问者的 session ,其实问过一些后端同学,解释得也都比较模糊。因为一般都是服务框架自带就有这功能,都是直接用。背后的原理是什么,并不一定会去关注。如果我们使用过koa框架,就知道koa自身是无法使用 session 的,这就似乎说明了 session 并不是服务器原生支持,必须由 koa-session 中间件去支持实现。那它到底是怎么个实现机制呢,接下来我们就进入源码解读。源码解读koa-session:https://github.com/koajs/session建议感兴趣的同学可以下载代码先看一眼解读过程中贴出的代码,部分有精简koa-session结构来看 koa-session 的目录结构,非常简单;主要逻辑集中在 context.js 。├── index.js // 入口├── lib│ ├── context.js│ ├── session.js│ └── util.js└── package.json先给出一个 koa-session 主要模块的脑图,可以先看个大概:屡一下流程我们从 koa-session 的初始化,来一步步看下它的执行流程:先看下 koa-sessin 的使用方法:const session = require(‘koa-session’);const Koa = require(‘koa’);const app = new Koa();app.keys = [‘some secret hurr’];const CONFIG = { key: ‘koa:sess’, // 默认值,自定义cookie中的key maxAge: 86400000};app.use(session(CONFIG, app)); // 初始化koa-session中间件app.use(ctx => { let n = ctx.session.views || 0; // 每次都可以取到当前用户的session ctx.session.views = ++n; ctx.body = n + ’ views’;});app.listen(3000);初始化初始化 koa-session 时,会要求传入一个app实例。实际上,正是在初始化的时候,往 app.context 上挂载了session对象,并且 session 对象是由 lib/context.js 实例化而来,所以我们使用的 ctx.session 就是 koa-session 自己构造的一个类。我们打开koa-session/index.js:module.exports = function(opts, app) { opts = formatOpts(opts); // 格式化配置项,设置一些默认值 extendContext(app.context, opts); // 划重点,给 app.ctx 定义了 session对象 return async function session(ctx, next) { const sess = ctx[CONTEXT_SESSION]; if (sess.store) await sess.initFromExternal(); await next(); if (opts.autoCommit) { await sess.commit(); } };};通过内部的一次初始化,返回一个koa中间件函数。一步一步的来看,formatOpts 是用来做一些默认参数处理,extendContext 的主要任务是对 ctx 做一个拦截器,如下:function extendContext(context, opts) { Object.defineProperties(context, { [CONTEXT_SESSION]: { get() { if (this[_CONTEXT_SESSION]) return this[_CONTEXT_SESSION]; this[_CONTEXT_SESSION] = new ContextSession(this, opts); return this[_CONTEXT_SESSION]; }, }, session: { get() { return this[CONTEXT_SESSION].get(); }, set(val) { this[CONTEXT_SESSION].set(val); }, configurable: true, } });}走到上面这段代码时,事实上就是给 app.context 下挂载了一个“私有”的 ContextSession 对象 ctx[CONTEXT_SESSION] ,有一些方法用来初始化它(如initFromExternal、initFromCookie)。然后又挂载了一个“公共”的 session 对象。为什么说到“私有”、“公共”呢,这里比较细节。用到了 Symbol 类型,使得外部不可访问到 ctx[CONTEXT_SESSION] 。只通过 ctx.session 对外暴露了 (get/set) 方法。再来看下 index.js 导出的中间件函数return async function session(ctx, next) { const sess = ctx[CONTEXT_SESSION]; if (sess.store) await sess.initFromExternal(); await next(); if (opts.autoCommit) { await sess.commit(); }};这里,将 ctx[CONTEXT_SESSION] 实例赋值给了 sess ,然后根据是否有 opts.store ,调用了 sess.initFromExternal ,字面意思是每次经过中间件,都会去调一个外部的东西来初始化 session ,我们后面会提到。接着看是执行了如下代码,也即执行我们的业务逻辑。await next()然后就是下面这个了,看样子应该是类似保存 session 的操作。sess.commit();经过上面的代码分析,我们看到了 koa-session 中间件的主流程以及保存操作。那么 session 在什么时候被创建呢?回到上面提到的拦截器 extendContext ,它会在接到http请求的时候,从 ContextSession类 实例化出 session 对象。也就是说,session 是中间件自己创建并管理的,并非由web服务器产生。我们接着看核心功能 ContextSession 。ContextSession类先看构造函数:constructor(ctx, opts) { this.ctx = ctx; this.app = ctx.app; this.opts = Object.assign({}, opts); this.store = this.opts.ContextStore ? new this.opts.ContextStore(ctx) : this.opts.store;}居然啥屁事都没干。往下看 get() 方法:get() { const session = this.session; // already retrieved if (session) return session; // unset if (session === false) return null; // cookie session store if (!this.store) this.initFromCookie(); return this.session;}噢,原来是一个单例模式(等到使用时候再生成对象,多次调用会直接使用第一次的对象)。这里有个判断,是否传入了 opts.store 参数,如果没有则是用 initFromCookie() 来生成 session 对象。那如果传了 opts.store 呢,又啥都不干吗,WTF?显然不是,还记得初始化里提到的那句 initFromExternal 函数调用么。if (sess.store) await sess.initFromExternal();所以,这里是根据是否有 opts.store ,来选择两种方式不同的生成 session 方式。问:store是什么呢?答:store可以在initFromExternal中看到,它其实是一个外部存储。问:什么外部存储,存哪里的?答:同学莫急,先往后看。initFromCookieinitFromCookie() { const ctx = this.ctx; const opts = this.opts; const cookie = ctx.cookies.get(opts.key, opts); if (!cookie) { this.create(); return; } let json = opts.decode(cookie); // 打印json的话,会发现居然就是你的session对象! if (!this.valid(json)) { // 判断cookie过期等 this.create(); return; } this.create(json);}在这里,我们发现了一个很重要的信息,session 居然是加密后直接存在 cookie 中的。我们 console.log 一下 json 变量,来验证下:initFromeExternalasync initFromExternal() { const ctx = this.ctx; const opts = this.opts; let externalKey; if (opts.externalKey) { externalKey = opts.externalKey.get(ctx); } else { externalKey = ctx.cookies.get(opts.key, opts); } if (!externalKey) { // create a new externalKey this.create(); return; } const json = await this.store.get(externalKey, opts.maxAge, { rolling: opts.rolling }); if (!this.valid(json, externalKey)) { // create a new externalKey this.create(); return; } // create with original externalKey this.create(json, externalKey);}可以看到 store.get() ,有一串信息是存在 store 中,可以 get 到的。而且也是在不断地要求调用 create() 。createcreate()到底做了什么呢?create(val, externalKey) { if (this.store) this.externalKey = externalKey || this.opts.genid(); this.session = new Session(this, val);}它判断了 store ,如果有 store ,就会设置上 externalKey ,或者生成一个随机id。基本可以看出,是在 sotre 中存储一些信息,并且可以通过 externalKey 去用来获取。由此基本得出推断,session 并不是服务器原生支持,而是由web服务程序自己创建管理。存放在哪里呢?不一定要在服务器,可以像 koa-session 一样骚气地放在 cookie 中!接着看最后一个 Session 类。Session类老规矩,先看构造函数:constructor(sessionContext, obj) { this._sessCtx = sessionContext; this._ctx = sessionContext.ctx; if (!obj) { this.isNew = true; } else { for (const k in obj) { // restore maxAge from store if (k === ‘_maxAge’) this._ctx.sessionOptions.maxAge = obj._maxAge; else if (k === ‘_session’) this._ctx.sessionOptions.maxAge = ‘session’; else this[k] = obj[k]; } }}接收了 ContextSession 实例传来 sessionContext 和 obj ,其他没有做什么。Session 类仅仅是用于存储 session 的值,以及_maxAge,并且提供了toJSON方法用来获取过滤了_maxAge等字段的,session对象的值。session如何持久化保存看完以上代码,我们大致知道了 session 可以从外部或者 cookie 中取值,那它是如何保存的呢,我们回到 koa-session/index.js 中提到的 commit 方法,可以看到:await next();if (opts.autoCommit) { await sess.commit();}思路立马就清晰了,它是在中间件结束 next() 后,进行了一次 commit() 。commit()方法,可以在 lib/context.js 中找到:async commit() { // …省略n个判断,包括是否有变更,是否需要删除session等 await this.save(changed);}再来看save()方法:async save(changed) { const opts = this.opts; const key = opts.key; const externalKey = this.externalKey; let json = this.session.toJSON(); // save to external store if (externalKey) { await this.store.set(externalKey, json, maxAge, { changed, rolling: opts.rolling, }); if (opts.externalKey) { opts.externalKey.set(this.ctx, externalKey); } else { this.ctx.cookies.set(key, externalKey, opts); } return; } json = opts.encode(json); this.ctx.cookies.set(key, json, opts);}豁然开朗了,实际就是默认把数据 json ,塞进了 cookie ,即 cookie 来存储加密后的 session 信息。然后,如果设置了外部 store ,会调用 store.set() 去保存 session 。具体的保存逻辑,保存到哪里,由 store 对象自己决定!小结koa-session 的做法说明了,session 仅仅是一个对象信息,可以存到 cookie ,也可以存到任何地方(如内存,数据库)。存到哪,可以开发者自己决定,只要实现一个 store 对象,提供 set,get 方法即可。延伸扩展通过以上源码分析,我们已经得到了我们文章开头那些疑问的答案。koa-session 中还有哪些值得我们思考呢?插件设计不得不说,store 的插件式设计非常优秀。koa-session 不必关心数据具体是如何存储的,只要插件提供它所需的存取方法。这种插件式架构,反转了模块间的依赖关系,使得 koa-session 非常容易扩展。koa-session对安全的考虑这种默认把用户信息存储在 cookie 中的方式,始终是不安全的。所以,现在我们知道使用的时候要做一些其他措施了。比如实现自己的 store ,把 session 存到 redis 等。这种session的登录方式,和token有什么区别呢这其实要从 token 的使用方式来说了,用途会更灵活,这里就先不多说了。后面会写一下各种登录策略的原理和比较,有兴趣的同学可以关注我一下。总结回顾下文章开头的几个问题,我们已经有了明确的答案。session 是一个概念,是一个数据对象,用来存储访问者的信息。session 的存储方式由开发者自己定义,可存于内存,redis,mysql,甚至是 cookie 中。用户第一次访问的时候,我们就会给用户创建一个他的 session ,并在 cookie 中塞一个他的 “钥匙key” 。所以即使 http请求 是无状态的,但通过 cookie 我们就可以拿到访问者的 “钥匙key” ,便可以从所有访问者的 session 集合中取出对应访问者的 session。关闭浏览器,服务端的 session 是不会马上过期的。session 中间件自己实现了一套管理方式,当访问间隔超过 maxAge 的时候,session 便会失效。那么除了 koa-session 这种方式来实现用户登录,还有其他方法吗?其实还有很多,可以存储 cookie 实现,也可以用 token 方式。另外关于登录还有单点登录,第三方登录等。如果大家有兴趣,可以在后面的文章继续给大家剖析。 ...

December 17, 2018 · 4 min · jiezi

koa2 一网打尽(基本使用,洋葱圈,中间件机制和模拟,源码分析(工程,核心模块,特殊处理),核心点,生态)

本文 github 地址: https://github.com/HCThink/h-blog/blob/master/source/koa2/readme.mdgithub 首页(star+watch,一手动态直达): https://github.com/HCThink/h-blog掘金 link , 掘金 专栏segmentfault 主页原创禁止私自转载koa2koa homepage优秀的下一代 web 开发框架。Koa 应用程序不是 HTTP 服务器的1对1展现。 可以将一个或多个 Koa 应用程序安装在一起以形成具有单个HTTP服务器的更大应用程序。基础使用快速搭建简易 koa server 服务koa 搭建一个服务还是很简单的, 主要代码如下, 完整代码如下. 切到主目录下,安装依赖: yarn执行入口: yarn startkoa demo 目录koa demo 主文件import Koa from ‘koa’;import https from ‘https’;import open from ‘open’;const Log = console.log;const App = new Koa();App.use(async (ctx, next) => { ctx.body = ‘Hello World’; Log(‘mid1 start…’); await next(); Log(‘mid1 end…’);});App.use(async (ctx, next) => { debugger; Log(‘mid2 start…’); await next(); Log(‘mid2 end…’);});App.use((ctx, next) => { Log(‘mid3…’);});// 服务监听: 两种方式。App.listen(3000); // 语法糖// http.createServer(app.callback()).listen(3000);https.createServer(App.callback()).listen(3001);open(‘http://localhost:3000’);// 如下为执行顺序, 实际上 http 会握手,所以输出多次// 如下执行特征也就是洋葱圈, 实际上熟悉 async、await 则不会比较意外。// mid1 start…// mid2 start…// mid3…// mid2 end…// mid1 end…koa2特性封装并增强 node http server[request, response],简单易容。洋葱圈处理模型。基于 async/await 的灵活强大的中间件机制。通过委托使得 api 在使用上更加便捷易用。api参考官网提供的基本 api ,不在赘述: https://koa.bootcss.com/部分 api 实现,参考: 源码分析常用 apiapp.listen: 服务端口监听app.callback: 返回适用于 http.createServer() 方法的回调函数来处理请求。你也可以使用此回调函数将 koa 应用程序挂载到 Connect/Express 应用程序中。app.use(function): 挂载中间件的主要方法。核心对象contextKoa Context 将 node 的 request 和 response 对象封装到单个对象中,为编写 Web 应用程序和 API 提供了许多有用的方法。 这些操作在 HTTP 服务器开发中频繁使用,它们被添加到此级别而不是更高级别的框架,这将强制中间件重新实现此通用功能。每个 请求都将创建一个 Context,并在中间件中作为接收器引用,或者 ctx 标识符。ctx.res requestctx.req: responsectx.request: koa request toolctx.response: koa response toolctx.cookiesctx.request.accepts(types): type 值可能是一个或多个 mime 类型的字符串,如 application/json,扩展名称如 json,或数组 [“json”, “html”, “text/plain”]。request.acceptsCharsets(charsets)…更多参考洋葱圈使用层面koa 洋葱圈执行机制图解koa 洋葱圈koa demo, koa demo 源码洋葱圈简易实现版执行方式: tsc onionRings.ts –lib ’es2015’ –sourceMap && node onionRings.js洋葱圈简易实现版 main, 洋葱圈简易实现版 源码简易实现 外部中间件参考: koa-bodyparsermain codepublic use(middleware: Function) { this.middList.push(middleware);}// 执行器private async deal(i: number = 0) { debugger; if (i >= this.middList.length) { return false; } await this.middList[i](this, this.deal.bind(this, i + 1));}实现思路use 方法注册 middleware。deal 模拟一个执行器: 大致思路就是将下一个 middleware 作为上一个 middleware 的 next 去 await,用以保证正确的执行顺序和中断。问题如果习惯了回调的思路, 你会不会有这种疑惑: 洋葱圈机制于在 一个中间件中调用另一个中间件,被调中间件执行成功,回到当前中间件继续往后执行,这样不断调用,中间件很多的话, 会不会形成一个很深的函数调用栈? 从而影响性能, 同时形成「xx 地狱」? – ps(此问题源于分享时原同事 小龙 的提问。)实际上这是个很好的问题,对函数执行机制比较了解才会产生的疑问。排除异步代码处理,我们很容易用同步方式模拟出这种调用层级。参考: 同步方式。 这种模式存在明显的调用栈问题。我可以负责任的回答: 不会的,下一个问题。 ???? ????不会的原因在 generator 中详细介绍,一两句说不清楚。实际上我认为这里是有语法门槛的。在 generator 之前,用任何方式处理这个问题,都显得怪异,而且难以解调用决层级带来的性能, 调试等带来问题。详细说明参考: generator 真.协程源码KOA 源码特别精简, 不像 Express 封装的功能那么多, git 源码: 【https://github.com/koajs/koa】工程koa2 的源码工程结构非常简洁,一目了然, 没有花里胡哨的东西。主文件├── History.md├── ….├── Readme.md├── benchmarks├── docs // doc│ ├── api ……├── lib // 源码│ ├── application.js // 入口文件,封装了context,request,response,核心的中间件处理流程。│ ├── context.js // context.js 处理应用上下文,里面直接封装部分request.js和response.js的方法│ ├── request.js // request.js 处理http请求│ └── response.js // response.js 处理http响应├── package.json└── test // 测试模块 ├── application ….package.jsonjest 做测试node 版本{ “engines”: { “node”: “^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4” }}主入口: “main”: “lib/application.js"koa 核心模块封装的 http server(node)核心对象 context, request、response中间件机制和剥洋葱模型的实现错误捕获和错误处理源码application.jsapplication.js 是 koa 的入口,继承了events , 所以框架有事件监听和事件触发的能力。application 还暴露了一些常用的api,比如toJSON、listen、use等等。context.jsrequest.jsresponse.js特殊处理委托摘自 context.js:context.jsconst proto = module.exports = { // …};delegate(proto, ‘response’) .method(‘attachment’) .method(‘redirect’) .method(‘remove’) .method(‘vary’) .method(‘set’) .method(‘append’) .method(‘flushHeaders’) .access(‘status’) .access(‘message’) .access(‘body’) .access(’length’) .access(’type’) .access(’lastModified’) .access(’etag’) .getter(‘headerSent’) .getter(‘writable’);delegate(proto, ‘request’) .method(‘acceptsLanguages’) .method(‘acceptsEncodings’) .method(‘acceptsCharsets’) .method(‘accepts’) .method(‘get’) .method(‘is’) .access(‘querystring’) .access(‘idempotent’) .access(‘socket’) .access(‘search’) .access(‘method’) .access(‘query’) .access(‘path’) .access(‘url’) .access(‘accept’) .getter(‘origin’) .getter(‘href’) .getter(‘subdomains’) .getter(‘protocol’) .getter(‘host’) .getter(‘hostname’) .getter(‘URL’) .getter(‘header’) .getter(‘headers’) .getter(‘secure’) .getter(‘stale’) .getter(‘fresh’) .getter(‘ips’) .getter(‘ip’);koa 为了方便串联中间件,提供了一个 context 对象,并且把核心的 response, request 对象挂载在上面, 但是这样往往就造成使用上写法冗余, eg: ctx.response.body, 而且某些对象还是经常使用的,这很不方便,所以产生了 delegates 库,用于委托操作, 委托之后,就可以在 ctx 上直接使用部分委托属性: ctx.body。源码分析如下delegates 源码解析delegates 库源码文件middleware 机制koa-composekoa 中 use 用来注册中间件,实际上是将多个中间件放入一个缓存队列中 this.middleware.push(fn);,然后通过koa-compose这个插件进行递归组合。因此严格来讲 middleware 的执行结构的组织并不在 koa 源码中完成,而是在依赖库 koa-compose 中。 koa 中使用: const fn = compose(this.middleware); 完成中间件的组合。koa-compose 核心逻辑如下, 主要思路大致是: 通过包装 middleware List 返回一个 组装好的执行器。组装思路是:将下一个 middleware 进行包装【执行器 + promise 化】作为上一个 middleware 的 next【dispatch.bind(null, i + 1)】。同时给中间件提供 context 对象。return function (context, next) { // last called middleware # let index = -1 return dispatch(0) function dispatch (i) { if (i <= index) return Promise.reject(new Error(’next() called multiple times’)) index = i let fn = middleware[i] if (i === middleware.length) fn = next // 函数洋葱的最后补上一个Promise.resolve(); if (!fn) return Promise.resolve() try { // middleware 是 async 函数, 返回 promise 。Promise.resolve 确保中间件执行完成 // 提供 ctx, next fn: dispatch.bind(null, i + 1) return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } }}koa-composekoa-compose 是一个非常精简的库,不做单独分析了, 他提供了一种主调型的递归: fn(context, dispatch.bind(null, i + 1)) , 这种方式可以认为是’懒递归’, 将递归的执行交给主调者控制,这样能够在更合适的时机执行后续处理, 但如果某个中间件不调用 next,那么其后的中间件就不被执行了。这和 js 协程【generator】有机制上的类似,都是使用者来控制 next 的执行时机, 可类比学习。generator易用性处理koa 非常易用, 原因是 koa 在源码层面做了大量的 委托 和针对复杂对象的封装,如 request, response 的 get/set. 用以提高工具的可用度,易用度。实际上我认为这一点是现代框架非常重要的东西,脱离用户的库都不是好库。koa 是好库。delegates 上面说过, 参考: delegates。get/setrequest, response 两个文件千行代码, 80% 左右的都是 get、set,参考:request.jsresponse.js另一方面,表现在 application.js 的 createContext 方法中,通过挂载引用和委托配合 get set 的实践配合提升易用度,单独不太好讲,分析注释在源码中。异常捕获中间件异常捕获, koa1 中间件基于 generator + co, koa2 中间件基于 async/await, async 函数返回 promise, 所以只要在组合中间件后 catch 即可捕获中间件异常fnMiddleware(ctx).then(handleResponse).catch(onerror);框架层发生错误的捕获机制, 这个通过继承 event 模块很容易实现监听。this.on(’error’, this.onerror);注册的 error 事件, 在 context.onerror 中被 emit this.app.emit(’error’, err, this);http 异常处理 : Execute a callback when a HTTP request closes, finishes, or errors.onFinished(res, onerror); // application.handleRequest中间件交互初用中间件可能会有一个疑问: 中间件如何通信?事实上这是个设计取舍逻辑, 中间件之间的数据交互并不是麻烦事, 特别是在 ECMAScript 推出 async await 之后,但问题是这样做的意义不大,原因是所有的中间件是可任意插拔组合的,这种不确定性,导致了中间件之间的数据交互就变得不稳定,最起码的数据格式就没办法固定,就更别谈处理了。灵活的插件机制导致中间件之间的交互难有统一层面的实现。另一方面从中间件的定位来看,其之间也没必要交互,中间件不能脱离 http 的请求响应而独立存在,他是服务于整个过程的,也因此所有的中间件第一个参数就是 ctx, 这个对象挂载了 request 和 response, 以及 koa 提供的封装和工具操作。核心点中断这是洋葱圈非常核心的支撑点, 我们稍微留意就能发现 koa 中间件执行机制于普通 js 的执行顺序很不一致, 我们看如下代码:app.use(async (cxt, next) => { Log(1); await next(); Log(2);});app.use(async (cxt, next) => { Log(3); await next(); Log(4);});上述代码执行顺序也就是洋葱圈: Log(1) -> await next (Log(3)) -> await next -> Log(4) -> Log(2).为了保证代码按照洋葱模型的执行顺序执行,程序需要在调用 next 的时候让代码等待,我称之为中断。实际上以前想要实现这种执行顺序,只能依赖 cb, promise.then 来模拟,而且即便实现了,在写法上也显得臃肿和别扭,要么是写出很胖的函数,要么是写出很长的函数。而且没法处理调用栈的问题。async/await 可以比较优雅的实现这种具有同步执行特征的前端代码来处理异步,代码执行到 await 这里,等待 await 表达式的执行,执行完成之后,接着往后执行。实际上这很类似于 generator 的 yield,特性。async 也就是 generator + 执行器的一个语法糖, 参考:async / awaitcogeneratorasync ? no , it’s generatorkoa.use 得确直接使用 async 函数处理中间件及其中可能存在的异步, 而 async/await 实现上是基于 generator 。async 在使用上可讲的点通常在他的 task 放在哪,以及执行时机 和 timeout ,promise 的执行顺序等。真正的中断特性得益于 generator。一位不愿透漏姓名的同事问了我一个问题,怎么证明 async 是 generator + 执行器 的语法糖?这是不得不讨论一个问题。相关的讨论参考: Async / Await > #generator 部分探讨生态koa 中间件并没有一个统一的 market 之类的地方,说实话找起来不是那么方便。如果你想找中间件的话,可以在 npm 上用 koa- 做关键字检索: https://www.npmjs.com/search?…官方 middleware源码使用的中间件koa-compose上面已有分析koa-is-jsonfunction isJSON(body) { if (!body) return false; if (‘string’ == typeof body) return false; if (‘function’ == typeof body.pipe) return false; if (Buffer.isBuffer(body)) return false; return true;}koa-convert用于兼容处理 generator 中间件,基本可以认为是 co + generator 中间件【也依赖 koa-compose 进行组织】other koa社区常用中间件合集: some middleware参考 & 鸣谢https://koa.bootcss.com/http://nodejs.cn/api/fs.html#…https://juejin.im/post/5ba786… ...

December 13, 2018 · 4 min · jiezi

NodeJS踩坑实录

nodejs的常用apiurl 主要是配置一系列和路径相关的信息url.parse(urlString[, parseQueryString[, slashesDenoteHost]]) 将一个URL字符串解析为URL对象urlString: 解析的路径字符串parseQueryString: 返回是布尔类型,主要用来解析query的slashesDenoteHost: 返回是布尔类型,当你不确定你的请求协议时,辅助帮助你进行解析url.format(urlObj,parseObj,slashesObj) 将url对象转换为字符串与parse参数相反url.resolve(from, to) 将基础路径和后缀路径转换成目标路径from 解析时相对的基本URLto 要解析的超链接 URL值得注意的是基本路径要在路径最后添加’/’,否则合并会找到你最近的’/‘并替换const url = require(‘url’);url.resolve(’/one/two/three’, ‘four’); // ‘/one/two/four’url.resolve(‘http://example.com/', ‘/one’); // ‘http://example.com/one'url.resolve('http://example.com/one', ‘/two’); // ‘http://example.com/two'queryString 为查询字符串提供扩展querystring 模块提供了一些实用函数,用于解析与格式化 URL 查询字符串querystring.parse(str,con,seq)str 要解析的 URL 查询字符串con用于界定查询字符串中的键值对的子字符串。默认为 ‘&‘seq 用于界定查询字符串中的键与值的子字符串。默认为 ‘=‘querystring.stringify(obj,con,seq)obj 要序列化成 URL 查询字符串的对象con 用于界定查询字符串中的键值对的子字符串。默认为 ‘&‘seq 用于界定查询字符串中的键与值的子字符串。默认为 ‘=‘querystring.escape(str) 相当于encodeURI 将Asc编码转换成utf-8对给定的str进行 URL编码该方法是提供给 querystring.stringify()使用的,通常不直接使用querystring.unescape(str) 相当于decodeURI 将utf-8转换成ASc对给定的str进行解码该方法是提供给 querystring.parse()使用的,通常不直接使用events - 事件触发器大多数 Node.js 核心 API 构建于惯用的异步事件驱动架构,其中某些类型的对象(又称触发器,Emitter)会触发命名事件来调用函数(又称监听器,Listener)当 EventEmitter 对象触发一个事件时,所有绑定在该事件上的函数都会被同步地调用例子,一个简单的 EventEmitter 实例,绑定了一个监听器。 eventEmitter.on() 方法用于注册监听器,eventEmitter.emit() 方法用于触发事件。const Eventemitter = require(“events”)class Player extends Eventemitter {}const player = new Player()//使用 eventEmitter.on() 注册监听器时,监听器会在每次触发命名事件时被调用player.on(“change”,(track) => { console.log(node事件机制,${track})})//使用 eventEmitter.once() 可以注册最多可调用一次的监听器。 当事件被触发时,监听器会被注销,然后再调用//player.once(“change”,(track) => {// console.log(node事件机制,${track})//})player.emit(“change”,“react”)player.emit(“change”,“vue”)fs - 文件系统fs 模块提供了一些接口用于以一种类似标准 POSIX 函数的方式与文件系统进行交互所有的文件系统操作都有同步和异步两种形式异步形式的最后一个参数都是完成时的回调函数。 传给回调函数的参数取决于具体方法,但回调函数的第一个参数都会保留给异常。 如果操作成功完成,则第一个参数会是 null 或 undefinedfs.Stats 类fs.Stats 对象提供了一个文件的信息stats.isDirectory() 如果 fs.Stats 对象表示一个文件系统目录,则返回 truestats.isFile() 如果 fs.Stats 对象表示一个普通文件,则返回 truefs.mkdir(path[, options], callback)异步地创建目录。 完成回调只有一个可能的异常参数// 创建 /temp/a/apple 目录,不管 /temp 和 /temp/a 目录是否存在。fs.mkdir(’/temp/a/apple’, (err) => { if (err) throw err;});fs.writeFile(file, data[, options], callback)异步地写入数据到文件,如果文件已经存在,则覆盖文件。 data 可以是字符串或 bufferfs.writeFile(’temp.js’, ‘keep study’, (err) => { if (err) throw err; console.log(‘文件已保存!’);});fs.appendFile(path, data[, options], callback)异步地追加数据到文件,如果文件不存在则创建文件。 data 可以是字符串或 Bufferfs.appendFile(’temp.js’, ‘追加的数据’, (err) => { if (err) throw err; console.log(‘数据已追加到文件’);});fs.readFile(path[, options], callback)异步地读取一个文件的全部内容fs.readFile(’/etc/passwd’, (err, data) => { if (err) throw err; console.log(data);});回调有两个参数 (err, data),其中 data 是文件的内容。如果未指定字符编码,则返回原始的 buffer。如果 options 是一个字符串,则它指定了字符编码。例子:fs.readFile(’/etc/passwd’, ‘utf8’, callback);fs.readdir(path[, options], callback)读取目录的内容。 回调有两个参数 (err, files),其中 files 是目录中文件名的数组,不包含 ‘.’ 和 ‘..’。options 参数用于传入回调的文件名。 它可以是一个字符串,指定字符编码。 也可以是一个对象,其中 encoding 属性指定字符编码。 如果 encoding 设为 ‘buffer’,则返回的文件名会是 Buffer 对象。fs.rmdir(path, callback)删除目录fs.readFileSync(path[, options])同步读取文件fs.readdirSync(path[, options])同步读取目录fs.unlink(path, callback)解除关系(也即删除文件)readFileSync和unlink结合实现删除一个目录及其目录下的文件的例子:const fs = require(‘fs’);fs.readdirSync(“logs”).map((file) => { fs.unlink(logs/${file},() => { console.log(“删除成功”) })})fs.rmdir(“logs”, (err)=> { console.log(“确定要删除吗?”)})node框架之expressnode框架之koa2文档持续更新中~~~ ...

November 13, 2018 · 1 min · jiezi

用typescript开发koa2的二三事

前言最近在写一个博客的项目,前端用的 vue+typescript+element-ui,后台则选择了 koa2+typescript+mongoDB的组合。写这篇博客的目的也是在写后台的过程遇到一些问题,查了很多资料才解决。于是权当总结,亦是记录,可以给别人做一个完整的参考。基本信息这里列出来的是会用到的一些配置信息,毕竟一直都在更新,可能这里说的以后某个版本就不支持了。“nodemon” : “^1.18.3”,“ts-node” : “^7.0.1”,“typescript” : “^3.1.1"“node” : “9.0.0"问题描述这次遇到的问题其实都和typescript有关。koa2已经出来很久了,开发基本成熟,但是这次找资料的时候鲜有发现使用typescript开发的,即便有,也都很简单,而且没法解决我的问题。那言归正传,使用ts开发koa,因为不涉及webpack打包编译,所以就会遇到几个问题:编译实时刷新,重启服务器debugger这些确实是初期很困扰我的地方,使用node开发,最简单的无非是 node xxx.js,进一步也就是热更新。但引入ts后就需要考虑编译和实时刷新的问题。毕竟不像每改一点代码,就手动重启服务器,手动编译。解决方案以下是我的解决方案,后面我会说一下为什么这样写,如果来不及看或者只想要答案的话复制就行。“watch” : “ts-node ./app/index.ts”,“start” : “nodemon –watch app/index.js”,“build” : “tsc”,“debugger” : “nodemon –watch ./app -e ts,tsx –exec node –inspect -r ts-node/register ./app/index.ts”,“watch-serve”: “nodemon –watch ‘./app/**/’ -e ts,tsx –exec ts-node ./app/index.ts"那我们一个一个来说。npm run watch这个命令就是在本地使用ts-node启动一个服务器。来看一下对ts-node的描述。TypeScript execution and REPL for node.js, with source map support. Works with typescript@>=2.0.这是一个在node.js的执行和交互的typescript环境,简而言之就是为了ts而生的!!那这条命令就是根据当前的入口运行程序,唯一的一个问题是,不支持热更新。所以pass。npm run build && npm run start这俩放一起说是因为相关性比较高。可以说是相互依赖的关系吧。先说第一条命令,很简单,就是编译当前的ts项目文件,输出目录需要在tsconfig.json中配置。我给大家看下我的运行结果。app是我的项目文件,运行命令后,会在根目录下创建dist文件夹存放我编译好的js文件,打开就是这样。现在再说第二条命令,就是根据编译好的文件入口启动服务器。并且支持热更新,但是,注意这里有个但是,它只支持编译过后的文件的热更新,其实就是用js开发koa的启动命令,那这时候在源文件中的任何修改都不会有作用,所以pass。npm run watch-serve 重点来了,这才是解决问题的关键!!!这里完美的解决了代码的热更新,实时编译,服务器重启等问题。很好的提升了开发体验。这个解决方案有一些中文博客提到,但是当初用的时候不知道为啥这样用,导致后期犯了一些现在看来很低级的错误,这个就不提了。不过确实没人说明这段命令的意思,直到昨天碰到一个问题,我才好好正视这个恶魔。nodemon和ts-node前文都介绍过了,我在这里只会针对具体的配置解释一下。原本我的理解是这里用逗号分隔了两个不同的命令,但是我太天真了。来看一下文档的介绍。By default, nodemon looks for files with the .js, .mjs, .coffee, .litcoffee, and .json extensions. If you use the –exec option and monitor app.py nodemon will monitor files with the extension of .py. However, you can specify your own list with the -e (or –ext) switch like so:nodemon -e js,jadeNow nodemon will restart on any changes to files in the directory (or subdirectories) with the extensions .js, .jade.nodemon有默认吃的几种文件类型,分别是 .js, .mjs, .coffee, .litcoffee, and .json,而我这里用的 .ts,并不在默认支持文件里,因此这里使用 -e来指定我需要扩展的文件类型,这里的逗号也不过是用来分隔不同类型用的。那这里提到了 –exec这个配置。原文里说如果用nodemon启动app.py这个文件,那么将默认支持.py这种扩展类型。另外文档里还写了别的。nodemon can also be used to execute and monitor other programs. nodemon will read the file extension of the script being run and monitor that extension instead of .js if there’s no nodemon.json:nodemon –exec “python -v” ./app.pyNow nodemon will run app.py with python in verbose mode (note that if you’re not passing args to the exec program, you don’t need the quotes), and look for new or modified files with the .py extension.这里说明,除了默认支持的扩展,通过这个配置,可以支持和正在运行的脚本一样的扩展。并且,如果扩展程序不需要传参数的话,可以不写单引号。综上所述,一个命令用于增加支持的文件类型,一个配置用来执行和监视其他类型的程序。至于—watch这个参数。By default nodemon monitors the current working directory. If you want to take control of that option, use the –watch option to add specific paths:nodemon –watch app –watch libs app/server.jsNow nodemon will only restart if there are changes in the ./app or ./libs directory. By default nodemon will traverse sub-directories, so there’s no need in explicitly including sub-directories.Don’t use unix globbing to pass multiple directories, e.g –watch ./lib/, it won’t work. You need a –watch flag per directory watched.这里面需要注意的有两点,一是nodemon会默认监视当前脚本文件执行的文件夹,另一个就是如果要指定具体的文件夹时,需要些详细的路径,比如绝对路径或者相对路径,绝对不要使用通配符。因此我命令行中的使用是无效且违反规则的,然而非要这样写也不影响运行。原本到这也就结束了,然而昨天用了一个npm包,我想看看怎么运行的,于是遇到了debugger的问题,这也是迫使我去认真弄懂这段命令的原因。npm run debugger基本的调试方式网上到处都有,我就不说了,问题还是导入typescript之后,让一切都混乱起来。我最开始尝试了以下几种命令:’nodemon –inspect –watch ./app -e ts,tsx –exec ts-node ./app/index.ts’’nodemon –watch –inspect ./app -e ts,tsx –exec ts-node ./app/index.ts’’nodemon –watch ./app -e ts,tsx –exec ts-node –inspect ./app/index.ts’这些都可以自己试着运行一下,反正也没啥用。然后就是今天一直想着这件事,换了几个关键字google,找到这两个地方。https://stackoverflow.com/que…https://github.com/TypeStrong…感谢stackoverflow和github,相互印证着看好像就明白是怎么回事了。这里说下-r这个参数:这里用于预加载一个模块,并且可以多次使用这个参数,那说回我写的命令里,ts-node/register 就是一个模块,或者不严谨的说,register是ts-node下的一个方法。这里就是使用node预加载ts-node的register模块用来运行ts程序,并且开启debugger模式。后语至此为止,在编译,热更新,debugger方面的坑应该是踩完了,希望后面的人看了我写的文章能少走些弯路吧。如果有些的不对的地方可以留言指正。所有的源码我应该暂时不会放出,至少等我写完吧。就酱紫, ...

November 12, 2018 · 2 min · jiezi

Node.js+koa2

const Koa = require(‘koa’)const app = new Koa()const bodyParser = require(‘koa-bodyparser’)app.use(bodyParser())app.use(async (ctx) => { if (ctx.url === ‘/’ && ctx.method === ‘GET’) { let html = &lt;h2&gt;This is demo&lt;/h2&gt; &lt;form method="POST" action="/"&gt; &lt;p&gt;username:&lt;/p&gt; &lt;input name="username"&gt; &lt;p&gt;age:&lt;/p&gt; &lt;input name="age"&gt; &lt;p&gt;website&lt;/p&gt; &lt;input name="website"&gt; &lt;button type="submit"&gt;submit&lt;/button&gt; &lt;/form&gt; ctx.body = html } else if (ctx.url === ‘/’ && ctx.method === ‘POST’) { let postData = ctx.request.body; ctx.body = postData } else { ctx.body = ‘<h2>Not find</h2>’ }})app.listen(3000, () => { console.log(‘demo is run’)})github地址:https://github.com/Rossy11/node ...

October 30, 2018 · 1 min · jiezi

傻瓜式解读koa中间件处理模块koa-compose

最近需要单独使用到koa-compose这个模块,虽然使用koa的时候大致知道中间件的执行流程,但是没仔细研究过源码用起来还是不放心(主要是这个模块代码少,多的话也没兴趣去研究了)。koa-compose看起来代码少,但是确实绕。闭包,递归,Promise。。。看了一遍脑子里绕不清楚。看了网上几篇解读文章,都是针对单行代码做解释,还是绕不清楚。最后只好采取一种傻瓜的方式:koa-compose去掉一些注释,类型校验后,源码如下:function compose (middleware) { return function (context, next) { // last called middleware # let index = -1 return dispatch(0) function dispatch (i) { if (i <= index) return Promise.reject(new Error(’next() called multiple times’)) index = i let fn = middleware[i] if (i === middleware.length) fn = next if (!fn) return Promise.resolve() try { return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } }}写出如下代码:var index = -1;function compose() { return dispatch(0)}function dispatch (i) { if (i <= index) return Promise.reject(new Error(’next() called multiple times’)) index = i var fn = middleware[i] if (i === middleware.length) fn = next if (!fn) return Promise.resolve(‘fn is undefined’) try { return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } function f1(context,next){ console.log(‘middleware 1’); next().then(data=>console.log(data)); console.log(‘middleware 1’); return ‘middleware 1 return’; } function f2(context,next){ console.log(‘middleware 2’); next().then(data=>console.log(data)); console.log(‘middleware 2’); return ‘middleware 2 return’; } function f3(context,next){ console.log(‘middleware 3’); next().then(data=>console.log(data)); console.log(‘middleware 3’); return ‘middleware 3 return’; }var middleware=[ f1,f2,f3]var context={};var next=function(context,next){ console.log(‘middleware 4’); next().then(data=>console.log(data)); console.log(‘middleware 4’); return ‘middleware 4 return’;};compose().then(data=>console.log(data));直接运行结果如下:“middleware 1"“middleware 2"“middleware 3"“middleware 4"“middleware 4"“middleware 3"“middleware 2"“middleware 1"“fn is undefined"“middleware 4 return"“middleware 3 return"“middleware 2 return"“middleware 1 return"按着代码运行流程一步步分析:dispatch(0)i==0,index==-1 i>index 往下index=0fn=f1Promise.resolve(f1(context, dispatch.bind(null, 0 + 1))) 这就会执行f1(context, dispatch.bind(null, 0 + 1))进入到f1执行上下文console.log(‘middleware 1’);输出middleware 1next() 其实就是调用dispatch(1) bind的功劳递归开始dispatch(1)i==1,index==0 i>index 往下index=1fn=f2Promise.resolve(f2(context, dispatch.bind(null, 1 + 1))) 这就会执行f2(context, dispatch.bind(null, 1 + 1))进入到f2执行上下文console.log(‘middleware 2’);输出middleware 2next() 其实就是调用dispatch(2)接着递归dispatch(2)i==2,index==1 i>index 往下index=2fn=f3Promise.resolve(f3(context, dispatch.bind(null, 2 + 1))) 这就会执行f3(context, dispatch.bind(null, 2 + 1))进入到f3执行上下文console.log(‘middleware 3’);输出middleware 3next() 其实就是调用dispatch(3)接着递归dispatch(3)i==3,index==2 i>index 往下index=3i === middleware.lengthfn=nextPromise.resolve(next(context, dispatch.bind(null, 3 + 1))) 这就会执行next(context, dispatch.bind(null, 3 + 1))进入到next执行上下文console.log(‘middleware 4’);输出middleware 4next() 其实就是调用dispatch(4)接着递归dispatch(4)i==4,index==3 i>index 往下index=4fn=middleware[4]fn=undefinedreuturn Promise.resolve(‘fn is undefined’) 回到next执行上下文console.log(‘middleware 4’);输出middleware 4return ‘middleware 4 return’Promise.resolve(‘middleware 4 return’)回到f3执行上下文console.log(‘middleware 3’);输出middleware 3return ‘middleware 3 return’Promise.resolve(‘middleware 3 return’)回到f2执行上下文console.log(‘middleware 2’);输出middleware 2return ‘middleware 2 return’Promise.resolve(‘middleware 2 return’)回到f1执行上下文console.log(‘middleware 1’);输出middleware 1return ‘middleware 1 return’Promise.resolve(‘middleware 1 return’)回到全局上下文至此已经输出"middleware 1"“middleware 2"“middleware 3"“middleware 4"“middleware 4"“middleware 3"“middleware 2"“middleware 1"那么"fn is undefined"“middleware 4 return"“middleware 3 return"“middleware 2 return"“middleware 1 return"怎么来的呢回头看一下,每个中间件里都有next().then(data=>console.log(data));按照之前的分析,then里最先拿到结果的应该是next中间件的,而且结果就是Promise.resolve(‘fn is undefined’)的结果,然后分别是f4,f3,f2,f1。那么为什么都是最后才输出呢?Promise.resolve(‘fn is undefined’).then(data=>console.log(data));console.log(‘middleware 4’);运行一下就清楚了或者setTimeout(()=>console.log(‘fn is undefined’),0);console.log(‘middleware 4’);整个调用过程还可以看成是这样的:function composeDetail(){ return Promise.resolve( f1(context,function(){ return Promise.resolve( f2(context,function(){ return Promise.resolve( f3(context,function(){ return Promise.resolve( next(context,function(){ return Promise.resolve(‘fn is undefined’) }) ) }) ) }) ) }) )}composeDetail().then(data=>console.log(data));方法虽蠢,但是compose的作用不言而喻了最后,if (i <= index) return Promise.reject(new Error(’next() called multiple times’))这句代码何时回其作用呢?一个中间件里调用两次next(),按照上面的套路走,相信很快就明白了。 ...

October 29, 2018 · 2 min · jiezi