javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > 前端URL查询参数

前端必知必会的实现URL查询参数的方法详解

作者:evle

URL 参数查询是指在 URL 中使用问号(?)后面附加的键值对参数,本文为大家详细介绍了前端实现URL查询参数的方法,希望对大家有所帮助

今天来给大家聊一聊 URL 查询参数。什么是 URL 参数查询?URL 参数查询是指在 URL 中使用问号(?)后面附加的键值对参数。例如:example.com/search?keyw…

URL 查询参数的发展历程

URL 查询参数的起源可以追溯到早期的互联网。在万维网诞生之初,为了实现简单的数据传递和页面交互,开发人员开始在 URL 中添加额外信息,以告诉服务器用户的需求。

到了 Web 2.0 时代,用户交互性增强,单页应用(SPA)兴起,URL 查询参数成为了前端路由和状态管理的关键工具。像 React Router 等路由库,就大量利用查询参数来管理页面状态,实现无刷新页面跳转和数据传递。

在 SPA 时代如何实现

首先忘记 useSearchParams 这个 React Router 提供的 Hook,让我们重新思考这个问题。

还是这个 Link 假设这是一个商品搜索页面,以前分享个页面内容,那叫一个费劲。现在有了 URL 查询参数,简单到飞起!用户直接复制链接就能分享给他人。比如说你在 React 项目里做了个搜索功能,搜完后,链接里的查询参数就包含了搜索关键词 接收者打开链接,直接就能看到和你一模一样的搜索结果,完全不用再重新输入关键词,是不是很方便!

https://example.com/search?keyword=手机&price=1000-2000&brand=apple

当访问这个页面时,我们需要从 URL 上取出这些参数,然后根据这些参数去发送网络请求查询商品。

那我们要解决第一个问题:获取 URL 参数的变化

浏览器 API

如果想获取浏览器地址栏上的数据,需要调用浏览器的 History APIwindow.location 对象 这是浏览器提供的核心 API,包含了当前 URL 的信息,假设当前 URL 是:https://example.com/path?search=test#hash

那么访问 window.location 则可以获得如下信息:

window.location.pathname  // "/path"
window.location.search   // "?search=test"
window.location.hash     // "#hash"

另外一个核心的 对象是 window.histroy 这个对象是操作浏览器的跳转的,比如 history.back() 就是用户点击了下图左侧箭头的效果。history.pushState(state, '', '/new-path') 就是模拟用户修改 上面提到的 window.location.pathname 跳转到了新的页面。

当用户点击前后按钮时会触发一个事件 popstate, 我们监听这个事件就可以捕捉到页面前进后退传递的变量或URL变化,现在可以打开掘进试试监听这个事件

window.addEventListener('popstate', (event) => {
  console.log('历史记录变化:', event);
});

那在juejin 点击前后的时候,就能看到juejin 页面跳转时传递的一些参数和变化

但是要注意的是 popstate 事件只能监听浏览器的前进后退,无法监听到 pushState 产生的变化,调用 pushState 会改变当前地址栏的URL,并且页面也不会重新加载。 这个特性在 SPA 中非常关键,也是路由切换的关键方法,如果想监听这个方法来实现监听地址栏 URL 变化的效果请继续看。

路由守卫

有了对 history API 的理解,我们就可以做很多场景了比如路由守卫,路由守卫就是当用户切换页面时,会有一个守卫,来检测当前用户是否能够访问该页面。

这里我们用一个图说明一下我们实现的流程

+------------------------+     注册     +-------------------------+
|                        |  -------->  |      路由守卫映射        |
|  registerGuard('/admin')|            | Map<路径, 守卫函数>      |
|                        |            |                         |
+------------------------+            | '/admin' => checkAdmin  |
                                     | '/user'  => checkUser   |
                                     +-------------------------+
                                              |
                                              | 监听
                                              ↓
+------------------------+     触发     +-------------------------+
|    路由变化事件          | -------->  |     路由守卫检查          |
|  1. history.pushState  |            | 1. 获取当前路径           |
|  2. popstate          |            | 2. 查找匹配的守卫函数      |
|                       |            | 3. 执行守卫逻辑           |
+------------------------+            +-------------------------+
                                              |
                                              | 结果
                                              ↓
                                    +-------------------------+
                                    |         处理结果          |
                                    | true  → 允许访问         |
                                    | false → 重定向到登录页    |
                                    +-------------------------+

