EmberFest 2017 - Ember @ Netflix

C8fccffc013096c4b465b50c284a5208?s=47 Lauren Tan
February 24, 2018

EmberFest 2017 - Ember @ Netflix

This talk was presented at EmberFest 2017
by Lauren Tan and Offir Golan. Recording available here -
https://pusher.com/sessions/meetup/emberfest/ember-netflix

For a few years now, Netflix has been leveraging Ember to build ambitious applications that help us manage and produce billions of dollars in content. In this talk, we'll share our lessons learned, talk about some of the common problems we've faced, and how we've solved them.

C8fccffc013096c4b465b50c284a5208?s=128

Lauren Tan

February 24, 2018
Tweet

Transcript

  1. React @ Netflix Offir Golan & Lauren Tan ReactFest 2017

  2. None
  3. None
  4. Lol jk

  5. Ember @ Netflix Offir Golan & Lauren Tan EmberFest 2017

  6. @offirgolan @offirgolan @offirgolan

  7. None
  8. ember-cp-validations ember-light-table ember-burger-menu ember-time-machine ember-parachute ember-data-copyable ember-require-module ember-validators ember-cli-nvd3 ember-addon-genie

    ember-bootstrap-cp-validations ember-cli-inject-meta ember-changeset-cp-validations ember-cli-jsoneditor ember-host-manager ember-query-builder
  9. @sugarpirate_ @poteto @sugarpirate

  10. How i got here

  11. I like computers

  12. ember-changeset ember-lazy-video ember-pipeline ember-composable-helpers ember-parachute ember-metrics ember-one-way-controls ember-crumbly ember-deep-set ember-in-viewport

    ember-test-component ember-cli-flash ember-changeset-validations ember-api-feature-flags ember-route-action-helper ember-macaroni
  13. None
  14. None
  15. None
  16. None
  17. None
  18. None
  19. None
  20. None
  21. WHY EMBER?

  22. Over 20 large apps

  23. Studio Apps Wrap Pitch Negotiation Production Prep Post Origin Story

    Watson Budget Gravity Murdock Jet Moneyball Amadeus Orion Sherlock
  24. Authentication ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓

    Data Loading ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ Shared Data Models ✓ ✓ ✓ ✓ ✓ ✓ ✓ Data Tables ✓ ✓ ✓ ✓ ✓ ✓ ✓ Data Entry ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ Deployment ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
  25. None
  26. None
  27. None
  28. None
  29. None
  30. AUTHENTICATION

  31. None
  32. BOLT UI

  33. None
  34. Bolt Meechum GET /users Authorization Bearer ...

  35. None
  36. None
  37. import DS from 'ember-data'; import MeechumProxyMixin from 'ember-cli-meechum-proxy/mixins/adapters/meechum-proxy'; export default

    DS.JSONAPIAdapter.extend(MeechumProxyMixin, { // ... });
  38. export default Service.extend(Evented, { /** * Handle an unauthorized response.

    Invoke this method if you are making a * non ember-data request that receives a 401. * * @method handleUnauthorizedResponse * @public */ handleUnauthorizedResponse() { run.cancel(this._refreshTokenTimer); this.trigger('sessionExpired'); }, // ... });
  39. None
  40. None
  41. BEFORE STUDIO

  42. None
  43. None
  44. None
  45. None
  46. DATA LOADING

  47. None
  48. import Ember from 'ember'; export default Ember.Route.extend({ model({ search })

    { return this.store.queryRecord('user', { search }); } });
  49. import Ember from 'ember'; export default Ember.Controller.extend({ queryParams: [ 'buyingTeam',

    'category', 'genre', 'programmingStatus', 'title' ], buyingTeam: null, category: null, genre: null, programmingStatus: null, title: null });
  50. import Ember from 'ember'; export default Ember.Route.extend({ queryParams: { buyingTeam:

    { refreshModel: true }, category: { refreshModel: true }, genre: { refreshModel: true }, programmingStatus: { refreshModel: true }, title: { refreshModel: true } } });
  51. import Ember from 'ember'; const { Mixin } = Ember;

    const { assign } = Object; export default function makeMixin(props = {}) { return Mixin.create(assign({}, props)); } https://ember-twiddle.com/de8c4d4aaf2b2e30f6593b671967c5a6?openFiles=utils.make-mixin.js%2C
  52. import Ember from 'ember'; import makeMixin from '../utils/make-mixin'; const mixinA

    = makeMixin({ value: 'a' }); const mixinB = makeMixin({ value: 'B' }); export default Ember.Controller.extend(mixinB, { appName: 'Ember Twiddle' }); https://ember-twiddle.com/de8c4d4aaf2b2e30f6593b671967c5a6?openFiles=utils.make-mixin.js%2C
  53. import QueryParamsBuilder from './index'; export default class EventQueryParamsBuilder extends QueryParamsBuilder

    { static TypeMap() { return { 'buying_teams': { type: 'array', defaultValue: undefined, refreshModel: true }, 'title': { type: 'string', defaultValue: undefined, refreshModel: true } }; } }
  54. PARACHUTE $ ember install ember-parachute

  55. None
  56. None
  57. ROUTE / model() async LOADING CONTROLLER / SEARCH INPUT search

    =
  58. ROUTE / model() async LOADING CONTROLLER / SEARCH INPUT search

    =
  59. ROUTE / model() async LOADING CONTROLLER / SEARCH INPUT search

    =
  60. None
  61. ROUTE / model() async LOADING CONTROLLER / SEARCH INPUT search

    =
  62. ROUTE / model() async LOADING CONTROLLER / SEARCH INPUT search

    =
  63. ROUTE / model() async LOADING CONTROLLER / SEARCH INPUT search

    =
  64. ROUTE / model() async LOADING CONTROLLER / SEARCH INPUT search

    =
  65. None
  66. import QueryParams from 'ember-parachute'; export const myQueryParams = new QueryParams({

    search: { defaultValue: '', refresh: true } });
  67. export default Ember.Controller.extend(myQueryParams.Mixin, { queryParamsDidChange({ shouldRefresh, queryParams }) { if

    (shouldRefresh) { this.get('fetchUsers').perform(queryParams.search); } }, fetchUsers: task(function*(search) { yield timeout(500); const res = yield fetch( `https://api.github.com/search/users?q=${search}` ).then(res => res.json()); this.set('users', res.items); }).restartable() });
  68. ROUTE / fetch() CONTROLLER / SEARCH INPUT queryParamsDidChange search =

  69. ROUTE / fetch() CONTROLLER / SEARCH INPUT queryParamsDidChange search =

  70. ROUTE / fetch() CONTROLLER / SEARCH INPUT queryParamsDidChange search =

  71. ROUTE / fetch() CONTROLLER / SEARCH INPUT queryParamsDidChange search =

  72. https://goo.gl/rGGatD

  73. COMPONENT user-card TEMPLATE template.hbs TEMPLATE loading.hbs STATE loading

  74. SO MUCH DATA

  75. None
  76. None
  77. None
  78. None
  79. None
  80. None
  81. ember install @netflix/studio-data-core

  82. // models/some-model.js import SomeModel from 'studio-data-core/models/some-model'; export default SomeModel.extend({ //

    App specific logic... });
  83. const EmberApp = require('ember-cli/lib/broccoli/ember-app'); module.exports = function(defaults) { let app

    = new EmberApp(defaults, { 'studio-data-core': { importAll: true } }); return app.toTree(); };
  84. WHAT ABOUT TESTING?

  85. None
  86. // app/models/series.js import DS from 'ember-data'; const { Model, hasMany,

    belongsTo } = DS; export default Model.extend({ statusType: belongsTo('status-type'), episodes: hasMany('episode') }); // mirage/models/series.js import { Model, belongsTo, hasMany } from 'ember-cli-mirage'; export default Model.extend({ statusType: belongsTo(), episodes: hasMany() }); Ember Data Models → Mirage Models
  87. None
  88. None
  89. None
  90. None
  91. // mirage/server.js // Register all models schema.registerModels(config.models);

  92. // Merge models from autogenerated Ember Data models with user

    defined models if (hasEmberData && config.discoverEmberDataModels) { let models = {}; assign(models, getModels(), config.models || {}); config.models = models; } // ... // Register all models schema.registerModels(config.models);
  93. Object.keys(requirejs.entries) .filter(module => !!module.match(modelMatchRegex)) .forEach(path => { let paths =

    path.split('/'); let modelName = paths[paths.length - 1]; let model = require(path, null, null, true).default; if (isDsModel(model)) { DsModels[modelName] = model; } });
  94. Object.keys(requirejs.entries) .filter(module => !!module.match(modelMatchRegex)) .forEach(path => { let paths =

    path.split('/'); let modelName = paths[paths.length - 1]; let model = require(path, null, null, true).default; if (isDsModel(model)) { dsModels[modelName] = model; } });
  95. None
  96. None
  97. Object.keys(getDsModels()).forEach(modelName => { let model = models[modelName]; let attrs =

    {}; model.eachRelationship((name, r) => { if (r.kind === 'belongsTo') { attrs[name] = belongsTo(r.type, r.options); } else if (r.kind === 'hasMany') { attrs[name] = hasMany(r.type, r.options); } }); mirageModels[modelName] = MirageModel.extend(attrs); });
  98. Object.keys(getDsModels()).forEach(modelName => { let model = models[modelName]; let attrs =

    {}; model.eachRelationship((name, r) => { if (r.kind === 'belongsTo') { attrs[name] = belongsTo(r.type, r.options); } else if (r.kind === 'hasMany') { attrs[name] = hasMany(r.type, r.options); } }); mirageModels[modelName] = MirageModel.extend(attrs); });
  99. Object.keys(getDsModels()).forEach(modelName => { let model = models[modelName]; let attrs =

    {}; model.eachRelationship((name, r) => { if (r.kind === 'belongsTo') { attrs[name] = belongsTo(r.type, r.options); } else if (r.kind === 'hasMany') { attrs[name] = hasMany(r.type, r.options); } }); mirageModels[modelName] = MirageModel.extend(attrs); });
  100. None
  101. DATA TABLES

  102. None
  103. Lol jk

  104. SLATE

  105. None
  106. SCROLL

  107. None
  108. SCROLL

  109. None
  110. None
  111. None
  112. None
  113. EMBER-LIGHT-TABLE EMBER-TABLE HANDS-ON-TABLE VERTICAL-COLLECTION HORIZONTAL-COLLECTION

  114. EMBER-X-TABLE Chris Garrett @pzuraq Erik Bryn @ebryn Offir Golan @offirgolan

    Alex Alvarez @alexander-alvarez Jan Buschtöns @buschtoens
  115. WHAT WE ARE WORKING TOWARDS

  116. None
  117. ember new my-app -b @netflix/studio-app DESIGN SYSTEM AUTHENTICATION METRICS LOGGING

    DEPLOYMENT CI / CD
  118. ENDE