Fork me on GitHub
余鸢

微信公众号开发(七):利用装饰器增强路由

获取家族详细数据

先获取到wikiId,再根据wikiId获取到详细数据,然后将详细数据通过整合后拿到最后的数据就是家族数据了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const HOUSES = [
{
name: 'House Stark of Winterfell',
cname: '史塔克家族',
words: 'Winter is Coming'
},
{
name: 'House Targaryen',
cname: '坦格利安家族',
words: 'Fire and Blood'
}
...
]
export const getHouses = async () => {
let data = R.map(getWikiId, HOUSES)
data = await Promise.all(data)
data = R.map(getWikiDetail, data)
data = await Promise.all(data)
fs.writeFileSync('./wikiHouses.json', JSON.stringify(data, null, 2), 'utf8')
}

最后将数据写入wikiHouses.json文件中。

关联家族数据与主要人物数据

将家族数据和人物数据进行组合,让这两个数据产生关联。引入家族数据houses和人物数据characters,将家族数据中的成员数据提取出来,将成员数据做匹配操作,筛选后的数据家族数据和人物数据中都包含,最后把这些数据列表挂载到houses上

1
2
let houses = require(resolve(__dirname, '../../wikiHouses.json'))
let characters = require(resolve(__dirname, '../../completeCharacters.json'))

分析数据:

查找title的值包含有伊耿历三世纪末的字样的数据,和人物的json数据做匹配,匹配到的数据就是人物关联的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const findSwornMembers = R.map(
R.compose(
i => _.reduce(i, (acc, item) => {
acc = acc.concat(item)
return acc
}, []),
R.map(i => {
let item = R.find(R.propEq('cname', i[0]))(characters)
return {
character: item.nmId,
text: i[1]
}
}),
R.filter(item => R.find(R.propEq('cname', item[0]))(characters)),
R.map(i => {
let item = i.split(',')
let name = item.shift()
return [name.replace(/(【|】|爵士|一世女王|三世国王|公爵|国王|王后|夫人|公主|王子)/g, ''), item.join(',')]
}),
R.nth(1),
R.splitAt(1),
R.prop('content'),
R.nth(0),
R.filter(i => R.test(/伊耿历三世纪末的/, i.title)),
R.prop('sections')
)
)

R.map:检索所有的数据

R.compose:将分析流程组织起来

R.prop('sections'):获取每条数据的sections

R.filter(i => R.test(/伊耿历三世纪末的/, i.title)):过滤title为伊耿历三世纪末成员的数据

过滤后的数据是个数组,通过R.nth(0)获取第一条数据,通过R.prop('content')获取匹配数据中的content,将第一条数据截断成两个数组R.splitAt(1),取出第一条数据R.nth(1)

通过逗号作为分割符号去掉后面的内容,去掉前面匹配到的字符串符号等等,从而拿到真正的人物名称,通过逗号连接起来

1
2
3
4
5
R.map(i => {
let item = i.split(',')
let name = item.shift()
return [name.replace(/(【|】|爵士|一世女王|三世国王|公爵|国王|王后|夫人|公主|王子)/g, ''), item.join(',')]
}),

对人物名称后进行对比

1
R.filter(item => R.find(R.propEq('cname', item[0]))(characters)),

将过滤后的数据组合起来,把cname传给characters,characters查找是否包含这个cname

1
2
3
4
5
6
7
R.map(i => {
let item = R.find(R.propEq('cname', i[0]))(characters)
return {
character: item.nmId,
text: i[1]
}
}),

最后将所有的数据串起来拼接到一个数组里,初始化是一个数组[]

1
2
3
4
i => _.reduce(i, (acc, item) => {
acc = acc.concat(item)
return acc
}, []),

lodash.reduce的解释

最后将数据写入本地completeHouses.json文件中

1
fs.writeFileSync('./completeHouses.json', JSON.stringify(houses, null, 2), 'utf8')

将数据写入数据库

建立家族和人物的数据模型