先实现注册系统

const routeGuards = new Map(); // 用一个 map 将所有守卫维护起来

// 注册路由守卫
export function registerGuard(path, guard) {
  routeGuards.set(path, guard);
}

下面我们给 /user 注册一个守卫,即:当用户访问 /user 时,运行一个检测逻辑验证用户是否可以访问。

registerGuard('/user', () => {
  const isLoggedIn = checkAuth();
  if (!isLoggedIn) {
    alert('请先登录!');
    return false;
  }
  return true;
});

接下来我们就需要监听了,前面提到 history.pushState API 不产生任何事件,我们无法监听,那 workaround 的方法是重写这个方法,并在使在调用这个API的时候先执行我们的 guard 逻辑。

export function setupRouteGuard() {
  // 初始状态检查
  checkCurrentRoute();
  
  // 监听 popstate 事件(浏览器前进/后退)
  window.addEventListener('popstate', checkCurrentRoute);
  
  // 拦截 history.pushState
  const originalPushState = history.pushState;
  history.pushState = function(...args) {
    const result = originalPushState.apply(this, args);
    checkCurrentRoute();
    return result;
  };
}

通过改写后,每当通过 pushState 跳转页面时,就会调 checkCurrentRoute 的方法, 该方法中

function checkCurrentRoute() {
  const currentPath = window.location.pathname;
  
  for (const [path, guard] of routeGuards) {
    if (currentPath.startsWith(path)) {
      // 执行用户注册的回调函数
      const result = guard({
        to: currentPath,
        from: document.referrer,
      });
      
      // 如果回调返回 false,则中断导航,比如调到其他未授权页面,或者错误提示什么的
      if (result === false) {
        return;
      }
    }
  }
}

实现 useNavigate

我们发现上面那种通过重写 pushState 方法实现监听并不优雅,既然 pushState 不产生事件,我们就不调用这个方法,我们提供一个新的方法 navigate来进行页面跳转并且发射事件。

这里补充一个知识,浏览器 API 可以发射自定义事件通过 new CustomEvent() 方法。

function useNavigate() {
  const navigate = useCallback((to, options = {}) => {
    // 更新URL, 添加新的历史记录
    window.history.pushState(state, '', to);

    // 触发自定义事件通知路由变化
    window.dispatchEvent(new CustomEvent('routechange', {
      detail: {
        pathname: window.location.pathname,
        search: window.location.search,
        hash: window.location.hash,
        state
      }
    }));
  }, []);

  return navigate;
}

定义好之后直接引用就行

 const navigate = useNavigate();
 navigate(`/search`);

当用户执行 navigate('/search')时,当前页面的地址栏就会变为 xxx/search

实现 useLocation

到目前为止我们只解决了 地址栏 URL 改变的问题,这时我们的程序其实是没办法知道 URL 变了的,因为 window.location.path 是一个普通变量,无法通知我们的程序,我们需要实现一个 useLocation Hook, 通过 setState 通知 React 框架来更新 UI。

function useLocation() {
  const [location, setLocation] = useState(() => ({
    pathname: window.location.pathname,
    search: window.location.search,
    hash: window.location.hash,
    state: window.history.state
  }));

  useEffect(() => {
    // 监听 popstate 事件(浏览器前进/后退时触发)
    const handlePopState = () => {
      setLocation({
        pathname: window.location.pathname,
        search: window.location.search,
        hash: window.location.hash,
        state: window.history.state
      });
    };

    // 监听自定义的路由变化事件
    const handleRouteChange = (event) => {
      setLocation(event.detail);
    };

    window.addEventListener('popstate', handlePopState);
    window.addEventListener('routechange', handleRouteChange);

    return () => {
      window.removeEventListener('popstate', handlePopState);
      window.removeEventListener('routechange', handleRouteChange);
    };
  }, []);

  return location;
}

回到例子

经历了一些 API 的理解,我们回到最初的例子,想支持 URL 查询参数,也就是用户粘贴过来带有参数的URL,我们可以把他变成筛选项并发起请求,

