This article builds on the previous Angular5 article, which covered the process of building An Angular5 Youdao translation and the solutions to the problems encountered.

We then changed the UI from Bootstrap4 to Angular Material. We won’t go into details here, and server-side rendering has nothing to do with changing the UI.

Those of you who have read the previous articles will notice that they are biased towards server-side rendering, vue nuxt, React Next.

Before this revision, I also tried to find a top-level package library like Nuxt.js and next.js, which could save a lot of time, but failed.

We decided to use Angular Universal (Universal (isomorphic) JavaScript support for Angular.), a front-end isomorphism solution available since Angular2.

Without going into the details of the documentation, this article will try to bring the Angular SSR in plain English

The premise

The udAO project is fully angular-CLI compliant, from build to package, which makes this article universal for all Angular-CLI built Angular5 projects.

Set up process

Install server-side dependencies first

yarn add @angular/platform-server express
yarn add -D ts-loader webpack-node-externals npm-run-all
Copy the code

Note that the version of @angular/platform-server should be installed based on the current Angular version, such as @angular/[email protected], to avoid version conflicts with other dependencies.

Create a file: the SRC/app/app. Server. The module. The ts

import { NgModule } from '@angular/core'
import { ServerModule } from '@angular/platform-server'

import { AppModule } from './app.module'
import { AppComponent } from './app.component'

@NgModule({
  imports: [
    AppModule,
    ServerModule
  ],
  bootstrap: [AppComponent],
})
export class AppServerModule {}Copy the code

Update file: SRC /app/app.module.ts

import { BrowserModule } from '@angular/platform-browser'
import { NgModule } from '@angular/core'
// ...

import { AppComponent } from './app.component'
// ...

@NgModule({
  declarations: [
    AppComponent
    // ...].imports: [
    BrowserModule.withServerTransition({ appId: 'udao' })
    // ...].providers: [].bootstrap: [AppComponent]
})
export class AppModule {}Copy the code

We need a master file to export the server module

Create a file: SRC /main.server.ts

export { AppServerModule } from './app/app.server.module'
Copy the code

Now update the @angular/cli configuration file.angular-cli.json

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json"."project": {
    "name": "udao"
  },
  "apps": [{"root": "src"."outDir": "dist/browser"."assets": [
        "assets"."favicon.ico"
      ]
      // ...
    },
    {
      "platform": "server"."root": "src"."outDir": "dist/server"."assets": []."index": "index.html"."main": "main.server.ts"."test": "test.ts"."tsconfig": "tsconfig.server.json"."testTsconfig": "tsconfig.spec.json"."prefix": "app"."scripts": []."environmentSource": "environments/environment.ts"."environments": {
        "dev": "environments/environment.ts"."prod": "environments/environment.prod.ts"}}] //... }Copy the code

The above //… The representation is omitted, but there is no comment about json, which looks strange….

Of course, the.angular-cli.json configuration is not fixed and can be modified as needed

We need to create the tsconfig configuration file for the server: SRC /tsconfig.server.json

{
  "extends": ".. /tsconfig.json"."compilerOptions": {
    "outDir": ".. /out-tsc/app"."baseUrl": ". /"."module": "commonjs"."types": []},"exclude": [
    "test.ts"."**/*.spec.ts"."server.ts"]."angularCompilerOptions": {
    "entryModule": "app/app.server.module#AppServerModule"}}Copy the code

Then update: SRC /tsconfig.app.json

{
  "extends": ".. /tsconfig.json"."compilerOptions": {
    "outDir": ".. /out-tsc/app"."baseUrl": ". /"."module": "es2015"."types": []},"exclude": [
    "test.ts"."**/*.spec.ts"."server.ts"]}Copy the code

You can now run the following command to see if the configuration works

ng build -prod --build-optimizer --app 0
ng build --aot --app 1
Copy the code

The result should look like the following figure

Then create the express.js service and create the file: SRC /server.ts

import 'reflect-metadata'
import 'zone.js/dist/zone-node'
import { renderModuleFactory } from '@angular/platform-server'
import { enableProdMode } from '@angular/core'
import * as express from 'express'
import { join } from 'path'
import { readFileSync } from 'fs'

enableProdMode();

const PORT = process.env.PORT || 4200
const DIST_FOLDER = join(process.cwd(), 'dist')

const app = express()

const template = readFileSync(join(DIST_FOLDER, 'browser'.'index.html')).toString()
const { AppServerModuleNgFactory } = require('main.server')

app.engine('html', (_, options, callback) => {
  const opts = { document: template, url: options.req.url }

  renderModuleFactory(AppServerModuleNgFactory, opts)
    .then(html= > callback(null, html))
});

app.set('view engine'.'html')
app.set('views'.'src')