server/database/wikiCharacter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const Mixed = Schema.Types.Mixed
const WikiCharacterSchema = new mongoose.Schema({
_id: String,
wikiId: Number,
nmId: String,
chId: String,
name: String,
cname: String,
playedBy: String,
profile: String,
images: [String],
sections: Mixed,
intro: [String],
meta: {
createAt: {
type: Date,
default: Date.now()
},
updateAt: {
type: Date,
default: Date.now()
}
}
})
WikiCharacterSchema.pre('save', function (next) {
if (this.isNew) {
this.meta.createAt = this.meta.updateAt = Date.now()
} else {
this.meta.updateAt = Date.now()
}
next()
})
const WikiCharacter = mongoose.model('WikiCharacter', WikiCharacterSchema)

server/database/wikiHouse.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const Mixed = Schema.Types.Mixed
const WikiHouseSchema = new mongoose.Schema({
name: String,
cname: String,
words: String,
cover: String,
sections: Mixed,
intro: String,
wikiId: Number,
swornMembers: [
{
character: {
type: String,
ref: 'WikiCharacter'
},
text: String
}
],
meta: {
createAt: {
type: Date,
default: Date.now()
},
updateAt: {
type: Date,
default: Date.now()
}
}
})
WikiHouseSchema.pre('save', function (next) {
if (this.isNew) {
this.meta.createAt = this.meta.updateAt = Date.now()
} else {
this.meta.updateAt = Date.now()
}
next()
})
const WikiHouse = mongoose.model('WikiHouse', WikiHouseSchema)

数据写入数据库

将数据导入本地数据库,先引入json文件获取wikiHouses和wikiCharacters数据

1
2
let wikiHouses = require(resolve(__dirname, '../../completeHouses.json'))
let wikiCharacters = require(resolve(__dirname, '../../completeCharacters.json'))

连接上数据库时拿到数据模型,判断是否存在existWikiHouses和existWikiCharacters,如果没有,插入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
mongoose.connection.on('open', async => {
console.log('数据库连接成功 ', config.db)
const wikiHouse = mongoose.model('WikiHouse')
const wikiCharacter = mongoose.model('WikiCharacter')
const existWikiHouses = await wikiHouse.find({}).exec
const existWikiCharacters = await wikiCharacter.find({}).exec
if (!existWikiHouses.length) wikiHouse.insertMany(wikiHouses)
if (!existWikiCharacters.length) wikiCharacter.insertMany(wikiCharacters)
})

修改start.js

1
require('./server')

数据写入数据库成功,如图:

wx45

利用Decorator重构Koa路由

不管是页面还是API接口都可以通过路由中间把请求匹配到后端的某一个控制器里,首先想到的是路由,有哪些路由,这些路由下又有些什么子路由。之前我们已经写了一个路由文件,但里面放的是测试微信相关的请求和返回数据单元,如果后面再增加小程序或者网站后端等等其他的,包括现在已有前端的接口,都放在一个文件里的话,那么此时文件看起来就显得臃肿,业务也只能通过代码分段的形式来区分,是不利于后期维护。我们设计一个主路由的控制器来修饰这个类,子路由设计相应的控制器来修饰里面的方法,这样层次结构就很鲜明了。也就是通过路由拆分层把不同类型的业务逻辑放到单独的路由文件里去,比如微信的只有跟微信相关的路由。

改造router

获取到所有的router文件,引入Route暴露一个构造函数来创建Route实例,调用init()方法初始化路由

server/middlewares/router.js

1
2
3
4
5
6
7
8
9
10
import Route from '../decorator/router'
import { resolve } from 'path
const r = path => resolve(__dirname, path)
export const router = app => {
const apiPath = r('../routes')
const router = new Route(app, apiPath)
router.init()
}

主路由

server/decorator/router.js,这个文件用来实现不同业务路由的分拆,作为主路由。主路由的控制器主要作用就是取得主路由,并且把它放到原型上作为一个静态的属性供由类里面的进行调用。

初始化路由

