Angular 6 服務端渲染之 udao 終章

orangexc發表於2018-05-10

先介紹下小朋友 udao,首先是一個開源專案,程式碼足夠簡單,其次是跟隨 Angular 大小版本一起成長的專案,會定期更新所有依賴包以及相容最新版本的寫法

Github 地址也貼出來好多次了:github.com/OrangeXC/ud…

本來行文目的只是新框架推出,本著學習的目的自己搞個東西出來玩,文章只是記錄專案的開發更迭過程,以及遇到的坑。

udao 系列文章有

從 Angular 5 寫到 6,逐步擴充套件 PWA,SSR 等,今天有讀者提了一個 issue,跟著歷史文章一步一步學習發現文章寫的是 Angular 5,但是 github 專案已經升級到了 Angular 6,是不是要保留多個版本分支?

我的回答是否定的,Angular 版本更迭之快想必大家都瞭解,每次大小版本的更新我都會在 github 上修改程式碼,但是不會一直出更新文章,因為每次更新的可能就幾行程式碼,循序漸進的更迭,更希望讀者能多一分敏銳的嗅覺,與框架相關的的實戰類文章總有退出江湖的一天,取決於框架的升級更新和框架的衰亡,相信當今翻閱 jquery 的實戰文章的人寥寥無幾,Angular 目前正處於半年一大版的節奏,既然觀察到 Angular 6 推出了,準備學習 Angular 5 文章之前就應該先看下作者的專案連結是不是 Angular 5 的專案,況且專案只是參考,寫文章想引出更多的是踩坑的過程。

嘮叨了這麼多之後,正如標題 終章 udao 系列文章到本篇結束,以後每個版本會持續更新迭代到 github 上,升級的程式碼變化可以順著 git commit 記錄查到

升級依賴

首先升級 angular-cli 到最新版本的 6.0,升級之前記得先解除安裝清 cache

全域性

npm uninstall -g @angular/cli
npm cache verify
# if npm version is < 5 then use `npm cache clean`
npm install -g @angular/cli@latest
複製程式碼

本地

rm -rf node_modules dist # use rmdir /S/Q node_modules dist in Windows Command Prompt; use rm -r -fo node_modules,dist in Windows PowerShell
yarn add @angular/cli@latest
yarn
複製程式碼

執行 ng update --all,從 angular-cli 1.7 開始支援 update,具體引數見github.com/angular/ang…

執行 --all 目的是修改 package.json,否則只提示不修改,親測這個 --all 引數有坑,會報各種異常,升級版本後還會重複提示升級,遇到警告可以採用降級方案,直接 ng update,根據提示一個一個去 package.json 裡修改,再報錯就是和這個方法無緣了,採用遠古時期方案去 npm 官網一個個查出最新版本更新上去。

注:typescript 停留在 2.7.2,即可不要升級到 2.8+,yarn 會報警高,也就是 angular-cli 的無腦 bug,ng update --all 建議升級到 2.8.3 不升級它就不往下跑,升級完 2.8.3 安裝 yarn 又警告被依賴的 typescript 版本應該 >2.7 & <2.8。

順利升級完所有依賴後,別忘了加幾個依賴上去

  • yarn add @nguniversal/express-engine
  • yarn add @nguniversal/module-map-ngfactory-loader
  • yarn add @angular-devkit/build-angular -D
  • yarn add webpack -D
  • yarn add webpack-cli -D

服務端入口

本次升級服務端渲染藉助 @nguniversal 實現

首先將 server.ts 從 src 目錄移動到根路徑,並修改如下

// These are important and needed before anything else
import 'zone.js/dist/zone-node';
import 'reflect-metadata';

import { enableProdMode } from '@angular/core';

import * as express from 'express';
import { join } from 'path';

// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

// Express server
const app = express();

const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist');

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main');

// Express Engine
import { ngExpressEngine } from '@nguniversal/express-engine';
// Import module map for lazy loading
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';

app.engine('html', ngExpressEngine({
  bootstrap: AppServerModuleNgFactory,
  providers: [
    provideModuleMap(LAZY_MODULE_MAP)
  ]
}));

app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'browser'));

// TODO: implement data requests securely
app.get('/api/*', (req, res) => {
  res.status(404).send('data requests are not supported');
});

// Server static files from /browser
app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));

// All regular routes use the Universal engine
app.get('*', (req, res) => {
  res.render('index', { req });
});

// Start up the Node server
app.listen(PORT, () => {
  console.log(`Node server listening on http://localhost:${PORT}`);
});
複製程式碼

此檔案需要 webpack 單獨打包,由於升級到了 webpack 4,原來的 webpack 3.x 語法需要稍作修改

webpack.config.js 更名為 webpack.server.config.js,準確表達打包的目標

const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: { server: './server.ts' },
  resolve: { extensions: ['.js', '.ts'] },
  target: 'node',
  mode: 'none',
  // this makes sure we include node_modules and other 3rd party libraries
  externals: [/node_modules/],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js'
  },
  module: {
    rules: [{ test: /\.ts$/, loader: 'ts-loader' }]
  },
  plugins: [
    // Temporary Fix for issue: https://github.com/angular/angular/issues/11580
    // for 'WARNING Critical dependency: the request of a dependency is an expression'
    new webpack.ContextReplacementPlugin(
      /(.+)?angular(\\|\/)core(.+)?/,
      path.join(__dirname, 'src'), // location of your src
      {} // a map of your routes
    ),
    new webpack.ContextReplacementPlugin(
      /(.+)?express(\\|\/)(.+)?/,
      path.join(__dirname, 'src'),
      {}
    )
  ]
};
複製程式碼

