Knockout.jsで大規模開発してみた件について。

0 件のコメント
筆者の担当していた案件では、Knockout.jsを使って開発していました。
今回はどんな感じで使ってたのか?というのを記事にしたいと思います。

Knockout.jsとは?

MVVM(Model-View-ViewModel)パターンをサポートするフレームワークです。
双方向バインディング云々…書こうとしましたが、こちらを参照したほうが良いでしょう。
わかりやすいです。

[Knockout]MVVMパターンでアプリケーションを構築する

どんな案件?

  • 内容
webviewを使ったスマートフォン向けのアプリ開発。

  • サポート端末
Android4.0以上
iOS6.0以上

  • 案件規模
フロントエンジニアは5名程度。

  • コード量
愚直に↓をしてみると、数十万行
$ find . -name "*.js" | xargs wc -l

いいとこわるいとこ

いいとこ
  • 学習コストが低い
機能がシンプル(このくらい)というのもあるが、チュートリアルがしっかりしているので、これだけやればすぐ案件に入れるレベルになる。
チュートリアル:http://learn.knockoutjs.com/

  • 機能はシンプルだが、拡張しやすい
data-bind内で指定する、バインディングを独自に拡張することができます。
例えば、
スマホの場合、clickイベントとtouchイベントを両方使うと問題が起きやすいので、
筆者の案件ではclickバインディングではtouchstartイベントを使うようにカスタムバインディングで上書きしている。
作り方は、こんなに簡単です↓

  • ko.utilsというユーティリティがついてくる
地味に便利です。
ko.utils.unwrapObservableなどはカスタムバインディングを作る際によく使います。

  • 分業できる
開発チーム内で、下記のように分業することができる。

フロントチームのコーディング担当
HTML+CSSを使ってViewのみを作成。作成後、JavaScript担当へパス。
独自タグもないので、とっつきやすい。

フロントチームのJavaScript担当
サーバーチームとAPIの設計について相談し、ViewModelを作成。
そして、コーディング担当から受け取ったViewにdata-bind属性を追加し動きをつける。

サーバーチーム
JavaScript担当と相談しAPIを作成する。

わるいとこ
  • テストしづらい
Karma(mocha+sinon-chai)を使ってテストをするようにしましたが、Viewと紐付いているViewModelの実装はテストしづらかったです。
View側のテストもできないし、何かいい手はないかなーという感じでした。
(案件内ではテスト必須にはならず)

  • だめというか、開発者が気をつけてなきゃいけないとこ
View側に複雑なロジックを入れないようにすること。
<!-- if hogehoge().length < 10 || nantokahuragu -->
このぐらいだとまだ良いが、ネストが深くなったり条件が複雑になると、可読性がものすごく下がる。
withの多様も要注意。

どんな感じで使ってたか

基本は、このような感じ。
(実際はファイル結合してminファイルを出力したり、styleはcssにちゃんと書いてたり、いろいろやってます。あと、ディレクトリ構成も適当です。)
<!DOCTYPE HTML>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title></title>
<script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/knockout/3.1.0/knockout-min.js"></script>
<script type="text/javascript" src="class.js"></script>
<script type="text/javascript" src="BaseVM.js"></script>
</head>
<body>
<div id="TestVM">
<p data-bind="text: hoge, click: addHoge"></p>
<input type="text" data-bind="value: hoge, style: {width: '200px'}">
</div>
<script type="text/javascript">
var TestVM = BaseVM.extend({
hoge: null,
init: function(callback) {
this._super();
// 双方向バインディングする対象を定義
this.hoge = ko.observable("hogehoge");
callback && callback();
},
addHoge: function() {
this.hoge(this.hoge() + "hoge");
}
});
var testVM = new TestVM(function() {
console.log("callback!!");
});
ko.applyBindings(testVM, document.getElementById('TestVM'));
</script>
</body>
</html>
view raw A_test.html hosted with ❤ by GitHub
/**
* ViewModelのベースクラス
*/
var BaseVM = Class.extend({
init: function() {
console.log("base vm init!");
}
});
view raw BaseVM.js hosted with ❤ by GitHub
/**
* Class風の処理実装をさせるライブラリ
*
* @sample
* var Animal = Class.extend({
* init: function() {
* console.log('animal');
* },
* walk: function() {
* console.log('walking');
* }
* });
* var Monkey = Animal.extend({
* _super: 'super!',
* init: function () {
* this._super();
* console.log('monkey');
* }
* });
*
* var monkey = new Monkey;
* monkey.walk();
* => walking
*/
(function (window) {
'use strict';
/**
* メソッド内に_superの記述があるかのチェック
* _superがあるメソッドだけ上書き
* _superがあるかどうか判断できない場合は[/ .* /]となって全部上書き
*/
var fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
function Class() { /* noop. */ }
Class.extend = function (props) {
var SuperClass = this;
function Class() {
if (typeof this.init === 'function') {
this.init.apply(this, arguments);
}
// _superプロパティを書き込み禁止
Object.defineProperty(this, '_super', {
value: undefined,
enumerable: false,
writable: false,
configurable: true
});
}
// prototypeチェーンを作る
Class.prototype = Object.create(SuperClass.prototype, {
constructor: {
value: Class,
enumerable: false,
writable: true,
configurable: true
}
});
// instanceメソッドをセット
Object.keys(props).forEach(function (key) {
var prop = props[key],
_super = SuperClass.prototype[key],
isMethodOverride = (typeof prop === 'function' && typeof _super === 'function' && fnTest.test(prop));
if (isMethodOverride) {
Class.prototype[key] = function () {
var ret,
tmp = this._super;
// _superプロパティを設定
Object.defineProperty(this, '_super', {
value: _super,
enumerable: false,
writable: false,
configurable: true
});
ret = prop.apply(this, arguments);
// _superプロパティを書き込み禁止にする
Object.defineProperty(this, '_super', {
value: tmp,
enumerable: false,
writable: false,
configurable: true
});
return ret;
};
} else {
Class.prototype[key] = prop;
}
});
Class.extend = SuperClass.extend;
return Class;
};
window.Class = Class;
}(window));
view raw class.js hosted with ❤ by GitHub


javascriptでClass風の実装ができるようなライブラリを使い、継承して使用する。
※ 案件で使ってるコード・ライブラリではないです。
(バンバン継承しよう!というのは推奨せずでしたが。)
まあ、CoffeeScriptなりTypeScript使っとけよって感じかも知れませんが。

ある程度コードに規則性もできたので、保守しやすくなりました。

また、機能がシンプルというのもあり、
Knockout.jsのカスタムバインディングなどを使って、下記のような様々な仕組みも実装しました。
・1ページ内にViewModelが複数の場合の制御
・SEの制御
・ダイアログの制御
などなど

おわりに

フレームワーク選定の際に重視するのは、学習コストだったり運用のしやすさだと思うので、Knockout.jsは結構使いやすいんじゃないかなーと思いました。

また、使ってみてですが、筆者はKnockout.jsの不具合には遭遇しませんでした。
要素が多くなりすぎるとパフォーマンスに影響がでるということはありました。
(ko.applyBindings実行時に負荷がかかるが、Knockoutで処理させる箇所が数百箇所とかにならなければそんなに問題にならなかったです。)

小規模向け・プロトタイプで使うと良い。と書かれてる記事もよく見ますが、
小規模でも使いやすく、大規模でも使いやすいやつでした。

以上!

0 件のコメント :

コメントを投稿