Location 与 History 对象:URL 操作与历史记录管理
导航双子星
在浏览器的导航系统中,location 和 history 是一对密切配合的对象。location 负责当前页面的 URL 信息和导航操作——告诉你"现在在哪里、要去哪里";history 则负责浏览历史记录的管理——告诉你"从哪里来、能否回去"。
这两个对象的配合,就像汽车的导航系统和行车记录仪:
- Location:实时导航,知道当前位置和目的地,可以立即前往新地点
- History:行车记录,记录走过的路线,可以原路返回
掌握这两个对象,是实现现代 Web 应用导航的关键,特别是单页应用(SPA)的路由管理。
Location 对象详解
location 对象包含当前页面的 URL 信息,并提供了导航到新页面的方法。
URL 组成部分
一个完整的 URL 包含多个部分,location 对象将它们分门别类:
https://user:[email protected]:8080/path/page.html?id=123&name=test#section
└─┬─┘ └───┬───┘ └────┬──────┘└┬─┘ └──────┬───────┘ └─────┬──────┘ └──┬───┘
protocol auth hostname port pathname search hash
└────────────┬────────────┘
host让我们看看如何访问这些部分:
javascript
// 假设当前 URL 是:
// https://example.com:8080/products/laptop.html?id=123&color=blue#specs
// 完整 URL
console.log(location.href);
// 'https://example.com:8080/products/laptop.html?id=123&color=blue#specs'
// 协议(包括冒号)
console.log(location.protocol);
// 'https:'
// 主机名 + 端口
console.log(location.host);
// 'example.com:8080'
// 主机名(不包括端口)
console.log(location.hostname);
// 'example.com'
// 端口
console.log(location.port);
// '8080'
// 路径(从根目录开始)
console.log(location.pathname);
// '/products/laptop.html'
// 查询字符串(包括问号)
console.log(location.search);
// '?id=123&color=blue'
// 锚点/哈希(包括井号)
console.log(location.hash);
// '#specs'
// 来源(协议 + 主机名 + 端口)
console.log(location.origin);
// 'https://example.com:8080'实际应用:URL 解析器
javascript
class URLParser {
constructor(url = window.location.href) {
this.url = new URL(url);
}
// 获取所有 URL 信息
getAll() {
return {
href: this.url.href,
protocol: this.url.protocol,
host: this.url.host,
hostname: this.url.hostname,
port: this.url.port,
pathname: this.url.pathname,
search: this.url.search,
hash: this.url.hash,
origin: this.url.origin,
};
}
// 解析查询参数
getParams() {
const params = {};
for (const [key, value] of this.url.searchParams.entries()) {
// 处理重复的参数名
if (params[key]) {
if (Array.isArray(params[key])) {
params[key].push(value);
} else {
params[key] = [params[key], value];
}
} else {
params[key] = value;
}
}
return params;
}
// 获取单个参数
getParam(name) {
return this.url.searchParams.get(name);
}
// 获取所有同名参数
getAllParams(name) {
return this.url.searchParams.getAll(name);
}
// 检查参数是否存在
hasParam(name) {
return this.url.searchParams.has(name);
}
// 格式化显示
display() {
console.log("=== URL 信息 ===");
const info = this.getAll();
Object.entries(info).forEach(([key, value]) => {
if (value) {
console.log(`${key}: ${value}`);
}
});
const params = this.getParams();
if (Object.keys(params).length > 0) {
console.log("\n=== 查询参数 ===");
Object.entries(params).forEach(([key, value]) => {
console.log(`${key}: ${value}`);
});
}
}
}
// 使用
const parser = new URLParser(
"https://example.com/search?q=javascript&category=tutorials&sort=date"
);
parser.display();
console.log("查询词:", parser.getParam("q")); // 'javascript'
console.log("分类:", parser.getParam("category")); // 'tutorials'
console.log("所有参数:", parser.getParams());
// { q: 'javascript', category: 'tutorials', sort: 'date' }查询参数操作
javascript
class QueryParams {
// 解析查询字符串
static parse(search = location.search) {
const params = new URLSearchParams(search);
const result = {};
for (const [key, value] of params.entries()) {
result[key] = value;
}
return result;
}
// 构建查询字符串
static stringify(params) {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((v) => searchParams.append(key, v));
} else if (value !== null && value !== undefined) {
searchParams.append(key, value);
}
});
return searchParams.toString();
}
// 添加或更新参数
static add(name, value) {
const params = this.parse();
params[name] = value;
return this.stringify(params);
}
// 删除参数
static remove(name) {
const params = this.parse();
delete params[name];
return this.stringify(params);
}
// 批量更新参数
static update(updates) {
const params = { ...this.parse(), ...updates };
return this.stringify(params);
}
// 清空所有参数
static clear() {
return "";
}
}
// 使用示例
// 当前 URL: https://example.com/products?category=laptop&price=1000
// 解析当前参数
const params = QueryParams.parse();
console.log("当前参数:", params);
// { category: 'laptop', price: '1000' }
// 添加新参数
const newQuery = QueryParams.add("color", "blue");
console.log("添加 color:", newQuery);
// 'category=laptop&price=1000&color=blue'
// 批量更新参数
const updatedQuery = QueryParams.update({
price: "1500",
brand: "TechCorp",
});
console.log("更新后:", updatedQuery);
// 'category=laptop&price=1500&brand=TechCorp'
// 删除参数
const queryWithoutPrice = QueryParams.remove("price");
console.log("删除 price:", queryWithoutPrice);
// 'category=laptop&brand=TechCorp'页面导航方法
location 对象提供了几种方式来导航到新页面:
javascript
// 1. 直接修改 href(最常用)
location.href = "https://example.com/newpage";
// 2. assign() 方法(与修改 href 等效)
location.assign("https://example.com/newpage");
// 3. replace() 方法(不在历史记录中留下记录)
location.replace("https://example.com/newpage");
// 4. reload() 方法(重新加载当前页面)
location.reload(); // 可能从缓存加载
location.reload(true); // 强制从服务器加载(已废弃,效果不保证)
// 对比 assign 和 replace
function navigateWithAssign() {
location.assign("/page2");
// 用户可以点击后退按钮返回
}
function navigateWithReplace() {
location.replace("/page2");
// 用户点击后退按钮会跳过当前页面
}实际应用:导航管理器
javascript
class NavigationManager {
// 安全导航(检查 URL)
static navigateTo(url, options = {}) {
const {
newTab = false,
replace = false,
confirm = false,
confirmMessage = "确定要离开当前页面吗?",
} = options;
// 可选的确认对话框
if (confirm && !window.confirm(confirmMessage)) {
return false;
}
// 在新标签页打开
if (newTab) {
window.open(url, "_blank", "noopener,noreferrer");
return true;
}
// 替换当前历史记录
if (replace) {
location.replace(url);
} else {
location.assign(url);
}
return true;
}
// 导航到相对路径
static navigateToPath(path, options = {}) {
const url = `${location.origin}${path}`;
return this.navigateTo(url, options);
}
// 带参数导航
static navigateWithParams(path, params, options = {}) {
const queryString = QueryParams.stringify(params);
const url = queryString ? `${path}?${queryString}` : path;
return this.navigateToPath(url, options);
}
// 更新 URL 参数(不刷新页面)
static updateParams(params, options = {}) {
const { replace = false } = options;
const newQuery = QueryParams.update(params);
const newUrl = `${location.pathname}${newQuery ? "?" + newQuery : ""}${
location.hash
}`;
if (replace) {
history.replaceState(null, "", newUrl);
} else {
history.pushState(null, "", newUrl);
}
}
// 重新加载页面(带选项)
static reloadPage(options = {}) {
const {
clearCache = false,
confirm = false,
confirmMessage = "确定要重新加载页面吗?",
} = options;
if (confirm && !window.confirm(confirmMessage)) {
return false;
}
if (clearCache) {
// 添加时间戳来绕过缓存
const timestamp = Date.now();
const separator = location.search ? "&" : "?";
location.href = `${location.pathname}${location.search}${separator}_t=${timestamp}${location.hash}`;
} else {
location.reload();
}
return true;
}
// 导航到锚点
static scrollToHash(hash, smooth = true) {
const element = document.getElementById(hash.replace("#", ""));
if (element) {
element.scrollIntoView({
behavior: smooth ? "smooth" : "auto",
block: "start",
});
// 更新 URL 的 hash,不触发页面跳转
if (history.pushState) {
history.pushState(null, "", `#${hash.replace("#", "")}`);
} else {
location.hash = hash;
}
}
}
}
// 使用示例
// 导航到新页面
NavigationManager.navigateTo("/products");
// 在新标签页打开
NavigationManager.navigateTo("https://example.com", { newTab: true });
// 带确认的导航
NavigationManager.navigateTo("/logout", {
confirm: true,
confirmMessage: "确定要退出登录吗?",
replace: true,
});
// 带参数导航
NavigationManager.navigateWithParams("/search", {
q: "javascript",
category: "tutorials",
page: 1,
});
// 更新 URL 参数(不刷新页面)
NavigationManager.updateParams({ page: 2, sort: "date" });
// 重新加载并清除缓存
NavigationManager.reloadPage({ clearCache: true });
// 平滑滚动到锚点
NavigationManager.scrollToHash("section-2");History 对象详解
history 对象管理浏览器的会话历史记录,提供了在历史记录中前进和后退的方法,以及 HTML5 新增的状态管理 API。
基本导航方法
javascript
// 历史记录长度
console.log(history.length); // 当前会话的历史记录数量
// 后退一页(相当于点击浏览器的后退按钮)
history.back();
// 前进一页(相当于点击浏览器的前进按钮)
history.forward();
// 跳转到指定位置
history.go(-1); // 后退一页(等同于 back())
history.go(1); // 前进一页(等同于 forward())
history.go(-2); // 后退两页
history.go(0); // 刷新当前页面
// 实际应用:导航按钮
function createNavigationButtons() {
const backBtn = document.getElementById("backBtn");
const forwardBtn = document.getElementById("forwardBtn");
backBtn.addEventListener("click", () => {
if (history.length > 1) {
history.back();
} else {
console.log("没有可返回的页面");
}
});
forwardBtn.addEventListener("click", () => {
history.forward();
});
}HTML5 History API
HTML5 引入了强大的 History API,允许在不刷新页面的情况下管理历史记录和 URL,这是实现单页应用(SPA)路由的基础。
pushState() - 添加历史记录
javascript
// pushState() 添加新的历史记录条目
// 语法: history.pushState(state, title, url)
// 添加简单的历史记录
history.pushState(
{ page: 1 }, // state: 状态对象(可以存储任何数据)
"", // title: 标题(大多数浏览器目前忽略此参数)
"/page1" // url: 新的 URL(必须同源)
);
// 添加带复杂状态的记录
history.pushState(
{
page: "products",
filter: { category: "laptop", price: 1000 },
timestamp: Date.now(),
},
"",
"/products?category=laptop&price=1000"
);
// 注意:pushState 不会触发页面刷新
console.log("URL 已更新,但页面没有刷新");replaceState() - 替换历史记录
javascript
// replaceState() 替换当前的历史记录条目
// 语法: history.replaceState(state, title, url)
history.replaceState({ page: "home" }, "", "/home");
// 与 pushState 的区别:
// - pushState: 添加新记录,可以后退到上一个页面
// - replaceState: 替换当前记录,无法后退到上一个页面state 属性
javascript
// 获取当前历史记录的状态对象
console.log(history.state); // 最近一次 pushState/replaceState 设置的状态
// 示例
history.pushState({ user: "John", id: 123 }, "", "/user/123");
console.log(history.state); // { user: 'John', id: 123 }popstate 事件
javascript
// 监听历史记录变化(用户点击后退/前进按钮时触发)
window.addEventListener("popstate", (event) => {
console.log("历史记录改变");
console.log("新的状态:", event.state);
console.log("新的 URL:", location.href);
// 根据状态更新页面内容
if (event.state) {
updatePageContent(event.state);
}
});
function updatePageContent(state) {
console.log("根据状态更新页面:", state);
// 实际应用中,这里会根据状态渲染相应的页面内容
}实际应用:简单的 SPA 路由器
javascript
class SimpleRouter {
constructor(routes) {
this.routes = routes; // { '/': handler, '/about': handler }
this.currentPath = null;
this.init();
}
init() {
// 监听浏览器前进/后退
window.addEventListener("popstate", (event) => {
this.handleRoute(location.pathname, event.state);
});
// 拦截链接点击
document.addEventListener("click", (event) => {
// 检查是否点击了带 data-link 属性的元素
const link = event.target.closest("[data-link]");
if (link) {
event.preventDefault();
const path = link.getAttribute("href");
this.navigate(path);
}
});
// 处理初始路由
this.handleRoute(location.pathname);
}
navigate(path, state = {}) {
// 避免重复导航
if (path === this.currentPath) {
return;
}
// 更新历史记录
history.pushState(state, "", path);
// 处理路由
this.handleRoute(path, state);
}
replace(path, state = {}) {
// 替换当前历史记录
history.replaceState(state, "", path);
this.handleRoute(path, state);
}
handleRoute(path, state = null) {
console.log(`路由变化: ${path}`);
this.currentPath = path;
// 查找匹配的路由
const handler = this.routes[path] || this.routes["/404"];
if (handler) {
// 调用路由处理器
handler(state);
} else {
console.error(`找不到路由: ${path}`);
}
}
back() {
history.back();
}
forward() {
history.forward();
}
}
// 使用示例
const router = new SimpleRouter({
"/": (state) => {
console.log("首页");
document.getElementById("app").innerHTML = `
<h1>首页</h1>
<nav>
<a href="/about" data-link>关于</a>
<a href="/products" data-link>产品</a>
<a href="/contact" data-link>联系</a>
</nav>
`;
},
"/about": (state) => {
console.log("关于页");
document.getElementById("app").innerHTML = `
<h1>关于我们</h1>
<p>欢迎来到 TechCorp</p>
<a href="/" data-link>返回首页</a>
`;
},
"/products": (state) => {
console.log("产品页", state);
const filter = state?.filter || {};
document.getElementById("app").innerHTML = `
<h1>产品列表</h1>
<p>当前筛选: ${JSON.stringify(filter)}</p>
<button id="laptopBtn">笔记本电脑</button>
<button id="phoneBtn">手机</button>
<a href="/" data-link>返回首页</a>
`;
document.getElementById("laptopBtn")?.addEventListener("click", () => {
router.navigate("/products", {
filter: { category: "laptop" },
});
});
document.getElementById("phoneBtn")?.addEventListener("click", () => {
router.navigate("/products", {
filter: { category: "phone" },
});
});
},
"/contact": (state) => {
console.log("联系页");
document.getElementById("app").innerHTML = `
<h1>联系我们</h1>
<p>Email: [email protected]</p>
<a href="/" data-link>返回首页</a>
`;
},
"/404": (state) => {
console.log("404 页面");
document.getElementById("app").innerHTML = `
<h1>404 - 页面未找到</h1>
<a href="/" data-link>返回首页</a>
`;
},
});
// 编程式导航
// router.navigate('/about');
// router.navigate('/products', { filter: { category: 'laptop' } });实际应用:高级路由管理器
javascript
class Router {
constructor() {
this.routes = [];
this.beforeHooks = [];
this.afterHooks = [];
this.currentRoute = null;
this.init();
}
init() {
window.addEventListener("popstate", (event) => {
this.handleRoute(location.pathname, event.state);
});
document.addEventListener("click", (event) => {
const link = event.target.closest("[data-link]");
if (link) {
event.preventDefault();
this.push(link.getAttribute("href"));
}
});
this.handleRoute(location.pathname);
}
// 注册路由
register(path, handler, options = {}) {
this.routes.push({
path,
handler,
pattern: this.pathToRegex(path),
...options,
});
return this;
}
// 将路径模式转换为正则表达式
pathToRegex(path) {
// 支持动态路由,如 /user/:id
const pattern = path
.replace(/\//g, "\\/")
.replace(/:(\w+)/g, "(?<$1>[^/]+)");
return new RegExp(`^${pattern}$`);
}
// 匹配路由
matchRoute(path) {
for (const route of this.routes) {
const match = path.match(route.pattern);
if (match) {
return {
route,
params: match.groups || {},
};
}
}
return null;
}
// 前置钩子
beforeEach(hook) {
this.beforeHooks.push(hook);
return this;
}
// 后置钩子
afterEach(hook) {
this.afterHooks.push(hook);
return this;
}
// 执行前置钩子
async runBeforeHooks(to, from) {
for (const hook of this.beforeHooks) {
const result = await hook(to, from);
// 如果钩子返回 false,中断导航
if (result === false) {
return false;
}
// 如果钩子返回字符串,重定向到该路径
if (typeof result === "string") {
this.push(result);
return false;
}
}
return true;
}
// 执行后置钩子
async runAfterHooks(to, from) {
for (const hook of this.afterHooks) {
await hook(to, from);
}
}
// 处理路由
async handleRoute(path, state = null) {
const matched = this.matchRoute(path);
if (!matched) {
console.error(`找不到路由: ${path}`);
return;
}
const to = {
path,
params: matched.params,
query: QueryParams.parse(),
state,
};
const from = this.currentRoute;
// 执行前置钩子
const shouldContinue = await this.runBeforeHooks(to, from);
if (!shouldContinue) {
return;
}
// 执行路由处理器
await matched.route.handler(to);
// 更新当前路由
this.currentRoute = to;
// 执行后置钩子
await this.runAfterHooks(to, from);
}
// 添加历史记录
push(path, state = {}) {
history.pushState(state, "", path);
this.handleRoute(path, state);
}
// 替换历史记录
replace(path, state = {}) {
history.replaceState(state, "", path);
this.handleRoute(path, state);
}
// 后退
back() {
history.back();
}
// 前进
forward() {
history.forward();
}
}
// 使用示例
const router = new Router();
// 注册路由(支持动态参数)
router
.register("/", async (route) => {
console.log("首页");
document.getElementById("app").innerHTML = "<h1>首页</h1>";
})
.register("/user/:id", async (route) => {
console.log("用户页", route.params);
const userId = route.params.id;
document.getElementById("app").innerHTML = `
<h1>用户: ${userId}</h1>
<p>查询参数: ${JSON.stringify(route.query)}</p>
`;
})
.register("/product/:category/:id", async (route) => {
console.log("产品页", route.params);
const { category, id } = route.params;
document.getElementById("app").innerHTML = `
<h1>产品</h1>
<p>分类: ${category}</p>
<p>ID: ${id}</p>
`;
});
// 前置钩子(用于权限检查等)
router.beforeEach((to, from) => {
console.log(`导航: ${from?.path || "(初始)"} -> ${to.path}`);
// 示例:检查认证
if (to.path.startsWith("/admin")) {
const isAuthenticated = false; // 实际应用中检查用户状态
if (!isAuthenticated) {
console.log("未认证,重定向到登录页");
return "/login"; // 重定向
}
}
// 返回 undefined 或 true 继续导航
});
// 后置钩子(用于分析等)
router.afterEach((to, from) => {
console.log("导航完成:", to.path);
// 发送页面浏览统计
// analytics.pageview(to.path);
});
// 编程式导航
// router.push('/user/123');
// router.push('/product/laptop/456');
// router.replace('/about');最佳实践
1. 安全的 URL 操作
javascript
// ❌ 不安全:直接拼接 URL
const url = "https://example.com/search?q=" + userInput;
// ✅ 安全:使用 URLSearchParams
const params = new URLSearchParams();
params.set("q", userInput); // 自动编码
const url = `https://example.com/search?${params}`;
// ✅ 更好:使用 URL 对象
const url = new URL("https://example.com/search");
url.searchParams.set("q", userInput);
console.log(url.href);2. 同源策略遵守
javascript
// pushState/replaceState 只能用于同源 URL
try {
history.pushState(null, "", "https://another-domain.com"); // ❌ 跨域错误
} catch (e) {
console.error("跨域错误:", e);
}
// ✅ 正确:同源 URL
history.pushState(null, "", "/new-path"); // 同域名下的路径3. 状态对象的大小限制
javascript
// ⚠️ 注意:不要在状态对象中存储大量数据
// 不同浏览器对状态对象大小有限制(通常 640KB - 10MB)
// ❌ 不推荐:存储大量数据
history.pushState(
{
user: {
/* 大量用户数据 */
},
products: [
/* 数千个产品 */
],
},
"",
"/page"
);
// ✅ 推荐:只存储必要的标识符
history.pushState(
{
userId: 123,
page: "products",
filters: { category: "laptop" },
},
"",
"/products"
);
// 大量数据应该存储在其他地方(localStorage、IndexedDB等)4. 优雅地处理后退按钮
javascript
// 防止用户意外离开(如有未保存的更改)
window.addEventListener("beforeunload", (event) => {
const hasUnsavedChanges = true; // 实际应用中检查状态
if (hasUnsavedChanges) {
const message = "您有未保存的更改,确定要离开吗?";
event.returnValue = message;
return message;
}
});
// 自定义后退行为
let canGoBack = true;
window.addEventListener("popstate", (event) => {
if (!canGoBack) {
// 如果不允许后退,立即前进回来
history.forward();
// 提示用户
alert("请先完成当前操作");
}
});小结
location 和 history 对象是浏览器导航系统的核心:
Location 对象:
- URL 信息(
href,protocol,host,pathname,search,hash) - 查询参数解析(URLSearchParams)
- 页面导航(
assign(),replace(),reload())
History 对象:
- 历史记录导航(
back(),forward(),go()) - HTML5 History API(
pushState(),replaceState(), state) - popstate 事件监听
最佳实践:
- 使用 URLSearchParams 安全处理查询参数
- 遵守同源策略
- 限制状态对象大小
- 合理使用 pushState 和 replaceState
- 优雅处理浏览器后退操作
掌握这两个对象,特别是 HTML5 History API,是构建现代单页应用(SPA)的基础。它们让你能够在不刷新页面的情况下实现流畅的导航体验。