829 Views
March 11, 15
スライド概要
2015/3/10の勉強会にて発表された資料です。
SCRIPTY#3 ~フロントエンド紳士・淑女のための勉強会~
http://scripty.connpass.com/event/12374/
2023年10月からSpeaker Deckに移行しました。最新情報はこちらをご覧ください。 https://speakerdeck.com/lycorptech_jp
React.jsで 広告テンプレートを 作りたい リッチラボ株式会社 穴井宏幸(@pirosikick) ! SCRIPTY#3 2015/03/10 ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
React.jsで 広告テンプレートを 作りたい リッチラボ株式会社 穴井宏幸(@pirosikick) ! SCRIPTY#3 2015/03/10 ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
React.jsで 広告テンプレートを リッチ広告 作りたい リッチラボ株式会社 穴井宏幸(@pirosikick) ! SCRIPTY#3 2015/03/10 ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
穴井 宏幸 リッチラボ株式会社 エンジニア @pirosikick (ぴろしきっく) JavaScript, React.js, Flux Golangを始めたい ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
話すこと • リッチ広告でReact.jsを使いたい • 検証のためいくつか書きなおした • よかったことや • あんまりよくなかったこと・実戦投入への課題など ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
/**
* "Hello! ${ 入力内容 }"と表示するだけのサンプル
*/
import React from "react";
!
let HelloApp = React.createClass({
getInitialState () {
return { name: this.props.defaultName || '' };
},
!
render () {
return (
<div className="wrapper">
<h1>Hello! { this.state.name }</h1>
<input type="text" onChange={this.onChange}/>
</div>
);
},
!
onChange (e) {
this.setState({ name: e.target.value });
}
});
!
React.render(<HelloApp defaultName="pirosikick" />, document.body);
©2015 Rich Lab Co., Ltd. All Rights Reserved.
無断利用・転載禁止
• View専門 • Virtual-DOM • JSX(別に使わなくても書けるけども) ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
リッチ広告 ちょっとデモ ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
リッチ広告 • ユーザイベントで変化させてリッチな感じに • • ex) scroll, deviceorientation, touch, etc… CTRや広告の印象が良かったりする ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
なんでReactで 作りたいか • DOMを組み立てていく様が普段の開発と似ていて なんか相性良さそう • 単純に新しいことをやりたい ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
React.jsで 書き直してみた バナープラス スクロールしている間だけ大きく表示される ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
比較 ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
Before 163行 (独自ライブラリ除く) ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
!!
!
'use strict';
'use es6';
import React, { PropTypes } from 'react';
let Banner = React.createClass({
propTypes: {
src: PropTypes.string.isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
margin: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
},
render () {
let props = this.props;
let style = {
width: props.width,
height: props.height,
margin: props.margin || '0 auto',
position: 'relative',
background: `transparent url(${props.src}) no-repeat 50% 50%`,
backgroundSize: 'contain'
};
!
!
return <div style={style}>{props.children}</div>;
}
});
let Extension = React.createClass({
propTypes: {
src: PropTypes.string.isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
offsetTop: PropTypes.number.isRequired,
offsetLeft: PropTypes.number.isRequired,
shown: PropTypes.bool
},
!
render () {
let props = this.props;
let style = {
width: props.width,
height: props.height,
position: 'absolute',
top: props.offsetTop,
left: props.offsetLeft,
zIndex: 100000,
opacity: props.shown ? 1 : 0,
background: `transparent url(${props.src}) no-repeat 50% 50%`,
backgroundSize: 'contain',
pointerEvents: 'none',
webkitTransition: '-webkit-transform 0s linear',
webkitTransform: 'translate3d(0, 0, 0)',
webkitTransitionDelay: `${props.show ? 0 : 200}ms`
};
!
!
!
return <div style={style}></div>;
}
});
let Anchor = React.createClass({
propTypes: {
href: PropTypes.string.isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
clickTracking: PropTypes.string
},
render () {
let props = this.props;
let style = {
position: 'absolute',
width: props.width,
height: props.height,
top: 0,
left: 0
};
!!
!!
!
return <a href={props.href} onClick={this.onClick}></a>;
},
After 185行
(本体除く)
onClick () {
if (this.props.clickTracking) {
var img = new Image();
img.src = this.props.clickTracking;
}
}
});
export default React.createClass({
displayName: 'BannerPlus',
propTypes: {
param: PropTypes.object.isRequired,
option: PropTypes.object
},
getInitialState () {
let param = this.props.param;
let option = this.props.option || {};
let state = {};
state.link = param.link;
state.width = option.width || 320;
state.height = option.height || 100;
state.banner = {
src: param['banner'],
margin: option['margin'] || '0 auto'
};
state.extension = {
src: param['extension'],
width: option['extensionWidth'] || 320,
height: option['extensionHeight'] || 200,
shown: false,
};
state.extension.offsetTop = -(state.extension.height - state.height) * 0.5;
state.extension.offsetLeft = -(state.extension.width - state.width) * 0.5;
state.clickTracking = option['clickTracking'];
state.researchTracking = option['researchTracking'] || [];
!!
!
!
!
!
!
!
!
!!
!
!
return state;
},
render () {
let state = this.state;
•
•
return (
<Banner
src={state.banner.src}
width={state.width}
height={state.height}
margin={state.banner.margin}>
<Anchor
href={state.link}
width={state.width}
height={state.height}></Anchor>
<Extension
src={state.extension.src}
width={state.extension.width}
height={state.extension.height}
offsetTop={state.extension.offsetTop}
offsetLeft={state.extension.offsetLeft}
shown={state.extension.shown}></Extension>
<div ref="hoge"></div>
</Banner>
);
},
showExtension () {
if (!this.state.extension.shown) {
this.state.extension.shown = true;
this.setState({ extension: this.state.extension });
}
},
•
ちょっと増えた
体感的にはそんなには書いていない
• propTypesの記述
• JSXを読みやすくするために
改行を多く入れたのが原因?
実際はrequireとかで分けるだろう
hideExtension () {
if (this.state.extension.shown) {
this.state.extension.shown = false;
this.setState({ extension: this.state.extension });
}
},
showExtensionDuringScroll: (function() {
let timerId;
return function () {
this.showExtension();
if (timerId) clearInterval(timerId);
timerId = setInterval(this.hideExtension, 200);
}
})(),
componentDidMount () {
document.body.addEventListener('touchmove', this.showExtensionDuringScroll);
window.addEventListener('scroll', this.showExtensionDuringScroll);
},
componentWillUnmount () {
document.body.removeEventListener('touchmove', this.showExtensionDuringScroll);
window.removeEventListener('scroll', this.showExtensionDuringScroll);
}
});
©2015 Rich Lab Co., Ltd. All Rights Reserved.
無断利用・転載禁止
Before 全体の構成 設定に基づきDOM構築 イベント処理記述 • ユーザイベントで style属性を書き換えるような処理 ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
Before
/**
* DOMの構築
*/
function $elem (elemName) { return $(document.createElement(elemName)); }
!
// CSSは使えないので直接style属性に定義
var banner $elem('div').css({
'width': config.width,
'height': config.height,
'margin': config.margin,
'position': 'relative',
'background': `transparent url(${config.banner.src}) no-repeat 50% 50%`,
'backgroundSize': 'contain'
});
!
var extension = $elem('div').css(/* 割愛 */);
!
var anchor = $elem('a').css(/* 割愛 */);
!
anchor
.attr('href', config.link)
.on('click', function () { ... };
!
banner.append(extension, anchor);
※注) 実際のコードは晒せないのでjQueryで似たようなコードを書きました
©2015 Rich Lab Co., Ltd. All Rights Reserved.
無断利用・転載禁止
After
import React from "react";
!
let Banner = React.createClass({
propTypes: {/* 割愛 */},
!
!
render () {
let props = this.props;
let style = {
width: props.width,
height: props.height,
margin: props.margin || '0 auto',
position: 'relative',
background: `transparent url(${props.src}) no-repeat 50% 50%`,
backgroundSize: 'contain'
};
}
});
return <div style={style}>{props.children}</div>;
!
// 上とstyle以外ほとんど一緒なので割愛
let Extension = React.createClass({/* 割愛 */});
let Anchor = React.createClass({/* 割愛 */});
•
そんなに変わらない
©2015 Rich Lab Co., Ltd. All Rights Reserved.
無断利用・転載禁止
After
let BannerPlus = React.createClass({
propTypes: { param: ..., option: ... },
!
!
!
// this.propsに来た設定値の初期化・Validationなど
getInitialState () {
let param = this.props.param;
let option = this.props.option;
return { link: param.link, banner: { ... }, extension: { ... } };
},
render () {
let state = this.state;
return (
// this.stateを子Componentに渡してDOMを構築する
<Banner src={state.banner.src} width={...} height={...}>
<Anchor href={state.link} width={...} height={...} />
<Extension isShown={state.isExtensionShown} src={...} width={...} height={...} />
</Banner>
);
},
// 外側の大きいバナーの表示・非表示
showExtension () {/* 後述 */},
hideExtension () {/* 後述 */}
});
!
// 広告描画
var param = { ... }, option = { ... }
React.render(<BannerPlus param={param} option={option} />, target);
©2015 Rich Lab Co., Ltd. All Rights Reserved.
無断利用・転載禁止
Before
/**
* ユーザイベントに合わせてstyle属性の書き換え
*/
!
var isExtensionShown = false;
var timerId;
!
// 大きいバナーを表示する処理
function showExtension () {
extension.css({ /* 割愛 */ });
isExtensionShown = true;
}
!
// 多きバナーを隠す処理
function hideExtension () {
dom.extension.css({ /* 割愛 */ });
isExtensionShown = false;
}
!
// スクロール時に表示・非表示
$(window).on('scroll', function () {
!isExtensionShown && showExtension();
!
!
if (timerId) clearInterval(timerId);
timerId = setInterval(function () {
hideExtension();
}, 200);
});
※注) 実際のコードは晒せないのでjQueryで似たようなコードを書きました
©2015 Rich Lab Co., Ltd. All Rights Reserved.
無断利用・転載禁止
let BannerPlus = React.createClass({
render () { /* 割愛 */ },
!
!
!
!
// state.extension.shownの切り替えで
// 子Componentのstyleを切り替える
showExtension () {
if (!this.state.isExtensionShown) this.setState({ isExtensionShown: true });
},
hideExtension () {
if (this.state.isExtensionShown) this.setState({ isExtensionShown: false });
},
onScroll () {
!this.state.isExtensionShown && this.showExtension();
if (this._timerId) clearInterval(this._timerId);
!
!
!
After
}
this._timerId = setInterval(function () {
this.hideExtension();
}.bind(this), 200);
componentDidMount () {
window.addEventListener('scroll', this.onScroll);
},
componentWillUnmount () {
window.removeEventListener('scroll', this.onScroll);
}
});
©2015 Rich Lab Co., Ltd. All Rights Reserved.
無断利用・転載禁止
After
/**
* 外側の大きいバナーのComponent
*/
!
let Extension = React.createClass({
propTypes: { /* 割愛 */ },
!
!
render () {
let props = this.props;
let style = {
/* 省略 */
!
/**
* props.isShownで表示・非表示
*/
opacity: props.isShown ? 1 : 0,
!
!
!
}
});
/* 省略 */
};
return <div style={style}></div>;
©2015 Rich Lab Co., Ltd. All Rights Reserved.
無断利用・転載禁止
良い点 ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
• JSX HTMLを書く感じで心地良い • 全体の見通しがよくなった • • Componentに見た目と振る舞いの定 義があるから 単体テストしやすい • this.props • React.addons.TestUtils ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
実戦投入への課題 ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
うまく 実装できなかったこと • 広告を挿入する要素の外側への処理 • • ex) expand時にbodyに要素を付け替え 実装できてもあんまりいいやり方では無さそう • 2回React.renderを呼び、予めbodyに要素追加 • Reactの外から要素を移動して戻す ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
サイズがデカイ • react.min.js v0.12.2 128KB • • 大体10KBに収まるように心がけているので、 めっちゃでかい。。。 他のVDOM系もそれなりのサイズ感 • deku.min.js 9.9KB • virtual-dom 28KB(uglifyjs) ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
まとめ ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
• リッチ広告への実戦投入は 現状出来なそう🙅 • 容量が小さいものが欲しい • 書いている時の気持ちよさ👏 (JSXに限る) • コードの見通しがよく感じた👍 ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
Thanks:) @pirosikick (ぴろしきっく) ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止