Micro Frontends with Vue.js

Micro Frontends with Vue.js

F275b2b79faa1b5f1a1bec7950dbee63?s=128

AlexanderTserkovniy

November 09, 2019
Tweet

Transcript

  1. Micro Frontends with Vue.js

  2. PRESENTATION CONTENT Intro Tech part Q&A

  3. PMLAB – это украинская продуктовая компания c full-cycle development в

    сфере Entertainment. Мы являемся частью международного Холдинга, который представлен в шести странах мира с общим числом сотрудников - 1 600 человек.
  4. У нас есть стенд, подходите пообщаемся

  5. In web dev since 2008 JS Fest program committee member

    Consultant in tech recruiting and Full Stack JavaScript architecture Author of the top 1 2016-2018 DOU article https://dou.ua/lenta/articles/dont-move-abroad/ Oleksandr Tserkovnyi FE Architect Backoffice & C team
  6. None
  7. Front-end Developer Handbook 2019 http://bit.ly/2GiDP3o Image link http://bit.ly/2UKlxSH

  8. https://hackernoon.com/understanding-micro-frontends-b1c11585a297 Micro Frontends

  9. https://www.cygnismedia.com/blog/micro-frontends-development/

  10. https://www.cygnismedia.com/blog/micro-frontends-development/

  11. Independent deploy When one team wants to deploy without triggering

    regression or just another team’s part. Clear boundaries Test coverage, monitoring, version control. A&B testing ease Easier to cover with A&B testing individual parts instead of hacking on top of monolith. Use/update any contact/library Micro Frontend team decides what to use in their project.
  12. You might NOT need it Is your delivery long? Do

    you have many product owners in one project? Might you trigger regression of another team? Is your code reusable as much as possible? You have 2-3 devs in the whole team and it is enough. Do you need different setup for various user groups?
  13. It is expensive Much time, regression, communication Hard to sell

    to business Make it incremental Dedicate only a part of resource Sell it as multiplier Sell it as individual products following common standard (ind. deploy)
  14. No technologies, no tools No industrial standard Only two runtime

    options (iframe and lazy load) It is not something, which is inevitably will become all frontends 14
  15. Issues are the same 1. Size 2. Scalability 3. Upgrade

    version of main framework/library 4. Long delivery 5. Long CI&CD
  16. One REST API to glue them all?

  17. None
  18. Front-end specificity Different consumers Not only code, but markup No

    concept of scalability rules for a “container” You cannot enter a “container” by user behalf (2g, device, surr.) Others code might break yours 18
  19. TECH PART Intro Tech part Q&A

  20. None
  21. None
  22. Backoffice Internal usage only Latest chrome (CSS Grids, no polyfills)

    Operating since 2016 Universal for the whole company
  23. We want independent deploy Team 1 wants to delivery their

    small change not affecting Team 2. We don’t want to stop development Please continue delivery of the features. We want standards Please be able to pass experience to other teams, even the outsource without loosing the quality. We want identical look and feel Every section should look the same.
  24. Business requirements 1. Business have to continue operating 2. Time

    to market 24
  25. None
  26. Discuss the request and possible solutions Share the experience POC

    Set requirements (DO NOT create a spaceship, solve only your issues) Only Vue.js Drawn architecture Tickets, planning and finally sprint Grooming
  27. Components reusability Services reusability (data fetching, user identity etc.) Caching

    of the libraries which are in use by other Micro Frontends Routing Integration testing UX adaptation
  28. TECH DEBT Components in the common folder Utils in the

    utils folder or scattered around Unit tests coverage is something around ~0% Constants in one file
  29. Components reusability

  30. https://storybook.js.org/ Industrial standard for reusable components Separate testing (snapshot, unit,

    screen capturing testing) Force to think components concepts Versatile (разносторонний) to npm and global find&replace Vue.use(BoKit) plugin instead of import BoButton from './BoButton'; Vue.component('BoButton', BoButton);
  31. Utilities and constants 31

  32. Adapter npm library instead of global find&replace imports left untouched

    but the inside content was changed: import { httpClient } from ‘bo-utils’; export default httpClient;
  33. Unit tests after separation

  34. POC schema 1. User enters website 2. Page with core.js

    and other vital small JavaScript code is loaded 3. Router is downloaded 4. User goes to some page via router 5. Encounters a spinner 6. Micro Frontend files are downloaded to the page 7. Cached libraries of the same version are used 8. Connected to the main store and router 9. Layout service places this Micro Frontend to his placeholder 10. Rendered 11. User interaction
  35. Don’t code just for code 1. User enters website 2.

    Core.js is basically what’s left inside main project and is called SHELL 3. Router is NOT downloaded, why for? 4. User goes to some page via router 5. Encounters a spinner 6. Micro Frontend files are downloaded to the page 7. Cashing might be in the future, but for now they need different versions 8. DO NOT Connected to the main store and router, why for? 9. DO NOT Overengineer we have only one layout, placeholders are just components 10. Rendered 11. User interaction
  36. POC Fate

  37. None
  38. Usually thrown away Ideal is not finished! The milestones road

    Make it release candidate
  39. POC is marinading on PROD 39 We polish our solution

  40. render /destroy / initialize

  41. static file server / CDN

  42. export to window

  43. stores separated / EE

  44. own router / root router / base

  45. Architecture entities 45 Following single responsibility principle

  46. SHELL – core entity, set up using config ROUTER –

    main routes and global routes orchestration is placed in SHELL as well (passed as a prop during each Micro Frontend's rendering phase). Visually it is presented in sidebar. It dictates at which URL, which Micro Frontend to run. LOADER – this entity could fetch user's Micro Frontends versions and the Micro Frontend files themselves. RENDERER – component which receives a prop of Micro Frontend name, retrieves it from global microservices storage (currently it is window[namespace] (Webpack)) using LOADER's method. MANIFEST – a json schema, which contains list of the files created by a single Micro Frontend and paths for these files, as they must be downloaded. LOCALIZATION – provides an API for localization of texts within any Micro Frontend (passed as a prop during each Micro Frontend's rendering phase). IDENTITY – provides an API to detect user related data and ACL, for instance email (passed as a prop during each Micro Frontend's rendering phase). Helper source: https://www.youtube.com/watch?v=2r9KqASOSeM Mykhailo Churilov "Front-end Microservices Architecture"
  47. MANIFEST 47 Cache breaking

  48. { "app.css": "/players-search-app.ad2.css", "app.js": "/players-search-app.ad2.js", "chunk-vendors.css": "/players-search-chunk-vendors.ad2.css", "chunk-vendors.js": "/players-search-chunk-vendors.ad2.js" }

  49. RENDERER 49 Component which receives a prop of Micro Frontend

    name, retrieves it from global microservices storage (currently it is window[namespace] (Webpack)) using LOADER's method.
  50. <template> <section> <div class="microfrontend-wrapper"> <div> <bo-loader /> </div> </div> <pre

    v-if="error" class="error"> Unable to load microfrontend '<strong>{{ name }}</strong>' <div> {{error.message}} {{error.stack}} </div> </pre> </section> </template>
  51. watch: { $route() { this.currentInstance && this.destroyMicrofrontend(this.currentInstance); this.renderMicrofrontend(this.name, this.base); }

    }, mounted() { this.renderMicrofrontend(this.name, this.base); }, destroyed() { this.destroyMicrofrontend(this.name); }
  52. async renderMicrofrontend(name, base) { await this.$mf.ensureMicrofrontendReady(this.name); this.isReady = false; this.error

    = null; try { const host = document.createElement('div'); const wrapper = this.$el.querySelector('.microfrontend-wrapper div'); wrapper.innerHTML = ''; wrapper.appendChild(host); const microfrontend = await this.$mf.getMicrofrontendByName(name); microfrontend.render(host, { base: base }); this.currentInstance = name; } catch (e) { this.error = e; throw e; }
  53. async destroyMicrofrontend(name) { try { const microfrontend =await this.$mf.getMicrofrontendByName(name); microfrontend

    && microfrontend.destroy(); } finally { this.isReady = false; this.currentInstance = null; } }
  54. LOADER 54 This entity could fetch user's Micro Frontends versions

    and the Micro Frontend files themselves.
  55. async function getManifest(uri) { return fetch(uri).then(response => { return response.json();

    }); } P.S. `try {} catch {}` is above
  56. async function getFileCollection(name, version) { const url = `/microfrontends/${name}/${version}`; const

    manifest = await getManifest(`${url}/manifest.json`); return Object.values(manifest).reduce((result, value) => { if (value.endsWith('.js')) { result.js.unshift(`${url}${value}`); } if (value.endsWith('.css')) { result.css.unshift(`${url}${value}`); } return result; }, { css: [], js: [] } ); }
  57. async function getFileCollectionWithLocalOverride(name, version) { const port = window.__MF_DEV[name.toUpperCase()]; if

    (port) { console.warn(`Microfrontend '${name}' was loaded from localhost`); return { js: [`//localhost:${port}/${name.toLowerCase()}-app.js?v=` + +new Date()] }; } return getFileCollection(name, version); }
  58. async ensureMicrofrontendReady(name) { if (this.state[name]) return true; const version =

    this.props.bomf.versions[name]; try { const config = window.__MF_DEV ? await getFileCollectionWithLocalOverride(name, version) : await getFileCollection(name, version); for (const css of config.css || []) {/**/} for (const js of config.js || []) {/**/} const microfrontend = await this.getMicrofrontendByName(name); const notify = this.props.notify; microfrontend && microfrontend.initialize({ ...this.props, notify: { error(e, atts = {}) { notify.error(e, { ...atts, source: name, version }); } } }); this.state[name] = microfrontend;
  59. getMicrofrontendByName(name) { const namespace = '__bo__mf__'; return window[namespace] && window[namespace][name];

    }
  60. window['__bo__mf__'] = window['__bo__mf__'] || {}, window['__bo__mf__']['players-search'] = // ...

  61. ROUTER 61 Main routes and global routes orchestration is placed

    in SHELL as well (passed as a prop during each Micro Frontend's rendering phase). Visually it is presented in sidebar. It dictates at which URL, which Micro Frontend to run.
  62. const router = new Router({ mode: 'history', routes: [ {

    path: '/login', name: 'Login', component: Login }, { path: '/players/search', components: { default: PlayerSearch, leftSideBar: Navigation } } // … was
  63. const router = new Router({ mode: 'history', routes: [ {

    path: '/login', name: 'Login', component: Login }, { path: '/players/search', components: { leftSideBar: Navigation, default: Microfrontend // RENDERER }, props: { default: { name: 'players-search', base: '/' } } }, // … became
  64. export const createRouter = base => { return new Router({

    mode: 'history', routes: [ { path: `${base}players/search`, name: 'PlayerSearch', component: PlayerSearch }, // ...
  65. { path: `${base}players/:playerId/`, name: 'header', component: Player, children: [ {

    path: `trxhistory`, name: 'trxhistory', component: TRXHistory }, { path: 'auditlog', name: 'auditlog', components: { default: Microfrontend }, props: { default: { name: 'auditlog', base: `${base}players` } } }, // ...
  66. Micro Frontend entry 66

  67. Vue.use(...); async function initialize(props) { applyIdentityService(props.identity); applyLocalizationService(props.localization); applyRootRouter(props.rootRouter); applyEventEmitter(props.ee); Vue.use(UIKit,

    {}); Vue.use(BoMicrofrontendKit, props); Vue.config.errorHandler = err => props.notify.error(err); } function render(element, { base }) {} function destroy() {} export { initialize, render, destroy };
  68. Local development 68 CI&CD feature versions, deploy

  69. Bootstrapping 69 `vue create` `-p, --preset <presetName> Skip prompts and

    use saved or remote preset` `vue create --preset gitlab:git.betlab.com:CoreFront/vue-cli-bo-microfrontend- template --clone project-name`
  70. None
  71. commitlint 71

  72. None
  73. None
  74. semantic-release

  75. Backoffice for Backoffice 75 Canary releases

  76. 76

  77. None
  78. None
  79. Newrelic 79 As a tool for post-production monitoring

  80. 80

  81. 81

  82. Final architecture flow

  83. ROUTER

  84. Decisions and achievements 1. 2 months only 2. Did not

    stop feature dev 3. Last week – release 4. Business operating releases as usual 5. Move to Micro Frontend 1 day 6. Create new Micro Frontend ~5 day 7. Node.js instead of BE devs 8. Flexibility in terms of versioning
  85. Next steps 1. Continuous deployment instead of continuous delivery 2.

    Automated rollbacks based on newrelic data 3. Performance testing of single MFE
  86. Take away list 86 1. Sell independent deploy as key

    to faster time to market 2. Sell MFEs as less error prone entities due to encapsulation 3. Think about local development 4. Use vue-cli presets for fast bootstrapping 5. Use storybook 6. Use POC as release candidate 7. Groom with the team 8. Don’t build spaceship. Focus on the goal. Ideal is not finished.
  87. THANK YOU

  88. Q&A Intro Tech part Q&A WE ARE HIRING http://bit.ly/2Cef3zx