HTML5 history
history.pushState
history.pushState 方法向当前浏览器会话的历史堆栈中添加一个状态(不刷新页面)
//state:状态对象是一个JavaScript对象,它与pushState()创建的新历史记录条目相关联,可通过popstate事件监听
//title: 目前浏览器都忽略
history.pushState(state, title, url)
popstate
通过popstate 监听回退
history.pushState({key: Date.now()}, '', '/time')
window.addEventListener('popstate', e => console.log(e), false)
再次载入到time.html url 时可以在console中看到这个 state对象
histoty.replaceState
histoty.replaceState 修改当前历史记录实体(不刷新页面,浏览器不记录该操作)
history.replaceState(state, title, url)
HashHistory
location.hash = 'time'
hashchange
通过hashchange 监听回退
window.addEventListener('hashchange', e => console.log(e), false)
history.js
history.js 是一个独立的第三方类库,兼容不同的浏览器,统一API,react-router核心依赖history.js
使用例子
import React from 'react';
import {createBrowserHistory} from 'history'
const history = createBrowserHistory()
//监听路由变化
history.listen(data => {
console.log(data)
})
export default props => {
return <a onClick={() => history.push('time')}>go</a>
}
history.js暴露3个方法
createBrowserHistory
Browser history 是 React Route 推荐的 history。它使用浏览器中的 History API 用于处理 URL。
Browser history 需要服务端配合设置,例如我们刷新example.com/a,服务器会去寻找根目录下的a.html,实际上不存在这样的文件,我们的单页应用只有一个index.html
nginx配置,访问任何url都指向index.html
server {
...
location / {
try_files $uri /index.html
}
}
部分源码
查看 createBrowserHistory 返回的history对象
var history = {
length: globalHistory.length,
action: 'POP',
location: initialLocation,
createHref: createHref,
push: push,
replace: replace,
go: go,
goBack: goBack,
goForward: goForward,
block: block,
listen: listen
};
return history;
push方法背后就是调用了HTML5 histoty.pushState
function supportsHistory() {
var 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;
}
function push(path, state) {
//省略...
//canUseHistory = supportsHistory()
if (canUseHistory) {
//globalHistory = window.history;
globalHistory.pushState({
key: key,
state: state
}, null, href);
}
//省略...
}
通过history.js我们就有了统一的api来操作history
createHashHistory
Hash history 使用 URL 中的 hash(#)部分去创建形如 example.com/#/some/path 的路由
Hash history 服务端不需要配置
createMemoryHistory
Memory history 不会在地址栏被操作或读取,非常适合测试和其他的渲染环境像React Native
React-Router
React-Router可以实现url和页面的同步,也就是history和component同步,举个例子
import React from 'react';
import {BrowserRouter as Router, Route, Link} from 'react-router-dom'
class App extends React.Component{
render(){
return (
<Router>
<Route exact path="/" component={Index}/>
<Route path="/list" component={List}/>
</Router>
)
}
}
这里应用了react-router-dom router-react 和 react-router-dom 的区别如下
- react-router实现了路由的核心功能
- react-route-dom 将于 react-router 实现了浏览器下的一些功能,例如Link组件、BrowserRouter组件(基于HTML5 history)、HashRouter
那么 history变化 React是如何更新组件的呢
React Context
这里插播一下React Context,先看一个🌰
React Context可以让我们不需要层层传递props又可以获取到父类的数据
//context.js
import React from 'react';
export const data = {
id: 0
}
export const DataContext = React.createContext(data)
//addKey.js
import React from 'react';
import {DataContext} from './context';
function AddKey() {
return (
<DataContext.Consumer>
{({id, addId}) => (
<button onClick={addId}>
{id}
</button>
)}
</DataContext.Consumer>
)
}
export default AddKey
//index.js
import React from 'react';
import {render} from 'react-dom'
import {DataContext, data} from './context';
import AddKey from './addKey';
class App extends React.Component {
constructor(props) {
super(props);
this.addId = () => {
this.setState(state => ({
id: state.id + 1
}))
}
this.state = {
id: data.id,
addId: this.addId,
}
}
render() {
return (
<DataContext.Provider value={this.state}>
<AddKey />
</DataContext.Provider>
);
}
}
render(<App/>, document.getElementById('root'))
Router的实现
Router组件是一个路由容器
var Router =
/*#__PURE__*/
function (_React$Component) {
_inheritsLoose(Router, _React$Component);
Router.computeRootMatch = function computeRootMatch(pathname) {
return {
path: "/",
url: "/",
params: {},
isExact: pathname === "/"
};
};
function Router(props) {
var _this;
_this = _React$Component.call(this, props) || this;
_this.state = {
location: props.history.location
}; // This is a bit of a hack. We have to start listening for location
// changes here in the constructor in case there are any <Redirect>s
// on the initial render. If there are, they will replace/push when
// they mount and since cDM fires in children before parents, we may
// get a new location before the <Router> is mounted.
_this._isMounted = false;
_this._pendingLocation = null;
if (!props.staticContext) {
_this.unlisten = props.history.listen(function (location) {
if (_this._isMounted) {
_this.setState({
location: location
});
} else {
_this._pendingLocation = location;
}
});
}
return _this;
}
var _proto = Router.prototype;
_proto.componentDidMount = function componentDidMount() {
this._isMounted = true;
if (this._pendingLocation) {
this.setState({
location: this._pendingLocation
});
}
};
_proto.componentWillUnmount = function componentWillUnmount() {
if (this.unlisten) this.unlisten();
};
_proto.render = function render() {
return React.createElement(context.Provider, {
children: this.props.children || null,
value: {
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}
});
};
return Router;
}(React.Component);
{
Router.propTypes = {
children: PropTypes.node,
history: PropTypes.object.isRequired,
staticContext: PropTypes.object
};
Router.prototype.componentDidUpdate = function (prevProps) {
warning(prevProps.history === this.props.history, "You cannot change <Router history>") ;
};
}
可以看到router利用了react context向其children 共享属性,通过 render 可以看到
_proto.render = function render() {
return React.createElement(context.Provider, {
children: this.props.children || null,
value: {
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}
});
};
//对应
_proto.render = function render() {
return(
<context.Provider value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}>
{this.props.children || null}
</context.Provider>
)
};
监听路由变化,调用了history.js listen API, 路由变化时,执行setState, 将当前的路由数据传递到子的Route组件。我们知道父组件执行setState,父组件下的子组件会渲染
_this.unlisten = props.history.listen(function (location) {
if (_this._isMounted) {
_this.setState({
location: location
});
} else {
_this._pendingLocation = location;
}
});
打印看看这个location的数据结构
{
hash: ""
key: "ud9ozl"
pathname: "/state"
search: ""
state: undefined
}
Route
Route定义实际的路由,根据Router路由容器传递的location.pathname当前Route的path进行匹配,匹配上了render
var Route =
/*#__PURE__*/
function (_React$Component) {
_inheritsLoose(Route, _React$Component);
function Route() {
return _React$Component.apply(this, arguments) || this;
}
var _proto = Route.prototype;
_proto.render = function render() {
var _this = this;
return React__default.createElement(context.Consumer, null, function (context$1) {
!context$1 ? invariant(false, "You should not use <Route> outside a <Router>") : void 0;
var location = _this.props.location || context$1.location;
var match = _this.props.computedMatch ? _this.props.computedMatch // <Switch> already computed the match for us
: _this.props.path ? matchPath(location.pathname, _this.props) : context$1.match;
var props = _extends({}, context$1, {
location: location,
match: match
});
var _this$props = _this.props,
children = _this$props.children,
component = _this$props.component,
render = _this$props.render; // Preact uses an empty array as children by
// default, so use null if that's the case.
if (Array.isArray(children) && children.length === 0) {
children = null;
}
return React__default.createElement(context.Provider, {
value: props
}, props.match ? children ? typeof children === "function" ? evalChildrenDev(children, props, _this.props.path) : children : component ? React__default.createElement(component, props) : render ? render(props) : null : typeof children === "function" ? evalChildrenDev(children, props, _this.props.path) : null);
});
};
return Route;
}(React__default.Component);