Создание приложения электронной коммерции GraphQL с нуля

Создание приложения электронной коммерции GraphQL с нуля

6 декабря 2022 г.

Это то, что вы будете строить!

A Опрос, проведенный Accenture (n>20 000 потребителей в 19 странах), показал, что 47 % покупателей в Интернете готовы платить больше, если им будет предоставлен опыт электронной коммерции, который превзойдет их ожидания.

Отличные новости, если вы интернет-магазин, верно? Но предостерегайте покупателей: точно такой же процент также сказал, что вместо этого они избегали бы покупать у розничного продавца, если бы их опыт разочаровывал.

Быстрый, отзывчивый и интуитивно понятный интерфейс для ваших покупателей имеет решающее значение, поэтому JAMstack — JavaScript, API, разметка — оказался популярным для электронной коммерции. Однако это еще не все, что вам нужно.

Суть здесь в том, чтобы отделить интерфейсную часть от серверной, сократить разрыв с помощью GraphQL и умного управления отрисовкой контента... и именно поэтому WunderGraph — инструмент, объединяющий REST, GraphQL и все ваши источники данных в единый, безопасный, типобезопасный, сквозной путь для всех ваших пользовательских интерфейсов — имеет смысл.

Надежная технология с современным дизайном.

Итак, давайте посмотрим, как мы можем использовать некоторые из этих технологий JAMstack — Next.js , Strapi , GraphQL и Snipcart – вместе таким образом, чтобы вы могли создать именно тот опыт покупок для своих пользователей, который вам нужен, и при этом не идти на компромиссы с разработчиками.

Вид с высоты 30 000 футов

Вот что вам понадобится для этого урока:

  1. Node.js & Установлены NPM и Python (требуется для локального тестирования Strapi с sqlite3).
  2. Промежуточные знания React/Next.js, TypeScript и CSS (здесь я использую Tailwind, потому что мне нравится CSS, ориентированный на полезность ; но классы Tailwind довольно хорошо переводятся практически в любое другое решение для стилей, потому что это буквально просто другой способ написания обычного CSS).
  3. Автономная CMS для быстрой загрузки нашего бэкэнда (и добавления GraphQL) без необходимости иметь дело с громоздкими сторонними клиентами, которые нам придется поставлять на стороне клиента. Strapi великолепен, поэтому мы используем его здесь.
  4. Snipcart учетная запись для добавления системы управления корзиной в наше приложение электронной коммерции (бесплатно в тестовом режиме). В этом руководстве используется Snipcart V3.

Во-вторых, вот краткая схема нашей архитектуры, чтобы вы знали, что будете создавать.

Бэкенд — Strapi + GraphQL

Хотя реляционная база данных сама по себе подойдет для большинства случаев использования, готовая к работе электронная коммерция — это то, где вам нужно нечто большее.

Таким образом, мы будем использовать безголовую JAMstack CMS, Strapi, в качестве PIM — решения для управления продуктом, информацией, управлением — единым источником. истины для вашей платформы электронной коммерции. Это будет центр для вашего каталога продуктов, откуда вы будете добавлять, изменять, обогащать и распространять свой каталог продуктов (как GraphQL через легко устанавливаемый плагин).

В этом руководстве в качестве базы данных Strapi используется sqlite3, но вы можете легко заменить ее в рабочей среде на PostgreSQL или другую по вашему выбору.

Лучший друг — WunderGraph

WunderGraph — ключ ко всему этому. Он находится между нашим интерфейсом и серверной частью, одновременно служа связующим звеном для их разделения и облегчения безопасного взаимодействия с типами.

Это BFF — Backend-for-Frontend — сервисный уровень. , или API-шлюз, как бы вы его ни называли, который служит единственным «бэкэндом», который может видеть ваш интерфейс.

Это работает путем консолидации данных из всех ваших источников данных (для нас здесь — конечной точки GraphQL) и использования запросов GraphQL, которые вы пишете, чтобы адаптировать эти данные (через безопасный JSON-RPC) для каждого отдельного взаимодействия с пользователем, которое вы хотите предоставить.

Вы можете использовать фильтры, объединения, переводы и т. д., сохраняя при этом четкое разделение обязанностей между приложением Next.js и серверной частью Strapi + DB и освобождая их для выполнения собственных задач.

Внешний интерфейс — Next.js + Tailwind CSS + Snipcart

