Angular 8: 为了提高性能,引入Universal并进行服务端渲染化
Angular8:为了提高性能,引入Universal并实现服务器端渲染。

环境
$ ng --version
_ _ ____ _ ___
/ \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
/ ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
|___/
Angular CLI: 8.3.18
Node: 10.15.3
OS: win32 x64
Angular: 8.2.13
... animations, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router, service-worker
Package Version
--------------------------------------------------------------------
@angular-devkit/architect 0.803.18
@angular-devkit/build-angular 0.803.18
@angular-devkit/build-optimizer 0.803.18
@angular-devkit/build-webpack 0.803.18
@angular-devkit/core 8.3.18
@angular-devkit/schematics 8.3.18
@angular/cdk 8.2.3
@angular/cli 8.3.18
@angular/fire 5.2.3
@angular/flex-layout 8.0.0-beta.27
@angular/material 8.2.3
@angular/platform-server 8.2.14
@angular/pwa 0.803.18
@ngtools/webpack 8.3.18
@nguniversal/express-engine 8.1.1
@nguniversal/module-map-ngfactory-loader 8.1.1
@schematics/angular 8.3.18
@schematics/update 0.803.18
rxjs 6.5.3
typescript 3.5.3
webpack 4.39.2
调查和准备工作
-
- さまざまなテストと調査の結果、firebaseuiとalgoliaはUniversalとのコンパチがヤバそうなのでそれらの機能を使用しているコンポーネントは別プロジェクト移した。
- ロールバックできるようにGitでの管理は必須。ロールバックGitコマンドは以下の通り。
$ git log //戻す対象のハッシュ値を調べる
commit ************************
$ git reset --hard ハッシュ値
通用推出
安装
命令如下。
$ ng add @nguniversal/express-engine --clientProject <プロジェクト名>
运行后,会添加以下文件:
src/main.server.ts
src/app/app.server.module.ts
tsconfig.server.json
webpack.server.config.js
server.ts
server.ts 是在 Cloud Functions 上执行的 Node Express 服务器的程序。
既存在的一些文件也将被更新。
package.json
angular.json
src/main.ts
src/app/app.module.ts
如果以上的操作成功使用build:ssr进行构建,然后使用serve:ssr启动服务器,即可实现SSR服务。
$ npm run build:ssr
$ npm run serve:ssr
> angular-universal-functions@0.0.0 serve:ssr ./projectName
> node dist/server
Node Express server listening on http://localhost:4000
然而实际上需要修改几个文件。
请修正这个地方。
无法同时使用Universal和Ivy。
运行”npm run build:ssr”时出错:
在Node中发生错误,C:/ProjectName/node_modules/@nguniversal/express-engine不存在。
由于似乎无法同时使用Universal和Ivy,所以要将Ivy禁用。
//tsconfig.app.json
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"types": []
},
"angularCompilerOptions": {
"enableIvy": false //<-- falseに
},
"exclude": [
"test.ts",
"**/*.spec.ts"
]
}
//tsconfig.spec.json
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"angularCompilerOptions": {
"enableIvy": false //<--falseに
},
"files": [
"test.ts",
"polyfills.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}
主要的路径对main.server.ts是错误的。
执行npm run build:ssr时出现错误:
ERROR in error TS6053: 找不到文件’C:/项目名/src/src/main.server.ts’。
因为没有src/src/..这样的文件夹,所以可能是构建脚本错误地引用了main.server.ts的路径描述。在某个配置文件中找到了tsconfig.server.json文件。
//tsconfig.server.json
{
"extends": "./tsconfig.app.json",
"compilerOptions": {
"outDir": "../out-tsc/app-server",
"module": "commonjs"
},
"files": [
"src/main.server.ts" //<-- src/を削除、"main.server.ts"とする。
],
"angularCompilerOptions": {
"entryModule": "./app/app.server.module#AppServerModule"
}
}
在SafePipe类中添加属性
在运行 npm run build:ssr 时出现错误:
ERROR in C:\ProjectName\src\app\service\safe.pipe.spec.ts
[tsl] ERROR in C:\ProjectName\src\app\service\safe.pipe.spec.ts(5,18)
TS2554:预期 1 个参数,但没有提供。
只有在使用SafePipe的情况下。
在“const pipe = new SafePipe();”的语法中会出现“Attribute不可用”的错误。请将其更改为“new SafePipe(null)”来解决。
//safe.pipe.spec.ts
import { SafePipe } from './safe.pipe';
describe('SafePipe', () => {
it('create an instance', () => {
const pipe = new SafePipe(); //<--属性にnullを入れてSafePipe(null)としておく
expect(pipe).toBeTruthy();
});
});
在/dist/server中找不到package.json文件的错误。
运行 npm run serve:ssr 时出现错误
main.js:165011
抛出一个错误:”package.json 不存在于 ” + package_json_path。
确认后,确实在构建目录dist/server中没有package.json。
不大清楚,但是我认为是指package.json的问题。
"build:client-and-server-bundles": "ng build --prod && ng run benzoinfojapan:server:production --bundleDependencies all"
在中文中给出以下句子的本地化释义,只需要一种选项:
を
"build:client-and-server-bundles": "ng build --prod && ng run benzoinfojapan:server:production --bundleDependencies none"
将 “–bundleDependencies all” 改为 “–bundleDependencies none”。
然后重新运行npm run build:ssr。
$ npm run build:ssr
Generating ES5 bundles for differential loading...
ES5 bundle generation complete.
$ Angular_Projects@0.0.0 compile:server Angular_Projects
$ webpack --config webpack.server.config.js --progress --colors
Hash: adsfadfadsfsdafadsf
Version: webpack 4.39.2
Time: 29389ms
Built at: 2019-12-22 16:02:13
Asset Size Chunks Chunk Names
server.js 958 KiB 0 [emitted] server
Entrypoint server = server.js
[0] ./server.ts 1.99 KiB {0} [built]
[2] external "events" 42 bytes {0} [built]
[3] external "fs" 42 bytes {0} [built]
[4] external "timers" 42 bytes {0} [optional] [built]
[5] external "crypto" 42 bytes {0} [built]
[13] external "path" 42 bytes {0} [built]
[22] external "util" 42 bytes {0} [built]
[30] external "net" 42 bytes {0} [built]
[35] external "buffer" 42 bytes {0} [built]
[56] external "stream" 42 bytes {0} [built]
[75] external "querystring" 42 bytes {0} [built]
[82] external "url" 42 bytes {0} [built]
[89] external "http" 42 bytes {0} [built]
[94] ./src sync 160 bytes {0} [built]
[121] external "require(\"./server/main\")" 42 bytes {0} [built]
+ 107 hidden modules
$ npm run serve:ssr
$ Angular_Projects@0.0.0 serve:ssr
./Angular_Projects
> node dist/server
Node Express server listening on http://localhost:4000
请访问http://localhost:4000

由于Firestore和Fireauth能够正常工作,所以很好。即使浏览器的JavaScript被关闭,也可以进行浏览。
准备将应用部署到Firebase
按照Firebase上的指示部署Angular 8 Universal(SSR)应用程序即可。
安装Firebase工具
npm install -g firebase-tools
在中文中,初始化Firebase项目可以这样表达:
初始化Firebase项目
如果已经与项目关联,可能最好重新开始一下。(选择使用Hosting和Cloud Functions)(选择使用TypeScript)(“您想将哪个目录用作公共目录?”为dist/browser目录)(“将其配置为单页面应用程序?”是的)(文件dist/browser/index.html已经存在。要覆盖吗?”否”)
$ firebase init
######## #### ######## ######## ######## ### ###### ########
## ## ## ## ## ## ## ## ## ## ##
###### ## ######## ###### ######## ######### ###### ######
## ## ## ## ## ## ## ## ## ## ##
## #### ## ## ######## ######## ## ## ###### ########
You're about to initialize a Firebase project in this directory:
./projectName
Before we get started, keep in mind:
* You are currently outside your home directory
? Are you ready to proceed? Yes
? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices.
( ) Database: Deploy Firebase Realtime Database Rules
( ) Firestore: Deploy rules and create indexes for Firestore
(*) Functions: Configure and deploy Cloud Functions
>(*) Hosting: Configure and deploy Firebase Hosting sites
( ) Storage: Deploy Cloud Storage security rules
? What language would you like to use to write Cloud Functions?
JavaScript
> TypeScript
? What do you want to use as your public directory? dist/browser
? Configure as a single-page app (rewrite all urls to /index.html)? Yes
? File dist/browser/index.html already exists. Overwrite? No
修改server.ts
将第24行的 `const app = express();` 更改为 `export const app = express();`。
然后,将最后的三行注释掉。
…省略…
// Start up the Node server
/*
app.listen(PORT, () => {
console.log(`Node Express server listening on http://localhost:${PORT}`);
});
*/
当我们进行firebase deploy时,需要将这3行代码注释掉,但是在运行npm run serve:ssr时是必要的。虽然有点麻烦…。
修改firebase.json
以以下方式表达。
{
"functions": {
"predeploy": [
"npm --prefix \"$RESOURCE_DIR\" run lint",
"npm --prefix \"$RESOURCE_DIR\" run build"
],
"source": "functions"
},
"hosting": {
"public": "dist/browser",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"function": "ssr" //変更
}
]
}
}
修改 webpack.server.config.js
输出:在 {} 中添加library和libraryTarget。
output: {
// Puts the output at the root of the dist folder
path: path.join(__dirname, 'dist'),
library: 'app',
libraryTarget: 'umd',
filename: '[name].js'
},
外部:如下所述对变更。
externals: [
/*
{
'./dist/server/main': 'require("./server/main")' // コメントアウトする
},
*/
/^firebase/
],
请编辑functions/src/index.ts文件。
请将以下代码转换成中文。
import * as functions from 'firebase-functions';
const universal = require(`${process.cwd()}/dist/server`).app;
export const ssr = functions.https.onRequest(universal);
添加一个复制dist文件夹的步骤
需要将构建Angular主体生成的dist文件夹复制到functions文件夹下,虽然可以手动操作,但希望通过脚本来实现。
一旦运行npm run build:ssr后,使用以下命令在functions文件夹内安装fs-extra。
Note: The translation provided is in Simplified Chinese.
$ cd functions
$ npm i fs-extra
虽然名字可以随便取,但在functions文件夹下创建一个名为copy-angular-app.js的文件,并粘贴并保存以下的代码。
const fs = require('fs-extra');
fs.copy('../dist', './dist').then(() => {
// distフォルダをfunctions配下にコピーしてかつ/dist/browser/index.htmlは削除するというわけ
fs.remove('../dist/browser/index.html').catch(e => console.error('REMOVE ERROR: ', e));
}).catch(err => {
console.error('COPY ERROR: ', err)
});
当进行firebase deploy时,需要删除browser/index.html文件,但是在运行npm run serve:ssr时需要保留该文件。该文件会在运行npm run build:ssr时重新创建。有点麻烦…。
修改functions/package.json
不要修改项目根目录下的package.json文件,而是修改functions文件夹下的package.json文件。将build字段修改为以下内容,以便运行先前创建的copy-angular-app.js文件。
{
"name": "functions",
"scripts": {
"lint": "tslint --project tsconfig.json",
"build": "node copy-angular-app && tsc", //変更
"serve": "npm run build && firebase serve --only functions",
"shell": "npm run build && firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"engines": {
"node": "8"
},
…省略…
Firebase本地测试与部署
我們現在開始進行測試。
首先,我們需要編譯 Angular 主體程式。
$ npm run build:ssr
接下来构建函数。
$ cd functions
$ npm run build
在本地运行Firebase
$ firebase serve
firebase serve 命令用于在本地环境中运行 Firebase 的命令。
$ firebase serve
=== Serving from './projectName'...
! Your requested "node" version "8" doesn't match your global version "10"
+ functions: Emulator started at http://localhost:5001
+
i functions: Watching "./projectName/functions" for Cloud Functions...
i hosting: Serving hosting files from: dist/browser
+ hosting: Local server: http://localhost:5000
+
+ functions[ssr]: http function initialized (http://localhost:5001/project/us-central1/ssr).
[hosting] Rewriting / to http://localhost:5001/project/us-central1/ssr for local Function ssr
当访问http://localhost:5000时,会调用Cloud Functions,并返回在服务器端渲染的结果。
如果可以确认正常启动,请部署到 Firebase。返回项目根目录并运行以下命令。
$ firebase deploy
$ firebase deploy
=== Deploying to 'project'...
i deploying functions, hosting
Running command: npm --prefix "$RESOURCE_DIR" run lint
> functions@ lint .projectName/functions
> tslint --project tsconfig.json
Running command: npm --prefix "$RESOURCE_DIR" run build
> functions@ build
> node copy-angular-app && tsc
+ functions: Finished running predeploy script.
i functions: ensuring necessary APIs are enabled...
+ functions: all necessary APIs are enabled
i functions: preparing functions directory for uploading...
i functions: packaged functions (21.99 MB) for uploading
+ functions: functions folder uploaded successfully
i hosting[project]: beginning deploy...
i hosting[project]: found 122 files in dist/browser
+ hosting[project]: file upload complete
i functions: updating Node.js 8 function ssr(us-central1)...
+ functions[ssr(us-central1)]: Successful update operation.
i hosting[project]: finalizing version...
+ hosting[project]: version finalized
i hosting[project]: releasing new version...
+ hosting[project]: release complete
+ Deploy complete!
Project Console: https://console.firebase.google.com/project/project/overview
Hosting URL: https://project.firebaseapp.com
附录 (Fù lù)
为了提高性能,我尝试了SSR化,但它并没有太大的贡献。
Universal引進之後

在删除Twitter分享按钮并对代码进行简化之后
在所有页面中都设置了 Twitter 分享按钮,但现在全部删除了。改进效果显著。无法确定代码的精简对此有多大贡献。要是这样的话,用 Lazy Load 好吗…?


将ServiceWorkerModule禁用
想起来一件事,为什么没有进行SSR操作的情况下PWA仍然可以工作呢?我仔细一看,原来在app.module.ts文件中仍然保留了ServiceWorkerModule。
import { ServiceWorkerModule } from '@angular/service-worker';
@NgModule({
imports: [
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
],

参考:将Angular 8 Universal(SSR)应用部署到Firebase
使用CircleCI将Angular 8 Universal应用部署到Firebase
在Firebase Cloud Functions上使用Angular服务器端渲染功能
Angular Firebase函数部署错误:找不到模块’firebase/app’