Первоначально опубликовано на моемGitHub blog, с архивом всех статей, добро пожаловать в звездочку
предисловие
Как самая популярная библиотека управления маршрутизацией в React, react-router стала в некотором смысле официальной библиотекой маршрутизации (но библиотека маршрутизации следующего поколенияreach-router уже готова к работе), и она была обновлена до версии v4, которая завершено Все является обновлением компонента. В этой статье будет проанализирован исходный код react-router v4 (далее rr4), чтобы понять, как rr4 помогает нам управлять статусом маршрутизации.
маршрутизация
Прежде чем анализировать исходный код, давайте разберемся с маршрутизацией. До распространения SPA на уровне front-end не было концепции маршрутизации, каждый URL соответствует странице, и все переходы или ссылки проходят через нее.<a>С постепенным процветанием SPA и популяризацией HTML5 появляется все больше и больше библиотек хеш-маршрутизации и маршрутизации на основе истории.
Самая большая роль библиотеки маршрутизацииURL-адрес синхронизации и соответствующая функция обратного вызова. Для маршрутизации на основе истории она проходит черезhistory.pushStateизменить URL-адрес с помощьюwindow.addEventListener('popstate', callback)для прослушивания событий вперед/назад; для маршрутизации хэшей, оперируяwindow.locationстрока для изменения хэша,window.addEventListener('hashchange', callback)для прослушивания изменений URL.
Реализация маршрутизации SPA
хэш-маршрутизация
class Router {
constructor() {
// 储存 hash 与 callback 键值对
this.routes = {};
// 当前 hash
this.currentUrl = '';
// 记录出现过的 hash
this.history = [];
// 作为指针,默认指向 this.history 的末尾,根据后退前进指向 history 中不同的 hash
this.currentIndex = this.history.length - 1;
this.backIndex = this.history.length - 1
this.refresh = this.refresh.bind(this);
this.backOff = this.backOff.bind(this);
// 默认不是后退操作
this.isBack = false;
window.addEventListener('load', this.refresh, false);
window.addEventListener('hashchange', this.refresh, false);
}
route(path, callback) {
this.routes[path] = callback || function() {};
}
refresh() {
console.log('refresh')
this.currentUrl = location.hash.slice(1) || '/';
this.history.push(this.currentUrl);
this.currentIndex++;
if (!this.isBack) {
this.backIndex = this.currentIndex
}
this.routes[this.currentUrl]();
console.log('指针:', this.currentIndex, 'history:', this.history);
this.isBack = false;
}
// 后退功能
backOff() {
// 后退操作设置为true
console.log(this.currentIndex)
console.log(this.backIndex)
this.isBack = true;
this.backIndex <= 0 ?
(this.backIndex = 0) :
(this.backIndex = this.backIndex - 1);
location.hash = `#${this.history[this.backIndex]}`;
}
}
полная реализацияhash-router,Ссылаться наhash router.
На самом деле, зная принцип маршрутизации, реализовать хэш-роутинг несложно, на что нужно обратить внимание, так это на реализацию backOff, в том числеhash routerТакже есть ошибка в реализации backOff в браузере, и срабатывает откат браузераhashChangeтак будетhistoryВ push новый путь, то есть каждый шаг будет записываться. так нуженbackIndexВ качестве идентификатора возвращаемого индекса при нажатии на новый URL возвращается backIndex.this.currentIndex.
Реализация маршрутизации на основе истории
class Routers {
constructor() {
this.routes = {};
// 在初始化时监听popstate事件
this._bindPopState();
}
// 初始化路由
init(path) {
history.replaceState({path: path}, null, path);
this.routes[path] && this.routes[path]();
}
// 将路径和对应回调函数加入hashMap储存
route(path, callback) {
this.routes[path] = callback || function() {};
}
// 触发路由对应回调
go(path) {
history.pushState({path: path}, null, path);
this.routes[path] && this.routes[path]();
}
// 后退
backOff(){
history.back()
}
// 监听popstate事件
_bindPopState() {
window.addEventListener('popstate', e => {
const path = e.state && e.state.path;
this.routes[path] && this.routes[path]();
});
}
}
Ссылаться на H5 Router
По сравнению с хеш-маршрутизацией, маршрутизация h5 больше не нуждается в каких-то уродливых модификациях.window.location, используйте вместоhistory.pushStateЗавершитьwindow.locationэксплуатация, использованиеwindow.addEventListener('popstate', callback)Для контроля вперед/назад, как и для обратного, вы можете использовать его напрямуюwindow.history.back()илиwindow.history.go(-1)Чтобы реализовать это напрямую, поскольку история браузера контролирует логику прямого/обратного хода, реализация намного проще.
маршрутизация в реакции
Как интерфейсный фреймворк, сам по себе React не имеет никаких функций, кроме представления (абстракция между данными и интерфейсом). выше изменения маршрутизации. Запущенная функция обратного вызоваПишем функцию DOM; В react мы не оперируем DOM напрямую, а управляем абстрагированным VDOM или JSX, для реакцииМаршрутизация должна управлять жизненным циклом компонентов и отображать разные компоненты для разных маршрутов..
Анализ исходного кода
Предварительные знания
Ранее мы узнали о цели создания маршрутов, реализации общей маршрутизации SPA и цели реактивной маршрутизации.Сначала давайте познакомимся с окружающими знаниями rr4, а затем начнем анализировать исходный код react-router.
history
historyбиблиотека, пара, от которой зависит rr4window.historyРасширенная версия библиотеки истории.
match
Получено из библиотеки истории, указывающее результат сопоставления текущего URL-адреса и пути.
match: {
path: "/", // 用来匹配的 path
url: "/", // 当前的 URL
params: {}, // 路径中的参数
isExact: pathname === "/" // 是否为严格匹配
}
location
Все еще полученный из библиотеки истории, он является производным от библиотеки истории, основанной на window.location.
hash: "" // hash
key: "nyi4ea" // 一个 uuid
pathname: "/explore" // URL 中路径部分
search: "" // URL 参数
state: undefined // 路由跳转时传递的 state
Разбираем исходники с вопросами, сначала разбираем функцию каждого компонента по отдельности, а в конце будет ответ, вот небольшой пример rr4.DEMO
- Процесс рендеринга при инициализации страницы?
- Щелкните ссылку, чтобы перейти и отобразить процесс обновления?
packages
RR4 разбирает маршрут на несколько пакетов — React-Router отвечает за универсальную логику маршрутизации, React-Router-Dom отвечает за управление маршрутом браузера, React-router-native отвечает за управление маршрутом React-Native, и общая часть — это прямой импорт в React-Router, пользователям нужно только ввести React-Router-Dom или React-router-native, React-Router действует как зависимости, которые больше не нужно вводить отдельно.
Router
import React from 'react'
import { render } from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import App from './components/App';
render(){
return(
<BrowserRouter>
<App />
</BrowserRouter>
)
)}
Вот как мы называем Router, возьмем BrowserRouter в качестве примера.
Исходный код BrowserRouter находится в react-router-dom.Это высокоуровневый компонент, который внутри создает глобальный объект истории (который может отслеживать изменения всего маршрута) и передает историю как реквизит компоненту маршрутизатора react-router (маршрутизатор Затем компонент передаст атрибут этой истории дочернему компоненту в качестве контекста)
render() {
return <Router history={this.history} children={this.props.children} />;
}
По сути, ядро всего Router находится в Router-компоненте react-router, следующим образом передавая компоненты Route с помощью контекста, что также объясняет, почему Router должен быть вне всех Routes.
getChildContext() {
return {
router: {
...this.context.router,
history: this.props.history,
route: {
location: this.props.history.location,
match: this.state.match
}
}
};
}
Это контекст, который маршрутизатор передает дочернему компоненту. На самом деле, маршрут также передает маршрутизатор в качестве контекста. Если мы добавим его к компоненту, отображаемому маршрутом
static contextTypes = {
router: PropTypes.shape({
history: PropTypes.object.isRequired,
route: PropTypes.object.isRequired,
staticContext: PropTypes.object
})
};
Для доступа к роутеру через контекст, а вот rr4 вообще передаётся через пропсы, а история, локация, совпадение передаются как три независимых пропса к отрисовываемому компоненту, что облегчает доступ (собственно, свойства объект маршрутизатора был полностью передан. ).
В компоненте RouterWillMount добавлено
componentWillMount() {
const { children, history } = this.props;
invariant(
children == null || React.Children.count(children) === 1,
"A <Router> may have only one child element"
);
// 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 <sStaticRouter>.
this.unlisten = history.listen(() => {
this.setState({
match: this.computeMatch(history.location.pathname)
});
});
}
history.listenВозможность прослушивания изменений маршрута и выполнения событий обратного вызова.
Здесь событие обратного вызова, выполняемое для каждого изменения маршрута,
this.setState({
match: this.computeMatch(history.location.pathname)
});
По сравнению с операциями, выполняемыми в setState, сам setState более значим — каждое изменение маршрута -> запускает событие обратного вызова маршрутизатора верхнего уровня -> маршрутизатор выполняет setState -> вниз nextContext (контекст содержит последнее местоположение) -> следующий маршрут получает новый nextContext, чтобы определить, следует ли отображать.
Причина, по которой эта функция подписки написана в componentWillMount, аналогична комментарию, данному в исходном коде: когда это для SSR, можно использовать Redirect.
Route
Роль Route состоит в том, чтобы сопоставить маршрут и передать его реквизитам компонента для рендеринга.
В компоненте RouteWillReceiveProps
componentWillReceiveProps(nextProps, nextContext) {
...
this.setState({
match: this.computeMatch(nextProps, nextContext.router)
});
}
Маршрут принимает контекст, переданный маршрутизатором верхнего уровня, а история в маршрутизаторе отслеживает изменения маршрутизации всей страницы. Реквизиты и контекст маршрута обновляются, чтобы судить о том, соответствует ли путь текущего маршрута местоположению, если он соответствует, он будет отображаться, в противном случае он не будет отображаться.
Основой для сопоставления является функция calculateMatch, которая будет проанализирована ниже, здесь вам нужно только знать, если совпадение не удается, то совпадениеnull, если совпадение прошло успешно, результат совпадения будет использоваться как часть реквизита и передаваться компоненту для рендеринга.
Затем взгляните на раздел рендеринга Route.
render() {
const { match } = this.state; // 布尔值,表示 location 是否匹配当前 Route 的 path
const { children, component, render } = this.props; // Route 提供的三种可选的渲染方式
const { history, route, staticContext } = this.context.router; // Router 传入的 context
const location = this.props.location || route.location;
const props = { match, location, history, staticContext };
if (component) return match ? React.createElement(component, props) : null; // Component 创建
if (render) return match ? render(props) : null; // render 创建
if (typeof children === "function") return children(props); // 回调 children 创建
if (children && !isEmptyChildren(children)) // 普通 children 创建
return React.Children.only(children);
return null;
}
rr4 предоставляет три метода для рендеринга компонентов: реквизиты компонентов, реквизиты для рендеринга и реквизиты для детей.Приоритет рендеринга также в порядке.Если предыдущий был отрендерен, он вернется напрямую.
- компонент (реквизит) — поскольку он был создан с помощью React.createElement, можно передать компонент класса.
- render (реквизиты) — напрямую вызовите render() для расширения дочерних элементов, поэтому вам нужно передать функциональный компонент без сохранения состояния.
- Children (реквизит) - на самом деле рендер похож, разница не оценивается совпадением, всегда будет рендериться.
- Children (дочерние элементы) — если ничего из вышеперечисленного, дочерние элементы будут отображаться по умолчанию, но дочерний элемент может быть только один.
Вот объяснение официального сайтаtips, компонент использует React.createElement для создания новых элементов, поэтому, если вы передаете встроенную функцию, такую как
<Route path='/' component={()=>(<div>hello world</div>)}
Если каждый компонент prOPs.component создан заново, React появится в новом компоненте при Diff, поэтому старые компоненты будут размонтированы, а затем снова смонтированы. В это время следует использовать рендер, меньше чем слой элемента Component, а тип элемента после рендера тот же, и будет перемонтирование (детей не бывает).
Switch
Давайте посмотрим на коммутатор рядом с маршрутом. Коммутатор используется для вложения вне маршрута. Когда первый маршрут в коммутаторе совпадает, никакие другие маршруты не будут отображаться.
render() {
const { route } = this.context.router;
const { children } = this.props;
const location = this.props.location || route.location;
let match, child;
React.Children.forEach(children, element => {
if (match == null && React.isValidElement(element)) {
const {
path: pathProp,
exact,
strict,
sensitive,
from
} = element.props;
const path = pathProp || from;
child = element;
match = matchPath(
location.pathname,
{ path, exact, strict, sensitive },
route.match
);
}
});
return match
? React.cloneElement(child, { location, computedMatch: match })
: null;
}
Switch также использует функцию matchPath, чтобы определить, было ли совпадение успешным. Она всегда обходит дочерние элементы в порядке дочерних элементов в Switch. местоположение и вычисленное совпадение помечены. Используйте React.cloneElement для рендеринга в конце и возврата, если нет подходящих дочерних элементов.null.
Далее давайте посмотрим, как matchPath определяет, соответствует ли местоположение пути.
matchPath
matchPath возвращает объект со следующей структурой
{
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;
}, {})
}
Эта информация будет передана в Route и Switch в качестве параметров сопоставления (Switch — это просто прокси, его роль заключается в отображении Route, вычисленное соответствие, вычисленное Switch, будет передано в Route для рендеринга, в это время Route будет напрямую использовать это вычисленное соответствие без необходимости вычислять его самостоятельно).
При компиляцииPath внутри matchPath есть
const patternCache = {};
const cacheLimit = 10000;
let cacheCount = 0;
В качестве кеша для pathToRegexp, потому что модуль импорта ES6 экспортирует значениеЦитировать, так что patternCache можно понимать как кеш глобальной переменной, и кеш начинается с{option:{pattern: }}Хранится в виде , то если вам нужно сопоставить путь того же шаблона и опции, вы можете получить регулярное выражение и ключи прямо из кеша.
Причина добавления кэширования заключается в том, что страницы маршрутизации в большинстве случаев похожи, например, доступ/user/123или/users/234, буду использовать/user/:idЭтот путь соответствует, нет необходимости каждый раз генерировать новое регулярное выражение. SPA поддерживает этот кеш на протяжении всего посещения страницы.
Link
На самом деле самое большее, что мы можем написать, это тег Link, начнем с его функции рендеринга.
render() {
const { replace, to, innerRef, ...props } = this.props; // eslint-disable-line no-unused-vars
invariant(
this.context.router,
"You should not use <Link> outside a <Router>"
);
invariant(to !== undefined, 'You must specify the "to" property');
const { history } = this.context.router;
const location =
typeof to === "string"
? createLocation(to, null, null, history.location)
: to;
const href = history.createHref(location);
// 最终创建的是一个 a 标签
return (
<a {...props} onClick={this.handleClick} href={href} ref={innerRef} />
);
}
Можно видеть, что Link, наконец, создает тег a для переноса элемента, который нужно перепрыгнуть, но если это обычный тег a с href, он будет напрямую переходить на новую страницу вместо SPA, поэтому в этом handleClick элемента тег будет preventDefault, запрещающий переход по умолчанию, поэтому href здесь не имеет практического эффекта, но он все же может помечать URL-адрес страницы, на которую нужно перейти, и имеет лучшую семантику html.
В handleClick, для && не нажата && левая кнопка мыши "preventDefault"_blankКлавиша && прыжка не зажимает другие функциональные клавиши "нажмите для предотвращения по умолчанию, а затем нажмите в историю, о которой также упоминалось ранее - смена маршрута и переход страницы не связаны друг с другом, rr4 есть передается по ссылке. Нажатие библиотеки истории вызывает историю HTML5pushState, но это только меняет маршрут и ничего больше. Вы все еще помните прослушивание в Router, он будет прослушивать изменения в маршрутах, а затем обновлять реквизиты и nextContext через контекст, чтобы позволить маршрутам более низкого уровня повторно сопоставляться и завершать обновление той части, которая должна быть отрисована.
handleClick = event => {
if (this.props.onClick) this.props.onClick(event);
if (
!event.defaultPrevented && // onClick prevented default
event.button === 0 && // ignore everything but left clicks
!this.props.target && // let browser handle "target=_blank" etc.
!isModifiedEvent(event) // ignore clicks with modifier keys
) {
event.preventDefault();
const { history } = this.context.router;
const { replace, to } = this.props;
if (replace) {
history.replace(to);
} else {
history.push(to);
}
}
};
withRouter
Роль withRouter заключается в том, чтобы позволить нам получать информацию о маршрутизации в обычных компонентах, которые не вложены напрямую в Route.WithRouter(wrappedComponent)Чтобы создать HOC для передачи свойств, WithRouter на самом деле является HOC, который обертывает SomeComponent с Route.
Существует три способа создания Маршрута, и здесь непосредственно используется передачаchildrenprops, потому что этот HOC будет отображать wrapComponent (childrenprops редко используется, в какой-то степени это внутренний метод).
Когда HOC наконец возвращается, используется метод hoistStatics. Функция этого метода состоит в том, чтобы сохранить статический метод класса SomeComponent. Поскольку HOC оборачивает слой Route во внешний слой wrapComponent, статический метод Класс wrapComponent необходимо перенести в новый.Рут, подробнее см.Static Methods Must Be Copied Over.
понимать
Теперь вернитесь к вопросу в начале и заново поймите процесс нажатия на ссылку для перехода.
Есть две вещи, которые необходимо сделать:
- изменения маршрутизации
- Изменения в рендеринговой части страницы
Процесс выглядит следующим образом:
- Когда Router впервые монтируется, Router прослушивает функцию обратного вызова в componentWillMount, которая управляется библиотекой истории.Эта функция обратного вызова запускается каждый раз при изменении маршрута. Эта функция обратного вызова вызовет setState.
- При щелчке тега Link фактически щелкается отображаемый на странице тег a, а затем используется preventDefault для предотвращения перехода тега a на страницу.
- В Link вы также можете получить историю, переданную через контекст в Router -> Route, и выполнить
hitsory.push(to), эта функция на самом деле является оболочкойwindow.history.pushState(), это API истории HTML5, но после pushState он не имеет никакого эффекта, кроме изменения адресной строки.На этом этапе цель 1: изменение маршрутизации выполнено. - На шаге 1 изменение маршрута вызовет setState маршрутизатора, как написано в главе Router: каждое изменение маршрута -> инициировать событие слушателя маршрутизатора верхнего уровня -> маршрутизатор запускает setState -> передать вниз новый nextContext (nextContext содержит последнее местоположение)
- Маршрут более низкого уровня получает новый nextContext и использует функцию matchPath, чтобы определить, соответствует ли путь местоположению. Если он совпадает, он будет отображаться. Если нет, он не будет отображаться. Цель 2: изменить часть рендеринга страницы.
Суммировать
Увидев это, я полагаю, вы уже можете понять реализацию front-end маршрутизации и реализацию react-router, но у react-router много недостатков, поэтому и появляетсяreach-router.
В следующей статье я расскажу, как создать маршрут, который можно кэшировать — например, когда страница со списком переходит на страницу сведений, а затем возвращается, внешний вид страницы со списком восстанавливается, включая статус и позицию прокрутки. .
Адрес склада:react-live-route, вы можете пометить, если хотите, и вы можете поднимать вопросы.
Первоначально опубликовано на моемGitHub blog, есть архив всех статей, добро пожаловать в звездочку.