详解Angular5 服务端渲染实战


Posted in Javascript onJanuary 04, 2018

本文基于上一篇 Angular5 的文章继续进行开发,上文中讲了搭建 Angular5 有道翻译的过程,以及遇到问题的解决方案。

详解Angular5 服务端渲染实战 

随后改了 UI,从 bootstrap4 改到 angular material,这里不详细讲,服务端渲染也与修改 UI 无关。

看过之前文章的人会发现,文章内容都偏向于服务端渲染,vue 的 nuxt,react 的 next。

在本次改版前也尝试去找类似 nuxt.js 与 next.js 的顶级封装库,可以大大节省时间,但是未果。

最后决定使用从 Angular2 开始就可用的前后端同构解决方案 Angular Universal (Universal (isomorphic) JavaScript support for Angular.)

在这里不详细介绍文档内容,本文也尽量使用通俗易懂的语言带入 Angular 的 SSR

前提

前面写的 udao 这个项目是完全遵从于 angular-cli 的,从搭建到打包,这也使得本文通用于所有 angular-cli 搭建的 angular5 项目。

搭建过程

首先安装服务端的依赖

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

这里需要注意的是 @angular/platform-server 的版本号最好根据当前 angular 版本进行安装,如: @angular/platform-server@5.1.0 ,避免与其它依赖有版本冲突。

创建文件: 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 { }

我们需要一个主文件来导出服务端模块

创建文件: src/main.server.ts

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

现在来更新 @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 没有注释一说,看着怪怪的....

当然 .angular-cli.json 的配置不是固定的,根据需求自行修改

我们需要为服务端创建 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"
 ]
}

现在可以执行以下命令,看配置是否有效

ng build -prod --build-optimizer --app 0
ng build --aot --app 1

运行结果应该如下图所示

详解Angular5 服务端渲染实战 

