Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Marionette.js in Single Page Application

koba04
January 21, 2014

Marionette.js in Single Page Application

Marionette.jsのView周りについてを中心にSingle Page Applicationを作るときの話

koba04

January 21, 2014
Tweet

More Decks by koba04

Other Decks in Programming

Transcript

  1. What is Single Page Application? (SPA) • http://en.wikipedia.org/wiki/Single-page_application ! !

    • αʔόʔ͸JSON API͚ͩΛఏڙ͍ͯͯ͠ϖʔδભҠ(࠶ಡΈࠐΈ)͸ൃੜͤͣɺ JavaScriptͰroutingͷ؅ཧ΍templateͳͲΛ༻͍ͨը໘ͷߋ৽ͳͲΛશͯߦ͏ • http://www.manning.com/mikowski/ In an SPA, either all necessary code – HTML, JavaScript, and CSS – is retrieved with a single page load,[1] or the appropriate resources are dynamically loaded and added to the page as necessary, usually in response to user actions. The page does not reload at any point in the process, nor does control transfer to another page, although modern web technologies (such as those included in HTML5) can provide the perception and navigability of separate logical pages in the application. Interaction with the single page application often involves dynamic communication with the web server behind the scenes.
  2. Backbone (1.1.0) • http://backbonejs.org/ • ΞϓϦέʔγϣϯΛModel, Collection, ViewͳͲͷཁૉͰߏ଄Խͯ͘͠ΕΔϑ ϨʔϜϫʔΫ •

    Backbone.EventsʹΑΔObserverύλʔϯͰͷ΍ΓͱΓ • ࠷௿ݶͷػೳ͚͕ͩఏڙ͞Ε͍ͯΔͷͰ͜Ε͚ͩͰSPAΛͭ͘Ζ͏ͱ͢Δͱɺ ͦͷ্ʹ΋͏Ұ૚ϑϨʔϜϫʔΫΛߏஙͤ͟ΔΛಘͳ͍ • ϝϞϦϦʔΫɺZombieViewා͍… • SPAͷ৔߹ɺϝϞϦϦʔΫ͸க໋త
  3. Backbone Is Not Enough • http://blog.shinetech.com/2013/09/06/backbone-is-not-enough/ When you pick a

    library like Backbone for a large SPA, sticky questions quickly arise. For example: ▪ How do I structure nested views and controllers? This is usually the first question I get asked by beginners. And I can honestly say after all this time: I don’t know – I’d have to do it myself for your particular use-case and then get back to you. ▪ How do I test my views? Backbone hasn’t got testing baked into it, so you have to piece together testing yourself. For views, this is hard – so hard that many people don’t bother to do it. ▪ Why is my app leaking memory? In SPAs, objects can hang around for a long time, rather than being recreated on a page refresh. This can be really useful, but also means that you have to make sure those objects that are not actually being used don’t take up memory. ▪ Why is my rendering so slow? With Backbone, it’s really easy to make many small updates to the DOM for a single user interaction. For large data sets, this lead to a poor user experience.
  4. Backbone Antipattern • http://blog.shinetech.com/2013/11/26/backbone-antipatterns/ • BackboneͰ։ൃ͢Δͱ͖ʹ΍Γ͕ͪͳΞϯνύλʔϯ͕10ݸ঺հ͞Ε͍ͯΔ Antipattern #1: Building a

    single page app when you don’t need to Antipattern #2: Using Backbone When You Should Use Another Framework Antipattern #3: No View Tests Antipattern #4: No Memory Management Antipattern #5: Data Attributes in the DOM Antipattern #6: Rendering Templates Asynchronously Antipattern #7: Undocumented Options Antipattern #8: Premature Use of Custom Events Antipattern #9: Building a Relationship Mapper Antipattern #10: Redundant Divs
  5. Using Backbone When You Should Use Another Framework • ήʔϜͷΑ͏ͳDOMΛΨπΨπ৮ΔΑ͏ͳΞϓϦͰͳ͘ɺJSON

    APIΛୟ͍ͯ ͦͷ݁ՌΛදࣔ͢ΔΑ͏ͳWebΞϓϦέʔγϣϯͩͱAngular.jsͷํ͕޲͍ͯ ͍Δ৔߹͕ଟ͍ • Backbone.jsܥ͸Backbone.stickit Έ͍ͨͳσʔλόΠϯσΟϯάϥΠϒϥϦ ΋͋Δ͚ͲͲ͏ͯ͠΋ίʔυྔ͸ଟ͘ͳΓ͕ͪ http://blog.shinetech.com/2013/09/06/backbone-is-not-enough/
  6. Why Marionette? • ͸͡Ί͸Backbone.js͚ͩͰ࡞͍ͬͯͬͨ • SPAͳͷͰෳ਺ViewͰͷදࣔͷ੾Γସ͑ɺωετͨ͠Viewߏ଄Λ͍͍ײ͡ʹ ѻ͍͔ͨͬͨ • ↑Έ͍ͨͳ͜ͱΛ࣮૷ͯ͠Δͱ͖ʹMarionette.jsΛ஌Δ •

    ࣮૷ΛݟΔͱࣗ෼ͷ΍Γ͔ͨͬͨ͜ͱ͕͍͍ײ͡ʹ࣮૷͞Ε͍ͯͨͷͰಋೖ • Marionette.jsྲྀʹ৐͔ͬΔ͜ͱͰϝϞϦϦʔΫͳͲΛগ͠Ͱ΋ҙࣝ͠ͳ͍Ͱ։ ൃ͔ͨͬͨ͠
  7. View of Marionette • Marionette͸ViewपΓ͕ΩϞ • RegionɺLayoutʹΑΔ֊૚Խͨ͠Viewͷ࢓૊ΈΛఏڙ • CollectionView, CompositeViewʹΑΔViewͷू߹Λ؅ཧ͢Δ࢓૊Έ

    • modelEvents, collectionEventsʹΑΔModel΍CollectionͷΠϕϯτߪಡͷ࢓૊Έ • onShow, onRenderɺonBeforeRenderͳͲ֤छΠϕϯτϋϯυϥ • ui, templateHelpers, serializeData ͳͲͪΐͬͱͨ͠ศརػೳ
  8. Sample of Marionette.Layout and Region • LayoutViewʹ֤DOMཁૉΛRegionͱͯ͠ొ࿥͢Δ͜ͱʹΑͬͯɺViewͷ੾Γ ସ͑Λαϙʔτͯ͘͠ΕΔ #menu #main

    #sub var layout = new Marionette.Layout()! layout.addRegions({! menu: “#menu”,! main: “#main”,! sub: “#sub”,! });! ! // ΠϯελϯεԽͨ͠ViewΛshowϝιουʹ౉͚ͩ͢! // render͸ࣗಈతʹݺΜͰ͘ΕΔ! layout.menu.show(new MyApp.MenuView());! layout.main.show(new MyApp.MainView());! layout.sub.show(new MyApp.SubView());! :! :! // લͷView͸close͞Εͯ৽͍͠View͕දࣔ͞ΕΔ! layout.main.show(new MyApp.OtherView());
  9. Implement of Marionette.Region#show show: function(view){! ! this.ensureEl();! ! var isViewClosed

    = view.isClosed || _.isUndefined(view.$el);! var isDifferentView = view !== this.currentView;! // ผͷViewΛදࣔ͢Δ࣌͢Ͱʹදࣔ͞Ε͍ͯΔViewΛclose͢Δ͜ͱͰޙ࢝຤͢Δ! if (isDifferentView) {! this.close();! }! view.render();! if (isDifferentView || isViewClosed) {! this.open(view);! }! ! this.currentView = view;! // RegionͱViewͷshowΠϕϯτΛൃߦ͢Δ! Marionette.triggerMethod.call(this, "show", view);! Marionette.triggerMethod.call(view, "show");! }, • https://github.com/marionettejs/backbone.marionette/blob/master/src/ marionette.region.js
  10. Implement of Marionette.ItemView#render render: function(){! this.isClosed = false;! ! this.triggerMethod("before:render",

    this);! this.triggerMethod("item:before:render", this);! ! var data = this.serializeData();! data = this.mixinTemplateHelpers(data);! ! var template = this.getTemplate();! var html = Marionette.Renderer.render(template, data);! ! this.$el.html(html);! this.bindUIElements();! ! this.triggerMethod("render", this);! this.triggerMethod("item:rendered", this);! ! return this;! }, • https://github.com/marionettejs/backbone.marionette/blob/master/src/ marionette.itemview.js
  11. Implement of Marionette.View#close close: function(){! // ͢Ͱʹดͯ͡Δ৔߹͸Կ΋͠ͳ͍! if (this.isClosed) {

    return; }! ! // onBeforeCloseϝιουͰfalseฦͨ͠ͱ͖͸ด͡Δͷ΍ΊΔ! var shouldClose = this.triggerMethod("before:close");! if (shouldClose === false){! return;! }! ! // closeΠϕϯτൃߦͯ͠uiʹbind͞ΕͯΔDOMΛ࡟আͯ͠viewΛ࡟আͯ͠Δ! this.isClosed = true;! this.triggerMethod(“close");! ! this.unbindUIElements();! ! this.remove();! }, • https://github.com/marionettejs/backbone.marionette/blob/master/src/ marionette.view.js
  12. View Structure • Marionette.Application͕RegionManagerΛ͍࣋ͬͯΔͷͰɺ Application(Region) > Layout > (Layout or

    ItemView or CollectionView or CompositeViewͱ͍͏ߏ੒ʹ͢Δ • Regionʹ௥Ճ͢ΔLayoutͰViewʹඞཁͳModel΍CollectionͷfetchΛߦ͓ͬͯ ͘ • ࢠViewΛඳը͢Δͱ͖ʹ͸͢Ͱʹσʔλ͕ἧ͍ͬͯΔํ͕ίʔυ͕ॻ͖΍ ͍͢ • LayoutҎԼͷView΋ඞཁͰ͋Ε͹Layoutʹͯ͠͞Βʹ෼ׂ͢Δ
  13. View Structure #menu (Region) #main (Region) Marionette Application LayoutView CollectionView

    ItemView ItemView ItemView ItemView LayoutViewͰitemViewͱ CollectionViewͰඞཁͳmodel΍ collectionΛfetch͓ͯ͘͠
  14. View Structure // Applicationͷmain RegionʹLayoutViewΛ௥Ճ͢Δ! App.main.show(new LayoutView()); // in LayoutView!

    onRender: function() {! var _this = this;! var model = new SomeModel();! var collection = new SomeCollection();! ! // modelͱcollectionΛfetchͯ͠itemͱcollectionʹViewΛ௥Ճ! model.fetch().done(function() {! _this.item.show(new ItemView(model: model);! });! collection.fetch().done(function() {! _this.list.show(new CollectionView(collection: collection);! });! ! // or! //$.when(model.fetch(), collection.fetch()).done(function(){! // _this.item.show(new ItemView(model: model);! // _this.list.show(new CollectionView(collection: collection);! //});! };!
  15. triggers MyView = Marionette.ItemView.extend({! ! // :۠੾ΓΛCamelCaseʹͯ͠onΛ໊͚ͭͨલͷϝιου͕͋Ε͹ݺΜͰ͘ΕΔ! // “foo:bar” =>

    onFooBar! triggers: {! “click .change-name”: “change:name”! },! ! onChangeName: function() { … },! }); • DOMΠϕϯτΛtriggersʹ·ͱΊͯॻ͍͓ͯ͘͜ͱ͕ग़དྷͯɺ preventDefaultɺstopPropagation΋΍ͬͯ͘ΕΔ
  16. BindUIElements MyView = Marionette.ItemView.extend({! ! // View͕৮ΔDOM͕͜͜Ͱఆٛ͞ΕͯΘ͔Γ΍͍͢͠! // ม਺໊:“ηϨΫλࢦఆ”! ui:

    {! name: “.name”! },! ! // triggersͷதͰ΋࢖͑Δ! triggers: {! “click @ui.name”: “change:name”! }! ! someMethod: function() {! // ͜Μͳײ͡Ͱ࢖͏! this.ui.name.hide()! }! }); • uiͱͯ͠ηϨΫλͱม਺໊Λఆ͓ٛͯ͘͠ͱthis.xxxͱ͍͏ܗࣜͰΞΫηε͢ Δ͜ͱ͕ग़དྷΔ
  17. modelEvents, collectionEvents, itemEvents… MyView = Marionette.ItemView.extend({! ! // Viewʹඥ͚͍ͮͯΔModelͷΠϕϯτΛߪಡ͢Δ! //

    space۠੾ΓͰෳ਺ͷϝιουΛొ࿥͢Δ͜ͱ΋ग़དྷΔ! // collectionEvents΋͋Δ! modelEvents: {! “change”: “onChangeModel otherMethod”! },! onChangeModel: function() { … },! otherMethod: function() { … },! }); • modelEvents, collectionEvents, itemEventsͱ͍ͬͨΦϒδΣΫτΛఆٛͯ͠ ͓͘͜ͱͰɺlistenTo΍stopListeningͱ͔ҙࣝͤͣʹ؆୯ʹΠϕϯτΛߪಡ͢ Δ͜ͱ͕ग़དྷΔ
  18. appEvents?? (My Custom Implement) • ApplicationҎԼͷRegionΛ·͍ͨͰ΍ΓͱΓΛ͍ͨ͜͠ͱ͕͋ͬͨͷͰ࣮૷ ͯ͠ΈͨɻApplicationʹlistenTo͢Δ͜ͱͰผRegionؒͰͷViewͷ΍ΓͱΓΛ Մೳʹ͢Δ • ↓ͷIssueͱجຊతʹ΍Γ͍ͨ͜ͱ͸ಉ͡

    • ʮSimplyfing listening to App.vent in viewsʯ • https://github.com/marionettejs/backbone.marionette/issues/826 • events, triggers, modelEventsͳͲҧͬͯɺ਌ࢠܑఋؔ܎ʹͳ͍ཁૉؒͳͷ Ͱ٫Լ͞ΕͯΔ
  19. appEvents?? (My Custom Implement) MyView = BaseView.extend({! // ߪಡ͢Δ! appEvents:

    {! “change:menu”: “change:menu”! },! onChangeMenu: function() {! …! }! }); // ࣮૷! // App.trigger(“xxxx”)Ͱൃߦ͢Δ! // modelEventsΈ͍ͨʹappEventsͰߪಡ͢Δ! BaseView = Marionette.Layout.extend({! delegateEvents: function(events){! Marionette.Layout.prototype.delegateEvents.call(this, events);! Marionette.bindEntityEvents(this, App, Marionette.getOption(this, “appEvents”));! },! undelegateEvents: function(){! Marionette.Layout.prototype.undelegateEvents.call(this);! Marionette.bindEntityEvents(this, App, Marionette.getOption(this, “appEvents”));! },! });
  20. serializeData and templateHelpers MyView = Marionette.ItemView.extend({! ! // σϑΥϧτͰthis.model͔this.collection͕templateʹ౉͞ΕΔͷΛม͍͑ͨ৔߹ʹ࢖͏! serializeData:

    function() {! return {! my: my.toJSON(),! notifications: notifications.toJSON(),! };! },! // templateͷதͰ࢖͍͍ͨؔ਺Λొ࿥ग़དྷΔ(serializeDataͱmerge͞ΕΔ)! // ൚༻తʹ࢖͏΋ͷ͸HandlebarsͩͱHandlebars.registerHelper࢖͏ͱ͍͍! templateHelpers: {! return {! notificationMessage: function(notification) {! // this͸serializeDataͷ஋! notification.message.replace(/__NAME__/g, this.my.name);! },! };! },! }); • templateʹ౉͢஋ΛΧελϚΠζ͍ͨ͠৔߹ʹ࢖͏
  21. PreCompiled template // Renderer.renderΛΦʔόʔϥΠυ͢Δ͜ͱͰprecompileͨ͠templateΛ࢖͏! Backbone.Marionette.Renderer.render = function(template, data){! return JST[template](data);!

    };! MyView = Marionette.ItemView.extend({! template:‘path/to/template’,! });! ! or! ! // templateʹ௚઀precompile͞ΕͨtemplateΛ౉͍ͯ͠Δ! MyView = Marionette.ItemView.extend({! template: JST[‘path/to/template’],! }); • templateʹ௚઀౉ͨ͠ΓɺMarionette.Renderer.renderΛΦʔόʔϥΠυͨ͠ Γ͢Δ͜ͱͰՄೳ
  22. Marionette.CollectionView And CompositeView • Marionette.ViewΛܧঝ͍ͯͯ͠ɺCollectionΛItemViewΛ࢖ͬͯ܁Γฦ͠දࣔ ͢Δͱ͖ʹ࢖͏ViewɻitemViewΛඞͣ࣋ͭ • template͸ఆٛग़དྷͳ͍ͷͰtableͳͲ୯७ͳ܁Γฦ͠Ͱ͸ͳ͍ͱ͖͸ CompositeViewΛ࢖͏ •

    CompositeView͸CollectionViewΛܧঝ͍ͯ͠Δ • Collection͕ۭͷ৔߹ʹ࢖ΘΕΔemptyViewΛఆٛग़དྷΔ • Collectionͷadd, remove, resetΠϕϯτ΋listen͍ͯͯ͠ɺViewͷߋ৽Λ΍ͬ ͯ͘ΕΔ
  23. Example of Marionette.CollectionView MyItemsView = Marionette.CollectionView.extend({! // tagΛࢦఆ͠ͳ͍ͱۭͷdiv͕࡞ΒΕΔͷͰɻ! // MyItemRowViewͷtagName͸”li”ʹͳΔ!

    tagName: “ul”,! itemView: MyItemRowView,! emptyView: EmptyItemView,! // itemEventsͱͯ͠! itemViewͷΠϕϯτΛߪಡ͢Δ͜ͱ͕ग़དྷΔ (>= 1.5.0)! itemEvents: {! “change:tab”: “onItemViewChangeTab”! },! onItemViewChangeTab: function(){ … },! });! ! var layout = new Marionette.Layout();! // myItemsͷ֤ཁૉΛMyItemRowViewΛ࢖ͬͯඳը͢Δ! // (ۭͷ৔߹͸EmptyItemView͕ඳը͞ΕΔ)! layout.myItems.show( new MyItemsView(collection: myItems) );
  24. Example of Marionette.CompositeView MyItemsView = Marionette.CompositeView.extend({! template: “composite/items”! itemViewContainer: “#item-body”,!

    itemView: MyItemRowView,! });! ! var layout = new Marionette.Layout();! layout.myItems.show( new MyItemsView(collection: myItems) );
  25. Application.module MyApp = new Marionette.Application();! ! // ఆٛͨ͠Ϟδϡʔϧɺݺͼग़͠ݩϞδϡʔϧɺBackbone, Marionette, $,

    _ ͷॱͰ౉͞ΕΔ! MyApp.module(“Model”, function(Model, App, Backbone, Marionette, $, _) {! Model.My = Backbone.Model.extend({});! }); ! ! // View͕ఆٛ͞Εͯͳ͚Ε͹ࣗಈతʹఆٛ͞ΕΔ! // defaultͰఆٛ͞Ε͍ͯΔҾ਺Ҏ֎ʹ΋͞Βʹ౉͢͜ͱ͕ग़དྷΔ! MyApp.module(“View.Layout”, function(Layout, App, Backbone, Marionette, $, _, w) {! Layout.My = Marionette.Layout.extend({});! }, window); ! ! ! // MyAppҎԼͷModule͕ಡΈࠐ·ΕΔ! MyApp.start()
  26. Generated HTML by server side • αʔόʔαΠυͰHTMLΛੜ੒͢ΔΑ͏ͳΞϓϦͷ৔߹͸ɺ Marionette.Region#attachView Λ࢖͏͜ͱͰcurrentView΁ͷׂΓ౰͚ͯͩΛ ߦ͏͜ͱ͕ग़དྷΔ

    • https://github.com/marionettejs/backbone.marionette/blob/master/src/ marionette.region.js // currentViewʹviewΛಥͬࠐΜͰ͍Δ͚ͩ! attachView: function(view){! this.currentView = view;! },
  27. Summary • MarionetteΛ࢖͍Marionette͕ఏڙ͢Δ࢓૊Έʹ৐͔࣮ͬͬͯ૷͢Δ͜ͱͰɺ Viewͷޙ࢝຤ͳͲҙࣝ͢Δ͜ͱ͕গͳ͘ͳΔ • BackboneͷViewͩͱ੍໿͕ͳ͍ͷͰࣗ༝ʹͳΓ͕ͪͳViewपΓͷ࣮૷͕ڞ௨Խ ͞Εͯɺଞͷਓ͕ॻ͍ͨίʔυ͕ಡΈ΍͘͢ͳΔ • modelEvents, triggers,

    ui ͳͲͰఆٛ͞ΕΔ͜ͱͰViewͷ໾ׂ͕Θ͔Γ΍͍͢ • BackboneͷԆ௕ઢ্ʹ͋ΔϑϨʔϜϫʔΫͳͷͰɺBackboneΛ࢖͍ͬͯΕ͹ཧ ղ͢Δͷ͸೉͘͠ͳ͍ɻ • ιʔε͕Θ͔Γ΍͍͢ͷͰษڧʹͳΔ