背景
自从接触处前端路由以来,一直想对前端路由深入理解,由于只会在项目中使用,无法深入其原理,遇到一些疑难问题容易只知其一不知其二,所以本文主要从零到一实现一个简易react-router-dom(纯底层实现)
路由原理
后端路由
路由这个概念最先是后端出现的。
响应的过程:
- 浏览器发出请求
- 服务器监听到80端口(或443)有请求过来,并解析url路径
- 根据服务器的路由配置,返回相应信息(可以是 html 字串,也可以是 json 数据,图片等)
- 浏览器根据数据包的
Content-Type
来决定如何解析数据
简单来说路由就是用来跟后端服务器进行交互的一种方式
前端路由
对 url 进行改变和监听,来让某个 dom 节点显示对应的视图。
所以并没有那么神秘。
具体功能
- 路由模式:browser,hash
- 组件:BrowserRouter,HashRouter,Route,Switch,Redirect,Link
- 当前路由信息:路径pathname,参数query
- 路由跳转:push,replace,go,goBack
具体实现
知识背景
BrowserRouter
具体思路:
- 这个就是React组件,使用Context的包装组件,负责往消费组件传递路由信息和路由跳转方法
- 监听路由变化,实时更新传递路由信息
import React from "react";
import { Provider} from "./context";
import History from "./history";
const url = require("url");
class BrowserRouter extends React.Component {
constructor() {
super();
this.state = {
pathname: window.location.pathname || "/",
count: 0
};
}
componentDidMount() {
window.addEventListener("pushState", () => {
console.log("pushState");
this.setPathname();
});
}
setPathname = () => {
this.setState(
{
pathname: window.location.pathname || "/",
count: ++this.state.count,
},
(v) => {
console.log(this.state);
}
);
};
render() {
let value = {
type: "BrowserRouter",
history: History,
location: Object.assign(
{
pathname: "/",
},
url.parse(this.state.pathname, true)
),
count: this.state.count,
cb:this.setPathname,
};
console.log(this.props.children);
return (
<Provider value={value}>
{this.props.children}
</Provider>
);
}
}
export default BrowserRouter;
Context
- Provider:包装组件
- Consumer:消费组件
import React from 'react';
let { Provider, Consumer } = React.createContext();
export { Provider, Consumer };
Route
React的一个消费组件,主要匹配正确的组件,渲染对应的组件
import React from "react";
import { Consumer } from "./context";
class Route extends React.Component {
render() {
return (
<div>
<Consumer>
{(state) => {
let { path, component: View } = this.props;
if (path === state.location.pathname) {
return <View {...state}></View>;
}
return null;
}}
</Consumer>
</div>
);
}
}
export default Route;
- state包装组件传递给消费组件的value
- props父组件传来的数据
Link
实现思路
- 返回一个阻止默认事件的a标签
- 然后通过实践跳转的方式
import React from 'react';
import { Consumer } from "./context";
class Link extends React.Component {
render() {
return (
<Consumer>
{(state) => {
let to = this.props.to || "/";
to = to.indexOf("/") === 0 ? to : "/" + to;
return (
<a
href={to}
onClick={(e) => {
if (e && e.preventDefault) {
e.preventDefault();
} else {
window.event.returnValue = false;
}
state.history.push(to);
}}
>
{this.props.children}
</a>
);
}}
</Consumer>
);
}
}
export default Link;
history
由于路由跳转有hash和browser两种方式,每种模式有push,replace,go,goBack 4种方式
hash
通过 location.hash = ‘foo’ 这样的语法来改变,路径就会由 baidu.com 变更为 baidu.com/#foo
通过 window.addEventListener(‘hashchange’) 这个事件,就可以监听到 hash 值的变化。
history
通过 history.pushState({}, ‘’, foo),可以让 baidu.com 变化为 baidu.com/foo。
history 路由的监听也有点坑,浏览器提供了 window.addEventListener(‘popstate’) 事件,但是它只能监听到浏览器回退和前进所产生的路由变化,对于主动的 pushState 却监听不到。需要自己封装一个 listen API
const url = require("url");
class History {
static push(path) {
if (typeof path === "string") {
window.history.pushState(null, "", path);
return;
}
if (typeof path === "object") {
let obj = {
pathname: path.path || "/",
query: path.query || {},
};
const formatUrl = url.format(obj);
window.history.pushState(null, "", formatUrl);
}
}
}
export default History;
listen
利用函数劫持的方式,来重写window.history.pushState, window.history.replaceState方法
(function () {
if (typeof window === undefined) {
return;
}
var _wr = function (type) {
var origin = window.history[type];
return function () {
var rv = origin.apply(this, arguments);
var e = new Event(type);
window.dispatchEvent(e);
return rv;
};
};
window.history.pushState = _wr("pushState");
window.history.replaceState = _wr("replaceState");
})();
验证demo
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MyRouter</title>
<script
crossorigin
src="https://unpkg.com/react@16/umd/react.production.min.js"
></script>
<script
crossorigin
src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"
></script>
<script src="https://cdn.bootcss.com/babel-standalone/6.22.1/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script src="./dist/main.js"></script>
</body>
<script type="text/babel">
let { BrowserRouter,Route,Link } = MyRouter;
class Home extends React.Component {
render() {
return (
<div>
<Link to="/demo">跳转demo</Link>
<h1>Home</h1>
</div>
)
}
}
class Demo extends React.Component {
render() {
return (
<div>
<Link to="/">跳转主页</Link>
<h1>Demo</h1>
</div>
)
}
}
class App extends React.Component {
render() {
return (
<div>
<BrowserRouter>
<Route path="/" component={Home}></Route>
<Route path="/demo" component={Demo}></Route>
</BrowserRouter>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
</script>
</html>
完整代码地址:html-app
后续内容,我们在手写一个react-router-dom简易版(二)继续实现