概要
「Opt Technologies Advent Calendar 2016」の一日目です。
最近とあるシステムを社内向けリリースしたので、どのように進めたかと実装について書いていきます。
(実際のコードは見せられませんが、参考用の過去記事へのリンクを用意しているので同じように設計で組むことはできるかと思います。)
作ったものの要件的な
- BtoBな管理画面系アプリ
- デザイナ不在
- 進め方は自由だがスケジュールしっかり守りたい
- フロント実装者は、自分以外は外部のフロントエンジニアと、社内のGUI未経験エンジニア
- 開発部内のKPIにテストカバレッジがあり、フロントでもカバレッジ取りたい
進め方
package.jsonの抜粋
全体像を掴めるかと思い、最初にpackage.jsonを貼っておきます。
後でそれぞれのライブラリやタスクについて触れていくので、なんとなく見て頂ければと思います。
{
"name": "~~",
"version": "1.0.0",
"scripts": {
"build:watch": "webpack --progress --colors --watch --config ./webpack.config.dev.js",
"build": "webpack --progress --colors --config ./webpack.config.dev.js",
"build-prd": "webpack --progress --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",
"coverage-html": "remap-istanbul -i ./coverage/*/coverage-final.json -o coverage-report -t html",
"coveralls": "remap-istanbul -i ./coverage/*/coverage-final.json -o ./coverage/*/coverage-final.json && istanbul report lcov && node mergeCoveralls.es6 < ./coverage/lcov.info",
"server": "node dev-server.es6"
},
"devDependencies": {
"@types/~~~",
"coveralls": "2.11.15",
"express": "4.14.0",
"fetch-mock": "5.5.0",
"istanbul": "0.4.5",
"istanbul-instrumenter-loader": "1.0.0",
"jasmine-core": "2.5.2",
"karma": "1.3.0",
"karma-coverage": "1.1.1",
"karma-jasmine": "1.0.2",
"karma-mocha-reporter": "2.2.0",
"karma-webpack": "1.8.0",
"react-addons-test-utils": "15.4.0",
"remap-istanbul": "0.7.0",
"typescript": "2.0.9",
"webpack": "1.13.3",
etc...
},
"dependencies": {
"isomorphic-fetch": "2.2.1",
"jquery": "3.1.1",
"materialize-css": "0.97.8",
"react": "15.4.0",
"react-dom": "15.4.0",
"react-materialize": "0.17.0",
"react-redux": "4.4.5",
"react-router": "3.0.0",
"redux": "3.6.0".
etc...
}
}
まず画面モックを作ってディレクタと認識合わせ
今回は凝ったUIは求められないので、CSSフレームワーク任せで楽しようと思い選定しました。BootStrap4系がまだアルファ版で諦め、今回は趣味もありMaterial Designの実装である Materialize を採用することにしました。
今思えばMaterial UIでも良かったのですが、画面モック作るときに都合が良いのでそのまま実装でも採用しています。
CSSフレームワーク採用したはいいもののこれで画面の要件を満たせるか不安もあり、初期に画面モックを作ってディレクタさんと相談しました。
このとき、Materializeでいけるかの判断もですが、画面構成が複雑になりすぎないように、極力ページ分割することを意識しておきました。(そのほうが使い勝手もよくなると信じて)
サーバーサイドと完全に切り離して実装できるようにする
スケジュールを意識したい&外部のパートナーさんも入る、とのことなので、滞りなく実装できるようにサーバーとのやりとりはWebAPIのみのSPA構成を取り入れました。(これにも僕の趣味が多分に入っています)
具体的には、どのようなデータ構造でやりとりすべきかをサーバー(Play2 + Swagger)でAPIドキュメント化し、フロントではそれを参考にしつつexpressで開発用サーバーを組んでいました。(package.jsonの "server": "node dev-server.es6"
のところです)
これにより、フロント開発をするときは画面モックとAPIドキュメントをみながら実装でき、動作確認もPlayをいじらなくても行えるようになり、とても楽ができたかと思います。
ルーティング、テストまで含めた雛形を用意する
画面モックを作ってみたところ、なかなか複雑な画面要件が出てきたため単純なjQueryベースの実装では苦しくなることが想定されました。そこで、個人的に知見があり社内でも注目を集めていたReact&Reduxを採用しました。(Angular2系は当時はまだベータ版だったので見送り)
また、ウチの会社的にScala推し・型チェック推しで既に色々なプロジェクトで採用されているTypeScriptも同時に取り入れました。
この技術スタックでさらに、バンドルはwebpack、テストはkarma、をそれぞれ採用しています。
これは、いずれAngular2が社内でも流行ると思ったので、そこのスタックに寄せていこうという形です。
実際にどのようにソースコードを組んでルーティングを行うか、さらにそこにテストも用意するかは、こちらにそれぞれまとめているので参考にしてください。
React + Redux + Typescriptシリーズ(随時更新)
あとはこれを実際の画面に組み込むのですが、MaterializeがjQuery依存しているためなかなか難儀しました。(大人しくMaterial UI使うべきだったかも)
工夫したこととしては、
- jQueryはグローバルに転がし、型定義だけ用意してバンドルには含めない。
-
react-materialize
というライブラリがあったので、必要に応じて直して使った - componentDidMountなどを駆使して、jQuery感のある初期化処理(tooltipの有効化など)を再現した。
という形です。
ここまでの結果を雛形として用意しておいたおかげで、外部のパートナーさんやフロント開発未経験のメンバーも、実装はもちろん率先してテストを書いていってくれました。
カバレッジ計測する
ここまでやるところは多くないのかもしれませんが、僕らはcoverallsを使ってカバレッジをチェックしています。そのためtsコードでのカバレッジ測定と、それをcoverallsに送る必要があります。
まずカバレッジ計測について、計測結果をローカルでhtmlで見る方法を書きます。
上記のnpm scriptでは、"test:coverage"
-> "coverage-html"
で見ることができるようにしています。
流れとしては、
- tsコードをカバレッジ取るためにjsに変換。ソースマップも吐く
- jsコードをkarmaでテスト
- karma上なので、webpackでビルドしつつ行う
- istanbulでカバレッジ取得
- 取得したjsコードのカバレッジとソースマップからtsのカバレッジに変換
- HTML形式で整形
という形になります。ちなみにkarmaの設定はこんな感じです。
//実行時引数から、テスト対象ファイルを選ぶ
const args = process.argv;
args.splice(0, 4);
//ポリフィルなどグローバルに入れておきたいものを置いておく。
const polyfils = [
'./node_modules/jquery/dist/jquery.min.js',
'./node_modules/materialize-css/dist/js/materialize.min.js'
];
var files = polyfils.concat(args);
module.exports = function(config) {
config.set({
basePath: '',
frameworks: ['jasmine'],
files: files,
preprocessors: {
'**/*.spec.ts': ['webpack'],
'**/*.spec.tsx': ['webpack'],
'**/*.spec.js': ['webpack']
},
webpack: {
resolve: {
extensions: ['', '.ts', '.js', ".tsx"]
},
module: {
loaders: [{
test: /\.tsx?$/,
loader: "ts-loader"
}],
postLoaders: [{
test: /\.js/,
exclude: /(__test__|node_modules|bower_components)/,
loader: 'istanbul-instrumenter'
}]
}
},
reporters: ['mocha', 'coverage'],
<etc...>,
coverageReporter: {
type : 'json',
dir : 'coverage/'
}
})
};
また、coverallsに送る時も同じような手順を踏んでいます。
npm scriptでは、上記のnpm scriptでは、"test:coverage"
-> "coveralls"
となります。最後だけ少し異なり、
- 取得したjsコードのカバレッジとソースマップからtsのカバレッジに変換
- レポートcoverallsの形式に変換
- Scala側のカバレッジレポートとマージしてcoverallsに送信
という流れになっています。この部分はかなり汚いハックをしているためお見せできません。。。
やってみての振り返りやツラみ
フロントエンド専門でない自分が主導したため、ところどころ無理やり感がありますが、全体としては開発はスムーズでその後の改修やライブラリ追従も無駄なくできていて、個人的にはかなり成功の部類に入ります。
ツラみとしては、やはりCSSフレームワークがReactのことを考えられていないのは面倒でした。react-materializeはまだ未完成で色々足りてないので、パッチを当てつつ使っています。
また、型定義周りでやはりReactに追従しきれてないところでトラブることがありました。そういうときはだいたいanyに逃げて誤魔化していました。。
また、SPAで組んでいると、気をつけないと(ページ)遷移の時に前の状態を引き継いでしまったり、モーダルが出しっぱなしになってしまったりなどの不具合によく遭遇しました。もっとWebが良い感じになればいいのにな。。
最後に
ご指摘や詳しく知りたい箇所などありましたらお気軽にコメント頂ければと思います。
次はAngular-cli使ってバブみを感じたい気持ちです!
参考資料
このシステムを作るまでに調べた色々です。こちらもあわせてどうぞ。