предисловие
Предыдущая статья представилаВнешняя маршрутизацияДва принципа реализации, сегодня я хочу начать сreact-router
Анализ исходного кода, как они управляют интерфейсной маршрутизацией. Поскольку версия V4 использовалась ранее, следующий анализ также основан на реакции-маршрутизаторе.v4.4.0版本
(далее V4), добро пожаловать на комментарии и обмен. Давайте начнем.
дошкольные знания
Прежде чем анализировать исходный код, ознакомьтесь со следующими сопутствующими знаниями, которые помогут лучше понять структуру исходного кода.
- существует
react
Как реализовать разные маршруты для рендеринга разных компонентов в ?
Как интерфейсный фреймворк, сам по себе react не имеет
view
(абстракция между данными и интерфейсом) за исключением любой функции, в предыдущей статье мы работали через триггерную функцию обратного вызоваDOM
, тогда как в реакции мы не работаем напрямуюDOM
, но управление абстрагируетсяVDOM
ИлиJSX
, для реагирования маршрутизация должна управлять компонентами生命周期
, который отображает разные компоненты для разных маршрутов.
-
history
(сторонняя библиотека) использовать
Поскольку React-Router основан на
history
В этой библиотеке реализован мониторинг изменений роутинга, поэтому сначала проведем простой анализ этой библиотеки. Конечно, мы в основном анализируем его режим мониторингаlisten
Как это реализовано, что имеет решающее значение для реализации маршрутизации, если вы хотите узнать больше о других API, пожалуйста, переместитеhistoryУзнать больше.
history
history
Основное использование выглядит следующим образом:
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
// 获取当前location。
const location = history.location;
// 监听当前location的更改。
const unlisten = history.listen((location, action) => {
// location是一个类似window.location的对象
console.log(action, location.pathname, location.state);
});
// 使用push、replace和go来导航。
history.push('/home', { some: 'state' });
// 若要停止监听,请调用listen()返回的函数.
unlisten();
Давайте посмотрим на модули исходного кода нижеindex.js, видно, что история выставляет семь методов:
export { default as createBrowserHistory } from './createBrowserHistory';
export { default as createHashHistory } from './createHashHistory';
export { default as createMemoryHistory } from './createMemoryHistory';
export { createLocation, locationsAreEqual } from './LocationUtils';
export { parsePath, createPath } from './PathUtils';
В приведенном выше примере давайте сделаем простое сравнениеcreateBrowserHistory
а такжеcreateHashHistory
:
-
createHashHistory
Использование URL-адреса вhash(#)
часть, чтобы создать что-то вродеexample.com/#/some/path
маршрут.createHashHistoryАнализ исходного кода:
function getHashPath() { // 我们不能使用window.location.hash,因为它不是跨浏览器一致- Firefox将预解码它! const href = window.location.href; const hashIndex = href.indexOf('#'); return hashIndex === -1 ? '' : href.substring(hashIndex + 1);//函数返回hush值 }
-
createBrowserHistory
с помощью браузераHistoryAPI для обработки URL-адресов, созданияexample.com/some/path
Такой реальный URL.createBrowserHistory.jsАнализ исходного кода:
//createBrowserHistory.js const PopStateEvent = 'popstate';//变量,下面window监听事件popstate用到 const HashChangeEvent = 'hashchange';//变量,下面window监听事件hashchange用到 function createBrowserHistory(props = {}) { invariant(canUseDOM, 'Browser history needs a DOM'); const globalHistory = window.history; // 创建一个使用HTML5 history API(包括)的history对象 const canUseHistory = supportsHistory(); const needsHashChangeListener = !supportsPopStateOnHashChange(); //... //push方法 function push(path, state) { if (canUseHistory) { globalHistory.pushState({ key, state }, null, href);//在push方法内使用pushState ... } //replace方法 function replace(path, state) { if (canUseHistory) { globalHistory.replaceState({ key, state }, null, href);//在replaceState方法内使用replaceState } } let listenerCount = 0; //注册路由监听事件 function checkDOMListeners(delta) { listenerCount += delta; if (listenerCount === 1 && delta === 1) { window.addEventListener(PopStateEvent, handlePopState);//popstate监听前进/后退事件 if (needsHashChangeListener) window.addEventListener(HashChangeEvent, handleHashChange);//hashchange监听 URL 的变化 } else if (listenerCount === 0) { window.removeEventListener(PopStateEvent, handlePopState); if (needsHashChangeListener) window.removeEventListener(HashChangeEvent, handleHashChange); } } //路由监听 function listen(listener) { const unlisten = transitionManager.appendListener(listener); checkDOMListeners(1); return () => { checkDOMListeners(-1); unlisten(); }; }
Как listen запускает прослушивание:
над
createBrowserHistory.js
Существует также введение в регистрацию прослушивателей маршрутизации.Давайте посмотрим, как активировать прослушиватели маршрутизации. Функция обратного вызова прослушивателя событий анализаhandlePopState
, который, наконец, проходит черезsetState
для запуска слушателей маршрута, гдеnotifyListeners
позвоню всемlisten
Функция обратного вызова, чтобы уведомить слушателей, прослушивающих изменения маршрутизации.
//createBrowserHistory.js
const transitionManager = createTransitionManager();
function setState(nextState) {
Object.assign(history, nextState);
history.length = globalHistory.length;
// 调用所有的listen 的回调函数,从而达到通知监听路由变化的监听者
transitionManager.notifyListeners(history.location, history.action);
}
//事件监听的回调函数handlePopState
function handlePopState(event) {
// 忽略WebKit中无关的popstate事件。
if (isExtraneousPopstateEvent(event)) return;
handlePop(getDOMLocation(event.state));
}
let forceNextPop = false;
//回调执行函数
function handlePop(location) {
if (forceNextPop) {
forceNextPop = false;
setState();
} else {
const action = 'POP';
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (ok) {
setState({ action, location });
} else {
revertPop(location);
}
}
);
}
}
// createTransitionManager.js
function notifyListeners(...args) {
//调用所有的listen 的回调函数
listeners.forEach(listener => listener(...args));
}
существуетreact-router
изRouter
Вызов history.listen вызывается в жизненном цикле компонента componentWillMount, поэтому при изменении маршрута будет вызываться метод setState для отображения соответствующего компонента маршрутизации.
// react-router/Router.js
constructor(props) {
super(props);
this.state = {
location: props.history.location
};
//调用history.lesten方法监听,setState渲染组件
this.unlisten = props.history.listen(location => {
this.setState({ location });
});
}
componentWillUnmount() {
//路由监听
this.unlisten();
}
резюме: Вышеприведенный анализreact-router
Как использовать сторонние библиотекиhistory
Процесс мониторинга изменений маршрутизации будет описан ниже.react-router
как это сочетаетсяhistory
сделай этоSPA
Изменения маршрутизации достигают эффекта рендеринга различных компонентов, давайте сначала посмотримreact-router
Основное использование, разобраться в идеях анализа.
Основное использование реактивного маршрутизатора
//引用官方例子
import React from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
function BasicExample() {
return (
<Router>
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/topics">Topics</Link>
</li>
</ul>
<hr />
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/topics" component={Topics} />
</div>
</Router>
);
}
function Home() {
return (
<div>
<h2>Home</h2>
</div>
);
}
function About() {
return (
<div>
<h2>About</h2>
</div>
);
}
function Topics({ match }) {
return (
<div>
<h2>Topics</h2>
<ul>
<li>
<Link to={`${match.url}/rendering`}>Rendering with React</Link>
</li>
<li>
<Link to={`${match.url}/components`}>Components</Link>
</li>
<li>
<Link to={`${match.url}/props-v-state`}>Props v. State</Link>
</li>
</ul>
<Route path={`${match.path}/:topicId`} component={Topic} />
<Route
exact
path={match.path}
render={() => <h3>Please select a topic.</h3>}
/>
</div>
);
}
function Topic({ match }) {
return (
<div>
<h3>{match.params.topicId}</h3>
</div>
);
}
export default BasicExample;
V4
Разделите маршрут на следующие пакеты:
-
react-router
Отвечает за общую логику маршрутизации -
react-router-dom
Отвечает за управление маршрутизацией браузера. -
react-router-native
Отвечает за управление маршрутизацией реактивных
Пользователям нужно только импортироватьreact-router-dom
илиreact-router-native
Вот и все,react-router
Существование в качестве зависимости больше не требует отдельного импорта.
React Router
Существует три типа компонентов:
- Компоненты маршрутизатора (
, ) - Компоненты сопоставления маршрутов (
, ) - Компоненты навигации (,
, ).
В приведенной выше демонстрации мы также используем эти три типа компонентов.Чтобы облегчить анализ исходного кода, мы можем разобраться в основном процессе:
- использовать
<BrowserRouter>
Создайте специальный объект истории и зарегистрируйтесь для прослушивания событий. - использовать
<Route>
соответствует пути и отображает соответствующий компонент. - использовать
<Link>
Создайте ссылку для перехода к компоненту, который вы хотите визуализировать.
Далее мы проанализируем шаг за шагом в соответствии с вышеуказанными этапами процесса.react-router
Код.
BrowserRouter
/* react-router-dom/BrowserRouter.js */
//从history引入createBrowserHistory
import { createBrowserHistory as createHistory } from "history";
import warning from "warning";
//导入react-router 的 Router 组件
import Router from "./Router";
class BrowserRouter extends React.Component {
//创建全局的history对象,这里用的是HTML5 history
history = createHistory(this.props);
render() {
//将 history 作为 props 传递给 react-router 的 Router 组件
return <Router history={this.history} children={this.props.children} />;
}
}
BrowserRouter
Исходный код находится вreact-router-dom
, который является компонентом более высокого порядка, который внутренне создает глобальныйhistory
объект (который может прослушивать изменения всего маршрута) и будетhistory
передан в качестве реквизитаreact-router
изRouter
компонент (компонент маршрутизатора затем передаст это свойство истории в качестве контекста дочерним компонентам). Как показано ниже, компоненты передаются в Route с контекстом, что также объясняет, почему Router должен быть вне всех Routes.
//react-router/Router.js
import React from "react";
import RouterContext from "./RouterContext";
//获取history、location、match...
function getContext(props, state) {
return {
history: props.history,
location: state.location,
match: Router.computeRootMatch(state.location.pathname),
staticContext: props.staticContext
};
}
class Router extends React.Component {
//定义Router组件的match属性字段
static computeRootMatch(pathname) {
return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
/*
* path: "/", // 用来匹配的 path
* url: "/", // 当前的 URL
* params: {}, // 路径中的参数
* isExact: pathname === "/" // 是否为严格匹配
*/
}
constructor(props) {
super(props);
this.state = {
location: props.history.location
};
//监听路由的变化并执行回调事件,回调内setState
this.unlisten = props.history.listen(location => {
this.setState({ location });
/*
*hash: "" // hash
*key: "nyi4ea" // 一个 uuid
*pathname: "/explore" // URL 中路径部分
*search: "" // URL 参数
*state: undefined // 路由跳转时传递的 state
*/
});
}
componentWillUnmount() {
//组件卸载时停止监听
this.unlisten();
}
render() {
const context = getContext(this.props, this.state);
return (
<RouterContext.Provider
children={this.props.children || null}
value={context}<!--借助 context 向 Route 传递组件-->
/>
);
}
}
export default Router;
По сравнению с обратным вызовом слушателяsetState
сделать операцию,setState
Это более значимо само по себе - каждое изменение маршрута -> срабатывание верхнего уровняRouter
событие обратного вызова ->Router
провестиsetState
-> пройти внизnextContext
(context
содержит последниеlocation
) -> нижеRoute
получить новыйnextContext
Определите, нужно ли рендерить.
Route
В исходном коде есть это введение: «Общедоступный API для сопоставления одного пути и рендеринга». просто понимается как нахождение
location
а также<router>
изpath
соответствующие компоненты и рендер.
//react-router/Route.js
//判断Route的子组件是否为空
function isEmptyChildren(children) {
return React.Children.count(children) === 0;
}
//获取history、location、match...
//父组件没传的话使用context中的
function getContext(props, context) {
const location = props.location || context.location;
const match = props.computedMatch
? props.computedMatch // <Switch> already computed the match for us
: props.path
? matchPath(location.pathname, props)//在./matchPath.js中匹配location.pathname与path
: context.match;
return { ...context, location, match };
}
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Route> outside a <Router>");
const props = getContext(this.props, context);
//context 更新 props 和 nextContext会重新匹配
let { children, component, render } = this.props;
// 提前使用一个空数组作为children默认值,如果是这样,就使用null。
if (Array.isArray(children) && children.length === 0) {
children = null;
}
if (typeof children === "function") {
children = children(props);
if (children === undefined) {
children = null;
}
}
return (
<RouterContext.Provider value={props}>
{children && !isEmptyChildren(children)
? children
: props.match//对应三种渲染方式children、component、render,只能使用一种
? component
? React.createElement(component, props)
: render
? render(props)
: null
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}
Route
принять верхнийRouter
входящийcontext
,Router
История в нем отслеживает изменения роутинга всей страницы.При скачках страницы,history
вызвать событие слушателя,Router
пройти внизnextContext
, он будет обновлятьсяRoute
изprops
а такжеcontext
судить о текущемRoute
изpath
соответствоватьlocation
, рендеринг, если он совпадает, иначе не делать.
Матч основан наmatchPath
Эта функция будет проанализирована в следующем разделе, здесь вам нужно только знать, если сопоставление не удается.match
дляnull
, если совпадение прошло успешно,match
результат какprops
часть компонента, переданного для рендеринга в render.
Из метода рендеринга вы можете узнать, что есть три способа рендеринга компонентов (children
,component
,render
) Приоритет отрисовки тоже в порядке.Если предыдущая отрендерилась, то вернётся напрямую.
-
children
(еслиchildren
это метод, выполнить этот метод, если это только дочерний элемент, напрямуюrender
этот элемент) -
component
(передайте компонент напрямую, затем перейдите кrender
компоненты) -
render
(рендеринг - это метод, пройдите методrender
этот компонент)
Далее мы смотрим наmatchPath
как судитьlocation
Соответствует ли он path .
function matchPath(pathname, options = {}) {
if (typeof options === "string") options = { path: options };
const { path, exact = false, strict = false, sensitive = false } = options;
const { regexp, keys } = compilePath(path, { end: exact, strict, sensitive });
const match = regexp.exec(pathname);
if (!match) return null;
const [url, ...values] = match;
const isExact = pathname === url;
if (exact && !isExact) return null;
return {
path, // 用来进行匹配的路径,其实是直接导出的传入 matchPath 的 options 中的 path
url: path === "/" && url === "" ? "/" : url, // URL的匹配部分
isExact, // url 与 path 是否是 exact 的匹配
// 返回的是一个键值对的映射
// 比如你的 path 是 /users/:id,然后匹配的 pathname 是 /user/123
// 那么 params 的返回值就是 {id: '123'}
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index];
return memo;
}, {})
};
}
Link
class Link extends React.Component {
static defaultProps = {
replace: false
};
handleClick(event, context) {
if (this.props.onClick) this.props.onClick(event);
if (
!event.defaultPrevented && // 阻止默认事件
event.button === 0 && // 忽略除左击之外的所有内容
!this.props.target && // 让浏览器处理“target=_blank”等。
!isModifiedEvent(event) // 忽略带有修饰符键的单击
) {
event.preventDefault();
const method = this.props.replace
? context.history.replace
: context.history.push;
method(this.props.to);
}
}
render() {
const { innerRef, replace, to, ...props } = this.props;
// eslint-disable-line no-unused-vars
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Link> outside a <Router>");
const location =
typeof to === "string"
? createLocation(to, null, null, context.location)
: to;
const href = location ? context.history.createHref(location) : "";
return (
<a
{...props}
onClick={event => this.handleClick(event, context)}
href={href}
ref={innerRef}
/>
);
}}
</RouterContext.Consumer>
);
}
}
Суммировать
Давайте вспомним процесс, через который мы прошли после прочтения основного использования:
- использовать
<BrowserRouter>
Создайте специальный объект истории и зарегистрируйтесь для прослушивания событий. - использовать
<Route>
соответствует пути и отображает соответствующий компонент. - использовать
<Link>
Создайте ссылку для перехода к компоненту, который вы хотите визуализировать.
В сочетании с исходным кодом мы проанализируем конкретную реализацию
- использовать
BrowserRouter
оказыватьRouter
создает глобальныйhistory
объект и черезprops
перешел кRouter
, пока вRouter
Функция прослушивателя устанавливается в , которая использует прослушивание библиотеки истории, и выполняется инициированный обратный вызов.setState
Передайте следующий контекст. - при нажатии на страницу
Link
Да, действительно нажалa
теги, но с использованиемpreventDefault
Не допуститьa
Переход на вкладку страницы, даваяa
Добавьте событие щелчка к метке для выполненияhitsory.push(to)
. - Изменение маршрута вызовет
Router
изsetState
Да, вRouter
В этой главе написано: каждое изменение маршрута -> активировать верхний уровеньRouter
событие слушателя ->Router
вызыватьsetState
-> передать новыйnextContext
(nextContext
содержит последниеlocation
). - Маршрут принимает новый nextContext и использует функцию matchPath, чтобы определить, соответствует ли путь местоположению.Если он соответствует, он будет отображаться, а если не соответствует, он не будет отображаться.