Фронтенд на самом деле не слишком сложен. WunderGraph генерирует невероятно полезные готовые к использованию хуки для запроса и изменения данных (опять же, на основе написанных вами операций GraphQL), поэтому для Next.js вы можете просто использовать их.

Snipcart – это отличный способ добавить простую в интеграции и красивую систему управления корзинами в любое веб-приложение, и она сослужит нам хорошую службу.

Эта архитектура позволяет реализовать возможности электронной коммерции в виде молниеносно быстрого, модульного, расширяемого интерфейса, а не громоздких универсальных клиентских приложений, которые были в далеком прошлом.

Вот готовый продукт с разными размерами экрана.

1. Создайте серверную часть Strapi + GraphQL

Strapi – это автономная CMS (система управления контентом) на основе Node.js с открытым исходным кодом, которая позволяет программистам быстро создавать самостоятельные, адаптируемые и эффективные API контента (RESTful и GraphQL) без написания кода вообще.

Шаг 1. Создание проекта Strapi

Создайте новый каталог, перейдите в него и выполните следующую команду:

npx create-strapi-app@latest my-project - quickstart

💡 Флаг --quickstart по умолчанию дает вам базу данных SQLite 3. Если вы предпочитаете переключаться между базами данных, см. здесь .

Теперь ваше приложение Strapi запущено и работает по адресу localhost:1337.

Зарегистрируйте администратора проекта Strapi. Это полностью локально для разработки и тестирования. Вам не нужно беспокоиться о действительных адресах электронной почты, надежных паролях и т. п.... пока.

Теперь у вас должен быть доступ к панели управления Strapi по адресу localhost:1337/admin.

Шаг 2. Создайте типы контента для каталога товаров.

Панель администратора Strapi позволяет быстро создать схему для ваших данных без необходимости возиться с реальной реляционной базой данных. Strapi называет эти модели типами контента. Они довольно интуитивно понятны, все, что вам нужно сделать, это создать новый Content-Type и добавить поля, которые нужны вашим продуктам.

Перейдите в раздел Content-Type Builder на боковой панели и выберите Создать новый тип коллекции. Появится модальное окно, где вы должны ввести продукт в качестве отображаемого имени, а затем нажать кнопку «Продолжить».

Следующая часть занята. Далее вам нужно будет добавить фактические данные (Диспетчер контента –> выберите Тип коллекции –> Создать новую запись, промойте и повторите для каждого продукта в вашем каталоге) Я загружаю примеры данных из FakeStoreAPI , поэтому в соответствии с этим мои типы контента выглядят так.

Не забудьте добавить отношение «многие к одному» между товаром и категорией!

Затем вам нужно будет предоставить разрешения на чтение для find и findOne для двух типов коллекций, чтобы они были общедоступными и их можно было запрашивать без аутентификации.

Для этого перейдите в «Настройки» на боковой панели, а затем:

  • Перейдите к разделу Роли в разделе Пользователи и разрешения
  • .
  • Нажмите на роль Общедоступная, чтобы изменить ее.
  • Нажмите раскрывающееся меню Разрешения пользователей, затем выберите параметры поиска и поиска для продукта и категории.

Теперь нажмите кнопку Сохранить в правом верхнем углу экрана, чтобы обновить роль.

Теперь мы можем выполнять запросы REST, такие как GET /products и GET /products/:id. Но, конечно же, GraphQL сделал бы все это намного намного проще. Итак, давайте сделаем это дальше.

Шаг 3. Добавление GraphQL API

Чтобы преобразовать наши конечные точки API (в настоящее время RESTful) в одну конечную точку GraphQL, нам нужно установить подключаемый модуль graphql, выполнив следующую команду в нашем внутреннем каталоге:

npm run strapi install graphql

Все готово, теперь (пере)запустите сервер Strapi, используя:

npm run develop

...и оставьте его работающим до конца этого урока. Теперь у вас есть конечная точка GraphQL по адресу localhost:1337/graphql, и вы можете немного поиграть здесь**,** написав запросы GraphQL для изучения ваших данных.

2. Настройте WunderGraph и наш внешний интерфейс.

Теперь, когда у нас есть источник данных GraphQL, пришло время настроить WunderGraph в качестве нашего лучшего друга. К счастью, разработчики WunderGraph предоставили стартовый шаблон, который мы можем использовать для получения как WunderGraph, так и шаблона. Приложение Next.js запущено и запущено одновременно.

