资讯专栏INFORMATION COLUMN

搞定 PM:天天改 URL,react-router 动态路由配置

dadong / 1133人阅读

摘要:分析一下,原本我们路径中的固定部分都是构造函数中的参数,动态部分都在参数。但是问题也来了,这样的路径也能匹配。于是在构造函数的参数的每一项中我们还要用一个标识来标记这个的这个属性值是固定的还是不固定的,如果是固定的输出否则输出。

项目用的 React 框架。公司有三类人掌握着 URL 生杀大权,产品总监,产品经理,还有 SEO ???特别是产品总监还兼职首席拼写官,导致 URL 一周一个样。即使上线了也是如此,告诉他们用户已经收藏了这个链接,不要随意更改,然而根本劝不动。

初始配置

  
    
        
        
        
        
      
  
    
  

像这样写死的配置显然是不行的,因为代码中到处充斥着 this.props.router.push("host/meetings/previous" + 1) 这样的调用。上线头一周产品总监就干掉了 host ,变成 "meetings/previous", 这样一来代码中处处要改动。而且调用 push 的时候,要来回确定路径有没有敲错,路径中的动态参数有没有缺少。

我需要一个灵活一点的,能够给他们随意折腾的配置,而且要能有友好的提示。那么上 Typescript 吧。

灵活一点

现在我需要一个对象,对象有一个方法能够生成 Route 中 path 的值,然后还有一个方法能生成跳转操作的路径。比如 UpcomingMeetingView 这个对应的路由,path 的值是 "upcoming/:index" 而实际跳转时候的路径是类似这样 "/host/upcomging/1" 的字符串。

比较一下两个值,可以发现,跳转时候的路径是和父路径相关的,也就是这个对象要保存父对象引用。
然后要怎么做友好的提示呢?对于路径中的参数我们要知道参数名和参数值类型,这就要用到泛型了。
上代码:

class PathItem

{ /** 自身的路径字符串 */ self: string /** 父路径引用 */ parent: PathItem /** 可选的参数,string 表示参数名,boolean 表示是否必选 */ params: [string, boolean][] constructor (self: string, parent?: PathItem, 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

{ /** 自身的路径字符串 */ private self: string /** 父路径引用 */ private parent: PathItem /** 可选的参数,string 表示参数名,boolean 表示是否必选 */ private params: [string, boolean, boolean][] /** * @param self {string} 自身的路径字符串 * @param parent {PathItem} 父路径引用 * @param params {[string, boolean, boolean][]} 可选的参数,string 表示参数名,第一个 boolean 表示参数是否必选,第二个 boolean 表示是路径还是动态参数 */ constructor (self: string, parent?: PathItem, 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 } }

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/88851.html

相关文章

  • 谈谈 react-router

    摘要:谈谈最近使用的来开发项目,感觉确实是爽的飞起,然而总感觉还是少了点什么。注意当前版本依赖的是请不要安装最新版。同样的也有这个方法表示在离开路由前执行。会深度优先遍历整个理由配置来寻找一个与给定的相匹配的路由。配置是建立在之上的。 谈谈 最近使用的 React + webpack 来开发项目,感觉确实是爽的飞起,然而总感觉还是少了点什么。对,是多页面,每次请求页面还要后端路由给你?多不爽...

    MASAILA 评论0 收藏0
  • 大厂高级前端面试题答案

    摘要:但出于隐私方面的原因,对象不再允许脚本访问已经访问过的实际。唯一保持使用的功能只有和方法。将当前和加入到中,并用新的和替换当前,不会造成页面刷新。 阿里 使用过的koa2中间件https://www.jianshu.com/p/c1e... koa-body原理https://blog.csdn.net/sinat_1... 有没有涉及到Clusterhttp://nodejs.cn/...

    oysun 评论0 收藏0
  • react-router 升级小记

    摘要:前言最近将公司项目的从版本升到了版本,跟完全不兼容,是一次彻底的重写。升级过程中踩了不少的坑,也有一些值得分享的点。没有就会匹配所有路由最后不得不说升级很困难,坑也很多。 前言 最近将公司项目的 react-router 从 v3 版本升到了 v4 版本,react-router v4 跟 v3 完全不兼容,是一次彻底的重写。这也给升级造成了极大的困难,与其说升级不如说是对 route...

    isLishude 评论0 收藏0
  • 高级前端面试题大汇总(只有试题,没有答案)

    摘要:面试题来源于网络,看一下高级前端的面试题,可以知道自己和高级前端的差距。 面试题来源于网络,看一下高级前端的面试题,可以知道自己和高级前端的差距。有些面试题会重复。 使用过的koa2中间件 koa-body原理 介绍自己写过的中间件 有没有涉及到Cluster 介绍pm2 master挂了的话pm2怎么处理 如何和MySQL进行通信 React声明周期及自己的理解 如何...

    kviccn 评论0 收藏0

发表评论

0条评论

dadong

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<