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

Clemens Müller

November 07, 2017
Tweet

More Decks by Clemens Müller

Other Decks in Programming

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. • 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
  4. 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. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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/
  24. 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
  25. 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
  26. 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": "*" } }
  27. 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({ ... }); } }
  28. enginification • re-use common style definitions • ember-cli-sass • variables,

    font sizes, ... within vars.scss • colors within colors.scss • mixins in mixins/ 30
  29. 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'] } });
  30. 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'] } });
  31. 33

  32. 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 } }
  33. 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 } }
  34. 38

  35. 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
  36. 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
  37. FastBoot and lazy engine 45 $ ember new fastboot-engine-test $

    ember install ember-cli-fastboot $ ember install ember-engines
  38. 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.
  39. FastBoot and lazy engine 47 $ nvm use 8 $

    ember new fastboot-engine-test-2 $ ember install ember-cli-fastboot $ ember install ember-engines
  40. FastBoot and lazy engine 49 $ nvm use 8 $

    ember new fastboot-engine-test-tmp $ ember install ember-engines $ ember install ember-cli-fastboot
  41. FastBoot and lazy engine 51 $ ember new fastboot-engine-test-xyz $

    ember install ember-cli-fastboot $ ember install ember-engines
  42. 53

  43. FastBoot and lazy engine • happily together after all with

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

  45. 59

  46. // 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); } } });
  47. // 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); } } });
  48. // 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" } ] } } }
  49. // 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); } } });
  50. // 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); } }, });
  51. // 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); } }, });
  52. 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; }); }); });
  53. 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; }); }); });
  54. FastBoot and lazy engine • Preload booking-flow engine • So

    booking flow assets are present when needed 70
  55. // 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'); } } });
  56. // 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'); } } });
  57. 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
  58. 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
  59. 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
  60. 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
  61. 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
  62. Problems: engine tests // tests/helpers/resolver.js import Resolver from 'mweb/resolver'; const

    resolver = Resolver.extend({ }).create(); export default resolver; 78
  63. 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
  64. 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
  65. 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
  66. 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
  67. 83

  68. 84

  69. 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; }); }); });
  70. 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; }); }); });
  71. 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