前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【路由】:路由那些事——上

【路由】:路由那些事——上

作者头像
WEBJ2EE
发布2021-04-07 00:50:30
1.8K0
发布2021-04-07 00:50:30
举报
文章被收录于专栏:WebJ2EEWebJ2EE

1. 思维导图

2. 什么是前端路由?

前端路由是前端页面的状态管理器

前端路由起源于 SPA 单页应用架构(现代前端开发中最流行的页面模型):

  • 单页面应用只有一个主页面,页面间的切换实际是 DOM 结构的动态替换(无刷新,用户体验好)。
  • 基于 React 的 SPA 应用,页面是由不同的组件构成,页面的切换其实就是不同组件间的切换。
  • 我们把页面间(即组件间)的切换与浏览器地址栏中 URL 的变换关联起来(例如:根据浏览器地址栏的变化切换页面),这就是前端路由。

浏览器地址变化 => 视觉上的页面切换 => 实际上的组件切换

前端路由就是用来完成这个任务的技术

3. 路由基本原理

前端三杰 Angular、React、Vue 都推荐单页面应用 SPA 开发模式,它们都有自己的前端路由解决方案:

  • Angular:@angular/router
  • React:react-router
  • Vue:vue-router。

一般来说,这些路由组件会在浏览器环境下,提供两种不同方式的路由:Hash 和 History;也提供非浏览器环境下(例如:Native环境、单元测试环境)的路由能力。

3.1. Hash

Hash —— 即地址栏 URL 中的 # 符号。路由里的 # 我们称之为 hash。

Any URL that contains a # character is a fragment URL. The portion of the URL to the left of the # identifies a resource that can be downloaded by a browser and the portion on the right, known as the fragment identifier, specifies a location within the resource.

  • # 代表网页中的一个位置。其右面的字符,就是该位置的标识符。而且在第一个 # 后面出现的任何字符,都会被浏览器解读为位置标识符。
  • HTTP请求中不包括 #。#是用来指导浏览器动作的,对服务器端完全无用。
  • 改变 # 不触发网页重载。仅改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页。
  • 改变#会改变浏览器的访问历史。每一次改变 # 后的部分,都会在浏览器的访问历史中增加一个记录,使用"后退"按钮,就可以回到上一个位置。
  • 可通过 window.location.hash 读取 # 值。
  • 当 # 值发生变化时,会触发 onhashchange 事件。

示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Hash-Router</title>
    <script src="jquery-1.11.3.min.js"></script>
</head>
<body>
<div class="router_box">
    <a href="/home" class="router" replace="true">主页</a>
    <a href="/news" class="router">新闻</a>
    <a href="/team" class="router">团队</a>
    <a href="/about" class="router">关于</a>
    <a href="/abcd" class="router">随便什么</a>
</div>
<div id="router-view"></div>
<script>
    class Router {
        constructor(params) {
            // 记录routes配置
            this.routes = params.routes || [];

            const that = this;

            // 绑定路由响应事件
            $('.router').click(function(e){
                // 阻止a标签的默认行为
                e.preventDefault();

                const $this = $(this);
                const aHref = $this.attr('href');
                const aReplace = $this.attr('replace');

                that.changeHash(aHref, aReplace);
            });

            // 监听路由改变
            window.addEventListener('hashchange', () => {
                this.changeRoute();
            });

            // 初始路由跳转
            this.changeRoute();
        }

        changeHash(href, replace) {
            // 判断是replace方法还是push方法
            if (replace) {
                const i = window.location.href.indexOf('#');
                // 通过replace方法直接替换url
                window.location.replace(
                    window.location.href.slice(0, i >= 0 ? i : 0) + '#' + href
                );
            } else {
                // 通过赋值追加
                window.location.hash = href;
            }
        }

        // 路由改变监听事件
        changeRoute() {
            const nowHash = window.location.hash;
            const index = this.routes.findIndex((item, index) => {
                return nowHash == ('#' + item.path);
            });
            if (index >= 0) {
                document.querySelector('#router-view').innerHTML = this.routes[index].component;
            } else {
                const defaultIndex = this.routes.findIndex((item, index) => {
                    return item.path == '*';
                });
                if (defaultIndex >= 0) {
                    const i = window.location.href.indexOf('#');
                    window.location.replace(
                        window.location.href.slice(0, i >= 0 ? i : 0) + '#' + this.routes[defaultIndex].redirect
                    );
                }
            }
        };
    }

    new Router({
        routes: [
            { path: '/home', component: '<h1>主页</h1><h4>这是主页</h4>' },
            { path: '/news', component: '<h1>新闻</h1><h4>这是新闻</h4>' },
            { path: '/team', component: '<h1>团队</h1><h4>这是团队</h4>' },
            { path: '/about', component: '<h1>关于</h1><h4>这是关于</h4>' },
            { path: '*', redirect: '/home' }
        ]
    });
