在用 react-hot-loader v1.3 的時候有些深層組件不會很完美的熱更新(多是我使用有問題)。而後在 react-hot-loader 首頁中看到 React Hot Loader 3 is on the horizon
,便想換成這個,結果就開啓了一週的踩坑之路...javascript
務必升級最新的 React-Hot-Loader v3.0.0-beta.3
這版修復了錯誤棧沒法跟蹤到內層組件的問題,不然內部組件報錯只能追溯到 AppContainer。html
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,依賴模塊的版本分別是:java
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。node
由於目前 React Hot Loader 3 還在測試階段,沒有文檔,因此須要在 gaearon/react-hot-boilerplate#61 這個 issue 中提到的兩個 commit 中查看升級方法:react
下面我來總結一下,具體要作哪些改動:webpack
$ npm install --save-dev react-hot-loader@^3.0.0-beta.2
.babelrc
在 .babelrc
中添加 react-hot-loader/babel
插件git
{ "presets": ["es2015", "react"], "plugins": ["react-hot-loader/babel"] }
須要注意的一點是,.babelrc 配置不須要再分 dev 環境:github
...
"env": { "development": { "plugins": ["react-hot-loader/babel"] } } ...
由於做者已經在 react-hot-loader 模塊中加了 process.env.NODE_ENV
判斷,所以它不會在生產環境運行。web
以 React + React-Router 爲例,目錄結構以下:npm
singlePageView
├── config
│ ├── App.jsx # 渲染 <Router> │ ├── 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
// index.jsx // 增長 AppContainer import { AppContainer } from 'react-hot-loader' import React from 'react' import { render } from 'react-dom' // <Router> 放在 ./config/App.jsx 中 import App from './config/App' const appElem = document.querySelector('#app') // 給原來的 <App /> 包裹一層 AppContainer render( <AppContainer> <App /> </AppContainer>, appElem ) if (module.hot) { // If you use Webpack 2 in ES modules mode, you can // use <App /> here rather than require() a <NextApp />. // 若是用 ES 模塊模式的 Webpack 2,能夠直接用 <App /> module.hot.accept('./config/App', () => { const NextApp = require('./config/App').default render( <AppContainer> <NextApp /> </AppContainer>, appElem ) }) }
// App.jsx import React, { Component } from 'react' import { browserHistory, Router } from 'react-router' import routes from './Routes' export default class App extends Component { render () { return <Router history={browserHistory} routes={routes} /> } }
這裏我用了 webpack 的 code splitting (require.ensure
),所以必須用 routes 的對象形式,而不是 JSX。
// Routes.js import 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
由於在 .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 文件以前運行。
好比,我一個單頁有 vendor.js & entry.js 最早加載的是 vendor,所以必須放在 vendor 最前面引用 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 中配置修改到此結束,下面介紹一些解決遺留問題的黑科技
根據 https://github.com/gaearon/react-hot-boilerplate/pull/61 其中 @dferber90 巨巨提出的解決方案整理
全部的組件必須用 const
來定義的,避免組件引用被修改,不然會使 react-hot-loader 失效。
這個版本 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.js export default {}
// Routes.js // ... import routeSource from './Routes' import referenctiallyEqualRootRoute from './referentially-equal-root-route' const routes = Object.assign(referenctiallyEqualRootRoute, routeSource) render () { return <Router routes={routes} /> } // ...
這樣修改之後, react-router 的報錯便再也不出現了。
異步路由組件在修改代碼後,看控制檯顯示熱更新完成,但組件卻沒有變化,除非從新加載一遍這個異步組件(後退前進 或 從別的路由路徑切換到這個更新的路由路徑),纔會更新。
(這個解決方法略微蛋疼)
在 ./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 <Router history={browserHistory} routes={routes} /> } }
這樣才能在開發環境中,對異步模塊進行熱更新。
(記得在 npm run build 腳本命令中加上 NODE_ENV=production
)
上述代碼均在開發和生產環境下測試經過,若是有問題,能夠在下方 Disqus 評論中問我,或者直接看 https://github.com/gaearon/react-hot-boilerplate/pull/61 裏的內容找解決辦法。
遺留問題若是沒遇到能夠不用解決,React Hot Loader 3 正式出來後這些問題應該都不存在了...