Шаг 1. Быстрое начало работы с WunderGraph и Next.js

компакт-диск в корень проекта (и из бэкэнда) и введите:

npx -y @wundergraph/wunderctl init - template nextjs-starter -o frontend

Затем компакт-диск в каталог проекта:

cd frontend

Установите зависимости и запустите:

npm i && npm start

Это загрузит серверы WunderGraph и Next.js (с использованием npm-run- all), предоставляя вам заставку/вступительную страницу по адресу localhost:3000 с примером запроса (я полагаю, для ракет SpaceX). Если вы это видите, все работает.

Теперь, чтобы фактически настроить WunderGraph для просмотра наших данных Strapi и создать для нас клиент Next.js — в комплекте с перехватчиками запросов/мутаций, которые наш интерфейс может затем использовать и отображать данные из нашего каталога продуктов Strapi.

Шаг 2. WunderGraph 101

Таким образом, WunderGraph работает следующим образом: вы сообщаете ему, от каких источников данных зависит ваше приложение, и он объединяет эти разрозненные источники данных в один слой виртуального графа, на котором вы затем можете определять операции с данными (используя GraphQL). Благодаря мощному самоанализу WunderGraph может превратить практически любой источник данных, о котором вы только можете подумать, в безопасный, типобезопасный API JSON-over-RPC; OpenAPI REST, GraphQL, PlanetScale, Fauna, MongoDB и другие, а также любую базу данных Postgres/SQLite/MySQL.

Это отлично работает для нас; у нас уже есть работающий источник данных GraphQL!

Итак, давайте приступим к делу. Откройте wundergraph.config.ts в каталоге .wundergraph и добавьте нашу конечную точку Strapi + GraphQL в качестве источника данных, от которого зависит наше приложение и который WunderGraph должен анализировать самостоятельно.

// existing code here

const strapi = introspect.graphql({
  apiNamespace: 'backend',
  url: 'http://localhost:1337/graphql',
})

const myApplication = new Application({
  name: 'app',
  apis: [strapi],
})

// more existing code, leave all of this alone

Обратите внимание, что для каждого интроспективного источника данных требуется пространство имен, поскольку каждый из них должен быть объединен в единый виртуальный граф.

После того, как вы запустите npm start, WunderGraph автоматически отслеживает необходимые файлы в каталоге вашего проекта, поэтому просто нажав здесь, вы запустите генератор кода, и он сгенерирует схему, которую вы можете проверить (если вы хотите) — файл wundergraph.app.schema.graphql в /.wundergraph/generated.

Шаг 3. Определение операций с помощью GraphQL

Это та часть, где мы пишем запросы/мутации в GraphQL для работы с созданным WunderGraph слоем виртуального графа и получаем нужные нам данные.

Итак, перейдите в ./wundergraph/operations и создайте новый файл GraphQL. Мы назовем его AllProducts.graphql. Имя файла/запроса не имеет значения; его содержимое и пространство имен (в формате 'namespace_collection') подходят.

Итак, это наш запрос, чтобы получить все продукты.

query AllProducts {
  backend_products {
    data {
      id
      attributes {
        title
        price
        image
        description
        review_score
        review_count
      }
    }
  }
}

Пока не празднуйте; вот еще одна операция, на этот раз для получения одного продукта по идентификатору (или slug. Ваш выбор, но slug, вероятно, будет лучше для SEO).

query ProductByID($id: ID!) {
  backend_products(filters: { id: { eq: $id } }) {
    data {
      id
      attributes {
        title
        image
        price
        description
        review_score
        review_count
        category {
          data {
            id
            attributes {
              name
            }
          }
        }
      }
    }
  }
}

Каждый раз, когда вы нажимали «Сохранить» в течение всего этого процесса, генерация кода WunderGraph работала в фоновом режиме (и будет работать, пока работает его сервер), генерируя типобезопасные, специфичные для клиента данные, извлекающие хуки React «на лету» для вы.

Вы можете просмотреть их в /components/nextjs.ts.

// existing code here