function ProductSearch() {
  const [searchParams] = useSearchParams();
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    // 从 URL 获取查询参数
    const keyword = searchParams.get('keyword');
    const price = searchParams.get('price');
    const brand = searchParams.get('brand');

    // 如果有查询参数,则发起请求
    if (keyword || price || brand) {
      fetchProducts();
    }
  }, [searchParams]); // 当 URL 参数变化时重新请求

  const fetchProducts = async () => {
    try {
      setLoading(true);
      // 构建查询参数
      const params = new URLSearchParams(searchParams);
      
      // 发起请求
      const response = await fetch(`/api/products?${params}`);
      const data = await response.json();
      setProducts(data);
    } catch (error) {
      console.error('查询失败:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      <h2>搜索结果</h2>
      {loading ? (
        <div>加载中...</div>
      ) : (
        <div className="product-list">
          {products.map(product => (
            <div key={product.id} className="product-item">
              <h3>{product.name}</h3>
              <p>价格: ¥{product.price}</p>
              <p>品牌: {product.brand}</p>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

这样实现的好处:

那关键点 useSearchParams 也没有那么神秘了,根据我们前面的理解 we can see how it works easily.

这里贴一个简化实现:

import { useState, useCallback, useEffect } from 'react';

function useSearchParams() {
  const [params, setParams] = useState(() => 
    new URLSearchParams(window.location.search)
  );

  const setSearchParams = useCallback((update) => {
    // 处理更新
    const newParams = new URLSearchParams(
      typeof update === 'object' ? update : update(params)
    );
    
    // 更新 URL 和状态
    history.pushState(null, '', `?${newParams}`);
    setParams(newParams);
  }, [params]);

  // 简化历史监听
  useEffect(() => {
    const syncParams = () => setParams(new URLSearchParams(location.search));
    window.addEventListener('popstate', syncParams);
    return () => window.removeEventListener('popstate', syncParams);
  }, []);

  return [params, setSearchParams];
}

export default useSearchParams;

使用 React Router 常见问题

在使用 React Router 时,我们经常需要通过 useLocation 获取路由传递的 state 数据:

为了监听 state的变化,我们可能会这么使用

const location = useLocation();

useEffect(()=>{
}, [location.state])

但是在 React 中,每次组件重新渲染时,组件内的代码都会重新执行,意味着 location.state 可能值没有变化,比如 state 是 'hello', 下次重新渲染即使还是 'hello', 也会导致 useEffect 会重复执行,因为 useEffect 对监听的这个 location.state 做了一个浅比较(也就是针对引用数据的类型比如对象,不比较具体的值,只比较引用),为什么浅比较呢,因为每个内容都比较慢呀。

React 的做法是将这个优化决定权交给了开发者。

解决方案:

使用 useMemo 优化 用来监听真正内容的变更:我们可以使用 useMemo 来记忆化 state 对象,只在真正需要更新的属性发生变化时才创建新的引用:

const memoizedState = useMemo(() => {
  return location.state;
}, [location.state?.source, location.state?.timestamp]); // 只监听需要的属性

useEffect(() => {
  if (id) {
    handleProductDetail(id, memoizedState);
  }
}, [id, memoizedState]);

使用 JSON.stringify

另一种方案是使用 JSON.stringify 来比较对象的值而不是引用(不优雅,也有局限性)

useEffect(() => {
  // 一些操作
}, [id, JSON.stringify(location.state)]);

场景

SPA 时代下,应用支持参数化查询已经成为一个标配。

如果没有会怎么样?

想象一下,当用户在使用一个复杂的数据筛选系统时,他们花费了大量时间调整各种参数,最终得到了理想的结果。如果这时他们想要分享这个结果给同事,传统方式可能需要写一份详细的操作说明。但有了合理的 URL 参数设计,用户只需要复制当前页面的链接,同事就能看到完全相同的结果。

对 URL查询参数的支持可以极大的提升用户体验,并且随处可见:

在设计这些参数时,“美观程度” 也很重要:

到此这篇关于前端必知必会的实现URL查询参数的方法详解的文章就介绍到这了,更多相关前端URL查询参数内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文