각도/반응両方を経験して得られた堅牢なアプリケーション設計
51123 단어 reactarchitectureangulartypescript
概要
アプリケーション開発において設計は非常に重要です.
本記事では 대기사항アプリを例に、これまで取り組んできた 반응하다アプリケーションのアーキテクチャや実装パターンを紹介します.
このアーキテクチャは筆者の 모서리의가 있다を用いたアプリケーション開発の経験が元になっており、모서리의가 있다のオピニオンや 모서리의가 있다コミュニティで紹介された設計手法が含まれています.
コンセプト
コンポーネントとロジックの分離を基本とし、依存関係を単方向にします.
下記に実装例を示します.
https://github.com/puku0x/todo-angular
https://github.com/puku0x/todo-react
https://github.com/puku0x/todo-vue
데이터
アプリケーション内で扱うデータは用途に応じて区別しましょう.
🤔なぜ?
GET リクエストと POST リクエストで型が違うことはよくあります。また、API の仕様変更に柔軟に対応するためです。
모델
アプリケーションへの「入力」を表現するデータ型です.定数や미국 석유 학회のレスポンス等が該当します.
interface Todo {
id: number;
title: string;
completed: boolean;
}
DTO(데이터 전송 객체)
アプリケーションからの「出力」を表現するデータ型です.미국 석유 학회リクエスト等が該当します.
interface TodoCreateDto {
title: string;
}
interface TodoUpdateDto {
id: number;
title: string;
completed: boolean;
}
서비스
ドメインに関するビジネスロジックは 서비스に記述します.
実装は関数やオブジェクトでも構いませんが、
class
を用いた DI 회사パターンは強力なのでお勧めです.export class TodoService {
constructor(private readonly http: HttpClient) {}
fetchAll(offset?: number, limit?: number): Promise<Todo[]> {
return this.http.get(url, params).then(/* 一覧データ */);
}
fetch(id: number): Promise<Todo> {
return this.http.get(url, params).then(/* 詳細データ */);
}
create(todo: TodoCreateDto): Promise<Todo> {
return this.http.post(url, body).then(/* 登録データ */);
}
update(id: number, todo: TodoUpdateDto): Promise<Todo> {
return this.http.put(url, body).then(/* 更新データ */);
}
remove(id: number): Promise<number> {
return this.http.delete(url).then(/* 削除されたデータのID */);
}
}
// Axios や Fetch API のラッパー
export class HttpClient {
...
}
서비스を実装する際は単一責任の原則を心がけましょう. CQRS に倣って入力と出力で分けても良いです.この他、汎用的なロジックはユーティリティとして分離する場合があります.
export function debounce<T>(fn: (args: T) => void, delay: number) {
let id: number | undefined;
return (args: T) => {
clearTimeout(id);
id = window.setTimeout(() => fn(args), delay);
};
}
参考: Introduction to services and dependency injection - Angular
백화점
アプリケーション全体で使用する状態は 백화점に保存します.백화점の実装は 모서리의가 있다では NgRx, 반응では Redux Toolkit + React Redux を使うと良いでしょう.
状態はイミュータブルかつ、감속기が副作用を持たないように実装しましょう.フォームの状態は後述する 프로그램 진행자内で保持するのをお勧めします.
アプリケーションによっては 백화점が必要ない場合もあります.将来的に実装方法が変わる場合に備え、後述する 외관等の中間層を作っておくと良いでしょう.
외관
외관は 백화점の実装をコンポーネントから隠すための中間層です.
모서리의가 있다では 서비스では 연결として実装すると良いでしょう.
export const useTodoListFacade = (arg: { offset?: number; limit?: number }) => {
const { offset, limit } = arg;
const history = useHistory();
const location = useLocation();
const dispatch = useDispatch<AppDispatch>();
const todos = useSelector(todosSelector);
const isFetching = useSelector(isFetchingSelector);
const fetchAll = useCallback((arg: { offset?: number; limit?: number; } = {}) => {
return dispatch(fetchAllTodos(arg)).then(unwrapResult);
}, [dispatch]);
const changeOffset = useCallback(
(offset: number) => {
const params = new URLSearchParams(location.search);
params.set('offset', `${offset}`);
history.push(`/todos?${params}`);
},
[history, location.search]
);
const changeLimit = useCallback(
(limit: number) => {
const params = new URLSearchParams(location.search);
params.set('limit', `${limit}`);
history.push(`/todos?${params}`);
},
[history, location.search]
);
useEffect(() => {
fetchAll({ offset, limit });
}, [offset, limit, fetchAll]);
return {
isFetching,
todos,
changeOffset,
changeLimit,
fetchAll,
} as const;
};
외관から 서비스を呼び出すこともあります.프로그램 진행자
표상 성분内のロジックを抽出したものが 프로그램 진행자です.
프로그램 진행자にはフォームの値やローカルな状態を持たせましょう.
interface FormValues {
title: string;
completed: boolean;
}
参考: Formik, React Hook Form
모서리의가 있다では 서비스では 연결として実装すると良いでしょう.
export const useTodoUpdatePresenter = (arg: { todo: Todo; onUpdate?: (todo: TodoUpdateDto) => void; }) => {
const { todo, onUpdate } = arg;
// const [counter, setCounter] = useState(0);
// フォーム初期値
const initialValues = useMemo(() => {
return {
title: todo.title,
completed: todo.completed;
} as FormValues;
}, [todo]);
// バリデーション用
const validationSchema = useMemo(() => {
return Yup.object().shape({
title: Yup.string().required('Title is required.')
});
}, []);
const formik = useFormik({
enableReinitialize: true,
initialValues,
validationSchema,
onSubmit: (values) => {
const value = {...} as TodoUpdateDto;
onUpdate && onUpdate(value);
},
});
// const increment = useCallback(() => {
// setCounter(counter + 1);
// }, [counter]);
// const decrement = useCallback(() => {
// setCounter(counter - 1);
// }, [counter]);
return {
...formik,
// counter,
// increment,
// decrement,
} as const;
};
매개 변수
매개 변수は 라우터から 통합 리소스 포지셔닝 주소パラメータを取得し、페이지 구성 요소に渡します.
모서리의가 있다では 서비스では 연결として実装すると良いでしょう.
import { useLocation } from 'react-router-dom';
export const useTodoListParams = () => {
const location = useLocation();
const params = new URLSearchParams(location.search);
const limitParam = params.get('limit') || '10';
const offsetParam = params.get('offset') || '0';
return {
limit: +limitParam,
offset: +offsetParam,
} as const;
};
ページネーションの状態や検索条件は 통합 리소스 포지셔닝 주소パラメータに保存しましょう./users?offset=0&limit=10
페이지 구성 요소
페이지 구성 요소は 매개 변수から取得したデータを 컨테이너 구성 요소に渡します.
冗長に見えますが컨테이너 구성 요소以下では既に 통합 리소스 포지셔닝 주소パラメータが解決されている」という状況を作り出すことでデバッグやテストを容易にする狙いがあります.
import { TodoListContainer } from './containers';
import { useTodoListParams } from './todo-list.params';
export const TodoListPage = memo(() => {
const { offset, limit } = useTodoListParams();
return <TodoListContainer offset={offset} limit={limit} />;
});
페이지 구성 요소は使い回さず 통합 리소스 포지셔닝 주소毎に作成しましょう./users/1
interface RouterParams {
id: number;
}
export const useTodoDetailParams = () => {
const { id } = useParams<RouterParams>();
return { id } as const;
};
import { TodoDetailContainer } from './containers';
import { useTodoDetailParams } from './todo-detail.params';
export const TodoDetailPage = memo(() => {
const { id } = useTodoDetailParams();
return <TodoDetailContainer id={id} />;
});
컨테이너 구성 요소
페이지 구성 요소がパースした値を入力として受け取ります.
외관経由で 백화점の状態を 표상 성분に渡したり、행동を 파견하다したりします.
import { TodoUpdate } from '../components';
type Props = {
id: number;
};
export const TodoUpdateContainer = (props: Props) => {
const { id } = props;
const { update } = useTodoUpdateFacade({ id });
return todo ? <TodoUpdate todo={todo} onUpdate={update} /> : null;
};
통합 리소스 포지셔닝 주소パラメータの変更は 외관で行いましょう.표상 성분
모델を描画するコンポーネントです.
前述した 프로그램 진행자やユーティリティ、서비스内の静的メソッドを呼ぶ場合がありますが、基本的に표상 성분にはロジックを書かず描画に専念させましょう.
import { useTodoUpdatePresenter } from './todo-update.presenter';
type Props = {
todo: Todo;
onUpdate?: (todo: TodoUpdateDto) => void;
};
export const TodoUpdate: React.FC<Props> = (props) => {
const { todo, onUpdate } = props;
const {
errors,
values,
handleChange,
handleSubmit,
...
} = useTodoUpdatePresenter({ todo, onUpdate });
return <>...</>
}
スタイルガイド
ほぼ Angular coding style guide と同じです.これは、반응하다に足りないオピニオンを 모서리의가 있다から取り入れることで意思決定コストを下げるという狙いがあります.
命名規則
Angular coding style guide に倣い、ファイル名は 카바브 사건に統一しましょう.この命名規則は検索性に優れるため 모서리의가 있다以外のプロジェクトでも有用です.
xxx.model.ts
xxx.service.ts
xxx.hook.ts
xxx.presenter.ts
xxx.facade.ts
xxx.params.ts
xxx.state.ts
xxx.selector.ts
xxx.reducer.ts
xxx.action.ts
xxx.route.tsx
xxx.page.tsx
xxx.container.tsx
xxx.component.tsx
xxx.service.spec.ts
, xxx.component.spec.tsx
class
名やコンポーネント名は PascalCase、関数は 낙타 껍질に統一しましょう.コンポーネント名のサフィックスは 반응하다の場合だと冗長なので消してしまって良いかもしれません.
// Angular
@Component({...})
export class TodoListContainerComponent {}
@Component({...})
export class TodoListComponent {}
// React
export const TodoListContainer: React.FC = () => {...}
export const TodoList: React.FC = () => {...}
ディレクトリ構成
모델, 서비스, 상점, 페이지を起点にドメイン別にディレクトリを分けましょう.ユニットテストはテスト対象となるファイルと同じディレクトリに配置します(コロケーション).アプリケーション全体で共有するコンポーネントやユーティリティは
shared
等に入れると良いでしょう.- src/
- models/
- todo/
- todo.model.ts
- index.ts
- index.ts
- services/
- todo/
- todo.service.ts
- todo.service.spec.ts
- index.ts
- index.ts
- store/
- todo/
- actions/
- todo.action.ts
- todo.action.spec.ts
- index.ts
- reducers/
- todo.reducer.ts
- todo.reducer.spec.ts
- index.ts
- selectors/
- todo.selector.ts
- todo.selector.spec.ts
- index.ts
- states/
- todo.state.ts
- index.ts
- index.ts
- index.ts
- pages/
- todo/
- todo-create/
- components/
- todo-create/
- todo-create.component.tsx
- todo-create.component.spec.tsx
- todo-create.presenter.ts
- todo-create.presenter.spec.tsx
- index.ts
- index.ts
- containers/
- todo-create/
- todo-create.container.tsx
- todo-create.container.spec.tsx
- todo-create.facade.ts
- todo-create.facade.spec.tsx
- index.ts
- index.ts
- todo-create.page.tsx
- todo-create.page.spec.tsx
- todo-create.params.ts
- todo-create.params.spec.tsx
- index.ts
- todo-detail/
- todo-list/
- todo-update/
- todo.route.tsx
- index.ts
- index.ts
- shared/
- components/
- hooks/
- utils/
- index.ts
その他推奨する規約
타자 스크립트自体の書き方に関しては TypeScript Deep Dive 等を参考にします.基本は ESLint/TSLintと 더 예쁘다によって自動的に決定されるため混乱は少ないと思われます.
enum
ではなく、협회型を使いましょう. any
ではなく、 unknown
を使いましょう. その他
라우팅 구성 요소
react-router-dom
を利用する場合、ルーティング用のコンポーネントを作成する場合があります.모서리의가 있다の xxx-routing.module.ts
に相当します.import { TodoCreatePage } from './todo-create';
import { TodoDetailPage } from './todo-detail';
import { TodoListPage } from './todo-list';
import { TodoUpdatePage } from './todo-update';
export const TodoRoute: React.FC = () => {
return (
<Suspense fallback={<div>loading...</div>}>
<Switch>
<Route exact path="/todos" component={TodoListPage} />
<Route exact path="/todos/new" component={TodoCreatePage} />
<Route exact path="/todos/:id" component={TodoDetailPage} />
<Route exact path="/todos/:id/edit" component={TodoUpdatePage} />
</Switch>
</Suspense>
);
};
バンドルの肥大化を防ぐため、라우팅 구성 요소は必ず動的インポートしましょう.페이지 구성 요소も同様にすると良いでしょう.export const TodoPage = React.lazy(() =>
import('./todo.route').then((m) => ({ default: m.TodoRoute }))
);
アプリケーション全体のルーティングを管理するコンポーネントに渡します.export const App: React.FC = () => {
return (
<Suspense fallback={<div>loading...</div>}>
<Switch>
<Route path="/todos" component={TodoPage} />
<Route path="/users" component={...} />
<Route path="/settings" component={...} />
</Switch>
</Suspence>
);
};
t 구성json
any
を許可しないようにしましょう."compilerOptions": {
"strict": true
}
원자 설계
非推奨です.アプリケーションの実装に持ち込まないようにしましょう.
원자 설계はコンポーネント指向を理解するのに有用ですが、コロケーションが崩れたり、粒度に関する不要な議論が増えるなどのデメリットがあります.
원자 설계のような設計手法が必要になるのは 사용자 인터페이스ライブラリを構築する時と考えられますが、その場合のディレクトリ構成は以下のようにすると良いでしょう.
- libs/
- ui-components/
- button/
- button.component.tsx
- button.component.spec.tsx
- index.ts
- icon/
- input/
- search-input/
- select/
- option/
- option.component.tsx
- option.component.spec.tsx
- index.ts
- select.component.tsx
- select.component.spec.tsx
- index.ts
- index.ts
components/molecules
のように 粒度だけでディレクトリを分ける のは絶対にやめましょう.ビルドツール
react 응용 프로그램 만들기を使ってビルドした場合、마성이공대학ライセンスに違反するため、튀어나오다して
webpack.config.js
を修正するか、 Nx 等の他ツールに 移行 するのを強くお勧めします.終わりに
반응하다を始めた当初、アプリケーションをどのように設計すれば良いか分からず苦労しましたが、過去に携わった 모서리의가 있다アプリケーションでの設計手法や 모서리의가 있다コミュニティを通して得た知識が役に立ちました.
本記事で紹介したアーキテクチャは 반응하다アプリケーション用に作成しましたが、もちろん 모서리의가 있다アプリケーションにも適用可能です.これから 모서리의가 있다や 반응하다で開発を始める際の参考になれば幸いです.
https://github.com/puku0x/todo-angular
https://github.com/puku0x/todo-react
https://github.com/puku0x/todo-vue
Reference
이 문제에 관하여(각도/반응両方を経験して得られた堅牢なアプリケーション設計), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/puku0x/angular-react-2h4j텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)