项目用的 React 框架。公司有三类人掌握着 URL 生杀大权,产品总监,产品经理,还有 SEO ???特别是产品总监还兼职首席拼写官,导致 URL 一周一个样。即使上线了也是如此,告诉他们用户已经收藏了这个链接,不要随意更改,然而根本劝不动。
初始配置
<Route path='/' component={App}>
<Route path='host' component={HostView}>
<Route path='meetings' component={MeetingsView}>
<Route path='previous/:index' component={PreviousMeetingView}></Route>
<Route path='upcoming/:index' component={UpcomingMeetingView}></Route>
<Route path='details/:id/:start/:end/:index' component={MeetingDetailsView}></Route>
<Route path='schedule(/:id)' component={ScheduleMeetingView}></Route>
</Route>
</Route>
<Route path='login' component={LoginView}></Route>
<Route path='*' component={FourOFour}></Route>
</Route>
像这样写死的配置显然是不行的,因为代码中到处充斥着 this.props.router.push(‘host/meetings/previous’ + 1) 这样的调用。上线头一周产品总监就干掉了 host ,变成 ‘meetings/previous’, 这样一来代码中处处要改动。而且调用 push 的时候,要来回确定路径有没有敲错,路径中的动态参数有没有缺少。
我需要一个灵活一点的,能够给他们随意折腾的配置,而且要能有友好的提示。那么上 Typescript 吧。
灵活一点
现在我需要一个对象,对象有一个方法能够生成 Route 中 path 的值,然后还有一个方法能生成跳转操作的路径。比如 UpcomingMeetingView 这个对应的路由,path 的值是 ‘upcoming/:index’ 而实际跳转时候的路径是类似这样 ‘/host/upcomging/1’ 的字符串。
比较一下两个值,可以发现,跳转时候的路径是和父路径相关的,也就是这个对象要保存父对象引用。
然后要怎么做友好的提示呢?对于路径中的参数我们要知道参数名和参数值类型,这就要用到泛型了。
上代码:
class PathItem<P> {
/** 自身的路径字符串 */
self: string
/** 父路径引用 */
parent: PathItem<any>
/** 可选的参数,string 表示参数名,boolean 表示是否必选 */
params: [string, boolean][]
constructor (self: string, parent?: PathItem<any>, params?: [string, boolean][]) {
this.self = self
this.parent = parent
this.params = params || []
}
/**
* 返回跳转路径字符串
* @param params {P} 路径中的动态配置参数
*/
pushPath (params?: P) {
if (this.parent && Object.keys(this.parent.params).some(v => params[v] === undefined)) {
console.error(`${this.parent.self} need a params, when called ${this.self}'s pushPath.`)
}
let path = this.self === '/' ? '/' : this.self + '/'
for (let i = 0, len = this.params.length; i < len; i++) {
let param = this.params[i]
if (!param) {
break
}
if (params) {
if (param[1]) {
if (params[param[0]] === undefined) {
console.error(`miss a required params in path ${this.self}, [${param[0]}], /n/n the params is ${JSON.stringify(params)}`)
}
}
let p = params[param[0]]
if (p) {
path += params[param[0]] + '/'
}
}
}
if (/^\//.test(this.self)) {
return path
} else {
return (this.parent ? this.parent.pushPath(params) : '') + path
}
}
/**
* 返回 react-route 中要配置的 path 值
*/
routePath () {
let path = this.self
for (let i = 0, len = this.params.length; i < len; i++) {
let param = this.params[i]
if (!param) {
break
}
if (param[1] === true) {
if (param[2] === true) {
path += `/${param[0]}`
} else {
path += `/:${param[0]}`
}
} else if (param[1] === false) {
if (param[2] === true) {
path += `(/${param[0]})`
} else {
path += `(/:${param[0]})`
}
}
}
return path
}
}
// 测试一下
const app = new PathItem('app')
console.log(app.routePath()) // => 'app'
console.log(app.pushPath()) // => 'app/'
// 然后 app 下面有一个子组件 host
const host = new PathItem('host', app)
// host 下面有一个子组件 upcoming, 这里提供了泛型参数的类型为一个对象,该对象包只含一个 meeting_id 属性,属性值必须是 number 类型
const upcoming = new PathItem<{
meeting_id: number,
meeting_desc?: string // 可选的参数
}>('upcomings', _host, [
['meeting_id', true],
['meeting_desc', false]
])
console.log(upcoming.routePath()) // => 'upcomings/:meeting_id(/:meeting_desc)'
// 这里如果传入的参数类型与 new upcoming 对象时指定的泛型参数不一致,会报错
console.log(upcoming.pushPath({
meeting_id: 1
})) // => 'host/upcomings/1/'
// 传入可选参数
console.log(upcoming.pushPath({
meeting_id: 1,
meeting_desc: 'foo'
})) // => host/upcomings/1/foo/
再灵活一点
上面的代码基本满足需求了,但是 pushPath 返回的值后面带了个 ‘/’, 这个骚后再说。
某天产品说 ‘host/upcomings/1/foo’ 这样的路径根本不知道 1 和 foo 代表的是什么意思,要改成 ‘host/upcomings/meeting_id/1/meeting_desc/foo’。
分析一下,原本我们路径中的固定部分都是 PathItem 构造函数中的 self 参数,动态部分都在 params 参数。对于这个需求,可以 new 一个下面这种 PathItem 对象
const upcoming_1 = new PathItem<{
meeting_id: string,
meeting_id_value: number,
meeting_desc?: string,
meeting_desc_value?: string
}>('upcomings', _host, [
['meeting_id', true],
['meeting_id_value', true],
['meeting_desc', false],
['meeting_desc_value', false]
])
console.log(upcoming_1.routePath()) // => 'upcomings/:meeting_id/:meeting_id_value(/:meeting_desc)(/:meeting_desc_value)'
看上面的输出,这样就能匹配 ‘host/upcomings/meeting_id/1/meeting_desc/foo’ 这种路径了。但是问题也来了, ‘host/xxxx/meeting_id/1/xxxxxxx/foo’ 这样的路径也能匹配。于是在构造函数的 params 参数的每一项中我们还要用一个标识来标记这个 params.meeting_id 的这个属性值是固定的还是不固定的,如果是固定的输出 /meeting_id 否则输出 /:meeting_id。
于是改动 routePath 方法为:
/**
* 返回 react-route 中要配置的 path 值
*/
routePath () {
let path = this.self
for (let i = 0, len = this.params.length; i < len; i++) {
let param = this.params[i]
if (!param) {
break
}
if (param[1] === true) { // 参数必选
if (param[2] === true) { // 固定值
path += `/${param[0]}`
} else {
path += `/:${param[0]}`
}
} else if (param[1] === false) { // 参数可选
if (param[2] === true) { // 固定值
path += `(/${param[0]})`
} else {
path += `(/:${param[0]})`
}
}
}
return path
}
// 测试一下
const upcoming_2 = new PathItem<{
meeting_id: string,
meeting_id_value: number,
meeting_desc?: string,
meeting_desc_value?: string
}>('upcomings', _host, [
['meeting_id', true, true], // 元组中的第三个参数为 true,表示这个 meeting_id 是路径中的固定值
['meeting_id_value', true, false], // 元组中的第三个参数为 false,表示这个 meeting_id_value 是路径中的动态参数
['meeting_desc', false, true],
['meeting_desc_value', false, false]
])
console.log(upcoming_2.routePath()) // => 'upcomings/meeting_id/:meeting_id_value(/meeting_desc)(/:meeting_desc_value)'
再完善一下
上面的 pushPath 方法返回的字符串末尾还有 ‘/’ 要去掉,一时没想到好方法,就用公有方法调用私有方法,在私有方法的返回值中去掉好了。
然后再提供一个 pattern 方法返回能测试 location.pathname 是否与 PathItem 的 pushPath() 返回值是否匹配的正则表达式。
class PathItem<P> {
/** 自身的路径字符串 */
private self: string
/** 父路径引用 */
private parent: PathItem<any>
/** 可选的参数,string 表示参数名,boolean 表示是否必选 */
private params: [string, boolean, boolean][]
/**
* @param self {string} 自身的路径字符串
* @param parent {PathItem<any>} 父路径引用
* @param params {[string, boolean, boolean][]} 可选的参数,string 表示参数名,第一个 boolean 表示参数是否必选,第二个 boolean 表示是路径还是动态参数
*/
constructor (self: string, parent?: PathItem<any>, params?: Array<[string, boolean, boolean]>) {
this.self = self
this.parent = parent
this.params = params || []
}
private __pushPath (params?: P) {
if (this.parent && Object.keys(this.parent.params).some(v => params[v] === undefined)) {
console.error(`${this.parent.self} need a params, when called ${this.self}'s pushPath.`)
}
let path = this.self === '/' ? '/' : this.self + '/'
for (let i = 0, len = this.params.length; i < len; i++) {
let param = this.params[i]
if (!param) {
break
}
if (params) {
if (param[1]) {
if (params[param[0]] === undefined) {
console.error(`miss a required params in path ${this.self}, [${param[0]}], /n/n the params is ${JSON.stringify(params)}`)
}
}
let p = params[param[0]]
if (p) {
path += params[param[0]] + '/'
}
}
}
if (/^\//.test(this.self)) {
return path
} else {
return (this.parent ? this.parent.__pushPath(params) : '') + path
}
}
/**
* 返回要跳转的路径
* @param params 路由中的配置项
*/
pushPath (params?: P) {
return this.__pushPath(params).replace(/\/$/, '')
}
__pattern () {
let pat = this.self === '/' ? '/' : this.self + '(\/)?'
if (/^\//.test(this.self)) {
pat = pat.replace(/^\//, '')
}
for (let i = 0, len = this.params.length; i < len; i++) {
let param = this.params[i]
if (!param) {
break
}
if (param[2]) { // 固定值
if (param[1]) { // 必选
pat += param[0] + '/'
} else {
pat += '(' + param[0] + '/)?'
}
} else { // 动态参数
if (param[1]) { // 必选
pat += '(.+)' + '(/)?'
} else {
pat += '(.+)?(/)?'
}
}
}
if (/^\//.test(this.self)) {
return pat
} else {
if (this.parent && this.parent.self !== '/') {
return this.parent.pattern() + pat
} else {
return pat
}
}
}
/**
* 返回与 pushPath() 返回值相匹配的正则表达式
*/
pattern () {
return new RegExp(this.__pattern().replace(/\/$/, ''))
}
/**
* 返回 react-route 中要配置的 path 值
*/
routePath () {
let path = this.self
for (let i = 0, len = this.params.length; i < len; i++) {
let param = this.params[i]
if (!param) {
break
}
if (param[1] === true) {
if (param[2] === true) {
path += `/${param[0]}`
} else {
path += `/:${param[0]}`
}
} else if (param[1] === false) {
if (param[2] === true) {
path += `(/${param[0]})`
} else {
path += `(/:${param[0]})`
}
}
}
return path
}
}