從 Redux 1 升級到 3

kazenokaze - Akari x Aria (id=53131016)

好久不見,在 資服創新競賽 結束後,我耍廢了好一段時間,更準確來說,從校內專題比賽結束後就開始耍廢了 XD,Hexo 的開發停滯這麼久真是不好意思 てへぺろ(・ω<)。

我在暑假時開始進入 Dcard,利用 React + Redux + React Router 寫了一個和現行網站完全分離的行動版網站,那時候 Redux 的文件還非常不齊全,很多東西都得自己摸索,不過拜 @tomchentw 所賜,解決了很多架構上的問題。

然而在我跑去寫 V2 API 的這兩個月,Redux 竟然從 1.0.0 升級到 3.0.4 了!React Router 也終於推出 1.0.0 了!原本以為新版 Admin panel 也能沿用相同的配置,沒想到還是有一些部分得推倒重來,不禁令人感嘆前端變化之快。

Redux

先說說 Redux 究竟有什麼差別吧,其實 Redux 本身的修改倒還好,只要看 Changelog 就 OK 的程度,但是它周邊的東西就麻煩了。react-redux 從 0.2.1 跳到 4.0.0,此外還多了一堆伙伴,Redux 生態圈的形成也他媽的太快了吧!

react-redux

react-redux 本身的 API 很簡單,但是 Changelog 卻能出現好幾次的 Breaking Change,這東西是有這麼容易 Break 逆!

主要的差別在於 connect function 變得更好用了,過去只能用來綁定 props,現在也能綁定 action,如此一來就能省去 bindActionCreators 的過程了。

1
2
3
4
5
@connect(state => ({
// props
}), {
// actions
})

另一點則是 Provider class,原本的寫法實在不怎麼優雅,this.props.children 傳入的是一個函數,而現在可以直接傳入 React element。

1
2
3
4
5
React.render(
<Provider store={store}>
<App/>
</Provider>
, document.getElementById('root'));

redux-router

redux-router 是用來整合 ReduxReact Router 的,他做的事情很簡單,就是把 Router 的資料存在 Redux store 裡,但是實際上用起來卻有不少問題,所以我先說結論:

改用 redux-simple-router,不然就不要把 Router 狀態存在 Redux store 裡。

你一定會很好奇我怎麼會去推薦一個版號連 0.1 都不到的超新星套件,但是 redux-router 這貨真的很坑,為了你的肝指數著想,請等到 1.0.0 正式版推出後再考慮它吧。

說了這麼多,這東西究竟有什麼問題呢?咱們來一一列舉吧:

  • redux-routerredux-simple-router 最大的不同就在於:前者把完整的 Router 狀態都存到了 Redux store,而後者只存了路徑,在 Dehydration/Rehydration 時,根本沒辦法處理 redux-router 的資料,只能忽略。
  • redux-router 的觸發時機早於 Rehydration,因此有些資料尚未初始化而發生錯誤。
  • redux-routergetRoutes 參數半壞,你如果想把 Redux store 傳進 routes 裡的話,自己實作比較保險。
  • redux-router 的原始碼不知道在複雜幾點的,而 redux-simple-router 的原始碼不到 100 行,雖然功能比較少,但是好用多了。

接著比較實際的程式碼吧:

redux-router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from 'react';
import {render} from 'react-dom';
import {createStore, compose} from 'redux';
import {ReduxRouter, reduxReactRouter} from 'redux-router';
import {createHistory} from 'history';
const store = compose(
middlewares,
reduxReactRouter({
routes,
createHistory
})
)(createStore)(rootReducer, initialState);
render(
<Provider store={store}>
<ReduxRouter/>
</Provider>
, document.getElementById('root'));
redux-simple-router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from 'react';
import {render} from 'react-dom';
import {createStore, compose} from 'redux';
import {createHistory} from 'history';
import {syncReduxAndRouter} from 'redux-simple-router';
import {Router} from 'react-router';
const history = createHistory();
const store = compose(
middlewares
)(createStore)(rootReducer, initialState);
syncReduxAndRouter(history, store);
render(
<Provider store={store}>
<Router routes={routes} history={history}/>
</Provider>
, document.getElementById('root'));

redux-router 需要在 createStore 時就加入 reduxReactRouter middleware,而 redux-simple-router 則是在 store 創建完成後才同步 router 狀態;redux-router render 時使用 ReduxRouter 元件,而 redux-simple-router 直接使用 React RouterRouter 元件。由於後者非常簡單,也減少了錯誤發生的機率。

redux-devtools

687474703a2f2f692e696d6775722e636f6d2f4a34476557304d2e676966.gif

這東西真的很炫,它可以在頁面中顯示側欄來顯示 action 的動態,還可以復原某些 action 對 store 的變更,使用起來非常簡單,讓我愛不釋手。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React from 'react';
import {render} from 'react-dom';
import {createStore, compose} from 'redux';
import {persistState} from 'redux-devtools';
import {createDevTools} from 'redux-devtools';
import LogMonitor from 'redux-devtools-log-monitor';
import DockMonitor from 'redux-devtools-dock-monitor';
const DevTools = createDevTools(
<DockMonitor
toggleVisibilityKey='H'
changePositionKey='Q'>
<LogMonitor/>
</DockMonitor>
);
const store = compose(
DevTools.instrument(),
persistState(
window.location.href.match(
/[?&]debug_session=([^&]+)\b/
)
)
)(createStore)(rootReducer, initialState);
render(
<Provider store={store}>
<DevTools/>
</Provider>
, document.getElementById('root'));

React Router

React Router 從 0.13 到 1.0 的差別非常大:

  • Named route 被移除了,原本 Link 元件能從 named route 自動產生完整的路徑,現在必須自己來
  • 不一定得用 JSX 來寫 route,現在也能用 plain object 了,這還挺方便的
  • willTransitionTo => onEnter, willTransitionFrom => onLeave
  • 支援 Async child routes,現在能更方便的切頁面了

我從暑假時因為 React context 的關係就開始用 React 0.14 + React Router 1.0 beta 了,所以轉換起來比較無痛,但是 1.0 beta 和正式版還是有些 API 差別的,因為一言難盡,還是看 Changelog 吧。

React Transform

687474703a2f2f692e696d6775722e636f6d2f416847593238542e676966.gif

原本我使用的是 React Hot Loader,這種方法其實用起來也沒什麼大問題,只是得另外開個 webpack-dev-server。不過原作者決定廢棄 React Hot Loader,又另外開了個 React Transform,所以我也只好轉移到 React Transform 了。相較上面幾個大改變來說,這只是編譯過程的改變而已,只需要改 Webpack 和 Babel 的配置即可。

首先把 webpack-dev-server 相關的東西都移除掉,安裝:

1
2
3
4
5
6
$ npm install babel-plugin-react-transform --save-dev
$ npm install react-transform-catch-errors --save-dev
$ npm install react-transform-hmr --save-dev
$ npm install redbox-react --save-dev
$ npm install webpack-dev-middleware --save-dev
$ npm install webpack-hot-middleware --save-dev

以下程式碼使用的是 Express,如果你用的是 Koa 的話,請把 webpack-dev-middleware 改成 koa-webpack-dev-middlewarewebpack-hot-middleware 改成 koa-webpack-hot-middleware,這兩個 middleware 的使用方式會有些差異,不過大致上是差不多的。

server.js
1
2
3
4
5
6
7
8
9
10
11
12
13
import webpack from 'webpack';
import webpackDevMiddleware from 'webpack-dev-middleware';
import webpackHotMiddleware from 'webpack-hot-middleware';
import webpackConfig from './config';
const compiler = webpack(webpackConfig);
app.use(webpackDevMiddleware(compiler, {
noInfo: true,
publicPath: webpackConfig.output.publicPath
}));
app.use(webpackHotMiddleware(compiler));
webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import webpack from 'webpack';
export default {
entry: {
main: [
'webpack-hot-middleware/client',
'./src/main'
]
},
module: {
loaders: [
{
test: /\.jsx?$/,
loaders: ['babel'],
exclude: /node_modules/,
query: {
plugins: ['react-transform'],
extra: {
'react-transform': [
{
transform: 'react-transform-hmr',
imports: ['react'],
locals: ['module']
},
{
transform: 'react-transform-catch-errors',
imports: ['react', 'redbox-react']
}
]
}
}
}
]
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin(),
new webpack.optimize.DedupePlugin(),
new webpack.optimize.OccurenceOrderPlugin()
]
};

Babel

Babel 在上個月也從 5 升級到 6 了,但現在還有很多問題:

  • 眾多功能拆成 Plugin 後,由於執行順序的問題,可能互相衝突,例如 Decorator 怪怪的
  • import * 的行為和過去不同,這有可能是上一點的問題
  • 最重要的是,React Transform 尚未支援

如果你現在 Babel 5 用得好好的,那就別折騰了。

後記

你可以在 tommy351/redux-example 看到完整的 Universal Redux example。

前端的變化這麼快實在太可怕了,明年說不定又有新玩意了,我還是回去寫後端壓壓驚吧

題圖是本季動畫「緋弾のアリアAA」,我原本以為這是和本傳差不多的動畫,沒想到一看就讓我震驚了,花了兩天把原作漫畫讀完,在某些方面來說,這的確和本傳差不多:

  • 主角「間宮明里」,和金次一樣有吸引後宮的體質,只是明里專門吸引サイコレズ,這世界就沒半個正常人吧。
  • 明里在平常就是個低等廢物,但是危急的時候總是能用祖傳炫泡絕技打爆敵人來收後宮。
  • 白雪(本傳)和志乃(AA)都非常喜歡主角,也都對亞莉亞抱有敵意www

目前動畫除了有些設定變更和大☆崩★壞的第五話以外,日常場景都挺不錯的,看來動畫工房的百合日常比較穩定。