</script>
</body>
</html>

3.2. History

HTML5 引入了 history.pushState() 和 history.replaceState() 方法,它们分别可以添加和修改历史记录条目。允许在不刷新页面的前提下,通过脚本语言的方式来进行页面上某块局部内容的更新。这些方法通常与 window.onpopstate 配合使用。

示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>History-Router</title>
    <script src="jquery-1.11.3.min.js"></script>
</head>
<body>
<div class="router_box">
    <a href="/home" class="router" replace="true">主页</a>
    <a href="/news" class="router">新闻</a>
    <a href="/team" class="router">团队</a>
    <a href="/about" class="router">关于</a>
    <a href="/abcd" class="router">随便什么</a>
</div>
<div id="router-view"></div>
<script>
    class Router {
        constructor(params) {
            // 记录routes配置
            this.routes = params.routes || [];

            const that = this;

            // 绑定路由响应事件
            $('.router').click(function(e) {
                // 阻止a标签的默认行为
                e.preventDefault();

                const $this = $(this);
                const aHref = $this.attr('href');
                const aReplace = $this.attr('replace');

                that.changeHis(aHref, aReplace);
            });

            // 监听路由改变
            window.addEventListener('popstate', e => {
                this.changeRoute();
            });

            // 初始路由跳转
            this.changeRoute();
        }

        changeHis(href, replace) {
            // 判断是replace方法还是push方法
            if (replace) {
                window.history.replaceState({}, '', window.location.origin + href);
                this.changeRoute();
            } else {
                window.history.pushState({}, '', window.location.origin + href);
                this.changeRoute();
            }
        }

        // 路由改变监听事件
        changeRoute() {
            let path = window.location.href.replace(window.location.origin, '');
            let index = this.routes.findIndex((item, index) => {
                return path == item.path;
            });
            if (index >= 0) {
                document.querySelector('#router-view').innerHTML = this.routes[index].component;
            } else {
                let defaultIndex = this.routes.findIndex((item, index) => {
                    return item.path == '*';
                });
                if (defaultIndex >= 0) {
                    window.history.pushState({}, '', window.location.origin + this.routes[defaultIndex].redirect);
                    this.changeRoute();
                }
            }
        };
    }

    new Router({
        routes: [
            { path: '/home', component: '<h1>主页</h1><h4>这是主页</h4>' },
            { path: '/news', component: '<h1>新闻</h1><h4>这是新闻</h4>' },
            { path: '/team', component: '<h1>团队</h1><h4>这是团队</h4>' },
            { path: '/about', component: '<h1>关于</h1><h4>这是关于</h4>' },
            { path: '*', redirect: '/home' }
        ]
    });
</script>
</body>
</html>

4. 有哪些路由集成方案?

除了 React、Vue 体系下的基础路由库:ReactRouter、VueRouter。通常各技术体系的 UI 开发框架上,还会提供对路由的深度集成功能。例如:

  • React 体系
    • AntDesign Pro
      • UmiJS
  • Vue 体系
    • iView-Admin
    • element-Admin

4.1. Umijs

