
В bene : studio мы любим делиться знаниями, чтобы помочь сообществу профессионалов. Чтобы поделиться нашим более чем 10-летним опытом, мы запустили центр знаний в нашем блоге с регулярными обновлениями учебных пособий, лучших практик и решений с открытым исходным кодом. В bene: studio мы любим делиться знаниями, чтобы помочь сообществу профессионалов. Чтобы поделиться нашим более чем 10-летним опытом, мы запустили центр знаний в нашем блоге, где регулярно обновляются учебные пособия, рекомендации и решения с открытым исходным кодом.
Простите, что прерываю, у нас важное сообщение! Мы стремимся расширить нашу команду талантливыми разработчиками. Ознакомьтесь с нашими открытыми вакансиями и подайте заявку.
Введение 👋
Эта статья основана на живом семинаре, который мы провели в апреле 2022 года. Следуйте инструкциям в этой статье, чтобы пройти семинар дома!
В этом уроке я хотел бы показать преимущества использования монорепозиториев для проекта с полным стеком. Мы создадим два интерфейса для общего сервера. Оба внешних приложения будут полагаться на некоторый общий код, перемещенный в отдельную библиотеку, все еще находящуюся в монорепозитории. Мы также интегрируем Stripe для поддержки Google Pay и Apple Pay.
Прежде чем ты начнешь
Предпосылки
- Бесплатная учетная запись ngrok
- Полосатый аккаунт
- Узел 14+ (рекомендуется nvm)
- Гугл Хром
- установить инструменты nx с помощью npm i -g nx
- установить http-сервер с помощью npm i -g http-server
- VS Code с установленным расширением консоли Nx. Также рекомендуется использовать расширение Git Graph.
- некоторые активы мы подготовили
Совет для профессионалов: посмотрите видео-семинар, чтобы легче следовать шагам, описанным в статье!
Создание рабочего пространства
Выберите папку для работы и запустите:
npx [email protected] nx-stripe-workshop
- Выберите угловой шаблон, имя приложения должно быть элегантным-бренд-веб; выберите CSS в качестве формата таблицы стилей и нет для облака nx
- Будет установлено много всего — это займет некоторое время. Вроде, правда.
- Когда закончите, откройте его в VS Code:
Изучение рабочего пространства, создание приложений
cd ./nx-stripe-workshop code .
Теперь у нас есть пустое рабочее пространство с одним приложением Angular (и проектом тестирования e2e на основе кипариса). Не стесняйтесь немного изучить папки проекта, прежде чем двигаться дальше.
Обратите внимание, что он также инициализировал репозиторий git и сделал первый коммит.
Мы также добавим еще одно приложение angular и приложение node в качестве серверной части. Чтобы сделать последнее, мы также добавляем узел и экспресс-плагины в рабочую область:
npm i -D @nrwl/node @nrwl/express
Теперь в VS Code откройте консоль Nx, выберите «Создать», а затем выберите @nrwl/express — приложение.
- Название: fashion-backend
- frontendProject: элегантный-бренд-веб
Остальное можно оставить по умолчанию. Щелкните Выполнить.
Рекомендуется зафиксировать изменения после создания пустого приложения или библиотеки в рабочей области:
git add . git commit -m "add backend"
В консоли Nx выберите генерировать, а затем выберите @nrwl/angular — приложение.
- Название: sporty-brand-web
- backendProject: fashion-бэкенд
Остальное можно оставить по умолчанию. Щелкните Выполнить.
Откройте apps/sporty-brand-web/proxy.conf.json и измените его содержимое на:
{
"/api": {
"target": "http://localhost:3333",
"secure": false
}
}
так что он нацелен на тот же API, что и первое приложение.
Опять же, хорошей идеей будет зафиксировать изменения после создания пустого приложения или библиотеки в рабочей области:
git add . git commit -m "add second app"
Пора запускать приложения…
- nx serve ‹имя приложения› компилирует и запускает приложение (с горячей перезагрузкой) — мы будем использовать его для запуска как внешнего, так и внутреннего проектов.
- nx test ‹app name› и nx lint ‹app name› запускает тесты и linting соответственно — мы не будем углубляться в это на семинаре.
Список продуктов в наших приложениях — бэкенд
Откройте apps/fashion-backend/src/main.ts и создайте две конечные точки для наших списков продуктов. Что касается изображений, нам нужно использовать их как статические изображения. Скопируйте папку с ресурсами из репозитория git в apps/fashion-backend/src/assets. Добавьте эти строки в main.ts:
app.use('/api/static', express.static(__dirname + '/assets'));
app.get('/api/elegant', (req, res) => {
res.send({
products: [
{
id: 1,
name: 'fancy shoes',
img: '/api/static/fancy-shoes.jpg',
price: 10000,
},
{
id: 2,
name: 'fancy overall',
img: '/api/static/fancy-overall.jpg',
price: 30000,
},
{
id: 3,
name: 'suit',
img: '/api/static/suit.jpg',
price: 100000,
},
],
});
});
app.get('/api/sporty', (req, res) => {
res.send({
products: [
{
id: 4,
name: 'running shoes',
img: '/api/static/running-shoe.jpg',
price: 10000,
},
{
id: 5,
name: 'sweatpants',
img: '/api/static/sweatpants.jpg',
price: 30000,
},
{
id: 6,
name: 'arctic jacket',
img: '/api/static/arctic-jacket.jpg',
price: 100000,
},
],
});
});
Теперь запустите серверную часть с помощью nx serve fashion-backend.
Список продуктов в наших приложениях — интерфейс
В дереве папок выберите apps/elegant-brand-web/src/app, щелкните правой кнопкой мыши, выберите Nx generate, введите component и нажмите Enter.
- Название компонента: продукт
- Обнаружение изменений: OnPush
Остальное можно оставить по умолчанию. Щелкните Выполнить. Это не только добавляет файлы для компонента, но также редактирует модуль приложения, чтобы сделать его доступным для приложения, добавляя его в объявления.
В связи с этим удалите NxWelcomeComponent из модуля приложения и удалите nx-welcome.component.ts.
Также добавьте HttpClientModule в массив импорта. VS Code должен иметь возможность импортировать его, но если это не так, это правильный оператор импорта:
import { HttpClientModule } from '@angular/common/http';
Теперь мы можем ввести немного:
product.component.ts
export class ProductComponent {
@Input()
name!: string;
@Input()
price!: number;
@Input()
img!: string;
}
Также измените строку импорта
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
product.component.html
<div class="product">
<div class="heading">{{name}}</div>
<img [src]="img">
<div class="details">
<span>{{price}} HUF</span>
<button>BUY NOW</button>
</div>
</div>
продукт.модели.тс
export interface IProduct {
name: string;
price: number;
img: string;
}
ПРИМЕЧАНИЕ. Это понадобится обоим приложениям, поэтому позже мы переместим его в общую библиотеку.
app.component.ts
export class AppComponent implements OnInit {
constructor(private http: HttpClient) {
}
ngOnInit(): void {
this.http.get<{ products: IProduct[] }>('/api/elegant').subscribe(res => {
this.products = res.products;
})
}
products: IProduct[] | null = null;
}
(Используйте действия VS Code, чтобы добавить отсутствующие операторы импорта)
ПРИМЕЧАНИЕ: в реальном приложении angular мы бы:
- создайте отдельный компонент для списка товаров; компонент приложения обычно совершенно пуст и используется просто как контейнер (для меню, заголовков, основного и т. д.)
- Конечно, не помещайте HTTP-запросы в компоненты — настоятельно рекомендуется использовать Redux. (В angular для этого стандартом де-факто является библиотека Ngrx.)
app.component.html
<ng-container *ngIf='products; else loading'>
<ng-container *ngFor='let product of products'>
<nx-stripe-workshop-product [name]='product.name' [price]='product.price' [img]='product.img'></nx-stripe-workshop-product>
</ng-container>
</ng-container>
<ng-template #loading>
Loading...
</ng-template>
Синтаксис шаблона Angular поначалу не очень прост для написания, но его легко читать; пока у нас есть продукты, мы перебираем их и создаем экземпляр нашего компонента продукта для каждого. Если они не готовы, показываем индикатор загрузки.
Положил все это вместе…
В двух отдельных терминалах запустите:
- nx serve fashion-backend (если он еще не запущен)
- nx обслуживает элегантный бренд-веб
и откройте веб-приложение в браузере (http://localhost:4200/). Мы должны увидеть список продуктов.
Листинг во втором приложении, создание общей библиотеки
В дереве папок выберите apps/sporty-brand-web/src/app, щелкните правой кнопкой мыши, выберите Nx generate, введите component, нажмите Enter.
- Название компонента: продукт
- Обнаружение изменений: OnPush
Остальное можно оставить по умолчанию. Это сгенерирует необходимые файлы и зарегистрирует компонент в модуле приложения.
Удалите NxWelcomeComponent из модуля приложения и удалите nx-welcome.component.ts.
Также добавьте HttpClientModule в массив импорта. VS Code должен иметь возможность импортировать его, но если это не так, это правильный оператор импорта:
import { HttpClientModule } from ‘@angular/common/http’;
Теперь мы можем просто скопировать содержимое product.component.ts и product.component.html. (Наши два внешних приложения будут в основном одинаковыми — пожалуйста, используйте свое воображение, чтобы представить, что эти два приложения отличаются немного больше, чем они есть на самом деле, поскольку у нас нет времени сделать их достаточно сложными, чтобы сделать это на самом деле 😊)
Совместное использование product.models в библиотеке
Теперь вы, возможно, помните, что следующее, что мы сделали, — это добавили интерфейс для продуктов. Но мы действительно не хотим копировать это! Пришло время создать общую библиотеку.
Нажмите кнопку Nx Console на боковой панели VS Code, нажмите «Создать». Выберите @nrwl/angular — библиотека.
- Имя: общая сеть
- остальное может остаться как есть
Щелкните Выполнить.
Затем в apps/elegant-brand-web/src/app щелкните правой кнопкой мыши product.models.ts и выберите «Вырезать».
Затем выберите libs/shared-web/src/lib, щелкните правой кнопкой мыши узел библиотеки и выберите Вставить. Скажите нет обновлению импорта на данный момент.
В libs/shared-web/src/index.ts добавьте следующую строку:
экспорт * из ‘./lib/product.models’;
Затем в apps/elegant-brand-web/src/app/app.component.ts измените import { IProduct } с ‘./product.models’; импортировать { IProduct } из ‘@nx-stripe-workshop/shared-web’;
Теперь мы можем продолжить работу над вторым приложением! Откройте apps/sporty-brand-web/src/app и внесите следующие изменения:
app.component.ts
export class AppComponent implements OnInit {
constructor(private http: HttpClient) {
}
ngOnInit(): void {
this.http.get<{ products: IProduct[] }>('/api/sporty').subscribe(res => {
this.products = res.products;
})
}
products: IProduct[] | null = null;
}
(Используйте действия VS Code, чтобы добавить отсутствующие операторы импорта)
app.component.html
<ng-container *ngIf='products; else loading'>
<ng-container *ngFor='let product of products'>
<nx-stripe-workshop-product [name]='product.name' [price]='product.price' [img]='product.img'></nx-stripe-workshop-product>
</ng-container>
</ng-container>
<ng-template #loading>
Loading...
</ng-template>
Дай им шанс! 🚀
(Вероятно, сейчас самое подходящее время для фиксации наших изменений.)
В двух отдельных терминалах запустите:
- nx serve fashion-backend (если он еще не запущен)
- nx serve Elegant-brand-web (если он еще не запущен)
- nx serve sporty-brand-web –port 4201 (мы меняем порт, чтобы он мог работать параллельно с другим приложением)
и откройте веб-приложения в браузере (http://localhost:4200/ и http://localhost:4201/). Мы должны увидеть список продуктов в обоих из них.
Заставьте их смотреть друг на друга
Скопируйте «style.css» из репозитория в apps/sporty-brand-web/src/styles.css и apps/sporty-brand-web/src/styles.css. Благодаря некоторым хитростям css один и тот же css можно использовать для стилизации обеих версий. Вам просто нужно добавить класс в корень вашего приложения. Добавьте следующие классы в index.html каждого приложения:
<html lang="en" class="elegant"> ... </html> <html lang="en" class="sporty"> ... </html>
Внедрение Stripe Pay, тестирование Google Pay
Во-первых, остановите наши запущенные приложения. Затем щелкните правой кнопкой мыши на shared-web › src › lib и снова выберите Nx generate =› component. Назовите ее полосатой кнопкой и добавьте галочку для экспорта; оставьте все как есть и нажмите «Выполнить».
Добавьте ссылку на этот скрипт в index.html в обоих приложениях: ‹script src="https://js.stripe.com/v3/"›‹/script›
Начнем с внутреннего кода. Сначала установите Stripe с помощью npm i stripe, а затем обновите наше серверное приложение:
main.ts
Поместите это перед первым вызовом app.get:
app.use(express.json());
Также преобразуйте списки продуктов в переменные, например:
const elegantProducts = [
{
id: 1,
name: 'fancy shoes',
img: '/api/static/fancy-shoes.jpg',
price: 10000,
},
{
id: 2,
name: 'fancy overall',
img: '/api/static/fancy-overall.jpg',
price: 30000,
},
{
id: 3,
name: 'suit',
img: '/api/static/suit.jpg',
price: 100000,
},
];
const sportyProducts = [
{
id: 4,
name: 'running shoes',
img: '/api/static/running-shoe.jpg',
price: 10000,
},
{
id: 5,
name: 'sweatpants',
img: '/api/static/sweatpants.jpg',
price: 30000,
},
{
id: 6,
name: 'arctic jacket',
img: '/api/static/arctic-jacket.jpg',
price: 100000,
},
];
app.get('/api/elegant', (req, res) => {
res.send({
products: elegantProducts,
});
});
app.get('/api/sporty', (req, res) => {
res.send({
products: sportyProducts,
});
});
Наконец, добавьте нашу новую конечную точку — это будет вызвано как часть процесса оплаты.
app.post('/api/stripe-init', async (req, res) => {
const stripe = require('stripe')(
'sk_test_xxx' //your key here
);
const product = [...elegantProducts, ...sportyProducts].find(p => p.id === req.body.id);
const paymentIntent = await stripe.paymentIntents.create({
amount: product.price * 100, // i.e. to pay 9.99, we have to pass 999 to Stripe
currency: 'huf',
});
res.send({
clientSecret: paymentIntent.client_secret,
});
});
Примечание: можно подумать о передаче цены и валюты этому методу напрямую (вместо того, чтобы использовать идентификатор продукта и искать цену), но на самом деле это было бы угрозой безопасности, которая могла бы позволить нашим пользователям устанавливать свои собственные цены. Таким образом, даже если клиент подделан, серверная часть гарантирует, что используется правильная цена.
Теперь расширим наш интерфейс, затем напишем наш компонент:
продукт.модели.тс
export interface IProduct {
id: number; // this is the new bit
name: string;
price: number;
img: string;
}
полоса-кнопка.component.ts
import { HttpClient } from '@angular/common/http';
import { Component, Input, OnInit } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { IProduct } from '../product.models';
declare const Stripe: any;
type PaymentState = 'INIT' | 'LOADING' | 'SUCCESS' | 'ERROR';
@Component({
selector: 'nx-stripe-workshop-stripe-button',
templateUrl: './stripe-button.component.html',
styleUrls: ['./stripe-button.component.css'],
})
export class StripeButtonComponent implements OnInit {
@Input() amount = 0;
@Input() productId = 0;
@Input() currency = 'huf';
@Input() label = 'Demo';
static counter = 0;
buttonId: number;
stripe: any; // It is possible to install TypeScript typings for Stripe; we'll skip that for now
canPay$ = new BehaviorSubject(true);
paymentState$ = new BehaviorSubject('LOADING' as PaymentState);
constructor(private http: HttpClient) {
this.buttonId = StripeButtonComponent.counter++;
this.stripe = Stripe(
'pk_test_xxx', // your key here
{
apiVersion: '2020-08-27',
}
);
}
async ngOnInit() {
const paymentRequest = this.stripe.paymentRequest({
country: 'HU',
currency: 'huf',
total: {
label: this.label,
amount: this.amount * 100, // i.e. to pay 9.99, we have to pass 999 to Stripe
},
requestPayerName: true,
requestPayerEmail: true,
});
const elements = this.stripe.elements();
const prButton = elements.create('paymentRequestButton', {
paymentRequest,
});
const result = await paymentRequest.canMakePayment();
if (result) {
prButton.mount('#stripe-button-' + this.buttonId);
paymentRequest.on('paymentmethod', (ev: any) => this.pay(ev));
} else {
// One-click payment is not available (app is not running on HTTPS or the browser has no access to saved cards)
this.canPay$.next(false);
}
this.paymentState$.next('INIT');
}
async pay(ev: any) {
this.paymentState$.next('LOADING');
this.http
.post<{ clientSecret: string }>('/api/stripe-init', {
id: this.productId
})
.subscribe(async ({ clientSecret }) => {
// Confirm the PaymentIntent without handling potential next actions (yet).
const { paymentIntent, error: confirmError } =
await this.stripe.confirmCardPayment(
clientSecret,
{ payment_method: ev.paymentMethod.id },
{ handleActions: false }
);
if (confirmError) {
// Report to the browser that the payment failed, prompting it to
// re-show the payment interface, or show an error message and close
// the payment interface.
ev.complete('fail');
this.paymentState$.next('ERROR');
} else {
// Report to the browser that the confirmation was successful, prompting
// it to close the browser payment method collection interface.
ev.complete('success');
// Check if the PaymentIntent requires any actions and if so let Stripe.js
// handle the flow.
if (paymentIntent.status === 'requires_action') {
// Let Stripe.js handle the rest of the payment flow.
const { error } = await this.stripe.confirmCardPayment(
clientSecret
);
if (error) {
this.paymentState$.next('ERROR');
} else {
this.success();
}
} else {
this.success();
}
}
});
}
async success() {
this.paymentState$.next('SUCCESS');
// In a real-word app, we would probably also tell the backend about this and/or the backend could listen to a Stripe webhook.
}
}
ПРИМЕЧАНИЕ. canPay$ и paymentState$ представлены в виде наблюдаемых, а не простых полей, чтобы помочь механизму обнаружения изменений, который обычно «теряет след» между вызовами async-await. Другими возможными решениями могут быть явный вызов детектора изменений всякий раз, когда мы изменяем эти свойства (что довольно подвержено ошибкам), или рефакторинг логики Stripe в службу, а затем «подключение» canPay и paymentState в качестве входных данных компонента (которые всегда получают правильное изменение). обнаружение) — на самом деле это был бы предпочтительный способ сделать это в «реальном мире», но на этом семинаре мы просто выберем более простое решение, основанное на наблюдаемых.
полоса-кнопка.component.html
<div [id]="'stripe-button-' + buttonId" *ngIf="canPay$ | async; else fallback"></div>
<div [ngSwitch]="paymentState$ | async">
<p *ngSwitchCase="'LOADING'">Loading...</p>
<p *ngSwitchCase="'SUCCESS'">Thank you!</p>
<p *ngSwitchCase="'ERROR'">Oh no :(</p>
</div>
<ng-template #fallback>
<!-- Fallback button. In a full solution, clicking this should show a "regular" credit card form, using Stripe or anything else -->
<button>Buy now</button>
</ng-template>
общий-web.module.ts
Просто добавьте import {HttpClientModule} из ‘@angular/common/http’; вверху и HttpClientModule в массив импорта.
Затем мы можем начать использовать его в наших приложениях:
- Импортируйте SharedWebModule в оба модуля приложения (импортируйте { SharedWebModule } из ‘@nx-stripe-workshop/shared-web’ вверху, а затем добавьте его в массив импорта)
- Измените компоненты продукта следующим образом:
product.component.ts
Добавьте этот ввод:
@Input() productId!: number;
product.component.html
Замените текущую кнопку на эту:
<nx-stripe-workshop-stripe-button [amount]="price" currency="HUF" [label]="name" [productId]="productId"></nx-stripe-workshop-stripe-button>
элегантный-бренд-веб/…/app.component.html
Установите атрибут productId в этой строке:
<nx-stripe-workshop-product [name]='product.name' [price]='product.price' [productId]='product.id'></nx-stripe-workshop-product>
И мы закончили! 👏
Пора попробовать:
- Запустите ./ngrok http 4200 (в папке, где у вас есть двоичные файлы ngrok), чтобы запустить наш прокси (это нужно для доступа к приложению через HTTPS, что требуется для Stripe Pay). Обратите внимание на свой URL (тот, что выше https).
- Запустите nx serve fashion-backend и nx serve Elegant-Brand-Web –disable-host-check в разных терминалах. (Дополнительный флаг во втором вызове необходим для того, чтобы ngrok мог получить доступ к нашему локальному серверу разработки Angular.)
- Если вы вошли в Chrome с помощью своего фактического аккаунта Google и в этом аккаунте сохранена карта Google Pay, вы можете сейчас открыть URL-адрес ngrok и увидеть кнопку Google Pay рядом с нашими продуктами. Вы можете нажать на них, но не нажимайте «Оплатить» — вы не захотите платить своей настоящей картой. Вместо этого создайте новый профиль в Chrome, щелкнув значок своего профиля и выбрав «Продолжить без учетной записи». (Опять же, если вы не вошли в Chrome, вы можете пропустить это.)
- Сохраните тестовую карту Stripe в Chrome, посетив chrome://settings/payments. Будьте осторожны, чтобы избежать тестирования с вашей реальной картой!
- Откройте URL-адрес ngrok в браузере. (Обязательно используйте кнопку с https.) Должны появиться кнопки оплаты Stripe (которые в основном такие же, как кнопка Google Pay, только с другим брендом), и вы сможете завершить покупку. Если вы добавили тестовую карту, требующую проверки 3DS, пользовательский интерфейс 3DS также должен запускаться автоматически.
Подробнее о брендинге кнопки: Stripe использует общий брендинг («Stripe Pay») на кнопке для карт, сохраненных локально в браузере — это то, что мы попробовали сейчас. Если вы войдете в Chrome со своей учетной записью Google и настроите Google Pay, вы увидите брендинг Google Pay. С той же кодовой базой, которую мы написали, мы могли бы также активировать брендинг Apple Pay в Safari (для пользователей, у которых настроен Apple Pay), но для этого требуется дополнительный этап проверки домена (который мы не смогли выполнить даже с бесплатной учетной записью ngrok). ) — это выходит за рамки этого семинара.
Устранение неполадок — если вы видите резервную кнопку: убедитесь, что вы открыли HTTPS-версию URL-адреса ngrok и что у вас есть сохраненная карточка, настроенная локально в Chrome. Если оба варианта верны, но все равно не работает, откройте https://stripe.com/docs/stripe-js/elements/payment-request-button. Если вы видите кнопку здесь, проблема в вашем приложении. Но если на этой странице отображается сообщение Либо ваш браузер не поддерживает API запроса платежа, либо у вас нет сохраненного способа оплаты. то либо вы по-прежнему не сохранили тестовая карта правильно (что маловероятно на данный момент), или есть проблемы на стороне Stripe или Google вне вашего контроля (что, к сожалению, иногда случается). Если ничего не помогает, вы можете хотя бы частично протестировать свое приложение, открыв URL-адрес ngrok с профилем Chrome, в котором действительно настроена карта Google Pay. менее подвержены ошибкам, чем локально сохраненные карты (и в реальном мире гораздо более распространены в любом случае), поэтому, по крайней мере, эта кнопка должна отображаться. Просто убедитесь, что вы не завершили процесс оплаты, чтобы избежать списания средств с вашей реальной карты.
У тебя есть вопросы? 🤔
Пожалуйста, присылайте их на [email protected], и мы будем рады поговорить с нашими инженерами.
Вы ищете партнера для вашего следующего проекта? Посетите нашу страницу услуги, чтобы узнать, что мы делаем, и давайте настроим бесплатную консультацию.
Эта запись изначально была опубликована в блоге Bene Studio, авторы Даниэль Казнар и Даниэль Кис-Надь.