国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁(yè) > 編程 > JavaScript > 正文

詳解Angular5 服務(wù)端渲染實(shí)戰(zhàn)

2019-11-19 14:36:06
字體:
來(lái)源:轉(zhuǎn)載
供稿:網(wǎng)友

本文基于上一篇 Angular5 的文章繼續(xù)進(jìn)行開(kāi)發(fā),上文中講了搭建 Angular5 有道翻譯的過(guò)程,以及遇到問(wèn)題的解決方案。

 

隨后改了 UI,從 bootstrap4 改到 angular material,這里不詳細(xì)講,服務(wù)端渲染也與修改 UI 無(wú)關(guān)。

看過(guò)之前文章的人會(huì)發(fā)現(xiàn),文章內(nèi)容都偏向于服務(wù)端渲染,vue 的 nuxt,react 的 next。

在本次改版前也嘗試去找類似 nuxt.js 與 next.js 的頂級(jí)封裝庫(kù),可以大大節(jié)省時(shí)間,但是未果。

最后決定使用從 Angular2 開(kāi)始就可用的前后端同構(gòu)解決方案 Angular Universal (Universal (isomorphic) JavaScript support for Angular.)

在這里不詳細(xì)介紹文檔內(nèi)容,本文也盡量使用通俗易懂的語(yǔ)言帶入 Angular 的 SSR

前提

前面寫的 udao 這個(gè)項(xiàng)目是完全遵從于 angular-cli 的,從搭建到打包,這也使得本文通用于所有 angular-cli 搭建的 angular5 項(xiàng)目。

搭建過(guò)程

首先安裝服務(wù)端的依賴

yarn add @angular/platform-server expressyarn 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è)主文件來(lái)導(dǎo)出服務(wù)端模塊

創(chuàng)建文件: src/main.server.ts

export { AppServerModule } from './app/app.server.module'

現(xiàn)在來(lái)更新 @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 沒(méi)有注釋一說(shuō),看著怪怪的....

當(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 0ng 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 || 4200const 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 配置文件來(lái)打包 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 文件

打開(kāi) http://localhost:4200/ 會(huì)正常顯示項(xiàng)目主頁(yè)面

 

從上面的開(kāi)發(fā)者工具可以看出 html 文檔是服務(wù)端渲染直出的,接下來(lái)嘗試請(qǐng)求數(shù)據(jù)試一下。

注意:本項(xiàng)目顯式(菜單可點(diǎn)擊)的幾個(gè)路由初始化都沒(méi)有請(qǐng)求數(shù)據(jù),但是單詞解釋的詳情頁(yè)是會(huì)在 ngOnInit() 方法里獲取數(shù)據(jù),例如: http://localhost:4200/detail/add 直接打開(kāi)時(shí)會(huì)發(fā)生奇怪的現(xiàn)象,請(qǐng)求在服務(wù)端和客戶端分別發(fā)送一次,正常的服務(wù)端渲染項(xiàng)目首屏初始化數(shù)據(jù)的請(qǐng)求在服務(wù)端執(zhí)行,在客戶端不會(huì)二次請(qǐng)求!

發(fā)現(xiàn)問(wèn)題后,就來(lái)踩平這個(gè)坑

試想如果采用一個(gè)標(biāo)記來(lái)區(qū)分服務(wù)端是否已經(jīng)拿到了數(shù)據(jù),如果沒(méi)拿到數(shù)據(jù)就在客戶端請(qǐng)求,如果已經(jīng)拿到數(shù)據(jù)就不發(fā)請(qǐng)求

當(dāng)然 Angular 早有一手準(zhǔn)備,那就是 Angular Modules for Transfer State

那么如何真實(shí)運(yùn)用呢?見(jià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)  } }}

代碼夠簡(jiǎn)單清晰,和上面描述的原理一致

現(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 分類里面沒(méi)有發(fā)起任何請(qǐng)求,只有 service-worker 的 cache 命中。

到這里坑都踩完了,項(xiàng)目運(yùn)行正常,沒(méi)發(fā)現(xiàn)其它 bug。

總結(jié)

2018 第一篇,目的就是探索所有流行框架服務(wù)端渲染的實(shí)現(xiàn),開(kāi)辟了 angular 這個(gè)最后沒(méi)嘗試的框架。

當(dāng)然 Orange 還是前端小學(xué)生一枚,只知道實(shí)現(xiàn),原理說(shuō)的不是很清楚,源碼看的不是很明白,如有紕漏還望指教。

最后 Github 地址和之前文章一樣:https://github.com/OrangeXC/udao

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持武林網(wǎng)。

發(fā)表評(píng)論 共有條評(píng)論
用戶名: 密碼:
驗(yàn)證碼: 匿名發(fā)表
主站蜘蛛池模板: 获嘉县| 青田县| 晋城| 铁力市| 石台县| 台山市| 玉屏| 钟祥市| 宁德市| 杂多县| 哈尔滨市| 丁青县| 巴林左旗| 太康县| 双牌县| 曲水县| 饶阳县| 揭东县| 平遥县| 额敏县| 砀山县| 福泉市| 内黄县| 南投县| 高唐县| 清河县| 娱乐| 延津县| 龙山县| 石屏县| 奎屯市| 勐海县| 北碚区| 镇原县| 丽江市| 南雄市| 屏东县| 凉城县| 义马市| 福鼎市| 夏邑县|