上一章講了SPA
如何實現組件/頁面切換,這一章講如何解決上一章出現的問題以及如何優雅的實現頁面切換。react
回顧一下上一章講的頁面切換,咱們經過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
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
。
在上面的流程中,咱們註冊了兩個路由,每一個路由的配置信息大概包含了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}`) } } }
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') }) }
先看結果,咱們但願咱們在訪問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
,因此咱們須要手動調用一遍,爲了初始化第一次訪問的頁面,這樣咱們就能夠經過不一樣的地址訪問不一樣的頁面了,而整個站點只初始化了一次(在不使用按需加載的狀況下),體驗很是好,還要另一種實行這裏先不講,往後有空獨立出來說關於路由的東西。框架
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") ) })
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
路由自己是不帶有任何特殊的屬性的,在與框架集成的時候,應該考慮框架的特色,好比react
的時候,咱們可使用react
和react-route
直接結合,也能夠經過使用react-route-dom
來結合。函數