Angular6 服務端渲染

Jucuzzi發表於2019-04-18

如果不想檢視本文,直接尋找問題的解決方案,請搜尋'坑'

原理

服務端渲染即在服務端渲染產生頁面之後直接返回到客戶端檢視

第一次請求網頁地址的時候,返回已經在服務端渲染好的靜態html檔案,上面沒有點選事件,鍵盤事件,和互動js,這段頁面用一個ID標註,然後開始在客戶端渲染頁面,渲染好之後,根據ID替換在服務端渲染的頁面,填補了main.js(有可能較大)的下載時間+頁面渲染事件的空窗期,使頁面在slow3G的情況下依然流暢

優勢

  1. 幫助網路爬蟲(SEO)
  2. 提升在手機和低功耗裝置上的效能
  3. 迅速顯示出第一個頁面

開發流程

安裝依賴

$ npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader ts-loader webpack-cli
複製程式碼

在app.module.ts中新增

@NgModule({
  bootstrap: [AppComponent],
  imports: [
	// 加上下面這句,appId就是上面提到用於替換的唯一標識
    BrowserModule.withServerTransition({appId: 'my-app'}),
    ...
  ],

})
export class AppModule {}
複製程式碼

同目錄下建立app.server.module.ts

import {NgModule} from '@angular/core';
import {ServerModule} from '@angular/platform-server';
import {ModuleMapLoaderModule} from '@nguniversal/module-map-ngfactory-loader';

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

@NgModule({
  imports: [
    AppModule,
    ServerModule,
    ModuleMapLoaderModule // 非常重要,用來支援惰性載入的
  ],
  bootstrap: [AppComponent],
})
export class AppServerModule {}
複製程式碼

src下建立main.js

export { AppServerModule } from './app/app.server.module';
複製程式碼

複製ts.app.json為ts.server.json並修改

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/app",
    "baseUrl": "./",
    // 重要
    "module": "commonjs",
    "types": []
  },
  "exclude": [
    "test.ts",
    "**/*.spec.ts"
  ],
  // 指向上面建立的AppServerModule
  "angularCompilerOptions": {
    "entryModule": "app/app.server.module#AppServerModule"
  }
複製程式碼

在angular.json中修改配置,打包server

"architect": {
  "build": { ... }
  "server": {
    "builder": "@angular-devkit/build-angular:server",
    "options": {
      "outputPath": "dist/my-project-server",
      "main": "src/main.server.ts",
      "tsConfig": "src/tsconfig.server.json"
    }
  }
}
複製程式碼

此時 ng run projectName:server應該可以得到下面結果

$ ng run my-project:server

Date: 2017-07-24T22:42:09.739Z
Hash: 9cac7d8e9434007fd8da
Time: 4933ms
chunk {0} main.js (main) 9.49 kB [entry] [rendered]
chunk {1} styles.css (styles) 0 bytes [entry] [rendered]
複製程式碼

注意!坑1:在伺服器渲染的時候路徑和編譯的時候不同,如果在這部報錯找不到'src/app/.....'的時候,是你使用了src/的絕對路徑,需要全部改為../../的相對位置

設定伺服器環境

在根目錄下,新建server.ts,並往裡面寫入

// 這些必須在最前面引入
import 'zone.js/dist/zone-node';
import 'reflect-metadata';

import { renderModuleFactory } from '@angular/platform-server';
import { enableProdMode } from '@angular/core';
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';

import * as express from 'express';
import { join } from 'path';
import { readFileSync } from 'fs';

//坑3:報錯document not defined,通過引入domino來解決
const domino = require('domino');
const fs = require('fs');
const path = require('path');
const template = fs.readFileSync('./dist/browser/index.html').toString();
const win = domino.createWindow(template);
const files = fs.readdirSync(`${process.cwd()}/dist/server`);
global['navigator'] = win.navigator;
global['window'] = win;
Object.defineProperty(win.document.body.style, 'transform', {
  value: () => {
    return {
      enumerable: true,
      configurable: true
    };
  },
});
global['document'] = win.document;
global['CSS'] = null;

enableProdMode();

const app = express();

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

// 這裡要根據我們自己的目錄來,指向的是瀏覽器端編譯的index.html
const template = readFileSync(join(DIST_FOLDER, 'browser', 'index.html')).toString();

const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./server/main');


app.engine('html', (_, options, callback) => {
  renderModuleFactory(AppServerModuleNgFactory, {
    document: template,
    url: options.req.url,
	// 依賴注入,這裡是我們實現懶載入的一點
    extraProviders: [
      provideModuleMap(LAZY_MODULE_MAP)
    ]
  }).then(html => {
    callback(null, html);
  });
});

app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'browser'));

// 靜態檔案
app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));

// angular路由
app.get('*', (req, res) => {
  res.render(join(DIST_FOLDER, 'browser', 'index.html'), { req });
});

// api的話寫在中間,可以作為一個mock伺服器

// 啟動
app.listen(PORT, () => {
  console.log(`Node server listening on http://localhost:${PORT}`);
});
複製程式碼

打包並在伺服器上使用

設定 webpack 配置,以處理 Node Express 的 server.ts 檔案,並啟動應用伺服器。

在應用的根目錄下,建立一個 Webpack 配置檔案 webpack.server.config.js,它會把 server.ts 及其依賴編譯到 dist/server.js 中。

const path = require('path');
const webpack = require('webpack');

// 坑2:用webpack引入後臺的nodemodule的時候注意某些server端專用的npm包是要加上commonjs字首的
var fs = require('fs');
var nodeModules = {};
fs.readdirSync('node_modules')
  .filter(function(x) {
    return ['.bin'].indexOf(x) === -1;
  })
  .forEach(function(mod) {
    if (mod=='redis'||mod=='express'){
      nodeModules[mod] = 'commonjs ' + mod;
    }
  });

module.exports = {
  entry: {  server: './server.ts' },
  resolve: { extensions: ['.js', '.ts'] },
  target: 'node',
  // this makes sure we include node_modules and other 3rd party libraries
  externals: nodeModules,
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js'
  },
  module: {
    rules: [
      { test: /\.ts$/, loader: 'ts-loader' }
    ]
  },
  plugins: [
    new webpack.ContextReplacementPlugin(
      /(.+)?angular(\\|\/)core(.+)?/,
      path.join(__dirname, 'src'), 
      {} 
    ),
    new webpack.ContextReplacementPlugin(
      /(.+)?express(\\|\/)(.+)?/,
      path.join(__dirname, 'src'),
      {}
    )
  ]
}
複製程式碼

現在我們使用 node dist/server.js應該是可以啟動服務的,進入localhost:4000就可以訪問到工程

指令碼

"scripts": {
  "build:ssr": "npm run build:client-and-server-bundles && npm run webpack:server",
  "serve:ssr": "node dist/server.js",
  "build:client-and-server-bundles": "ng build --prod && ng run my-project:server:production",
  "webpack:server": "webpack --config webpack.server.config.js --progress --colors",
  ...
}
複製程式碼

執行npm run build:ssr 之後執行npm run serve:srr即可

坑3:報錯NotYetImplemented 這個其實是因為你引用了Cookie或者什麼之類在server上訪問不到的模組,這些模組需要你自己在工程裡面進行排查和debug,目前沒有更好的解決方法

坑4:報錯_angular_common_http__WEBPACK_IMPORTED_MODULE_5__.ɵHttpInterceptingHandler is not a constructor 這個是angular/core版本的問題,需要在package.json中升級angular/core就可以解決,參考:stackoverflow.com/questions/5…

相關文章