服務端渲染 3 大好處

  1. SEO 即搜索引擎優化,方便爬蟲抓取網頁。
  2. 提高應用在移動端和低性能設備上的體驗
  3. 更快的展示頁面。

工具包

angular 的服務端面渲染依賴於 platform-server 這個包,它裡面封裝了一些和 DOM 交互,發送 XMLHttpRequest 請求,以及一些在低性能設備上不支持的工具方法。我們將使用它在服務端渲染出整個應用。

在服務端代碼中我們需要這個包中的 renderModuleFactory()方法。這個方法接收一個 HTML 模板作為輸入,通常是 index.html,一個 angular 模塊以及一個路由地址。每一個從客戶端發送上來的路由信息都會被映射成一個靜態頁面,當客戶端請求時,由 renderModuleFactor()方法負責渲染相應的視圖,最終把靜態頁面發送給客戶端。

開發流程

  1. 安裝依賴。
  2. 更新angular前端代碼。
  3. 在angular.json中修改構建配置。
  4. 使用Nodejs建立一個服務端。
  5. 服務端的webpack配置。

第4,5步實際需要根據具體的項目來設置,這裡僅是為了演示。服務端渲染可以用在任何語言開發的伺服器上,只需要伺服器能夠調用這個包中的 renderModuleFactory()方法即可。

開始

安裝依賴

$ npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader ts-loader
// 回顯

第一個包前面已經介紹過了。@nguniversal/module-map-ngfactory-loader 用來處理懶載入的內容,ts-loader 用來協助 webpack 進行構建。

更新angular前端代碼

對需要服務端渲染的應用,我們需要進行以下改造:

1.Angular Universal 支持

在應用的根模塊,通常是 app.module.ts 中添加以下配置

@NgModule({
bootstrap: [AppComponent],
imports: [
BrowserModule.withServerTransition({appId: may-app}), // 打包時的id,只要保證和其它配置不重複即可。
...
],

})
export class AppModule {}

2.創建 server root 模塊

這個模塊作為在服務端運行時的根模塊,這裡直接引入原先應用的根模塊 AppModule,然後添加一些服務端渲染所需的模塊。這裡我們創建一個名為 app.server.module.ts 的文件,命名上就可以看出它是服務端渲染時的根模塊。

import { NgModule } from @angular/core;
import { ServerModule } from @angular/platform-server;
import { ModuleMapLoaderModule } from @nguniversal/module-map-ngfactory-loader;

import { AppModule } from ./app.module;
import { AppComponent } from ./app.component;

@NgModule({
imports: [
AppModule,
ServerModule,
ModuleMapLoaderModule // 如需處理懶載入模塊,切記引入它!
],
bootstrap: [AppComponent] // 此模塊需要明確知道從哪個組件啟動
})
export class AppServerModule {}

3.添加入口文件,暴露出 server root 模塊

在src/目錄下創建 main.server.ts 的入口文件。

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

4.配置 AppServerModule 文件

創建一個 tsconfig.server.json 文件,直接把 tsconfig.app.json的內容複製過來,但是還更改一些配置。

{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"baseUrl": "./",
"module": "commonjs", // 此處有變化
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
],
// 新增下面的配置項,path#moduleName 的寫法相信你已經見過了
"angularCompilerOptions": {
"entryModule": "app/app.server.module#AppServerModule"
}
}

在angular.json中修改構建配置

在angular.json中的architect增加寫的新的打包配置,這裡我們使用 server 作為打包的名稱, 同時為了便於管理,把之前build時的輸出路徑修改成browser。

"architect": {
"build": {
...
"options": {
...
"outputPath": "dist/browser",
}
},
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/server",
"main": "src/main.server.ts",
"tsConfig": "src/tsconfig.server.json"
}
}
}

輸出後的dist目錄結構

dist/

----browser/

----server/

使用ng run命令進行打包,打包名稱格式為 項目名稱:打包目標,假設項目名稱為my-project,構建剛設置好的server這個包,在命令行輸入:

$ ng build --prod && ng run my-project:server
// 打包過程回顯

執行完成後,dist/browser目錄下的文件就是正常的打包文件 , 和不使用SSR時是一模一樣的,dist/server下的文件就是服務端渲染所需要的代碼,你寫的所有代碼都會被壓縮到它裡面。

使用Nodejs建立一個服務端

把預編譯好的AppServerModule 傳入到 PlatformServer 的 renderModuleFactory() 方法中,它會幫助我們初始化應用,將結果返回給客戶端。其中,AppServerModuleNgFactory,provideModuleMap和LAZY_MODULE_MAP 是經過 wepack 打包後 通過 require 進來的參數或方法,renderModuleFactory需要從@angular/platform-server的包中import進來