然后就是创建 Express.js 服务, 创建文件: 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}!`)
})

理所当然需要一个 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"
}

现在尝试运行 npm run build ,将会看到如下输出:

详解Angular5 服务端渲染实战 

node 运行刚刚打包好的 node dist/server.js 文件

打开 http://localhost:4200/ 会正常显示项目主页面

详解Angular5 服务端渲染实战 

从上面的开发者工具可以看出 html 文档是服务端渲染直出的,接下来尝试请求数据试一下。

注意:本项目显式(菜单可点击)的几个路由初始化都没有请求数据,但是单词解释的详情页是会在 ngOnInit() 方法里获取数据,例如: http://localhost:4200/detail/add 直接打开时会发生奇怪的现象,请求在服务端和客户端分别发送一次,正常的服务端渲染项目首屏初始化数据的请求在服务端执行,在客户端不会二次请求!

发现问题后,就来踩平这个坑

试想如果采用一个标记来区分服务端是否已经拿到了数据,如果没拿到数据就在客户端请求,如果已经拿到数据就不发请求

当然 Angular 早有一手准备,那就是 Angular Modules for Transfer State

那么如何真实运用呢?见下文

请求填坑

在服务端入口和客户端入口分别引入 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 { }

以本项目为例在 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)
  }
 }
}

代码够简单清晰,和上面描述的原理一致

现在我们只需要对 main.ts 文件进行小小的调整,以便在 DOMContentLoaded 时运行我们的代码,以使 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))
})

到这里运行 npm run build && node dist/server.js 然后刷新 http://localhost:4200/detail/add 到控制台查看 network 如下:

详解Angular5 服务端渲染实战 

发现 XHR 分类里面没有发起任何请求,只有 service-worker 的 cache 命中。

到这里坑都踩完了,项目运行正常,没发现其它 bug。

总结

2018 第一篇,目的就是探索所有流行框架服务端渲染的实现,开辟了 angular 这个最后没尝试的框架。

当然 Orange 还是前端小学生一枚,只知道实现,原理说的不是很清楚,源码看的不是很明白,如有纰漏还望指教。

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
window.open()弹出居中的窗口
Feb 01 Javascript
js函数的延迟加载实现代码
Oct 11 Javascript
Jquery动态更改一张位图的src与Attr的使用
Jul 31 Javascript
原生javascript图片自动或手动切换示例附演示源码
Sep 04 Javascript
怎么判断js脚本加载完成
Feb 28 Javascript
jQuery学习笔记之jQuery中的$
Jan 19 Javascript
js的for in循环和java里foreach循环的区别分析
Jan 28 Javascript
JS解析XML文件和XML字符串详解
Apr 17 Javascript
jQuery中each循环的跳出和结束实例
Aug 16 jQuery
微信小程序实现倒计时调用相机自动拍照功能
Jun 10 Javascript
Vue模板语法中数据绑定的实例代码
May 17 Javascript
JS计算斐波拉切代码实例
Sep 12 Javascript
JavaScript中的高级函数
Jan 04 #Javascript
Three.js 再探 - 写一个微信跳一跳极简版游戏
Jan 04 #Javascript
JS实现带导航城市列表以及输入搜索功能
Jan 04 #Javascript
微信小程序实现的贪吃蛇游戏【附源码下载】
Jan 03 #Javascript
详解Angular2学习笔记之Html属性绑定
Jan 03 #Javascript
Angular2学习笔记之数据绑定的示例代码
Jan 03 #Javascript
vue、react等单页面项目应该这样子部署到服务器
Jan 03 #Javascript
You might like
PHP中创建并处理图象
2006/10/09 PHP
php中使用ExcelFileParser处理excel获得数据(可作批量导入到数据库使用)
2010/08/21 PHP
页面利用渐进式JPEG来提升用户体验度
2014/12/01 PHP
php根据一个给定范围和步进生成数组的方法
2015/06/19 PHP
在Mac OS上搭建PHP的Yii框架及相关测试环境
2016/02/14 PHP
详解PHP 7.4 中数组延展操作符语法知识点
2019/07/19 PHP
javascript 图片上一张下一张链接效果代码
2010/03/12 Javascript
jQuery焦点图切换特效插件封装实例
2013/08/18 Javascript
thinkphp中常用的系统常量和系统变量
2014/03/05 Javascript
禁止iframe脚本弹出的窗口覆盖了父窗口的方法
2014/09/06 Javascript
jquery动态改变div宽度和高度
2015/02/09 Javascript
理解js回收机制通俗易懂版
2016/02/29 Javascript
JavaScript定时器实现的原理分析
2016/12/06 Javascript
es6+angular1.X+webpack 实现按路由功能打包项目的示例
2017/08/16 Javascript
vue-music 使用better-scroll遇到轮播图不能自动轮播问题
2018/12/03 Javascript
vue调用语音播放的方法
2019/09/27 Javascript
JavaScript设计模式之门面模式原理与实现方法分析
2020/03/09 Javascript
Vue 封装防刷新考试倒计时组件的实现
2020/06/05 Javascript
JavaScript中window和document用法详解
2020/07/28 Javascript
原生JavaScript实现刮刮乐
2020/09/29 Javascript
python自动化测试之从命令行运行测试用例with verbosity
2014/09/28 Python
Python批量创建迅雷任务及创建多个文件
2016/02/13 Python
详解Django的model查询操作与查询性能优化
2018/10/16 Python
python实现事件驱动
2018/11/21 Python
如何用python写一个简单的词法分析器
2018/12/18 Python
python爬虫 爬取超清壁纸代码实例
2019/08/16 Python
Python run()函数和start()函数的比较和差别介绍
2020/05/03 Python
python + selenium 刷B站播放量的实例代码
2020/06/12 Python
新春联欢会主持词
2014/03/24 职场文书
2014年班主任自我评价范文
2014/04/23 职场文书
品牌推广活动策划方案
2014/08/19 职场文书
党员教师个人对照检查材料(群众路线)
2014/09/26 职场文书
幼儿教师2014年度工作总结
2014/12/16 职场文书
MySQL单表千万级数据处理的思路分享
2021/06/05 MySQL
Apache Pulsar集群搭建部署详细过程
2022/02/12 Servers
HTML常用标签超详细整理
2022/03/19 HTML / CSS