Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Real-World Angular Series, Part 8a: Lazy Loading, Production Deployment, SSL

DZone's Guide to

Real-World Angular Series, Part 8a: Lazy Loading, Production Deployment, SSL

Sadly, the end is in sight. We begin the last part of this series by taking a look at lazy loading, and the requirements to push our app to production deploy.

· Web Dev Zone
Free Resource

Never build auth again! Okta makes it simple to implement authentication, authorization, MFA and more in minutes. Try the free developer API today! 

The seventh part of this tutorial (Part 7a and Part 7b) covered deleting events, retrieving relational data from MongoDB to list events a user has RSVPed to, and silently renewing authentication tokens.

The eighth and final installment in the series covers NgModule refactoring, lazy loading, and production deployment on VPS with NGINX and SSL.

Angular: Refactor NgModules

Let's pick up right where we left off last time. Angular uses NgModules to organize an application into cohesive blocks of functionality. We currently have just one NgModule: our root module, called AppModule. To improve our app's organization and enable lazy loading, we're going to do some refactoring by adding additional NgModules.

Let's start the NgModules refactor by creating modules for our src/app/authand src/app/core directories. This will start to help clean up our app.module.ts file.

Create Auth Module

Create a module with the Angular CLI like so:

$ ng g module auth

This command generates an auth.module.ts file in our existing auth folder. Let's open this file and make the following updates:

// src/app/auth/auth.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AuthService } from './auth.service';

@NgModule({
  imports: [
    CommonModule
  ],
  declarations: [],
  providers: [
    AuthService
  ]
})
export class AuthModule { }

This module now makes the AuthService provider available to our application when imported.

Create Core Module

Now we'll create a core module the same way:

$ ng g module core

Open the new core.module.ts file and add:

// src/app/core/core.module.ts
import { NgModule } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { DatePipe } from '@angular/common';
import { ApiService } from './api.service';
import { UtilsService } from './utils.service';
import { FilterSortService } from './filter-sort.service';
import { SubmittingComponent } from './forms/submitting.component';
import { LoadingComponent } from './loading.component';
import { HeaderComponent } from './../header/header.component';
import { FooterComponent } from './../footer/footer.component';

@NgModule({
  imports: [
    CommonModule,
    HttpClientModule,
    RouterModule,
    FormsModule,
    ReactiveFormsModule
  ],
  declarations: [
    HeaderComponent,
    FooterComponent,
    LoadingComponent,
    SubmittingComponent
  ],
  providers: [
    Title,
    DatePipe,
    ApiService,
    UtilsService,
    FilterSortService
  ],
  exports: [
    HttpClientModule,
    RouterModule,
    FormsModule,
    ReactiveFormsModule,
    HeaderComponent,
    FooterComponent,
    LoadingComponent,
    SubmittingComponent
  ]
})
export class CoreModule { }

We'll move HTTP and forms modules to our core module, as well as the title service and necessary features in the src/app/core directory and our header and footer. We'll import the appropriate modules, services, and components. We also need to addRouterModule in order to support the Header component's navigation directives. We'll add the modules to the NgModule's imports array, the components to the declarations array, and the services to the providersarray. Then we'll also need to export the modules and components if we want to be able to use them in the components of other NgModules.

Note: We are not moving the BrowserAnimationsModule. This is because this module is in @angular/platform-browser. Because BrowserModule needs to be imported in the app module, all ancillary modules from @angular/platform-browser need to be imported in the same module. Otherwise, we will get a BrowserModule has already been loaded error when we implement lazy loading.

Our core module is fairly substantial but collects the shared features of our application in one place. They're no longer mixed in with all of our app's other components.

Update App Module

We need to add our new modules to our app module, as well as clean up the imports we moved. Open the app.module.ts file and modify it like so:

// src/app/app.module.ts
// @TODO: remove auth and core imports/declarations
...
import { AuthModule } from './auth/auth.module';
import { CoreModule } from './core/core.module';
...
@NgModule({
  ...
  imports: [
    ...
    AuthModule,
    CoreModule
  ],
  ...

We'll import the two new modules we created at the top of the file, and also add them to the imports array in the @NgModule(). We also need to clean up the app.module.ts file so we don't have duplicates of anything; this will cause compiler errors. Make sure all the imports and references to moved features have been removed.

Note: Your IDE's intellisense and any Angular compiler errors in the browser should help you clean this up fairly easily.

You'll notice that most of the remaining imports in our app module (with a few exceptions) are now page components. The file looks much cleaner and more manageable. Now we will create a few more modules to manage features.

Create Event Module

Our event component has several child components, including event details, RSVPs, and the RSVP form. Let's create a module for this page as a whole.

$ ng g module pages/event

Open the event.module.ts file and add:

// src/app/pages/event/event.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CoreModule } from './../../core/core.module';
import { EventComponent } from './event.component';
import { EventDetailComponent } from './event-detail/event-detail.component';
import { RsvpComponent } from './rsvp/rsvp.component';
import { RsvpFormComponent } from './rsvp/rsvp-form/rsvp-form.component';

@NgModule({
  imports: [
    CommonModule,
    CoreModule
  ],
  declarations: [
    EventComponent,
    EventDetailComponent,
    RsvpComponent,
    RsvpFormComponent
  ]
})
export class EventModule { }

We'll need to import the CoreModule we created earlier in order for our event components to access its exports. We'll then import all of our event components and add them to the declarations array.

Create Admin Module

Now we'll do the same with our Admin component and its children.

Create the new module:

$ ng g module pages/admin

Open the admin.module.ts file and add the following:

// src/app/pages/admin/admin.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CoreModule } from './../../core/core.module';
import { AdminComponent } from './admin.component';
import { CreateEventComponent } from './create-event/create-event.component';
import { UpdateEventComponent } from './update-event/update-event.component';
import { EventFormComponent } from './event-form/event-form.component';
import { DeleteEventComponent } from './update-event/delete-event/delete-event.component';

@NgModule({
  imports: [
    CommonModule,
    CoreModule
  ],
  declarations: [
    AdminComponent,
    CreateEventComponent,
    UpdateEventComponent,
    EventFormComponent,
    DeleteEventComponent
  ]
})
export class AdminModule { }

Like in our event module, we'll import CoreModule and all the components associated with our admin page.

Update App Module

Now it's time to update app.module.ts again. We'll delete all the imports and references that we moved. Then we'll import the two new modules we created:

// src/app/app.module.ts
// @TODO: remove event and admin component imports/declarations
...
import { EventModule } from './pages/event/event.module';
import { AdminModule } from './pages/admin/admin.module';
...
@NgModule({
  ...
  imports: [
    ...
    EventModule,
    AdminModule
  ],
  ...

Note: After we verify that refactored modules work, we'll be removing these imports in favor of lazy loading them instead.

Our app should continue to function the way it always did, but we have a cleaner architecture in place. We're now ready to implement lazy loading for our event and admin routes.

Angular: Lazy Loading

Currently, all of our routes are eagerly loaded. This means all routes are compiled in the same bundle, which loads on initialization of our single page app (SPA) in the browser. It's often a better approach to only load routes when they're needed (lazy loading). With this approach, bundle chunks are compiled as separate JavaScript files. Each chunk is only loaded when the user navigates to the route that needs that code.

Lazy loading in Angular relies on modules. In addition to gaining improved architecture, we also get the ability to easily add lazy loading as a result of creating an EventModule and AdminModule. Instead of loading components with our routes, we'll use loadChildren with a string pointing to the proper NgModule.

Note: You could lazy load all routes in your application by adding NgModules for each route. We'll only lazy load the event and admin routes in this tutorial, but feel free to add more lazy loading in your apps.

Let's implement lazy loading!

Lazy Load Event Route

We'll lazy load our event module first. The event feature is a substantial amount of code and functionality, so we'll load it on demand. We can implement lazy loading in three simple steps.

The first thing we need to do is move routing for our event component to the EventModule. We'll create a new file that exports an EVENT_ROUTES constant. Make a new file in src/app/pages/event called event.routes.ts:

// src/app/pages/event/event.routes.ts
import { Routes } from '@angular/router';
import { EventComponent } from './event.component';

export const EVENT_ROUTES: Routes = [
  {
    path: '',
    component: EventComponent
  }
];

This is a child route of our existing 'event/:id' route, hence the empty path(it inherits its route from the 'event/:id' parent). The parent 'event/:id' in our app-routing.module.ts will become componentless and will reference the event NgModule instead of the EventComponent.

Let's import the EVENT_ROUTES constant into our event.module.ts and implement it:

// src/app/pages/event/event.module.ts
...
import { RouterModule } from '@angular/router';
import { EVENT_ROUTES } from './event.routes';

@NgModule({
  imports: [
    ...,
    RouterModule.forChild(EVENT_ROUTES)
  ],
  ...
})
...

We'll import the RouterModule and the EVENT_ROUTES constant. Then we'll import RouterModule and set forChild(), passing the event routes we just created.

Now open the app-routing.module.ts:

// src/app/app-routing.module.ts
// @TODO: remove EventComponent import
...

const routes: Routes = [
  ...,
  {
    path: 'event/:id',
    loadChildren: './pages/event/event.module#EventModule',
    canActivate: [
      AuthGuard
    ]
  },
  ...
];

...

First, we'll remove the EventComponent import. Then we'll replace the component: EventComponent in our 'event/:id' route with the following:

loadChildren: './pages/event/event.module#EventModule',

This is a string referencing the path to our event.module.ts and the module's name, EventModule. The code will then look in the event module for routes and load them on demand.

Lazy Load Admin Routes

Now we'll lazy load the admin routes. This is a set of routes rather than a single one, unlike our event route. Lazy loading the admin routes together is useful because any user who logs in who is not an admin will never need to download the admin components. However, if the user is an admin, we can load all the admin routes together the first time they access an admin page. The implementation is the same as above, so let's begin.

Create a new file that exports an ADMIN_ROUTES constant. Make a new file in src/app/pages/admin called admin.routes.ts:

// src/app/pages/admin/admin.routes.ts
import { Routes } from '@angular/router';
import { AdminComponent } from './admin.component';
import { CreateEventComponent } from './create-event/create-event.component';
import { UpdateEventComponent } from './update-event/update-event.component';

export const ADMIN_ROUTES: Routes = [
  {
    path: '',
    component: AdminComponent,
  },
  {
    path: 'event/new',
    component: CreateEventComponent
  },
  {
    path: 'event/update/:id',
    component: UpdateEventComponent
  }
];

This is the same as the children array from the app-routing.module.ts.

Now implement these routes in the admin.module.ts:

// src/app/pages/admin/admin.module.ts
...
import { RouterModule } from '@angular/router';
import { ADMIN_ROUTES } from './admin.routes';

@NgModule({
  imports: [
    ...,
    RouterModule.forChild(ADMIN_ROUTES)
  ],
  ...
})
...

Lastly, update the app-routing.module.ts:

// src/app/app-routing.module.ts
// @TODO: remove all admin route component imports
...

const routes: Routes = [
  ...,
  {
    path: 'admin',
    loadChildren: './pages/admin/admin.module#AdminModule',
    canActivate: [
      AuthGuard,
      AdminGuard
    ]
  },
  ...
];

...

Remove the admin component imports (AdminComponent, CreateEventComponent, and UpdateEventComponent). Then replace the children array with loadChildren instead.

Remove Event and Admin Modules From App Module

Now that our event and admin modules are set up to be loaded lazily, we no longer need to import them in our root AppModule. Instead, they will be loaded on demand.

Open the app.module.ts file and delete the import statements for EventModule and AdminModule. In addition, delete these modules from the NgModule's imports array.

Production Build to Verify Lazy Loading

Lazy loading is now implemented! Visit the app in the browser. Everything should continue to work as expected. Verify that you can access all the routes without errors.

We can see that everything works, but in order to really see our lazy loading in action, we need to create a production build. This will use Ahead-of-Time (AOT) compilation instead of Just-in-Time (JIT). It will split our bundles into separate chunks the way we expect for lazy loading.

Stop both the Angular CLI server and the Node server. Then run:

$ ng build --prod
$ node server

The ng build --prod command will create a /dist folder containing our compiled production Angular app. Running node server without the NODE_ENV=dev environment variable will serve our application from Node, running the API while also serving the Angular front end from the production /dist folder.

Note: AOT is the default compilation for production builds in the Angular CLI as of v1.0.0-beta.28.

We can now access our production build at http://localhost:8083. The app should load and run as expected.

Note: Silent token renewal will produce errors on a production build if you don't update the redirect URLs in the silent.html file. However, this is actually the perfect opportunity to test token renewal error handling. You may want to decrease the token expiration again in your Auth0 API to trigger a renewal attempt that will fail due to improper configuration. Then you can ensure that your app handles silent authentication errors gracefully and as expected.

Once you have the app running on http://localhost:8083, open the browser developer tools to the Network tab. Then navigate through the app, keeping an eye on which resources are loaded.

Note: You can change the Network output to JS instead of All to make it easier to see the bundles that are being loaded.

On initial load of an eagerly loaded route such as the homepage, the network panel should look something like this:

Network panel lazy loading eager route

On first load, there are several JS files here. Notice there is a main...bundle.jsfile. This is the JS for our app module, associated code, and eagerly loaded routes. If we hadn't implemented lazy loading, there wouldn't be any additional bundle loads appearing as long as we continue using our single page app in this session.

However, if you navigate from the homepage to a lazy loaded route such as an event details page, you should see a ...chunk.js file loaded, like so:

Angular lazy loaded route network

This is JavaScript loaded on demand for our lazy loaded route. Click through the app while paying attention to the network panel to verify that lazy loading is functioning properly. The first admin route visited should load one more chunk that covers all admin pages. If the user isn't an admin, that code will never burden the network by loading needlessly.

If everything is working as we expect, it's time to get ready to deploy our application!

Intro to Deploying a MEAN App

We now have a production build of our RSVP application! The obvious next step is to deploy it. Let's take a look at the requirements for production deployment.

VPS Hosting

We can't use shared hosting for our application. We're going to be running a Node server, so a VPS (Virtual Private Server) is our best bet. Fortunately, there are robust and affordable options.

In this tutorial, we will deploy our app to a DigitalOcean VPS. We'll talk about this in more detail shortly.

Note: Heroku is another option, but requires that things be done a specific way. Because of this, if Heroku is your intended production platform, it's strongly recommended that you follow a Heroku-specific MEAN tutorial instead.

MongoDB

You can also consider upgrading your mLab account where the MongoDB is hosted. If the free Sandbox plan will not be sufficient for your production app, you can upgrade to a larger plan. If you're only looking to develop and sandbox, the current free plan should be fine.

Alternatively, you could host your MongoDB locally on your VPS. If it grows large, it will take up space on the server but will be located in the same place as your app. You can find instructions on how to set up MongoDB on DigitalOcean here.

Domain Name

You'll need a domain name to associate with your VPS so that you can access your app in the browser and secure it with SSL. If you need to register a domain name, you can do so through any number of registrars. Personally, I like to use namecheap.

Deployment Summary

Let's do a quick summary of the steps we need to take in order to deploy our app to production:

  • Spin up an Ubuntu droplet on DigitalOcean and set up the server.
  • Install Node.
  • Prep our app for VPS deployment via Git.
  • Clone and set up our project on VPS.
  • Keep app running with pm2.
  • Set up a domain name with VPS (either use one you already own or register a new one).
  • Get an SSL certificate with Let's Encrypt.
  • Install NGINX and configure with domain and SSL.
  • Update Auth0 Client settings for production.

There are a quite a few steps here and we'll be relying on a few tutorials from DigitalOcean's community to get going.

We'll dive into his in Part 8b tomorrow! 

Launch your application faster with Okta’s user management API. Register today for the free forever developer edition!

Topics:
web dev ,angular ,web application development ,lazy loading

Published at DZone with permission of Kim Maida, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}