Otro enfoque para construir arquitectura en el frente

Buenas tardes queridos lectores. En este artículo intentaré hablar sobre el principio de construir una arquitectura para el frontend, en particular para React, ya que una buena arquitectura debe ser un elemento independiente del sistema.





En el marco del artículo, intentaré simplemente considerar y dar respuestas a los siguientes temas:





  • qué es la arquitectura y por qué debería estar limpia;





  • cómo escribir una arquitectura basada en servicios;





  • un ejemplo de construcción de una arquitectura para una aplicación de notas;





  • integración de arquitectura con react.






Introducción

, . . , , , .





- , - .





- , , , . /, , -, , , ..





, , . , . .





- , . , , , .





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);
      
      



"", , , :





  1. - , . , JSON, , - ;





  2. - , , , ;





  3. ( ) - , , , , .. ;





  4. - , . . , , ..;





  5. - , , ;





  6. - "" , .





. , .





, , . , , .





, . .





, 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 , , .





. , .





:





  1. , - ;





  2. - , .





:





-

- , - 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]);
    // ...
}
      
      



, :





  • - , , ;





  • - , , .





, .





, , .





, . , .





, , . - .





, , : , . , , .





El ejemplo considerado de construcción de una aplicación muestra solo recomendaciones y un enfoque para construir su arquitectura. Por lo tanto, su arquitectura debe ser suya y depender del significado mismo de la aplicación.








All Articles