Skip to content

Commit 7a2eda6

Browse files
committed
feature #482 [Vue] Add support for lazy-loading with Async Components (Kocal)
This PR was squashed before being merged into the 2.x branch. Discussion ---------- [Vue] Add support for lazy-loading with Async Components | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes <!-- please update src/**/CHANGELOG.md files --> | Tickets | Fix #... <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead --> | License | MIT I was a bit surprised when trying the Symfony UX' Vue integration, to see all Vue components/controllers to be loaded to render at least only one controllers. This PR aims to reduce the bundle size and only load the required code and Vue controllers to render on the page. To enable lazy-loading, add the 4th parameter `"lazy"` to `require.context()`: ```diff - registerVueControllerComponents(require.context('./vue/controllers', true, /\.vue$/)); + registerVueControllerComponents(require.context('./vue/controllers', true, /\.vue$/, "lazy")); ``` _Note: the sync-loading still works_ # _Real life_ example Given 6 really simple Vue controllers which all look the same: <img width="688" alt="image" src="https://user-images.githubusercontent.com/2103975/192136360-556dcee8-7eb7-423d-976f-7b000bf516a7.png"> If I render only two of them on a single page: <img width="524" alt="image" src="https://user-images.githubusercontent.com/2103975/192136376-05dd75f7-b917-4732-a1dd-49a0081ecf62.png"> ## Before <img width="1033" alt="Capture d’écran 2022-09-25 à 11 03 16" src="https://user-images.githubusercontent.com/2103975/192136387-4d415c40-6960-4e46-8bc4-7458730d5301.png"> You can see the `app.js` is about 4.8 kB, it contains all of the 6 Vue controllers. ## After <img width="1033" alt="Capture d’écran 2022-09-25 à 11 03 38" src="https://user-images.githubusercontent.com/2103975/192136390-df1274ac-dd57-45a3-a7a7-86f2659067ba.png"> You can see the `app.js` is now about 3.4 kB, and two more files have been lazy-loaded (our two Vue controllers rendered on the page). On a real project, with a lot of Vue controllers, the difference would be very significative. I will try to post some results when migrating our main project at work. --- Note: ~I still have to add tests and update the documentation.~ done Commits ------- 8fa08d0 [Vue] Add support for lazy-loading with Async Components
2 parents 47be741 + 8fa08d0 commit 7a2eda6

File tree

6 files changed

+88
-23
lines changed

6 files changed

+88
-23
lines changed