app.engine(html, (_, options, callback) => {
renderModuleFactory(AppServerModuleNgFactory, {
document: template, // index.html
url: options.req.url,
// 懶載入模塊配置
extraProviders: [
provideModuleMap(LAZY_MODULE_MAP)
]
}).then(html => {
callback(null, html);
});
});

在使用 express 框架的nodejs中,還可以使用更加便捷的方法

安裝依賴

$ npm install --save @nguniversal/express-engine

示例代碼

import { ngExpressEngine } from @nguniversal/express-engine;

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

基於nodejs,express的完整示例代碼

// 下面2行必須首先引入
import zone.js/dist/zone-node;
import reflect-metadata;

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();

// Express server
const app = express();

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

// 生成 index.html
const template = readFileSync(join(DIST_FOLDER, browser, index.html)).toString();

// *注意, 這裡通過require引入,因為這是通過webpack動態生成的。
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require(./dist/server/main.bundle);

const { provideModuleMap } = require(@nguniversal/module-map-ngfactory-loader);

app.engine(html, (_, options, callback) => {
renderModuleFactory(AppServerModuleNgFactory, {
document: template,
url: options.req.url,
extraProviders: [
provideModuleMap(LAZY_MODULE_MAP)
]
}).then(html => {
callback(null, html);
});
});

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

// /browser 目錄下的靜態文件
app.get(*.*, express.static(join(DIST_FOLDER, browser)));

// Universal engine 通用路由
app.get(*, (req, res) => {
res.render(join(DIST_FOLDER, browser, index.html), { req });
});

// 啟動node伺服器
app.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
});

服務端的webpack配置

在後台應用根目錄新建一個webpack配置以便它可以處理 server.ts(nodejs 應用的啟動文件,以項目中的為準),例如 webpack.server.config.js。

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

module.exports = {
entry: { server: ./server.ts },
resolve: { extensions: [.js, .ts] },
target: node,
// this makes sure we include node_modules and other 3rd party libraries
externals: [/(node_modules|main..*.js)/],
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),
{}
)
]
}

後台啟動應用

node dist/server.js

我們可以在package.json中添加增加一些輔助命令,以幫助我們執行以上指令

"scripts": {
// Common scripts
"build:ssr": "npm run build:client-and-server-bundles && npm run webpack:server",
"serve:ssr": "node dist/server.js",

// Helpers for the scripts
"build:client-and-server-bundles": "ng build --prod && ng run yourProjectName:server",
"webpack:server": "webpack --config webpack.server.config.js --progress --colors"
}
...

本地系統中執行以下命令:

npm run build:ssr && npm run serve:ssr

瀏覽器介面處理

在服務端執行應用代碼時,是無法引用到瀏覽器環境中的一些介面的,例如:document,navigator,location對象等,可以通過邏輯判斷來迴避這些對象的使用,也可以通過 angular 提供的一些可注入服務,在它們身上找到可以替代瀏覽器原始對象上的方法,如果angular沒有提供的話,你可能需要自己抽象出一些方法。

1.通過邏輯判斷

import { isPlatformBrowser } from @angular/common;
import { Inject, PLATFORM_ID, Component, OnInit } from @angular/core;

@Component({...})
export class MyComponent implement {
constructor(@Inject(PLATFORM_ID) private platformId: Object) { }

ngOnInit() {
if (isPlatformBrowser(platformId)) {
....
}
}
}

2.使用angular 提供依賴取代全局對象,例如使用Location 依賴取代瀏覽器的全局對象location:

import { Location } from @angular/common;
import { Compnoent } from @angular/core;

@Componet({})
export class MyComponent {
constructor(private location: Location) { }
}

3.在服務端mock出瀏覽器環境中的一些全局對象,如window, location,history,localStorage等,掛載到node的global對象上。比較常用的庫有 domino,mock-browser等,使用方法也很簡單。

總結

總體來說對於angular項目,要實現seo還是非常簡單的,關鍵在於理解和把握以下幾點:

  1. 打包後的服務端代碼中包含了整個項目的所有代碼和功能,因此在服務端運行時,除了環境導致的API使用差異外,其它過程都是相同的,因此一些依賴包服務端也必不可少。
  2. 服務端執行渲染的核心方法是:renderModuleFactory,它根據客戶端請求上來的url和打包好的前端代碼生成靜態頁面。
  3. 對於服務端渲染的項目,編碼過程中要盡量避免一些全局對象及環境依賴。

這是目前為止我讀過的寫最好的關於服務端渲染的文章 請自備梯子。

Angular完全開發手冊?

www.hijavascript.com
圖標

推薦閱讀:
相关文章