前端路由的原理大致相同:当页面的URL发生变化时,页面的显示结果可以根据URL的变化而变化,但是页面不会刷新。
要实现URL变化页面不刷新有两种方法:通过hash实现、通过History API实现。
1. 实现方法
- hash实现原理
改变页面的hash值不会刷新页面,而hashchange的事件,可以监听hash的变化,从而在hash变化时渲染新页面。
- History API实现原理
History API中pushState、replaceState方法会改变当前页面url,但是不会伴随着刷新,但是调用这两个方法改变页面url没有事件可以监听。有个history库增强了history API,采用发布订阅模式来对url的变化作出反映。其暴露出一个listen方法来添加订阅者,通过重写push、replace方法,使得这两个方法调用时通知订阅者,从而在url变化时渲染新页面。
2. react-route库
2.1 基本结构
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from "react-router-dom";
export default function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/users">Users</Link>
</li>
</ul>
</nav>
<Switch>
<Route path="/about">
<About />
</Route>
<Route path="/users">
<Users />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
</div>
</Router>
);
}
function Home() {
return <h2>Home</h2>;
}
function About() {
return <h2>About</h2>;
}
function Users() {
return <h2>Users</h2>;
}
react-router使用的基本结构是:
- 外层使用
<Router>
包裹整个app,主要类型有<BrowserRouter>
和<HashRouter>
,分别对应上面两种实现方法;首先把location、history对象(增强的)通过react context API注入到子组件中,然后在<Router>
中会调用history.listen
方法监听location变化,当location变化时采用setState改变location触发子组件的更新。 -
<Link>
标签做导航用,点击时会调用history.push
或history.replace
方法,并改变context中的location。 - context变化导致
<Switch>
重新渲染,找到匹配的<Route>
渲染。 -
Route
组件根据Swtich
的匹配结果渲染component,并通过React context API将location、history对象注入到子组件。
2.2 StaticRouter
服务端渲染时页面是静态的,没有state,不能通过state改变去触发子组件更新。在服务端是根据req.url
来渲染页面的,其基本使用方式如下:
import http from "http";
import React from "react";
import ReactDOMServer from "react-dom/server";
import { StaticRouter } from "react-router-dom";
import App from "./App.js";
http
.createServer((req, res) => {
const context = {};
const html = ReactDOMServer.renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
);
//重定向时触发
if (context.url) {
res.writeHead(context.status, {
Location: context.url
});
res.end();
} else {
res.write(`
<!doctype html>
<div id="app">${html}</div>
`);
res.end();
}
})
.listen(3000);
在<StaticRouter>
中没有使用history库了,而是创建了一个简单的history对象,其对应history库创建的history对象,但是其中的方法大多数为空的,例如:
handleListen = () => {};
只是为了将history传递时不报错。其中的push和replace方法是有效的,调用时会给context.url、context.location赋值。如上所示,但context.url有值时会重定向。
由于<StaticRouter>
内部会
return <Router {...rest} history={history} staticContext={context} />;
而statusContext属性在客户端渲染时不存在,可以通过这个条件去增加返回码:
<Route
render={({ staticContext }) => {
if (staticContext) staticContext.status = status;
// Redirect会调用push或replace
return <Redirect from={from} to={to} />;
}}
/>
2.3 静态路由 React Router Config
import { renderRoutes } from "react-router-config";
const routes = [
{
component: Root,
routes: [
{
path: "/",
exact: true,
component: Home
},
{
path: "/child/:id",
component: Child,
routes: [
{
path: "/child/:id/grand-child",
component: GrandChild
}
]
}
]
}
];
const Root = ({ route }) => (
<div>
<h1>Root</h1>
{/* child routes won't render without this */}
{renderRoutes(route.routes)}
</div>
);
const Home = ({ route }) => (
<div>
<h2>Home</h2>
</div>
);
const Child = ({ route }) => (
<div>
<h2>Child</h2>
{/* child routes won't render without this */}
{renderRoutes(route.routes, { someProp: "these extra props are optional" })}
</div>
);
const GrandChild = ({ someProp }) => (
<div>
<h3>Grand Child</h3>
<div>{someProp}</div>
</div>
);
//renderRoutes方法对routes进行map生成<Route>
ReactDOM.render(
<BrowserRouter>
{/* kick it all off with the root route */}
{renderRoutes(routes)}
</BrowserRouter>,
document.getElementById("root")
);
3. Universal Router库
Universal Router是一个轻量化的静态路由库,可以使用在客户端和服务端。
client端的处理:
- 引入history库,通过
history.location
获得当前location并进行初始渲染。 - 调用
history.listen
监听url变化,url变化时触发重新渲染函数。 - 渲染函数中首先得到
location.pathname
,调用router.resolve({pathname})得到匹配的route,最后调用render方法进行渲染。
server端的处理:
- 服务端没有url状态的变化,可以直接从req.path的的得到路由信息
- 调用router.resolve({pathname})得到匹配的route,最后调用render方法进行渲染。
路由配置代码的基本结构:
const routes = [
{ path: '/one', action: () => '<h1>Page One</h1>' },
{ path: '/two', action: () => '<h1>Page Two</h1>' },
{ path: '(.*)', action: () => '<h1>Not Found</h1>' }
]
//context this.context = { router: this, ...options.context }
const router = new UniversalRouter(routes, {context,resolveRoute})
//resolve的参数pathnameOrContext
// const context = {
// ...this.context,
// ...(typeof pathnameOrContext === 'string'
// ? { pathname: pathnameOrContext }
// : pathnameOrContext),
// }
router.resolve({ pathname: '/one' }).then(result => {
document.body.innerHTML = result
// renders: <h1>Page One</h1>
})
-
首先通过routes定义静态路由,path属性是必须的,action是resolve时默认的调用函数
function resolveRoute(context, params) { if (typeof context.route.action === 'function') { return context.route.action(context, params) } return undefined }
- 生成router实例,此时可以通过resolveRoute option定义
router.resolve
时的逻辑,通过context添加自定义的方法和属性。 - 调用
router.resolve
去匹配pathname,该函数的参数都会加到context属性上,函数内部返回resolveRoute(context, params)
的返回值。
权限管理:
- context对象上有next方法,调用
context.next()
会遍历resolve其子路由,调用context.next(true)
会遍历resolve所有剩余路由。 - resolve得到的返回值为undefined时将会尝试匹配其子路由,得到的返回值为null时将会尝试匹配其兄弟路由
const middlewareRoute = {
path: '/admin',
action(context) {
if (!context.user) {
return null // route does not match (skip all /admin* routes)
}
if (context.user.role !== 'Admin') {
return 'Access denied!' // return a page (for any /admin* urls)
}
return undefined // or `return context.next()` - try to match child routes
},
children: [/* admin routes here */],
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。