新建一个路由类,里面建一个初始化路由的方法。初始化时遍历所有的路由文件,得到请求路径和方法,路径和controller装饰器的参数拼接,通过koa-router实例调用请求方法(请求路径, 对应的路由中间件) ,再通过koa实例载入router中间件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
export let routersMap = new Map()
export const symbolPrefix = Symbol('prefix')
export const isArray = v => _.isArray(v) ? v : [v]
export const normalizePath = path => path.startsWith('/') ? path : `${path}`
export default class Route {
constructor (app, apiPath) {
this.app = app
this.router = new Router()
this.apiPath = apiPath
}
init () {
glob.sync(resolve(this.apiPath, './*.js')).forEach(require)
for (let [conf, controller] of routersMap) {
const controllers = isArray(controller)
let prefixPath = conf.target[symbolPrefix]
if (prefixPath) prefixPath = normalizePath(prefixPath)
const routerPath = prefixPath + conf.path
this.router[conf.method](routerPath, ...controllers)
}
this.app.use(this.router.routes())
this.app.use(this.router.allowedMethods())
}
}

这里说个小知识:ES6引入了一种新的原始数据类型Symbol,表示独一无二的值,而且创建之后不能修改。

注意:Symbol函数前不能使用new命令,否则会报错。这是因为生成的Symbol是一个原始类型的值,不是对象。Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。

定义请求方法

我们把路由配置项都存起来,里面用请求方式和子路由内容为键,类里面的方法提取出来为值。

这里的target 可看做是两层意思,第一层可以理解为命名空间,也就是 controller = path => target 这一层,我们看作是命名空间,比如 ‘/wx’就代表是命名空间 ;第二层是具体到某个路由路径的 Map 映射,也就是 get = path => router({}) 进而进入到 routersMap.set({ target: target}) 这一层,先执行第一层,后执行第二层,最终实现子路由,比如 /wx下面可以继续增加a b来对外暴露/wx/a /wx/b这样的路由。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export const router = conf => (target, key, desc) => {
conf.path = normalizePath(conf.path)
routersMap.set({
target: target,
...conf
}, target[key])
}
export const controller = path => target => target.prototype[symbolPrefix] = path
export const get = path => router({
method: 'get',
path: path
})
export const post = path => router({
method: 'post',
path: path
})
export const put = path => router({
method: 'put',
path: path
})
export const del = path => router({
method: 'del',
path: path
})

微信路由

新建server/routes文件夹用来存放各个不同类型的业务的中间件。

server/routes/wechat.js,用来放与微信相关的路由。

@controller('')括号里的路径相当于是个命名空间,请求地址匹配到这个路径的都应该控制在这个路径中,和上面代码export const controller = path => target => target.prototype[symbolPrefix] = path相呼应。 @get修饰,则说明以get方式获得, @post则以post方式提交。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { controller, get, post, required } from '../decorator/router'
import config from '../config'
import reply from '../wechat/reply'
import wechatMiddle from '../wechat-lib/middleware'
import { resolve } from 'path'
import { signature, redirect, oauth } from '../controllers/wechat'
@controller('')
export class WechatController {
@get('/wx')
async wx (ctx, next) {
const middle = wechatMiddle(config.wechat, reply)
const body = await middle(ctx, next)
ctx.body = body
}
@post('/wx')
async wxPost (ctx, next) {
const middle = wechatMiddle(config.wechat, reply)
const body = await middle(ctx, next)
ctx.body = body
}
@get('/wx-signature')
async wxSignature (ctx, next) {
await signature(ctx, next)
}
@get('/wx-redirect')
async wxRedirect (ctx, next) {
await redirect(ctx, next)
}
@get('/wx-oauth')
async wxOauth (ctx, next) {
await oauth(ctx, next)
}
}

这样写法的好处:可以将这些路由拆开,不同的业务对应不同的文件,整个路由的层次更加清晰明了,只需要看@controller的命名空间就知道它对应的是哪些业务对应的路由。还有get、post请求可以做更加清晰的控制。

编译

由于es2017的装饰器就目前而言,浏览器中和node暂未实现,需要使用Babel进行编译。在start.js文件中引入插件transform-decorators-legacy和module-alias,对修饰器的语法编译。

1
2
3
4
5
6
7
8
9
10
11
'plugins': [
'transform-decorators-legacy',
[
'module-alias', [
{
src: r('./server'), 'expose': '~',
src: r('./server/database'), 'expose': 'database'
}
]
]
]

安装插件

1
2
3
yarn add babel-plugin-module-alias -D
yarn add babel-plugin-transform-decorators-legacy -D

运行测试:

wx46