angular服务端渲染

此项目未使用ng-zorro

基本准备

  1. 创建一个angular项目,ng new 项目名
  2. 运行ng add @nguniversal/express-engine --clientProject 项目名
  3. 安装一些服务端模块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' // 这里改成这样
    },
  1. 修改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}`);
});
  1. 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)
    }
}
  1. 修改需要渲染的页面
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']
    }