詳解Angular5 服務(wù)端渲染實(shí)戰(zhàn)
本文基于上一篇 Angular5 的文章繼續(xù)進(jìn)行開發(fā),上文中講了搭建 Angular5 有道翻譯的過程,以及遇到問題的解決方案。
隨后改了 UI,從 bootstrap4 改到 angular material,這里不詳細(xì)講,服務(wù)端渲染也與修改 UI 無關(guān)。
看過之前文章的人會(huì)發(fā)現(xiàn),文章內(nèi)容都偏向于服務(wù)端渲染,vue 的 nuxt,react 的 next。
在本次改版前也嘗試去找類似 nuxt.js 與 next.js 的頂級(jí)封裝庫,可以大大節(jié)省時(shí)間,但是未果。
最后決定使用從 Angular2 開始就可用的前后端同構(gòu)解決方案 Angular Universal (Universal (isomorphic) JavaScript support for Angular.)
在這里不詳細(xì)介紹文檔內(nèi)容,本文也盡量使用通俗易懂的語言帶入 Angular 的 SSR
前提
前面寫的 udao 這個(gè)項(xiàng)目是完全遵從于 angular-cli 的,從搭建到打包,這也使得本文通用于所有 angular-cli 搭建的 angular5 項(xiàng)目。
搭建過程
首先安裝服務(wù)端的依賴
yarn add @angular/platform-server express yarn add -D ts-loader webpack-node-externals npm-run-all
這里需要注意的是 @angular/platform-server 的版本號(hào)最好根據(jù)當(dāng)前 angular 版本進(jìn)行安裝,如: @angular/platform-server@5.1.0 ,避免與其它依賴有版本沖突。
創(chuàng)建文件: src/app/app.server.module.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 { }
更新文件: 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 { }
我們需要一個(gè)主文件來導(dǎo)出服務(wù)端模塊
創(chuàng)建文件: src/main.server.ts
export { AppServerModule } from './app/app.server.module'
現(xiàn)在來更新 @angular/cli 的配置文件 .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"
}
}
]
// ...
}
上面的 // ... 代表省略掉,但是 json 沒有注釋一說,看著怪怪的....
當(dāng)然 .angular-cli.json 的配置不是固定的,根據(jù)需求自行修改
我們需要為服務(wù)端創(chuàng)建 tsconfig 配置文件: 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"
}
}
然后更新: src/tsconfig.app.json
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"baseUrl": "./",
"module": "es2015",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts",
"server.ts"
]
}
現(xiàn)在可以執(zhí)行以下命令,看配置是否有效
ng build -prod --build-optimizer --app 0 ng build --aot --app 1
運(yùn)行結(jié)果應(yīng)該如下圖所示
然后就是創(chuàng)建 Express.js 服務(wù), 創(chuàng)建文件: 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('*', (req, res) => {
res.render('index', { req })
})
app.listen(PORT, () => {
console.log(`listening on http://localhost:${PORT}!`)
})
理所當(dāng)然需要一個(gè) webpack 配置文件來打包 server.ts 文件: 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' }
]
}
}
為了打包方便最好在 package.json 里面加幾行腳本,如下:
"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"
}
現(xiàn)在嘗試運(yùn)行 npm run build ,將會(huì)看到如下輸出:
node 運(yùn)行剛剛打包好的 node dist/server.js 文件
打開 http://localhost:4200/ 會(huì)正常顯示項(xiàng)目主頁面
從上面的開發(fā)者工具可以看出 html 文檔是服務(wù)端渲染直出的,接下來嘗試請(qǐng)求數(shù)據(jù)試一下。
注意:本項(xiàng)目顯式(菜單可點(diǎn)擊)的幾個(gè)路由初始化都沒有請(qǐng)求數(shù)據(jù),但是單詞解釋的詳情頁是會(huì)在 ngOnInit() 方法里獲取數(shù)據(jù),例如: http://localhost:4200/detail/add 直接打開時(shí)會(huì)發(fā)生奇怪的現(xiàn)象,請(qǐng)求在服務(wù)端和客戶端分別發(fā)送一次,正常的服務(wù)端渲染項(xiàng)目首屏初始化數(shù)據(jù)的請(qǐng)求在服務(wù)端執(zhí)行,在客戶端不會(huì)二次請(qǐng)求!
發(fā)現(xiàn)問題后,就來踩平這個(gè)坑
試想如果采用一個(gè)標(biāo)記來區(qū)分服務(wù)端是否已經(jīng)拿到了數(shù)據(jù),如果沒拿到數(shù)據(jù)就在客戶端請(qǐng)求,如果已經(jīng)拿到數(shù)據(jù)就不發(fā)請(qǐng)求
當(dāng)然 Angular 早有一手準(zhǔn)備,那就是 Angular Modules for Transfer State
那么如何真實(shí)運(yùn)用呢?見下文
請(qǐng)求填坑
在服務(wù)端入口和客戶端入口分別引入 TransferStateModule
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';
// ...
@NgModule({
imports: [
// ...
ServerModule,
ServerTransferStateModule
]
// ...
})
export class AppServerModule { }
import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';
// ...
@NgModule({
declarations: [
AppComponent
// ...
],
imports: [
BrowserModule.withServerTransition({ appId: 'udao' }),
BrowserTransferStateModule
// ...
]
// ...
})
export class AppModule { }
以本項(xiàng)目為例在 detail.component.ts 里面,修改如下
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=${encodeURIComponent(apiURL)}`)
.subscribe(res => {
this.transData(res)
this.state.set(DETAIL_KEY, res as any)
this.loading = false
})
})
} else {
this.transData(this.details)
}
}
}
代碼夠簡單清晰,和上面描述的原理一致
現(xiàn)在我們只需要對(duì) main.ts 文件進(jìn)行小小的調(diào)整,以便在 DOMContentLoaded 時(shí)運(yùn)行我們的代碼,以使 TransferState 正常工作:
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))
})
到這里運(yùn)行 npm run build && node dist/server.js 然后刷新 http://localhost:4200/detail/add 到控制臺(tái)查看 network 如下:
發(fā)現(xiàn) XHR 分類里面沒有發(fā)起任何請(qǐng)求,只有 service-worker 的 cache 命中。
到這里坑都踩完了,項(xiàng)目運(yùn)行正常,沒發(fā)現(xiàn)其它 bug。
總結(jié)
2018 第一篇,目的就是探索所有流行框架服務(wù)端渲染的實(shí)現(xiàn),開辟了 angular 這個(gè)最后沒嘗試的框架。
當(dāng)然 Orange 還是前端小學(xué)生一枚,只知道實(shí)現(xiàn),原理說的不是很清楚,源碼看的不是很明白,如有紕漏還望指教。
最后 Github 地址和之前文章一樣:https://github.com/OrangeXC/udao
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
AngularJS 如何在控制臺(tái)進(jìn)行錯(cuò)誤調(diào)試
本文主要介紹AngularJS 如何在控制臺(tái)進(jìn)行錯(cuò)誤調(diào)試,還不錯(cuò),分享給大家,希望給大家做一個(gè)參考。2016-06-06
使用Angular Cli如何創(chuàng)建Angular私有庫詳解
這篇文章主要給大家介紹了關(guān)于使用Angular Cli如何創(chuàng)建Angular私有庫的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-01-01
AngularJS入門教程之?dāng)?shù)據(jù)綁定用法示例
這篇文章主要介紹了AngularJS之?dāng)?shù)據(jù)綁定用法,結(jié)合實(shí)例形式分析了AngularJS基于內(nèi)置指令ng-model實(shí)現(xiàn)數(shù)據(jù)綁定的操作技巧,需要的朋友可以參考下2016-11-11
Angular 4.x中表單Reactive Forms詳解
這篇文章主要介紹了Angular 4.x中表單Reactive Forms的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),相信對(duì)大家具有一定的參考價(jià)值,需要的朋友們下面來一起看看吧。2017-04-04
AngularJS入門心得之directive和controller通信過程
Angular JS (Angular.JS) 是一組用來開發(fā)Web頁面的框架、模板以及數(shù)據(jù)綁定和豐富UI組件,接下來通過本文給大家介紹AngularJS入門心得之directive和controller通信過程,對(duì)angularjs相關(guān)知識(shí)感興趣的朋友一起學(xué)習(xí)吧2016-01-01

