Angular Universal(SSR)入门

508 阅读2分钟

1.Angular Universal教程

1.1 为项目添加SSR支持

执行下面的命令为项目添加SSR支持:

ng add @nguniversal/express-engine

1.2 在服务端使用绝对URL进行HTTP(数据)请求

在服务端渲染的应用中,HTTP URL 必须是绝对的(比如,https://my-server.com/api/heroes)。这意味着当在服务器上运行时,URL 必须以某种方式转换为绝对 URL,而在浏览器中运行时,它们是相对 URL。

1.3 使用浏览器API

1.3.1 提供服务端访问的浏览器API对象

服务端代码如果使用到了浏览器API,需要在全局变量中提供相应的Mock对象,在server.ts中提供,

// server.ts 
const distFolder = join(process.cwd(), 'dist/<your_project_name>/browser');
const template = readFileSync(join(distFolder, 'index.html')).toString();
const win = domino.createWindow(template);

global['window'] = win; 
global['document'] = win.document; 
global['location'] = win.location; 
global['navigator'] = win.navigator;

// 如果引入了其他第三方模块,Mock对象需要在AppServerModule前面声明 
import import { AppServerModule } from './src/main.server';

在tsconfig.server.json中更换模块格式,解决打包报错问题

{ 
    "extends": "./tsconfig.app.json", 
    "compilerOptions": { 
        // fix 打包问题 
        "module": "commonjs", 
    }, 
 }

1.3.2 根据环境选择性执行代码

export class SomeService { 	
    constructor( 		
        @Inject(PLATFORM_ID) private platformId: string, 	
    ) { 		
        if(isPlatformBrowser(this.platformId)) { 			
            // 只在浏览器端执行 		
        } 	
    } 
 }

1.4 组件在初始化时执行异步操作

UI组件在constructorngOnInit等生命周期函数中发起异步操作时,需要确保Observable的next回调会被执行,否则有可能会阻塞服务器渲染。

// good example 
export class GoodComponent{  	
    constructor(private someService: SomeService,) { 
        this.getData(); 	
    }  	
    
    getData(): void { 		
        this.someService.getData()
            .subscribe(res => { 				
                // do something 	
            }) 	
    } 
}  

// bad example 
export class BadComponent { 	
    isLogin = false;  	
    constructor() { 		
        this.getData(); 	
    }  	
    
    getData() {
        // isLogin 为false,Observable next回调永远不会执行	
        timer(10000).pipe(
            filter(() => this.isLogin))
        .subscribe(() => { 			
            // do something 		
        }) 	
   }  
}

如果没有国际化的需求,可以跳过下面的内容。

1.5 服务端生成多语种build

执行下面的命令生成多语种build

ng build --localize && ng run <your_project_name>:server --localize

1.5.1 server.ts适配多语种build

需要对server.ts改造如下:

export function app(locale: string): express.Express {
  const server = express();
  const distFolder = join(process.cwd(), 'dist/<your_project_name>/browser', locale);
  const indexHtml = existsSync(join(distFolder, 'index.original.html'))
    ? 'index.original.html'
    : 'index';

  // Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
  server.engine('html', ngExpressEngine({
    bootstrap: AppServerModule,
    providers: [
      { provide: LOCALE_ID, useValue: locale },
    ],
  }));

  server.set('view engine', 'html');
  server.set('views', distFolder);

  // Example Express Rest API endpoints
  // server.get('/api/**', (req, res) => { });
  // Serve static files from /browser
  server.get('*.*', express.static(distFolder, {
    maxAge: '1y'
  }));

  // All regular routes use the Universal engine
  server.get('*', (req, res) => {
    res.render(indexHtml, {
      req,
      providers: [
        { provide: APP_BASE_HREF, useValue: req.baseUrl },
      ]
    });
  });

  return server;
}

function run(): void {
  const server = app('');
  const port = process.env['PORT'] || 4000;

  // Start up the Node server
  server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port}`);
  });
}

// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
// only run in dev mode
if ((moduleFilename === __filename && isDev) ||
  moduleFilename.includes('iisnode')
) {
  run();
}

export * from './src/main.server';

1.5.2 通过express分发多个语种

创建一个proxy-server.js文件,通过express分发不同语种的build,仅在生产环境使用

const express = require("express"); 
const path = require("path");  
// 获取对应语种的build 
const getLocalizedServer = (locale) => { 	
    const distFolder = path.join(process.cwd(), `dist/<your_project_name>/server/${locale}`);
    const server = require(`${distFolder}/main.js`); 	
    return server.app(locale); 
};  
function run() { 	
    const appZh = getLocalizedServer("zh");     
    const appEn = getLocalizedServer("en"); 
    const server = express(); 	
    server.use("/zh", appZh); 	
    server.use("", appEn);  	
    server.listen(4200, () => {
        console.log(`Node Express server listening on http://localhost:4200`); 	
    }); 
}  

run();

2 参考资料