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

Avatar for Clemens Müller

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