Boa tarde, queridos leitores. Neste artigo tentarei falar sobre o princípio de construção de uma arquitetura para o frontend, em particular para o React, uma vez que uma boa arquitetura deve ser um elemento independente do sistema.
No âmbito do artigo, tentarei simplesmente considerar e fornecer respostas aos seguintes tópicos:
o que é arquitetura e por que deveria ser limpa;
como escrever uma arquitetura baseada em serviços;
um exemplo de construção de uma arquitetura para uma aplicação de notas;
integração da arquitetura com o react.
Introdução
, . . , , , .
- , - .
- , , , . /, , -, , , ..
, , . , . .
- , . , , , .
OSAL (Operating System Abstraction Layer) - , HAL (Hardware Abstraction Layer), , . .
BSAL (Browser System Abstraction Layer). , , .
, , NodeJS.
. . , , .
, , - .
, , - . , - ( ), .
:
import { createBrowserHistory } from 'history';
class HistoryAPI {
protected history = createBrowserHistory({});
push(pathname: string): void {
this.history.push(pathname);
}
}
export default HistoryAPI;
, , , - . - . , .
- , , - . , .
- . , . , .
, . new
. , . , , .
:
const netAPI = new NetAPI();
const newNoteRepository = new NetNoteRepository(netAPI);
const noteService = new NoteService(newNoteRepository);
"", , , :
- , . , JSON, , - ;
- , , , ;
( ) - , , , , .. ;
- , . . , , ..;
- , , ;
- "" , .
. , .
, , . , , .
, . .
, UML .
, JSON. . .
:
interface Note {
id: string;
name: string;
description: string;
created: number;
tags: string[];
}
interface Filter {
including: string;
tags: string[];
}
interface User {
name: string;
role: 'user' | 'admin';
}
- , .
:
interface Factory { makeNote(): Note; makeFilter(): Filter; }
, , : , - .
:
interface NoteHandler {
note: Note;
setName(name: string): void;
setDescription(description: string): void;
addTag(tag: string): void;
removeTag(tag: string): void;
validate(): Errors<Note>;
}
.
, API.
API
API , , , , ..
:
interface NetAPI {
get<T>(url: string): Promise<T>;
post<T, D>(url: string, data: D): Promise<T>;
}
API .
, axios, fetch , NetAPI
.
- , . , .
:
interface NoteRepository {
loadNotes(filter: Filter): Promise<Note[]>;
save(note: Note): Promise<boolean>;
}
, . , MockedNoteRepository
.
, , , .
,
, . , , - .
, , , .
:
interface Emmitable<E> {
on<K extends keyof E>(event: K, cb: (event: E[K]) => void): void;
off<K extends keyof E>(event: K, cb: (event: E[K]) => void): void;
emit<K extends keyof E>(event: K, data: E[K]): void;
}
interface NoteEvents {
change: undefined;
notesChange: Note[];
filterChange: Filter;
}
class NoteService implements Emmitable<NoteEvents> {
noteRepository: NoteRepository;
notes: Note[] = [];
filter: Filter = {
including: '',
tags: [],
}
private callbacks: {
[K in keyof NoteEvents]?: ((event: NoteEvents[K]) => void)[];
} = {};
on<K extends keyof NoteEvents>(event: K, cb: (event: NoteEvents[K]) => void): void {
if (!this.callbacks[event]) {
this.callbacks[event] = [];
}
const callbacks = this.callbacks[event];
if (!Array.isArray(callbacks)) {
return;
}
callbacks.push(cb);
}
off<K extends keyof NoteEvents>(event: K, cb: (event: NoteEvents[K]) => void): void {
if (!this.callbacks[event]) {
return;
}
const callbacks = this.callbacks[event];
if (!Array.isArray(callbacks)) {
return;
}
const index = callbacks.findIndex((aCallback) => aCallback === cb);
if (index !== -1) {
callbacks.splice(index, 1);
}
}
emit<K extends keyof NoteEvents>(event: K, data: NoteEvents[K]): void {
setTimeout(() => {
if (!this.callbacks[event]) {
return;
}
const callbacks = this.callbacks[event];
if (!Array.isArray(callbacks)) {
return;
}
callbacks.forEach((callback) => {
callback(data);
});
}, 0);
}
constructor(noteRepository: NoteRepository) {
this.noteRepository = noteRepository;
}
loadNotes(): Promise<boolean> {
return this.noteRepository
.loadNotes(this.filter)
.then((notes) => {
this.notes = notes;
this.emit('notesChange', this.notes);
this.emit('change', undefined);
return true;
});
}
saveNote(note: Note): Promise<boolean> {
return this.noteRepository
.save(note)
.then(() => this.loadNotes());
}
setFilter(filter: Filter): void {
this.filter = filter;
this.emit('filterChange', this.filter);
this.loadNotes();
}
}
Emmitable<E>
, , .
, , - , . change
. , filterChange
.
- , - .
, MobX , , .
. , .
:
, - ;
- , .
:
-
- , - index.ts
. , API, . new
. , . Lego, .
:
const netAPI = new NetAPI();
const tokenGetter = new TokenGetter(netAPI);
const authNetAPI = new AuthNetAPI(tokenGetter);
const historyAPI = new HistoryAPI();
const services: Services = {
note: new NoteService(new NoteRepository(authNetAPI)),
modal: new ModalService(),
page: new PageService(historyAPI),
auth: new AuthService(new AuthRepository(netAPI)),
user: new UserService(new UserRepository(authNetAPI)),
};
const application = new Application(services, tokenGetter, authNetRequest);
, .
, , .
:
function saveNote(services: Services, note: Note): void {
services.note.saveNote(note)
.then(() => {
services.modal.setModal({
type: 'success',
title: ' ',
description: '',
onClose: () => {
services.modal.setModal(undefined);
},
});
services.page.setPage({
type: 'notes',
});
})
.catch(() => {
services.modal.setModal({
type: 'error',
title: ' ',
description: '',
onClose: () => {
services.modal.setModal(undefined);
},
});
})
}
, . . :
class Scenarios {
private services: Services;
constructor(services: Services) {
this.services = services;
}
saveNote(note: Note): void {
// ...
}
}
.
, - , index.ts
:
const root = document.getElementById('root');
ReactDOM.render(<App services={services} />, root);
, :
export default React.createContext<Services>({} as Services);
:
interface AppProps {
services: Services;
}
const App: FC<AppProps> = ({ services }) => {
return (
<ServiceContext.Provider value={services}>
{<AppContainer />}
</ServiceContext.Provider>
);
};
:
export default function useService<K extends keyof Services>(service: K): Services[K] {
const services = useContext(ServiceContext);
return services[service];
}
:
const NotesPage: FC = () => {
const noteService = useService('note');
const [notes, setNotes] = useState<Note[]>(noteService.notes);
useEffect(() => {
const onChange = () => {
setNotes(noteService.notes.concat());
};
noteService.on('change', onChange);
return () => {
noteService.off('change', onChange);
};
}, [noteService]);
// ...
}
, :
- , , ;
- , , .
, .
, , .
, . , .
, , . - .
, , : , . , , .
O exemplo considerado de construção de um aplicativo mostra apenas recomendações e uma abordagem para construir sua arquitetura. Portanto, sua arquitetura deve ser sua e depender do próprio significado da aplicação.