koa 中依赖的库 parseurl

1,157 阅读2分钟

「这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

这个系列是源码阅读文章,还是从我最熟悉的 koa 入手,这次不看 koa 本身,来读一读 koa 依赖的一些工具库的源码,这篇为本系列第一篇,聊一聊 url 解析库 parseurl

parseurl 是一个 url 解析工具,其解析结果和 node 标准库 url.parse 的解析效果相同。既然标准库已经提供了 url 解析能力,koa 为什么还要依赖一个第三方库呢?带着问题开始 parseurl 的源码阅读。

在阅读之前先看一下 parseurl 库在 koa 中的应用,在 koa 中只有四处引用到 parseurl 库,用于处理 request 中 path 和 querystring 的 getter 和 setter 方法。

get path () {
	return parse(this.req).pathname
},
set path (path) {
	const url = parse(this.req)
	if (url.pathname === path) return

	url.pathname = path
	url.path = null
	this.url = stringify(url)
},
get querystring () {
	if (!this.req) return ''
	return parse(this.req).query || ''
},
set querystring (str) {
	const url = parse(this.req)
	if (url.search === `?${str}`) return

	url.search = str
	url.path = null

	this.url = stringify(url)
},

parseurl 库导出两个函数:默认导出的 parseurl 和 originalurl,两个函数都接收 req 作为参数,req 的类型即 http 模块中 createServer 回调函数中的 req 参数,实际上只需要关注 url 和 originalUrl 两个属性即可,即:

type Req {
	url?: string;
	originalUrl?: string;
}

其中 parseurl 解析 url 属性,originalurl 解析 originalUrl,没有 originalUrl 时解析 url,其他方面两个方法没有区别。

我们来看测试用例:使用 parseurl 解析 http://localhost:8888/foo/bar ,会得到一个 Url 对象:

Url {
  protocol: 'http:',
  slashes: true,
  auth: null,
  host: 'localhost:8888',
  port: '8888',
  hostname: 'localhost',
  hash: null,
  search: null,
  query: null,
  pathname: '/foo/bar',
  path: '/foo/bar',
  href: '<http://localhost:8888/foo/bar>',
  _raw: '<http://localhost:8888/foo/bar>'
}

对比 url.parse 的结果:

Url {
  protocol: 'http:',
  slashes: true,
  auth: null,
  host: 'localhost:8888',
  port: '8888',
  hostname: 'localhost',
  hash: null,
  search: null,
  query: null,
  pathname: '/foo/bar',
  path: '/foo/bar',
  href: '<http://localhost:8888/foo/bar>'
}

会发现 parseurl 多一个 _raw 字段,这个字段是 parseurl 特有的功能,我们来看在源码中的使用:

function parseurl (req) {
  var url = req.url

  if (url === undefined) {
    // URL is undefined
    return undefined
  }

  var parsed = req._parsedUrl

  if (fresh(url, parsed)) {
    // Return cached URL parse
    return parsed
  }

  // Parse the URL
  parsed = fastparse(url)
  parsed._raw = url

  return (req._parsedUrl = parsed)
};

这里 fastparse 是 url 真正的解析逻辑,可以看到在解析结果上的 _raw 字段是额外添加的,而添加它的真正目的就在最后一行,最终的解析结果被赋值给了原始的 req 对象。每次进入 parseurl 时会先去判断是否有一个已存在的 _parsedUrl 结果,如果已有结果并且它的 _raw 和本次的 url 一致,这时就可以直接返回这个结果,这里起到的就是一个缓存的效果。

至于 fastparse 内部逻辑很简单,本质还是调用 url.parse 实现的,不过这里有一个优化,当解析 / 开头的路径时,如果没有特殊字符这里可以直接返回字符串,不会进行进一步 parse 逻辑,从一定程度上优化了性能。

function fastparse (str) {
  if (typeof str !== 'string' || str.charCodeAt(0) !== 0x2f /* / */) {
    return parse(str)
  }

  var pathname = str
  var query = null
  var search = null

  // This takes the regexp from <https://github.com/joyent/node/pull/7878>
  // Which is /^(\/[^?#\s]*)(\?[^#\s]*)?$/
  // And unrolls it into a for loop
  for (var i = 1; i < str.length; i++) {
    switch (str.charCodeAt(i)) {
      case 0x3f: /* ?  */
        if (search === null) {
          pathname = str.substring(0, i)
          query = str.substring(i + 1)
          search = str.substring(i)
        }
        break
      case 0x09: /* \t */
      case 0x0a: /* \n */
      case 0x0c: /* \f */
      case 0x0d: /* \r */
      case 0x20: /*    */
      case 0x23: /* #  */
      case 0xa0:
      case 0xfeff:
        return parse(str)
    }
  }

  var url = Url !== undefined
    ? new Url()
    : {}

  url.path = str
  url.href = str
  url.pathname = pathname

  if (search !== null) {
    url.query = query
    url.search = search
  }

  return url
}

在仓库中提供了 bench 测试脚本,可以看到 parseurl 的综合性能表现是很高的,这大概就是 koa 选用 parseurl 的原因吧。

在 url 中,与 parse 相反的方法是 format,它会把 Url 对象转化为字符串,在上面看到的 koa 中的 path 和 querystring 的 setter 方法中 stringify 就是 url.format 方法,这里没有使用第三方库,是直接使用 node.js 的原生方法。

至此 parseurl 源码的全部内容就结束了,其实很多 npm 包做的事情都很简单,下一篇会看下 koa 依赖的其他 npm 包。