app.get('*. *', express.static(join(DIST_FOLDER, 'browser')))

app.get(The '*', (req, res) => {
  res.render('index', { req })
})

app.listen(PORT, () => {
  console.log(`listening on http://localhost:${PORT}! `)})Copy the code

Of course you need a webpack configuration file to package the server.ts file: webpack.config.js

const path = require('path');
var nodeExternals = require('webpack-node-externals');

module.exports = {
  entry: {
    server: './src/server.ts'
  },
  resolve: {
    extensions: ['.ts'.'.js'].alias: {
      'main.server': path.join(__dirname, 'dist'.'server'.'main.bundle.js')}},target: 'node'.externals: [nodeExternals()],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js'
  },
  module: {
    rules: [{test: /\.ts$/.loader: 'ts-loader'}}}]Copy the code

It is best to add a few lines of script to package.json for easy packaging, as follows:

"scripts": {
  "ng": "ng",
  "start": "ng serve",
  "build": "run-s build:client build:aot build:server",
  "build:client": "ng build -prod --build-optimizer --app 0",
  "build:aot": "ng build --aot --app 1",
  "build:server": "webpack -p",
  "test": "ng test",
  "lint": "ng lint",
  "e2e": "ng e2e"
}
Copy the code

Now try running NPM run build and you will see the following output:

Node runs the node dist/server.js file that you just packaged

The main page of the project is normally displayed when you open http://localhost:4200/

As you can see from the developer tools above, the HTML document is rendered directly by the server.

Note: none of the explicit (menu-clickable) route initializations in this project request data, but the detail page of the word explanation does get data in the ngOnInit() method, for example: http://localhost:4200/detail/add directly open the strange phenomenon happens, the request on the server and the client sends a respectively, the normal rendering of a service project, the first screen to initialize the data request on the server, the client won’t second request!

When you find a problem, step on the hole

Imagine using a flag to distinguish whether the server has received the data or not, asking the client if it has not received the data, and not sending the request if it has received the data

Of course, Angular has something in store, Angular Modules for Transfer State

So how do you actually use it? See below

Request filling holes

Import TransferStateModule in the server and client portals respectively

import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';
// ...

@NgModule({
  imports: [
    // ...
    ServerModule,
    ServerTransferStateModule
  ]
  // ...
})
export class AppServerModule {}Copy the code
import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';
// ...

@NgModule({
  declarations: [
    AppComponent
    // ...].imports: [
    BrowserModule.withServerTransition({ appId: 'udao' }),
    BrowserTransferStateModule
    // ...
  ]
  // ...
})
export class AppModule {}Copy the code

Take this project as an example. In detail.component.ts, modify as follows

import { Component, OnInit } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { Router,  ActivatedRoute, NavigationEnd } from '@angular/router'
import { TransferState, makeStateKey } from '@angular/platform-browser'

const DETAIL_KEY = makeStateKey('detail')

// ...

export class DetailComponent implements OnInit {
  details: any

  // some variable

  constructor(
    private http: HttpClient,
    private state: TransferState,
    private route: ActivatedRoute,
    private router: Router
  ) {}

  transData (res) {
    // translate res data
  }

  ngOnInit () {
    this.details = this.state.get(DETAIL_KEY, null as any)

    if (!this.details) {
      this.route.params.subscribe((params) = > {
        this.loading = true

        const apiURL = `https://dict.youdao.com/jsonapi?q=${params['word']}`

        this.http.get(` /? url=The ${encodeURIComponent(apiURL)}`)
        .subscribe(res= > {
          this.transData(res)
          this.state.set(DETAIL_KEY, res as any)
          this.loading = false})})}else {
      this.transData(this.details)
    }
  }
}
Copy the code

The code is simple and clear enough to follow the principles described above

Now we just need to make a small adjustment to the main.ts file to run our code with DOMContentLoaded to make TransferState work:

import { enableProdMode } from '@angular/core'
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'

import { AppModule } from './app/app.module'
import { environment } from './environments/environment'

if (environment.production) {
  enableProdMode()
}

document.addEventListener('DOMContentLoaded', () => {
  platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err= > console.log(err))
})
Copy the code

Here run NPM run build && node dist/server. The js and then refresh the http://localhost:4200/detail/add to the console view network is as follows:

No requests were made in the XHR category, only the service-worker cache hit.

By this point, the pits have been stomped out, and the project is running normally, with no other bugs found.

conclusion

The first post of 2018 aims to explore server-side rendering implementations of all the popular frameworks, opening up the last untried angular framework.

Of course, Orange is still a front-end pupil, only knows the implementation, but the principle is not very clear, and the source code is not very clear. If there is any mistake, please kindly advise.

The last lot address as same as previous articles: https://github.com/OrangeXC/udao

Github has an online link, so that’s it