Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

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

5,303 views

Published on

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

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

Published in: Technology
  • Dating direct: ♥♥♥ http://bit.ly/39mQKz3 ♥♥♥
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
  • Sex in your area is here: ❶❶❶ http://bit.ly/39mQKz3 ❶❶❶
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here

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

  1. 1. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 React.jsで 広告テンプレートを 作りたい リッチラボ株式会社 穴井宏幸(@pirosikick) ! SCRIPTY#3 2015/03/10
  2. 2. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 React.jsで 広告テンプレートを 作りたい リッチラボ株式会社 穴井宏幸(@pirosikick) ! SCRIPTY#3 2015/03/10
  3. 3. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 React.jsで 広告テンプレートを 作りたい リッチラボ株式会社 穴井宏幸(@pirosikick) ! SCRIPTY#3 2015/03/10 リッチ広告
  4. 4. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 穴井 宏幸 リッチラボ株式会社 エンジニア @pirosikick (ぴろしきっく) JavaScript, React.js, Flux Golangを始めたい
  5. 5. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 話すこと • リッチ広告でReact.jsを使いたい • 検証のためいくつか書きなおした • よかったことや • あんまりよくなかったこと・実戦投入への課題など
  6. 6. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止
  7. 7. ©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);
  8. 8. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 • View専門 • Virtual-DOM • JSX(別に使わなくても書けるけども)
  9. 9. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 リッチ広告 ちょっとデモ
  10. 10. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 リッチ広告 • ユーザイベントで変化させてリッチな感じに • ex) scroll, deviceorientation, touch, etc… • CTRや広告の印象が良かったりする
  11. 11. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 なんでReactで 作りたいか • DOMを組み立てていく様が普段の開発と似ていて
 なんか相性良さそう • 単純に新しいことをやりたい
  12. 12. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 React.jsで 書き直してみた バナープラス スクロールしている間だけ大きく表示される
  13. 13. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 比較
  14. 14. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 Before 163行 (独自ライブラリ除く)
  15. 15. ©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>; }, !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 }); } }, !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); } }); After 185行 (本体除く) • ちょっと増えた • 体感的にはそんなには書いていない • propTypesの記述 • JSXを読みやすくするために
 改行を多く入れたのが原因? • 実際はrequireとかで分けるだろう
  16. 16. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 イベント処理記述 設定に基づきDOM構築 • ユーザイベントで
 style属性を書き換えるような処理 Before 全体の構成
  17. 17. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 /** * 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); Before ※注) 実際のコードは晒せないのでjQueryで似たようなコードを書きました
  18. 18. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 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({/* 割愛 */}); After • そんなに変わらない
  19. 19. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 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); After
  20. 20. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 ※注) 実際のコードは晒せないのでjQueryで似たようなコードを書きました 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); });
  21. 21. ©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); ! this._timerId = setInterval(function () { this.hideExtension(); }.bind(this), 200); } ! componentDidMount () { window.addEventListener('scroll', this.onScroll); }, ! componentWillUnmount () { window.removeEventListener('scroll', this.onScroll); } }); After
  22. 22. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 /** * 外側の大きいバナーの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>; } }); After
  23. 23. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 良い点
  24. 24. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 • JSX HTMLを書く感じで心地良い • 全体の見通しがよくなった • Componentに見た目と振る舞いの定 義があるから • 単体テストしやすい • this.props • React.addons.TestUtils
  25. 25. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 実戦投入への課題
  26. 26. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 うまく 実装できなかったこと • 広告を挿入する要素の外側への処理 • ex) expand時にbodyに要素を付け替え • 実装できてもあんまりいいやり方では無さそう • 2回React.renderを呼び、予めbodyに要素追加 • Reactの外から要素を移動して戻す
  27. 27. ©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)
  28. 28. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 まとめ
  29. 29. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 • リッチ広告への実戦投入は
 現状出来なそう🙅 • 容量が小さいものが欲しい • 書いている時の気持ちよさ👏
 (JSXに限る) • コードの見通しがよく感じた👍
  30. 30. ©2015 Rich Lab Co., Ltd. All Rights Reserved. 無断利用・転載禁止 Thanks:) @pirosikick (ぴろしきっく)

×