参考资料:
基础概念:简单来说路由就是用来跟后端服务器进行交互的一种方式,通过不同的路径,来请求不同的资源,请求不同的页面是路由的其中一种功能。
访问过程为:
-
- 浏览器发出请求
-
- 服务器监听到80端口(或443)有请求过来,并解析url路径
-
- 根据服务器路由配置,返回相应信息(可以是html信息,也可以是json数据,图片等)
-
- 浏览器根据数据包的
Content-Type
来决定如何解析数据
- 浏览器根据数据包的
一、router 原理
web前端,路由位URL与UI的映射关系,改变URL不引起页面刷新。
1、hash路由
- 特点
- url 中带有一个#符号,但是#只是浏览器端/客户端的状态,不会传递给服务端
- hash 值的更改,不会导致页面的刷新
- hash 值的更改,会在浏览器的访问历史中添加一条记录。所以我们才可以通过浏览器的返回、前进按钮来控制 hash 的切换
- 修改:location.hash=’#aaa’
- 监测:
hashchange
事件
2、history路由
- 特点
- url无#,美观,服务器可接收到路径和参数变化,需服务器适配
- 基于浏览器的history对象实现,主要为
history.pushState
和history.replaceState
来进行路由控制。通过这两个方法,可以实现改变 url 且不向服务器发送请求
- 修改
- 点击后退/前进触发
popstate
事件,监听进行页面更新 - 调用
history.pushState
或history.replaceState
触发相应的函数后,在后面手动添加回调更新页面
- 点击后退/前进触发
- 监测:无监测事件
3、非浏览器模式
在非浏览器环境,使用抽象路由实现导航的记录功能
- react-router的 memoryHistory
- vue-router 的 abstract 封装
二、vue-router
可参考 vue-router
1、实现原理
1、hash模式
/**
* 添加 url hash 变化的监听器
*/
setupListeners () {
const router = this.router
/**
* 每当 hash 变化时就解析路径
* 匹配路由
*/
window.addEventListener('hashchange', () => {
const current = this.current
/**
* transitionTo:
* 匹配路由
* 并通过路由配置,把新的⻚⾯ render 到 ui-view 的节点
*/
this.transitionTo(getHash(), route => {
replaceHash(route.fullPath)
})
})
}
复制代码
2、history模式
export class HTML5History extends History {
constructor (router, base) {
super(router, base)
/**
* 原理还是跟 hash 实现⼀样
* 通过监听 popstate 事件
* 匹配路由,然后更新⻚⾯ DOM
*/
window.addEventListener('popstate', e => {
const current = this.current
// Avoiding first `popstate` event dispatched in some browsers but first
// history route not updated since async guard at the same time.
const location = getLocation(this.base)
if (this.current === START && location === initLocation) {
return
}
this.transitionTo(location, route => {
if (supportsScroll) {
handleScroll(router, route, current, true)
}
})
})
}
go (n) {
window.history.go(n)
}
push (location, onComplete, onAbort) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
// 使⽤ pushState 更新 url,不会导致浏览器发送请求,从⽽不会刷新⻚⾯
pushState(cleanPath(this.base + route.fullPath))
onComplete && onComplete(route)
}, onAbort)
}
replace (location, onComplete, onAbort) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
// replaceState 跟 pushState 的区别在于,不会记录到历史栈
replaceState(cleanPath(this.base + route.fullPath))
onComplete && onComplete(route)
}, onAbort)
}
}
复制代码
2、源码分析
1、use
- 用法:
Vue.use(plugin),参数{Object | Function} plugin
- 作用:安装Vue.js插件,如果插件是一个对象,必须提供install方法。如果插件是一个函数,被作为install方法。调用install方法时,会将Vue作为参数传入。install方法被同一个插件多次调用时,插件也只会被安装一次。
- 本质:调用插件install方法,将插件推入数组保存
Vue.use = function (plugin) {
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
if(installedPlugins.indexOf(plugin) > -1){
return this
}
const args = toArray(arguments, 1)
args.unshift(this)
if(typeof plugin.install === 'function'){
plugin.install.apply(plugin, args);
} else if (typeof plugin === 'function'){
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
复制代码
2、router
- install
//vue-router中install.js
//1、原型挂载$router、$route对象
//2、全局注册<router-view>、<router-link>组件
import View from './components/view'
import Link from './components/link'
export let _Vue
export function install (Vue) {
if (install.installed && _Vue === Vue) return
install.installed = true
_Vue = Vue
const isDef = v => v !== undefined
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
//挂载$router对象
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
//挂载$route对象
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
//全局注册组件,<router-view>、<router-link>可全局使用
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
复制代码
- 构造函数
//index.js, vue-router构造器
export default class VueRouter {
//...
//路由守卫钩子相关定义
beforeEach(){/* */}
//路由方法定义
push(){/* */},
replace(){/* */},
//构造函数
constructor (options: RouterOptions = {}) {
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this)
let mode = options.mode || 'hash'
this.fallback =
mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
//非浏览器,默认abstract模式
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
//不同模式,封装了不同的对象
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
}
}
复制代码
//HTML5History对象
//html5history:封装了go、push等方法,transitionTo代码在History里
export class HTML5History extends History {
go (n: number) {
window.history.go(n)
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
//内部调用history.pushState({ key: setStateKey(genStateKey()) }, '', url)
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
//内部调用history.replaceState(stateCopy, '', url)
replaceState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
}
复制代码
3、使用
import Vue from 'vue'
import VueRouter from 'vue-router'
import App from "./App.vue"
import Home from "./Home.vue"
//全局挂载router相关实例
Vue.use(VueRouter)
const router = new VueRouter({
mode: 'history',
routes: [
{path: '/app', component: App},
{
path: '/home',
component: Home,
children: [
{
path: 'home-sub',
//异步加载,独立拆包
component: () => import(/* webpackChunkName: "home-sub" */ "@/HomeSub.vue"),
}
]
},
]
})
new Vue({
router
}).$mount("#app");
复制代码
//正则表达式匹配参数
const reg = /([^#&?]+)=([^#&?]+)/g;
const url = "wangjxk.top/#weriw?we=12&jjj=eee"
const obj = {}
url.replace(reg, (_, c1, c2) => {
obj[c1] = c2;
})
console.log(obj); //{we: '12', jjj: 'eee'}
复制代码
三、react-router
React Router已被拆分成三个包:react-router,react-router-dom和react-router-native
- react-router包提供核心的路由组件与函数,不建议直接安装使用
- react-router-dom和react-router-native提供运行环境(浏览器与react-native)所需的特定组件,但是他们都暴露出react-router中暴露的对象与方法(底层安装了react-router库)
react-router-dom和react-router和history库三者什么关系?
history
可以理解为react-router
的核心,也是整个路由原理的核心,里面集成了popState,history.pushState
等底层路由实现的原理方法,接下来我们会一一解释。react-router
可以理解为是react-router-dom
的核心,里面封装了Router,Route,Switch
等核心组件,实现了从路由的改变到组件的更新的核心功能,在我们的项目中只要一次性引入react-router-dom
就可以了。react-router-dom
,在react-router
的核心基础上,添加了用于跳转的Link
组件,和histoy模式下的BrowserRouter
和hash模式下的HashRouter
组件等。所谓BrowserRouter和HashRouter,也只不过用了history库中createBrowserHistory和createHashHistory方法
。
1、使用和原理
不像 Vue 那样,将 router 的一些实例方法挂载到全局,组件可以通过 this.route 访问到,react-router 为组件注入的方法,使用函数式编程。
1、react-router使用
import React from 'react';
import ReactDOM from 'react-dom';
import App from "./App";
import Home from "./views/Home";
import Home1 from "./views/Home1";
import Home2 from "./views/Home2";
import About from "./views/About";
//路由配置
import {Router, Route} from 'react-router';
import {createBrowserHistory} from 'history';
const histroy = createBrowserHistory();
//访问: http://localHost:3000/home,展示Home组件
ReactDOM.render(
<Router history={history}>
<App>
<Route path="/home" component={Home}/>
<Route path="/about" component={props => {
return (
/* 子路由配置 */
<App1>
<Route path="/home1" component={Home1}/>
<Route path="/home2" component={Home2}/>
</App1>
)
}}/>
</App>
</Router>,
document.getElementById('root')
)
复制代码
2、react-router-dom使用
- Link:封装的专属a标签,用于路由跳转,类似于router-link
- to:字符串或者位置对象(包含路径名,search,hash和state的组合)
- Switch:匹配多个路由选择其一组件,从前往后,匹配成功,将route克隆返回
- Route:路由组件,类似于router-view
- path:字符串,描述路由匹配的路径名类型
- component:一个React组件。当带有component 参数的的路由匹配时,路由将返回一个新元素,其类型是一个React component (使用React.createElement创建)
- render:一个返回React元素的函数,它将在path匹配时被调用。这与component类似,应用于内联渲染和更多参数传递
- children:一个返回React元素的函数,无论路由的路径是否与当前位置相匹配,都将始终被渲染
- exact属性:指定 path 是否需要绝对匹配才会跳转
- BrowserRouter:histroy路由组件
- HashRouter:hash路由组件
- withRouter:高阶函数,使用后可使用注入的context对象上的属性
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import Home from './views/Home'
import About from './views/About'
const NoMatch = () => <>404</>
import {Route, BrowserRouter, Switch} from 'react-router-dom'
//switch和route组件可分散在任何组件中,下例子为根组件使用
const extraProps = { color: 'red' }
ReactDOM.render(
<BrowserRouter>
<App>
<Switch>
<Route path="/home" component={Home}/>
<Route path="/about" component={About}/>
<Route path='/page' render={(props) => (
<Page {...props} data={extraProps}/>
)}/>
{/* props.match指通过路径匹配获取到的参数信息,包含如下信息:
1. params – (object)从对应于路径的动态段的URL解析的键/值对
2. isExact – (boolean)true如果整个URL匹配(没有尾随字符)
3. path – (string)用于匹配的路径模式,作用于构建嵌套的
4. url – (string)URL的匹配部分,作用于构建嵌套的
*/}
<Route path='/page' children={(props) => (
props.match
? <Page {...props}/>
: <EmptyPage {...props}/>
)}/>
<Route path="*" component={NoMatch}/>
</Switch>
</App>
</BrowserRouter>,
document.getElementById('root')
)
复制代码
1、switch代码分析
代码在react-router/modules/Switch.js中
/**
* The public API for rendering the first <Route> that matches.
*/
class Switch extends React.Component {
render() {
return (
{/* 使用RouterContext.Consumer包裹,可使用注入的context */}
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Switch> outside a <Router>");
const location = this.props.location || context.location;
let element, match;
// React.Children.forEach为react遍历this.props.children的方法
// switch子元素的遍历,即Route组件遍历
React.Children.forEach(this.props.children, child => {
if (match == null && React.isValidElement(child)) {
element = child;
const path = child.props.path || child.props.from;
match = path
? matchPath(location.pathname, { ...child.props, path })
: context.match;
}
});
return match //匹配到则返回,保证唯一,克隆的新元素,注入了location等对象参数
? React.cloneElement(element, { location, computedMatch: match })
: null;
}}
</RouterContext.Consumer>
);
}
}
//...
export default Switch;
复制代码
2、Route代码分析
组件通过Route组件包装后,添加了location、history等对象参数props
调试技巧:在chrome中,source栏目中使用ctrl+p查找文件Home.js,在函数中添加断点,可查看props添加如下对象:history、location、match、staticContext,可直接使用。
export default function Home({location, history}){
return (
<div onClick={() => history.push({
pathname: '/player',
state: {age: 18}
})}>
Home
</div>
)
}
复制代码
原理解析:通过Routers组件通过context注入使用对象,Route组件中通过props注入创建组件
//react-router/modules/RouterContext.js
//react-router/modules/createNameContext.js
import createContext from "mini-create-react-context";
const createNamedContext = name => {
const context = createContext();
context.displayName = name;
return context;
};
export default createNamedContext;
//react-router/modules/Routers.js
import HistoryContext from "./HistoryContext.js";
import RouterContext from "./RouterContext.js";
class Router extends React.Component {
//通过Provider注入history、location、match、staticContext对象
render() {
return (
<RouterContext.Provider
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}
>
<HistoryContext.Provider
children={this.props.children || null}
value={this.props.history}
/>
</RouterContext.Provider>
);
}
}
//react-router/modules/Route.js
//The public API for matching a single path and rendering.
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Route> outside a <Router>");
const location = this.props.location || context.location;
const match = this.props.computedMatch
? this.props.computedMatch // <Switch> already computed the match for us
: this.props.path
? matchPath(location.pathname, this.props)
: context.match;
const props = { ...context, location, match };
let { children, component, render } = this.props;
// Preact uses an empty array as children by
// default, so use null if that's the case.
if (Array.isArray(children) && isEmptyChildren(children)) {
children = null;
}
//解析顺序:children、component、render
return (
<RouterContext.Provider value={props}>
{props.match
? children
? typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props) //将props以入参形式注入
: children
: component
? React.createElement(component, props)
: render
? render(props)
: null
: typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}
export default Route;
复制代码
3、withRouter原理及使用
想让业务组件即非Route组件包裹的组件使用context对象上的内容,可使用withRouter高阶函数包裹
//使用通用语法实现
import {__RouterContext} from 'react-router'
function App(){
return (
<__RouterContext.Consumer>
{
/* 此时props已包含context传递的属性 */
props => {
return <div className = "App">App</div>
}
}
</__RouterContext.Consumer>
)
}
//通常使用高阶组件实现
import {__RouterContext} from 'react-router'
function App({children}){
return (
<div className = "App">App</div>
)
}
function Ctx(Component){
return props => <__RouterContext.Consumer>
{
/* context为router注入的对象 */
context => <Component {...context} {...props}/>
}
</__RouterContext.Consumer>
}
export default ctx(App);
//使用默认withRouter方法,使用原context方法
import {withRouter} from 'react-router'
function App(){
return (
<div className = "App">
App
</div>
)
}
export default withRouter(App);
复制代码
2、路由守卫
react-router-dom使用Prompt和getUserConfirmation函数实现路由守卫
//Home组件
import React from 'react'
import {Prompt} from 'react-router'
export default function Home({location, history}){
const [message, setMessage] = useState("")
return (
<>
<div onClick={()=> history.push({
pathname: '/about',
state: {state: 12}
})}>
Home
{/* 确定继续跳转路由,取消不跳转 */}
<Prompt message={'是否跳转'} when={!!message}/>
</div>
<span onClick={()=> setMessage(Math.random())}>修改了{message}</span>
</>
)
}
//App.js组件
//使用子定义函数处理实现路由守卫,定义后,不会弹alert框
//getUserConfirmation函数使用在history.js中
const getUserConfirmation = (msg, callback) => {
console.log(msg) //Home中Prompt的message值
callback(true); //跳转路由
}
ReactDOM.render(
<BrowserRouter getUserConfirmation={getUserConfirmation}>
<App>
<Switch>
<Route path="/home" component={Home}/>
<Route path="/about" component={About}/>
<Route path="*" component={NoMatch}/>
</Switch>
</App>
</BrowserRouter>,
document.getElementById('root')
)
复制代码
3、手动实现
1、BrowserRouter
// BrowserRouter.js
import React, { Component } from "react";
import Context from './context';
export default class BrowserRouter extends Component {
constructor(props) {
super(props);
this.state = {
location: {
pathname: window.location.pathname || "/",
search: undefined,
},
match: {
}
}
}
componentWillMount() {
window.addEventListener("popstate", () => {
this.setState({
location: {
pathname: window.location.pathname
}
})
})
}
render() {
const currentRoute = {
location: this.state.location,
match: this.state.match,
history: {
push: (to) => {
// 根据当前to 去匹配不同的路由 实现路由切换
if (typeof to === 'object') {
let { pathname, query } = to;
// 只是改变当前state的数据, 不触发reRender
this.setState({
location: {
query: to.query,
pathname: to.pathname
}
});
window.history.pushState({}, {}, pathname)
} else {
// 如果是字符串
this.setState({
location: {
pathname: to
}
})
window.history.pushState({}, {}, to)
}
}
}
}
return (
<Context.Provider value={currentRoute}>{this.props.children}</Context.Provider>
)
}
}
// context.js
import React from 'react';
export default React.createContext();
复制代码
2、Route
import React, { Component } from "react";
import { pathToRegexp, match } from "path-to-regexp";
import context from "./context";
export default class Route extends Component {
static contextType = context;
render() {
const currenRoutePath = this.context.location.pathname; // 从上下文context中获取到当前路由
const { path, component: Component, exact = false } = this.props; // 获取Route组件props的路由
const paramsRegexp = match(path, { end: exact }); // 生成获取params的表达式
const matchResult = paramsRegexp(currenRoutePath);
console.log("路由匹配结果", matchResult);
this.context.match.params = matchResult.params;
const props = {
...this.context
}
const pathRegexp = pathToRegexp(path, [], { end: exact }); // 生成路径匹配表达式
if (pathRegexp.test(currenRoutePath)) {
return (<Component {...props}></Component>) // 将蛋清概念上下文路由信息当作props传递给组件
}
return null;
}
}
复制代码
// 调用
import Page1 from './pages/page1'
import Page2 from './pages/page2'
import Page3 from './pages/page3'
import React, { Component } from 'react'
import { Route, BrowserRouter } from './react-router-dom';
console.log(Route, BrowserRouter)
class Router extends Component {
constructor(props) {
super(props)
this.state = {}
}
render() {
return (
<BrowserRouter>
<Route exact path="/" component={(Page1)} />
<Route exact path="/page2" component={(Page2)} />
<Route exact path="/page3" component={(Page3)} />
</BrowserRouter>
)
}
}
export default Router
复制代码
4、v6版本
v6相关组件和属性:BrowserRouter, HashRouter, Link, MemoryRouter, NavLink, Navigate, Outlet, Route, Router, Routes, UNSAFE_LocationContext, UNSAFE_NavigationContext, UNSAFE_RouteContext, createRoutesFromChildren, createSearchParams, generatePath, matchPath, matchRoutes, renderMatches, resolvePath, unstable_HistoryRouter, useHref, useInRouterContext, useLinkClickHandler, useLocation, useMatch, useNavigate, useNavigationType, useOutlet, useOutletContext, useParams, useResolvedPath, useRoutes, useSearchParams
- 废弃
Switch
组件,由Routes
代替(使用了智能匹配路径算法) - 废弃
Redirect
组件,由Navigate
代替 - 废弃
useHistory
方法,由useNavigate
代替 Route
组件移除原有component
及render
属性,统一通过element
属性传递:<Route element={<Home />}>
Route
组件支持嵌套写法(v3 版本用法回归)Route
组件的path
规则变更,不再支持类正则写法- 消除了
v5
版本中带后斜杠的路径时,Link
组件的跳转模糊的问题 Link
组件支持自动携带当前父路由,以及相对路径写法../home
- 新增
useRoutes
方法,代替之前的react-router-config
写法,同样支持嵌套 - 其他一些 API 名称变更