分享前端技术架构与工程的学习笔记
目前实现前端路由的途径有两种:
- Hash模式:使用URL的hash标识作为路径标记,通过监听hashchange事件实现回调逻辑。
- History模式:使用URL的path作为路径标记,建筑History API及其相关事件实现跳转和回调逻辑。
Hash模式
一个完整的URL包括协议、用户、域名、端口、路径、查询参数和hash标识,比如从http://www.foo.com/user?name=bar#message可以获取如下表的信息
| 协议 | 用户名 | 域名 | 端口号 | 路径 | 查询参数 | hash标识 |
|---|---|---|---|---|---|---|
| http | 空 | www.foo.com | 80 | /user | name=bar | message |
当用户、域名、端口、路径、查询参数放生变化时,浏览器将其判定为新的URL,进而以新的URL发起请求,然而hash标识变化不会触发次行为。所以hash模式的有点之一是不需要服务器支持,是前端完全自主的路由机制。相对依赖于HTML5规范新增History API的History路由,Hash在浏览器兼容方面表现更优。
API设计
一个路由必须包含最基础的两个因子:路径(path)和回调函数(action),为了便于调试和定位要额外加上唯一的name属性。并且增加动态路由的支持,此时路由的配置API雏形如下:
new Router([{
path: '/',
name: 'home',
action() {}
},{
// name和sex为动态参数
path: 'user/:name/:sex',
name: 'user',
action() {}
}])
这样就完成了一个简单路由的配置,但不足以应对一些复杂的场景,还需要进一步抽象。Vue-router是很好的借鉴案例,每个路由都是一个类似组件的对象,有自己的行为和声明周期。仿照Vue-router的设计,为路由定义声明周期:
- beforeEach和afterEach是全局钩子函数,分别在进入每个路由之前和之后执行。
- 路由内部的beforeEnter在action之前执行,并且是阻塞式,根据返回值分发后续逻辑,返回true继续跳转,false终止跳转。
- 路由有两种行为:leave和update,分别触发beforeLeave和beforeUpdate,两者均为阻塞式。
- 路由的afterEnter在action之后执行,afterUpdate在路由更行成功之后执行。
结合以上生命周期的设计便可确定配置API的文正形态,可分为两方面:一是全局钩子,二是每个路由的参数和钩子函数。
new Router({
routes: [{
path: '/',
name: 'home',
beforeEnter() {},
action() {},
afterEnter(){},
beforeLeave() {},
beforeUpdate() {},
afterUpdate() {}
}],
// 全局钩子函数
beforeEach() {},
afterEach() {}
})
类结构设计
为了便于路基拆分,我们设计两个类:负责外层路由统筹的Router和负责单个路由的Route。Router类唯一个所有Route的列表,监听hashchange事件跳转对应的路由;对外暴露go和back两个API进行路由的跳转和回退。每个用户配置的路由均对应一个Route实例,每个路由维护本身的参数和状态等信息,同时对外暴露match和parse两个API,均接受URL的完整路径作为参数,match返回是否匹配自身路由,parse解析路径中的参数信息并执行回调。TS定义两者的接口类型:
interface RouterInterface {
go(path: string): void;
back(): void;
}
class Router implements RouterInterface {}
interface RouteInterface {
name: string;
beforeEnter: Function | null;
afterEnter: Function | null;
beforeUpdate: Function | null;
afterUpdate: Function | null;
beforeLeave: Function | null;
// 动态参数
params: {
[k: string]: string | undefined;
}
// 完整路径
fullpath: string;
match(fullpath: string): boolean;
// 解析URL的完整路径并执行回调
parse(fullpath: string): void;
}
class Route implements RouterInterface {}
Router初始化之后启动hashchange事件监听,为了支持刷新还需要立即判断当前URL与用户配置路由是否匹配:
private _start() {
window.addEventListener('hashchange', (ev: HashChangeEvent) => {
this._onHashChange(ev.newURL)
})
this._restore()
}
// 恢复当前URL对应的路由
private _restore() {
this._onHashChange(window.location.href)
}
_onHashChange方法的逻辑是先获取hash值,然后循环调用每个Route的match方法进行匹配。对于动态路由处理先用一个简单的正则表达式从配置中参数path中解析出动态参数的名称,并将所有参数汇总为一个路由匹配正则表达式:
export class Route implements RouteInterface {
static paramPattern = /\/\:\b(\w+)\b/g
constructor(info) {
this._path = info.path
let tmpPath = this._path
let match = Route.paramPattern.exec(this._path)
while(macth) {
const param = match[1]
this._requiredParams.push(param)
this.params[param] = undefined
tmpPath.replace(`/:${param}`, '/{\\w+}')
match = Route.paramPattern.exec(this._path)
}
Route.paramPattern.lastIndex = 0
this._validator = new RegExp(`^${tmpPath.replace(/\//g,'\\/')}$`)
}
}
配置的动态路由/user/:name/:sex经过以上逻辑处理之后可提取出两个参数name和sex,路由匹配正则_validator为/^\user/(\w+)/(\w+)$/.Route.match功能便是用 _validator测试入参是否匹配。
阻塞shi路由钩子函数
阻塞式钩子函数最简单的实现方式是根据函数返回值是true哈市false进行逻辑分发,但需要明确各个钩子函数的触发条件。beforeEach在进入每个路由之前执行,那么触发它的条件必须是当前路由的beforeLeave返回true;而触发beforeLeave触发条件是目标枯井与当前路由并非相同。对于动态路由/user/:name/:sex而言,改变name和sex相当于更新而非离开当前路由,触发beforeUpdate而不是beforeLeave。调用Router.go(targetPath)进行跳转的流程图如路由流程图所示,流程中每个阻塞式钩子函数的返回值均会决定到底是跳转还是中断(具体方式参考源码)
History模式
History路由在前后端整体架构上不同于Hash路由最显著的特点是需要服务器支持,History路由支持刷新的条件是服务端将所有路由的请求rewrite(注意不是redicet)到跟路由,然后前端浏览器环境下进行子路由恢复,在功能实现上,History路由和Hash路由的主要区别在于路由间的跳转和监听方式:
- 跳转新路径是由history.pushState API,回退和前进是由history.back和history.go API。
- 通过监听popstate事件处理路由回退
popstate事件只有在调用history.back和hietory.go时才触发,而不会被pushState触发。如果不进行特殊处理,仅使用原生的hsitory API只能监听到路由的前进和后退,而监听不到新路由的跳转。所以我们必须在跳转新路由时触发并监听某种事件,可以借助CustomEvent API实现popstate语义相反的“pushstate”事件,这是一个自定义事件的函数并将其命名为pushstate,这样就可以被window监听到。
// 创建pushstate事件
function createPushstateEvent(state:KV<string>): PushShtateEvent{
const ev = new CustomEvent('pushstate')
ev['state'] = state
return <PushStateEvent>ev
}
// 监听pushstate事件
window.addEventListener('pushstate', (ev: PushShtateEvent) => {
this._onRouteChange(window.location.pathname,ev.state.name)
})
跳转新路径的逻辑为调用history.pushState API的同时调用createPushStateEvent函数创建一个pushstate事件,并且以window对象为target触发,其中state对象的属性name为目标路由的名称,借助此属性可以简化目标路径的匹配逻辑。
private _pushState(state: KV<string>,path: string) {
this._history.pushtate(state, '', path)
window.dispatchEvent(createPushstateEvent(state))
}
this._pushState({
name: targetRoute.name
}, path)
History路由的有点在于能够被爬虫程序抓取到路径,然而仅路由被抓取还远不足以支撑SEO,因为页面的本质仍然是CSR的SPA,若想达到更好的SEO效果则必须采用SSR。Node.js的出现改变了Web技术的传统格局,不仅可以取代PHP、Java等作为SSR的承载者,而且为JavaScript同构编程提供了强有力的技术支撑。
Node.js中间层与同构编程
....待续