How do you go from a normal app to a server-rendered app? Let’s start with a normal app. Download Tour of Heroes. This is the completed tutorial app they use over at angular.io.
Once downloaded, head over to the root of the app and npm i to install the dependencies. Then do npm start and head over to http://localhost:4200 to check the app working. You should see something like:
When you first go into the page, there is nothing. the HTML the server sends doesnt have anything in the body more than the script files. The browser has to first get the scripts and execute them before we can see something. This is how SPAs (Single Page Application) work.
With SSR (Server Side Rendering) we can change that so that when we navigate to a page, we get pre-rendered HTML like we did in the pre SPA days
To get this in Angular you do:
ng add @nguniversal/express-engine –clientProject angular-io-example
This modifies your app to support Server Side Rendering. It also generates files that are now needed for it to work. You should see the terminal output the new files generated and the ones modified in your project:
Note: If you don’t see new the new files, bump up the TypeScript version to something like 3.2 and try again. Happened to me and found the answer in stack overflow. If you still can’t run the app, use my version.
That’s it. Now your app supports SSR. Don’t believe me? Run the following:
npm run build:ssr && npm run serve:ssr
That will build the app and start a node server on http://localhost:4000. Head over there to see for yourself. Check that you are on port 4000 and not 4200.
Note: You can still run the app in its normal non-SSR mode whenever you want with npm start
Proof that it Works
When you open the server rendered version you can check it works by heading over to the network tab in the chrome inspector. Take a look at the first document the browser receives, it’s always the HTML. Inside you will see there’s a bunch of content inside the body tag.
This means the browser does not have to wait for the Javascript to execute to paint something in the screen. It can do that immediately by parsing the HTML thanks to having stuff rendered in the server.
If you were to run the normal app without SSR again. You could again check the network tab and inspect the document, you will see nothing in the preview since there is no pre-rendered content coming in from the server. Everything has to be built by the browser when it executes the Javascript.
Limitations of Server Side Rendering
Server Rendering has its limitations. If you slow down the speed, you will notice that some functionality of the app is not working, even if a lot has already been painted to the browser. This is because it needs to wait for the Javascript to load to have full functionality. Some things work right away though, like navigation.
Navigation works right away because if the Javascript hasn’t loaded and a user clicks a link, that request is sent back to the server where it is handled. Once the Javascript is loaded, however, the navigation is handled by the browser with the use of Angular’s router. It’s quite awesome really, the user doesn’t perceive who’s handling the navigation or when the change of navigation ownership happens, it just works.
The few milliseconds you are left with only navigation capabilities is something you should take into consideration when developing with SSR. Do you want users to be able to do something right away when a component loads? Maybe you should put a loader on that component, so the users know that part is still loading. You can take off the loader once angular has been bootstrapped.
You can also use preboot to capture any actions the user has taken before the app fully loads and replay them.
Generated files: In-depth
Let’s go through the generated files and what they do.
main.server.ts
This is the bootstrapper for the server app. It exports the AppServerModule .
app.server.module.ts
This is the main module when using server side rendering. It imports the AppModule , which contains all the app’s logic. This is good, the app shouldn’t know if it’s going to be server rendered or not.
Then we import the ServerModule from the @angular/platform-server package. This module probably has code that supports running Angular on the server, just like BrowserModule gives Angular support on the browser.
Lastly we import ModuleMapLoaderModule which comes from the universal package. This is used to load the modules instantly instead of lazily, we dont need lazy loading when loading modules in the server.
tsconfig.server.json
Pretty self-explanatory. It’s the compile config for TypeScript in the server. It actually extends the tsconfig.app.json file.
webpack.server.config.js
Webpack server configuration. This tells Webpack to compile the server server.ts (which is in TypeScript) and output in the dist folder.
server.ts
Our node express server. This server handles requests to the app, builds what it can on the server (SSR), and sends it to the browser.
How does it all tie in together?
So we saw the generated files and know what they do. Now we can look at how it all ties in together, the workings of this new app.
Build and Run
First of all, let’s go back to these commands:
npm run build:ssr && npm run serve:ssr
It builds the app in SSR mode and runs a server to support it. Let’s look at the first command build:ssr . This is an npm command which was generated when we made the app support Server Side Rendering. You can find it in the package.json. It does this:
npm run build:client-and-server-bundles && npm run compile:server
The first command build:client-and-server-bundles will do the builds for both the app and server bundles:
ng build --prod && ng run angular.io-example:server:production
As you can see, it boils down to using angular-cli to build the prod version of the app and also this angular.io-example:server:production , which builds the server bundle. For the client app, the main.ts file is used as entry point. For the server, it’s main.server.ts
The second command compile:server will compile the server with a direct command to Webpack. Since this is outside Angular, we can’t use the angular-cli. Also, we need to run this because the server source file is in TypeScript, so it needs to be compiled and sent to the dist folder from where it will be run.
webpack --config webpack.server.config.js --progress --colors
Let’s go back to the original commands and see the second command npm run serve:ssr . If we check the package.json we see this refers to this command: node dist/server . It starts a node server file located in dist/server, which is the compiled version of the server.ts file from before.
The last part is important. We now have a server. Whereas before we didn’t need one, now we do, and we can’t have SSR without it. Think about this when you design the architecture where your app will run. You could have a node express server running to handle page requests, and have another one in any tech you want that handles data requests. Or you can use the node server as a proxy between your app and your data server too, or just have everything in node. Up to you!
Flow
We know how the app is built and run now. Let’s look at how it flows.
It all starts with navigating to the app. Upon entering a URL, say the default page, the request for that page is sent to the server. This is where server.ts comes in.
The first relevant code line we find in the server.ts is
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main');
We are importing the server module, the one we export in main.server.ts. Remember this one has the three imports we discussed earlier: AppModule, ServerModule, ModuleMapLoaderModule.
After that we have this piece of code:
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) app.engine('html', ngExpressEngine({ bootstrap: AppServerModuleNgFactory, providers: [ provideModuleMap(LAZY_MODULE_MAP) ] }));
We are telling express that we will use the ngExpressEngine as the HTML engine, a handy tool to abstract Server Side Rendering stuff. We pass in the server module containing everything needed to run the app, and also the lazy module map to load modules instantly.
So when a request comes in, this is the part being fired:
// All regular routes use the Universal engine app.get('*', (req, res) => { res.render('index', { req }); });
the HTML engine we configured earlier will take care of handling the request by compiling angular and spitting out the HTML to the browser.
Once in the browser, the flow of the app is the same as a non-SSR app, with the exception that now the page renders faster.
Leave a Reply