Чем больше вы знаете, тем больше вы не знаете
点赞
Посмотри еще раз, аромат остался в руке, и слава
предисловие
Современные интерфейсные проекты в основном представляют собой одностраничные веб-приложения (SPA), а маршрутизация — важная часть одностраничных веб-приложений.
Каждая современная передняя структура имеет соответствующую реализацию маршрутизации, такую как Vue-маршрутизатор, React-маршрутизатор и т. Д.
В этой статье не рассматривается реализация vue-router и react-router, но представлены основные принципы реализации и методы реализации интерфейсной маршрутизации.
Анализ исходного кода vue-router и react-router будет постепенно представлен в следующих статьях.
Что такое СПА
SPA — это аббревиатура одностраничного веб-приложения, что переводится как одностраничное веб-приложение.
Проще говоря, SPA — это веб-проект только с одной HTML-страницей.После загрузки страницы SPA не будет перезагружать или переходить страницу из-за действий пользователя. Вместо этого используйте JS для динамического преобразования содержимого HTML, чтобы имитировать переход между несколькими представлениями.
От традиционного к просмотру страницы
Новичкам сложно понять разницу между традиционными страницами и представлениями SPA.
Здесь две диаграммы используются для иллюстрации различий между традиционными страницами и представлениями SPA:
На изображении выше показано, что в традиционном дизайне веб-сайта каждый HTML-файл представляет собой завершенную HTML-страницу, охватывающую полную HTML-структуру.
На приведенном выше рисунке показано, что в дизайне приложения SPA приложение имеет только один файл HTML, и файл HTML содержит заполнитель (то есть контейнер на рисунке), а содержимое, соответствующее заполнителю, определяется каждым представлением. Для SPA переключение страниц — это переключение между представлениями.
Происхождение внешней маршрутизации
Первая веб-страница была многостраничной, и только с появлением Ajax постепенно развивалась SPA.
Появление SPA значительно улучшает интерактивный опыт веб-приложений. Во время взаимодействия с пользователем больше не нужно обновлять страницу, а данные поступают асинхронно через Ajax, и отображение страницы становится более плавным.
Однако, поскольку взаимодействие с пользователем в SPA достигается за счет изменения содержимого HTML через JS, URL-адрес самой страницы не меняется, что приводит к двум проблемам:
- SPA не может запомнить запись операции пользователя, будь то обновление, перемотка вперед или назад, она не может показать реальный ожидаемый контент пользователя.
- Несмотря на то, что в SPA существуют разные формы отображения страниц из-за разных предприятий, существует только один URL-адрес, который не удобен для SEO и неудобен для индексации поисковыми системами.
Внешняя маршрутизация, по-видимому, решает вышеуказанные проблемы.
Что такое Frontend-маршрутизация
Проще говоря, он заключается в том, чтобы сопоставить специальный URL-адрес для каждой формы отображения представления в SPA, при этом гарантируя, что существует только одна HTML-страница, а страница не обновляется и не пропускается при взаимодействии с пользователем. Это достигается с помощью этого специального URL-адреса при обновлении, перемотке вперед, назад и SEO.
Для достижения этой цели нам необходимо сделать следующее:
- Измените URL-адрес и не позволяйте браузеру отправлять запрос, как сервер.
- Может отслеживать изменения URL
Режим хеширования и режим истории, которые будут представлены далее, предназначены для реализации вышеуказанных функций.
хэш-режим
Хэш здесь относится к знаку # после URL-адреса и следующих символов. Например "www.baidu.com/#hashhash" , где "#hashhash" — ожидаемое значение хеш-функции.
Поскольку изменение значения хеша не заставит браузер отправить запрос на сервер, а изменение хеша вызовет событие hashchange, браузер также может управлять им вперед и назад, поэтому до появления режима истории H5, в основном используйте хеш-шаблон для реализации внешней маршрутизации.
Используемый API:
window.location.hash = 'hash字符串'; // 用于设置 hash 值
let hash = window.location.hash; // 获取当前 hash 值
// 监听hash变化,点击浏览器的前进后退会触发
window.addEventListener('hashchange', function(event){
let newURL = event.newURL; // hash 改变后的新 url
let oldURL = event.oldURL; // hash 改变前的旧 url
},false)
Далее мы реализуем объект маршрутизации
Создайте объект маршрутизации и реализуйте метод register для регистрации функции обратного вызова, соответствующей каждому хеш-значению.
class HashRouter{
constructor(){
//用于存储不同hash值对应的回调函数
this.routers = {};
}
//用于注册每个视图
register(hash,callback = function(){}){
this.routers[hash] = callback;
}
}
При отсутствии хеш-значения она считается домашней страницей, поэтому реализуется метод registerIndex для регистрации callback-функции домашней страницы.
class HashRouter{
constructor(){
//用于存储不同hash值对应的回调函数
this.routers = {};
}
//用于注册每个视图
register(hash,callback = function(){}){
this.routers[hash] = callback;
}
//用于注册首页
registerIndex(callback = function(){}){
this.routers['index'] = callback;
}
}
Отслеживайте изменения хэша с помощью hashchange и определяйте функцию обратного вызова при изменении хэша.
class HashRouter{
constructor(){
//用于存储不同hash值对应的回调函数
this.routers = {};
window.addEventListener('hashchange',this.load.bind(this),false)
}
//用于注册每个视图
register(hash,callback = function(){}){
this.routers[hash] = callback;
}
//用于注册首页
registerIndex(callback = function(){}){
this.routers['index'] = callback;
}
//用于调用不同视图的回调函数
load(){
let hash = location.hash.slice(1),
handler;
//没有hash 默认为首页
if(!hash){
handler = this.routers.index;
}else{
handler = this.routers[hash];
}
//执行注册的回调函数
handler.call(this);
}
}
Давайте сделаем пример, чтобы продемонстрировать HashRouter, который мы только что закончили.
<body>
<div id="nav">
<a href="#/page1">page1</a>
<a href="#/page2">page2</a>
<a href="#/page3">page3</a>
</div>
<div id="container"></div>
</body>
let router = new HashRouter();
let container = document.getElementById('container');
//注册首页回调函数
router.registerIndex(()=> container.innerHTML = '我是首页');
//注册其他视图回到函数
router.register('/page1',()=> container.innerHTML = '我是page1');
router.register('/page2',()=> container.innerHTML = '我是page2');
router.register('/page3',()=> container.innerHTML = '我是page3');
//加载视图
router.load();
Взгляните на эффект:
Мы реализовали базовую функцию маршрутизации, но есть небольшие проблемы.
- Отсутствует обработка хеш-значений, не прописанных в маршрутах
- Функция обратного вызова, соответствующая хэш-значению, выдает исключение во время выполнения.
Соответствующие решения следующие:
- Мы добавляем метод registerNotFound для регистрации функции обратного вызова по умолчанию, когда хэш-значение не найдено;
- Измените метод нагрузки, добавьте TRY / CALL, чтобы получить исключения, добавьте метод reperireError для обработки исключений
После модификации кода:
class HashRouter{
constructor(){
//用于存储不同hash值对应的回调函数
this.routers = {};
window.addEventListener('hashchange',this.load.bind(this),false)
}
//用于注册每个视图
register(hash,callback = function(){}){
this.routers[hash] = callback;
}
//用于注册首页
registerIndex(callback = function(){}){
this.routers['index'] = callback;
}
//用于处理视图未找到的情况
registerNotFound(callback = function(){}){
this.routers['404'] = callback;
}
//用于处理异常情况
registerError(callback = function(){}){
this.routers['error'] = callback;
}
//用于调用不同视图的回调函数
load(){
let hash = location.hash.slice(1),
handler;
//没有hash 默认为首页
if(!hash){
handler = this.routers.index;
}
//未找到对应hash值
else if(!this.routers.hasOwnProperty(hash)){
handler = this.routers['404'] || function(){};
}
else{
handler = this.routers[hash]
}
//执行注册的回调函数
try{
handler.apply(this);
}catch(e){
console.error(e);
(this.routers['error'] || function(){}).call(this,e);
}
}
}
Другой пример для демонстрации:
<body>
<div id="nav">
<a href="#/page1">page1</a>
<a href="#/page2">page2</a>
<a href="#/page3">page3</a>
<a href="#/page4">page4</a>
<a href="#/page5">page5</a>
</div>
<div id="container"></div>
</body>
let router = new HashRouter();
let container = document.getElementById('container');
//注册首页回调函数
router.registerIndex(()=> container.innerHTML = '我是首页');
//注册其他视图回到函数
router.register('/page1',()=> container.innerHTML = '我是page1');
router.register('/page2',()=> container.innerHTML = '我是page2');
router.register('/page3',()=> container.innerHTML = '我是page3');
router.register('/page4',()=> {throw new Error('抛出一个异常')});
//加载视图
router.load();
//注册未找到对应hash值时的回调
router.registerNotFound(()=>container.innerHTML = '页面未找到');
//注册出现异常时的回调
router.registerError((e)=>container.innerHTML = '页面异常,错误消息:<br>' + e.message);
Взгляните на эффект:
На данный момент на основе внешней маршрутизации, реализованной с помощью хэша, мы завершили базовый прототип.
Далее давайте представим еще один режим внешней маршрутизации: режим истории.
режим истории
Перед HTML5 браузеры имели объект истории. Но в ранней истории его можно использовать только для нескольких страничных прыжков:
history.go(-1); // 后退一页
history.go(2); // 前进两页
history.forward(); // 前进一页
history.back(); // 后退一页
В спецификации HTML5 история добавляет следующие API:
history.pushState(); // 添加新的状态到历史状态栈
history.replaceState(); // 用新的状态代替当前状态
history.state // 返回当前状态对象
отMDNобъяснение:
В HTML5 появились методы history.pushState() и history.replaceState(), которые соответственно добавляют и изменяют записи истории. Эти методы обычно используются в сочетании с window.onpopstate.
history.pushState() и history.replaceState() получают по три параметра (состояние, заголовок, URL)
Описание параметра выглядит следующим образом:
- Состояние: действующий объект JavaScript, который можно использовать в популярных событиях
- title: Теперь большинство браузеров игнорируют этот параметр, вместо него вы можете напрямую использовать null
- url: любой допустимый URL для обновления адресной строки браузера
Разница между history.pushState() и history.replaceState() заключается в следующем:
- history.pushState() добавляет URL-адрес в историю, сохраняя при этом существующую историю.
- history.replaceState() заменит текущую историю страниц в истории URL-адресом.
Так как history.pushstate() и history.replaceState() могут менять URL, страница не обновляется, поэтому Histroy в HTML5 имеет возможность реализовать front-end маршрутизацию.
Вспоминая режим хеширования, который мы выполнили ранее, когда хеш изменяется, его можно отслеживать через hashchange. Изменение истории не вызывает никаких событий, поэтому мы не можем напрямую отслеживать изменение истории и вносить соответствующие изменения.
Поэтому нам нужно изменить свое мышление, мы можем перечислить все ситуации, которые могут спровоцировать изменение истории, и перехватывать эти методы один за другим, и замаскированным образом отслеживать изменение истории.
Для режима истории одностраничного приложения изменение URL-адреса может быть вызвано только следующими четырьмя способами:
- Нажмите кнопку браузера вперед или назад
- щелкните вкладку
- Активировать функцию history.pushState в коде JS
- Активировать функцию history.replaceState в коде JS
Идея уже есть, давайте реализуем объект маршрутизации
- Создайте объект маршрута и реализуйте метод register для регистрации функции обратного вызова, соответствующей каждому значению location.pathname.
- Когда location.pathname === '/', это считается домашней страницей, поэтому реализуйте метод registerIndex для функции обратного вызова при регистрации домашней страницы.
- Чтобы решить проблему отсутствия соответствующего совпадения в location.path, добавьте метод registerNotFound для регистрации функции обратного вызова по умолчанию.
- Устраните исключение при выполнении зарегистрированной функции возврата и добавьте метод registerError для обработки нештатной ситуации.
class HistoryRouter{
constructor(){
//用于存储不同path值对应的回调函数
this.routers = {};
}
//用于注册每个视图
register(path,callback = function(){}){
this.routers[path] = callback;
}
//用于注册首页
registerIndex(callback = function(){}){
this.routers['/'] = callback;
}
//用于处视图未找到的情况
registerNotFound(callback = function(){}){
this.routers['404'] = callback;
}
//用于处理异常情况
registerError(callback = function(){}){
this.routers['error'] = callback;
}
}
- Определите метод assign для запуска функции history.pushState через JS.
- Определите метод замены, чтобы вызвать функцию astistram.replacstate через js
class HistoryRouter{
constructor(){
//用于存储不同path值对应的回调函数
this.routers = {};
}
//用于注册每个视图
register(path,callback = function(){}){
this.routers[path] = callback;
}
//用于注册首页
registerIndex(callback = function(){}){
this.routers['/'] = callback;
}
//用于处理视图未找到的情况
registerNotFound(callback = function(){}){
this.routers['404'] = callback;
}
//用于处理异常情况
registerError(callback = function(){}){
this.routers['error'] = callback;
}
//跳转到path
assign(path){
history.pushState({path},null,path);
this.dealPathHandler(path)
}
//替换为path
replace(path){
history.replaceState({path},null,path);
this.dealPathHandler(path)
}
//通用处理 path 调用回调函数
dealPathHandler(path){
let handler;
//没有对应path
if(!this.routers.hasOwnProperty(path)){
handler = this.routers['404'] || function(){};
}
//有对应path
else{
handler = this.routers[path];
}
try{
handler.call(this)
}catch(e){
console.error(e);
(this.routers['error'] || function(){}).call(this,e);
}
}
}
- Вызовите соответствующую функцию обратного вызова для прослушивания поптации для обработки вперед и назад, когда
- Глобально заблокируйте событие по умолчанию ссылки A, получите атрибут href ссылки A и вызовите метод history.pushState.
- Определите метод загрузки для вызова соответствующей функции обратного вызова в соответствии с location.pathname при первом входе на страницу.
Окончательный код выглядит следующим образом:
class HistoryRouter{
constructor(){
//用于存储不同path值对应的回调函数
this.routers = {};
this.listenPopState();
this.listenLink();
}
//监听popstate
listenPopState(){
window.addEventListener('popstate',(e)=>{
let state = e.state || {},
path = state.path || '';
this.dealPathHandler(path)
},false)
}
//全局监听A链接
listenLink(){
window.addEventListener('click',(e)=>{
let dom = e.target;
if(dom.tagName.toUpperCase() === 'A' && dom.getAttribute('href')){
e.preventDefault()
this.assign(dom.getAttribute('href'));
}
},false)
}
//用于首次进入页面时调用
load(){
let path = location.pathname;
this.dealPathHandler(path)
}
//用于注册每个视图
register(path,callback = function(){}){
this.routers[path] = callback;
}
//用于注册首页
registerIndex(callback = function(){}){
this.routers['/'] = callback;
}
//用于处理视图未找到的情况
registerNotFound(callback = function(){}){
this.routers['404'] = callback;
}
//用于处理异常情况
registerError(callback = function(){}){
this.routers['error'] = callback;
}
//跳转到path
assign(path){
history.pushState({path},null,path);
this.dealPathHandler(path)
}
//替换为path
replace(path){
history.replaceState({path},null,path);
this.dealPathHandler(path)
}
//通用处理 path 调用回调函数
dealPathHandler(path){
let handler;
//没有对应path
if(!this.routers.hasOwnProperty(path)){
handler = this.routers['404'] || function(){};
}
//有对应path
else{
handler = this.routers[path];
}
try{
handler.call(this)
}catch(e){
console.error(e);
(this.routers['error'] || function(){}).call(this,e);
}
}
}
Еще один пример для демонстрации HistoryRouter, который мы только что закончили.
<body>
<div id="nav">
<a href="/page1">page1</a>
<a href="/page2">page2</a>
<a href="/page3">page3</a>
<a href="/page4">page4</a>
<a href="/page5">page5</a>
<button id="btn">page2</button>
</div>
<div id="container">
</div>
</body>
let router = new HistoryRouter();
let container = document.getElementById('container');
//注册首页回调函数
router.registerIndex(() => container.innerHTML = '我是首页');
//注册其他视图回到函数
router.register('/page1', () => container.innerHTML = '我是page1');
router.register('/page2', () => container.innerHTML = '我是page2');
router.register('/page3', () => container.innerHTML = '我是page3');
router.register('/page4', () => {
throw new Error('抛出一个异常')
});
document.getElementById('btn').onclick = () => router.assign('/page2')
//注册未找到对应path值时的回调
router.registerNotFound(() => container.innerHTML = '页面未找到');
//注册出现异常时的回调
router.registerError((e) => container.innerHTML = '页面异常,错误消息:<br>' + e.message);
//加载页面
router.load();
Взгляните на эффект:
На данный момент, на основе внешней маршрутизации, реализованной историей, мы завершили базовый прототип.
Однако следует отметить, что история изменяет URL-адрес, хотя страница не обновляется, но мы обновляем вручную или напрямую входим в приложение через URL-адрес, Сервер не может идентифицировать этот URL. Поскольку мы одностраничное приложение, только один HTML-файл, сервер появится, когда появятся URL-адреса других путей. Итак, если вы хотите применить режим истории, вам нужно добавить ресурс-кандидат, перезаписывающий все ситуации на сервере: Если URL-адрес не соответствует ни одному из статических ресурсов, вы должны вернуть файл HTML для одностраничного приложения.
Далее давайте рассмотрим, когда использовать режим хеширования и когда использовать режим истории.
Как выбрать между хэшем и историей
Преимущества режима хеширования по сравнению с режимом истории:
- Лучшая совместимость, совместимость с IE8
- Серверу не нужно взаимодействовать с URL-адресом не одной страницы.
Недостатки хеш-режима по сравнению с режимом истории:
- выглядеть некрасивее.
- Это приведет к сбою функции привязки.
- Одно и то же хеш-значение не запускает действия по добавлению записей в стек истории, в отличие от pushState.
Подводя итог, когда нам не нужна совместимость со старой версией браузера IE и мы можем контролировать ресурсы-кандидаты, которые сервер покрывает во всех случаях, мы можем с удовольствием использовать режим истории.
Наоборот, к сожалению, можно использовать только режим уродливого хеширования~
конец
В этой статье просто анализируется и реализуется режим хеширования и режим истории в одностраничной маршрутизации.Конечно, это слишком просто по сравнению с vue-router и react-router.Будет выполнен анализ исходного кода vue-router и react-router в дальнейшем.Статья вводится постепенно.
Рекомендуемая серия статей
- «Front-end Advanced» полностью понимает каррирование функций.
- Память кучи памяти стека "Front-end advanced" в JS
- «Расширенное переднее» управление памятью в JS
- Массив "Front-end advanced" вышел из строя
Ссылаться на
напиши в конце
- Если в статье есть ошибки, исправьте их в комментариях, если статья вам поможет, добро пожаловать
点赞
а также关注
- Эта статья была впервые опубликована одновременно сgithub, доступны наgithubНайдите больше отличных статей в
Watch
&Star ★
- Для последующих статей см.:строить планы
Добро пожаловать внимание на общедоступный номер микроканала
【前端小黑屋】
, 1–3 высококачественные высококачественные статьи публикуются каждую неделю, чтобы помочь вам в продвижении вперед.