传统的多页模式
后端控制路由
在以前我们采用的都是一个 URL 对应一个 html 页面的方式,由后端或者服务器去做路由控制。当一个 URL 找不到对应的页面时就会返回 404.
单页模式(single page application,简称SPA)
单页应用现在是越来越流行了,单页应用和传统的多页应用相比,前端只有一个页面。在不刷新页面的前提下,通过监听 URL 的变化来渲染对应的 UI ,以此来实现多页的功能。
前端控制路由
如果使用的是 browserHistory,这里需要注意一个问题,我们在进入某个路由下的时候,页面刷新就会 404。这是因为服务器上只有一个 index.html,而你当前访问的比如 /home 在服务器上根本就找不到。所以需要让后端或者服务器把所有匹配不到的路径都指向这个 index.html,然后让前端 js 来控制路由。如 nginx 中可以这样配置
server {
...
location / {
try_files $uri /index.html
}
}
为了实现 URL 变化而页面不刷新,可以有两种方式:
- 通过hash来实现,如 http://www.mysite.com#666。hash 的变化是不会引起浏览器刷新的,可以在页面 onload 的时候获取到location.hash,找到对应的 UI 模块渲染到页面上。另外还要 onhashchange 监听下 hash 的变化,来更新 UI。
- 通过 h5 提供的 history 来实现, 如 http://www.mysite.com/666。可以在页面 onload 的时候获取到 location.pathname,根据自己的规则找到对应的 UI 模块渲染到页面上。然后通过 pushState、replaceState 和 popState 实现前进和后退(这两位仁兄也不会引起浏览器刷新),再监听一下 popState 事件来处理 UI 层的更新
history 介绍
history 是一个 JavaScript 库,可让您在 JavaScript 运行的任何地方轻松管理会话历史记录。history 抽象出各种环境中的差异,并提供最小的 API ,使您可以管理历史堆栈,导航,确认导航以及在会话之间保持状态。
history 有三种实现方式:
- BrowserHistory:用于支持 HTML5 历史记录 API 的现代 Web 浏览器(请参阅跨浏览器兼容性)
- HashHistory:用于旧版Web浏览器
- MemoryHistory:用作参考实现,也可用于非 DOM 环境,如 React Native 或测试
react-router 的基本原理
这老兄其实就是一些 react 组件的集合,作用就是实现 URL 和 UI 的同步,内部是基于 history.js 来实现的浏览器历史记录管理。它包括了一下几个核心部分:
- react-router,路由核心内容,Rrouter、Rroute、Redirect、withRouter都在这里
- react-router-dom,基于 react-router,针对浏览器做的一些封装,如 Link 组件
- react-router-native,基于 react-router,针对 ReactNative 做的一些封装
几个核心的组件的作用
- Router,包裹着 Route 组件,维护这一张路由与组件映射关系的路由表
- Route,描述了每条路由与组件的匹配规则
- Link,最终会被编译成<a>标签,它的
to、query、hash
属性会被组合在一起并做为 href 属性
我们通过一个小例子来庖丁解牛一下其中原理
import { browserHistory } from 'react-router'
React.render((
<Router history={ browserHistory }>
<Route path="/" component={App}>
<IndexRoute component={Dashboard} />
<Route path="about" component={About} />
<Route path="inbox" component={Inbox}>
{/* 使用 /messages/:id 替换 messages/:id */}
<Route path="/messages/:id" component={Message} />
</Route>
<Link to="/about">About</Link>
<Link to="/inbox">Inbox</Link>
</Route>
</Router>
), document.body)
下面我们一个一个看
Router
render() {
return (
<RouterContext.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
}}
/>
);
}
Router 组件是基于 RouterContext 来实现的,也就是 react 中的上下文对象 context,只不过这个 context 是利用 'mini-create-react-context' 这个包去创建的,和原生的 context 用法有些不同,但是目的都是让数据可以跨组件纵向传递。
Provider 是生产数据的地方,这里会放入 history、location、match 三个对象,以便子孙组件可以方便的使用
Route
render() {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Route> outside a <Router>");
const location = this.props.location || context.location;
const match = this.props.computedMatch
? this.props.computedMatch // <Switch> already computed the match for us
: this.props.path
? matchPath(location.pathname, this.props)
: context.match;
const props = { ...context, location, match };
let { children, component, render } = this.props;
// 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 (
<RouterContext.Provider value={props}>
{props.match
? children
? typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: children
: component
? React.createElement(component, props)
: render
? render(props)
: null
: typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
Link
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Link> outside a <Router>");
const { history } = context;
const location = normalizeToLocation(
resolveToLocation(to, context.location),
context.location
);
const href = location ? history.createHref(location) : "";
const props = {
...rest,
href,
navigate() {
const location = resolveToLocation(to, context.location);
const method = replace ? history.replace : history.push;
method(location);
}
};
// React 15 compat
if (forwardRefShim !== forwardRef) {
props.ref = forwardedRef || innerRef;
} else {
props.innerRef = innerRef;
}
return React.createElement(component, props);
}}
</RouterContext.Consumer>
);
<Link> 组件最终会被转译成 <a> 标签,然后给 <a> 标签加一个 click 事件,并组织 <a> 标签的默认行为(跳转),然后执行 history.push(to) 或 history.replace(to) 来实现跳转。
withRouter
return (
<RouterContext.Consumer>
{context => {
invariant(
context,
`You should not use <${displayName} /> outside a <Router>`
);
return (
<Component
{...remainingProps}
{...context}
ref={wrappedComponentRef}
/>
);
}}
</RouterContext.Consumer>
);
withRouter 是一个高阶组件,它的入参是一个组件假设为 A 。经过它封装之后返回一个新的组件,这个新组件会把 history、location、match 三个对象当做属性传递给组件 A。这也是为什么我们使用 withRouter 包装之后的组件,可以在内部使用 props.xxx 调用这三者的原因。
那么 withRouter 这个高阶组件又是从哪里获取的这三个对象呢?
还记得之前在 Router 里,使用 Provider 提供的这三个对象吗? 这里使用了 Consumer 来消费数据,其实就是通过 react 的上下文对象 context 来跨组件传递数据的。
问题
<Link>标签和<a>标签有什么区别?
react-router:只更新变化的部分从而减少DOM性能消耗
而 <a> 标签是整个页面刷新,重新渲染