上一章讲了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
来结合。函数