Меняйте жизнь с помощью технологий и улучшайте технологии с помощью жизни. Front-end инженер из Ctrip (Lvyue), который усердно работает над полным стеком.
Синхронизация WeChat: wDxKn89
Базовое понимание React-Router
Для React-Router это определенная для React библиотека маршрутизации, которая используется для сопоставления URL-адресов и компонентов.Простое руководство по использованию React Router
Анализ исходного кода React-Router
Простая реализация внешней маршрутизации
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>router</title>
</head>
<body>
<ul>
<li><a href="#/">turn white</a></li>
<li><a href="#/blue">turn blue</a></li>
<li><a href="#/green">turn green</a></li>
</ul>
<script>
function Router() {
this.routes = {};
this.currentUrl = '';
}
<!--
//针对不同的地址进行回调的匹配
//1:用户在调用Router.route('address',function),在this.routes对象中进行记录或者说address与function的匹配
-->
Router.prototype.route = function(path, callback) {
this.routes[path] = callback || function(){};
};
<!--
//处理hash的变化,针对不同的值,进行页面的处理
//1:在init中注册过事件,在页面load的时候,进行页面的处理
//2:在hashchange变化时,进行页面的处理
-->
Router.prototype.refresh = function() {
this.currentUrl = location.hash.slice(1) || '/';
this.routes[this.currentUrl]();
};
<!--
//1:在Router的prototype中定义init
//2:在页面load/hashchange事件触发时,进行回调处理
//3:利用addEventListener来添加事件,注意第三个参数的用处
//4:bind的使用区别于apply/call的使用
-->
Router.prototype.init = function() {
window.addEventListener('load', this.refresh.bind(this), false);
window.addEventListener('hashchange', this.refresh.bind(this), false);
}
window.Router = new Router();//在window对象中构建一个Router对象
window.Router.init();//页面初始化处理
var content = document.querySelector('body');
// change Page anything
function changeBgColor(color) {
content.style.backgroundColor = color;
}
Router.route('/', function() {
changeBgColor('white');
});
Router.route('/blue', function() {
changeBgColor('blue');
});
Router.route('/green', function() {
changeBgColor('green');
});
</script>
</body>
</html>
Вышеупомянутая система маршрутизации в основном состоит из трех частей.
- Router.protopyte.init используется для регистрации события инициализации (загрузки) страницы/изменения URL-адреса страницы.
- Router.protopyte.route регистрирует путь (адрес) и функцию обратного вызова (функцию) и сохраняет их в маршрутизаторе для использования при загрузке/изменении хэша.
- Router.protopyte.refresh выполняет обработку обратного вызова для разных путей (адресов)
Простая реализация React-Router
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>包装方式</title>
</head>
<body>
<script>
var body = document.querySelector('body'),
newNode = null,
append = function(str){
newNode = document.createElement("p");
newNode.innerHTML = str;
body.appendChild(newNode);
};
// 原对象(这里可以是H5的history对象)
var historyModule = {
listener: [],
listen: function (listener) {
this.listener.push(listener);
append('historyModule listen.')
},
updateLocation: function(){
append('historyModule updateLocation tirgger.');
this.listener.forEach(function(listener){
listener('new localtion');
})
}
}
// Router 将使用 historyModule 对象,并对其包装
var Router = {
source: {},
//复制historyModule到Router中
init: function(source){
this.source = source;
},
//处理监听事件,在Router对页面进行处理时,利用historyModule中处理页面
listen: function(listener) {
append('Router listen.');
// 对 historyModule的listen进行了一层包装
return this.source.listen(function(location){
append('Router listen tirgger.');
listener(location);
})
}
}
// 将 historyModule 注入进 Router 中
Router.init(historyModule);
// Router 注册监听
Router.listen(function(location){
append(location + '-> Router setState.');
})
// historyModule 触发监听回调(对页面进行渲染等处理)
historyModule.updateLocation();
</script>
</body>
</html>
На самом деле операция обращения только для обработки апгрейда фронтенда простой маршрутизации + historyModule. Операция тоже аналогична.
- Router.init(historyModule) ==> Router.protopyte.init
- Router.listen(function()) ==> Router.protopyte.route
- Router.updateLocation ==> Router.protopyte.refresh
Анализ реализации кода React-Router
Из-за некоторых различий в обработке между версиями React-Router просто нажмитеПоследняя версияанализировать.
Реализация historyModule(история)
//Проанализируйте BrowserRouter.js в react-router-dom здесь
import warning from "warning";
import React from "react";
import PropTypes from "prop-types";
import { createBrowserHistory as createHistory } from "history";//这里的history就是上面第二个例子中的historyModule
import Router from "./Router"; //对应第二个例子中的Router对象
/**
* The public API for a <Router> that uses HTML5 history. //这里是重点
*/
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
export default BrowserRouter;
Отслеживание реализации истории Путь к файлу index.ts в истории в исходном коде
//定义一个接口
export interface History {
length: number;
action: Action;
location: Location;
push(path: Path, state?: LocationState): void;
push(location: LocationDescriptorObject): void;
replace(path: Path, state?: LocationState): void;
replace(location: LocationDescriptorObject): void;
go(n: number): void;
goBack(): void;
goForward(): void;
block(prompt?: boolean): UnregisterCallback;
listen(listener: LocationListener): UnregisterCallback;
createHref(location: LocationDescriptorObject): Href;
}
Помимо типа интерфейса, знакомы ли вы с атрибутами, определенными в истории?window.history
Регистрация функции прослушивания
React-Router/Router.js
/**
* The public API for putting history on context. //这里的道理类似于例子二中第二步
*/
class Router extends React.Component {
static childContextTypes = {
router: PropTypes.object.isRequired
};
getChildContext() {
return {
router: {
...this.context.router,
history: this.props.history,
route: {
location: this.props.history.location,
match: this.state.match
}
}
};
}
state = {
match: this.computeMatch(this.props.history.location.pathname)
};
computeMatch(pathname) {
return {
path: "/",
url: "/",
params: {},
isExact: pathname === "/"
};
}
componentWillMount() {
const { children, history } = this.props;
// Do this here so we can setState when a <Redirect> changes the
// location in componentWillMount. This happens e.g. when doing
// server rendering using a <StaticRouter>.
this.unlisten = history.listen(() => {
this.setState({
match: this.computeMatch(history.location.pathname)
});
});
}
componentWillReceiveProps(nextProps) {
warning(
this.props.history === nextProps.history,
"You cannot change <Router history>"
);
}
componentWillUnmount() {
this.unlisten();
}
render() {
const { children } = this.props;
return children ? React.Children.only(children) : null;
}
}
export default Router;
Есть несколько моментов, на которые следует обратить внимание выше
- React-Router использует ReactContextсвязь между компонентами. дочернийконтексттипес/getChildContext
- Требуется специальный основной компонентWillMount, что означает, что прослушивание было зарегистрировано до загрузки компонента Router. По сути, этот шаг аналогичен принципу инициализации в первом примере.
- Метод не зарегистрирован в componentWillUnmount для освобождения памяти.
- Здесь упоминается, что фактически используется для сопоставления URL и компонентов.
Узнайте о Redirect.js
react-router/Redirect.js
//这里省去其他库的引用
import generatePath from "./generatePath";
/**
* The public API for updating the location programmatically
* with a component.
*/
class Redirect extends React.Component {
//这里是从Context中拿到history等数据
static contextTypes = {
router: PropTypes.shape({
history: PropTypes.shape({
push: PropTypes.func.isRequired,
replace: PropTypes.func.isRequired
}).isRequired,
staticContext: PropTypes.object
}).isRequired
};
isStatic() {
return this.context.router && this.context.router.staticContext;
}
componentWillMount() {
invariant(
this.context.router,
"You should not use <Redirect> outside a <Router>"
);
if (this.isStatic()) this.perform();
}
componentDidMount() {
if (!this.isStatic()) this.perform();
}
componentDidUpdate(prevProps) {
const prevTo = createLocation(prevProps.to);
const nextTo = createLocation(this.props.to);
if (locationsAreEqual(prevTo, nextTo)) {
warning(
false,
`You tried to redirect to the same route you're currently on: ` +
`"${nextTo.pathname}${nextTo.search}"`
);
return;
}
this.perform();
}
computeTo({ computedMatch, to }) {
if (computedMatch) {
if (typeof to === "string") {
return generatePath(to, computedMatch.params);
} else {
return {
...to,
pathname: generatePath(to.pathname, computedMatch.params)
};
}
}
return to;
}
//进行路由的匹配操作
perform() {
const { history } = this.context.router;
const { push } = this.props;
//Router中拿到需要跳转的路径,然后传递给history
const to = this.computeTo(this.props);
if (push) {
history.push(to);
} else {
history.replace(to);
}
}
render() {
return null;
}
}
export default Redirect;
note :
- Для истории h5 push/replace только изменяет URL-адрес, но не вызываетсобытие popstate
Обработка функции generatePath
//该方法只是对路径进行处理
/**
* Public API for generating a URL pathname from a pattern and parameters.
*/
const generatePath = (pattern = "/", params = {}) => {
if (pattern === "/") {
return pattern;
}
const generator = compileGenerator(pattern);
return generator(params);
};
Визуализировать страницу для пути
Нужно смотреть на структуру маршрутизатора
//这里的Router只是一个容器组件,用于从Redux/react中获取数据,而真正的路径/组件信息存放在Route中
<Router>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
<Route path="/topics" component={Topics}/>
</Router>
Взгляните на обработку компонентов Route.
/**
* The public API for matching a single path and rendering.
*/
class Route extends React.Component {
//从Router中获取信息
static contextTypes = {
router: PropTypes.shape({
history: PropTypes.object.isRequired,
route: PropTypes.object.isRequired,
staticContext: PropTypes.object
})
};
//自己定义了一套Contex用于子组件的使用
static childContextTypes = {
router: PropTypes.object.isRequired
};
//自己定义了一套Contex用于子组件的使用
getChildContext() {
return {
router: {
...this.context.router,
route: {
location: this.props.location || this.context.router.route.location,
match: this.state.match
}
}
};
}
state = {
match: this.computeMatch(this.props, this.context.router)// matching a URL pathname to a path pattern.如果不匹配,返回null,也就是找不到页面信息
};
render() {
const { match } = this.state;
const { children, component, render } = this.props;//从Router结构中获取对应的处理方法
const { history, route, staticContext } = this.context.router;//从Context中获取数据
const location = this.props.location || route.location;
const props = { match, location, history, staticContext };
//如果页面匹配成功,进行createElement的渲染。在这里就会调用component的render===>页面刷新 这是处理第一次页面渲染
if (component) return match ? React.createElement(component, props) : null;
//这里针对首页已经被渲染,在进行路由处理的时候,根据props中的信息,进行页面的跳转或者刷新
if (render) return match ? render(props) : null;
return null;
}
}
export default Route;
Buzzer
Для React-Router он фактически инкапсулирует историю H5, чтобы он мог идентифицировать и сопоставлять изменения URL-адреса с рендерингом компонента.
- Рефакторинг истории H5 на основе различных API, таких как BrowserRouter.
- Строительство конструкции и регистрация атрибута истории.
- Зарегистрируйте обратный вызов истории в componentWillMount Router.
- Рассчитайте путь в Redirect и вызовите history.push/history.replace, чтобы обновить информацию об истории.
- В Route процесс рендеринга первой страницы/обновления страницы выполняется в соответствии с вычисленным результатом сопоставления.