react-router踩坑分享
背景
有一個Web項目,采用的是前后端分離的方式,前端使用的是react技術棧,后端使用的是Django框架。
有這么需求: 在一個新聞列表里,點擊某一條新聞,要求新聞詳情頁里的title keywords description
是動態的。
辛苦歷程
JavaScript動態修改
當從后端取回數據時,用JavaScript動態改變title標簽和meta標簽就ok了。但是后來才發現,這個需求的目的是為了SEO,而爬蟲可以爬到的數據是后端返回的html頁面上的數據。如果是通過JavaScript來動態處理title keywords description
的話,是不利於SEO的,因此,動態修改title keywords description
只能放到后端做。后端將html拼接好,返回前端來,前端再進行相應的邏輯操作。
第一次嘗試
當思路理清楚之后,其實想着也就很簡單, 三步就可以了。
- 前端向后端發送請求。
- 后端獲取前端發送請求的URL地址,得到參數,然后從數據庫里讀出相應的數據。
- 返回一個新的html頁面。
然后開始進行測試:
首先,打開主頁面,點擊任意一條新聞,此時的url還是hash模式的。
當跳到新聞詳情頁表的時候,打開控制台,進入Network, 查看所發送的請求。
可以看見,請求的路徑是localhost:3000
, 也就是根目錄,而后台配置的根路由的代碼如下:
urlpatterns = [
url(r'^$', TemplateView.as_view(template_name='index.html'), name='index'),
]
因此,可以知道,在請求后台的時候,后台返回了index.html
模板回來。但是,問題出現了,我們想要返回的index.html
里的keywords description title
是在后端拼接好然后返回給前端,而目前我們所能實現的僅僅是返回一個index.html
, 我們沒辦法改變那3個值,因為我們沒有辦法獲取到前端的URL,或者說是前端沒有辦法傳參數到后端去, 導致后端不能從數據庫里查出來那三個值。
那么,現在需要解決的問題就是如何將參數傳到后端去。
第二次嘗試
由於,點擊某條新聞的時候,跳到新聞詳情頁的時候,這是瀏覽器主動去向后端發送的請求,因此是很難去控制這個過程的。
所以,有了下面這個解決方案: 既然很難在瀏覽器主動發請求的時候去控制參數的傳遞,那么就不用去控制。而是在瀏覽器返回了index.html
后,這個時候,已經有了新聞的id
, 因此就可以人為的控制再發一次請求,這個時候后端配置一個路由,根據id
去數據庫里查找keywords description title
, 並在后端拼接好返回給前端。只不過有個缺點,會刷新一次頁面。
但是,在實踐的過程中,我理論上推導了一次整個過程,當人為控制發請求后,后端返回一個index.html
, 然后又會執行js文件,然后又會發請求,然后又會返回index.html
, 然后又會執行js文件,然后又會發請求。。。 就這樣,一直循環下去,顯然是不可能的。
第三次嘗試
這探索新的解決的方案的時候,了解了Http Header里的referer
, 當瀏覽器向web服務器發送請求的時候,一般都會帶上referer
, 告訴服務器,我是從哪個連接來的。因為想着后端只要獲取到了前端的URL,一切就搞定了,但是顯而易見,referer
依然不行。拿網易雲音樂測試如下:
第四次嘗試
在不斷嘗試的過程中,珩哥提到,后端獲取不到前端的URL地址,是因為react-router路由是使用的hash路由(http://localhost:3001/#/news/index/4141?_k=mv8udy
),如果將hashHistory改為browserHistory(http://localhost:3001/news/index/4141
)的話,那么后端一定就可以獲取到前端的URL。
解決方案:
cd projectName/app/src
vim app.js
// hash路由
import { Router, hashHistory as historyProvider, match } from 'react-router';
// 修改為browserHistory
import { Router, browserHistory as historyProvider, match } from 'react-router';
進行查看:
主頁面:
新聞詳情頁:
總結步驟
- 設置react-router路由方式為:
import { browserHistory } from 'react-router';
。 - 對服務器進行改造,否則用戶直接請求某個某個路由,就會報404。
- 進行測試。
那么如何進行測試呢?
- 看前端的請求地址。
- 多刷新頁面試試。(因為之前的是單頁面的,就算改變了路由,還是不會向后台發送請求。)
- 多看
title
的變化, 會遺留title
不變化或者說是變化出錯的bug。 - 只要有處理路由相關的地方,都測一下。(很重要)
其他方案
不知道大家是否還有其他解決方案?
原理
History
它是一個庫,react-router基於它來管理歷史會話記錄。
簡單的說,一個history知道如何去監聽瀏覽器地址欄的變化,並解析這個URL轉化為location對象,然后router使用它匹配到路由,最后正確地渲染對應的組件。
常見的3種History
BrowserHistory
它其實就是HTML5推出的歷史記錄API,可以把瀏覽器記錄當作一個棧,通過History提供的API來對瀏覽器記錄進行相應的操作。
常見的方法有下面這幾種:
- history.push(path, [state])
- history.replace(path, [state])
- history.go(n)
- history.goBack()
- history.goForword()
BrowserHistory是使用react-router應用推薦的history。它使用瀏覽器中的HistoryAPI用於處理URL,會創建一個像example.com/some/path
這樣的真實的URL。
要使用這個History的話,必須在服務器端做好處理URL的准備。一般從下面幾個方面來考慮:
- 處理
/
的請求,Django
簡單配置如下:
// urls.py
from apps import views
urlpatterns = [
url(r'^$', views.index, name='index'),
]
- 處理打開新頁面的URL跳轉,比如點擊某條新聞,跳轉到新聞詳情頁這樣的URL跳轉(即本文開頭的介紹)。
- 處理每個URL的路由, 這也是最容易被忽視的一部分。當你在導航欄里點來點去,顯示是沒有問題的,就像下面這樣。
但是當你刷新頁面的時候,你會得到這樣一個結果:
這是由於,你的應用是單頁的,當你點擊導航欄的時候,頁面並沒有刷新,只是JavaScript去請求了后台,然后更新了前端路由和組件狀態,但是沒有刷新頁面,導致沒有去請求后台的地址。所以,必須為每個URL在后端配置路由。一般來說,只需要配置簡單的根路由就可以了,除非你需要動態更改模板index.html
上的數據。
HashHistory
Hash history使用URL中的hash(#)部分去創建形如: example.com/#/path的路由。
像?_k=ckuvip沒用的URL是什么東西?
-
每個URL對應都會對應一個state對象,你可以在對象里存儲數據(比如當前頁面滾動到哪個位置了),但是這個數據不會出現在URL中。實際上,數據被存到了sessionStorage中。
-
當一個history通過應用程序的push或者replace作跳轉時,它可以在新的location里存儲
location state
, 而不用顯示在URL中,它類似於一個HTML中的post的表單數據。 -
在DOM API中,這些hash history通過
window.location.hash = newHash
很簡單地用於被跳轉,且不用存儲它們的location state。但我們想要每個history都能使用location.state, 因此要為每個location創建一個唯一的key, 並把它們的狀態存儲在sessionStorage中。當點擊'后退'和'前進'時,我們就會有一個機制去恢復這些location state。
MemoryHistory
Memory history不會在地址欄里被操作或讀取,這就解釋了是如何實現服務端渲染的。
BrowserHistory與HashHistory的對比
實際上,兩種History都只是人為控制路由的一種手段。
相同點:
它們都只是簡單的改變了地址欄里的URL,如果沒有刷新頁面的話,都不會向后端主動發送請求。
不同點:
當頁面刷新的時候,對於BrowserHistory, 瀏覽器會向后台發送整個URL的請求, 而對於HashHistory, 它只會請求后台的根目錄。
使用場景
- 不能完全是單頁應用。
- 必要要刷新頁面,因為這樣,才會有向后端發送請求。
- 需要動態改變模板里的內容,比如
head
里的內容。 - 不是特別復雜的系統。
其他
推薦閱讀: 關於如何爬取ajax應用
感覺單頁的seo,除了用SSR,就沒有其他的辦法了。