前端必知必會的實現(xiàn)URL查詢參數(shù)的方法詳解
今天來給大家聊一聊 URL 查詢參數(shù)。什么是 URL 參數(shù)查詢?URL 參數(shù)查詢是指在 URL 中使用問號(?)后面附加的鍵值對參數(shù)。例如:example.com/search?keyw…
URL 查詢參數(shù)的發(fā)展歷程
URL 查詢參數(shù)的起源可以追溯到早期的互聯(lián)網(wǎng)。在萬維網(wǎng)誕生之初,為了實現(xiàn)簡單的數(shù)據(jù)傳遞和頁面交互,開發(fā)人員開始在 URL 中添加額外信息,以告訴服務器用戶的需求。
到了 Web 2.0 時代,用戶交互性增強,單頁應用(SPA)興起,URL 查詢參數(shù)成為了前端路由和狀態(tài)管理的關鍵工具。像 React Router 等路由庫,就大量利用查詢參數(shù)來管理頁面狀態(tài),實現(xiàn)無刷新頁面跳轉和數(shù)據(jù)傳遞。
在 SPA 時代如何實現(xiàn)
首先忘記 useSearchParams
這個 React Router 提供的 Hook,讓我們重新思考這個問題。
還是這個 Link 假設這是一個商品搜索頁面,以前分享個頁面內(nèi)容,那叫一個費勁。現(xiàn)在有了 URL 查詢參數(shù),簡單到飛起!用戶直接復制鏈接就能分享給他人。比如說你在 React 項目里做了個搜索功能,搜完后,鏈接里的查詢參數(shù)就包含了搜索關鍵詞 接收者打開鏈接,直接就能看到和你一模一樣的搜索結果,完全不用再重新輸入關鍵詞,是不是很方便!
https://example.com/search?keyword=手機&price=1000-2000&brand=apple
當訪問這個頁面時,我們需要從 URL 上取出這些參數(shù),然后根據(jù)這些參數(shù)去發(fā)送網(wǎng)絡請求查詢商品。
那我們要解決第一個問題:獲取 URL 參數(shù)的變化
瀏覽器 API
如果想獲取瀏覽器地址欄上的數(shù)據(jù),需要調(diào)用瀏覽器的 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
跳轉到了新的頁面。
當用戶點擊前后按鈕時會觸發(fā)一個事件 popstate
, 我們監(jiān)聽這個事件就可以捕捉到頁面前進后退傳遞的變量或URL變化,現(xiàn)在可以打開掘進試試監(jiān)聽這個事件
window.addEventListener('popstate', (event) => { console.log('歷史記錄變化:', event); });
那在juejin 點擊前后的時候,就能看到juejin 頁面跳轉時傳遞的一些參數(shù)和變化
但是要注意的是 popstate
事件只能監(jiān)聽瀏覽器的前進后退,無法監(jiān)聽到 pushState
產(chǎn)生的變化,調(diào)用 pushState
會改變當前地址欄的URL,并且頁面也不會重新加載。 這個特性在 SPA 中非常關鍵,也是路由切換的關鍵方法,如果想監(jiān)聽這個方法來實現(xiàn)監(jiān)聽地址欄 URL 變化的效果請繼續(xù)看。
路由守衛(wèi)
有了對 history API 的理解,我們就可以做很多場景了比如路由守衛(wèi),路由守衛(wèi)就是當用戶切換頁面時,會有一個守衛(wèi),來檢測當前用戶是否能夠訪問該頁面。
這里我們用一個圖說明一下我們實現(xiàn)的流程
+------------------------+ 注冊 +-------------------------+ | | --------> | 路由守衛(wèi)映射 | | registerGuard('/admin')| | Map<路徑, 守衛(wèi)函數(shù)> | | | | | +------------------------+ | '/admin' => checkAdmin | | '/user' => checkUser | +-------------------------+ | | 監(jiān)聽 ↓ +------------------------+ 觸發(fā) +-------------------------+ | 路由變化事件 | --------> | 路由守衛(wèi)檢查 | | 1. history.pushState | | 1. 獲取當前路徑 | | 2. popstate | | 2. 查找匹配的守衛(wèi)函數(shù) | | | | 3. 執(zhí)行守衛(wèi)邏輯 | +------------------------+ +-------------------------+ | | 結果 ↓ +-------------------------+ | 處理結果 | | true → 允許訪問 | | false → 重定向到登錄頁 | +-------------------------+
先實現(xiàn)注冊系統(tǒng)
const routeGuards = new Map(); // 用一個 map 將所有守衛(wèi)維護起來 // 注冊路由守衛(wèi) export function registerGuard(path, guard) { routeGuards.set(path, guard); }
下面我們給 /user
注冊一個守衛(wèi),即:當用戶訪問 /user
時,運行一個檢測邏輯驗證用戶是否可以訪問。
registerGuard('/user', () => { const isLoggedIn = checkAuth(); if (!isLoggedIn) { alert('請先登錄!'); return false; } return true; });
接下來我們就需要監(jiān)聽了,前面提到 history.pushState API 不產(chǎn)生任何事件,我們無法監(jiān)聽,那 workaround 的方法是重寫這個方法,并在使在調(diào)用這個API的時候先執(zhí)行我們的 guard 邏輯。
export function setupRouteGuard() { // 初始狀態(tài)檢查 checkCurrentRoute(); // 監(jiān)聽 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 跳轉頁面時,就會調(diào) checkCurrentRoute
的方法, 該方法中
function checkCurrentRoute() { const currentPath = window.location.pathname; for (const [path, guard] of routeGuards) { if (currentPath.startsWith(path)) { // 執(zhí)行用戶注冊的回調(diào)函數(shù) const result = guard({ to: currentPath, from: document.referrer, }); // 如果回調(diào)返回 false,則中斷導航,比如調(diào)到其他未授權頁面,或者錯誤提示什么的 if (result === false) { return; } } } }
實現(xiàn) useNavigate
我們發(fā)現(xiàn)上面那種通過重寫 pushState 方法實現(xiàn)監(jiān)聽并不優(yōu)雅,既然 pushState 不產(chǎn)生事件,我們就不調(diào)用這個方法,我們提供一個新的方法 navigate
來進行頁面跳轉并且發(fā)射事件。
這里補充一個知識,瀏覽器 API 可以發(fā)射自定義事件通過 new CustomEvent()
方法。
function useNavigate() { const navigate = useCallback((to, options = {}) => { // 更新URL, 添加新的歷史記錄 window.history.pushState(state, '', to); // 觸發(fā)自定義事件通知路由變化 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`);
當用戶執(zhí)行 navigate('/search')
時,當前頁面的地址欄就會變?yōu)?xxx/search
實現(xiàn) useLocation
到目前為止我們只解決了 地址欄 URL 改變的問題,這時我們的程序其實是沒辦法知道 URL 變了的,因為 window.location.path
是一個普通變量,無法通知我們的程序,我們需要實現(xiàn)一個 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(() => { // 監(jiān)聽 popstate 事件(瀏覽器前進/后退時觸發(fā)) const handlePopState = () => { setLocation({ pathname: window.location.pathname, search: window.location.search, hash: window.location.hash, state: window.history.state }); }; // 監(jiān)聽自定義的路由變化事件 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; }
回到例子
經(jīng)歷了一些 API 的理解,我們回到最初的例子,想支持 URL 查詢參數(shù),也就是用戶粘貼過來帶有參數(shù)的URL,我們可以把他變成篩選項并發(fā)起請求,
function ProductSearch() { const [searchParams] = useSearchParams(); const [products, setProducts] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { // 從 URL 獲取查詢參數(shù) const keyword = searchParams.get('keyword'); const price = searchParams.get('price'); const brand = searchParams.get('brand'); // 如果有查詢參數(shù),則發(fā)起請求 if (keyword || price || brand) { fetchProducts(); } }, [searchParams]); // 當 URL 參數(shù)變化時重新請求 const fetchProducts = async () => { try { setLoading(true); // 構建查詢參數(shù) const params = new URLSearchParams(searchParams); // 發(fā)起請求 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> ); }
這樣實現(xiàn)的好處:
- URL 可分享,用戶可以直接分享搜索結果
- 刷新頁面不會丟失搜索條件
- 支持瀏覽器前進/后退操作
- 便于跟蹤用戶搜索行為
那關鍵點 useSearchParams
也沒有那么神秘了,根據(jù)我們前面的理解 we can see how it works easily.
這里貼一個簡化實現(xiàn):
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 和狀態(tài) history.pushState(null, '', `?${newParams}`); setParams(newParams); }, [params]); // 簡化歷史監(jiān)聽 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 時,我們經(jīng)常需要通過 useLocation 獲取路由傳遞的 state 數(shù)據(jù):
為了監(jiān)聽 state的變化,我們可能會這么使用
const location = useLocation(); useEffect(()=>{ }, [location.state])
但是在 React 中,每次組件重新渲染時,組件內(nèi)的代碼都會重新執(zhí)行,意味著 location.state 可能值沒有變化,比如 state 是 'hello', 下次重新渲染即使還是 'hello', 也會導致 useEffect 會重復執(zhí)行,因為 useEffect 對監(jiān)聽的這個 location.state 做了一個淺比較(也就是針對引用數(shù)據(jù)的類型比如對象,不比較具體的值,只比較引用),為什么淺比較呢,因為每個內(nèi)容都比較慢呀。
React 的做法是將這個優(yōu)化決定權交給了開發(fā)者。
解決方案:
使用 useMemo 優(yōu)化 用來監(jiān)聽真正內(nèi)容的變更:我們可以使用 useMemo 來記憶化 state 對象,只在真正需要更新的屬性發(fā)生變化時才創(chuàng)建新的引用:
const memoizedState = useMemo(() => { return location.state; }, [location.state?.source, location.state?.timestamp]); // 只監(jiān)聽需要的屬性 useEffect(() => { if (id) { handleProductDetail(id, memoizedState); } }, [id, memoizedState]);
使用 JSON.stringify
另一種方案是使用 JSON.stringify 來比較對象的值而不是引用(不優(yōu)雅,也有局限性)
useEffect(() => { // 一些操作 }, [id, JSON.stringify(location.state)]);
場景
SPA 時代下,應用支持參數(shù)化查詢已經(jīng)成為一個標配。
如果沒有會怎么樣?
想象一下,當用戶在使用一個復雜的數(shù)據(jù)篩選系統(tǒng)時,他們花費了大量時間調(diào)整各種參數(shù),最終得到了理想的結果。如果這時他們想要分享這個結果給同事,傳統(tǒng)方式可能需要寫一份詳細的操作說明。但有了合理的 URL 參數(shù)設計,用戶只需要復制當前頁面的鏈接,同事就能看到完全相同的結果。
對 URL查詢參數(shù)的支持可以極大的提升用戶體驗,并且隨處可見:
- 表格的狀態(tài)保持
- 多步驟表單
- 文檔閱讀位置
- 多語言切換
- 視頻播放狀態(tài)
- 等等
在設計這些參數(shù)時,“美觀程度” 也很重要:
- 直觀易懂 - 參數(shù)名稱要見名知義
- 簡潔明確 - 避免冗余和歧義
- 可預測 - 參數(shù)的行為要符合用戶預期
- 穩(wěn)定可靠 - 確保參數(shù)在各種情況下都能正常工作
到此這篇關于前端必知必會的實現(xiàn)URL查詢參數(shù)的方法詳解的文章就介紹到這了,更多相關前端URL查詢參數(shù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
javascript學習隨筆(編寫瀏覽器腳本 Navigator Scripting )
javascript學習隨筆(編寫瀏覽器腳本 Navigator Scripting )...2007-03-03