详解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 相关文章推荐
javascript Base类 包含基本的方法
Jul 22 Javascript
Jquery 选中表格一列并对表格排序实现原理
Dec 15 Javascript
jquery入门—数据删除与隔行变色以及图片预览
Jan 07 Javascript
node.js中的fs.symlinkSync方法使用说明
Dec 15 Javascript
jquery获得当前html页面源码的方法
Jul 14 Javascript
JS如何判断是否为ie浏览器的方法(包括IE10、IE11在内)
Dec 13 Javascript
基于JavaScript的操作系统你听说过吗?
Jan 28 Javascript
jQuery-mobile事件监听与用法详解
Nov 23 Javascript
Angularjs自定义指令实现三级联动 选择地理位置
Feb 13 Javascript
VUE axios发送跨域请求需要注意的问题
Jul 06 Javascript
微信小程序 循环及嵌套循环的使用总结
Sep 26 Javascript
angularJs在多个控制器中共享服务数据的方法
Sep 30 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
function.inc.php超越php
2006/12/09 PHP
php 冒泡排序 交换排序法
2011/05/10 PHP
PHP批量生成图片缩略图的方法
2015/06/18 PHP
php实现简单的上传进度条
2015/11/17 PHP
thinkPHP框架整合tcpdf插件操作示例
2018/08/07 PHP
PHP小程序支付功能完整版【基于thinkPHP】
2019/03/26 PHP
使Ext的Template可以解析二层的json数据的方法
2007/12/22 Javascript
基于jquery的blockui插件显示弹出层
2011/04/14 Javascript
如何使用jQuery来处理图片坏链具体实现步骤
2013/05/02 Javascript
jQuery实现iframe父窗体和子窗体的相互调用
2016/06/17 Javascript
利用jQuery实现打字机字幕效果实例代码
2016/09/02 Javascript
微信小程序 slider 详解及实例代码
2017/01/10 Javascript
canvas实现图片根据滑块放大缩小效果
2017/02/24 Javascript
详解Vue中过度动画效果应用
2017/05/25 Javascript
React-redux实现小案例(todolist)的过程
2019/09/29 Javascript
[01:45]典藏宝瓶2+祈求者身心——这就是DOTA2TI9总奖金突破3000万美元的秘密
2019/07/21 DOTA
Python之eval()函数危险性浅析
2014/07/03 Python
详解Django中Request对象的相关用法
2015/07/17 Python
Python实现将DOC文档转换为PDF的方法
2015/07/25 Python
python os.listdir按文件存取时间顺序列出目录的实例
2018/10/21 Python
python版本五子棋的实现代码
2018/12/11 Python
tensorflow 2.0模式下训练的模型转成 tf1.x 版本的pb模型实例
2020/06/22 Python
Python 捕获代码中所有异常的方法
2020/08/03 Python
联想新加坡官方网站:Lenovo Singapore
2017/10/24 全球购物
德国高品质男装及配饰商城:Cultizm(Raw Denim原色牛仔裤)
2018/04/16 全球购物
专注澳大利亚特产和新西兰特产的澳洲中文网:0061澳洲制造
2019/03/24 全球购物
运动会广播稿300字
2014/01/10 职场文书
手机促销活动方案
2014/02/05 职场文书
人事专员的职责
2014/02/26 职场文书
库房管理员岗位职责
2014/03/09 职场文书
征兵宣传标语
2014/06/20 职场文书
求职自我评价范文100字
2014/09/23 职场文书
驳回起诉裁定书
2015/05/19 职场文书
2016年助残日旅游活动总结
2016/04/01 职场文书
Python常遇到的错误和异常
2021/11/02 Python
JavaScript流程控制(分支)
2021/12/06 Javascript