Umi,中文可发音为乌米,是可扩展的企业级前端应用框架。Umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。 https://umijs.org/zh-CN/docs

4.2. iView-admin

iView-admin是iView生态中的成员之一,是一套采用前后端分离开发模式,基于Vue的后台管理系统前端解决方案。iView-admin2.0脱离1.x版本进行重构,换用Webpack4.0 + Vue-cli3.0作为基本开发环境。内置了开发后台管理系统常用的逻辑功能,和开箱即用的业务组件,旨在让开发者能够以最小的成本开发后台管理系统,降低开发量。 https://lison16.github.io/iview-admin-doc/#/

4.3. vue-element-admin

vue-element-admin 是一个后台前端解决方案,它基于 vue 和 element-ui实现。它使用了最新的前端技术栈,内置了 i18 国际化解决方案,动态路由,权限验证,提炼了典型的业务模型,提供了丰富的功能组件,它可以帮助你快速搭建企业级中后台产品原型。 https://panjiachen.github.io/vue-element-admin-site/zh/guide/

5. React Router 案例分析

5.1. 示例:基础

描述:

  • 常规业务路由包含:/login、/home;
  • 404 路由使用 / 实现;

效果图:

实现策略:

  • 使用 <Link> 组件实现声明式跳转。
  • 使用 <Switch> 组件实现互斥型路由渲染,只渲染匹配到的第一个。
  • 使用 <Route> 组件描述每一个路由条目。

关键代码:

import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";

// This site has 3 pages, all of which are rendered
// dynamically in the browser (not server rendered).
//
// Although the page does not ever refresh, notice how
// React Router keeps the URL up to date as you navigate
// through the site. This preserves the browser history,
// making sure things like the back button and bookmarks
// work properly.

export default function BasicExample() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/login">Login</Link>
          </li>          
          <li>
            <Link to="/home">Home</Link>
          </li>
          <li>
            <Link to="/path-not-exist">path-not-exist</Link>
          </li>
        </ul>

        <hr />

        {/*
          A <Switch> looks through all its children <Route>
          elements and renders the first one whose path
          matches the current URL. Use a <Switch> any time
          you have multiple routes, but you want only one
          of them to render at a time
        */}
        <Switch>
          <Route path="/login">
            <Login />
          </Route>
          <Route path="/home">
            <Home />
          </Route>
          <Route path="/">
            <Error404 />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

// You can think of these components as "pages"
// in your app.

function Home() {
  return (
    <div>
      <h2>Home</h2>
    </div>
  );
}

function Error404() {
  return (
    <div>
      <h2>404</h2>
    </div>
  );
}

function Login() {
  return (
    <div>
      <h2>Login</h2>
    </div>
  );
}

function Dashboard() {
  return (
    <div>
      <h2>Dashboard</h2>
    </div>
  );
}

5.2. 示例:URL 参数

描述:

  • url 分两组:/group1、/group2
  • /group1/:id,渲染 Child1 组件
  • /group2/:id,渲染 Child2 组件
  • 采用 useParams 获取路由参数

效果图:

实现策略:

  • 使用"path-to-regexp":"^1.7.0" 能够识别的路径模式(例:采用 : 配置参数)配置路由

关键代码:

import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  useParams,
} from "react-router-dom";

// Params are placeholders in the URL that begin
// with a colon, like the `:id` param defined in
// the route in this example. A similar convention
// is used for matching dynamic segments in other
// popular web frameworks like Rails and Express.

