react-router 从 2 / 3 升级到 4.x.x
1. 说明
react-router 版本更新到 4.x.x 已经有一段时间了,但是由于 API 改动较大,为了项目的稳定,一直没有更新,最近有了空闲时间,整体对 react 的项目进行了一次升级,这里记录一下 如何从 react-router 2.x.x/3.x.x 迁移到 react-router 4.x.x
本次使用 react-router 版本为 4.1.0
React-router-v4 - Webpack 实现按需加载(code-splitting)
导航
- 说明
- 重要文件版本
- 新的API说明
- 从 2 / 3 迁移到 4.x.x
2. 重要文件版本
{
"postcss-loader": "^1.3.3",
"react-hot-loader": "^3.0.0-beta.7",
"webpack": "^2.6.1",
"webpack-dev-server": "^2.5.0",
"webpack-merge": "^4.1.0",
"es6-promise": "^4.1.0",
"history": "^4.6.3",
"isomorphic-fetch": "^2.2.1",
"pure-render-decorator": "^1.2.1",
"react": "^15.6.1",
"react-addons-css-transition-group": "^15.6.0",
"react-dom": "^15.6.1",
"react-redux": "^5.0.5",
"react-router-dom": "^4.1.1",
"react-router-redux": "^5.0.0-alpha.6",
"react-tap-event-plugin": "^2.0.1",
"redux": "^3.7.0",
"redux-thunk": "^2.2.0"
}
注意:有很多 webpack 配置需要的插件没有列出,因为改动不是很大,对项目运行没有很大的影响,具体的 package.json 可以看源码,这个是旧版的模板,新版本会在近期提交(新版源码)
postcss-loader
插件 最新版本已经到了 2.x.x ,但是最新版与 1.x.x配置使用上有出入,如果使用新版本遇到报错,可以查看 官方文档 或者 这篇博客, 大概意思就是不能像之前一样使用webpack.LoaderOptionsPlugin
配置 这个插件 例如 配置autoprefixer
,可以添加一个postcss.config.js
配置文件,或者 在 写成下边这样,我这里还是使用的 1.x.x 版本
{
loader:"postcss-loader",
options: {
sourceMap: true,
plugins: (loader) => [
require('autoprefixer')(),
]
}
}
webpack
使用的是 2.x.x 的版本,因为测试的时候出了点 BUG ,所以没有升级到最新的 3.x.xreact
相关组件都升级到最新版本,其中 为配合 react-router 4 的使用react-router-redux
使用的是5.0.0-alpha.6
, 而且增加了history
组件react-router-dom
代码是基于react-router
的 而且做了升级,只用下载react-router-dom
就可以了
3. 新的 API 说明
在这里查看官方文档
1. <Router>
Router 是所有路由组件共用的底层接口,用来与Redux状态管理库的history 保持同步,但是在
4.x.x
版本中一般不使用这个组件,而是使用<BrowserRouter> <HashRouter>
等
<Router history={history}>
<App/>
</Router>
2. <BrowserRouter>
使用 HTML5 提供的 history API 来保持 UI 和 URL 的同步,一下是他的属性
basename: 设置基准的 URL,使用场景:应用部署在 服务器的二级目录,将其设置为目录名称,不能以
/
结尾,设置之后跳转的页面都会加上 basename的前缀forceRefresh: 是否强制刷新页面,用于适配不支持 h5 history 的浏览器
getConfirmation: 弹出浏览器的确认框,进行确认
3. <Route>
与 2 / 3 版本的 Route 作用一致,都是在 location 匹配 path 的路径的时候,渲染指定组件,但是写法上有变化,而且增加了一些设定
注意:4.x.x 版本的
<Route>
不在有onEnter onLeave
这样的路由钩子函数,如果需要这个功能,要在 Route 对应组件中 写在componentWillMount
或者componentWillUnmount
中渲染内容方法-1:
component
类似 2 / 3 中的 component 属性,值为一个 react 组件,只有地址匹配的时候才会渲染组件
// 定义组件
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return <span>App</span>;
}
}
// Route
<Route component={App}/>
- 渲染内容方法-2:
render
值可以选择传一个在地址匹配时被调用的函数,而不是创建一个组件,但是需要一个返回值,返回一个组件或者null
<Route render={(props) => (
<App>
<SomeCom>
</App>
)/>
渲染内容方法-3:
children
与render
一样,但是不会匹配地址,路径不匹配时 URL的match 值为 null,可以用来根据路由是否匹配动态调整UI绑定在 Route 上的组件或者函数都会获得以下的属性
component
绑定的组件可以通过 this.props.[history, location, match] 获取;render, children
下的函数;会得到 param = {match, location, history} 的参数
// 伪代码
history: {
goBack(), // 浏览器回退
goForward(), // 前进
push(), // 新增跳转
replace(), // 覆盖跳转
}
location: {
hash, // hash (#123)
pathname, // 路径 (/home)
search, // (?id=123)
state, // ({name: 'name', id: 'id'})
// query, // 4.x.x 没有 query 了,要从 search 中解析
}
match: {
isExact, // 是否整个URL都需要匹配
params, // 路径参数,通过解析URL中动态的部分获得的键值对
path, // 路径格式
url, // URL匹配的部分
params, //
}
path
属性, 用于匹配 location 路径 如果没有 path 属性则总是匹配exact
属性 是否需要完全匹配 ,不会判断末尾的/
/*
以下两个就是不完全匹配,只有前半部分 一致 /one
如果没有设置 exact=true,则可以进入到 path 对应的组件,
如果设置 exact 则 不能渲染组件
*/
path: /one
location.pathname : /one/two
strict
属性 用来强制判断路径结尾是否含有/
, 只有path 和 loation.pathname 的结尾都含有或者都不含有/
才会匹配关于路由嵌套,4.x.x 不能像以前的版本那样嵌套了,需要新的方式,会在下边的 从 2 / 3 迁移到 4.x.x 中有演示
4. <Redirect>
与 2 / 3 版本一样都是用来重定向到新的地址,默认会覆盖访问记录中的原地址,但是多了一些属性
to={string|object}
重定向的目标,可以是字符串,也可以是一个地址的对象from={string}
匹配需要重定向的地址push
bool 表示是否需要不替换地址,值为true的时候,不会把访问记录中的地址覆盖
<Redirect push to={{pathname: '/login', search: '?id=123'}}>
5. <Switch/>
用来渲染匹配地址的第一个 或者 , 可以用来配置过度动画,更多介绍看这里
6. <Link>
与 2 / 3 相同,增加了属性 replace 表示是否替换掉原地址
7. withRouter
很重要的一个功能,将普通组件用
withRouter
包裹后,组件就会获得 location history match 三个属性,可以用来直接为子组件提供 历史相关的功能
export default withRouter(App);
// App.js
const {history, location, match} = this.props;
4. 从 2 / 3 迁移到 4.x.x
由于API 改动比较大,使用方式也有变化,因此
1. 构建工具
webpack 的配置文件方面没有什么需要改动的,如果你的插件
postcss-loader
更新了,则需要修改一下配置部分
// webpack.config.js
rules: [
{
test: /\.(scss|sass|css)$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
sourceMap: false,
plugins: (loader) => [
autoprefixer(config.autoConfig) // autoConfig 为 autoprefixer 的配置
]
}
},
'sass-loader' + config.sassLoaderSuffix // sass的配置
]
}
]
...
// 取消掉之前的 postcss 插件配置
plugins: [
// new webpack.LoaderOptionsPlugin({
// options: {
// context: '/',
// minimize: true,
// postcss: [autoprefixer(config.autoConfig)]
// }
// }),
]
2. app 入口文件 app/js/index.js
改动不大
/*
app/js/index.js
入口文件, 配置 webpack 热加载模块
*/
import '../scss/index.scss'; // 引入样式
import React from 'react';
import ReactDOM from 'react-dom';
import {AppContainer} from 'react-hot-loader';
import injectTapEventPlugin from 'react-tap-event-plugin';
// 引入路由配置模块
import Root from './routes.js';
const mountNode = document.getElementById('app'); // 设置要挂在的点
// react 的插件,提供onTouchTap() // 更新后没有测试是否好使
injectTapEventPlugin();
// 封装 render
const render = (Component) => {
ReactDOM.render((
<AppContainer>
<Component/>
</AppContainer>
), mountNode);
};
render(Root); // 初始化
console.log(process.env.NODE_ENV);
if (module.hot && process.env.NODE_ENV !== 'production') {
module.hot.accept('./routes.js', (err) => {
if (err) {
console.log(err);
}
/*
卸载 react 模块后 重装
*/
ReactDOM.unmountComponentAtNode(mountNode);
render(Root);
});
}
3. Root 基础的路由配置文件 app/js/routes.js
这部分变动很大,上一个版本还有一个叫Root.js 的文件 用来协助配置 路由,这个版本合并在一起
/*
Root, Router 配置
*/
import React from 'react';
import {Provider} from 'react-redux';
import {
/*
注意:这里也可以使用 BrowserRouter ,但是为了配合 redux 使用,引入了 react-router-redux,提供了一个用来关联的 ConnectedRouter
*/
// BrowserRouter as Router,
Route,
Switch,
Redirect
} from 'react-router-dom';
import {ConnectedRouter} from 'react-router-redux'; // 版本需要 5.0.0-alpha.6
import createHistory from 'history/createBrowserHistory';
import store from './store/index';
import {App, Home, Test} from './containers/index';
const history = createHistory();
// Router 下边只能有一个节点
const Root = () => (
<Provider store={store}>
<ConnectedRouter history={history} >
<div>
<Switch>
<Route path="/" render={(props) => (
<App>
<Switch>
<Route path="/" exact component={Home}/>
<Route path="/home" component={Home}/>
<Route path="/test" component={Test}/>
<Redirect from="/undefined" to={{pathname: '/', search: '?mold=redirect'}}/>
</Switch>
</App>
)}/>
<Route render={() => (<Redirect to="/"/>)}/>
</Switch>
</div>
</ConnectedRouter>
</Provider>
);
export default Root;
4. 路由嵌套的写法
4.x.x 定义每一个 Route 都是普通的 react 组件,所有使用的时候也要像普通的组件一样,嵌套的时候,可以按照下边的方式
// 定义外层路由
const Root = () => {
<Provider store={store}>
<ConnectedRouter history={history} >
<Route path="/" exact component={App}/>
</ConnectedRouter>
</Provider>
}
// App.js 组件中定义在App下的路由
import {Route, Link} from 'react-router-dom';
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div id="app-container">
<header className="app-header">成员列表</header>
<div className="app-body">
<Link to="/test">点击进入 Test 页面</Link>
<Route path="/test" component={Test}/>
</div>
</div>
);
}
}
上边这样的写法,与没有升级前的写法变化很大,但是更加灵活了,不过如果用来版本迁移,工作量会很大,下边是一个用于兼容上个版本代码的写法,可以把所有路由集中在一个文件中管理
// 方法就是在需要有子级路由的地方使用 render 方法,方法中使用父级路由对应的组件将其他子级路由包裹住
const Root = () => (
<Provider store={store}>
<ConnectedRouter history={history}>
<div>
<Switch>
<Route path="/" render={(props) => (
<App>
<Switch>
<Route path="/" exact component={Login}/>
<Route path="/login" component={Login}/>
<Route path="/unable" component={Unable}/>
<Route path="/show" render={(props) => (
<Show>
<Switch>
<Route path="/show" exact component={Home}/>
<Route path="/show/test" component={Test}/>
</Switch>
</Show>
)}/>
<Route path="/404" component={NotFoundPage}/>
</Switch>
</App>
)}/>
</Switch>
</div>
</ConnectedRouter>
</Provider>
);
5. store 的配置 app/js/store
增加了 router 的中间件
// store/configureStore.js
import { compose, createStore, applyMiddleware } from 'redux';
import { routerMiddleware } from 'react-router-redux'; // 新增 route 中间件
// 引入thunk 中间件,处理异步操作
import thunk from 'redux-thunk';
import createHistory from 'history/createBrowserHistory'; // 引入 history
export const history = createHistory();
const routeMiddleware = routerMiddleware(history);
const middleware = [routeMiddleware, thunk];
/*
辅助使用chrome浏览器进行redux调试
*/
const composeEnhancers =
process.env.NODE_ENV !== 'production' &&
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Specify here name, actionsBlacklist, actionsCreators and other options
}) : compose;
/*
调用 applyMiddleware ,使用 middleware 来增强 createStore
*/
const configureStore = composeEnhancers(
applyMiddleware(...middleware)
)(createStore);
export default configureStore;
// store/index.js
import configureStore from './configureStore';
import reducer from '../reducers';
// 给增强后的store传入reducer
const store = configureStore(reducer);
export default store;
6. reducer 配置 app/js/reducers/index.js
与升级前没有区别
// 引入reducer
import {combineReducers} from 'redux';
import {routerReducer} from 'react-router-redux';
import rootReducer from './rootReduer';
// 合并到主reducer
const reducer = combineReducers({
rootReducer,
routing: routerReducer
});
export default reducer;
7. 组件中使用
写在
<Router component={Component}>
中的组件自动获得,location,history,match 三个属性普通组件使用 需要通过父组件传递或者通过withRouter
import React, { Component } from 'react';
import {withRouter} from 'react-router-dom';
import '../../../scss/app.scss';
class AppCom extends Component {
constructor(props) {
super(props);
}
render() {
// 获得的 location 属性
const path = this.props.location.pathname;
return (
<div id="app-container">
<header className="app-header">地址:{path}</header>
<div className="app-body">
{this.props.children}
</div>
</div>
);
}
}
export default withRouter(AppCom)
8. 手动跳转页面与路由钩子函数onEnter onLeave
上一个版本中大量使用了
browserHistory
进行页面跳转,这个版本中需要使用 传入的 history 来进行跳转新版本中路由的onEnter onLeave 方法取消,可以通过组件的 componentWillMount 和 componentWillUnmount 实现
import React, { Component } from 'react';
import {withRouter} from 'react-router-dom';
import confPut from './utils/confPut';
import '../../../scss/home.scss';
class TestCom extends Component {
constructor(props) {
super(props);
}
componentWillMount() {
// 代替 原 Route 组件的 onEnter()
}
componentWillUnmount() {
// 代替 原 Route 组件的 onLeave()
}
handleClick = () => {
confPut('add', 'will');
this.props.history.push({
pathname: '/home',
search: '?name=testname'
});
}
render() {
console.log(this.props);
return (
<div className="test-container">
this is Test Page
<button onClick={this.handleClick}>点击回到</button>
</div>
);
}
}
export default withRouter(TestCom);
5. 总结
版本升级工作还在进行中,应该还会有其他的问题出现,这里只是一个阶段的记录,还有一些像 code splitting 还没有尝试,留着下一阶段试验。