React.jsで広告テンプレートを作りたい #scripty03

595 Views

March 11, 15

スライド概要

2015/3/10の勉強会にて発表された資料です。

SCRIPTY#3 ~フロントエンド紳士・淑女のための勉強会~
http://scripty.connpass.com/event/12374/

profile-image

2023年10月からSpeaker Deckに移行しました。最新情報はこちらをご覧ください。 https://speakerdeck.com/lycorptech_jp

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

React.jsで 広告テンプレートを 作りたい リッチラボ株式会社 穴井宏幸(@pirosikick) ! SCRIPTY#3 2015/03/10 ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止

2.

React.jsで 広告テンプレートを 作りたい リッチラボ株式会社 穴井宏幸(@pirosikick) ! SCRIPTY#3 2015/03/10 ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止

3.

React.jsで 広告テンプレートを リッチ広告 作りたい リッチラボ株式会社 穴井宏幸(@pirosikick) ! SCRIPTY#3 2015/03/10 ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止

4.

穴井 宏幸 リッチラボ株式会社 エンジニア @pirosikick (ぴろしきっく) JavaScript, React.js, Flux Golangを始めたい ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止

5.

話すこと • リッチ広告でReact.jsを使いたい • 検証のためいくつか書きなおした • よかったことや • あんまりよくなかったこと・実戦投入への課題など ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止

6.

©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止

7.
[beta]
/**
* "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.
無断利用・転載禁止

8.

• View専門 • Virtual-DOM • JSX(別に使わなくても書けるけども) ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止

9.

リッチ広告 ちょっとデモ ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止

10.

リッチ広告 • ユーザイベントで変化させてリッチな感じに • • ex) scroll, deviceorientation, touch, etc… CTRや広告の印象が良かったりする ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止

11.

なんでReactで 作りたいか • DOMを組み立てていく様が普段の開発と似ていて なんか相性良さそう • 単純に新しいことをやりたい ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止

12.

React.jsで 書き直してみた バナープラス スクロールしている間だけ大きく表示される ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止

13.

比較 ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止

14.

Before 163行 (独自ライブラリ除く) ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止

15.
[beta]
!!
!

'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.
無断利用・転載禁止

16.

Before 全体の構成 設定に基づきDOM構築 イベント処理記述 • ユーザイベントで style属性を書き換えるような処理 ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止

17.
[beta]
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.
無断利用・転載禁止

18.
[beta]
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.
無断利用・転載禁止

19.
[beta]
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.
無断利用・転載禁止

20.
[beta]
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.
無断利用・転載禁止

21.
[beta]
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.
無断利用・転載禁止

22.
[beta]
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.
無断利用・転載禁止

23.

良い点 ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止

24.

• JSX HTMLを書く感じで心地良い • 全体の見通しがよくなった • • Componentに見た目と振る舞いの定 義があるから 単体テストしやすい • this.props • React.addons.TestUtils ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止

25.

実戦投入への課題 ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止

26.

うまく 実装できなかったこと • 広告を挿入する要素の外側への処理 • • ex) expand時にbodyに要素を付け替え 実装できてもあんまりいいやり方では無さそう • 2回React.renderを呼び、予めbodyに要素追加 • Reactの外から要素を移動して戻す ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止

27.

サイズがデカイ • 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. 無断利用・転載禁止

28.

まとめ ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止

29.

• リッチ広告への実戦投入は 現状出来なそう🙅 • 容量が小さいものが欲しい • 書いている時の気持ちよさ👏 (JSXに限る) • コードの見通しがよく感じた👍 ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止

30.

Thanks:) @pirosikick (ぴろしきっく) ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止