export default function ParamsExample() {
  return (
    <Router>
      <div>
        <h2>Accounts</h2>

        <ul>
          <li>
            <Link to="/group1/netflix">Netflix</Link>
          </li>
          <li>
            <Link to="/group1/zillow-group">Zillow Group</Link>
          </li>
          <li>
            <Link to="/group2/yahoo">Yahoo</Link>
          </li>
          <li>
            <Link to="/group2/modus-create">Modus Create</Link>
          </li>
        </ul>

        <Switch>
          <Route path="/group1/:id">
            <Child1 />
          </Route>
          <Route path="/group2/:id">
            <Child2 />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

function Child1() {
  // We can use the `useParams` hook here to access
  // the dynamic pieces of the URL.
  const { id } = useParams();

  return (
    <div>
      <h2>This is [Child1], ID: {id}</h2>
    </div>
  );
}

function Child2() {
  // We can use the `useParams` hook here to access
  // the dynamic pieces of the URL.
  const { id } = useParams();

  return (
    <div>
      <h2>This is [Child2], ID: {id}</h2>
    </div>
  );
}

5.3. 示例:嵌套路由

描述:

  • 一级路由:/、/topics、/resources
  • /topics 和 /resources 一级路由下,都包含二级路由:
    • /rendering
    • /components
    • /props-v-state

效果图:

实现策略:

  • 使用 <Route> 组件的 exact 属性,实现精确、模糊匹配。
    • 容器路由(父路由),采用模糊匹配
    • 叶子路由(子路由),采用精确匹配

关键代码:

import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  useParams,
  useRouteMatch
} from "react-router-dom";

// Since routes are regular React components, they
// may be rendered anywhere in the app, including in
// child elements.
//
// This helps when it's time to code-split your app
// into multiple bundles because code-splitting a
// React Router app is the same as code-splitting
// any other React app.

export default function NestingExample() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/topics">Topics</Link>
          </li>
          <li>
            <Link to="/resources">Resources</Link>
          </li>          
        </ul>

        <hr />

        <Switch>
          <Route exact path="/">
            <Home />
          </Route>
          <Route path="/topics">
            <Topics />
          </Route>
          <Route path="/resources">
            <Topics />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

function Home() {
  return (
    <div>
      <h2>Home</h2>
    </div>
  );
}

function Topics() {
  // The `path` lets us build <Route> paths that are
  // relative to the parent route, while the `url` lets
  // us build relative links.
  let { path, url } = useRouteMatch();

  return (
    <div>
      <h2>{url}</h2>
      <ul>
        <li>
          <Link to={`${url}/rendering`}>Rendering with React</Link>
        </li>
        <li>
          <Link to={`${url}/components`}>Components</Link>
        </li>
        <li>
          <Link to={`${url}/props-v-state`}>Props v. State</Link>
        </li>
      </ul>

      <Switch>
        <Route exact path={path}>
          <h3>Please select a topic.</h3>
        </Route>
        <Route path={`${path}/:topicId`}>
          <Topic />
        </Route>
      </Switch>
    </div>
  );
}

function Topic() {
  // The <Route> that rendered this component has a
  // path of `/topics/:topicId`. The `:topicId` portion
  // of the URL indicates a placeholder that we can
  // get from `useParams()`.
  let { topicId } = useParams();

  return (
    <div>
      <h3>{topicId}</h3>
    </div>
  );
}

5.4. 示例:路由重定向(鉴权)

效果图:

实现策略:

  • 主要是利用 <Redirect> 组件,判定到权限不满足时,就重定向。

关键代码:

import React, { useContext, createContext, useState } from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  Redirect,
  useHistory,
  useLocation,
} from "react-router-dom";

// This example has 3 pages: a public page, a protected
// page, and a login screen. In order to see the protected
// page, you must first login. Pretty standard stuff.
//
// First, visit the public page. Then, visit the protected
// page. You're not yet logged in, so you are redirected
// to the login page. After you login, you are redirected
// back to the protected page.
//
// Notice the URL change each time. If you click the back
// button at this point, would you expect to go back to the
// login page? No! You're already logged in. Try it out,
// and you'll see you go back to the page you visited
// just *before* logging in, the public page.

