摘要:也就是說通過我們自己構建來解釋是否是一個合適的路由抽象。首先,并不需要,因為如果路由中沒有給那么將會自動渲染。基本上我們的路由只要關心的變化并且返回相應的即可。為了解決這個問題,需要跟蹤每一條并且當路由發生改變的時候調用。
作者:Tyler
編譯:胡子大哈翻譯原文:http://huziketang.com/blog/posts/detail?postId=58d36df87413fc2e82408555
英文原文:Build your own React Router v4)
轉載請注明出處,保留原文鏈接以及作者信息
我還記得我第一次學習開發客戶端應用路由時的感覺,那時候我還是一個涉足在“單頁面應用”的未出世的小伙子,那會兒,要是說它沒把我的腦子弄的跟屎似的,那我是在撒謊。一開始的時候,我的感覺是我的應用程序代碼和路由代碼是兩個獨立且不同的體系,就像是兩個同父異母的兄弟,互相不喜歡但是又不得不在一起。
經過了一些年的努力,我終于有幸能夠教其他開發者關于路由的一些問題了。我發現,好像很多人對于這個問題的思考方式都和我當時很類似。我覺得有幾個原因。首先,路由問題確實很復雜,對于那些路由庫的開發者而言,找到一個合適的路由抽象概念來解釋這個問題就更加復雜。第二,正是由于路由的復雜性,這些路由庫的使用者傾向于只使用庫就好了,而不去弄懂到底背后是什么原理。
本文中,我們會深入地來闡述這兩個問題。我們會通過創建一個簡單版本的 React Router v4 來解決第二個問題,而通過這個過程來闡釋第一個問題。也就是說通過我們自己構建 RRv4 來解釋 RRv4 是否是一個合適的路由抽象。
下面是將要用來測試我們所構建的 React Router 的代碼。最終的代碼實例你可以在這里得到。
const Home = () => (Home
) const About = () => (About
) const Topic = ({ topicId }) => ({topicId}
) const Topics = ({ match }) => { const items = [ { name: "Rendering with React", slug: "rendering" }, { name: "Components", slug: "components" }, { name: "Props v. State", slug: "props-v-state" }, ] return () } const App = () => (Topics
{items.map(({ name, slug }) => (
{items.map(({ name, slug }) => (- {name}
))}( )} /> ))} ( Please select a topic.
)}/>)
- Home
- About
- Topics
如果你還不熟悉 React Router v4,就先了解幾個基本問題。Route 用來渲染 UI,當一個 URL 匹配上了你所指定的路由路徑,就進行渲染。Link 提供了一個可以瀏覽訪問你 app 的方法。換句話講,Link 組件允許你更新你的 URL,而 Route 組件根據你所提供的新 URL 來改變 UI。
本文并不會手把手的教你 RRV4 的基礎,所以如果上面的代碼你看起來很費勁的話,可以先來這里看一下官方文檔。把玩一下里面的例子,當你覺得順手了的時候,歡迎回來繼續閱讀。
如上段所說,路由給我們提供了兩個組件可以用于你的 app:Link 和 Route。我喜歡 React Router v4 的原因是它的 API “只是組件”而已,可以理解成沒有引入其他概念。這就是說如果你對 React 很熟悉的話,那么你對組件以及怎么組合組件一定有自己的理解,而這對于你寫路由代碼依然適用。這就很方便了,因為已經熟悉了如何創造組件,那么創建你自己的 React Router 就只是做你已經熟悉的事情——創建組件。
現在就來一起創建我們的 Route 組件。在上面的例子中,可以注意到
static propTypes = { exact: PropTypes.bool, path: PropTypes.string, component: PropTypes.func, }
這里有些小細節。首先,path 并不需要,因為如果路由中沒有給 path 那么將會自動渲染。第二,component 也不需要,是因為如果路徑匹配上了,有很多不同的方法來告訴 React Router 要渲染什么 UI。其中一個上面沒有提到的方法就是使用 render 來通知 React Router,具體代碼像這樣:
{ return }} />
render 允許你創建一個直接返回 UI 的內聯函數而不用創建額外的組件,所以我們也可以把它添加到 proTypes 中:
static propTypes = { exact: PropTypes.bool, path: PropTypes.string, component: PropTypes.func, render: PropTypes.func, }
現在我們知道了 Route 接收的屬性,我們來了解一下它們的具體功能。還記得上面說的:“當 URL 匹配上了你所指定的路由 path 以后,Route 渲染其對應的 UI”。基于這樣的定義,可以知道,
一起來看一下這個匹配函數應該怎么寫,暫且把它叫做 matchPath 吧。
class Route extends Component { static propTypes = { exact: PropTypes.bool, path: PropTypes.string, component: PropTypes.func, render: PropTypes.func, } render () { const { path, exact, component, render, } = this.props const match = matchPath( location.pathname, // 全局 DOM 變量 { path, exact } ) if (!match) { // 什么都不做,因為沒有匹配上 path 屬性 return null } if (component) { // 如果當前地址匹配上了 path 屬性 // 以 component 創建新元素并且通過 match 傳遞 return React.createElement(component, { match }) } if (render) { // 如果匹配上了且 component 沒有定義 // 則調用 render 并以 match 作為參數 return render({ match }) } return null } }
上面的代碼即實現了:如果匹配上了 path 屬性,就返回 UI,否則什么也不做。
我們再來談一下路由的問題。在客戶端應用這邊,一般來講只有兩種方式更新 URL。一種是用戶點擊 a 標簽,一種是點擊后退/前進按鈕。基本上我們的路由只要關心 URL 的變化并且返回相應的 UI 即可。假設我們知道更新 URL 的方式只有上面兩種,那么就可以針對這兩種情況做特殊處理了。稍后在構建 組件的時候再詳細介紹 a 標簽的情況,這里先討論后退/前進按鈕。 React Router 使用了 History工程里的 .listen 方法來監聽當前 URL 的變化,為了避免再引入其他的庫,我們使用 HTML5 的 popstate 事件來實現這一功能。當用戶點擊了后退/前進按鈕,popstate 就被觸發,我們需要的就是這個功能。因為 Route 渲染 UI 是根據當前 URL來做的,因此給 Route 配上監聽能力也是合理的,在 popstate 觸發的地方重新渲染 UI。就是說在觸發 popstate 時檢查是否匹配上了新的 URL,如果是則渲染 UI,如果不是,什么也不做,下面看一下代碼。
class Route extends Component { static propTypes: { path: PropTypes.string, exact: PropTypes.bool, component: PropTypes.func, render: PropTypes.func, } componentWillMount() { addEventListener("popstate", this.handlePop) } componentWillUnmount() { removeEventListener("popstate", this.handlePop) } handlePop = () => { this.forceUpdate() } render() { const { path, exact, component, render, } = this.props const match = matchPath(location.pathname, { path, exact }) if (!match) return null if (component) return React.createElement(component, { match }) if (render) return render({ match }) return null } }
這里要注意的是我們只是加了一個 popstate 監聽,當 popstate 觸發的時候,調用 forceUpdate 來強制做重新渲染的判斷。
這樣就實現了所有的
到現在,我們一直還沒有實現的是 matchPath 函數。這個函數在我們的 router 中是特別關鍵的,因為它是判斷當前 URL 是否匹配上了
只有當所給路徑精確匹配上 location.pathname 時才返回 true。
接下來就來具體實現 matchPath 函數。如果你回頭看一下上面 Route 組件的代碼,你可以看到 matchPath 函數是這樣的:
const match = matchPath(location.pathname, { path, exact })
這里的 match 要么是對象,要么是 null,這得取決于是否匹配上 path。根據這個聲明,我們來寫 matchPath 代碼:
const matchPatch = (pathname, options) => { const { exact = false, path } = options }
這里使用 ES6 語法。上面的意思是,創建一個叫做 exact 的變量,使其等于 options.exact,并且如果非 null 的話則設置其為 false。同樣創建一個叫做 path 的變量,使其等于 options.path。
接下來就添加判斷是否匹配。React Router 使用 pathToRegex 來實現,只需要寫簡單的正則匹配就可以了。
const matchPatch = (pathname, options) => { const { exact = false, path } = options if (!path) { return { path: null, url: pathname, isExact: true, } } const match = new RegExp(`^${path}`).exec(pathname) }
如果匹配上了,那么返回一個包含有所有匹配串的數組,否則返回 null。
下面是我們示例 app 的路由 "/topics/components" 的一些匹配項。
注意:每個
都在自己的渲染方法里調用 matchPath,所以要為每個 配一個 match。
現在我們要做的是添加判斷是否有匹配的代碼:
const matchPatch = (pathname, options) => { const { exact = false, path } = options if (!path) { return { path: null, url: pathname, isExact: true, } } const match = new RegExp(`^${path}`).exec(pathname) if (!match) { // 沒有匹配上 return null } const url = match[0] const isExact = pathname === url if (exact && !isExact) { // 匹配上了,但是不是精確匹配 return null } return { path, url, isExact, } }
提示一下之前有講過的,對于用戶來講,有兩種方式更新 URL:通過后退/前進按鈕和通過點擊 a 標簽。對于后退/前進點擊來說,使用 popstate 事件給 Route 添加監聽就可以,現在來看一下如何通過 Link 解決 a 標簽問題。
Link 的 API 如下:
這里 to 是一個 string 類型,指的是要鏈接到的地址。replace 是一個布爾值,如果是 true,那么點擊鏈接將替換當前的實體到歷史堆棧,而不是添加一個新的進去。
添加這些 propTypes 到 Link 組件就得到:
class Link extends Component { static propTypes = { to: PropTypes.string.isRequired, replace: PropTypes.bool, } }
我們知道在 Link 組件中的渲染函數需要返回一個 a 標簽,但是我們不想每次變路由都進行一次全頁面刷新,所以通過增加一個 onClick 處理程序來劫持 a 標簽。
class Link extends Component { static propTypes = { to: PropTypes.string.isRequired, replace: PropTypes.bool, } handleClick = (event) => { const { replace, to } = this.props event.preventDefault() // 這里是路由 } render() { const { to, children} = this.props return ( {children} ) } }
ok,代碼寫到現在,就差更改當前 URL 了。在 React Router 是使用 History 工程里面的 push 和 replace 方法。為了避免增加新依賴,這里我使用 HTML5 的 pushState 和 replaceState。
本文中我們為了防止引入額外的依賴,一直也沒采用 History 庫。但是它對真實的 React Router 卻是至關重要的,因為它對不同的 session 管理和不同的瀏覽器環境進行了規范化處理。
pushState 和 replaceState 都接收三個參數。第一個參數是一個與歷史實體相關聯的對象,我們不需要,所以設置成一個空對象。第二個參數是標題,我們也不需要,所以也設置成空。第三個是我們需要使用的,指的是:相關 URL。
const historyPush = (path) => { history.pushState({}, null, path) } const historyReplace = (path) => { history.replaceState({}, null, path) }
在 Link 組件內部,會調用 historyPush 或者 historyReplace,依賴于前面提到的 replace 屬性。
class Link extends Component { static propTypes = { to: PropTypes.string.isRequired, replace: PropTypes.bool, } handleClick = (event) => { const { replace, to } = this.props event.preventDefault() replace ? historyReplace(to) : historyPush(to) } render() { const { to, children} = this.props return ( {children} ) } }
現在就只剩下最后一件很關鍵的問題了,如果你想把上面的例子用在自己的路由代碼里面,你需要注意這個問題。當你瀏覽時,URL 會發生改變,但是 UI 卻沒有刷新,這是為什么呢?這是因為,盡管你通過 historyReplace 或者 historyPush 改變了地址,但是
React Router 通過設置狀態、上下文和歷史信息的組合來解決這個問題。監聽路由組件的內部代碼。
為了使路由簡單,我們通過把所有路由對象放到一個數組里的方式來實現
let instances = [] const register = (comp) => instances.push(comp) const unregister = (comp) => instances.splice(instances.indexOf(comp), 1)
注意這里創建了兩個函數。當
首先更新
class Route extends Component { static propTypes: { path: PropTypes.string, exact: PropTypes.bool, component: PropTypes.func, render: PropTypes.func, } componentWillMount() { addEventListener("popstate", this.handlePop) register(this) } componentWillUnmount() { unregister(this) removeEventListener("popstate", this.handlePop) } ... }
再更新 historyPush 和 historyReplace:
const historyPush = (path) => { history.pushState({}, null, path) instances.forEach(instance => instance.forceUpdate()) } const historyReplace = (path) => { history.replaceState({}, null, path) instances.forEach(instance => instance.forceUpdate()) }
這時只要 被點擊并且地址發生變化,每個
這就完成了所有的路由代碼了,并且實例 app 用這些代碼可以完美運行!
import React, { PropTypes, Component } from "react" let instances = [] const register = (comp) => instances.push(comp) const unregister = (comp) => instances.splice(instances.indexOf(comp), 1) const historyPush = (path) => { history.pushState({}, null, path) instances.forEach(instance => instance.forceUpdate()) } const historyReplace = (path) => { history.replaceState({}, null, path) instances.forEach(instance => instance.forceUpdate()) } const matchPath = (pathname, options) => { const { exact = false, path } = options if (!path) { return { path: null, url: pathname, isExact: true } } const match = new RegExp(`^${path}`).exec(pathname) if (!match) return null const url = match[0] const isExact = pathname === url if (exact && !isExact) return null return { path, url, isExact, } } class Route extends Component { static propTypes: { path: PropTypes.string, exact: PropTypes.bool, component: PropTypes.func, render: PropTypes.func, } componentWillMount() { addEventListener("popstate", this.handlePop) register(this) } componentWillUnmount() { unregister(this) removeEventListener("popstate", this.handlePop) } handlePop = () => { this.forceUpdate() } render() { const { path, exact, component, render, } = this.props const match = matchPath(location.pathname, { path, exact }) if (!match) return null if (component) return React.createElement(component, { match }) if (render) return render({ match }) return null } } class Link extends Component { static propTypes = { to: PropTypes.string.isRequired, replace: PropTypes.bool, } handleClick = (event) => { const { replace, to } = this.props event.preventDefault() replace ? historyReplace(to) : historyPush(to) } render() { const { to, children} = this.props return ( {children} ) } }
另外:React Router API 還自然派生出了
class Redirect extends Component { static defaultProps = { push: false } static propTypes = { to: PropTypes.string.isRequired, push: PropTypes.bool.isRequired, } componentDidMount() { const { to, push } = this.props push ? historyPush(to) : historyReplace(to) } render() { return null } }
注意這個組件并不渲染任何 UI,它只用來做路由定向使用。
我希望這篇文章對你在認識 React Router 上有所啟發。我總跟我的朋友們講,React 會使你成為一個好的 JavaScript 程序員,而 React Router 會使你成為一個好的 React 程序員。因為一切皆為組件,你懂 React,你就懂 React Router。
我最近正在寫一本《React.js 小書》,對 React.js 感興趣的童鞋,歡迎指點。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/82409.html
摘要:插件開發前端掘金作者原文地址譯者插件是為應用添加全局功能的一種強大而且簡單的方式。提供了與使用掌控異步前端掘金教你使用在行代碼內優雅的實現文件分片斷點續傳。 Vue.js 插件開發 - 前端 - 掘金作者:Joshua Bemenderfer原文地址: creating-custom-plugins譯者:jeneser Vue.js插件是為應用添加全局功能的一種強大而且簡單的方式。插....
摘要:前端每周清單半年盤點之與篇前端每周清單專注前端領域內容,以對外文資料的搜集為主,幫助開發者了解一周前端熱點分為新聞熱點開發教程工程實踐深度閱讀開源項目巔峰人生等欄目。與求同存異近日,宣布將的構建工具由遷移到,引發了很多開發者的討論。 前端每周清單半年盤點之 React 與 ReactNative 篇 前端每周清單專注前端領域內容,以對外文資料的搜集為主,幫助開發者了解一周前端熱點;分為...
摘要:項目問題總結這個項目,很簡單,前端使用,后端使用進行開發。方便移動端開發。當動畫結束后,有一個鉤子函數可以使用其他一些功能組件,都是自己嘗試去編寫的,像日歷組件組件組件等。版本的,是沒有任何的鉤子函數,我就感覺懵逼了。。。 todo-list 項目問題總結 這個 todo-list 項目,很簡單,前端使用 react,后端 nodejs 使用 koa2 進行開發。數據庫使用 Mysql...
摘要:前言最近將公司項目的從版本升到了版本,跟完全不兼容,是一次徹底的重寫。升級過程中踩了不少的坑,也有一些值得分享的點。沒有就會匹配所有路由最后不得不說升級很困難,坑也很多。 前言 最近將公司項目的 react-router 從 v3 版本升到了 v4 版本,react-router v4 跟 v3 完全不兼容,是一次徹底的重寫。這也給升級造成了極大的困難,與其說升級不如說是對 route...
摘要:概述相對于幾乎是重寫了新版的更偏向于組件化。汲取了很多思想,路由即是組件,使路由更具聲明式,且方便組合。如果你習慣使用,那么一定會很快上手新版的。被一分為三。不止是否有意義參考資料遷移到關注點官方文檔 概述 react-router V4 相對于react-router V2 or V3 幾乎是重寫了, 新版的react-router更偏向于組件化(everything is comp...
閱讀 3573·2021-10-11 10:59
閱讀 1591·2021-09-29 09:35
閱讀 2259·2021-09-26 09:46
閱讀 3771·2021-09-10 10:50
閱讀 953·2019-08-29 12:17
閱讀 821·2019-08-26 13:40
閱讀 2433·2019-08-26 11:44
閱讀 2103·2019-08-26 11:22