1

项目用的 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
  }
}

huaguzheng
496 声望1 粉丝