容易忽略的URL

3,634 阅读3分钟

场景再现

众所周知,vue-router有三种模式 :hashhtml5abstract , 一般的前端项目中会选择hash模式进行开发,最近做了一个运营活动就是基于vue-router的hash模式进行开发的。

  • 项目注册了两个路由(抽象出来的Demo)
var router = new VueRouter({
    routes: [{
        name: 'index',
        path: '',
        component: indexcomponent
    },{
        name: 'demo',
        path: '/demo',
        component: democomponent
    }]
});
  • 入口页面需要参数,所以提供URL:https://www.xxx.com?from=weixin, 浏览器里输入URL回车后,页面自动增加一个#/变为https://www.xxx.com?from=weixin#/

  • index页面中一个按钮点击后跳转demo,同时想携带index中获取的参数,看API选择了如下方式,结果URL变成了:https://www.xxx.com?from=weixin#/test?userId=123

router.push({ 
    path: 'demo',
    query: { 
        plan: 'private'
    }
})

产生质疑

  • URL有什么标准?(上面Demo页面跳转后URL看起来怪怪的)
  • vue-router是如何控制URL的?

质疑探究

URL标准

统一资源定位符(或称统一资源定位器/定位地址、URL地址等,英语:Uniform Resource Locator,常缩写为URL)

标准格式:scheme:[//authority]path[?query][#fragment]

例子

下图展示了两个 URI 例子及它们的组成部分。

                    hierarchical part
        ┌───────────────────┴─────────────────────┐
                    authority               path
        ┌───────────────┴───────────────┐┌───┴────┐
  abc://username:password@example.com:123/path/data?key=value&key2=value2#fragid1
  └┬┘   └───────┬───────┘ └────┬────┘ └┬┘           └─────────┬─────────┘ └──┬──┘
scheme  user information     host     port                  query         fragment

  urn:example:mammal:monotreme:echidna
  └┬┘ └──────────────┬───────────────┘
scheme              path

URL中的『?』『#』

  • 『?』

    • 路径与参数分隔符
    • 浏览器只识别url中的第一个『?』,后面的会当做参数处理
  • 『#』

    • 『#』一般是页面内定位用的,如我们最熟悉不过的锚点定位
    • 浏览器可以通过『onhashchange』监听hash的变化
    • http请求中不包含#
    • Request Headers中的Referer不包含#
    • 改变#不触发网页重载
    • url中#后面出现的任何字符都会被截断。(http://www.xxx.com/?color=#fff发出请求是:/color=
    • 改变#会改变history
    • window.location.hash读取#值

URL读取和操作

URL读取和操作涉及location和history两个对象,具体如下:

location API :

  • 属性
    • href = protocol + hostName + port + pathname + search + hash
    • host
    • origin
  • 方法
    • assign
    • href
    • replace ,不记录history
    • reload

history API:

  • 方法
    • back()
    • forward()
    • go()
  • H5新增API
    • pushState()
    • replaceState()
    • popstate监听变化

vue-router路由实现浅析

初始化router的时候,根据指定的mode选择路由实现,当然mode判断有一定逻辑和兼容策略

switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
}

我们选择hash模式进行深入分析,对应HashHistory模块,该模块是history/hash.js实现的,当被调用的时候,对全局路由变化进行了监听

window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
      ...
})

同时hash.js中也实现了push等api方法的封装,我们以push为例,根据源码可以看出,它的实现是基于基类transitionTo的实现,具体如下:

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      pushHash(route.fullPath)
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }

既然调用了transitionTo那么来看它的实现,获取参数后调用confirmTransition

transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    // 获取URL中的参数
    const route = this.router.match(location, this.current)
    this.confirmTransition(route, () => {
      this.updateRoute(route)
      onComplete && onComplete(route)
      this.ensureURL()
      ...
    })
  }

同时confirmTransition里实现了一个队列,顺序执行,iterator通过后执行next,进而志新pushHash(),实现页面hash改变,最终实现了${base}#${path}的连接

function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}

function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path
  }
}

问题解决

  • https://www.xxx.com?from=weixin#/test?userId=123这个页面看起来感觉怪,是因为这个连接中几乎包含了所有的参数,而且hash里面还有一个问号,一个URL中多个问号的不常见
  • vue-router也是基于基本的URL操作来进行URL切换的,在基本基础上进行了封装。里面很多思路还是应该多学习借鉴的。比如实现的队列、继承的运用等

总结

  • 标准的URL应该是 search + hash ,不要被当下各种框架欺骗,误以参数应该在hash后面拼接
  • URL中可以有多个问号,但为了便于理解,还是尽量避免这种写法
  • 避免上面尴尬问题的一个方法是 HTML5 Histroy 模式,感兴趣的同学可以关注并实践一下
  • 了解原理,了解设计模式,可以借鉴到平时开发项目中

参考文档