Selamat siang, para pembaca yang budiman. Pada artikel ini saya akan mencoba berbicara tentang prinsip membangun arsitektur untuk frontend, khususnya untuk React, karena arsitektur yang baik harus menjadi elemen independen dari sistem.
Dalam kerangka artikel, saya akan mencoba untuk sekadar mempertimbangkan dan memberikan jawaban atas topik-topik berikut:
apa itu arsitektur dan mengapa harus bersih;
bagaimana menulis arsitektur yang didasarkan pada layanan;
contoh membangun arsitektur untuk aplikasi catatan;
integrasi arsitektur dengan react.
pengantar
, . . , , , .
- , - .
- , , , . /, , -, , , ..
, , . , . .
- , . , , , .
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]);
// ...
}
, :
- , , ;
- , , .
, .
, , .
, . , .
, , . - .
, , : , . , , .
Contoh yang dipertimbangkan dalam membangun aplikasi hanya menunjukkan rekomendasi dan pendekatan untuk membangun arsitektur Anda. Oleh karena itu, arsitektur Anda harus menjadi milik Anda dan bergantung pada arti aplikasi itu sendiri.