Skip to content

Location 与 History 对象:URL 操作与历史记录管理

导航双子星

在浏览器的导航系统中,locationhistory 是一对密切配合的对象。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("请先完成当前操作");
  }
});

小结

locationhistory 对象是浏览器导航系统的核心:

Location 对象:

  • URL 信息(href, protocol, host, pathname, search, hash
  • 查询参数解析(URLSearchParams)
  • 页面导航(assign(), replace(), reload()

History 对象:

  • 历史记录导航(back(), forward(), go()
  • HTML5 History API(pushState(), replaceState(), state)
  • popstate 事件监听

最佳实践:

  1. 使用 URLSearchParams 安全处理查询参数
  2. 遵守同源策略
  3. 限制状态对象大小
  4. 合理使用 pushState 和 replaceState
  5. 优雅处理浏览器后退操作

掌握这两个对象,特别是 HTML5 History API,是构建现代单页应用(SPA)的基础。它们让你能够在不刷新页面的情况下实现流畅的导航体验。