一次性让你明白react-router所有的工作原理~
🎧 浏览器实现单页面应用的工作原理:
关键点是可以改变浏览器地址,又不刷新页面, 页面刷新了就不叫单页应用了~
1.通过Hash实现前端路由(一般不用这个)
因为改变url的hash值是不会刷新页面的, 所以可以通过hash来实现前端路由,从而实现无刷新的效果。
// hash属性位于location对象中,在当前页面中,可以通过:
window.location.hash='edit'
// 来实现改变当前url的hash值。执行上述的hash赋值后,页面的url发生改变。
// 赋值前:http://localhost:3000
// 赋值后:http://localhost:3000/#edit
hashchange的事件,可以监听hash的变化,我们可以通过下面两种方式来监听hash的变化:
// window.onhashchange:当前 URL 的锚部分(以 '#' 号为开始) 发生改变时触发。触发的情况如下:
// a、通过设置Location 对象 的 location.hash 或 location.href 属性修改锚部分;
// b、使用不同history操作方法到带hash的页面;
// c、点击链接跳转到锚点。<a href="#edit">edit</a>
window.onhashchange=function(event){
console.log(event);
}
window.addEventListener('hashchange',function(event){
console.log(event);
})
// HashChangeEvent的具体值为:
{isTrusted: true, oldURL: "http://localhost:3000/", newURL: "http://localhost:3000/#teg", type: "hashchange".....}
有了监听事件,且改变hash页面不刷新,这样我们就可以在监听事件的回调函数中,执行我们展示和隐藏不同UI显示的功能,从而实现前端路由。
hash的兼容性较好,因此在早期的前端路由中大量的采用,但是使用hash也有很多缺点:
- 搜索引擎对带有hash的页面不友好
- 带有hash的页面内难以追踪用户行为
2.通过Html5的history实现前端路由(目前用的)
HTML5的History接口(window.history),History接口允许我们操作浏览器会话历史记录,有一个npm包'history'就是封装了这个接口: npm install history -S
兼容性
export const supportsHistory = () => {
const ua = window.navigator.userAgent;
if (
(ua.indexOf("Android 2.") !== -1 || ua.indexOf("Android 4.0") !== -1) &&
ua.indexOf("Mobile Safari") !== -1 &&
ua.indexOf("Chrome") === -1 &&
ua.indexOf("Windows Phone") === -1
)
return false;
return window.history && "pushState" in window.history;
};
// 从中看出,window.history在chrome、mobile safari和windows phone下是支持的,但不支持安卓2.x以及安卓4.0。
// pushState直到IE10才被支持。https://www.caniuse.com/#search=pushState
// 当在不支持的浏览器中调用相关方法时,就会用刷新页面的方式来向下兼容。
History接口:
// 2个主要的属性:
history.length
// 返回在会话历史中有多少条记录,包含了当前会话页面。
// 如果打开一个新的Tab,那么这个length的值为1。
// history.length 为 3 表示当前窗口一共访问过3个网址。
history.state
// history 保存了会出发popState事件的方法,所传递过来的属性对象。
// 5个主要方法:
// 返回浏览器会话历史中的上一页,跟浏览器的回退按钮功能相同。
window.history.back();
// 指向浏览器会话历史中的下一页,跟浏览器的前进按钮相同。
window.history.forward();
// 可以跳转到浏览器会话历史中的指定的某一个记录页。
window.history.go(-1);
window.history.go(1);
window.histroy.go(num); // 跳转到相应的访问记录,其中num大于0,则前进, 小于0,则后退。
// pushState可以将给定的数据压入到浏览器会话历史栈中,该方法接收3个参数,对象,title和一串url。pushState后会改变当前页面url,但是不会伴随着刷新。
history.pushState(stateData, title, url);
window.history.pushState({foo:'bar'}, "page 2", "bar.html");
// replaceState将当前的会话页面的url替换成指定的数据,replaceState后也会改变当前页面的url,但是也不会刷新页面。
history.replaceState(stateData, title, url);
// stateData:存储JSON字符串,可以用在popstate事件中。
// title:现在大多数浏览器不支持或者忽略这个参数,最好用null代替。
// url任意有效的URL,用于更新浏览器的地址栏,并不在乎URL是否已经存在地址列表中。
// pushState是压入浏览器的会话历史栈中,会使得History.length加1,而replaceState是替换当前的这条会话历史,因此不会增加History.length。
// 1个事件
// 当调用history.go()、history.back()、history.forward()时触发;pushState()\replaceState()方法不触发
window.onpopstate = function() {}
window.history.pushState({foo:'bar'}, "page 2", "bar.html");
// 执行上述方法前:http://localhost:3000
//执行上述方法后:http://localhost:3000/bar.html
//输出window.history.state:
console.log(window.history.state);
// {foo:'bar'}
// window.history.state就是我们pushState的第一个对象参数。
// history.replaceState()方法不会改变hitroy的长度
总结:
如果用history做为前端路由的基础,那么需要用到的是history.pushState和history.replaceState,在不刷新的情况下可以改变url的地址,如果页面发生回退back或者forward时,会触发popstate事件。
优点:
缺点:
- 兼容性不如hash
- 需要后端做相应的配置,否则直接访问子页面会出现404错误
🎸 react-router源码解析
router的几个重要的npm包:
history
// window.histroy的封装,react-router中使用history.listen来监听跳转和刷新组件。
react-router
// 实现了路由的核心功能
// 主要文件有: Router, Route, Switch, withRouter, Redirect, matchPath等
react-router-dom
// 基于react-router,加入了在浏览器运行环境下的一些功能,例如:Link组件,BrowserRouter和HashRouter 组件。
// 安装react-router-dom后,不需要再显式的引用react-router, npm会自动解析react-router-dom包中package.json的依赖并安装react-router。
react-router-redux
// 用redux的方式去操作react-router。
// 例如,react-router 中跳转需要调用 router.push(path),集成了react-router-redux 就可以通过dispatch的方式使用router,跳转可以这样做 store.dispatch(push(url))。
// 本质上,是把react-router自己维护的状态,例如location、history、match等等,也交给redux管理。
// react-router v4后路由组件可以直接获取这些属性, 非路由组件就必须通过withRouter修饰后才能获取这些属性了。
// react-router v4后官方不再维护,也不需要再引入这个库。
// 目前dva还依赖该组件。
react-router-config
// 静态路由配置的小助手
react-router-native
// 基于react-router,类似react-router-dom,加入了react-native运行环境下的一些功能。
withRouter的作用:
withRouter高阶函数,使用Route来包裹组件,用来为组件添加当前路由状态。
作用1, 在组件中想使用history来控制路由跳转, 读取location、history、match状态。
<Route path='/' component={App}/>
- App组件中如果有一个子组件Foo,那么Foo就不能直接获取路由中的属性了,必须通过withRouter修饰后才能获取到,如withRouter(connect(Component))
作用2,withRouter是专门用来处理数据更新问题的,避免更新受阻。
- 在使用一些redux的的connect()或者mobx的inject()的组件中,如果依赖于路由的更新要重新渲染,会出现路由更新了但是组件没有重新渲染的情况。这是因为redux和mobx的这些连接方法会修改组件的shouldComponentUpdate。
- 在使用withRouter解决更新问题的时候,一定要保证withRouter在最外层,比如withRouter(connect(Component))
路由初始化
import { Router, Route, IndexRoute } from 'react-router'
const PrimaryLayout = props => (
<div className="primary-layout">
<header>
Our React Router 3 App
</header>
<main>
{props.children}
</main>
</div>
)
const HomePage = () => <div>Home Page</div>
const UsersPage = () => <div>Users Page</div>
const App = () => (
<Router history={browserHistory}>
<Route path="/" component={PrimaryLayout}>
<IndexRoute component={HomePage} />
<Route path="/users" component={UsersPage} />
</Route>
</Router>
)
render(<App />, document.getElementById('root'))
Router, BrowserRouter, ConnectedRouter
这几个组件没有很大的区别,BrowserRouter, ConnectedRouter都包裹Router:
- react-router v4里的BrowserRouter组件: 使用createHistory为histroy包裹Router。
// BrowserRouter.js
//省略......
import { createBrowserHistory as createHistory } from "history";
//省略......
class BrowserRouter extends React.Component {
// histroy默认为BrowserHistory
history = createHistory(this.props);
render() {
// 渲染Router
return <Router history={this.history} children={this.props.children} />;
}
}
// 省略......
export default BrowserRouter;
- react-router-redux里的ConnectedRouter组件: 监听浏览器状态,实时修改store里location的值,并包裹Router。
// ConnectedRouter.js
class ConnectedRouter extends PureComponent {
constructor(props) {
super(props)
// 省略......
// 监听history事件,当浏览器地址变化时, store.dispatch({type: LOCATION_CHANGE, payload: location}); 改变store里location的状态
this.unlisten = history.listen(handleLocationChange)
handleLocationChange(history.location, history.action, true)
}
// 省略......
render() {
const { history, children } = this.props
return (
// 渲染Router
<Router history={history}>
{ children }
</Router>
)
}
}
- 💸 Router组件: react-router最外层组件,主要负责监听history和刷新整个组件,并把router对象放入到context里。
// Router.js
class Router extends React.Component {
// 省略....
getChildContext() {
return {
router: {
...this.context.router,
history: this.props.history,
route: {
location: this.props.history.location,
match: this.state.match
}
}
};
}
// 省略....
componentWillMount() {
const { children, history } = this.props;
// 省略....
// 调用history.listen监听方法,该方法的返回函数是一个移除监听的函数
// 当url改变时,更改Router下不同path组件的match结果, 并且触发一次setState
// 作为Router的最外层,刷新的时候,所有的children都会进行一次render计算
this.unlisten = history.listen(() => {
this.setState({
match: this.computeMatch(history.location.pathname)
});
});
}
// 省略....
render() {
const { children } = this.props;
return children ? React.Children.only(children) : null;
}
}
export default Router;
- Route组件: 当url改变的时候,将path属性与改变后的url做对比,如果匹配成功,则渲染该组件的componet或者children属性所赋值的那个组件。
// Route.js
class Route extends React.Component {
// 省略....
state = {
match: this.computeMatch(this.props, this.context.router)
};
// 省略....
computeMatch(
{ computedMatch, location, path, strict, exact, sensitive },
router
) {
// 如果外层组件是<Switch>,并且当前组件已经被Switch match则直接返回这个match
if (computedMatch) return computedMatch;
invariant(
router,
"You should not use <Route> or withRouter() outside a <Router>"
);
const { route } = router;
const pathname = (location || route.location).pathname;
return matchPath(pathname, { path, strict, exact, sensitive }, route.match);
}
// 省略....
componentWillReceiveProps(nextProps, nextContext) {
// Router被刷新后,会被重新setState
// 判断当前location是否match, 如果是则返回这个match并渲染这个组件。
// 如果不是,则返回null, 如果当前组件已被渲染,则会被unmout。
// match的结构:{
// isExact: true,
// params: {},
// path: "/dashboard"
// url: "/dashboard"
// }
this.setState({
match: this.computeMatch(nextProps, nextContext.router)
});
}
// 省略....
render() {
const { match } = this.state;
const { children, component, render } = this.props;
const { history, route, staticContext } = this.context.router;
const location = this.props.location || route.location;
const props = { match, location, history, staticContext };
// match是否为null, 如果不是null, 那么根据优先级顺序component属性、render属性和children属性来渲染其所指向的React组件。
if (component) return match ? React.createElement(component, props) : null;
if (render) return match ? render(props) : null;
if (typeof children === "function") return children(props);
if (children && !isEmptyChildren(children))
return React.Children.only(children);
return null;
}
// 省略....
}
- Switch组件:
渲染第一个被location匹配到的并且作为子元素的或者, switch组件不是必须的。
class Switch extends React.Component {
// 省略....
render() {
const { route } = this.context.router;
const { children } = this.props;
const location = this.props.location || route.location;
let match, child;
React.Children.forEach(children, element => {
if (match == null && React.isValidElement(element)) {
const {
path: pathProp,
exact,
strict,
sensitive,
from
} = element.props;
const path = pathProp || from;
child = element;
match = matchPath(
location.pathname,
{ path, exact, strict, sensitive },
route.match
);
}
});
// 只返回第一个匹配到的match
return match
? React.cloneElement(child, { location, computedMatch: match }) // 把computedMatch传给Route
: null;
}
// 省略....
}
BrowserHistory的后端设置
单页面应用通常只使用一个索引文件(一般是index.html),该索引文件可以由浏览器访问。当访问其它地址时,后台需要把该请求代理到index.html里,否者会出现404错误。
- 对于一般的node后台,我们可以使用connect-history-api-fallback这个包
// 比如后台是koa
// npm install koa2-connect-history-api-fallback -D
// 省略......
const Koa = require('koa')
const history = require('koa2-connect-history-api-fallback')
app.use(history())
// 省略......
- 当部署到正式环境时,我们不再使用node后台,需要在nginx里作相应的设置:
server {
listen 8011;
location / {
root /home/www/web/quickFrontLab_test/dist/admin/;
try_files $uri /index.html; #设置try_files
}
}
keep-alive-router(待续)
react-router 5.0(待续)
有用的资源
一次性让你明白react-router所有的工作原理~
🎧 浏览器实现单页面应用的工作原理:
1.通过Hash实现前端路由(一般不用这个)
因为改变url的hash值是不会刷新页面的, 所以可以通过hash来实现前端路由,从而实现无刷新的效果。
hashchange的事件,可以监听hash的变化,我们可以通过下面两种方式来监听hash的变化:
hash的兼容性较好,因此在早期的前端路由中大量的采用,但是使用hash也有很多缺点:
2.通过Html5的history实现前端路由(目前用的)
HTML5的History接口(window.history),History接口允许我们操作浏览器会话历史记录,有一个npm包'history'就是封装了这个接口: npm install history -S
兼容性
History接口:
总结:
如果用history做为前端路由的基础,那么需要用到的是history.pushState和history.replaceState,在不刷新的情况下可以改变url的地址,如果页面发生回退back或者forward时,会触发popstate事件。
优点:
缺点:
🎸 react-router源码解析
router的几个重要的npm包:
withRouter的作用:
withRouter高阶函数,使用Route来包裹组件,用来为组件添加当前路由状态。
作用1, 在组件中想使用history来控制路由跳转, 读取location、history、match状态。
作用2,withRouter是专门用来处理数据更新问题的,避免更新受阻。
路由初始化
Router, BrowserRouter, ConnectedRouter
这几个组件没有很大的区别,BrowserRouter, ConnectedRouter都包裹Router:
渲染第一个被location匹配到的并且作为子元素的或者, switch组件不是必须的。
BrowserHistory的后端设置
单页面应用通常只使用一个索引文件(一般是index.html),该索引文件可以由浏览器访问。当访问其它地址时,后台需要把该请求代理到index.html里,否者会出现404错误。
server { listen 8011; location / { root /home/www/web/quickFrontLab_test/dist/admin/; try_files $uri /index.html; #设置try_files } }keep-alive-router(待续)
react-router 5.0(待续)
有用的资源