整個服務端入口完成了,下面搞一下服務端打包

服務端打包

服務端渲染專案,大家印象比較深刻的地方就是,客戶端和服務端分別打兩個 bundle,分別供瀏覽器和伺服器執行。

這裡也不例外

src 下面的 main.server.ts 指向了打包入口

export { AppServerModule } from './app/app.server.module';
複製程式碼

看下 src/app/app.server.module 裡面有哪些修改

import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';
...
@NgModule({
  imports: [
    ...
    ModuleMapLoaderModule
  ]
  ...
})
複製程式碼

增加了 ModuleMapLoaderModule 作用是使用模組對映代替原來的模組懶載入,加快 node 環境下的執行速度,整個 bundle 打包下來只有一個 js 檔案 6M 多

客戶端入口

客戶端部分和上一版 Angular 5 的專案差不多,這裡面優化了 module 的拆分,將 router 和 ui 部分抽離到單獨的檔案再引入,使得 app.module 檔案不那麼臃腫。

新增了 console 記錄頁面的渲染環境

export class AppModule {
  constructor(
    @Inject(PLATFORM_ID) private platformId: Object,
    @Inject(APP_ID) private appId: string) {
    const platform = isPlatformBrowser(platformId) ?
      'in the browser' : 'on the server';
    console.log(`Running ${platform} with appId=${appId}`);
  }
}
複製程式碼

配置檔案

程式碼層面的修改,上面介紹的差不多了,接下來是配置檔案,從命名到寫法都是 breaking change

首先根路徑下 .angular-cli.json 更名為 angular.json

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "udao": {
      "root": "",
      "sourceRoot": "src",
      "projectType": "application",
      "prefix": "app",
      "schematics": {},
      "architect": {
        "build": {},
        "serve": {},
        "extract-i18n": {},
        "test": {},
        "lint": {},
        "server": {
          "builder": "@angular-devkit/build-angular:server",
          "options": {
            "outputPath": "dist/server",
            "main": "src/main.server.ts",
            "tsConfig": "src/tsconfig.server.json"
          }
        }
      }
    },
    "udao-e2e": {}
  },
  "defaultProject": "udao"
}
複製程式碼

這個檔案具體怎麼從 angular 5 版本遷移過來的,因為沒有遷移文件說明,乾脆用最新的 cli 新建一個專案,把對應的值和入口替換,上面經過精簡的 json 關鍵是 architect 裡的 server,作用是指明服務端構建的工具,入口,出口,配置項。

一眼看上去與原來的配置檔案相比,多了一層 projects,也就是支援多專案構建。

PWA 升級

這也是 udao 進階 PWA 的點睛之筆,升級過程更是 angular-cli 本次升級的精華所在。

升級之前把原來所有與 PWA 配置相關的程式碼全部刪除,切忌保留任何相關程式碼,否則會帶來不必要的麻煩,事先最好先解除安裝已有的 @angular/pwa 包,清理完畢後只需要一行程式碼搞定 PWA

ng add @angular/pwa --project *project-name*
複製程式碼

沒錯,專案裡 PWA 相關的程式碼都填充到對應位置了,什麼都不用修改。

這個 PWA 和 SSR 本身有那麼一點衝突,怎麼講呢,兩者同樣是為了加快頁面首屏速度,@angular/pwa 中的 service-worker 擴充套件預設會把 html 檔案快取到本地,這個 html 的內容部分是空的,每次訪問網頁時 service-worker 先進行請求攔截,把空內容頁面丟擲來,資料請求完全發生在前端,而我們希望的 SSR 是首屏請求在 node 端完成,直出完整 html,頁面也不會 loading 和白屏,但是不加 PWA 又不能離線和快取其它資源,好吧,這些細節上的問題可能沒那麼多人關心,當然有更好的解決方案歡迎交流。

語法變化

rxjs 升級到 6.x 引入方式和用法需要調整,專案太大不想調整的話 rxjs 提供了降級相容方案 rxjs-compat,直接 npm 安裝即可。

更新指令碼

既然入口檔案和配置檔案都做了相應的修改,那 npm 的 script 命令也要跟著更新一波了

{
  "scripts": {
    "dev": "ng serve",
    "start": "node dist/server.js",
    "build:ssr": "run-s build:client-and-server-bundles webpack:server",
    "build:client-and-server-bundles": "ng build --prod && ng run udao:server",
    "webpack:server": "webpack --config webpack.server.config.js --progress --colors",
  },
}
複製程式碼

注:到這裡執行 npm run build:ssr 即可整體打包,不可將 build:client-and-server-bundleswebpack:server 調換位置,因為 server.ts 入口檔案中有對打包好的 server bundle 的引用 require('./dist/server/main')

總結

到這裡 udao 小朋友成功的從 Angular 5 成功邁向了 Angular 6,也是本系列的最後一篇終章,總之 Angular 6 也有被歷史淘汰的一天,擁抱變化吧,喜歡玩 Angular 最新版本的歡迎關注一波 Github,這裡並沒有鼓吹大家 fork 和 star,感興趣就隨便看看,也沒達到讓大家作為範例的程度,整體來講版本的更新非常及時,功能的更新非常緩慢。

相關文章