天天看点

Angular6 服务端渲染

如果不想查看本文,直接寻找问题的解决方案,请搜索'坑'

原理

服务端渲染即在服务端渲染产生页面之后直接返回到客户端查看

第一次请求网页地址的时候,返回已经在服务端渲染好的静态html文件,上面没有点击事件,键盘事件,和交互js,这段页面用一个ID标注,然后开始在客户端渲染页面,渲染好之后,根据ID替换在服务端渲染的页面,填补了main.js(有可能较大)的下载时间+页面渲染事件的空窗期,使页面在slow3G的情况下依然流畅

优势

  1. 帮助网络爬虫(SEO)
  2. 提升在手机和低功耗设备上的性能
  3. 迅速显示出第一个页面

开发流程

安装依赖

$ 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

上一篇: Es6的那些事

继续阅读