A few weeks ago we dealt with the question if it's possible to mix client-side Angular 2 components with static content rendered on the server-side.
The idea was to have different interactive Angular components like for example a chat or a search widget mixed together with static content rendered by the server. Therefore we came up with two different approaches. In this blog post we want to summarize our insights which we got from our rough proof-of-concept, so that people that are already a bit familiar with Angular can benefit from it. The problem is that Angular is template-based and you cannot mix it with server-side rendered html for example coming from a CMS like Wordpress. So we needed to find a solution to break out of the restrictions of the template to mix it with the static html content.
So our requirements for the proof-of-concept are:
We have static html content which is delivered by the server.
We have Angular components that should be able to communicate with each other, but be placed in different locations floating around the static content on the page.
First approach: Multiple Angular apps
Our first idea was to use multiple Angular apps on one page, which of course leads to the question, if it’s even possible to bootstrap them. Therefore we built a prototype based on the Angular Quickstart setup with SystemJS, a module loader tool. The code and a detailed guide (step-by-step) can be found on github. For the communication between those apps we built a shared service that can be injected in both apps. Theoretically this sounds like a good approach: With multiple apps you could nicely separate their development. In this case even different teams can provide their own apps. But in practice there are huge pitfalls – and that is the reason why we dismissed the idea and came up with a second different approach. The downsides were:
The two apps don’t share the same Angular context, this means for example you cannot use (inject) services/pipes from each other. So to establish a communication you have to create a shared service in the global namespace between these two apps.
Another downside is that Angular apps use many unique resources in the browser like cookies, title and location. This is also stated in the Angular code. Consequently this possibly results in conflicts if both apps manipulate the same resource.
The Angular version and versions of the dependencies used by the apps have to be identical. Because the dependencies are loaded in the same browser window context, different versions of the same dependency could cause conflicts. This limits an independent development.
Second approach: One Angular app
Because we wanted to address these major downsides, we moved over to just bootstrapping multiple components managed by one Angular app. Because these components stay together in the same app context it’s possible to use the standard capabilities of Angular like dependency injection or modularization. So the components neither require a shared communication service, nor do they have to share the unique resources of the Angular framework with other apps. Moreover the modules of this one app could be developed almost independently.
The html delivered by a server then only needs to contain specific tags that should be replaced by Angular components, as in the standard bootstrapping process of Angular – just with multiple components.
In order to bootstrap the Angular components we can use the regular NgModule bootstrap property, but we want to bootstrap only the components that really exist on the page. Considering that, we use the lifecycle-hook ngDoBootstrap to overwrite the default bootstrap behavior. With the selector of the component we query the DOM tocheck if the component should be displayed on the page (see line 4). If the selector matches we bootstrap the corresponding component (see line 5).
Conditional bootstrapping the components that exist on a page
A drawback is that it’s currently not possible to pass input values to the top-level components that should be boostraped, so external data needs either be provided by the window object or by using ElementRef. Like shown in the following snippet:
External data as input for the top-level component
This approach should be sufficient for the most use cases, but we wanted to try if it’s possible to lazy load some of the components to decrease the bundle size.
With the code above we deliver all components in one bundle. For smaller or fewer components the load times should be fine. But in case we want to deliver complex nested components, lazy loading of these parts becomes more and more relevant. Therefore we can improve this by delivering just the components that are really used on the page, so the initial load times are shorter.
We are using Angular CLI which uses Webpack under the hood. Webpack supports lazy loading of different modules. But for now Angular CLI does not provide an add-on functionality to customize the Webpack configuration. Therefore we use the Routes definition as a workaround to let Webpack generate the lazily loaded chunks (see line 1). This is just a pragmatic way for our proof-of-concept and could be easily done in an adequately configured Webpack build. As the bundling shifts common modules to a separate file (vendor.ts) which can be cached by the browser, only the first page load uses a little more bandwidth. The lazy components can then be fetched with a relatively small overhead.
In Angular the Router allows us to implement a lazy loading mechanism. But instead of only loading one module, we want to be able to load multiple modules at a time. Therefore we utilized the SystemJsNgModuleLoader which is used by the Router under the hood. We use the load method of this module in the ngDoBootstrap function of AppModule to load the modules on demand (see line 12). The SystemJsNgModuleLoader uses System.import (which can be handled by Webpack) to load and compile modules.
Lazy load respective module and bootstrap for each the specified component
// if the components selector is found on the page it is bootstrapped at this point
The ngDoBootstrap method in the code above first queries tags on the page (see lines 5-9) which hold a data-module-path attribute (like:
<app-lazy-widget data-module-path="./lazy/lazy.module#LazyModule"></app-lazy-widget>). Each specified module will then be lazily loaded and bootstrapped in parallel (see line 12). Important is that all components that should be bootstrapped are listed in the entryComponents of each lazy loaded module. Only then will Angular create a ComponentFactory and store it in the ComponentFactoryResolver, as shown in the code above (see lines 16-19). Finally the selector of the component is used to bootstrap it in the right place on the page (see lines 21-22).
One drawback of using lazy modules is that the providers of a lazy module are module-scoped and so are only visible in that module. That means for communication between lazy loaded modules we need services defined in the root module. This behaviour is also described in more detail in the FAQ: Lazy loaded module provider visibility
This proof-of-concept shows that it is possible to enrich server-side content with Angular components and if necessary even with lazy loaded modules. In this example we decided to focus on an Angular based way to achieve a solution to break out of the restrictions of its template to mix it with the static html content rendered on the server. It should be noted, however, that there might be other frameworks which can also do this.