Vue in a legacy application
December 2018 - Stéphane Reiss
How we did it at Alayacare
September 2020
Who am I?
- Web developer
- Backend background
- I love javascript
Stéphane Reiss
AlayaCare Cloud
- 332 tenants across 3 regions (CAN, USA, AUS)
- 86k active employees
- 321K active patients
Legacy App - 2015
- Monolith PHP app
- 1 big database schema
- jQuery everywhere
- No tests
- No documentation
<head>
<script src="/scripts/files/jquery.js"></script>
<script type="text/javascript" src="..."></script>
// ... more files ...
<script type="text/javascript" src="..."></script>
</head>
<body>
<div class="body_1">
<div id="top">
// ... more elements ...
<div id="content" class="content-center-resize relative">
<div class="primary-content"></div>
<div class="secondary-content"></div>
</div>
// ... more elements ...
</div>
</div>
</body>
$(window).bind('hashchange', function(e) {
var url = $.param.fragment();
if (url == '') {
return
}
Dash.loadContent(url);
})
ROUTING
PAGE LAYOUT
Game plan
- Micro service architecture for the backend
- Frontend needs modularity, maintainability
- Two way data-binding
REQUIREJS + LESS + BOWER
First try - 2015
<head>
//...
<?php if (ENVIRONMENT == 'prod'): ?>
<?= Ui::getVersionedCSSTag('/web/css/main_old.css'); ?>
<?= Ui::getVersionedScriptTag('/web/js/built/old_app.js') ?>
<?php else: ?>
<link rel="stylesheet/less" type="text/css" href="/web/less/old_layout/main.less" />
<script type="text/javascript">var less = less || {}; less.env='development';</script>
<script src="/web/js/lib/vendor/less.min.js"></script>
<script type="text/javascript" src="/web/js/lib/vendor/require.js"></script>
<script type="text/javascript">
require(['/web/js/config.js'], function() {
require(['old_app']);
});
</script>
<?php endif; ?>
//...
</head>
define(['t3', 'vue'], (Box, Vue) => Box.Application.addModule('calendar-legend', () => {
const el = 'calendar-legend';
const props = ['statusList']; // [ { name }, { name }, ... ]
const template = '#calendar-legend-template';
return {
init() {
this.vue = new Vue({
el,
props,
template,
});
},
destroy() {
this.vue.$destroy();
},
};
}));
<template id="calendar-legend-template">
<div class="calendar-legend-container">
<ul id="calendar-legend" class="calendar-legend">
<li v-for="status in statusList"
:class="status.cssClass">
{{ status.ui_name }}
</li>
</ul>
</div>
</template>
<div data-module="calendar-legend">
<calendar-legend :status-list="<?= //... ?>"></calendar-legend>
</div>
PHP
T3 + VUEJS in a require module
Our first vue component - 2016
Let's start over! - 2016
<body>
<div class="body_1">
<div id="top">
// ... more elements ...
<div id="content" class="content-center-resize relative">
<div class="primary-content"></div>
<div class="secondary-content"></div>
</div>
// ... more elements ...
</div>
</div>
</body>
PAGE LAYOUT
<script type="text/javascript" src="/web/js/built/build.js"></script>
with webpack and Vue.js
module.exports = {
entry: {
build: './web/js/main.js',
css: './web/less/main.less',
old_css: './web/less/old_layout/main.less',
},
output: {
path: path.resolve(__dirname, './web/js/built'),
publicPath: '/web/js/built/',
filename: '[name].js',
hotUpdateChunkFilename: 'hot/hot-update.js',
hotUpdateMainFilename: 'hot/hot-update.json',
},
//...
};
webpack.config.js
import Vue from 'vue';
import tagsDefined from './components-export';
let components = [];
observeDOMChanges();
scanNodeForVueComponent(document);
function observeDOMChanges() {...}
function scanNodeForVueComponent(node) {...}
function createVueComponent(node) {...}
function destroyVueComponent(node) {...}
function destroyChildren(vueInstance) {..}
main.js
new Vue({
el: node,
components: {
[tagName]: tagsDefined[tagName],
},
});
vueInstance.$destroy();
export default {'component-a': require('./<path>/component-a'), ... 'component-x': require('./<path>/component-x'), }
<div class="widget">
<calendar-legend
:status-list="<?= //... ?>"
/>
</div>
Backend response
- Detect DOM Change
- Scan for components
- Create Vue components
- Destroy removed components
Vue dev tools
main app
Vue dev tools
Vuex
Allow cross components communication through a centralized state
import store from 'components/vuex/store';
//...
new Vue({
el: node,
store,
components: {
[tagName]: tagsDefined[tagName],
},
});
//...
main.js
Vue router
Vue Router is the official router for Vue.js
$(window).bind('hashchange', function(e) {
var url = $.param.fragment();
if (url == '') {
return
}
Dash.loadContent(url);
})
ROUTING
import store from 'components/vuex/store';
import router from 'components/router';
//...
new Vue({
el: node,
store,
router,
components: {
[tagName]: tagsDefined[tagName],
},
});
//...
main.js
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
const router = new VueRouter({
routes: [
{
path: '*',
component: LegacySection,
}
],
});
export default router;
router.js
<div class="primary-content">
<app>
<router-view></router-view>
</app>
</div>
PAGE LAYOUT
//...
{
path: '/settings/scheduling',
redirect: '/settings/scheduling/offer-responses',
component: VerticalTabs,
props: {
header: 'Scheduling Settings',
headerIcon: 'fa fa-cog',
mode: 'vertical',
showBreadcrumb: true,
},
children: [
{
name: 'recurrence-template',
path: 'recurrence-template',
component: LegacySection,
props: {
legacyUrl: '/scheduling/recurrencetemplate/list',
},
meta: {
label: i18n.t('Recurrence Template'),
},
},
{
name: 'entity-tags',
path: 'entity-tags',
component: LegacySection,
props: {
legacyUrl: '/scheduling/tag/list',
},
meta: {
label: i18n.t('Entity Tags'),
},
}
],
},
//...
More complex routes
router.js
//...
{
name: ACCOUNTING_BILLING_ROUTES_NAME.MEDICAID_FUNDER_SUMMARY,
path: 'medicaid/funders',
component: { template: '<router-view/>' },
children: ACCOUNTING_BILLING_MEDICAID_ROUTES_DEFINITION,
redirect: {
name: ACCOUNTING_BILLING_MEDICAID_ROUTES_NAME.INDEX,
},
meta: {
label: i18n.t('Medicaid Summary'),
acls: [ACL_ACCOUNTING_DASHBOARD],
features(store) {
return (
store.getters.hasFeature(FEATURES.ACCOUNTING) &&
store.getters.hasFeature(FEATURES.ELECTRONIC_BILLING_MEDICAID) &&
!store.getters.hasFeature(FEATURES.ELECTRONIC_BILLING_V1_HIDE)
);
},
},
}
//...
Even more complex routes
<...>/routes/accounting/billing/routes.definition.js
Things I wish I knew
- Don't use Vue inside T3 inside RequireJs
- Build mini app by detecting DOM mutation
- Define core components
- Use vuex for cross component communication
- Test your components!
- <component>.vue
- <component>.test.vue
- JEST + Vue Test Utils (https://jestjs.io - https://vue-test-utils.vuejs.org)
- Refactor your old components as you improve your code base
- To not end up with 5 differents patterns after 2 years
- https://github.com/chrisvfritz/vue-enterprise-boilerplate
Things I wish I knew - 2 years later!
- Give proper ownership to your core components
- Ours grew in an unmaintainable way
- We started their refactorDesign System
- Renderless components!
- Think about the vuex store architecture and keep an eye on it
- The "component-export" pattern could have been replaced by vue-router and the legacy section faster
- We started to remove it only now, as part of the performance improvements
- Don't include components in your store, it makes the tests harder
- We even have some circular dependencies
Thank you!
VueJs in legacy app - 2 years later
By Stéphane Reiss
VueJs in legacy app - 2 years later
See original talk: https://slides.com/sreiss/vuejs-in-legacy-app/fullscreen
- 406