各位观众老爷,晚上好!今天咱们来聊聊 Vue Router 里的两位老朋友:createWebHistory
和 createWebHashHistory
。这两位,一个用正经的 URL,一个用“不正经”的 URL(带 #
),但都能让你的 Vue 应用实现页面跳转,背后到底藏着什么猫腻呢? 咱们今天就来扒一扒它们的底裤(源码),看看它们是怎么监听 URL 变化的。
开场白:历史模式与哈希模式,两个世界的选择
首先,简单回顾一下这两种模式的区别:
- Web History 模式 (createWebHistory):使用标准的 URL,看起来更“优雅”,例如
https://example.com/about
。需要服务器端的配置来支持,否则刷新页面可能会出现 404 错误。 - Web Hash 模式 (createWebHashHistory):URL 中带有一个
#
符号,例如https://example.com/#/about
。不需要服务器端配置,因为#
后面的内容不会发送到服务器。
选择哪种模式取决于你的需求和服务器环境。如果你的服务器支持 History API,那么 History 模式是更好的选择。否则,Hash 模式是一个安全的选择。
第一幕:createWebHistory
的身世之谜
createWebHistory
返回一个 history 对象,这个对象包含了一系列的方法和属性,用于操作浏览器的历史记录。我们先来看看它的主要实现部分:
// 简化后的 createWebHistory 实现
function createWebHistory(base) {
base = normalizeBase(base); // 规范化 base URL
const history = {
location: getCurrentLocation(base), // 获取当前 URL
state: historyState, // 获取历史状态
push: (to, data) => {
const targetLocation = resolve(to); // 解析 to 地址
pushWithRedirect(targetLocation, data); // 执行 push 操作
},
replace: (to, data) => {
const targetLocation = resolve(to);
replaceWithRedirect(targetLocation, data); // 执行 replace 操作
},
go: (delta) => {
window.history.go(delta); // 调用浏览器的 go 方法
},
back: () => history.go(-1), // 返回上一页
forward: () => history.go(1), // 前进到下一页
listen: (callback) => {
const removeListener = popStateHandler.listen(callback); // 监听 popstate 事件
return removeListener;
},
destroy: () => {
popStateHandler.destroy();
},
};
//初始化页面状态
if (historyState === START) {
replace(history.location)
}
return history;
}
// 规范化 base URL
function normalizeBase(base) {
if (!base) {
return '/';
}
if (base.startsWith('.')) {
base = new URL(base, document.baseURI).pathname;
}
if (!base.endsWith('/')) {
base += '/';
}
return base;
}
// 获取当前 URL
function getCurrentLocation(base) {
const raw = window.location.pathname;
// Remove base first
if (base !== '/' && raw.startsWith(base)) {
return raw.slice(base.length);
}
return raw;
}
// 解析 to 地址
function resolve(to) {
if (typeof to === 'string') {
return to;
}
if (typeof to === 'object' && to != null) {
const { path, query, hash } = to;
let target = path;
if (query) {
target += '?' + new URLSearchParams(query).toString();
}
if (hash) {
target += '#' + hash;
}
return target;
}
// 不支持的类型直接返回空
return '';
}
//push 操作,如果需要重定向,则处理重定向
function pushWithRedirect(to, data){
try {
// This will fail on file:// because the security settings
window.history.pushState(data, '', base + to);
history.location = to;
history.state = data;
popStateHandler.onPopState({ state: data, path: to });
} catch (error) {
//如果出错,调用replace
replaceWithRedirect(to, data)
}
}
//replace 操作,如果需要重定向,则处理重定向
function replaceWithRedirect(to, data){
try {
window.history.replaceState(data, '', base + to);
history.location = to;
history.state = data;
popStateHandler.onPopState({ state: data, path: to });
} catch (error) {
window.location.replace(base + to);
}
}
const START = Symbol()
let historyState = START
const popStateHandler = {
listeners: [],
listen(callback) {
this.listeners.push(callback);
return () => {
this.listeners = this.listeners.filter((l) => l !== callback);
};
},
onPopState(event) {
const to = getCurrentLocation(base);
const from = history.location;
history.location = to
this.listeners.forEach((callback) => {
callback(to, from, event.state);
});
},
destroy() {
window.removeEventListener('popstate', this.onPopState);
},
};
window.addEventListener('popstate', (event) => {
popStateHandler.onPopState(event);
});
代码解读:
-
normalizeBase(base)
: 这个函数负责规范化base
URL。base
是你的应用部署的根路径,例如/app/
。规范化的过程包括:- 如果
base
为空,则设置为/
。 - 如果
base
以.
开头,则将其解析为绝对路径。 - 如果
base
不以/
结尾,则添加/
。
- 如果
-
getCurrentLocation(base)
: 这个函数用于获取当前 URL,并移除base
部分。例如,如果当前 URL 是https://example.com/app/about
,base
是/app/
,那么getCurrentLocation
会返回about
。 -
push(to, data)
和replace(to, data)
: 这两个函数分别用于向历史记录中添加一个新的条目和替换当前的条目。它们的核心是调用window.history.pushState(data, '', base + to)
和window.history.replaceState(data, '', base + to)
。这两个方法是 HTML5 History API 的一部分,它们允许你修改 URL 而无需重新加载页面。data
参数用于存储与该历史记录条目相关的数据,可以在popstate
事件中访问。 -
go(delta)
、back()
和forward()
: 这些函数用于在历史记录中前进或后退。它们直接调用window.history.go(delta)
,其中delta
是一个整数,表示要前进或后退的步数。back()
和forward()
只是go()
的简写形式。 -
resolve(to)
: 这个函数用来标准化to的格式,to可以是一个字符串,也可以是一个对象,如果是对象则将其解析为字符串路径。 -
listen(callback)
: 重点来了!这个函数用于监听 URL 的变化。它通过监听popstate
事件来实现。popstate
事件在浏览器的历史记录发生变化时触发,例如用户点击了浏览器的前进或后退按钮。listen
函数将callback
添加到popStateHandler.listeners
数组中,当popstate
事件发生时,popStateHandler.onPopState
会调用所有注册的回调函数。 -
popStateHandler
对象: 这个对象负责管理popstate
事件的监听器。它包含一个listeners
数组,用于存储所有的回调函数。onPopState
方法在popstate
事件发生时被调用,它会遍历listeners
数组,并调用每个回调函数,将新的 URL 和历史状态传递给它们。 -
初始化逻辑: 如果
historyState === START
,表示页面是第一次加载,那么会调用replace
方法,用当前的 URL 替换历史记录中的第一个条目。这样做的目的是为了防止用户在刷新页面后,点击后退按钮会返回到空白页面。
popstate
事件:监听 URL 变化的秘密武器
popstate
事件是 createWebHistory
监听 URL 变化的关键。当用户点击浏览器的前进或后退按钮,或者调用 history.go()
、history.back()
或 history.forward()
方法时,浏览器会触发 popstate
事件。
createWebHistory
通过监听 popstate
事件,可以感知到 URL 的变化,并执行相应的操作,例如更新 Vue 应用的状态。
第二幕:createWebHashHistory
的另类人生
与 createWebHistory
不同,createWebHashHistory
使用 URL 中的 #
符号来模拟页面跳转。我们来看看它的实现:
// 简化后的 createWebHashHistory 实现
function createWebHashHistory(base) {
base = normalizeBase(base);
const history = {
location: getHash(), // 获取当前 hash 值
state: historyState,
push: (to, data) => {
writeHash(to, data);
},
replace: (to, data) => {
writeHash(to, data, true);
},
go: (delta) => {
window.history.go(delta);
},
back: () => history.go(-1),
forward: () => history.go(1),
listen: (callback) => {
const removeListener = hashChangeHandler.listen(callback);
return removeListener;
},
destroy: () => {
hashChangeHandler.destroy()
},
};
//初始化页面状态
if (historyState === START) {
replace(history.location)
}
return history;
}
function normalizeBase(base) {
if (!base) {
return '/';
}
if (base.startsWith('.')) {
base = new URL(base, document.baseURI).pathname;
}
if (!base.endsWith('/')) {
base += '/';
}
return base;
}
function getHash() {
let href = window.location.href;
const hashIndex = href.indexOf('#');
if (hashIndex < 0) {
return '/';
}
href = href.slice(hashIndex + 1);
return href;
}
function writeHash(path, data, replace) {
if (replace === true) {
window.location.replace(getHashBaseURL() + '#' + path);
} else {
window.location.hash = path;
}
history.location = path;
history.state = data;
hashChangeHandler.onHashChange({newURL: getHashBaseURL() + '#' + path, oldURL: getHashBaseURL() + '#' + history.location});
}
function getHashBaseURL() {
const href = window.location.href;
const hashIndex = href.indexOf('#');
return hashIndex < 0 ? href : href.slice(0, hashIndex);
}
const START = Symbol()
let historyState = START
const hashChangeHandler = {
listeners: [],
listen(callback) {
this.listeners.push(callback);
return () => {
this.listeners = this.listeners.filter((l) => l !== callback);
};
},
onHashChange(event) {
const to = getHash();
const from = history.location;
history.location = to;
this.listeners.forEach((callback) => {
callback(to, from, event.state);
});
},
destroy() {
window.removeEventListener('hashchange', this.onHashChange)
},
};
window.addEventListener('hashchange', (event) => {
hashChangeHandler.onHashChange(event);
});
代码解读:
-
getHash()
: 这个函数用于获取当前 URL 中的 hash 值。它通过查找#
符号的位置来提取 hash 值。 -
writeHash(path, data, replace)
: 这个函数用于修改 URL 中的 hash 值。它直接修改window.location.hash
属性。如果replace
参数为true
,则使用window.location.replace
方法,这样可以避免在历史记录中添加新的条目。 -
listen(callback)
: 与createWebHistory
类似,这个函数用于监听 URL 的变化。但是,createWebHashHistory
监听的是hashchange
事件。hashchange
事件在 URL 中的 hash 值发生变化时触发。 -
hashChangeHandler
对象: 这个对象负责管理hashchange
事件的监听器。它的工作方式与popStateHandler
类似。 -
getHashBaseURL()
: 获取#号前面的链接
hashchange
事件:监听 Hash 变化的利器
hashchange
事件是 createWebHashHistory
监听 URL 变化的关键。当用户修改 URL 中的 hash 值时,浏览器会触发 hashchange
事件。
createWebHashHistory
通过监听 hashchange
事件,可以感知到 URL 的变化,并执行相应的操作。
第三幕:URL 变化监听机制大PK
特性 | createWebHistory |
createWebHashHistory |
---|---|---|
使用的 API | History API | window.location.hash |
监听的事件 | popstate |
hashchange |
URL 形式 | 标准 URL | 带 # 的 URL |
服务器端配置需求 | 需要 | 不需要 |
总结:
createWebHistory
和 createWebHashHistory
都提供了监听 URL 变化的能力,但它们使用了不同的 API 和事件。createWebHistory
使用 History API 和 popstate
事件,可以创建更“优雅”的 URL,但需要服务器端的配置支持。createWebHashHistory
使用 window.location.hash
和 hashchange
事件,不需要服务器端的配置,但 URL 中会带有一个 #
符号。
希望今天的讲解能帮助你更好地理解 Vue Router 的内部机制。下次再见!