先日、あるアコーディオン的な Form を作って、CSS で slide up / down アニメーションをつけました。実際 CSS animation を使うとき、要素の display: none から display: block に, visibility: visible から visibility: hidden を変更すると、アニメーションが実行されません。現在 CSS animation はある数字から、次の数字までしか動きません。Height の場合、0 から 200 までアニメートできますが、0 から auto まではできません。
では、なぜ jQuery animate 関数を使わないかと、CSS の方が効率がいいし、面白いからです。Chrome の FPS ツールでみると、CSS の場合、主に 50 FPS に一定していますが、jQuery animate は、JavaScript を実行しているので、FPS 一定ではないです。(https://greensock.com/js/speed.html)
それで、display: none -> block の代わりに、maxHeight: 0 -> 200px を css transition でアニメートするようにしました。maxHeight 0 となっても、実際その要素は DOM に存在していますので、Screen Reader は認識しています。さらに、そのエリアに Form <input /> があるため、Tab Stop も発生しています。
Focus が見えないところに行くのは大変良くないことです。ユーザーが Tab キーをおして、しばらく Focus はどこにあるかわからなくて、結構迷うようです。
では、css animation を使うとき、accessibility について実装方法としては幾つかコツがあります。
まず、<input /> みたいな Form 要素に対して、tabIndex を使って、 tab order を変更します。0 から -1 にします。0 の場合は、tab order をブラウザーを任していますが、-1 の場合、JavaScript の setFocus() 関数を使う必要があります。これで、ユーザーの Tab キーはその要素をスキップするようになります。
次、aria-hidden を使って、見えない部分を Screen Reader から隠します。aria-hidden を true にすれば、すべての Screen Reader が読まないようになります。
具体的に:
<form aria-hidden={isOpen ? false : true}>
<input tabIndex={isOpen ? 0 : -1} />
</form>
にするだけです。
これで、ユーザーの Tab Stop が Form Hidden の場合、そのエリアの <input /> などに行かないように、矢印キーを使っても、その部分が読まないようになります。アニメーションは maxHeight を使って、開いたり、閉じたりできます。
Accessibility について、いいコースがあります。英語ですけど。。。
https://www.udacity.com/course/web-accessibility--ud891
それでは。
Code
2016年8月28日日曜日
2016年8月6日土曜日
React.js Component Wrapper
先週、pre-loaded データを使って、Component を render する代わりに、ajax を使って、データロードして、そのあと、 Component を render する機能が出てきました。この機能を実現するために、Wrapper Component を作ったら、簡単にできました。
具体的には autoLoadingComponent を作って、その中に、state を持たせて、componentDidMount の中に、ajax call を呼び出して、callback には setState() を読んで、state を更新します。この state をベースに元の Component に props として渡します。
そうすると、state が更新するたびに、元の Component が re-render されますので、auto load 機能が実現できます。簡単な例として:
autoLoader = React.createClass({
mixin: [pureRenderMixin],
getInitialState: function () {
return {
data: null,
loading: true
};
},
componentDidMount: function () {
// ajax call to get data
getJson(url, this.handleResponse);
},
handleResponse: function (response) {
this.setState({
data: response,
loading: false
});
},
render: function () {
if (this.state.loading) {
return <loading />;
} else {
return <component data={this.state.data} />;
}
}
});
これで、過去の Component も再利用できますし、どんなものでも auto load 機能がつけられます。
さらに、Redux を使ってる場合、Redux.connect() という関数があります。この関数も同じ考え方で、state を持ってる Wrapper を作って、過去の Component を自動更新できるようにします。その中も subscribe などの biolaplate コードもいっぱい入ってます。
それでは。
具体的には autoLoadingComponent を作って、その中に、state を持たせて、componentDidMount の中に、ajax call を呼び出して、callback には setState() を読んで、state を更新します。この state をベースに元の Component に props として渡します。
そうすると、state が更新するたびに、元の Component が re-render されますので、auto load 機能が実現できます。簡単な例として:
autoLoader = React.createClass({
mixin: [pureRenderMixin],
getInitialState: function () {
return {
data: null,
loading: true
};
},
componentDidMount: function () {
// ajax call to get data
getJson(url, this.handleResponse);
},
handleResponse: function (response) {
this.setState({
data: response,
loading: false
});
},
render: function () {
if (this.state.loading) {
return <loading />;
} else {
return <component data={this.state.data} />;
}
}
});
これで、過去の Component も再利用できますし、どんなものでも auto load 機能がつけられます。
さらに、Redux を使ってる場合、Redux.connect() という関数があります。この関数も同じ考え方で、state を持ってる Wrapper を作って、過去の Component を自動更新できるようにします。その中も subscribe などの biolaplate コードもいっぱい入ってます。
それでは。
2016年7月24日日曜日
React.js re-render で State のクリア方法
通常 React.js を使うと、コンポーネットを作って、DOM に render したら、たまには props を渡して、元の状態でもう一度 render したい時があります。ただ、state はコンポーネントを保持しているので、props が変更するだけで、re-render をしても、state が最初に戻らないのです。なぜなら、コンポーネントの getInitialState はコンポーネント最初の一回のみ実行されて、unmount を実行して、再 render しない限り、state は前の状態に保持されます。
内部の操作としては、props が変更したら、React.js はコンポーネントを DOM から消して、もう一度作るではなく、変更した部分だけ再度 render するだけです。同時に、ライフサイクルの関数を見てみると、 componentDidMount は一回だけ呼び出されて、そのあとは全て、componentDidUpdate が呼び出されます。
じゃ、どうやって、props 変更する際に state もクリアしますかと。基本的に二つ方法があります。
まずは、componentWillReceiveProps () 関数が毎回新しい props が渡されるとき呼び出されますので、その中で props に応じて、setState を呼び出して、リセットします。 それに、この関数の中で setState で state を変更しても、re-render が起こらないです。props の変更と state の変更が一回の re-render で更新されます。他のところで、 setState を呼び出すと、shouldcomponentUpdate から render、componentDidUpdate まで一通り実行されます。これは最善策です。
具体的に、getInitialState () で prepareInitialState を使って、新しい object を返します。親が re-render して、props が渡されたら、componentWillReceiveProps () 関数の中で、条件を判断して、もし state をクリアしたい場合、setState (prepareInitialState()) を呼び出すと、state が最初の状態に戻されます、props の変更とともに。
もう一つの方法は props と一緒に key を変更する。key をコンポーネントに設定すると、React.js の Virtual DOM 比較アウルゴリスムはコンポーネントのタイプ、key などの属性を最初に比較します。key が変更したことは、別のコンポーネントになるわけです。
ネットでは key を new Date() を設定して、親からの re-render があれば、変更されますので、いつもコンポーネントを最初から re-render します。
個人的には、方法1を使うべきです。React.js はすでに componentWillReceiveProps() 関数が提供されたので、それを使うべきです。方法2としては、確かに同じ目的が達成できるかもしれないが、実行されるコードが多いのです。それに、本来なら、re-render 必要ないかもしれないサイクルでも、key が変更されて、re-render になります。全体的に複雑になります。
それでは。
内部の操作としては、props が変更したら、React.js はコンポーネントを DOM から消して、もう一度作るではなく、変更した部分だけ再度 render するだけです。同時に、ライフサイクルの関数を見てみると、 componentDidMount は一回だけ呼び出されて、そのあとは全て、componentDidUpdate が呼び出されます。
じゃ、どうやって、props 変更する際に state もクリアしますかと。基本的に二つ方法があります。
まずは、componentWillReceiveProps () 関数が毎回新しい props が渡されるとき呼び出されますので、その中で props に応じて、setState を呼び出して、リセットします。 それに、この関数の中で setState で state を変更しても、re-render が起こらないです。props の変更と state の変更が一回の re-render で更新されます。他のところで、 setState を呼び出すと、shouldcomponentUpdate から render、componentDidUpdate まで一通り実行されます。これは最善策です。
具体的に、getInitialState () で prepareInitialState を使って、新しい object を返します。親が re-render して、props が渡されたら、componentWillReceiveProps () 関数の中で、条件を判断して、もし state をクリアしたい場合、setState (prepareInitialState()) を呼び出すと、state が最初の状態に戻されます、props の変更とともに。
もう一つの方法は props と一緒に key を変更する。key をコンポーネントに設定すると、React.js の Virtual DOM 比較アウルゴリスムはコンポーネントのタイプ、key などの属性を最初に比較します。key が変更したことは、別のコンポーネントになるわけです。
ネットでは key を new Date() を設定して、親からの re-render があれば、変更されますので、いつもコンポーネントを最初から re-render します。
個人的には、方法1を使うべきです。React.js はすでに componentWillReceiveProps() 関数が提供されたので、それを使うべきです。方法2としては、確かに同じ目的が達成できるかもしれないが、実行されるコードが多いのです。それに、本来なら、re-render 必要ないかもしれないサイクルでも、key が変更されて、re-render になります。全体的に複雑になります。
それでは。
2016年7月9日土曜日
iOS 9 HTML5 モーダル スクロールできない問題
TL; DR: iOS で modal を表示しても、body が相変わらずスクロールできるの修正方法:modal-open クラスに positive: relative を追加すれば、修正されます。
Bootstrap などを使うと、簡単に Modal ウィンドウが作れるようになります。基本的に CSS もあって、関数を呼び出すだけです。そのモーダルウィンドウは一般的に画面の真ん中に、スクロールできないようになっています。これは modal-open というクラスを body tag に設定することで、body の overflow を hidden に設定するだけでできています。
そうすると、モーダルウィンドウ自身はスクロールできるし、いつも画面の真ん中にあります。ユーザーに操作を提示しているため、最適していると思います。
iOS 9 がリリースされて、一つの問題は発生しました。body のスクロール禁止が効かなくなりました。
対応方法としては
modal-open { overflow: hidden; position: relative; }
にして、元と同じように動きます。PC ではこの問題が起きません。
他の修正方法としては、html, body と一緒に overflow: hidden を設定します。
それでは。
Bootstrap などを使うと、簡単に Modal ウィンドウが作れるようになります。基本的に CSS もあって、関数を呼び出すだけです。そのモーダルウィンドウは一般的に画面の真ん中に、スクロールできないようになっています。これは modal-open というクラスを body tag に設定することで、body の overflow を hidden に設定するだけでできています。
そうすると、モーダルウィンドウ自身はスクロールできるし、いつも画面の真ん中にあります。ユーザーに操作を提示しているため、最適していると思います。
iOS 9 がリリースされて、一つの問題は発生しました。body のスクロール禁止が効かなくなりました。
対応方法としては
modal-open { overflow: hidden; position: relative; }
にして、元と同じように動きます。PC ではこの問題が起きません。
他の修正方法としては、html, body と一緒に overflow: hidden を設定します。
それでは。
2016年6月18日土曜日
DOM は遅い?なぜ Virtual DOM を使うか
先日 DOM は遅いですか?なぜ Virtual DOM を使うかと聞かれました。DOM は遅いというか、DOM 操作が遅いです。ブラウザーには reflow, repaint などの処理があります。最初 HTML と CSS を分析して、ノードツリーをレンダリングした後に、JavaScript でダイナミックにノードの高さなどを変更したり、さらにノードを追加したりすると、ツリー中の他のノードを影響されるし、ツリーを再構築するかもしれません。それは当然遅いです。
実際 HTML のノードを遍歴する時や、ノードの属性を取得するときは遅いというか、むしろ速いです。これは jQuery 自分の event システムを持つ理由もなります。一つの要素をクリックすると、その event は buble して、親要素を遡って、ルートまで達して、event handler を呼び出します。もしこの中に DOM 操作がなければ、10 ms ぐらいで、できるかと思います。
ただし、もしノードの offsetHeight などの computed style を取得して、さらにその値をベースに、ノードの style.height を変更するときに、ブラウザーは大変な仕事になります。
通常ではブラウザーは変更をキャッシュして、ある時間、ある程度貯まったら、画面を一気に更新します。なぜなら、細かい更新をすると、main thread が止まったりするから、ユーザーには不親切です。が、offsetHeight などの属性を取得する時、ブラウザーはできるだけ正しい値を返すために、すべての更新を実行しなければなりません。次、style.height を変更すると、さらに、ツリーの中の他のノードの位置を計算します。この Read + Write が多くなると、main thread が応答しない場合もあります。
じゃ、どのように DOM 操作を早くできるかというと、いくつかの方法があります。
1 要素のスタイルを変更する場合は、width, height などを個別に変更するではなくて、新しい css class を作って、要素に入れると、こちらの更新が一つの reflow, repaint で済みます。
2 また、offsetHeight などの値取得する時や、getBoundingClientRect() 関数を呼び出す時、できるだけ、Read を完了したら、Write を行います。つまり、Read と Write を分けます。
3 DOM 操作を一つ一つするではなくて、まずキャッシュして、一気に行ったほうがいいです。例えば、documentFragment を使って、すべての要素を入れてから、その fragment を DOM に入れるとか。まず、要素の display を none にして(隠して)、その後に、height などを変更してから、display: block にして、再表示するとか。
他には、
4 アニメーションをするときに、opacityや、transform, scale など一つの layer になる css 属性を使いましょう。また、アニメーション対象の要素の position を absolute にするとか。
Virtual DOM を使う理由は上記の 3 と関係しています。JavaScript でサイトを操作するとき、Virtual DOM を使うと、変更がメモリーにキャッシュされます。すべての操作が終わったら、Virtual DOM から DOM に更新しに行きます。この場合、ブラウザーはすべての変更を一つの reflow, repaint でできるため、速いです。
今 React.js 以外、他に incremental dom、mithril などいろいろ JavaScript があります。まぁ、開発はコードを書くだけではなくて、テストなども考えなければなりませんので、今の時点で React.js は一番かなと思います。
それでは。
実際 HTML のノードを遍歴する時や、ノードの属性を取得するときは遅いというか、むしろ速いです。これは jQuery 自分の event システムを持つ理由もなります。一つの要素をクリックすると、その event は buble して、親要素を遡って、ルートまで達して、event handler を呼び出します。もしこの中に DOM 操作がなければ、10 ms ぐらいで、できるかと思います。
ただし、もしノードの offsetHeight などの computed style を取得して、さらにその値をベースに、ノードの style.height を変更するときに、ブラウザーは大変な仕事になります。
通常ではブラウザーは変更をキャッシュして、ある時間、ある程度貯まったら、画面を一気に更新します。なぜなら、細かい更新をすると、main thread が止まったりするから、ユーザーには不親切です。が、offsetHeight などの属性を取得する時、ブラウザーはできるだけ正しい値を返すために、すべての更新を実行しなければなりません。次、style.height を変更すると、さらに、ツリーの中の他のノードの位置を計算します。この Read + Write が多くなると、main thread が応答しない場合もあります。
じゃ、どのように DOM 操作を早くできるかというと、いくつかの方法があります。
1 要素のスタイルを変更する場合は、width, height などを個別に変更するではなくて、新しい css class を作って、要素に入れると、こちらの更新が一つの reflow, repaint で済みます。
2 また、offsetHeight などの値取得する時や、getBoundingClientRect() 関数を呼び出す時、できるだけ、Read を完了したら、Write を行います。つまり、Read と Write を分けます。
3 DOM 操作を一つ一つするではなくて、まずキャッシュして、一気に行ったほうがいいです。例えば、documentFragment を使って、すべての要素を入れてから、その fragment を DOM に入れるとか。まず、要素の display を none にして(隠して)、その後に、height などを変更してから、display: block にして、再表示するとか。
他には、
4 アニメーションをするときに、opacityや、transform, scale など一つの layer になる css 属性を使いましょう。また、アニメーション対象の要素の position を absolute にするとか。
Virtual DOM を使う理由は上記の 3 と関係しています。JavaScript でサイトを操作するとき、Virtual DOM を使うと、変更がメモリーにキャッシュされます。すべての操作が終わったら、Virtual DOM から DOM に更新しに行きます。この場合、ブラウザーはすべての変更を一つの reflow, repaint でできるため、速いです。
今 React.js 以外、他に incremental dom、mithril などいろいろ JavaScript があります。まぁ、開発はコードを書くだけではなくて、テストなども考えなければなりませんので、今の時点で React.js は一番かなと思います。
それでは。
2016年6月17日金曜日
2016年6月5日日曜日
Angular から React.js への移行
昨日今やってるプロジェクトと似ている新しいウェブアップがリリースされました。AngularJS 1.0 を使っています。マネージャーたちは使ってみたら、なぜうちのプロジェクトと比べると、遅いと感じられるかと聞かれました。そもそも AngularJS 1.0 は重いからです。ぐるぐる回るロードアイコンが止まったり、なかなか応答が返ってこなかったりしたので、そう聞かれても仕方がありませんでした。
じゃ、どうやって、その AngularJS プロジェクトを React.js に移行するかと考えました。
まず、AngularJS 1.0 で ng-repeat を使ってるところを directive にして、React.js を使ってレンダリングしたほうがいいと思います。原因は ng-repeat は超を付くほど重いからです。key を使っても、ajax から返ってきたデータは毎回違うから、HTML の reuse がほぼ不可能です。逆に、React.js の場合、DOM ノードは Virtual DOM をベースに更新されるので、データが変わるなら、その分のみ更新されます。DOM ノードはそのままを使うケースが結構あります。
具体的には、ng-repeat の Collection を prop として、Component に入れます。その中で、Rendering する。directive の作り方は:
.directive('react', () => {
return {
scope: { data: '=' },
link: function (scope, element, attrs) {
scope.$watch(data, onDataChange);
function onDataChange () {
React.render(
React.createElement(REACT_COMPONENT, { data: data }),
element[0]);
};
}
}
});
これで、data が変更するたびに、React.render が呼び出されます。Virtual DOM を使っていますので、複数 rendering しても、変更する部分だけ更新されます。
次は ng-include や ng-view を使ってる部分を切り出して、React.js に個別にレンダリングします。ReactDOM.render 関数や、React.js 自身は同じページに複数の Component をレンダリング機能がサポートしています。ng-include などは独立な Component らしいので、controller の関数を React.js Component に入れて、データバインディングを完成すれば、すぐ使えます。
基本的に、service や、factory 部分を Flux や Redux の Store に変わるので、そんなに変更はないはずです。ただ、$http みたいな機能は React.js 提供してないから、qwest など別のライブラリを使ったほうがいいでしょう。これを考えると $http の promise が resovle されたら、$rootScope.$applyAsync 関数が呼ばれますので、ページ全体の Watcher が2回呼ばれます。。。重いですね。
最初の一歩は一番難しいですが、変更するたびに、どんどん新しい React.js Component もできるので、やりやすくなります。最近は React.js Develop tool を使って、他のサイトはどうやっているかが見れます。
Chrome React.js Developer tool
例えば Airbnb などは一つ一つ Component を作って、画面に入れ替わっていることが見れます。
じゃ、どうやって、その AngularJS プロジェクトを React.js に移行するかと考えました。
まず、AngularJS 1.0 で ng-repeat を使ってるところを directive にして、React.js を使ってレンダリングしたほうがいいと思います。原因は ng-repeat は超を付くほど重いからです。key を使っても、ajax から返ってきたデータは毎回違うから、HTML の reuse がほぼ不可能です。逆に、React.js の場合、DOM ノードは Virtual DOM をベースに更新されるので、データが変わるなら、その分のみ更新されます。DOM ノードはそのままを使うケースが結構あります。
具体的には、ng-repeat の Collection を prop として、Component に入れます。その中で、Rendering する。directive の作り方は:
.directive('react', () => {
return {
scope: { data: '=' },
link: function (scope, element, attrs) {
scope.$watch(data, onDataChange);
function onDataChange () {
React.render(
React.createElement(REACT_COMPONENT, { data: data }),
element[0]);
};
}
}
});
これで、data が変更するたびに、React.render が呼び出されます。Virtual DOM を使っていますので、複数 rendering しても、変更する部分だけ更新されます。
次は ng-include や ng-view を使ってる部分を切り出して、React.js に個別にレンダリングします。ReactDOM.render 関数や、React.js 自身は同じページに複数の Component をレンダリング機能がサポートしています。ng-include などは独立な Component らしいので、controller の関数を React.js Component に入れて、データバインディングを完成すれば、すぐ使えます。
基本的に、service や、factory 部分を Flux や Redux の Store に変わるので、そんなに変更はないはずです。ただ、$http みたいな機能は React.js 提供してないから、qwest など別のライブラリを使ったほうがいいでしょう。これを考えると $http の promise が resovle されたら、$rootScope.$applyAsync 関数が呼ばれますので、ページ全体の Watcher が2回呼ばれます。。。重いですね。
最初の一歩は一番難しいですが、変更するたびに、どんどん新しい React.js Component もできるので、やりやすくなります。最近は React.js Develop tool を使って、他のサイトはどうやっているかが見れます。
Chrome React.js Developer tool
例えば Airbnb などは一つ一つ Component を作って、画面に入れ替わっていることが見れます。
登録:
投稿 (Atom)