Router入門0x202: 本身實現 Router 頁面調度和特定頁面訪問

0x000 概述

上一章講了SPA如何實現組件/頁面切換,這一章講如何解決上一章出現的問題以及如何優雅的實現頁面切換。react

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'))太冗餘了git

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
  • 使用github

    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
    在上面的流程中,咱們註冊了兩個路由,每一個路由的配置信息大概包含了pathnamecomponent三個鍵值對,但其實只有path是必須的,其餘的都是非必須的,能夠結合框架、業務來傳須要的參數;在註冊路由的同時傳入了路由觸發時的動做。這裏設定爲將父節點的子節點所有移除後替換爲新的子節點,也就達到了組件切換的功能,經過callbackprops參數,咱們能夠獲取到當前觸發的路由配置和觸發該路由配置的時候的數據,好比後面調用Route.push('/index',{name:1})的時候,callbackprops數組

    {
        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的文章的詳情頁面,因此咱們須要添加幾個方法:app

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組件更新,從而使組件切換。dom

0x007 總結

路由自己是不帶有任何特殊的屬性的,在與框架集成的時候,應該考慮框架的特色,好比react的時候,咱們可使用reactreact-route直接結合,也能夠經過使用react-route-dom來結合。函數

0x008 資源

相關文章
相關標籤/搜索