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

Micro Frontends with Vue.js

Micro Frontends with Vue.js

AlexanderTserkovniy

November 09, 2019
Tweet

Other Decks in Programming

Transcript

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

    сфере Entertainment. Мы являемся частью международного Холдинга, который представлен в шести странах мира с общим числом сотрудников - 1 600 человек.
  2. 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
  3. 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.
  4. 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?
  5. 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)
  6. 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
  7. Issues are the same 1. Size 2. Scalability 3. Upgrade

    version of main framework/library 4. Long delivery 5. Long CI&CD
  8. 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
  9. Backoffice Internal usage only Latest chrome (CSS Grids, no polyfills)

    Operating since 2016 Universal for the whole company
  10. 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.
  11. 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
  12. 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
  13. 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
  14. 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);
  15. 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;
  16. 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
  17. 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
  18. 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"
  19. 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.
  20. <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>
  21. 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); }
  22. 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; }
  23. async destroyMicrofrontend(name) { try { const microfrontend =await this.$mf.getMicrofrontendByName(name); microfrontend

    && microfrontend.destroy(); } finally { this.isReady = false; this.currentInstance = null; } }
  24. 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: [] } ); }
  25. 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); }
  26. 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;
  27. 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.
  28. const router = new Router({ mode: 'history', routes: [ {

    path: '/login', name: 'Login', component: Login }, { path: '/players/search', components: { default: PlayerSearch, leftSideBar: Navigation } } // … was
  29. 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
  30. export const createRouter = base => { return new Router({

    mode: 'history', routes: [ { path: `${base}players/search`, name: 'PlayerSearch', component: PlayerSearch }, // ...
  31. { 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` } } }, // ...
  32. 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 };
  33. 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`
  34. 76

  35. 80

  36. 81

  37. 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
  38. Next steps 1. Continuous deployment instead of continuous delivery 2.

    Automated rollbacks based on newrelic data 3. Performance testing of single MFE
  39. 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.