0x000 概述
上一章講了SPA
如何實現元件/頁面切換,這一章講如何解決上一章出現的問題以及如何優雅的實現頁面切換。
0x001 問題分析
回顧一下上一章講的頁面切換,我們通過LeactDom.render(new ArticlePage(),document.getElementById(`app`))
來切換頁面,的確做到了列表頁和詳情頁的切換,但是我們可以看到,瀏覽器的網址始終沒有變化,是http://localhost:8080
,那如果我們希望直接訪問某個頁面,比如訪問某篇文章呢?也就是我們希望我們訪問的地址是http://localhost:8080/article/1
,進入這個地址之後,可以直接訪問id 為1的文章。
問題1:無法做到訪問特定頁面並傳遞引數
問題2:通過LeactDom.render(new ArticlePage(),document.getElementById(`app`))
太冗餘了
0x002 簡單的路由實現並和原生js
結合:
基本上也是基於
釋出-訂閱
模式,
register: 註冊路由
push: 路由跳轉
- 原始碼
class Router {
static routes = {}
/**
* 如果是陣列
* 就遍歷陣列並轉化成 {"/index":{route:{...},callback:()=>{....}}} 形式
* 並執行 init 方法
* 如果是物件
* 就轉化成 {"/index":{route:{...},callback:()=>{....}}} 形式
* 並和原來的 this.route 合併
* 注意: 如果用物件形式必須手動執行 init 方法
* 最終 this.route 形式為
* [
* {"/index":{route:{...},callback:()=>{....}}}
* {"/detail":{route:{...},callback:()=>{....}}}
* ]
* @param routes
* @param callback
*/
static register(routes, callback) {
if (Array.isArray(routes)) {
this.routes = routes.map(route => {
return {
[route.path]: {
route: route,
callback: callback
}
}
}).reduce((r1, r2) => {
return {...r1, ...r2}
})
}
this.routes = {
...this.routes,
...{
[routes.path]: {
route: routes,
callback: callback
}
}
}
}
/**
* 跳轉到某個路由
* 本質是遍歷所有路由並執行 callback
*
* @param path
* @param data
*/
static push(path, data) {
Object.values(this.routes).forEach(route => {
route.callback(data, this.routes[ path].route, path)
})
}
}
export default Router
-
使用
import Router from "./core/Router"; Router.register([ { path: "/index", name: "主頁", component: (props) => { return document.createTextNode(`這是${props.route.name}`) } }, { path: "/detail", name: "詳情頁", component: (props) => { return document.createTextNode(`這是${props.route.name}`) } } ], (data, route, match) => { if (route.path === match) { let app = document.getElementById(`app`) app.childNodes.forEach(c => c.remove()) app.appendChild(new route.component({data,route,match})) } }) Router.push(`/index`) setTimeout(()=>{ Router.push(`/detail`) },3000)
-
說明:
當push
方法呼叫的時候,會觸發register
的時候傳入的callback
,並找到push
傳入的path
匹配的路由資訊,然後將該路由資訊作為callback
的引數,並執行callback
。
在上面的流程中,我們註冊了兩個路由,每個路由的配置資訊大概包含了path
、name
、component
三個鍵值對,但其實只有path
是必須的,其他的都是非必須的,可以結合框架、業務來傳需要的引數;在註冊路由的同時傳入了路由觸發時的動作。這裡設定為將父節點的子節點全部移除後替換為新的子節點,也就達到了元件切換的功能,通過callback
的props
引數,我們可以獲取到當前觸發的路由配置和觸發該路由配置的時候的資料,比如後面呼叫Route.push(`/index`,{name:1})
的時候,callback
的props
為{ data:{ name:1 }, route:{ path: "/index", name: "主頁", component: (props) => { return document.createTextNode(`這是${props.route.name}`) } } }
0x003 和上一章的SPA
結合
import Router from "./core/Router";
import DetailPage from "./page/DetailPage";
import ArticlePage from "./page/ArticlePage";
import LeactDom from "./core/LeactDom";
Router.register([
{
path: "/index",
name: "主頁",
component: ArticlePage
},
{
path: "/detail",
name: "詳情頁",
component: DetailPage
}
], (data, route,match) => {
if (route.path !== match) return
LeactDom.render(new route.component(data), document.getElementById(`app`))
})
然後在頁面跳轉的地方,修改為Route
跳轉
// ArticlePage#componentDidMount
componentDidMount() {
let articles = document.getElementsByClassName(`article`)
;[].forEach.call(articles, article => {
article.addEventListener(`click`, () => {
// LeactDom.render(new DetailPage({articleId: article.getAttribute(`data-id`)}), document.getElementById(`app`))
Router.push(`/detail`,{articleId:article.getAttribute(`data-id`)})
})
}
)
}
// DetailPage#componentDidMount
componentDidMount() {
document.getElementById(`back`).addEventListener(`click`, () => {
LeactDom.render(new ArticlePage(), document.getElementById(`app`))
Router.push(`/index`)
})
}
0x004 指定跳轉頁面-hash
先看結果,我們希望我們在訪問http://localhost:8080/#detail?articleId=2
的時候跳轉到id=2
的文章的詳情頁面,所以我們需要新增幾個方法:
import Url from `url-parse`
class Router {
static routes = {}
/**
* 初始化路徑
* 新增 hashchange 事件, 在 hash 發生變化的時候, 跳轉到相應的頁面
* 同時根據訪問的地址初始化第一次訪問的頁面
*
*/
static init() {
Object.values(this.routes).forEach(route => {
route.callback(this.queryStringToParam(), this.routes[`/` + this.getPath()].route,`/`+this.getPath())
})
window.addEventListener(`hashchange`, () => {
Object.values(this.routes).forEach(route => {
route.callback(this.queryStringToParam(), this.routes[`/` + this.getPath()].route,`/`+this.getPath())
})
})
}
/**
* 如果是陣列
* 就遍歷陣列並轉化成 {"/index":{route:{...},callback:()=>{....}}} 形式
* 並執行 init 方法
* 如果是物件
* 就轉化成 {"/index":{route:{...},callback:()=>{....}}} 形式
* 並和原來的 this.route 合併
* 注意: 如果用物件形式必須手動執行 init 方法
* 最終 this.route 形式為
* [
* {"/index":{route:{...},callback:()=>{....}}}
* {"/detail":{route:{...},callback:()=>{....}}}
* ]
* @param routes
* @param callback
*/
static register(routes, callback) {
if (Array.isArray(routes)) {
this.routes = routes.map(route => {
return {
[route.path]: {
route: route,
callback: callback
}
}
}).reduce((r1, r2) => {
return {...r1, ...r2}
})
this.init()
}
this.routes = {
...this.routes,
...{
[routes.path]: {
route: routes,
callback: callback
}
}
}
}
/**
* 跳轉到某個路由
* 其實只是簡單的改變 hash
* 觸發 hashonchange 函式
*
* @param path
* @param data
*/
static push(path, data) {
window.location.hash = this.combineHash(path, data)
}
/**
* 獲取路徑
* 比如 #detail => /detail
* @returns {string|string}
*/
static getPath() {
let url = new Url(window.location.href)
return url.hash.replace(`#`, ``).split(`?`)[0] || `/`
}
/**
* 將 queryString 轉化成 引數物件
* 比如 ?articleId=1 => {articleId: 1}
* @returns {*}
*/
static queryStringToParam() {
let url = new Url(window.location.href)
let hashAndParam = url.hash.replace(`#`, ``)
let arr = hashAndParam.split(`?`)
if (arr.length === 1) return {}
return arr[1].split(`&`).map(p => {
return p.split(`=`).reduce((a, b) => ({[a]: b}))
})[0]
}
/**
* 將引數變成 queryString
* 比如 {articleId:1} => ?articleId=1
* @param params
* @returns {string}
*/
static paramToQueryString(params = {}) {
let result = ``
Object.keys(params).length && Object.keys(params).forEach(key => {
if (result.length !== 0) {
result += `&`
}
result += key + `=` + params[key]
})
return result
}
/**
* 組合地址和資料
* 比如 detail,{articleId:1} => detail?articleId=1
* @param path
* @param data
* @returns {*}
*/
static combineHash(path, data = {}) {
if (!Object.keys(data).length) return path.replace(`/`, ``)
return (path + `?` + this.paramToQueryString(data)).replace(`/`, ``)
}
}
export default Router
說明:這裡修改了push
方法,原本callback
在這裡呼叫的,但是現在換成在init
呼叫。在init
中監聽了hashchange
事件,這樣就可以在hash
變化的時候,需要路由配置並呼叫callback
。而在監聽變化之前,我們先呼叫了一次,是因為如果我們第一次進入就有hash
,那麼就不會觸發hanshchange
,所以我們需要手動呼叫一遍,為了初始化第一次訪問的頁面,這樣我們就可以通過不同的地址訪問不同的頁面了,而整個站點只初始化了一次(在不使用按需載入的情況下),體驗非常好,還要另外一種實行這裡先不講,日後有空獨立出來講關於路由的東西。
0x005 將自己實現的路由和React
整合
- 重構
ArticlePage
class ArticlePage extends React.Component {
render() {
return <div>
<h3>文章列表</h3>
<hr/>
{
ArticleService.getAll().map((article, index) => {
return <div key={index} onClick={() => this.handleClick(article)}>
<h5>{article.title}</h5>
<p>{article.summary}</p>
<hr/>
</div>
})
}
</div>
}
handleClick(article) {
Router.push(`/detail`, {articleId: article.id})
}
}
- 重構
DetailPage
class DetailPage extends React.Component {
render() {
const {title, summary, detail} = ArticleService.getById(this.props.data.articleId)
return <div>
<h3>{title}</h3>
<p>{summary}</p>
<hr/>
<p>{detail}</p>
<button className=`btn btn-success` onClick={() => this.handleClick()}>返回</button>
</div>
}
handleClick() {
Router.push(`/index`)
}
}
- 重構路由配置和渲染
const routes = [
{
path: "/index",
name: "主頁",
component: ArticlePage
},
{
path: "/detail",
name: "詳情頁",
component: DetailPage
}
];
Router.register(routes, (data, route) => {
let Component = route.component
ReactDom.render(
<Component {...{data, route}}/>,
document.getElementById("app")
)
})
0x006 為React
定製Router
元件
在上面每呼叫一次
Router.push
,就會執行一次ReactDom.render
,並不符合React
的思想,所以,需要為React
定義一些元件
-
RouteApp
元件
class RouterApp extends React.Component {
componentDidMount(){
Router.init()
}
render() {
return {...this.props.children}
}
}
-
Route
元件
class Route extends React.Component {
constructor(props) {
super()
this.state={
path:props.path,
match:``,
data:{}
}
}
componentDidMount() {
Router.register({
path: this.props.path
}, (data, route) => {
this.setState({
match:route.path,
data:data
})
})
}
render() {
let Component = this.props.component
if (this.state.path===this.state.match){
return <Component {...this.state.data}/>
}
return null
}
}
- 使用
class App extends React.Component {
render() {
return (<div>
<Route path="/index" component={ArticlePage}/>
<Route path="/detail" component={DetailPage}/>
</div>)
}
}
ReactDom.render(
<RouterApp>
<App/>
</RouterApp>,
document.getElementById(`app`)
)
- 說明
在RouterApp
元件中呼叫了Route.init
來初始化呼叫,然後在每個Route
中註冊路由,每次路由變化的時候都會導致Route
元件更新,從而使元件切換。
0x007 總結
路由本身是不帶有任何特殊的屬性的,在與框架整合的時候,應該考慮框架的特點,比如react
的時候,我們可以使用react
和react-route
直接結合,也可以通過使用react-route-dom
來結合。