Понимание с практической реализацией

На дворе 2023 год, и в настоящее время тренд на JavaScript-фреймворки движется в сторону Remix. Если вы о нем не слышали, настоятельно рекомендую ознакомиться. Remix является конкурентом Next.js, и в блоге есть отличный пост об их различиях.

Я чувствую, что Remix — это отличная ставка на будущее мира JavaScript, и чтобы внести свой вклад, я собираюсь показать, как мы можем использовать более устойчивую и ориентированную на будущее архитектуру с этим фреймворком. И если вы хотите узнать больше об этой архитектуре, у меня есть еще один пост на эту тему — посмотрите здесь.

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

export async function loader() {}
export async function action() {}
export function links() {}

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

Remix полностью на стороне сервера — в отличие от Next.js, где вы можете использовать SSG, SSR и ISR. Но не поймите неправильно, хотя Remix является серверной частью, он предназначен для работы и использования точно так же, как SPA, с использованием именно этих функций.

Позвольте мне показать вам пример использования этих функций:

import styles from './index.css'

export default function AddTodoPage() {
  const todoList = useLoaderData();

  return (
    <main>
      <TodoForm />
      <TodoList todoList={todoList} />
    </main>
  );
}

export async function loader() {
  const todoList = await getTodoList();
  return todoList;
}

export async function action({ request }) {
  const formData = await request.formData();
  const todoData = Object.fromEntries(formData);
  await addTodo(todoData);

  return true;
  // You can redirect here
  // return redirect('/todo-list');
}

export function links() {
  return [{ rel: 'stylesheet', href: styles }]
}

Здесь у нас есть четыре функции:

  • export default function AddTodoPage() {} Здесь мы экспортируем компонент страницы — он должен быть экспортом по умолчанию — Remix требует, чтобы он был экспортом по умолчанию.
  • export async function loader() {} Функция загрузчика — это место, откуда мы извлекаем данные для страницы. Например, получение списка добавленных задач, и это произойдет на стороне сервера.
  • export async function action() {} Действие, в котором мы обрабатываем CRUD-материалы. Remix имеет свой собственный компонент <Form />, и он напрямую связан с этой функцией, все, что вы отправляете в форму, будет обрабатываться компонентом action.
  • export function links() {} Remix позволяет импортировать и экспортировать стили из любого места. Все, что вам нужно, — это добавить их в файл маршрута с помощью функции ссылок.

Теперь, как мы можем внедрить в него принципы чистой архитектуры?

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

Добавив к нему несколько слоев, мы можем разделить задачи и сосредоточиться на том, чтобы оставить наш Core/Domain нетронутым.

Начнем с Pслоя представления

Как показано в приведенном выше примере кода, Remix требует, чтобы мы экспортировали компонент по умолчанию в качестве нашей страницы, и мы можем сделать это следующим образом:

// src/routes/ducks/new-duck.tsx
import NewDuckPage from '../../presentation/ducks/new-duck';

export default NewDuckPage;
// src/presentation/ducks/new-duck.tsx
import { Form, Link } from '@remix-run/react';

export default function NewDuckPage() {
  return (
    <div>
      <h1>Add your duck here</h1>

      <Form method="post">
        <label htmlFor="name"></label>
        <input type="text" name="name" id="name" />
        <button>Add duck</button>
      </Form>

      <Link to="/">Home</Link>
    </div>
  );
}

Таким образом, у нас есть маршрут только на основе файлов, в то время как все наши компоненты и подкомпоненты остаются внутри папки презентации.

Прикладной уровень

На уровне приложения у нас будут хуки и соединения, которые они делают с репозиторием. Здесь у нас будет 3 файла. С помощью пользовательского хука мы будем обрабатывать все, что касается самой страницы и двух файлов для функций action и loader, которые нужны Remix.

// src/presentation/ducks/new-duck.tsx
import { Form, Link } from '@remix-run/react';
import { useNewDuck, addDuckAction, getDucksLoader } from '../../application/duck/';
import { DuckList } from './components/duck-list';

export default function NewDuckPage() {
  const { isSubmitting } = useNewDuck();

  return (
    <div>
      <h1>Add your ducko here</h1>

      <Form method="post">
        <label htmlFor="name"></label>
        <input type="text" name="name" id="name" />
        <button disabled={isSubmitting}>Add ducko</button>
      </Form>

      <DuckList />

      <Link to="/">Home</Link>
    </div>
  );
}

export { getDucksLoader as loader, addDuckAction as action };

Поскольку это наша страница, нам нужно экспортировать loader и action отсюда и импортировать/экспортировать их по маршруту, например:

// src/routes/ducks/new-duck.tsx

import Index, { loader, action } from '../../presentation/ducks/new-duck';

export { loader, action }
export default Index;

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

Слой адаптера

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

// src/adapter/repository/duck/duckFileRepository.ts
import fs from 'fs/promises';
import { DuckE } from 'packages/duck/src/domain/entity';
import { DuckRepositoryI } from 'packages/duck/src/domain/ports/DuckRepository';

const filepath = 'db/ducks.json';

export function DuckFileRepository(): DuckRepositoryI {
  function addDuck(ducks: DuckE[]) {
    return fs.writeFile(filepath, JSON.stringify({ ducks }));
  }

  async function getDucks() {
    const rawFileContent = await fs.readFile(filepath, { encoding: 'utf-8' });
    const data = JSON.parse(rawFileContent);
    const storedDucks = data.ducks ?? [];

    return storedDucks;
  }

  return { addDuck, getDucks };
}

И мы экспортируем его, объединяя все репозитории, на случай, если в будущем мы добавим еще:

// src/adapter/repository/duck/index.ts
import { DuckFileRepository } from './duck/duckFileRepository';

function buildRepository() {
  return {
    duck: { ...DuckFileRepository() },
  };
}

export const Repository = buildRepository();

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

Ядро нашего приложения, уровень домена

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

Для простоты этого примера наша сущность будет иметь только один атрибут.

Объект:

// src/domain/entity/duck.ts
export class DuckE {
  constructor(
    readonly name: string
  ) {}
}

Порты:

// src/domain/ports/DuckRepository.ts
import { DuckE } from "../entity/duck";

export interface DuckRepositoryI {
  getDucks: () => Promise<DuckE[]>;
  addDuck: (ducks: DuckE[]) => void;
}

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

На объекте у нас могут быть проверки для нашего класса, а также внутри домена у нас может быть domain/services/duck.ts для большей бизнес-логики домена, которую можно использовать не только в Duck Entity (если вы посмотрите на этот другой проект, вы увидите пример этого).

Подведение итогов

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

Я очень надеюсь, что вы попробуете этот фреймворк. Вы можете проверить репозиторий на наличие исходного кода здесь. Не стесняйтесь связаться со мной на мой LinkedIn или оставить комментарий здесь. Спасибо за чтение и счастливого нового года!