Chaiwut Sittiboonta
May 2, 2018 · 4 min read

ความสามารถของ search engine รวมไปถึง Social media ในปัจจุบันไม่ได้แสดงเพียงแค่ลิ้งค์ แต่ยังดึงเอารายละเอียด รวมไปถึงรูปภาพแสดงออกมาให้ผู้ใช้งานเห็นก่อนตัดสินใจเข้าไปยังเว็บไซต์


Angular เป็น Single Page Application (SPA) framework ที่ render ในบราวเซอร์ หรือที่เราเรียกว่า client-side rendering ทำให้ search engine และ social media ไปดึงข้อมูล HTML ที่ยังไม่ได้ผ่านการ render ออกมาแสดง

Angular Universal จะทำหน้าที่เป็น server-side renderer เพื่อให้ search engine และ social media ได้ดึงเอา HTML ที่ถูก render ไว้ก่อนแล้วไปใช้งาน เพื่อการแสดงผลที่ถูกต้อง

เริ่มจากการติดตั้ง Angular CLI ลงในเครื่อง

npm install -g @angular/cli@latest

สร้างโปรเจคใหม่พร้อมใช้งาน sass ในโปรดเจคด้วย ดูเพิ่มเติม : https://stackoverflow.com/a/39816365/2045817

ng new --style=scss demo-universal
cd demo-universal

จากนั้น install ts-loader angular/platform-server และ @nguniversal/module-map-ngfactory-loader

npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader ts-loader

แก้ไขไฟล์ app.mudule.tsโดยเพิ่ม withServerTransition() เพื่อให้แอพของเราทำงานร่วมกับ Universal

@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule.withServerTransition({appId: 'demo-universal'}),
],
providers: [],
bootstrap: [AppComponent]
})

ขั้นตอนต่อไปสร้างไฟล์ module เพื่อเรียกใช้บนฝั่ง server ใน src/app/app.server.module.ts เพื่อจะเรียกใช้ AppModule ผ่านทาง ServerMudule

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 {}

ต่อไปสร้างไฟล์สำหรับ Universal bundle เอาไว้ export AppServerModule ใช้ชื่อว่า src/main.server.ts

export { AppServerModule } from './app/app.server.module';

เสร็จแล้วให้เรากลับไปดูที่ไฟล์ tsconfig.app.json คัดลอกโค๊ดทั้งหมด แล้วสร้างไฟล์ tsconfid.server.json วางโค๊ดที่คัดลอกมาจาก tsconfig.app.json ให้เราเปลี่ยน module format จาก es2015 เป็น commonjs

{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"baseUrl": "./",
"module": "commonjs",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
]
}

เพิ่ม extra property เพื่อบอกให้ compile ไฟล์ app.server.module สำหรับรายละเอียดเพิ่มเติมสามารถอ่านได้ที่ https://github.com/UltimateAngular/aot-loader/wiki/tsconfig.json

{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"baseUrl": "./",
"module": "commonjs",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
],
"angularCompilerOptions": {
"entryModule": "app/app.server.module#AppServerModule"
}
}

กลับไปที่ไฟล์ angular.json ใน property ที่ชื่อว่า architect เราจะเพิ่ม property ใหม่เข้าไป เพื่อกำหนดค่าการ build สำหรับ server

"architect": {
...
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/server",
"main": "src/main.server.ts",
"tsConfig": "src/tsconfig.server.json"
}
}
}

จากนั้นให้เราลอง build project ของเราได้เลยครับ

$ ng run demo-universal:server

Setting up an Express Server

เราสร้างแอพ ตั้งค่าต่างๆ รวมไปถึง build ทุกอย่างผ่านได้แล้ว ขั้นตอนสุดท้ายเราจะ Run ได้ยังไง? เราจะใช้ Express.js สำหรับ run Universal bundle ของเรา

สร้างไฟล์ server.ts ไว้ใน root ของแอพ

import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import {enableProdMode} from '@angular/core';
// Express Engine
import {ngExpressEngine} from '@nguniversal/express-engine';
// Import module map for lazy loading
import {provideModuleMap} from '@nguniversal/module-map-ngfactory-loader';

import * as express from 'express';
import {join} from 'path';

// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

// Express server
const app = express();

const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist');

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main');

// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [
provideModuleMap(LAZY_MODULE_MAP)
]
}));

app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'browser'));

// Example Express Rest API endpoints
// app.get('/api/**', (req, res) => { });

// Server static files from /browser
app.get('*.*', express.static(join(DIST_FOLDER, 'browser'), {
maxAge: '1y'
}));

// All regular routes use the Universal engine
app.get('*', (req, res) => {
res.render('index', { req });
});

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

ถึงตรงนี้เราได้ตั้งค่าสำหรับใช้ Node Expreess ไว้แล้ว ขั้นตอนต่อไป เป็นการตั้งค่า Webpack เพื่อบอกให้ Webpack ใช้ไฟล์ server.ts สำหรับ serve แอพของเรา ให้สร้างไฟล์ชื่อ webpack.server.config.js ใน root ของแอพ

// Work around for https://github.com/angular/angular-cli/issues/7200

const path = require('path');
const webpack = require('webpack');

module.exports = {
mode: 'none',
entry: {
// This is our Express server for Dynamic universal
server: './server.ts',
},
target: 'node',
resolve: { extensions: ['.ts', '.js'] },
// Make sure we include all node_modules etc
externals: [/node_modules/],
output: {
// Puts the output at the root of the dist folder
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
module: {
rules: [
{ test: /\.ts$/, loader: 'ts-loader' }
]
},
plugins: [
new webpack.ContextReplacementPlugin(
// fixes WARNING Critical dependency: the request of a dependency is an expression
/(.+)?angular(\\|\/)core(.+)?/,
path.join(__dirname, 'src'), // location of your src
{} // a map of your routes
),
new webpack.ContextReplacementPlugin(
// fixes WARNING Critical dependency: the request of a dependency is an expression
/(.+)?express(\\|\/)(.+)?/,
path.join(__dirname, 'src'),
{}
)
]
}

กลับไปที่ไฟล์ package.json ใน property scripts เพิ่มค่าเข้าไป

"scripts": {
"build:universal": "npm run build:client-and-server-bundles && npm run webpack:server",
"serve:universal": "node dist/server.js",
"build:client-and-server-bundles": "ng build --prod && ng run demo-universal:server",
"webpack:server": "webpack --config webpack.server.config.js --progress --colors"
}

จากนั้นใช้คำสั่ง

$ npm run build:universal && npm run serve:universal

Perfect!

หมายเหตุ:

ขั้นตอนการใช้ Express Server นั้นเป็นการใช้เพื่อประกอบบทความ และใช้เป็นตัวอย่าง ควรจะตั้งค่าการเข้าถึง และความปลอดภัยหากนำไปใช้กับโปรเจคจริงๆ

จากการใช้งานจริงอาจจะมองหา framework เช่น PM2 (http://pm2.keymetrics.io/) เป็นต้น

ช่วยเหลือ

หากเราใช้ Algular CLI ที่มาพร้อมกับ Webpack 4 อาจจะเกิดปัญหา build ไม่ผ่าน ให้เปลี่ยน tsloader กลับไปเป็นเวอร์ชั่น 4.2.0

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade