この記事は「Opt Technologies Advent Calendar 2017」1日目です。
2日目の記事のほうが先に出ている気がしますが気のせいでしょう。
はじめに
去年のアドベントカレンダーにて、「React + TypeScriptでWebシステムを作った話」を書きました。
本記事では、上記システムで使っている技術が一年経ってどう変わったかを書こうと思います。
(ちなみに筆者は、上記システムにおいては開発はしておらず、フロントエンド中心に開発環境やライブラリ周りの整備だけしています。)
(社内的な要件や技術スタックは、上記記事に書いているのでぜひご覧ください。)
package.jsonの抜粋
去年のはこのような形ですね。 package.jsonの抜粋
今の状態はこんな感じです。
{
"name": "~~~",
"version": "1.0.0",
"scripts": {
"postinstall": "rm -f ./node_modules/immutable/dist/*.d.ts && sh ./moveToPublic.sh",
"build:watch": "webpack --progress --colors --watch --config ./webpack.config.dev.js",
"build": "webpack --colors --config ./webpack.config.dev.js",
"build:prod": "webpack --colors --config ./webpack.config.prod.js",
"test:ut": "karma start karma.conf.js",
"test:all": "karma start karma.conf.js **/*.spec.tsx **/*.spec.ts",
"test:coverage": "tsc && karma start karma.conf.js ./dist/**/*.spec.js",
"server": "node dev-server.js",
"lint:unit": "tslint -c tslint.json",
"lint:ci": "git diff --name-only --no-renames HEAD $(git merge-base HEAD origin/master) | grep ^web-console/front/src | sed -e 's@web-console/front/@@' | xargs npm run lint:unit dummy",
"lint:unit-fix": "tslint --fix -c tslint.json",
etc...
},
"devDependencies": {
"@types/~~~",
"enzyme": "3.2.0",
"enzyme-adapter-react-16": "1.1.0",
"express": "4.16.2",
"fetch-mock": "5.13.1",
"jasmine-core": "2.8.0",
"karma": "1.7.1",
"karma-chrome-launcher": "2.2.0",
"karma-jasmine": "1.1.0",
"karma-mocha-reporter": "2.2.5",
"karma-webpack": "2.0.6",
"multer": "1.3.0",
"react-test-renderer": "16.1.1",
"ts-loader": "3.1.1",
"tslint": "5.8.0",
"tslint-config-standard": "7.0.0",
"tslint-react": "3.2.0",
"typescript": "2.6.1",
"uglifyjs-webpack-plugin": "1.1.0",
"webpack": "3.8.1",
etc...
},
"dependencies": {
"classnames": "2.2.5",
"date-fns": "1.29.0",
"immutable": "3.8.2",
"jquery": "3.2.1",
"materialize-css": "0.98.0",
"react": "16.1.1",
"react-dom": "16.1.1",
"react-materialize": "1.1.0",
"react-redux": "5.0.6",
"react-router-dom": "4.2.2",
"react-select": "1.0.0-rc.10",
"redux": "3.7.2",
etc...
}
}
順番にお話していこうと思います。
画面デザインについて
変わらずmaterialize ベースになっています。
一部ややこしいコンポーネントはreact-matrializeというライブラリを使っていて、問題なく運用出来てたり、たまにPR出してみたりしています。
このライブラリが最近React v16対応するまでは npm install
したあとにreact v15のライブラリを削除してビルドする、という邪悪なことをしていました。。(もちろんテスト通ることは確認した上で)
Reactのバージョンアップ
Reactをv16にバージョンアップしました。上記システムにおいて、reactを上げる際に対応したのは以下の点です。
- 削除されるAPIへの依存の撤廃
- min.jsの配置が変わった
- テスト時にライフサイクル系のメソッドが一部呼ばれなくなった
削除されるAPIへの依存の撤廃
react v15.5で、削除されるAPIを利用している場合にwarningが出るようになりました。
https://reactjs.org/blog/2017/04/07/react-v15.5.0.html#new-deprecation-warnings
自分たちで書いているコードは問題ないのですが、利用しているライブラリで問題があったので、issueのやり取りを追いながらちまちま対応していました。
min.jsの配置が変わった
上記システムでは、ビルド時間短縮のためreactなどのコードはバンドルせずにmin.jsをそのまま配置して読ませるようにしています。
v15系では react/dist/react.min.js
にあったのですが、 react/umd/react.production.min.js
に置かれるようになったのでそこだけ対応しました。
テスト時にライフサイクル系のメソッドが一部呼ばれなくなった
React v16に上げた際に、enzymeを使った単体テストで componentDidMount
などのメソッドが呼ばれなくなっていました。
これはenzymeのバージョンを上げて、 enzyme-adapter-react-16 を使うことで解消出来ました。
TypeScriptのバージョンアップ
こちらは特にバージョンを上げるにあたって苦労した記憶はないのですが、型の表現力が増してきてredux周りのコードがよりキレイに書けるようになってきました。
詳しくは筆者の別記事「React + Redux + TypeScriptの最小構成」に書いてありますが、こんな感じです。
import {Action} from 'redux'
enum ActionNames {
INC = 'counter/increment',
DEC = 'counter/decrement',
}
interface IncrementAction extends Action {
type: ActionNames.INC
plusAmount: number
}
export const incrementAmount = (amount: number): IncrementAction => ({
type: ActionNames.INC,
plusAmount: amount
})
interface DecrementAction extends Action {
type: ActionNames.DEC
minusAmount: number
}
export const decrementAmount = (amount: number): DecrementAction => ({
type: ActionNames.DEC,
minusAmount: amount
})
export type CounterActions = IncrementAction | DecrementAction
export default function reducer(state: CounterState = initialState, action: CounterActions): CounterState {
switch (action.type) {
case ActionNames.INC:
return {num: state.num + action.plusAmount}
case ActionNames.DEC:
return {num: state.num - action.minusAmount}
default:
return state
}
}
- enumの値にstring型を使えること
- literal型が使えること
- switch句の分岐先で型がより厳密になること
などを利用しています。
(ただ、下記の実装はTypeScript2.6で入ったstrictFunctionTypesを使ってしまうとエラーで弾かれてしまいます。
これは、reducerの引数のactionは本来 Action
型のどんなオブジェクトでも入りうるのだけど CounterActions
という型だけを想定してしまっているからです。型に厳密にやろうとしてもどこかでキャストせざるを得ず、reduxの限界を感じています。。。)
react-router v4対応
react-routerの使い方や気にすべき点に関しては別記事「React + Redux + TypeScriptでreact-router」にてまとめているのですが、route設定やテストなどかなり差分が出て大変でした。
特にページ遷移時のトリガーの変更については、かなり汚いハックをしてしまいました。。
const history = createBrowserHistory()
let wasChanged = false;
//ページ内で何か編集中だと wasChanged がtrueになる
window.addEventListener('beforeunload', (e: any) => {
if(wasChanged) e.returnValue = ""; // Chromeではメッセージを選べないらしい
});
history.block(() => {
if (wasChanged)
return '編集中ですが画面遷移しても良いですか?'
})
//ページ遷移したあとは再びfalseに戻す
history.listen(() => {
wasChanged = false;
})
lintツールの導入
プロジェクト初期では必要としなかったのですが、筆者が開発から退くこともあり、他メンバーの要望もあり、lintチェックを入れようということになりました。
しかし既存のコードが大量にある状態からlintを導入すると、とても直せる量じゃないエラーが出てきてしまいます。
チェックを緩くして徐々に、という手もあるのですが、それだとlintの意味が薄くなると思い、「編集したコードについてだけ厳しいlintチェックを掛ける」という運用にしました。
npm scriptの lint:ci
というのがそれです。masterブランチと比較して差分のあるファイルだけを対象にしています。
まとめ
上記システムのフロントエンドの技術スタックですが、一年経っても負債化することなく順調に進化できています。
筆者が開発から退いてしばらく経ちますが、開発においても問題なく回っているようです。
今後も、メンテナンス性も考慮しつつ新技術を積極的に取り入れていきたいと思います。