سلام، Server-Side Rendering (SSR) در منظر توسعه وب مدرن بیشتر از یک کلمه متداول است. این نقش اساسی در بهبود عملکرد یک برنامه وب و بهینهسازی موتور جستجو (SEO) دارد. اگر از Angular، چارچوب محبوب گوگل برای ایجاد برنامههای مشتری-سمت استفاده میکنید، شانس خوبی دارید! Angular دارای پشتیبانی داخلی برای SSR از طریق Angular Universal است. بیایید به جزئیات فنی پیادهسازی رندرینگ سمت سرور در برنامههای Angular پرداخته و ببینیم که Angular Universal چیست؟
آنگولار یونیورسال چیست؟
آنگولار یونیورسال مشابه یک ابزار چاقوی سوئیسی برای اجرای برنامههای آنگولار شما در سرور است. این ابزار تمام تنظیمات و رابطهای مورد نیاز برای رندرینگ سمت سرور (SSR) را گروهبندی میکند و به طور قابل توجهی فرآیند ایجاد برنامههای وب با عملکرد بالا و مناسب برای موتورهای جستجویی و سئو را سادهتر میکند. حتی اگر برنامهی آنگولار موجودی داشته باشید، میتوانید با استفاده از چند دستور CLI ساده به آنگولار یونیورسال اضافه کنید.
چرا نیاز به رندرینگ سمت سرور داریم؟
مزایای سئو
هرچند موتور جستجوی گوگل میتواند صفحات وب مبتنی بر جاوااسکریپت را جستجو کرده و نمایهگذاری کند، اما موتورهای جستجوی دیگر ممکن است در این زمینه عقبافتاده باشند. استفاده از رندرینگ سمت سرور قابلیتهای سئوی برنامهی شما را با تولید محتوای ضروری در سرور بهبود میبخشد و امکان پیدا شدن آسانتر توسط رباتهای موتورهای جستجو را فراهم میکند.
سریعترین نمایش محتوای اولیه (FCP)
رندرینگ سمت سرور به سرور اجازه میدهد تا مارکاپ HTML اولیه را تولید کند. به عبارت دیگر، کاربران سریعتر یک رابط کاربری پر شده را مشاهده میکنند که اثر مثبتی بر بقای کاربران و اشتیاق آنها دارد.
بارگذاری بهینهسازی شده سمت مشتری
رندرینگ سمت سرور برخی از وظایف محاسباتی را از سمت مشتری به سمت سرور منتقل میکند، که برنامههای وب شما را برای کاربرانی با دستگاههای ضعیف یا شرایط شبکه کمسرعت قابل دسترسی تر میکند.
ایجاد کردن پروژه جدید
برای ایجاد یک پروژه Angular جدید:
ng new project-name
در داخل این پروژه، بستههای زیر را دانلود کنید و Angular Universal را اضافه کنید:
perlCopy codecd project-name npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader ts-loader@3.5.0 express ng generate universal project-name
این دستورات فایلهای زیر را ایجاد و بهروز میکنند:
src/app/app.server.module.ts
src/main.server.ts
src/tsconfig.server.json
package.json
.angular-cli.json
src/main.ts
src/app/app.module.ts
.gitignore
ماژولها: اکنون دو ماژول ریشه جداگانه دارید: app.server.module.ts
و app.module.ts
. ماژول سرور، ServerModule
را از بسته @angular/platform-server
وارد میکند. ماژول مرورگر، متد withServerTransition()
از BrowserModule
را فراخوانی میکند که به Angular اطلاع میدهد ما از رندر سمت سرور استفاده میکنیم و باید نمایش تا زمانی که چارچوب کامل بارگذاری شود تعویض شود.
نقطه ورود: شما همچنین دو نقطه ورود برای برنامه دارید: src/main.ts
و src/main.server.ts
. این آخری نقطه ورود برای سرور است و به سادگی ماژول سرور ما را صدا میزند.
فایلهای پیکربندی: برای اعلام به کامپایلر Angular که دو ماژول ورود داریم، فایل tsconfig.server.json
ایجاد میشود. tsconfig.app.json
نیز برنامه مرورگر را کامپایل میکند.
Angular CLI: در فایل angular-cli.json
، یک پروفایل دوم برای مجموعه سرور اضافه میشود.
بوتاستراپ: فایل main.ts
شما با تابع زیر بهروزرسانی میشود:
javascriptCopy codedocument.addEventListener('DOMContentLoaded', () => { platformBrowserDynamic().bootstrapModule(AppModule) .catch(err => console.log(err)); });
این کار برای اطمینان از اینکه برنامه Angular پس از بارگذاری DOM بوتاستراپ میشود انجام میشود. منطق بوتاستراپ برنامه در داخل رویداد DOMContentLoaded
قرار داده میشود.
Node Server
بعداً، شما باید یک سرور در دایرکتوری اصلی برنامه ایجاد کنید. این فایل از فایل جاوااسکریپتی استفاده میکند که با اجرای دستور npm run build:ssr با استفاده از برنامه سروری که در فایل .angular-cli.json تنظیم شده است تولید شده است. سپس به صفحه index.html اعمال میشود. یک فایل server.ts در دایرکتوری اصلی پروژه خود ایجاد کنید و کد زیر را اضافه کنید:
// These are important and needed before anything else import 'zone.js/dist/zone-node'; import 'reflect-metadata'; import { renderModuleFactory } from '@angular/platform-server'; import { enableProdMode } from '@angular/core'; import * as express from 'express'; import { join } from 'path'; import { readFileSync } from 'fs'; // Faster server renders w/ Prod mode (dev mode never needed) enableProdMode(); // Express server const app = express(); const PORT = process.env.PORT || 4201; const DIST_FOLDER = join(process.cwd(), 'dist'); // Our index.html we'll use as our template const template = readFileSync(join(DIST_FOLDER, 'index.html')).toString(); // * NOTE :: leave this as require() since this file is built Dynamically from webpack const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist-server/main.bundle'); const { provideModuleMap } = require('@nguniversal/module-map-ngfactory-loader'); app.engine('html', (_, options, callback) => { renderModuleFactory(AppServerModuleNgFactory, { // Our index.html document: template, url: options.req.url, // DI so that we can get lazy-loading to work differently (since we need it to just instantly render it) extraProviders: [ provideModuleMap(LAZY_MODULE_MAP) ] }).then(html => { callback(null, html); }); }); app.set('view engine', 'html'); app.set('views', DIST_FOLDER); // Server static files from dist folder app.get('*.*', express.static(DIST_FOLDER)); // 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 server listening on http://localhost:${PORT}`); });
فایل server.ts به یک تنظیمات webpack نیاز دارد تا فایل جاوااسکریپتی را تولید کند که در سرور اجرا شود. یک فایل به نام webpack.server.config.js در دایرکتوری اصلی برنامهی خود ایجاد کنید و کد زیر را به آن اضافه کنید:
const path = require('path'); const webpack = require('webpack'); module.exports = { entry: { server: './server.ts' }, resolve: { extensions: ['.ts', '.js'] }, target: 'node', // this makes sure we include node_modules and other 3rd party libraries externals: [/(node_modules|main\..*\.js)/], output: { path: path.join(__dirname, 'dist'), filename: '[name].js' }, module: { rules: [ { test: /\.ts$/, loader: 'ts-loader' } ] }, plugins: [ // Temporary Fix for issue: https://github.com/angular/angular/issues/11580 // for "WARNING Critical dependency: the request of a dependency is an expression" new webpack.ContextReplacementPlugin( /(.+)?angular(\\|\/)core(.+)?/, path.join(__dirname, 'src'), // location of your src {} // a map of your routes ), new webpack.ContextReplacementPlugin( /(.+)?express(\\|\/)(.+)?/, path.join(__dirname, 'src') ) ] };
به فایل package.json خود، دستورات زیر را به آرایهی scripts اضافه کنید:
اول npm run build:ssr را اجرا کنید و وقتی که انجام شد، npm run serve:ssr را اجرا کنید. برنامهی شما باید روی localhost:4201 اجرا شود.
انتقال وضعیت (Transfer State)
زمانی که از Angular Universal استفاده میکنیم، API که محتوا را ارائه میدهد دو بار ترکیب میشود. اولین بار زمانی که سرور صفحه را پردازش میکند و دومین بار زمانی که برنامه راهاندازی میشود. این امر باعث مشکلات تأخیر و تجربهی کاربری نامطلوبی میشود زیرا صفحه معمولاً وقتی این اتفاق میافتد، چشمک میزند. برای مشاهده نمودار زیر، به تصویر زیر نگاه کنید تا ببینید که چگونه کار میکند:
ما میتوانیم از سرویس TransferState برای ارسال اطلاعات از سرور به مشتری استفاده کنیم، که از ایجاد تماسهای API تکراری جلوگیری میکند. مشاهده کنید که این چگونه کار میکند:
در اینجا اطلاعات داده شده در مورد استفاده از سرویس TransferState در برنامهی ما ترجمه شده است:
"بیایید در برنامهی ما از سرویس TransferState استفاده کنیم. در فایل app.module.ts، ماژول BrowserTransferStateModule را وارد کنید:
imports: [ BrowserModule.withServerTransition({ appId: 'my-app' }), BrowserTransferStateModule, ]
در فایل app.server.module.ts، ماژول ServerTransferStateModule را وارد کنید:
```typescript import { ServerModule, ServerTransferStateModule } from '@angular/platform-server'; imports: [ AppModule, ServerModule, ServerTransferStateModule, ... ]
میتوانید از تابع makeStateKey برای ایجاد یک کلید برای ذخیره دادهها در وضعیت (که به مرور به مرورگر منتقل میشود) استفاده کنید. شما از this.state.get برای دریافت داده از وضعیت و this.state.set برای تنظیم داده در وضعیت استفاده خواهید کرد. وقتی یک فراخوانی API انجام میشود، دادههای بازگشتی را با استفاده از کلیدی که با makeStateKey ایجاد کردهاید در وضعیت ذخیره خواهید کرد.
در فایلی که دارید از API استفاده میکنید، ماژولهای TransferState و makeStateKey را وارد کنید:
```typescript import { TransferState, makeStateKey } from '@angular/platform-browser'; ```
سرویس TransferState را در تابع سازندهی خود درج کنید:
```typescript constructor( private state: TransferState, ... ) {} ```
کلیدها را برای ذخیره دادههای خود ایجاد کنید:
const KEY_NAME = makeStateKey('variable_name');
در داخل تابعی که به API فراخوانی میکنید، دادههای خود را از وضعیت با استفاده از this.state.get دریافت کنید. اگر مشخصات یافت نشود، فراخوانی HTTP خود را انجام دهید. وقتی دادههای خود را از فراخوانی HTTP دریافت کردید، آن را با استفاده از this.state.set در وضعیت ذخیره کنید.
functionName() { let variable_name = this.state.get(KEY_NAME, null as any); if (variable_name) { return Observable.of(variable_name); } return this.http.get('url') ... this.state.set(KEY_NAME, variable_name as any); return variable_name; }
حالا سمت مشتری شما هنگام بازگشت داده از سرور رندرینگ، چون در وضعیت ذخیره شده است، فراخوانی HTTP انجام نخواهد داد."