此项目未使用ng-zorro
基本准备
- 创建一个
angular
项目,ng new 项目名
- 运行
ng add @nguniversal/express-engine --clientProject 项目名
- 安装一些服务端模块
compression(页面压缩),http-proxy-middleware(http代理),multer(文件上传)
服务端准备
平时开发过程中,我们仍然使用之前的模式,自备本地server
1. 新建文件夹server
,该server
主要用来存放服务端(类似以前的server.ts和fileUtils.ts)相关的文件
2. 将server.ts文件移动到server文件夹中,修改webpack.server.config.js
文件
entry: {
server: './server/server.ts' // 这里改成这样
},
- 修改
server.ts
文件并增加fileUtils.ts
文件
import 'zone.js/dist/zone-node';
//新增
import { config } from './config/config.js'
import * as express from 'express';
//新增
import { createProxyMiddleware } from 'http-proxy-middleware'
import { join } from 'path';
import { APP_BASE_HREF } from '@angular/common';
//新增
import fileUtils from './fileUtils';
//新增
import * as multer from 'multer'
//新增
import * as compression from 'compression'
const app = express();
//新增
app.use(compression())
// 修改
const PORT = config.port;
// 修改
const DIST_FOLDER = join(process.cwd(), 'dist/dist');
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
const { AppServerModuleNgFactory, LAZY_MODULE_MAP, ngExpressEngine, provideModuleMap } = require('./dist/server/main');
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [
provideModuleMap(LAZY_MODULE_MAP)
]
}));
app.set('view engine', 'html');
app.set('views', DIST_FOLDER);
//新增
var proxyConfig = createProxyMiddleware({
target: config.proxy.target,
changeOrigin: true,
pathRewrite: {
'^/api': ''
},
onProxyReq: function onProxyReq(proxyReq, req, res) {
}
});
//新增
app.use('/api', proxyConfig)
app.get('*.*', express.static(DIST_FOLDER, {
maxAge: '1y'
}));
let uploadSingle = multer({
dest: 'upload/'
});
//新增
app.post('/upload', uploadSingle.single('file'), function (req, res) {
// 这里是文件上传代码 具体可以查看详细文件
});
app.get('*', (req, res) => {
res.render('index', { req });
});
app.listen(PORT, () => {
console.log(`Node Express server listening on http://localhost:${PORT}`);
});
server
中的config
也做了一定的修改,请仔细查看
服务端请求拦截
主要是针对需要渲染数据的页面,需要服务器渲染的使用绝对路径(这里统一配置),详细说明,官方已指出,地址运行在服务端时,使用绝对URL发起请求,在浏览器中使用相对URL
1. 新建universal-interceptor.ts
文件
import { Injectable, Inject, Optional } from '@angular/core'
import { HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http'
import { Request } from 'express'
import { REQUEST } from '@nguniversal/express-engine/tokens'
import { config } from '../../server/config/config.js'
@Injectable()
export class UniversalInterceptor implements HttpInterceptor {
constructor(@Optional() @Inject(REQUEST) protected request: Request) { }
// 服务器渲染时 请求拦截,在这里组装为绝对路径
// 这里也可以对路径做处理,然后api.service可以不做处理
intercept(req: HttpRequest<any>, next: HttpHandler) {
let serverReq: HttpRequest<any> = req;
if (this.request) {
// 这里的config和server的config是用同一个的,获取接口的origin部分
let newUrl = config.proxy.target
if (!req.url.startsWith('/')) {
newUrl += '/'
}
newUrl += req.url;
serverReq = req.clone({ url: newUrl })
}
return next.handle(serverReq)
}
}
2. `app.server.module.ts`中引入`universal-interceptor`文件
浏览器端渲染数据
主要需要使用
TransferState
,ServerTransferStateModule
,BrowserTransferStateModule
1. 在app.module.ts
中引入BrowserTransferStateModule
2. 在app.server.module.ts
中引入ServerTransferStateModule
3. 在需要服务端渲染的接口中做判断
import { Injectable, PLATFORM_ID, Inject } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common'
import { HttpService } from './http.service'
@Injectable()
export class ApiService {
//prefix = '/api/v1'
prefix = '/api/mock/5e9575d592cee10f807aabe6/test'
prefix_render = '/mock/5e9575d592cee10f807aabe6/test'
constructor(private http: HttpService, @Inject(PLATFORM_ID) private platformId) { }
public getIndexData(params) {
// 在这里判断当前环境是否是浏览器端
let url = isPlatformBrowser(this.platformId) ? `${this.prefix}/test` : `${this.prefix_render}/test`
return this.http.get(url, params)
}
public upload(params) {
let url = '/upload'
return this.http.upload(url, params)
}
}
- 修改需要渲染的页面
async getData() {
// 获取存储在TransferState中key为INDEX_DATA的值
const kfcList: any[] = this.state.get(INDEX_DATA, null as any);
let res;
// 该判断主要是为了处理请求两次的问题
if (!kfcList) {
res = await this.api.getIndexData({ page: this.page.pageNow, pageSize: this.page.pageSize })
this.list = res['list']
this.page.total = res['total']
// 获取到值后将值存储到state中
this.state.set(INDEX_DATA, res)
} else {
// 假如已经请求过了,则将值取出来
res = this.state.get(INDEX_DATA, { list: [], total: 1 })
this.list = res.list
}
// 判断当前是否是在浏览器环境下,如果是的话计算页码
if (isPlatformBrowser(this.platformId)) {
this.calcPage(res.total) // 计算页码
}
}
// 这个请求主要是分页请求,是浏览器请求,不适用上面那个方法,所以另外新定义了
async getDataByPage(pageNow) {
this.page.pageNow = pageNow
let res = await this.api.getIndexData({ page: pageNow, pageSize: this.page.pageSize })
this.list = res['list']
}