export const useQuery = {
  AllProducts: (args: QueryArgsWithInput<AllProductsInput>) =>
    hooks.useQueryWithInput<AllProductsInput, AllProductsResponseData, Role>(
      WunderGraphContext,
      {
        operationName: 'AllProducts',
        requiresAuthentication: false,
      }
    )(args),
  ProductByID: (args: QueryArgsWithInput<ProductByIDInput>) =>
    hooks.useQueryWithInput<ProductByIDInput, ProductByIDResponseData, Role>(
      WunderGraphContext,
      {
        operationName: 'ProductByID',
        requiresAuthentication: false,
      }
    )(args),
}

// more existing code. Change nothing.

Отлично; сейчас вы, наверное, можете немного отпраздновать.

Как вы можете заметить, WunderGraph предоставил вам два крючка, которые вы можете использовать при создании своего сайта — AllProducts и ProductByID. Оба принимают входные данные; первое, ограничение на разбивку на страницы, а второе, ну... идентификатор для фильтрации, конечно.

<сильный>3. Создайте внешний интерфейс Next.js

Хорошие новости; большая часть нашей тяжелой работы сделана. С этого момента мы полностью живем в мире пользовательского интерфейса. Если вы знакомы с Next.js, это не составит труда. Все, что мы будем делать, — это создавать внешний интерфейс для нашего приложения электронной коммерции, используя те 2 хука, которые были экспортированы на предыдущем шаге.

import Head from 'next/head'
import '../styles/global.css'

function MyApp({ Component, pageProps }) {
  return (
    <>
      <Head>
        <meta charSet="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <script src="https://cdn.tailwindcss.com"></script>
      </Head>
      <main className="min-h-screen justify-center dark:bg-neutral-900">
        <Component {...pageProps} />
      </main>
    </>
  )
}

export default MyApp

Ваш файл _app.tsx

import { NextPage } from 'next'
/* WG generated hooks */
import { useQuery, withWunderGraph } from '../components/generated/nextjs'
/* my components */
import NavBar from '../components/NavBar'
import ProductCard from '../components/ProductCard'
import Snipcart from '../components/Snipcart'
/* types */
import { result } from '../types/AllProductsResult'

const Home: NextPage = () => {
  const allProducts = useQuery.AllProducts({
    input: { a: { pageSize: 10 } },
  }).result as result

  return (
    <div>
      <NavBar />
      <Snipcart />
      <div className="relative mx-auto flex flex-col items-center">
        <div className="grid gap-4 p-4 md:grid-cols-2 lg:grid-cols-3">
          {allProducts.data ? (
            <>
              {allProducts.data.backend_products.data.map((product) => {
                return (
                  <ProductCard
                    id={product.id}
                    title={product.attributes.title}
                    image={product.attributes.image}
                    description={product.attributes.description}
                    price={product.attributes.price}
                    rating={product.attributes.review_score}
                    reviews={product.attributes.review_count}
                  />
                )
              })}
            </>
          ) : (
            <>
              <span> Loading...</span>
            </>
          )}
        </div>
      </div>
    </div>
  )
}

export default withWunderGraph(Home)

Ваш файл index.tsx

/* NextJS stuff */
import Image from 'next/image'
import { useRouter } from 'next/router'
/* my styles */
import styles from './ProductCard.module.css'

type Props = {
  id: string
  title: string
  image: string
  price: number
  description?: string
  rating: number
  reviews: number
}

const ProductCard = (props: Props) => {
  const router = useRouter()
  // handle click on Product Image
  const routeTo = (productId) => {
    router.push({
      pathname: `/products/${productId}`,
    })
  }

  return (
    <div className={styles.container}>
      <div className={styles.card}>
        <div className={styles.imgBx}>
          <Image
            onClick={() => routeTo(props.id)}
            className="img"
            src={props.image}
            alt="Product image"
            height={300}
            width={300}
          />
        </div>
        <div className={styles.contentBx}>
          <div className={styles.title}>
            <h2 onClick={() => routeTo(props.id)}>{props.title}</h2>
          </div>
          <div className={styles.rating}>
            <h2 className="font-medium text-zinc-100">{props.rating} </h2>
            <h4 className="font-medium text-amber-500">{props.reviews} </h4>
          </div>
          <div className={styles.price}>
            <h4 className="text-2xl font-bold text-gray-800">${props.price}</h4>
          </div>
          <div className={styles.cartBtn}>
            {/* <a href="#">Add 🛒</a> */}
            <button
              className="snipcart-add-item"
              data-item-id={props.id}
              data-item-price={'' + props.price}
              data-item-description={props.description}
              data-item-image={props.image}
              data-item-name={props.title}
            >
              Add 🛒
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}

export default ProductCard

Ваш файл ProductCard.tsx

/* WG generated hooks */
import { useQuery, withWunderGraph } from '../../components/generated/nextjs'
/* NextJS stuff */
import Image from 'next/image'
import { useRouter } from 'next/router'
/* my components */
import NavBar from '../../components/NavBar'
/* types */
import { result } from '../../types/OneProductResult'

type Props = {}

const Product = (props: Props) => {
  const router = useRouter()
  const { id } = router.query
  const productId: string = id as string // needed because id as is will be of type string[] | string (union)

  const oneProduct = useQuery.ProductByID({
    input: { id: productId },
  }).result as result

  const data = oneProduct.data?.backend_products?.data[0].attributes

  return (
    <div>
      <NavBar />
      <div>
        <div className="items-center">
          <div className="grid grid-cols-2 gap-0 p-4">
            <div className="content-center">
              <Image
                src={data?.image}
                alt="Picture of product"
                width={400}
                height={400}
              />
            </div>
            <div>
              <h1 className="text-4xl font-bold text-zinc-100">
                {data?.title}
              </h1>

              <h2 className="mt-2 mb-2 bg-amber-500 p-2 text-2xl font-bold text-black ">
                ${data?.price}
              </h2>
              <h2 className="mt-2 mb-2 text-lg text-neutral-400 ">
                {data?.description}
              </h2>
              <h3 className="mt-2 mb-2 text-xl text-zinc-100 ">
                {data?.review_score} 
              </h3>
              <h3 className="mt-2 mb-2 text-lg text-zinc-100 ">
                {data?.review_count} reviews
              </h3>

              <button
                className="snipcart-add-item mt-2 mb-2 bg-zinc-100 p-3 text-xl font-bold text-black"
                data-item-id={oneProduct.data?.backend_products?.data[0].id}
                data-item-price={'' + data?.price}
                data-item-description={data?.description}
                data-item-image={data?.image}
                data-item-name={data?.title}
              >
                Add To Cart 🛒
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

export default withWunderGraph(Product)

Ваш файл [id].tsx[

4. Реализуйте управление корзиной с помощью Snipcart

Snipcart предоставляет комплексное решение для управления корзиной и оформления заказа для любого веб-приложения, поставляемое в виде пользовательских скриптов и таблиц стилей, что упрощает интеграцию.

После того, как вы зарегистрируетесь, перепроверите, что вы находитесь в тестовом режиме и что у вас есть открытый ключ API (и сохраненный в вашем файле .env.local), вы все установить.

Только одно изменение: вместо добавления скриптов Snipcart (получите их здесь) вручную в <body> нашего приложения ;, мы создадим для них компонент и вместо этого включим его в наш Index.tsx.

/* NextJS stuff */
import Script from 'next/script'

const Snipcart = () => {
  return (
    <>
      <Script
        id="show-cart"
        dangerouslySetInnerHTML={{
          __html: `
          window.SnipcartSettings = {
              publicApiKey: "${process.env.NEXT_PUBLIC_SNIPCART_API_KEY}",
              loadStrategy: "on-user-interaction",
              modalStyle: "side",
          };
          (function(){var c,d;(d=(c=window.SnipcartSettings).version)!=null||(c.version="3.0");var s,S;(S=(s=window.SnipcartSettings).currency)!=null||(s.currency="usd");var l,p;(p=(l=window.SnipcartSettings).timeoutDuration)!=null||(l.timeoutDuration=2750);var w,u;(u=(w=window.SnipcartSettings).domain)!=null||(w.domain="cdn.snipcart.com");var m,g;(g=(m=window.SnipcartSettings).protocol)!=null||(m.protocol="https");var f,v;(v=(f=window.SnipcartSettings).loadCSS)!=null||(f.loadCSS=!0);var E=window.SnipcartSettings.version.includes("v3.0.0-ci")||window.SnipcartSettings.version!="3.0"&&window.SnipcartSettings.version.localeCompare("3.4.0",void 0,{numeric:!0,sensitivity:"base"})===-1,y=["focus","mouseover","touchmove","scroll","keydown"];window.LoadSnipcart=o;document.readyState==="loading"?document.addEventListener("DOMContentLoaded",r):r();function r(){window.SnipcartSettings.loadStrategy?window.SnipcartSettings.loadStrategy==="on-user-interaction"&&(y.forEach(function(t){return document.addEventListener(t,o)}),setTimeout(o,window.SnipcartSettings.timeoutDuration)):o()}var a=!1;function o(){if(a)return;a=!0;let t=document.getElementsByTagName("head")[0],n=document.querySelector("#snipcart"),i=document.querySelector('src[src^="'.concat(window.SnipcartSettings.protocol,"://").concat(window.SnipcartSettings.domain,'"][src$="snipcart.js"]')),e=document.querySelector('link[href^="'.concat(window.SnipcartSettings.protocol,"://").concat(window.SnipcartSettings.domain,'"][href$="snipcart.css"]'));n||(n=document.createElement("div"),n.id="snipcart",n.setAttribute("hidden","true"),document.body.appendChild(n)),$(n),i||(i=document.createElement("script"),i.src="".concat(window.SnipcartSettings.protocol,"://").concat(window.SnipcartSettings.domain,"/themes/v").concat(window.SnipcartSettings.version,"/default/snipcart.js"),i.async=!0,t.appendChild(i)),!e&&window.SnipcartSettings.loadCSS&&(e=document.createElement("link"),e.rel="stylesheet",e.type="text/css",e.href="".concat(window.SnipcartSettings.protocol,"://").concat(window.SnipcartSettings.domain,"/themes/v").concat(window.SnipcartSettings.version,"/default/snipcart.css"),t.prepend(e)),y.forEach(function(h){return document.removeEventListener(h,o)})}function $(t){!E||(t.dataset.apiKey=window.SnipcartSettings.publicApiKey,window.SnipcartSettings.addProductBehavior&&(t.dataset.configAddProductBehavior=window.SnipcartSettings.addProductBehavior),window.SnipcartSettings.modalStyle&&(t.dataset.configModalStyle=window.SnipcartSettings.modalStyle),window.SnipcartSettings.currency&&(t.dataset.currency=window.SnipcartSettings.currency),window.SnipcartSettings.templatesUrl&&(t.dataset.templatesUrl=window.SnipcartSettings.templatesUrl))}})();
        `,
        }}
      />
    </>
  )
}

export default Snipcart

Как только это будет сделано, все, что нам нужно сделать, это добавить необходимые реквизиты Snipcart для предоставления метаданных корзины везде, где у нас есть <button> для добавления элемента в корзину.

// Add to Cart button in ProductCard.tsx
<button
  className="snipcart-add-item"
  data-item-id={props.id}
  data-item-price={'' + props.price}
  data-item-description={props.description}
  data-item-image={props.image}
  data-item-name={props.title}
>
  Add 🛒
</button>

Все сделано! Просто убедитесь, что вы прочитали их документы , так как эта статья не является исчерпывающим руководством по работе с Snipcart. API.

Подводя итоги

И это все, ребята! Надеемся, что это руководство дало вам представление о том, как вы можете использовать возможности JAMstack с WunderGraph, чтобы выйти за рамки того, что вы могли бы сделать с помощью одного только инструментария JAMstack.

Значимое разделение интерфейса и сервера, а затем преодоление разрыва между ними и безопасный способ загрузки — вероятно, самая распространенная проблема, когда дело доходит до создания веб-приложений. Новая парадигма, такая как JAMstack, делает работу в Интернете намного проще и быстрее как для разработчиков, так и для пользователей, но имеет свой собственный набор подводных камней и утомительное написание связующего кода.

Используемый с JAMstack и GraphQL в качестве сервисного уровня/BFF, WunderGraph устраняет большинство этих болевых точек, гарантируя, что вы будете заниматься только бизнес-логикой, а также создавать и поставлять потрясающие вещи своим клиентам. Полное комплексное решение без каких-либо дополнительных зависимостей.

Об авторе

Притвиш Нэт — полнофункциональный веб-разработчик, слишком приверженный философии "всегда строить". Когда он не работает над приключенческими игровыми движками на основе парсеров, он, вероятно, качается в The Tragically Hip (снова) или на кухне, совершенствуя свое aglio e olio. Проверьте его на https://medium.com/@prithwish.nath.


Также опубликовано здесь


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE