|
| 1 | +# Introduction |
| 2 | + |
| 3 | +*This guide assumes that the [App Shell guide](https://mobile.angular.io/guides/app-shell.html) has been completed.* |
| 4 | + |
| 5 | +Now we're going to take a look at another part of the Angular Mobile Toolkit - the App Shell Runtime Parser. The Runtime Parser is a library which has the following features: |
| 6 | + |
| 7 | +- Automatically generates the App Shell of our application by using a predefined template. |
| 8 | +- Provides the App Shell template from the local cache automatically for each view that should use it. |
| 9 | + |
| 10 | +This way, when the user navigates to a different view in the application she will instantly get a minimal working UI that will be later replaced with the view's full functionality. You can find more about the Application Shell concept in the context of [Progressive Web Applications](https://developers.google.com/web/progressive-web-apps/) (PWA) on the [following link](https://developers.google.com/web/updates/2015/11/app-shell?hl=en). |
| 11 | + |
| 12 | +# Introduction to the Runtime Parser |
| 13 | + |
| 14 | +The Angular App Shell Runtime Parser works together with the App Shell directives, described in the [previous section](https://mobile.angular.io/guides/app-shell.html), and [Angular Universal](https://universal.angular.io). |
| 15 | + |
| 16 | +Now lets dig deeper in the features of the parser! |
| 17 | + |
| 18 | +First, lets suppose we have the following "Hello Mobile" component: |
| 19 | + |
| 20 | +```typescript |
| 21 | +import { Component } from '@angular/core'; |
| 22 | +import { APP_SHELL_DIRECTIVES } from '@angular/app-shell'; |
| 23 | +import { MdToolbar } from '@angular2-material/toolbar'; |
| 24 | +import { MdSpinner } from '@angular2-material/progress-circle'; |
| 25 | + |
| 26 | +@Component({ |
| 27 | + moduleId: module.id, |
| 28 | + selector: 'hello-mobile-app', |
| 29 | + template: ` |
| 30 | + <md-toolbar> |
| 31 | + <div class="icon ng"></div> |
| 32 | + {{title}} |
| 33 | + </md-toolbar> |
| 34 | + <md-spinner *shellRender></md-spinner> |
| 35 | + <h1 *shellNoRender>App is Fully Rendered</h1> |
| 36 | + `, |
| 37 | + styles: [` |
| 38 | + md-spinner { |
| 39 | + margin: 24px auto 0; |
| 40 | + } |
| 41 | + .icon { |
| 42 | + width: 40px; |
| 43 | + height: 40px; |
| 44 | + display: inline-block; |
| 45 | + } |
| 46 | + .icon.ng { |
| 47 | + background-image: url(./images/angular.png); |
| 48 | + } |
| 49 | + `], |
| 50 | + directives: [APP_SHELL_DIRECTIVES, MdToolbar, MdSpinner] |
| 51 | +}) |
| 52 | +export class HelloMobileAppComponent { |
| 53 | + title = 'Hello Mobile'; |
| 54 | +} |
| 55 | +``` |
| 56 | + |
| 57 | +...and in Universal, we bootstrap our application in the following way: |
| 58 | + |
| 59 | +```typescript |
| 60 | +import { provide } from '@angular/core'; |
| 61 | +import { APP_BASE_HREF } from '@angular/common'; |
| 62 | +import { APP_SHELL_RUNTIME_PROVIDERS } from '@angular/app-shell'; |
| 63 | +import { HelloMobileAppComponent } from './app/'; |
| 64 | +import { |
| 65 | + REQUEST_URL, |
| 66 | + ORIGIN_URL |
| 67 | +} from 'angular2-universal'; |
| 68 | + |
| 69 | +export const options = { |
| 70 | + directives: [ |
| 71 | + // The component that will become the main App Shell |
| 72 | + HelloMobileAppComponent |
| 73 | + ], |
| 74 | + platformProviders: [ |
| 75 | + APP_SHELL_RUNTIME_PROVIDERS, |
| 76 | + provide(ORIGIN_URL, { |
| 77 | + useValue: '' |
| 78 | + }) |
| 79 | + ], |
| 80 | + providers: [ |
| 81 | + // What URL should Angular be treating the app as if navigating |
| 82 | + provide(APP_BASE_HREF, {useValue: '/'}), |
| 83 | + provide(REQUEST_URL, {useValue: '/'}) |
| 84 | + ], |
| 85 | + async: false, |
| 86 | + preboot: false |
| 87 | +}; |
| 88 | +``` |
| 89 | + |
| 90 | +In the example above, we prerender the component using Universal and the `APP_SHELL_RUNTIME_PROVIDERS`. Once the user opens our prerendered application she will see the following user interface: |
| 91 | + |
| 92 | +<img src="img/prerendered-universal.png" width="300" alt="Prerendered page by Universal"> |
| 93 | + |
| 94 | +This is how the app will look once it has been completely rendered as well. However, for our App Shell we want to provide only the minimal UI which shows that the app is working and its initialization is in progress. One way to do this is by using the `APP_SHELL_BUILD_PROVIDERS` instead of `APP_SHELL_RUNTIME_PROVIDERS`. This way Universal will strip the content marked with the `shellNoRender` directive and output only the part of the application that is intended to be visualized as Application Shell. |
| 95 | + |
| 96 | +Unfortunately, this way we introduce the following problems: |
| 97 | + |
| 98 | +- We cannot reuse the given view as the App Shell for other routes in our application. |
| 99 | +- We must annotate the template of each individual route component which should be progressively loaded with `shellRender` and `shellNoRender` directives. |
| 100 | + |
| 101 | +On top of that, if the final App Shell has references to external images, users with slow Internet connection will not have the best experience possible. For instance, in the example above the Angular logo in the header is an external resource which needs to be fetched from the network. |
| 102 | + |
| 103 | +By using the Angular App Shell Runtime Parser in a Service Worker we can solve all of these issues! Lets see how! |
| 104 | + |
| 105 | +# Exploring the Runtime Parser |
| 106 | + |
| 107 | +First, lets take a look at a sample Service Worker which uses the Runtime Parser: |
| 108 | + |
| 109 | +```typescript |
| 110 | +importScripts( |
| 111 | + '/node_modules/reflect-metadata/Reflect.js', |
| 112 | + '/node_modules/systemjs/dist/system.js', |
| 113 | + '/system-config.js' |
| 114 | +); |
| 115 | + |
| 116 | +const context = <any>self; |
| 117 | + |
| 118 | +const SHELL_PARSER_CACHE_NAME = 'shell-cache'; |
| 119 | +const APP_SHELL_URL = '/home'; |
| 120 | +const INLINE_IMAGES: string[] = ['png', 'svg', 'jpg']; |
| 121 | +const ROUTE_DEFINITIONS = [ |
| 122 | + '/home', |
| 123 | + '/about/:id' |
| 124 | + ]; |
| 125 | + |
| 126 | +self.addEventListener('install', function (event: any) { |
| 127 | + const parser = System.import('@angular/app-shell') |
| 128 | + .then((m: any) => { |
| 129 | + context.ngShellParser = m.shellParserFactory({ |
| 130 | + APP_SHELL_URL, |
| 131 | + ROUTE_DEFINITIONS, |
| 132 | + SHELL_PARSER_CACHE_NAME, |
| 133 | + INLINE_IMAGES |
| 134 | + }); |
| 135 | + }) |
| 136 | + .then(() => context.ngShellParser.fetchDoc()) |
| 137 | + .then((res: any) => context.ngShellParser.parseDoc(res)) |
| 138 | + .then((strippedResponse: any) => { |
| 139 | + return context.caches.open(SHELL_PARSER_CACHE_NAME) |
| 140 | + .then((cache: any) => cache.put(APP_SHELL_URL, strippedResponse)); |
| 141 | + }) |
| 142 | + .catch((e: any) => console.error(e)); |
| 143 | + event.waitUntil(parser); |
| 144 | +}); |
| 145 | + |
| 146 | +self.addEventListener('fetch', function (event: any) { |
| 147 | + event.respondWith( |
| 148 | + context.ngShellParser.match(event.request) |
| 149 | + .then((response: any) => { |
| 150 | + if (response) return response; |
| 151 | + return context.caches.match(event.request) |
| 152 | + .then((response: any) => { |
| 153 | + if (response) return response; |
| 154 | + return context.fetch(event.request); |
| 155 | + }) |
| 156 | + }) |
| 157 | + ); |
| 158 | +}); |
| 159 | +``` |
| 160 | + |
| 161 | +Since the logic in the snippet above is not trivial, lets explore it step-by-step: |
| 162 | + |
| 163 | +## Step 1 - Configuration |
| 164 | + |
| 165 | +```typescript |
| 166 | +const SHELL_PARSER_CACHE_NAME = 'app-shell:cache'; |
| 167 | +const APP_SHELL_URL = '/home'; |
| 168 | +const INLINE_IMAGES: string[] = ['png', 'svg', 'jpg']; |
| 169 | +const ROUTE_DEFINITIONS = [ |
| 170 | + '/home', |
| 171 | + '/about/:id', |
| 172 | + ]; |
| 173 | +``` |
| 174 | + |
| 175 | +First, we declare a constant called `SHELL_PARSER_CACHE_NAME`. This is the name of the cache where we are going to store the App Shell's template once it has been generated. |
| 176 | + |
| 177 | +The `APP_SHELL_URL` is the URL of the view which template we are going to use in order to generate the App Shell. |
| 178 | + |
| 179 | +Since our final goal is to render the entire App Shell as quickly as possible, we want to inline all the referenced within its elements resources. The next step of our declarations is the `INLINE_IMAGES` array. It provides a list of image extensions that we want to be inlined as base64 strings. |
| 180 | + |
| 181 | +Finally, we declare a list of routes that we want to be handled by the parser. For instance, once the user visits the `/home` route the first thing that we want to do is to render the cached App Shell. Right after the root component of the route has been initialized and all the associated to it external resources are available, its content will be rendered on the place of the App Shell. As we can see from the route `/about/:id`, the `ROUTE_DEFINITIONS` supports wildcards, similar to the parameters in Angular's router. |
| 182 | + |
| 183 | + |
| 184 | +## Step 2 - Handling the Install Event |
| 185 | + |
| 186 | +As next step, lets see how we are going to handle the Service Worker's `install` event: |
| 187 | + |
| 188 | +```typescript |
| 189 | +self.addEventListener('install', function (event: any) { |
| 190 | + const parser = System.import('@angular/app-shell') |
| 191 | + .then((m: any) => { |
| 192 | + context.ngShellParser = m.shellParserFactory({ |
| 193 | + APP_SHELL_URL, |
| 194 | + ROUTE_DEFINITIONS, |
| 195 | + SHELL_PARSER_CACHE_NAME, |
| 196 | + INLINE_IMAGES |
| 197 | + }); |
| 198 | + }) |
| 199 | + .then(() => context.ngShellParser.fetchDoc()) |
| 200 | + .then((res: any) => context.ngShellParser.parseDoc(res)) |
| 201 | + .then((strippedResponse: any) => { |
| 202 | + return context.caches.open(SHELL_PARSER_CACHE_NAME) |
| 203 | + .then((cache: any) => cache.put(APP_SHELL_URL, strippedResponse)); |
| 204 | + }) |
| 205 | + .catch((e: any) => console.error(e)); |
| 206 | + event.waitUntil(parser); |
| 207 | +}); |
| 208 | +``` |
| 209 | + |
| 210 | +Once the Service Worker's install event has been triggered, we load the Runtime Parser using SystemJS and instantiate it with the `shellParserFactory` method. Notice that as arguments to the factory method we pass an object literal with the constants that we defined above. Once the factory method returns the Runtime Parser, we attach it as a property to the global context. |
| 211 | + |
| 212 | +Right after we instantiate the Runtime Parser, we invoke its `fetchDoc` method. The `fetchDoc` method is going to make an HTTP GET request to the `APP_SHELL_URL` that we declared above. |
| 213 | + |
| 214 | +Once we've successfully fetched the page that is intended to be used as a template for the App Shell, we invoke the `parseDoc` method. This method will perform all the required transformations over the fetched template in order to generate the final Application Shell. |
| 215 | + |
| 216 | +Finally, when the `parseDoc`'s execution completes, we cache the App Shell's template locally. |
| 217 | + |
| 218 | +### Template Transformations |
| 219 | + |
| 220 | +In order to get a better understanding of what the `parseDoc` method does, lets take a look at the response that Universal is going to return once the request to `APP_SHELL_URL` completes: |
| 221 | + |
| 222 | +```html |
| 223 | +<!DOCTYPE html> |
| 224 | +<html lang="en"><head> |
| 225 | + <meta charset="utf-8"> |
| 226 | + <style> |
| 227 | + /* App styles */ |
| 228 | + </style> |
| 229 | + <style> |
| 230 | + /* Material toolbar styles */ |
| 231 | + </style> |
| 232 | + <style> |
| 233 | + /* Material circle styles */ |
| 234 | + </style> |
| 235 | + </head> |
| 236 | +<body> |
| 237 | + <hello-mobile-app _nghost-gcc-1=""> |
| 238 | + <md-toolbar _ngcontent-gcc-1=""><div class="md-toolbar-layout"> <md-toolbar-row> |
| 239 | + <div _ngcontent-gcc-1="" class="icon ng"></div> |
| 240 | + Hello Mobile |
| 241 | + </md-toolbar-row> </div></md-toolbar> |
| 242 | + <!--template bindings={}--><!--shellRender(<md-spinner _ngcontent-gcc-1="" mode="indeterminate" role="progressbar" _nghost-gcc-3=""> <svg _ngcontent-gcc-3="" preserveAspectRatio="xMidYMid meet" viewBox="0 0 100 100"> <circle _ngcontent-lqi-3="" cx="50px" cy="50px" r="40px" style="stroke-dashoffset:251.3274;"></circle> </svg> </md-spinner>)--> |
| 243 | + <!--template bindings={}--><h1 _ngcontent-gcc-1="" shellNoRender="">App is Fully Rendered</h1> |
| 244 | + </hello-mobile-app> |
| 245 | + |
| 246 | + <script src="/node_modules/core-js/client/shim.min.js?1468224624471"></script> |
| 247 | + <script src="/node_modules/systemjs/dist/system.src.js?1468224624493"></script> |
| 248 | + <script src="./system-config.js"></script> |
| 249 | + |
| 250 | + <script src="/node_modules/zone.js/dist/zone.js?1468224624495"></script> |
| 251 | + <script src="/node_modules/rxjs/bundles/Rx.js?1468224624496"></script> |
| 252 | + |
| 253 | + <script> |
| 254 | + // Application initialization |
| 255 | + System.import('app/main') |
| 256 | + .catch(function (e) { |
| 257 | + console.error(e, |
| 258 | + 'Report this error at https://github.com/mgechev/angular2-seed/issues'); |
| 259 | + }); |
| 260 | + </script> |
| 261 | +</body> |
| 262 | +</html> |
| 263 | +``` |
| 264 | + |
| 265 | +Notice the following: |
| 266 | + |
| 267 | +- Inside the `hello-mobile-app` element the App Shell of the application is wrapped inside `<!--shellRender(...)-->`. |
| 268 | +- The elements which are not supposed to be part of the Application Shell are annotated with the `shellNoRender` attribute (note that the attributes are case sensitive). |
| 269 | + |
| 270 | +Once we invoke the `parseDoc` method with argument a response which as body has the template above, the following actions will be performed: |
| 271 | + |
| 272 | +- All the referenced within the template images that match any of the file extensions defined in `INLINE_IMAGES` will be inlined as base64 strings. |
| 273 | +- All the elements annotated with `shellNoRender` attribute will be stripped. |
| 274 | +- The content of all `<!--shellRender(...)-->` comments will be used as part of the Application Shell. |
| 275 | + |
| 276 | +## Step 3 - Handling the Fetch Event |
| 277 | + |
| 278 | +As final step, lets see how our Service Worker is going to handle the `fetch` event: |
| 279 | + |
| 280 | +```typescript |
| 281 | +self.addEventListener('fetch', function (event: any) { |
| 282 | + event.respondWith( |
| 283 | + context.ngShellParser.match(event.request) |
| 284 | + .then((response: any) => { |
| 285 | + if (response) return response; |
| 286 | + return context.caches.match(event.request) |
| 287 | + .then((response: any) => { |
| 288 | + if (response) return response; |
| 289 | + return context.fetch(event.request); |
| 290 | + }) |
| 291 | + }) |
| 292 | + ); |
| 293 | +}); |
| 294 | +``` |
| 295 | + |
| 296 | +Once the `fetch` event is triggered, we match the request using the `match` method of the Runtime Parser, against the list of routes defined in `ROUTE_DEFINITIONS`. In case the URL of the request matches any of the routes as response we are going to get the App Shell from the cache. Otherwise, we are going to fallback to the network. |
| 297 | + |
| 298 | +## Example |
| 299 | + |
| 300 | +Once the user opens our application and the App Shell Service Worker is registered successfully, its install event will be triggered. This will load the Runtime Parser and later the template located at the `APP_SHELL_URL`. Once the template is available, we are going to parse it using the Runtime Parser and store the result locally. This means that during the very first request to our application the App Shell Service Worker wouldn't have taken control yet. |
| 301 | + |
| 302 | +Now, lets suppose the user navigates to `/about/42`. This action will trigger the `fetch` method of the Service Worker, which will invoke the associated callback. Inside of it's body, we pass the target request to the `match` method of the Runtime Parser. Since in the `ROUTE_DEFINITIONS` we have the route `/about/:id` the Runtime Parser will return a response with body the template of the Application Shell. |
| 303 | + |
| 304 | +The template will be taken from a cache with name `SHELL_PARSER_CACHE_NAME`. The App Shell will be instantly rendered by the browser. In the mean time Angular will start fetching all the required by the page `/about/42` external resources in background. Once they have been loaded successfully, the Application Shell will be replaced with the requested page. |
| 305 | + |
| 306 | +The final result can be seen on the gif below: |
| 307 | + |
| 308 | +<img src="img/app-shell-parser-demo.gif" width="300" alt="Application Shell Runtime Parser Demo"> |
| 309 | + |
0 commit comments