前端路由的原理大致相同:当页面的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使用的基本结构是:

  1. 外层使用<Router>包裹整个app,主要类型有<BrowserRouter><HashRouter>,分别对应上面两种实现方法;首先把location、history对象(增强的)通过react context API注入到子组件中,然后在<Router>中会调用history.listen方法监听location变化,当location变化时采用setState改变location触发子组件的更新。
  2. <Link>标签做导航用,点击时会调用history.pushhistory.replace方法,并改变context中的location。
  3. context变化导致<Switch>重新渲染,找到匹配的<Route>渲染。
  4. 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端的处理:

  1. 引入history库,通过history.location获得当前location并进行初始渲染。
  2. 调用history.listen监听url变化,url变化时触发重新渲染函数。
  3. 渲染函数中首先得到location.pathname,调用router.resolve({pathname})得到匹配的route,最后调用render方法进行渲染。

server端的处理:

  1. 服务端没有url状态的变化,可以直接从req.path的的得到路由信息
  2. 调用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 */],
}

karl
78 声望5 粉丝