src/Vue/Resources/assets/dist/register_controller.js

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,39 @@
1+
import { defineAsyncComponent } from 'vue';
2+
13
function registerVueControllerComponents(context) {
2-
const vueControllers = {};
3-
const importAllVueComponents = (r) => {
4-
r.keys().forEach((key) => (vueControllers[key] = r(key).default));
5-
};
6-
importAllVueComponents(context);
7-
window.resolveVueComponent = (name) => {
8-
const component = vueControllers[`./${name}.vue`];
9-
if (typeof component === 'undefined') {
10-
throw new Error(`Vue controller "${name}" does not exist`);
4+
const vueControllers = context.keys().reduce((acc, key) => {
5+
acc[key] = undefined;
6+
return acc;
7+
}, {});
8+
function loadComponent(name) {
9+
const componentPath = `./${name}.vue`;
10+
if (componentPath in vueControllers && typeof vueControllers[componentPath] === 'undefined') {
11+
const module = context(componentPath);
12+
if (module.default) {
13+
vueControllers[componentPath] = module.default;
14+
}
15+
else if (module instanceof Promise) {
16+
vueControllers[componentPath] = defineAsyncComponent(() => new Promise((resolve, reject) => {
17+
module
18+
.then((resolvedModule) => {
19+
if (resolvedModule.default) {
20+
resolve(resolvedModule.default);
21+
}
22+
else {
23+
reject(new Error(`Cannot find default export in async Vue controller "${name}".`));
24+
}
25+
})
26+
.catch(reject);
27+
}));
28+
}
29+
else {
30+
throw new Error(`Vue controller "${name}" does not exist.`);
31+
}
1132
}
12-
return component;
33+
return vueControllers[componentPath];
34+
}
35+
window.resolveVueComponent = (name) => {
36+
return loadComponent(name);
1337
};
1438
}
1539

src/Vue/Resources/assets/src/register_controller.ts

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
'use strict';
1111

1212
import type { Component } from 'vue';
13+
import { defineAsyncComponent } from 'vue';
1314

1415
declare global {
1516
function resolveVueComponent(name: string): Component;
@@ -20,21 +21,45 @@ declare global {
2021
}
2122

2223
export function registerVueControllerComponents(context: __WebpackModuleApi.RequireContext) {
23-
const vueControllers: { [key: string]: object } = {};
24+
const vueControllers = context.keys().reduce((acc, key) => {
25+
acc[key] = undefined;
26+
return acc;
27+
}, {} as Record<string, object | undefined>);
2428

25-
const importAllVueComponents = (r: __WebpackModuleApi.RequireContext) => {
26-
r.keys().forEach((key) => (vueControllers[key] = r(key).default));
27-
};
29+
function loadComponent(name: string): object | never {
30+
const componentPath = `./${name}.vue`;
31+
32+
if (componentPath in vueControllers && typeof vueControllers[componentPath] === 'undefined') {
33+
const module = context(componentPath);
34+
if (module.default) {
35+
vueControllers[componentPath] = module.default;
36+
} else if (module instanceof Promise) {
37+
vueControllers[componentPath] = defineAsyncComponent(
38+
() =>
39+
new Promise((resolve, reject) => {
40+
module
41+
.then((resolvedModule) => {
42+
if (resolvedModule.default) {
43+
resolve(resolvedModule.default);
44+
} else {
45+
reject(
46+
new Error(`Cannot find default export in async Vue controller "${name}".`)
47+
);
48+
}
49+
})
50+
.catch(reject);
51+
})
52+
);
53+
} else {
54+
throw new Error(`Vue controller "${name}" does not exist.`);
55+
}
56+
}
2857

29-
importAllVueComponents(context);
58+
return vueControllers[componentPath] as object;
59+
}
3060

3161
// Expose a global Vue loader to allow rendering from the Stimulus controller
3262
window.resolveVueComponent = (name: string): object => {
33-
const component = vueControllers[`./${name}.vue`];
34-
if (typeof component === 'undefined') {
35-
throw new Error(`Vue controller "${name}" does not exist`);
36-
}
37-
38-
return component;
63+
return loadComponent(name);
3964
};
4065
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
<h1>Goodbye {{ name }}</h1>
3+
</template>

src/Vue/Resources/assets/test/register_controller.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,24 @@
1212
import {registerVueControllerComponents} from '../src/register_controller';
1313
import {createRequireContextPolyfill} from './util/require_context_poylfill';
1414
import Hello from './fixtures/Hello.vue'
15+
import Goodbye from './fixtures-lazy/Goodbye.vue'
1516

1617
require.context = createRequireContextPolyfill(__dirname);
1718

1819
describe('registerVueControllerComponents', () => {
19-
it('test', () => {
20+
it('test should resolve components synchronously', () => {
2021
registerVueControllerComponents(require.context('./fixtures', true, /\.vue$/));
2122
const resolveComponent = window.resolveVueComponent;
2223

2324
expect(resolveComponent).not.toBeUndefined();
2425
expect(resolveComponent('Hello')).toBe(Hello);
2526
});
27+
28+
it('test should resolve lazy components asynchronously', () => {
29+
registerVueControllerComponents(require.context('./fixtures-lazy', true, /\.vue$/, 'lazy'));
30+
const resolveComponent = window.resolveVueComponent;
31+
32+
expect(resolveComponent).not.toBeUndefined();
33+
expect(resolveComponent('Goodbye')).toBe(Goodbye);
34+
});
2635
});

src/Vue/Resources/doc/index.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ You also need to add the following lines at the end to your ``assets/app.js`` fi
4747
// they are not necessary.
4848
registerVueControllerComponents(require.context('./vue/controllers', true, /\.vue$/));
4949
50+
// If you prefer to lazy-load your Vue.js controller components, in order to reduce to keep the JavaScript bundle the smallest as possible,
51+
// and improve performances, you can use the following line instead:
52+
//registerVueControllerComponents(require.context('./vue/controllers', true, /\.vue$/, 'lazy'));
53+
5054
5155
Usage
5256
-----

ux.symfony.com/assets/app.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ import Tab from 'bootstrap/js/dist/tab';
1414

1515
// initialize symfony/ux-react
1616
registerReactControllerComponents(require.context('./react/controllers', true, /\.(j|t)sx?$/));
17-
registerVueControllerComponents(require.context('./vue/controllers', true, /\.vue?$/));
17+
registerVueControllerComponents(require.context('./vue/controllers', true, /\.vue?$/, 'lazy'));

0 commit comments

Comments
 (0)