如果不想查看本文,直接寻找问题的解决方案,请搜索'坑'
原理
服务端渲染即在服务端渲染产生页面之后直接返回到客户端查看
第一次请求网页地址的时候,返回已经在服务端渲染好的静态html文件,上面没有点击事件,键盘事件,和交互js,这段页面用一个ID标注,然后开始在客户端渲染页面,渲染好之后,根据ID替换在服务端渲染的页面,填补了main.js(有可能较大)的下载时间+页面渲染事件的空窗期,使页面在slow3G的情况下依然流畅
优势
- 帮助网络爬虫(SEO)
- 提升在手机和低功耗设备上的性能
- 迅速显示出第一个页面
开发流程
安装依赖
$ npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader ts-loader webpack-cli
复制代码
在app.module.ts中添加
@NgModule({
bootstrap: [AppComponent],
imports: [
// 加上下面这句,appId就是上面提到用于替换的唯一标识
BrowserModule.withServerTransition({appId: 'my-app'}),
...
],
})
export class 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 {}
复制代码
src下创建main.js
export { AppServerModule } from './app/app.server.module';
复制代码
复制ts.app.json为ts.server.json并修改
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"baseUrl": "./",
// 重要
"module": "commonjs",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
],
// 指向上面建立的AppServerModule
"angularCompilerOptions": {
"entryModule": "app/app.server.module#AppServerModule"
}
复制代码
在angular.json中修改配置,打包server
"architect": {
"build": { ... }
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/my-project-server",
"main": "src/main.server.ts",
"tsConfig": "src/tsconfig.server.json"
}
}
}
复制代码
此时 ng run
projectName
:server应该可以得到下面结果
$ ng run my-project:server
Date: T22::Z
Hash: cac7d8e9434007fd8da
Time: ms
chunk {} main.js (main) kB [entry] [rendered]
chunk {} styles.css (styles) bytes [entry] [rendered]
复制代码
注意!坑1:在服务器渲染的时候路径和编译的时候不同,如果在这部报错找不到'src/app/.....'的时候,是你使用了src/的绝对路径,需要全部改为../../的相对位置
设置服务器环境
在根目录下,新建server.ts,并往里面写入
// 这些必须在最前面引入
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { renderModuleFactory } from '@angular/platform-server';
import { enableProdMode } from '@angular/core';
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
import * as express from 'express';
import { join } from 'path';
import { readFileSync } from 'fs';
//坑3:报错document not defined,通过引入domino来解决
const domino = require('domino');
const fs = require('fs');
const path = require('path');
const template = fs.readFileSync('./dist/browser/index.html').toString();
const win = domino.createWindow(template);
const files = fs.readdirSync(`${process.cwd()}/dist/server`);
global['navigator'] = win.navigator;
global['window'] = win;
Object.defineProperty(win.document.body.style, 'transform', {
value: () => {
return {
enumerable: true,
configurable: true
};
},
});
global['document'] = win.document;
global['CSS'] = null;
enableProdMode();
const app = express();
const PORT = process.env.PORT || ;
const DIST_FOLDER = join(process.cwd(), 'dist');
// 这里要根据我们自己的目录来,指向的是浏览器端编译的index.html
const template = readFileSync(join(DIST_FOLDER, 'browser', 'index.html')).toString();
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./server/main');
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'));
// 静态文件
app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));
// angular路由
app.get('*', (req, res) => {
res.render(join(DIST_FOLDER, 'browser', 'index.html'), { req });
});
// api的话写在中间,可以作为一个mock服务器
// 启动
app.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
});
复制代码
打包并在服务器上使用
设置 webpack 配置,以处理 Node Express 的 server.ts 文件,并启动应用服务器。
在应用的根目录下,创建一个 Webpack 配置文件 webpack.server.config.js,它会把 server.ts 及其依赖编译到 dist/server.js 中。
const path = require('path');
const webpack = require('webpack');
// 坑2:用webpack引入后台的nodemodule的时候注意某些server端专用的npm包是要加上commonjs前缀的
var fs = require('fs');
var nodeModules = {};
fs.readdirSync('node_modules')
.filter(function(x) {
return ['.bin'].indexOf(x) === ;
})
.forEach(function(mod) {
if (mod=='redis'||mod=='express'){
nodeModules[mod] = 'commonjs ' + mod;
}
});
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: nodeModules,
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
module: {
rules: [
{ test: /\.ts$/, loader: 'ts-loader' }
]
},
plugins: [
new webpack.ContextReplacementPlugin(
/(.+)?angular(\\|\/)core(.+)?/,
path.join(__dirname, 'src'),
{}
),
new webpack.ContextReplacementPlugin(
/(.+)?express(\\|\/)(.+)?/,
path.join(__dirname, 'src'),
{}
)
]
}
复制代码
现在我们使用
node dist/server.js
应该是可以启动服务的,进入localhost:4000就可以访问到工程
脚本
"scripts": {
"build:ssr": "npm run build:client-and-server-bundles && npm run webpack:server",
"serve:ssr": "node dist/server.js",
"build:client-and-server-bundles": "ng build --prod && ng run my-project:server:production",
"webpack:server": "webpack --config webpack.server.config.js --progress --colors",
...
}
复制代码
执行
npm run build:ssr
之后执行
npm run serve:srr
即可
坑3:报错NotYetImplemented 这个其实是因为你引用了Cookie或者什么之类在server上访问不到的模块,这些模块需要你自己在工程里面进行排查和debug,目前没有更好的解决方法
坑4:报错_angular_common_http__WEBPACK_IMPORTED_MODULE_5__.ɵHttpInterceptingHandler is not a constructor 这个是angular/core版本的问题,需要在package.json中升级angular/core就可以解决,参考:stackoverflow.com/questions/5…
转载于:https://juejin.im/post/5cb831f7f265da038e54a35a