От автора: AngularFire Lite – самая первая и на данный момент единственная библиотека, поддерживающая серверный рендеринг по умолчанию как для Firestore, так и Realtime Database. В библиотеке Angularfire2 есть и другие функции, такие как Storage, Observable based Transactions, Batched Writes и Cloud Messaging.
Чтобы использовать в Angular Firebase для рендеринга на стороне сервера, вам понадобится:
Angular 5
AngularFire Lite (легкая оболочка Angular Firebase API)
История про название: библиотеку назвали AngularFire Lite просто потому, что она на 50% легче Angularfire2. В отличие от цифры 2 в официальной библиотеке, здесь решили подбирать версии Angular, поэтому на данный момент это версия 5. Можете ознакомиться с библиотекой в ее репозитории.
Руководство
Шаг 1: установка зависимостей и генерация нового проекта Angular CLI:
Напоминание: установите nodejs отсюда и Angular CLI с помощью команды:
npm i @angluar/cli
далее сгенерируйте новый проект:
ng new AngularFireLiteSSR
установите следующие зависимости, которые нужны нам для серверного рендера:
npm i @angular/platform-server @nguniversal/module-map-ngfactory-loader ts-loader
Шаг 2: установка AngularFire Lite и Firebase
Смените директорию на папку проекта и установите оба пакета: Angularfire Lite и Firebase. Замечание: на момент написания поста в firebase sdk 4.8.1 есть проблемы, поэтому устанавливайте 4.8.0:
cd AngularFireLiteSSR npm i angularfire-lite [email protected]
Шаг 3: создание нового проекта Firebase и получение учетных данных
Откройте в браузере https://console.firebase.google.com. Кликните на Add Project, укажите название проекта и далее Create Project:
Для получения учетных данных кликните на Add Firebase to your web app
Скопируйте только объект config
Шаг 4: сохранение учетных данных Firebase в проекте Angular
Откройте файл environment.prod.ts, расположенный в папке src/environments/environment.ts в редакторе и вставьте объект config, полученный из консоли Firebase внутрь своего объекта environment. Повторите то же самое для файла enviroment.ts:
export const environment = { production: true, // production: false => in enviroment.ts config : { apiKey: 'your api key goes here', authDomain: 'your authDomain goes here', databaseURL: 'your databaseUrl goes here', projectId: 'your projectId goes here', storageBucket: 'your storageBucket goes here', messagingSenderId: 'your messagingSenderId goes here' } };
Шаг 5: настройка AngularFire Lite и совместимость с Universal
Откройте корневой модуль app.module.ts и импортируйте AngularFireLite, передайте объект config, который мы храним в файле environment.
Чтобы AppModule был совместим с Universal, необходимо вызвать BrowserModule.withServerTransition и передать appId. Ваш модуль должен выглядеть так:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { AngularFireLite } from 'angularfire-lite'; import { environment } from '../environments/environment'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AngularFireLite.forRoot(environment.config), BrowserModule.withServerTransition({appId: 'angularfire-lite-project'}), ], providers: [ ], bootstrap: [AppComponent] }) export class AppModule { }
Шаг 6: создание серверного модуля и его экспорт
Создайте новый модуль в папке src/app и назовите его app.server.module.ts. Модуль должен импортировать ваш модуль ядра, за которым следует ServerModule из пакета @angular/platform-server (порядок важен).
import { NgModule } from '@angular/core'; import { ServerModule, ServerTransferStateModule } 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, ServerTransferStateModule ], bootstrap: [ AppComponent ] }) export class AppServerModule { }
Для поддержки модуля ленивой загрузки на сервере мы добавили Map Loader Module.
Очень важно импортировать ServerTransferStateModule, он необходим для AngularFire Lite, чтобы создавать переходы без мерцаний при первичной загрузке приложения на Angular. Офигеть!!
Чтобы экспортировать серверный модуль, создайте новый файл main.server.ts в папке /src и скопируйте в него следующую строку:
export { AppServerModule } from './app/app.server.module';
Шаг 7: создание tsconfig для пакета Universal
Просто скопируйте файл tsconfig, который Angular CLI генерирует (tsconfig.app.json в папке /src) и назовите новый файл tsconfig.server.json
Нужно сделать всего пару вещей
Сменить назначение модуля: с es2015 на commonjs, чей узел мы используем
Добавить angularcompilerOptions, чтобы компилятор знал о его модуле ввода AppServerModule. Мы используем хэш-символ в пути, чтобы достичь реального класса.
{ "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/app", "baseUrl": "./", "module": "commonjs", "types": [] }, "exclude": [ "test.ts", "**/*.spec.ts" ], "angularCompilerOptions": { "entryModule": "app/app.server.module#AppServerModule" } }
Шаг 8: настройка Angular CLI для создания серверного приложения
Откройте файл .angular-cli.json в корне проекта – это файл настроек Angular CLI, там описаны способы создания приложения.
Взгляните на массив apps – в нем хранятся записи, которые сообщают Angular CLI, как собрать клиентское приложение с помощью простой команды ng build.
В массив apps необходимо добавить новый объект, чтобы Angular CLI знал, как компилировать наше серверное приложение. Ваш массив apps должен выглядеть так:
"apps": [ { "root": "src", "outDir": "dist/browser", "assets": [ "assets", "favicon.ico" ], "index": "index.html", "main": "main.ts", "polyfills": "polyfills.ts", "test": "test.ts", "tsconfig": "tsconfig.app.json", "testTsconfig": "tsconfig.spec.json", "prefix": "app", "styles": [ "styles.css" ], "scripts": [], "environmentSource": "environments/environment.ts", "environments": { "dev": "environments/environment.ts", "prod": "environments/environment.prod.ts" } }, { "name": "ssr", "platform": "server", "root": "src", "outDir": "dist/server", "assets": [ "assets", "favicon.ico" ], "index": "index.html", "main": "main.server.ts", "test": "test.ts", "tsconfig": "tsconfig.server.json", "testTsconfig": "tsconfig.spec.json", "prefix": "app", "styles": [ "styles.css" ], "scripts": [], "environmentSource": "environments/environment.ts", "environments": { "dev": "environments/environment.ts", "prod": "environments/environment.prod.ts" } } ],
В первой записи необходимо лишь изменить outDir на dist/browser, чтобы в этой папке хранился пакет клиентского приложения.
Во второй записи ssr мы говорим CLI, что необходимая нам платформа – это сервер, а наш пакет Universal должен быть в dist/server. Также это необходимо указать в tsconfig, который мы создали для сервера и главного файла сервера.
Шаг 9: подъем Express Server и использование Webpack
Не буду вдаваться в детали, но здесь мы создаем экспресс сервер для локального использования нашего серверного приложения.
Создайте server.ts в корне файла проекта и файл webpack.server.config.js для упаковки файлы ts с помощью ts-loader, который мы установили ранее
Файл 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 = 5000; const DIST_FOLDER = join(process.cwd(), 'dist'); // Our index.html we'll use as our template const template = readFileSync(join(DIST_FOLDER, 'browser', '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', join(DIST_FOLDER, 'browser')); // Server static files from /browser app.get('*.*', express.static(join(DIST_FOLDER, 'browser'))); // All regular routes use the Universal engine app.get('*', (req, res) => { res.render(join(DIST_FOLDER, 'browser', 'index.html'), {req}); }); // Start up the Node server app.listen(PORT, () => { console.log(`Node server listening on http://localhost:${PORT}`); });
Файл webpack.server.config.js:
const path = require('path'); const webpack = require('webpack'); 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: [/(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'), {} ) ] }
Шаг 10: скрипт NPM для пакета и использования приложения
Теперь нам осталось лишь добавить скрипт npm, чтобы сэкономить время написания длинной команды для создания приложения через CLI, запаковать в файл server.ts с помощью Webpack и запустить с помощью узла.
Скопируйте следующую команду в объект scripts в файл package.json:
"universal": "ng build --prod && ng build --prod --app ssr --output-hashing=false && webpack --config webpack.server.config.js --progress --colors && node dist/server.js"
Использование AngularFire Lite
Здесь мы не будем делать ничего сложного, просто распечатаем кое-какие данные из Firestore в тег header, но не уходите, скоро я выпущу новое пошаговое руководство по создания красивых приложений на AngularFire Lite, готовых к выкату в продакшн.
Если добавить документ и назвать его hello с полем firestore и значением Hello Firestore from AngularFire Lite, то его можно легко вытащить в app.component.ts следующим образом:
Импортируете AngularFireLiteFirestore и вставляете его в конструктор
Добавляете ссылку в хук жизненного цикла ngOnInt и вызываете метод read
Подписываетесь на компонент или шаблон с помощью async пайпа для получения данных
Ниже показан пример использования нескольких функций AngularFire Lite, среди которых Realtime Database, Firestore (читает и запаковывает записи) и authentication (не забудьте установить права на чтение и запись в панели управления Firebase в true чисто для теста).
import {Component, OnInit} from '@angular/core'; import {AngularFireLiteAuth, AngularFireLiteDatabase, AngularFireLiteFirestore} from 'angularfire-lite'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit { constructor(public db: AngularFireLiteDatabase, public auth: AngularFireLiteAuth, public firestore: AngularFireLiteFirestore) { } databaseData; databaseList; databaseQuery; firestoreData; firestoreList; firestoreQuery; authState; ngOnInit() { // Realtime Database this.db.read('hello/hello').subscribe((data) => { this.databaseData = data; }); // Realtime Database list retrieval this.databaseList = this.db.read('hello'); // Realtime Database query this.db.query('hello').limitToLast(1).orderByKey().on('value').subscribe((data) => { this.databaseQuery = data; }); // Firestore this.firestore.read('hello/hellodoc').subscribe((data) => { this.firestoreData = data; }); // Firestore list retrieval this.firestoreList = this.firestore.read('hello'); // Firestore Query this.firestore.query('query').limit(1).on().subscribe((data) => { this.firestoreQuery = data; }); // Authentication this.auth.isAuthenticated().subscribe((isAuth) => { this.authState = isAuth; }); } // Login Button Clicked login() { this.auth.signin('[email protected]', '123456'); } }
И шаблон:
<br> <h1>AngularFire Lite Realtime Database</h1> <br> <span style="color:crimson">realtime database </span><h4>{{databaseData}}</h4> <span style="color:crimson">realtime database list of data</span> <ul> <li *ngFor="let item of databaseList | async">{{item}}</li> </ul> <span style="color:crimson">realtime database query</span><h4>{{databaseQuery}}</h4> <br> <h1>AngularFire Lite Firestore</h1> <br> <span style="color:crimson">firestore database</span><h4>{{firestoreData?.hellofield }}</h4> <span style="color:crimson">firebase database list of data</span> <ul> <li *ngFor="let doc of firestoreList | async">{{doc | json}}</li> </ul> <span style="color:crimson">firestore query</span><h4 *ngFor="let query of firestoreQuery">{{query?.data1}}</h4> <br> <br> <span>I am logged in?</span><h4>{{ authState }}</h4> <button (click)="login()">login</button>
Наслаждайтесь серверным великолепием! Осталось лишь запустить
npm run universal
Откройте http://localhost:5000/, там должны появиться ваши данных, так как вы храните их в Realtime Database или Firestore. Если открыть исходный код, можно посмотреть реальный HTML.
Вы получаете преимущества в:
Меньше время загрузки (первый рендер/первая отрисовка)
Вам лень? Клонируйте демо репозиторий AngularFire Lite
Если вам немного лень, клонируйте демо репозиторий AngularFire Lite с GitHub по ссылке.
Что по поводу рендера с помощью Firebase Cloud Functions и firebase хостинга?
Эта статья входит в серию, и я обязательно расскажу про эти темы. Я хотел начать с базовых принципов серверного рендера и продемонстрировать, что AngularFire Lite действительно позволяет Angular Universal рендерить HTML, используя данные, хранящиеся в Firebase realtime database или firestore, в отличие от Angularfire2, который выбрасывает ошибку.
Если не хотите пропустить будущие руководства, подпишитесь. Вы будете получать уведомления при выходе новых статей. Увидимся!
Автор: Hamed Baatour
Источник: https://medium.com/
Редакция: Команда webformyself.