前言
在用 react-hot-loader v1.3 的时候有些深层组件不会很完美的热更新(可能是我使用有问题)。然后在 首页中看到 React Hot Loader 3 is on the horizon
,便想换成这个,结果就开启了一周的踩坑之路...
模块依赖
务必升级最新的
这版修复了错误栈无法跟踪到内层组件的问题,否则内部组件报错只能追溯到 AppContainer。Warning: React.createElement: type should not be null, undefined, boolean, or number. It should be a string (for DOM elements) or a ReactClass (for composite components). Check the render method of `AppContainer`. Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object. Check the render method of `AppContainer`.
截止 2016-08-08 00:00,依赖模块的版本分别是:
webpack | ^1.13.1 |
webpack-dev-server | ^1.14.1 |
react-hot-loader | ^3.0.0-beta.2 |
babel-core | ^6.13.2 |
babel-loader | ^6.2.4 |
注:我没有用到 Redux。
升级方法
因为目前 React Hot Loader 3 还在测试阶段,没有文档,所以需要在 这个 issue 中提到的两个 commit 中查看升级方法:
下面我来总结一下,具体要做哪些改动:
1. 安装 React Hot Loader 3
$ npm install --save-dev react-hot-loader@^3.0.0-beta.2
2. 修改 .babelrc
在 .babelrc
中添加 react-hot-loader/babel
插件
{ "presets": ["es2015", "react"], "plugins": ["react-hot-loader/babel"] }
需要注意的一点是,.babelrc 配置不需要再分 dev 环境:
... "env": { "development": { "plugins": ["react-hot-loader/babel"] } } ...
因为作者已经在 react-hot-loader 模块中加了 process.env.NODE_ENV
判断,因此它不会在生产环境运行。
3. 修改入口及路由组件
以 React + React-Router 为例,目录结构如下:
singlePageView ├── config│ ├── App.jsx # 渲染│ ├── Routes.js # routes│ └── config.js├── index.html├── index.jsx # 入口文件└── views # 单页 views ├── application │ ├── Home │ │ └── index.jsx │ └── Layout │ ├── Header.jsx │ ├── Menu.jsx │ └── index.jsx └── users ├── Business └── Employee └── index.jsx
a) 入口 index.jsx:
// index.jsx// 增加 AppContainerimport { AppContainer } from 'react-hot-loader' import React from 'react' import { render } from 'react-dom' //放在 ./config/App.jsx 中 import App from './config/App' const appElem = document.querySelector('#app') // 给原来的 包裹一层 AppContainer render( , appElem ) if (module.hot) { // If you use Webpack 2 in ES modules mode, you can // use here rather than require() a . // 如果用 ES 模块模式的 Webpack 2,可以直接用 module.hot.accept('./config/App', () => { const NextApp = require('./config/App').default render( , appElem ) }) }
b) ./config/App.jsx:
// App.jsximport React, { Component } from 'react' import { browserHistory, Router } from 'react-router' import routes from './Routes' export default class App extends Component { render () { return} }
c) ./config/Routes.js
这里我用了 webpack 的 code splitting (require.ensure
),因此必须用 routes 的对象形式,而不是 JSX。
// Routes.jsimport Layout from '../views/application/Layout' import Home from '../views/application/Home' const routes = { path: '/manage-admin', component: Layout, indexRoute: { component: Home }, childRoutes: [{ path: 'users/employee', getComponent (nextState, cb) { require.ensure([], require => { const Employee = require('../views/users/Employee') /** * 注意:babel 6 不再暴露默认的 `module.exports` * 可以使用 babel-plugin-add-module-exports 插件 * 或者像下面这样直接使用 Module.default */ cb(null, Employee.default) }) } }] } export default routes
4. 修改 webpack.dev.config.js
因为在 .babelrc
中加上了 react-hot-loader/babel
插件,针对 js/jsx 的 loaders 可以去掉 'react-hot':
// ...loaders: [{ test: /\.jsx?$/i, // loaders: ['react-hot', 'babel'], loaders: ['babel'], exclude: /(node_modules|bower_components)/ } // ...
注意点: 在 entry 中要加上 react-hot-loader/patch
这个脚本,而且必须先于页面引用的 JS 文件之前运行。
react-hot-loader/patch
,否则放到 entry 中,是无法进行热更新的。 错误示范:
// ...entry: { vendor: ['react', 'react-dom', 'react-router', 'react-tap-event-plugin', 'babel-polyfill'], 'manage-admin': [ 'webpack-dev-server/client?http://localhost:8080', 'webpack/hot/only-dev-server', // patch放在这里无效,因为 vendor 最先加载,且包含 react 'react-hot-loader/patch', './src/views/manage-admin/index.jsx' ] }, // ...
正确方法:
// ...entry: { // patch 要放在 vendor 最前面 vendor: ['react-hot-loader/patch', 'react', 'react-dom', 'react-router', 'react-tap-event-plugin', 'babel-polyfill'], 'manage-admin': [ 'webpack-dev-server/client?http://localhost:8080', 'webpack/hot/only-dev-server', './src/views/manage-admin/index.jsx' ] }, // ...
按照 issue 中配置修改到此结束,下面介绍一些解决遗留问题的黑科技
遗留问题
根据 其中 巨巨提出的解决方案整理
1. 避免 react-hot-loader 失效
所有的组件必须用 const
来定义的,避免组件引用被修改,否则会使 react-hot-loader 失效。
2. 避免 react-router 输出报错信息
这个版本 react-router 和 react-hot-loader 3 不太兼容,在每次热更新时 react-router 会报错: Warning: [react-router] You cannot change <Router routes>; it will be ignored
可以通过引入一个空对象,用 Object.assign 合并 routes 到空对象上,避免「change <Router routes>
」:
创建 ./config/referentially-equal-root-route.js
// referentially-equal-root-route.jsexport default { }
// Routes.js// ...import routeSource from './Routes' import referenctiallyEqualRootRoute from './referentially-equal-root-route' const routes = Object.assign(referenctiallyEqualRootRoute, routeSource) render () { return} // ...
这样修改以后, react-router 的报错便不再出现了。
3. 为异步(Code Splitting)路由组件提供热更新
异步路由组件在修改代码后,看控制台显示热更新完成,但组件却没有变化,除非重新加载一遍这个异步组件(后退前进 或 从别的路由路径切换到这个更新的路由路径),才会更新。
(这个解决方法略微蛋疼)
在 ./config/Routes.js 中我们只要引用任何异步模块:
// ...getComponent (nextState, cb) { require.ensure([], require => { const Employee = require('../views/users/Employee') cb(null, Employee.default) }) } // ...
都需要在 ./config/App.jsx (即 Root
组件) 中 require 一遍:
// ...if (process.env.NODE_ENV !== 'production') { // ... 有多少异步模块就 require 多少 require('../views/users/Employee') } export default class App extends Component { render () { return} }
这样才能在开发环境中,对异步模块进行热更新。
(记得在 npm run build 脚本命令中加上NODE_ENV=production
) 最后
上述代码均在开发和生产环境下测试通过,如果有问题,可以在下方 Disqus 评论中问我,或者直接看 里的内容找解决办法。
遗留问题如果没遇到可以不用解决,React Hot Loader 3 正式出来后这些问题应该都不存在了...