Upgrade to Pro — share decks privately, control downloads, hide ads and more …

yarn downgrade ember

yarn downgrade ember

... or how being too lazy helped fixing a stuttering engine

A tale of marrying an Engine with FastBoot

97c57e7e99431e76fbc04173cca51eab?s=128

Clemens Müller

November 07, 2017
Tweet

Transcript

  1. yarn downgrade ember … or how being too lazy helped

    fixing a stuttering engine A tale of marrying an Engine with FastBoot Ember.run.begin();
  2. FastBoot and lazy Ember Engine … or how being too

    lazy helped fixing a stuttering engine A tale of marrying an Engine with FastBoot
  3. WHOIS • Hi, I’m Clemens • @pangratz • freelancer •

    ember-data core team
  4. • Currently working with the fine folks at • Help

    them build ship mobile and desktop app • Early adopters of SproutCore and Ember.js • But for this talk we focus on mobile app
  5. 5

  6. 6

  7. 7

  8. 8

  9. 9

  10. 10

  11. 11

  12. 12

  13. 13

  14. 14

  15. 15

  16. 16

  17. 17

  18. 18

  19. Status quo • FastBoot used to get advantages of SSR

    • used for decreasing perceived loading time • for now only shows basic skeleton and loading states, until app takes over 19
  20. Idea: FastBoot with lazy Ember Engine • make process snappier

    for first visit by only load relevant stuff: search form with station, passenger and date selection • move rest into engine to reduce initial, critical asset size, since only relevant once user decides to book • lazy engine is exactly for that 20
  21. ember-engine • composable applications for ambitious user experiences • allow

    you to compose a single application out of multiple, smaller applications • addon to extract common functionality and use it like it would be written within the app itself 21
  22. Idea: FastBoot with lazy Ember Engine • used versions at

    start of quest • Ember-CLI ~2.14 • Ember ~2.14 • Ember-Data ~2.12 • FastBoot 1.0.5 22
  23. enginification - attack plan • move most stuff out of

    app/ into lib/ • common in-repo-addon which contains
 styles / components / helpers • booking-flow in-repo-engine, which contains
 everything after search 23
  24. enginification: 1. step • ember generate in-repo-addon common • move

    common components / helpers into in-repo addon 24 ember generate component loading-indicator --in-repo common git mv app/components/loading-indicator.js \ lib/common/addon/components/ git mv app/templates/components/loading-indicator.hbs \ lib/common/addon/templates/components/
  25. enginification: 0. step 25 FASTBOOT_DISABLED=true ember serve

  26. enginification: 2. step • styles into common in-repo addon •

    placed in lib/common/app/styles/common // lib/common/app/styles/common/colors.scss $color-navy: #21314d; $color-snow: #f2f4f7; // lib/common/app/styles/common.scss @import "common/vars"; @import "common/colors"; @import "common/components/loading-indicator"; // app/styles/app.scss @import "common"; 26
  27. enginification: 3. step • move booking-flow specific stuff into in-repo-addon

    • ember generate in-repo-addon booking-flow • setup according to ember-engines.com • depends on common addon • moved over routes, controllers, components, ... 27
  28. enginification • npm dependencies • ember-browserify 28 // package.json {

    "dependencies": { "ember-browserify": "^1.2.0", "mailcheck": "^1.1.1" } } // lib/booking-flow/package.json { "dependencies": { "mailcheck": "*" } }
  29. enginification // lib/booking-flow/app/index.js import creditCardType from 'npm:credit-card-type'; import Mailcheck from

    'npm:mailcheck'; 29 // lib/booking-flow/addon/components/email-suggestion.js import Component from '@ember/component'; import Mailcheck from 'npm:mailcheck'; export default Component.extend({ didReceiveAttrs() { this._super(...arguments); this.checkEmailValidity(); }, checkEmailValidity() { Mailcheck.run({ ... }); } }
  30. enginification • re-use common style definitions • ember-cli-sass • variables,

    font sizes, ... within vars.scss • colors within colors.scss • mixins in mixins/ 30
  31. enginification // lib/booking-flow/addon/styles/addon.scss @import "common/colors"; @import "common/vars"; @import "common/mixins/button"; 31

    // lib/booking-flow/index.js const EngineAddon = require('ember-engines/lib/engine-addon'); module.exports = EngineAddon.extend({ sassOptions: { includePaths: ['lib/common/app/styles'] } });
  32. enginification // lib/booking-flow/addon/styles/addon.scss @import "common/colors"; @import "common/vars"; @import "common/mixins/button"; 32

    // lib/booking-flow/index.js const EngineAddon = require('ember-engines/lib/engine-addon'); module.exports = EngineAddon.extend({ sassOptions: { includePaths: ['lib/common/app/styles'] } });
  33. 33

  34. enginification 34 // app/router.js Router.map(function() { this.mount('booking-flow', { as: 'booking-flow',

    path: '/' }); });
  35. enginification 35 // lib/booking-flow/addon/routes/index.js import Route from '@ember/routing/route'; export default

    Route.extend({ redirect() { this.replaceWithExternal('index'); } });
  36. make it lazy • everything moved into engine / common

    addon • next: make it lazy 36 // lib/booking-flow/index.js const EngineAddon = require('ember-engines/lib/engine-addon'); module.exports = EngineAddon.extend({ lazyLoading: { enabled: false } }
  37. make it lazy • everything moved into engine / common

    addon • next: make it lazy 37 // lib/booking-flow/index.js const EngineAddon = require('ember-engines/lib/engine-addon'); module.exports = EngineAddon.extend({ lazyLoading: { enabled: true } }
  38. 38

  39. make it lazy 39 // TODO insert happy GIF

  40. FastBoot and lazy engine 40 FASTBOOT_DISABLED=true ember serve

  41. FastBoot and lazy engine 41 ember serve

  42. None
  43. FastBoot and lazy engine // lib/booking-flow/index.js const EngineAddon = require('ember-engines/lib/engine-addon');

    module.exports = EngineAddon.extend({ sassOptions: { includePaths: ['lib/common/app/styles'] }, getEngineConfigContents() { return `export default { modulePrefix: '${this.name}' };`; }, updateFastBootManifest(manifest) { manifest.vendorFiles.push('assets/node-asset-manifest.js'); manifest.vendorFiles.push('engines-dist/booking-flow/assets/engine-vendor.js'); manifest.vendorFiles.push('engines-dist/booking-flow/assets/engine.js'); return manifest; } }); 43
  44. FastBoot and lazy engine 44 $ nvm use 8 Now

    using node v8.5.0 (npm v5.3.0) $ ember -v ember-cli: 2.14.1 node: 8.5.0 os: darwin x64
  45. FastBoot and lazy engine 45 $ ember new fastboot-engine-test $

    ember install ember-cli-fastboot $ ember install ember-engines
  46. FastBoot and lazy engine 46 Error: Failed to load asset

    manifest. For browser environments, verify the meta tag with name "booking-flow/config/asset-manifest" is present. For non-browser environments, verify that you included the node-asset-manifest module.
  47. FastBoot and lazy engine 47 $ nvm use 8 $

    ember new fastboot-engine-test-2 $ ember install ember-cli-fastboot $ ember install ember-engines
  48. FastBoot and lazy engine 48 TypeError: Cannot read property 'router'

    of undefined
  49. FastBoot and lazy engine 49 $ nvm use 8 $

    ember new fastboot-engine-test-tmp $ ember install ember-engines $ ember install ember-cli-fastboot
  50. FastBoot and lazy engine 50 The route index was not

    found
  51. FastBoot and lazy engine 51 $ ember new fastboot-engine-test-xyz $

    ember install ember-cli-fastboot $ ember install ember-engines
  52. FastBoot and lazy engine 52 ember serve

  53. 53

  54. None
  55. FastBoot and lazy engine 55 $ ember -v

  56. FastBoot and lazy engine 56 $ ember -v ember-cli: 2.13.2

    node: 6.9.1 os: darwin x64
  57. FastBoot and lazy engine • happily together after all with

    ember 2.13 • FastBoot for snappy first impression • App / Engine takes over once loaded 57
  58. 58

  59. 59

  60. // app/routes/application.js import Route from '@ember/routing/route'; import { inject }

    from '@ember/service'; import assetsConfig from 'mweb/config/asset-manifest'; export default Route.extend({ assetLoader: inject(), fastboot: inject(), headData: inject(), afterModel() { if (this.get('fastboot.isFastBoot')) { let assets = assetsConfig['bundles']['booking-flow']['assets']; let engineCSS = assets.find(({ type }) => type === 'css'); this.set('headData.engineCSS', engineCSS.uri); } } });
  61. // app/routes/application.js import Route from '@ember/routing/route'; import { inject }

    from '@ember/service'; import assetsConfig from 'mweb/config/asset-manifest'; export default Route.extend({ assetLoader: inject(), fastboot: inject(), headData: inject(), afterModel() { if (this.get('fastboot.isFastBoot')) { let assets = assetsConfig['bundles']['booking-flow']['assets']; let engineCSS = assets.find(({ type }) => type === 'css'); this.set('headData.engineCSS', engineCSS.uri); } } });
  62. // app/routes/application.js import Route from '@ember/routing/route'; import { inject }

    from '@ember/service'; import assetsConfig from 'mweb/config/asset-manifest'; export default Route.extend({ assetLoader: inject(), fastboot: inject(), headData: inject(), afterModel() { if (this.get('fastboot.isFastBoot')) { let assets = assetsConfig['bundles']['booking-flow']['assets']; let engineCSS = assets.find(({ type }) => type === 'css'); this.set('headData.engineCSS', engineCSS.uri); } } }); { "bundles": { "booking-flow": { "assets": [ { "uri": "/engines-dist/booking-flow/assets/engine-vendor.js", "type": "js" }, { "uri": "/engines-dist/booking-flow/assets/engine.css", "type": "css" }, { "uri": "/engines-dist/booking-flow/assets/engine.js", "type": "js" } ] } } }
  63. // app/routes/application.js import Route from '@ember/routing/route'; import { inject }

    from '@ember/service'; import assetsConfig from 'mweb/config/asset-manifest'; export default Route.extend({ assetLoader: inject(), fastboot: inject(), headData: inject(), afterModel() { if (this.get('fastboot.isFastBoot')) { let assets = assetsConfig['bundles']['booking-flow']['assets']; let engineCSS = assets.find(({ type }) => type === 'css'); this.set('headData.engineCSS', engineCSS.uri); } } });
  64. // app/templates/head.js {{#if model.engineCSS}} <link rel="stylesheet" href={{model.engineCSS}} > {{/if}}

  65. // lib/booking-flow/addon/routes/application.js import Route from '@ember/routing/route'; import { inject }

    from '@ember/service'; export default Route.extend({ fastboot: inject(), headData: inject(), beforeModel() { if (this.get('fastboot.isFastBoot')) { this.set('headData.includeEngineCSS', true); } }, });
  66. // lib/booking-flow/addon/routes/application.js import Route from '@ember/routing/route'; import { inject }

    from '@ember/service'; export default Route.extend({ fastboot: inject(), headData: inject(), beforeModel() { if (this.get('fastboot.isFastBoot')) { this.set('headData.includeEngineCSS', true); } }, });
  67. // app/templates/head.js {{#if model.engineCSS}} {{#if model.includeEngineCSS}} <link rel="stylesheet" href={{model.engineCSS}} >

    {{/if}} {{/if}}
  68. FastBoot tests 68 // fastboot-tests/order-test.js const { expect } =

    require('chai'); const visit = require('./utils/visit'); describe('order route', function() { it('includes the CSS of the booking-flow engine', function() { return visit('/checkout/confirm/__test__').then((response) => { let engineCSS = response.$('link[rel=stylesheet][href*="booking-flow/assets/engine.css"]'); expect(engineCSS.length).to.be.ok; }); }); });
  69. FastBoot tests 69 // fastboot-tests/order-test.js const { expect } =

    require('chai'); const visit = require('./utils/visit'); describe('order route', function() { it('includes the CSS of the booking-flow engine', function() { return visit('/checkout/confirm/__test__').then((response) => { let engineCSS = response.$('link[rel=stylesheet][href*="booking-flow/assets/engine.css"]'); expect(engineCSS.length).to.be.ok; }); }); });
  70. FastBoot and lazy engine • Preload booking-flow engine • So

    booking flow assets are present when needed 70
  71. // app/routes/application.js import Route from '@ember/routing/route'; import { inject }

    from '@ember/service'; import assetsConfig from 'mweb/config/asset-manifest'; export default Route.extend({ assetLoader: inject(), fastboot: inject(), headData: inject(), afterModel() { if (this.get('fastboot.isFastBoot')) { let assets = assetsConfig['bundles']['booking-flow']['assets']; let engineCSS = assets.find(({ type }) => type === 'css'); this.set('headData.engineCSS', engineCSS.uri); } else { let assetLoader = this.get('assetLoader'); assetLoader.loadBundle('booking-flow'); } } });
  72. // app/routes/application.js import Route from '@ember/routing/route'; import { inject }

    from '@ember/service'; import assetsConfig from 'mweb/config/asset-manifest'; export default Route.extend({ assetLoader: inject(), fastboot: inject(), headData: inject(), afterModel() { if (this.get('fastboot.isFastBoot')) { let assets = assetsConfig['bundles']['booking-flow']['assets']; let engineCSS = assets.find(({ type }) => type === 'css'); this.set('headData.engineCSS', engineCSS.uri); } else { let assetLoader = this.get('assetLoader'); assetLoader.loadBundle('booking-flow'); } } });
  73. Problems: engine tests • Engine tests are within apps' tests/

    folder • Existing tests need to be rewritten to use resolver from engine • our approach is to overwrite tests' resolver 73
  74. Problems: engine tests // tests/integration/components/search-station-test.js import { moduleForComponent, test }

    from 'ember-qunit'; import hbs from 'htmlbars-inline-precompile'; moduleForComponent('search-station', 'Integration | Component | search-station', { integration: true }); test('renders le component', function(assert) { this.render(hbs`{{search-station}}`); // ... }); 74
  75. Problems: engine tests // tests/integration/components/search-station-test.js import { moduleForComponent, test }

    from 'ember-qunit'; import hbs from 'htmlbars-inline-precompile'; moduleForComponent('search-station', 'Integration | Component | search-station', { integration: true }); test('renders le component', function(assert) { this.render(hbs`{{search-station}}`); // ... }); 75
  76. Problems: engine tests // tests/integration/components/search-station-test.js import { moduleForComponent, test }

    from 'ember-qunit'; import hbs from 'htmlbars-inline-precompile'; import engineResolverFor from 'ember-engines/test-support/engine-resolver-for'; const resolver = engineResolverFor('booking-flow'); moduleForComponent('search-station', 'Integration | Component | search-station', { integration: true, resolver }); test('renders le component', function(assert) { this.render(hbs`{{search-station}}`); // ... }); 76
  77. Problems: engine tests // tests/integration/components/search-station-test.js import { moduleForComponent, test }

    from 'ember-qunit'; import hbs from 'htmlbars-inline-precompile'; import engineResolverFor from 'ember-engines/test-support/engine-resolver-for'; const resolver = engineResolverFor('booking-flow'); moduleForComponent('search-station', 'Integration | Component | search-station', { integration: true, resolver }); test('renders le component', function(assert) { this.render(hbs`{{search-station}}`); // ... }); 77
  78. Problems: engine tests // tests/helpers/resolver.js import Resolver from 'mweb/resolver'; const

    resolver = Resolver.extend({ }).create(); export default resolver; 78
  79. Problems: engine tests // tests/helpers/resolver.js import Resolver from 'mweb/resolver'; import

    engineResolverFor from 'ember-engines/test-support/engine-resolver-for'; let bookingFlowResolver = engineResolverFor('booking-flow'); const resolver = Resolver.extend({ resolve() { let resolved = this._super(...arguments); if (resolved) { return resolved; } return bookingFlowResolver.resolve(...arguments); }, }).create(); export default resolver; 79
  80. Problems: engine tests // tests/helpers/resolver.js import Resolver from 'mweb/resolver'; import

    engineResolverFor from 'ember-engines/test-support/engine-resolver-for'; let bookingFlowResolver = engineResolverFor('booking-flow'); const resolver = Resolver.extend({ resolve() { let resolved = this._super(...arguments); if (resolved) { return resolved; } return bookingFlowResolver.resolve(...arguments); }, }).create(); export default resolver; 80
  81. Problems: engine tests // tests/integration/components/search-station-test.js import { moduleForComponent, test }

    from 'ember-qunit'; import hbs from 'htmlbars-inline-precompile'; moduleForComponent('search-station', 'Integration | Component | search-station', { integration: true }); test('renders le component', function(assert) { this.render(hbs`{{search-station}}`); // ... }); 81
  82. Next tasks • Interdependent services, hard to move to engine

    • Ember Data models need to be defined in app • FastBoot itself leads to a new category of issues: • not-yet-functioning state needs to be communicated to the user 82
  83. 83

  84. 84

  85. FastBoot tests 85 // fastboot-tests/order-test.js const { expect } =

    require('chai'); const visit = require('./utils/visit'); describe('order route', function() { it('disables interactive elements', function() { return visit('/order/__test__').then((response) => { expect(response.$('[data-test-travel-details-header]').hasClass('disabled')).to.be.true; expect(response.$('[data-test-cancel-folder]').hasClass('disabled')).to.be.true; }); }); it('includes the CSS of the booking-flow engine', function() { return visit('/checkout/confirm/__test__').then((response) => { let engineCSS = response.$('link[rel=stylesheet][href*="booking-flow/assets/engine.css"]'); expect(engineCSS.length).to.be.ok; }); }); });
  86. FastBoot tests 86 // fastboot-tests/order-test.js const { expect } =

    require('chai'); const visit = require('./utils/visit'); describe('order route', function() { it('disables interactive elements', function() { return visit('/order/__test__').then((response) => { expect(response.$('[data-test-travel-details-header]').hasClass('disabled')).to.be.true; expect(response.$('[data-test-cancel-folder]').hasClass('disabled')).to.be.true; }); }); it('includes the CSS of the booking-flow engine', function() { return visit('/checkout/confirm/__test__').then((response) => { let engineCSS = response.$('link[rel=stylesheet][href*="booking-flow/assets/engine.css"]'); expect(engineCSS.length).to.be.ok; }); }); });
  87. Learnings • FastBoot adds another layer of complexity • you

    basically exchange SSR advantages for new kind of problems you have to think about • Engines are great and pretty straight forward to use • common addon from the start • the most obnoxious bugs are always one line fixes 87
  88. Presentation ingredients • https://www.smore.com/clippy-js • QuickTime Screen Recording • https://github.com/jclem/gifify

    • https://atom.io/packages/copy-as-rtf && Atom Editor 88
  89. Ember.run.end();