Angular 服務端渲染
服務端渲染 3 大好處
- SEO 即搜索引擎優化,方便爬蟲抓取網頁。
- 提高應用在移動端和低性能設備上的體驗
- 更快的展示頁面。
工具包
angular 的服務端面渲染依賴於 platform-server 這個包,它裡面封裝了一些和 DOM 交互,發送 XMLHttpRequest 請求,以及一些在低性能設備上不支持的工具方法。我們將使用它在服務端渲染出整個應用。
在服務端代碼中我們需要這個包中的 renderModuleFactory()方法。這個方法接收一個 HTML 模板作為輸入,通常是 index.html,一個 angular 模塊以及一個路由地址。每一個從客戶端發送上來的路由信息都會被映射成一個靜態頁面,當客戶端請求時,由 renderModuleFactor()方法負責渲染相應的視圖,最終把靜態頁面發送給客戶端。
開發流程
- 安裝依賴。
- 更新angular前端代碼。
- 在angular.json中修改構建配置。
- 使用Nodejs建立一個服務端。
- 服務端的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還是非常簡單的,關鍵在於理解和把握以下幾點:
- 打包後的服務端代碼中包含了整個項目的所有代碼和功能,因此在服務端運行時,除了環境導致的API使用差異外,其它過程都是相同的,因此一些依賴包服務端也必不可少。
- 服務端執行渲染的核心方法是:renderModuleFactory,它根據客戶端請求上來的url和打包好的前端代碼生成靜態頁面。
- 對於服務端渲染的項目,編碼過程中要盡量避免一些全局對象及環境依賴。
這是目前為止我讀過的寫最好的關於服務端渲染的文章 請自備梯子。
Angular完全開發手冊推薦閱讀: