如果不想檢視本文,直接尋找問題的解決方案,請搜尋'坑'
原理
服務端渲染即在服務端渲染產生頁面之後直接返回到客戶端檢視
第一次請求網頁地址的時候,返回已經在服務端渲染好的靜態html檔案,上面沒有點選事件,鍵盤事件,和互動js,這段頁面用一個ID標註,然後開始在客戶端渲染頁面,渲染好之後,根據ID替換在服務端渲染的頁面,填補了main.js(有可能較大)的下載時間+頁面渲染事件的空窗期,使頁面在slow3G的情況下依然流暢
優勢
- 幫助網路爬蟲(SEO)
- 提升在手機和低功耗裝置上的效能
- 迅速顯示出第一個頁面
開發流程
安裝依賴
$ 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…