路径处理
m.route
和 m.request
都涉及路径的概念,用于生成路由跳转或请求的 URL。
路径类型
路径主要有两种类型:原始路径和参数化路径。
- 原始路径:直接作为 URL 使用的简单字符串。不会进行任何替换或分割,仅进行规范化处理,并将所有参数附加到末尾。
- 参数化路径:支持在路径中插入值,默认情况下会对插入的值进行转义,既方便使用又能防止 URL 注入。
对于 m.request
,路径几乎可以是任意 URL。但对于路由,路径只能是不包含协议(例如 https://
)或域名的绝对 URL 路径名。
路径参数
路径参数的使用方式很简单,有两种形式:
:foo
- 将params.foo
的值注入到 URL 中,并对该值进行转义。:foo...
- 将params.foo
的值作为原始路径注入到 URL 中,不对其进行任何转义。
这里的 params
对象指的是 m.route.set(path, params)
或 m.request({url, params})
中的 params
。
当通过 m.route(root, defaultRoute, routes)
接收路由时,可以使用这些参数从路由中提取值。提取方式与路径生成基本相同,只是方向相反。
// 编辑单个条目
m.route(document.body, '/edit/1', {
'/edit/:id': {
view: function () {
return [m(Menu), m('h1', '正在编辑用户 ' + m.route.param('id'))];
},
},
});
// 编辑由路径标识的条目
m.route(document.body, '/edit/pictures/image.jpg', {
'/edit/:file...': {
view: function () {
return [m(Menu), m('h1', '正在编辑文件 ' + m.route.param('file'))];
},
},
});
在第一个例子中,如果导航到示例中的默认路由,m.route.param("id")
将读取为 "1"
。在第二个例子中,m.route.param("file")
将读取为 pictures/image.jpg
。
路径参数可以用 /
、-
或 .
分隔,从而实现动态路径段,比单一的路径名更加灵活。例如,可以匹配 "/edit/:name.:ext"
这样的路由,以便根据文件扩展名进行编辑,或者匹配 /:lang-:region/view
这样的路由,以便进行本地化路由。
路径参数是贪婪匹配的。给定一个声明的路由 "/edit/:name.:ext"
,如果导航到 /edit/file.test.png
,则提取的参数将是 {name: "file.test", ext: "png"}
, 而不是 {name: "file", ext: "test.png"}
。类似地,给定 "/route/:path.../view/:child..."
,如果访问 /route/foo/view/bar/view/baz
,则提取的参数将是 {path: "foo/view/bar", child: "baz"}
。
参数规范化
插入到路径名中的路径参数会被省略,不会出现在查询字符串中。这样做既方便,又能保持路径名的可读性。例如,以下代码会发送一个服务器请求:GET /api/user/1/connections?sort=name-asc
,省略了 URL 字符串中重复的 id=1
参数。
m.request({
url: 'https://example.com/api/user/:userID/connections',
params: {
userID: 1,
sort: 'name-asc',
},
});
以下示例与上述示例等效:
m.request({
url: 'https://example.com/api/user/:userID/connections?sort=name-asc',
params: {
userID: 1,
},
});
当然,也可以混合使用路径参数和查询参数。以下代码会发送一个 GET /api/user/1/connections?sort=name-asc&first=10
请求。
m.request({
url: 'https://example.com/api/user/:userID/connections?sort=name-asc',
params: {
userID: 1,
first: 10,
},
});
这一特性也适用于路由匹配:可以匹配带有显式查询字符串的路由。匹配的参数会被保留以便使用,因此仍然可以通过 vnode 参数或通过 m.route.param
访问它们。请注意,虽然这是可行的,但通常不推荐这样做,因为应该优先使用路径来定义页面。如果需要为特定文件类型生成略有不同的视图,但从逻辑上讲,它仍然是一个类似查询的参数,而不是一个完全独立的页面,那么这在某些情况下可能很有用。
// 注意:通常*不*建议使用 - 你应该优先使用路径进行路由
// 声明,而不是查询字符串。
m.route(document.body, '/edit/1', {
'/edit?type=image': {
view: function () {
return [m(Menu), m('h1', '正在编辑照片')];
},
},
'/edit': {
view: function () {
return [m(Menu), m('h1', '正在编辑 ' + m.route.param('type'))];
},
},
});
查询参数是隐式使用的,无需显式命名即可接受。可以基于现有值进行匹配,例如在 "/edit?type=image"
中,但不需要使用 "/edit?type=:type"
来接受该值。实际上,Mithril.js 会将 "/edit?type=:type"
视为尝试按字面意思匹配 m.route.param("type") === ":type"
,因此通常不应该这样做。简而言之,使用 m.route.param("key")
或路由组件属性来读取查询参数。
路径规范化
解析后的路径总是返回规范化的结果:所有重复的参数和多余的斜杠都会被删除,并且总是以斜杠开头。这些细微的差异常常会造成阻碍,使路由和路径处理变得比预期复杂。Mithril.js 内部会对路由路径进行规范化处理,但不会直接暴露当前规范化路由。(可以通过 m.parsePathname(m.route.get()).path
计算它。)
匹配时处理重复参数,查询字符串中的参数优先于路径名中的参数,URL 末尾的参数优先于开头的参数。
路径转义
有些字符如果想按字面意思使用,就需要转义。幸运的是,encodeURIComponent
会对这些字符(以及更多字符)进行编码。在替换参数和添加查询参数时,Mithril.js 会根据需要自动使用此方法进行编码。以下是 Mithril.js 解释的字符:
:
=%3A
/
=%2F
(仅在路径中需要)%
=%25
?
=%3F
(仅在路径中需要)#
=%23
当然,还有其他一些字符必须按照 URL 规范进行转义,例如空格。但正如已经提到的,encodeURIComponent
会自动处理这些字符,并且 Mithril.js 在替换参数时会隐式地使用它。因此,只有在显式指定参数时才需要注意转义,例如在 m.request("https://example.com/api/user/User%20Name/:field", {params: {field: ...}})
中。