export default function AuthExample() {
  return (
    <ProvideAuth>
      <Router>
        <div>
          <AuthButton />

          <ul>
            <li>
              <Link to="/public">Public Page</Link>
            </li>
            <li>
              <Link to="/protected">Protected Page</Link>
            </li>
          </ul>

          <Switch>
            <Route exact path="/">
              <Redirect to={"/public"} />
            </Route>
            <Route path="/public">
              <PublicPage />
            </Route>
            <Route path="/login">
              <LoginPage />
            </Route>
            <PrivateRoute path="/protected">
              <ProtectedPage />
            </PrivateRoute>
          </Switch>
        </div>
      </Router>
    </ProvideAuth>
  );
}

/**
 * 权限管理逻辑
 * 1. fakeAuth:登录、登出模拟
 * 2. ProvideAuth:以 Provider 模式,向下传递认证信息
 * 3. useAuth:Hooks 方式,获取当前认证信息
 */
const fakeAuth = {
  isAuthenticated: false,
  signin(cb) {
    fakeAuth.isAuthenticated = true;
    setTimeout(cb, 100); // fake async
  },
  signout(cb) {
    fakeAuth.isAuthenticated = false;
    setTimeout(cb, 100);
  },
};

const authContext = createContext<{
  user: string;
  signin: (cb: Function) => void;
  signout: (cb: Function) => void;
}>(null);

function ProvideAuth({ children }) {
  const [user, setUser] = useState(null);

  const signin = (cb) => {
    return fakeAuth.signin(() => {
      setUser("user");
      cb();
    });
  };

  const signout = (cb) => {
    return fakeAuth.signout(() => {
      setUser(null);
      cb();
    });
  };

  const auth = {
    user,
    signin,
    signout,
  };
  return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}

function useAuth() {
  return useContext(authContext);
}

/**
 * 权限组件
 * 1. 权限按钮(负责显示登陆状态、退出登录状态)
 * 2. 私有路由(无权限时,重定向到登陆页)
 * 3. 登陆页
 */

function AuthButton() {
  let history = useHistory();
  let auth = useAuth();

  return auth.user ? (
    <p>
      Welcome!{" "}
      <button
        onClick={() => {
          auth.signout(() => history.push("/"));
        }}
      >
        Sign out
      </button>
    </p>
  ) : (
    <p>You are not logged in.</p>
  );
}

function PrivateRoute({ children, ...rest }) {
  let auth = useAuth();
  return (
    <Route
      {...rest}
      render={({ location }) =>
        auth.user ? (
          children
        ) : (
          <Redirect
            to={{
              pathname: "/login",
              state: { from: location },
            }}
          />
        )
      }
    />
  );
}

function LoginPage() {
  let history = useHistory();
  let location = useLocation();
  let auth = useAuth();

  let { from } = location.state || { from: { pathname: "/" } };
  let login = () => {
    auth.signin(() => {
      history.replace(from);
    });
  };

  return (
    <div>
      <p>You must log in to view the page at {from.pathname}</p>
      <button onClick={login}>Log in</button>
    </div>
  );
}

/**
 * 业务逻辑页面
 */
function PublicPage() {
  return <h3>Public</h3>;
}

function ProtectedPage() {
  return <h3>Protected</h3>;
}

参考:

Angular、React、Vue 路由解决方案: https://angular.io/guide/router https://reacttraining.com/react-router/ https://github.com/ReactTraining/react-router/ https://github.com/ReactTraining/history https://router.vuejs.org/zh/ 路由原理: https://developer.mozilla.org/en-US/docs/Web/API/Window/hashchange_event http://blog.httpwatch.com/2011/03/01/6-things-you-should-know-about-fragment-urls/ https://developer.mozilla.org/zh-CN/docs/Web/API/History_API 路由路径匹配: https://github.com/pillarjs/path-to-regexp 路由集成方案: https://umijs.org/zh-CN/docs/routing https://pro.ant.design/docs/router-and-nav-cn https://nextjs.org/docs/routing/introduction https://panjiachen.github.io/vue-element-admin-site/zh/ https://lison16.github.io/iview-admin-doc/#/%E8%B7%AF%E7%94%B1%E9%85%8D%E7%BD%AE

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-03-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 WebJ2EE 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档