Building large scale Angular applications
- or -
The curious incident of the my-account evolution
Ofir Dagan
*(based on a best seller slides by Shahar Talmi)
Who's This Guy?
- Ofir Dagan
- My Account TL (soon to be crm mobile TL)
- Doggie Sitter @ Wix
Let's Talk About Huge Applications
By Type
-
app/
- controllers/
- services/
- directives/
By Feature
-
app/
- dashboard/
- contacts/
- activities/
- market/
This is not what this presentation is about!
What About Them?
- Hard to maintain without making spaghetti
- Multiple teams working on same project
- One big deployable => risky deployments
- One big build => tests run forever
- Everything needs to load on page load
- Inferior overall performance
- I don't like them
So what did we do?
AnCient History
- We had iframes, one per every application section
- The hosting page needed to:
- Resize/move the iframe
- Show modals (and put iframes inside the modals)
- Communicate deep links over iframe address
- No, iframes are no fun at all
The Big Rewrite
The Big Rewrite
- Okay, this time without iframes
- Except for third-party apps, for security reasons
- We created many single page applications
- Avoid single page app that contains multiple apps
- BUT: Full page refresh when navigating between apps
- Deal with it later!
Widgets
Surprisingly, we don't like iframes.
HOW WE SEE WIDGETS
We want each widget to be a deployable app with it's own directives, services, controllers... But we also want it to be isolated
And we want it to load lazily since widgets might be removed or added during run-time in the future
A SHORT ANGULAR STORY
We usually tell angular what module we want to manage our page with the ng-app attribute
When the page loads, angular looks for ng-app, creates a new injector (which holds all the injectables), loads that module and all its dependencies, and $compiles the DOM element
But if you load scripts asynchronously and want to wait with all this until you are ready, you leave out ng-app and instead invoke angular.bootstrap(document, ['myApp'])
Title Text
MIND BLOWN
Actually, you can angular.bootstrap many times, each time for a different element and module
Each run will create a different injector with different instances of all injectables, completely isolated from the rest of the apps in the page
We have a pretty simple directive which lazy loads the widget's js, css, html, etc. It then calls angular.boostrap with the widget's module name and html
widgetsProvider
.setManifestGenerator(function (name) {
return manifestLoader.loadWidgetManifest({
staticsBaseUrl: clientTopology.baseUrl,
manifestUrl: 'my-widget.manifest.json',
config: [runInConfigTime()]
});
});
<ng-widget src="'widget-name'"></ng-widget>
SIMPLIFIED CODE
{
"module": "myWidgetApp",
"html": "views/main.view.preload.html",
"scripts": [
{
"debug": [
"scripts/app.js",
"scripts/constants/pages.js",
],
"release": "scripts/scripts.js"
},
"styles": [
{
"debug": [
"styles/main.css"
],
"release": "styles/main.css"
}
],
"locale": [
{
"debug": [
"scripts/locale/messages_${locale}.js"
],
"release": "scripts/locale/messages_${locale}.js"
}
]
}
Manifest.json
SIMPLIFIED CODE
downloadWidget(
manifest.html, manifest.files)
.then(function (html) {
var element = angular.element(html),
angular.bootstrap(element,
[manifest.module]),
node.html('').append(element);
});
angular.bootstrap = function (element, modules) {
if (element.injector()) throw 'already bootstrapped!';
modules.unshift(functioan ($provide) {
$provide.value('$rootElement', element);
}
var injector = createInjector(modules);
injector.invoke(function ($rootScope, $rootElement,
$compile, $injector) {
$rootScope.$apply(function () {
$rootElement.data('$injector', $injector);
$compile($rootElement)($rootScope);
});
});
return injector;
}
So what does bootstrap do Again?
Not surprising But...
Applications are actually fullScreen widgets
$routeProvider.when('/app1/:eatall*?', {
template: '<ng-widget src="\'app1\'" />'});
$routeProvider.when('/app2/:eatall*?', {
template: '<ng-widget src="\'app2\'" />'});
$routeProvider.when('/app3/:eatall*?', {
template: '<ng-widget src="\'app3\'" />'});
All you need is to connect the widget to a router
Wait, What?
You set ng-route and manifest generator for "mother app"
You can do whatever you want in "micro app"
Including having an internal router! (ng-route or ui-router)
You can even have nested widgets inside your apps/widgets
Wanna see a demo?
Wanna see an impressive demo?
Sharing Is Caring
You can instruct widgets provider to share a service
widgetsProvider.addServiceToShare('service-name');
$location is a shared service by default
You can instruct widgets provider to propagate scope events
widgetsProvider.addEventToForward('event-name');
$locationChangeStart is a propagated scope event
Bottom line - Pros & cons
Pros:
- No Iframes :)
- Sharing services across injecotrs
- Minimizing bandwidth
- just plain cool
- css are shared (we use namespaces)
- commons libraries are shared
- Upgrade common library effects everyone
- widgets share global namespace
Cons:
My Account vision
One App to rule them all!
Check it out
https://github.com/wix/angular-widget
$ bower install angular-widget
Thanks!
* demo code is inside
** pull requests are welcome
angular-widget
By ofird
angular-widget
- 1,525