共计 9752 个字符,预计需要花费 25 分钟才能阅读完成。
写作不易,未经作者容许禁止以任何模式转载!<br/> 如果感觉文章不错,欢送关注、点赞和分享!
博客原文链接:浅析前端路由 Router
前端路由介绍
什么前端路由
路由这个概念最早呈现在后端,通过⽤户申请的 url 导航到具体的 html ⻚⾯。当初的前端路由不同
于传统路由,它不须要服务器解析,⽽是能够通过 hash 函数或者 history API 来实现。在前端开发中,我
们能够使⽤路由设置拜访门路,并依据门路与组件的映射关系切换组件的显示,⽽这整个过程都是在同
⼀个⻚⾯中实现的,不波及⻚⾯间的跳转,这也就是咱们常说的单⻚应⽤(spa)。
前端路由带来了什么
相⽐多⻚应⽤ (mpa) 来说,spa 有以下长处:
- 不波及 html ⻚⾯跳转,内容扭转不须要从新加载⻚⾯,对服务器压⼒⼩。
- 只波及组件之间的切换,因而跳转晦涩,⽤户体验好。
- ⻚⾯成果会⽐较炫酷(⽐如切换⻚⾯内容时的转场动画)。
- 组件化开发便捷。
然而同时 spa 也有以下毛病:
- ⾸屏加载过慢。
- 不利于 seo。
- ⻚⾯复杂度提⾼很多。
⽤原⽣ js 实现前端路由
什么前端路由
路由这个概念最早呈现在后端,通过⽤户申请的 url 导航到具体的 html ⻚⾯。当初的前端路由不同于
传统路由,它不须要服务器解析,⽽是能够通过 hash 函数或者 h5 history API 来实现。在前端开发
中,咱们能够使⽤路由设置拜访门路,并依据门路与组件的映射关系切换组件的显示,⽽这整个过程都
是在同⼀个⻚⾯中实现的,不波及⻚⾯间的跳转,这也就是咱们常说的单⻚应⽤(spa)。
原⽣ js 实现前端路由
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<meta http-equiv="X-UA-Compatible" content="ie=edge" /> | |
<title>lesson2</title> | |
</head> | |
<body> | |
<ul> | |
<li><a href="#/home"> 首页 </a></li> | |
<li><a href="#/user"> 用户核心 </a></li> | |
<li><a href="#/login"> 登录 </a></li> | |
</ul> | |
<div id="view"></div> | |
</body> | |
<script> | |
let view = null; | |
window.addEventListener("DOMContentLoaded", onLoad); | |
// 监听 hash 变动 | |
window.addEventListener("hashchange", onHashChange); | |
function onLoad() {view = document.getElementById("view"); | |
onHashChange();} | |
function onHashChange() {switch (location.hash) { | |
case "#/home": | |
view.innerHTML = "首页"; | |
break; | |
case "#/user": | |
view.innerHTML = "用户核心"; | |
break; | |
case "#/login": | |
view.innerHTML = "登录"; | |
break; | |
} | |
} | |
</script> | |
</html> |
环境配置与 react-router 简介
资源
- React 官网
- react-router
指标
- 把握 cra 环境
- 把握 react-router 的根本应用
知识点
疾速开始
npx create-react-app router-nut | |
cd router-nut | |
yarn start |
配置 less 与装璜器
yarn add @craco/craco craco-less @babel/plugin-proposal-decorators
根目录下增加 craco.config.js 文件
// * 配置实现后记得重启下 | |
const CracoLessPlugin = require("craco-less"); | |
module.exports = { | |
babel: { | |
// 用来反对装璜器 | |
plugins: [["@babel/plugin-proposal-decorators", {legacy: true}]] | |
}, | |
plugins: [ | |
{plugin: CracoLessPlugin} | |
] | |
}; |
批改 package.json
"scripts": { | |
"start": "craco start", | |
"build": "craco build", | |
"test": "craco test" | |
}, |
react-router 简介
react-router 蕴含 3 个库,react-router、react-router-dom 和 react-router-native。react-router 提供最根本的路由性能,理论应用的时候咱们不会间接装置 react-router,而是依据利用运行的环境抉择装置 react-router-dom(在浏览器中应用)或 react-router-native(在 rn 中应用)。react-router-dom 和 react-router-native 都依赖 react-router,所以在装置时,react-router 也会主动装置,创立 web 利用。
装置
yarn add react-router-dom
BrowserRouter 与 HashRouter 比照
- HashRouter 最简略,不须要服务器端渲染,靠浏览器的 #的来辨别 path 就能够,BrowserRouter 须要服务器端对不同的 URL 返回不同的 HTML,后端配置可参考。
- BrowserRouter 应用 HTML5 history API(pushState,replaceState 和 popstate 事件),让页面的 UI 同步与 URL。
- HashRouter 不反对 location.key 和 location.state,动静路由跳转须要通过? 传递参数。
- Hash history 不须要服务器任何配置就能够运行,如果你刚刚入门,那就应用它吧。然而咱们不举荐在理论线上环境中用到它,因为每一个 web 利用都应该渴望应用
browserHistory
。
MemoryRouter
把 URL 的历史记录保留在内存中的 <Router>
(不读取、不写入地址栏)。在测试和非浏览器环境中很有用,如 React Native。
根本应用
react-router 中奉行所有皆组件的思维,路由器 -Router、链接 -Link、路由 -Route、独占 -Switch、重定向 -Redirect都以组件模式存在
import {BrowserRouter as Router, Route, Link} from "react-router-dom"; | |
import HomePage from "./pages/HomePage"; | |
import UserPage from "./pages/UserPage"; | |
import LoginPage from "./pages/LoginPage"; | |
function App() { | |
return ( | |
<div className="App"> | |
<Router> | |
<Link to="/"> 首页 </Link> | |
<Link to="/user"> 用户核心 </Link> | |
<Link to="/login"> 登录 </Link> | |
{/* 根路由要增加 exact,实现准确匹配 */} | |
<Route exact path="/" component={HomePage} /> | |
<Route path="/user" component={UserPage} /> | |
<Route path="/login" component={LoginPage} /> | |
</Router> | |
</div> | |
); | |
} | |
export default App; |
Route 渲染内容的三种形式
资源
- React 官网
- react-router
指标
- 把握 Route 渲染内容的三种形式
- 把握 404 路由
知识点
Route 渲染优先级:children>component>render。
三者能接管到同样的[route props],包含 match, location and history,然而当不匹配的时候,children 的 match 为 null。
这三种形式互斥,你只能用一种。
import React, {useState} from "react"; | |
import {BrowserRouter as Router, Route, Link, Switch} from "react-router-dom"; | |
import HomePage from "./pages/HomePage"; | |
import UserPage from "./pages/UserPage"; | |
import LoginPage from "./pages/LoginPage"; | |
import _404Page from "./pages/_404Page"; | |
function App() {const [count, setCount] = useState(0); | |
return ( | |
<div className="App"> | |
<button | |
onClick={() => {setCount(count + 1); | |
}}> | |
add: {count} | |
</button> | |
<Router> | |
<Link to="/"> 首页 </Link> | |
<Link to="/user"> 用户核心 </Link> | |
<Link to="/login"> 登录 </Link> | |
{/* 独占路由 */} | |
<Switch> | |
<Route | |
path="/" | |
exact | |
//children={children} | |
component={HomePage} | |
// ! 渲染 component 的时候会调用 React.createElement,如果应用上面这种匿名函数的模式,每次都会生成一个新的匿名的函数,// ! 导致生成的组件的 type 总是不雷同,这个时候会产生反复的卸载和挂载 | |
//component={() => <HomePage />} | |
// render={render} | |
/> | |
<Route path="/user" component={UserPage} /> | |
<Route path="/login" component={LoginPage} /> | |
<Route component={_404Page} /> | |
</Switch> | |
</Router> | |
</div> | |
); | |
} | |
export default App; | |
function children(props) {console.log("children props", props); //sy-log | |
return <div>children</div>; | |
} | |
function render(props) {console.log("props props", props); //sy-log | |
return <div>render</div>; | |
} |
children:func
有时候,不论 location 是否匹配,你都须要渲染一些内容,这时候你能够用 children。
除了不论 location 是否匹配都会被渲染之外,其它工作办法与 render 齐全一样。
import React, {Component} from "react"; | |
import ReactDOM from "react-dom"; | |
import {BrowserRouter as Router, Link, Route} from "react-router-dom"; | |
function ListItemLink({to, name, ...rest}) { | |
return ( | |
<Route | |
path={to} | |
children={({match}) => (<li className={match ? "active" : ""}> | |
<Link to={to} {...rest}> | |
{name} | |
</Link> | |
</li> | |
)} | |
/> | |
); | |
} | |
export default class RouteChildren extends Component {render() { | |
return ( | |
<div> | |
<h3>RouteChildren</h3> | |
<Router> | |
<ul> | |
<ListItemLink to="/somewhere" name="链接 1" /> | |
<ListItemLink to="/somewhere-else" name="链接 2" /> | |
</ul> | |
</Router> | |
</div> | |
); | |
} | |
} |
render:func
然而当你用 render 的时候,你调用的只是个函数。然而它和 component 一样,能拜访到所有的[route props]。
import React from "react"; | |
import ReactDOM from "react-dom"; | |
import {BrowserRouter as Router, Route} from "react-router-dom"; | |
// 不便的内联渲染 | |
ReactDOM.render( | |
<Router> | |
<Route path="/home" render={() => <div>Home</div>} /> | |
</Router>, | |
node | |
); | |
// wrapping/composing | |
// 把 route 参数传递给你的组件 | |
function FadingRoute({component: Component, ...rest}) { | |
return (<Route {...rest} render={routeProps => <Component {...routeProps} />} /> | |
); | |
} | |
ReactDOM.render( | |
<Router> | |
<FadingRoute path="/cool" component={Something} /> | |
</Router>, | |
node | |
); |
component: component
只在当 location 匹配的时候渲染。
留神
当你用 component
的时候,Route 会用你指定的组件和 React.createElement 创立一个新的[React element]。这意味着当你提供的是一个内联函数的时候,每次 render 都会创立一个新的组件。这会导致不再更新曾经现有组件,而是间接卸载而后再去挂载一个新的组件。因而,当用到内联函数的内联渲染时,请应用 render 或者 children。
Route 外围渲染代码如下:
404 页面
设定一个没有 path 的路由在路由列表最初面,示意肯定匹配
<Switch> | |
<Route path="/" exact component={HomePage} /> | |
<Route path="/user" component={UserPage} /> | |
<Route path="/login" component={LoginPage} /> | |
<Route component={_404Page} /> | |
</Switch> |
动静路由
资源
- React 官网
- react-router
指标
- 动静路由
知识点
动静路由
应用:id 的模式定义动静路由
定义路由:
<Route path="/product/:id" component={Product} />
增加导航链接:
<Link to={"/product/123"}> 搜寻 </Link>
创立 Search 组件并获取参数:
function Product({location, match}) {console.log("match", match); //sy-log | |
const {id} = match.params; | |
return <h1>Product-{id}</h1>; | |
} |
嵌套路由
资源
- React 官网
- react-router
⽬标
- 把握嵌套路由
知识点
嵌套路由
Route 组件嵌套在其余⻚⾯组件中就产⽣了嵌套关系
批改 Product,增加新增和详情
<Route path={url + "/detail"} component={Detail} />
function Product({match}) {console.log("match", match); //sy-log | |
const {params, url} = match; | |
const {id} = params; | |
return ( | |
<div> | |
<h1>Search-{id}</h1> | |
<Link to={url + "/detail"}> 详情 </Link> | |
<Route path={url + "/detail"} component={Detail} /> | |
</div> ); | |
} |
⼿写实现BrowserRouter、Route、Link
资源
- React 官网
- react-router
⽬标
- react-router 初步实现
知识点
跨层级传输数据 Context
import React from "react"; | |
// 应用 Context 做数据跨层级传递 | |
// step1: 创立 context 对象 | |
export const RouterContext = React.createContext(); | |
// step2: 应用 context 对象的 Provider 传递 value | |
// step3: 子组件生产 value:Consumer、useContext、contextType |
实现Router
import React, {Component} from "react"; | |
import {RouterContext} from "./Context"; | |
export default class Router extends Component {static computeRootMatch(pathname) {return {path: "/", url: "/", params: {}, isExact: pathname === "/"}; | |
} | |
constructor(props) {super(props); | |
this.state = {location: props.history.location}; | |
this.unlisten = props.history.listen(location => { | |
this.setState({location}); | |
}); | |
} | |
componentWillUnmount() {if (this.unlisten) {this.unlisten(); | |
} | |
} | |
render() { | |
return ( | |
<RouterContext.Provider | |
value={{ | |
history: this.props.history, | |
location: this.state.location, | |
match: Router.computeRootMatch(this.state.location.pathname) | |
}}> | |
{this.props.children} | |
</RouterContext.Provider> | |
); | |
} | |
} |
实现BrowserRouter
BrowserRouter:历史记录治理对象 history 初始化及向下传递,location 变更监听
import React, {Component} from "react"; | |
import {createBrowserHistory} from "history"; | |
import Router from "./Router"; | |
export default class BrowserRouter extends Component {constructor(props) {super(props); | |
this.history = createBrowserHistory();} | |
render() {return <Router history={this.history}>{this.props.children}</Router>; | |
} | |
} |
实现Route
路由配置,匹配检测,内容渲染
// match 依照互斥规定 优先渲染程序为 children component render null,children 如果是 function 执⾏ function,是节点间接渲染 | |
// 不 match children 或者 null(只渲染 function)export default class Route extends Component {render() { | |
return ( | |
<RouterContext.Consumer> | |
{context => { | |
// 优先从 props 中取值 | |
const location = this.props.location || context.location; | |
// 优先从 props 中取值计算 | |
const match = this.props.computedMatch | |
? this.props.computedMatch | |
: this.props.path | |
? matchPath(location.pathname, this.props) | |
: context.match; | |
const props = { | |
...context, | |
location, | |
match | |
}; | |
let {path, children, component, render} = this.props; | |
// match 渲染这三者之⼀:children component render 或者 null | |
// 不 match,渲染 children 或者 null | |
return (<RouterContext.Provider value={props}> | |
{match | |
? children | |
? typeof children === "function" | |
? children(props) | |
: children | |
: component | |
? React.createElement(component, props) | |
: render | |
? render(props) | |
: null | |
: typeof children === "function" | |
? children(props) | |
: null} | |
</RouterContext.Provider> | |
); | |
}} | |
</RouterContext.Consumer>); | |
} | |
} | |
实现Link
Link.js: 跳转链接,解决点击事件
import React from "react"; | |
import {RouterContext} from "./RouterContext"; | |
export default function Link({to, children, ...restProps}) {const context = React.useContext(RouterContext); | |
const handleClick = e => {e.preventDefault(); | |
context.history.push(to); | |
}; | |
return (<a href={to} {...restProps} onClick={handleClick}> | |
{children} | |
</a> | |
); | |
} |
实现Switch
渲染与该地址匹配的第⼀个⼦节点 <Route> 或者 <Redirect>。
import React, {Component, isValidElement} from "react"; | |
import {RouterContext} from "./Context"; | |
import matchPath from "./matchPath"; | |
export default class Switch extends Component {render() { | |
return ( | |
<RouterContext.Consumer> | |
{context => {const {location} = context; | |
let match, element; | |
// children element | array | |
React.Children.forEach(this.props.children, child => {if (match == null && React.isValidElement(child)) { | |
element = child; | |
const {path} = child.props; | |
match = path | |
? matchPath(location.pathname, child.props) | |
: context.match; | |
} | |
}); | |
return match | |
? React.cloneElement(element, {computedMatch: match}) | |
: null; | |
}} | |
</RouterContext.Consumer> | |
); | |
} | |
} |
原文链接:浅析前端路由 Router
掘金:前端 LeBron
知乎:前端 LeBron
继续分享技术博文,关注微信公众号👇🏻