프런트엔드용 MVC 서비스 알아보기: VanillaJS

65009 단어 mvcjavascript

소개하다.


이 글은 MVC 구조가 어떻게 전방 응용 프로그램을 만드는지 이해할 수 있는 세 편의 문장 중 첫 번째 문장이다.이 시리즈의 목적은 자바스크립트를 스크립트 언어로 사용하는 웹 페이지를 자바스크립트를 대상 언어로 사용하는 응용 프로그램으로 발전시켜 전방 응용 프로그램을 구축하는 방법을 알아보는 것이다.
이 문서의 첫 번째 기사에서 응용 프로그램은 VanillaJS를 사용하여 구축될 것이다.따라서 본고는 DOM과 관련된 대량의 코드를 개발할 것이다.그러나 응용 프로그램의 모든 부분이 어떻게 서로 관련되고 어떻게 구성되는지 이해하는 것은 매우 중요하다.
두 번째 기사에서는 JavaScript 코드를 TypeScript 버전으로 변환하여 향상합니다.
마지막으로 이전 글에서 우리는 코드를 바꾸어 Angular 프레임워크와 통합시킬 것이다.

프로젝트 구조


우리가 구축하고자 하는 내용을 이해하는 데 도움을 줄 수 있는 그림은 한 장도 없다. 다음은 GIF 그림으로 우리가 구축하고자 하는 응용 프로그램을 보여 준다.

이 프로그램은 문서의 DOM을 수정하고 모든 작업을 수행하는 단일 자바스크립트 파일로 구축할 수 있지만, 이것은 강력한 결합 코드이기 때문에 본문에 적용하지 않으려고 합니다.
MVC 아키텍처란?MVC는
  • 모델 - 응용 프로그램의 데이터를 관리합니다.이 모델들은 서비스에 제출될 것이기 때문에 빈약할 것이다.
  • 뷰 - 모델의 시각적 표현입니다.
  • 컨트롤러 - 서비스와 보기 사이의 링크.
  • 다음은 문제 영역의 파일 구조를 보여 줍니다.

    색인html 파일은 화포를 충당하고 전체 응용 프로그램은 그 위에 루트 요소를 사용하여 동적 구축을 할 것이다.또한 이 파일은 html 파일 자체에 링크되기 때문에 모든 파일을 불러오는 프로그램을 충당할 것이다.
    마지막으로 파일 아키텍처는 다음 JavaScript 파일로 구성됩니다.
  • 사용자.모델js - 사용자의 속성(모델)입니다.
  • 사용자.제어기.js - 서비스와 보기에 가입하는 것을 책임지는 사람.
  • 사용자.서비스js - 사용자의 모든 작업을 관리합니다.
  • 사용자.의견.js- 디스플레이 리셋과 변경을 책임집니다.
  • HTML 파일은 다음과 같습니다.
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    
        <title>User App</title>
    
        <link rel="stylesheet" href="style.css" />
      </head>
    
      <body>
        <div id="root"></div>
        <script src="models/user.model.js"></script>
        <script src="services/user.service.js"></script>
        <script src="controllers/user.controller.js"></script>
        <script src="views/user.view.js"></script>
        <script src="app.js"></script>
      </body>
    </html>
    

    모형(빈혈)


    이 예에서 구축된 첫 번째 클래스는 응용 프로그램 모델user입니다.모델js, 이것은 클래스 속성과 무작위 id를 생성하는 개인적인 방법으로 구성되어 있습니다. (이 id들은 서버의 데이터베이스에서 나올 수 있습니다.)
    모델에는 다음 필드가 포함됩니다.
  • id. 유일치.
  • 성명.사용자의 이름입니다.
  • 연령.사용자의 연령.
  • 완료.부울 값, 우리가 사용자를 목록에서 그릴 수 있는지 여부를 알려 줍니다.
  • 사용자모델js는 다음과 같습니다.
    /**
     * @class Model
     *
     * Manages the data of the application.
     */
    
    class User {
      constructor({ name, age, complete } = { complete: false }) {
        this.id = this.uuidv4();
        this.name = name;
        this.age = age;
        this.complete = complete;
      }
    
      uuidv4() {
        return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
          (
            c ^
            (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
          ).toString(16)
        );
      }
    }
    
    사용자에 대한 작업은 서비스에서 수행됩니다.모든 논리적 부하가 모델에 있기 때문에 서비스는 모델을 빈약하게 만든다.이 예에서, 우리는 하나의 그룹을 사용하여 모든 사용자를 저장하고, 읽기, 수정, 생성, 삭제 (CRUD) 사용자와 관련된 네 가지 방법을 구축할 것이다.이 서비스는 모델을 활용하여 LocalStorage에서 사용자 클래스로 추출된 대상을 실례화했다는 것을 주의해야 합니다.이는 LocalStorage가 데이터의 원형이 아니라 데이터만 저장하기 때문이다.백엔드에서 백엔드로 전송되는 데이터도 같은 상황이 발생하는데, 그것들의 종류를 실례화하지 않는다.
    우리 클래스의 구조 함수는 다음과 같다.
      constructor() {
        const users = JSON.parse(localStorage.getItem('users')) || [];
        this.users = users.map(user => new User(user));
      }
    
    사용자라는 클래스 변수를 정의했습니다. 사용자가 평면 대상에서 사용자 클래스의 원형 대상으로 바뀌면 모든 사용자를 저장합니다.
    다음에, 우리는 서비스에서 우리가 개발하고자 하는 모든 조작을 정의해야 한다.다음은 TypeScript의 행 대신 ECMAScript를 사용하여 이러한 작업을 보여 줍니다.
    add(user) {
        this.users.push(new User(user));
    
        this._commit(this.users);
      }
    
      edit(id, userToEdit) {
        this.users = this.users.map(user =>
          user.id === id
            ? new User({
                ...user,
                ...userToEdit
              })
            : user
        );
    
        this._commit(this.users);
      }
    
      delete(_id) {
        this.users = this.users.filter(({ id }) => id !== _id);
    
        this._commit(this.users);
      }
    
      toggle(_id) {
        this.users = this.users.map(user =>
          user.id === _id ? new User({ ...user, complete: !user.complete }) : user
        );
    
        this._commit(this.users);
      }
    
    데이터 저장소에 저장된 작업을 수행하는 제출 방법을 정의할 필요가 있습니다. (이 예는 LocalStorage입니다.)
      bindUserListChanged(callback) {
        this.onUserListChanged = callback;
      }
    
      _commit(users) {
        this.onUserListChanged(users);
        localStorage.setItem('users', JSON.stringify(users));
      }
    
    이 방법은 서비스를 만들 때 귀속된 리셋 함수를 호출합니다. bindUserListChanged 방법의 정의와 같습니다.이 리셋 함수는 화면에 있는 사용자 목록을 리셋하는 데 있어서 보기에서 나온다고 말할 수 있습니다.
    파일 사용자입니다.서비스구체적으로 다음과 같다.
    /**
     * @class Service
     *
     * Manages the data of the application.
     */
    class UserService {
      constructor() {
        const users = JSON.parse(localStorage.getItem('users')) || [];
        this.users = users.map(user => new User(user));
      }
    
      bindUserListChanged(callback) {
        this.onUserListChanged = callback;
      }
    
      _commit(users) {
        this.onUserListChanged(users);
        localStorage.setItem('users', JSON.stringify(users));
      }
    
      add(user) {
        this.users.push(new User(user));
    
        this._commit(this.users);
      }
    
      edit(id, userToEdit) {
        this.users = this.users.map(user =>
          user.id === id
            ? new User({
                ...user,
                ...userToEdit
              })
            : user
        );
    
        this._commit(this.users);
      }
    
      delete(_id) {
        this.users = this.users.filter(({ id }) => id !== _id);
    
        this._commit(this.users);
      }
    
      toggle(_id) {
        this.users = this.users.map(user =>
          user.id === _id ? new User({ ...user, complete: !user.complete }) : user
        );
    
        this._commit(this.users);
      }
    }
    
    뷰는 모델의 시각적 표현입니다.HTML 내용을 만들고 주입하는 대신 전체 보기를 동적으로 만들기로 했습니다. (많은 프레임에서 그렇습니다.)첫 번째는 뷰의 모든 변수를 DOM 방법으로 캐시하는 것입니다. 예를 들어 뷰 구조 함수와 같습니다.
    constructor() {
        this.app = this.getElement('#root');
    
        this.form = this.createElement('form');
        this.createInput({
          key: 'inputName',
          type: 'text',
          placeholder: 'Name',
          name: 'name'
        });
        this.createInput({
          key: 'inputAge',
          type: 'text',
          placeholder: 'Age',
          name: 'age'
        });
    
        this.submitButton = this.createElement('button');
        this.submitButton.textContent = 'Submit';
    
        this.form.append(this.inputName, this.inputAge, this.submitButton);
    
        this.title = this.createElement('h1');
        this.title.textContent = 'Users';
        this.userList = this.createElement('ul', 'user-list');
        this.app.append(this.title, this.form, this.userList);
    
        this._temporaryAgeText = '';
        this._initLocalListeners();
      }
    
    보기의 다음 가장 관련된 점은 보기와 서비스 방법(컨트롤러를 통해 발송)의 결합이다.예를 들어 bindAddUser 방법은 서비스에서 설명한addUser 작업을 수행하는 드라이버 함수를 매개 변수로 받습니다.bindXXX 메서드에는 각 뷰 컨트롤에 대한 EventListener가 정의되어 있습니다.보기에서 우리는 사용자가 화면에서 제공한 모든 데이터에 접근할 수 있음을 주의하십시오.그것들은 처리 함수를 통해 연결된다.
     bindAddUser(handler) {
        this.form.addEventListener('submit', event => {
          event.preventDefault();
    
          if (this._nameText) {
            handler({
              name: this._nameText,
              age: this._ageText
            });
            this._resetInput();
          }
        });
      }
    
      bindDeleteUser(handler) {
        this.userList.addEventListener('click', event => {
          if (event.target.className === 'delete') {
            const id = event.target.parentElement.id;
    
            handler(id);
          }
        });
      }
    
      bindEditUser(handler) {
        this.userList.addEventListener('focusout', event => {
          if (this._temporaryAgeText) {
            const id = event.target.parentElement.id;
            const key = 'age';
    
            handler(id, { [key]: this._temporaryAgeText });
            this._temporaryAgeText = '';
          }
        });
      }
    
      bindToggleUser(handler) {
        this.userList.addEventListener('change', event => {
          if (event.target.type === 'checkbox') {
            const id = event.target.parentElement.id;
    
            handler(id);
          }
        });
      }
    
    보기의 나머지 코드는 문서의 DOM을 처리합니다.파일 사용자입니다.뷰.구체적으로 다음과 같다.
    /**
     * @class View
     *
     * Visual representation of the model.
     */
    class UserView {
      constructor() {
        this.app = this.getElement('#root');
    
        this.form = this.createElement('form');
        this.createInput({
          key: 'inputName',
          type: 'text',
          placeholder: 'Name',
          name: 'name'
        });
        this.createInput({
          key: 'inputAge',
          type: 'text',
          placeholder: 'Age',
          name: 'age'
        });
    
        this.submitButton = this.createElement('button');
        this.submitButton.textContent = 'Submit';
    
        this.form.append(this.inputName, this.inputAge, this.submitButton);
    
        this.title = this.createElement('h1');
        this.title.textContent = 'Users';
        this.userList = this.createElement('ul', 'user-list');
        this.app.append(this.title, this.form, this.userList);
    
        this._temporaryAgeText = '';
        this._initLocalListeners();
      }
    
      get _nameText() {
        return this.inputName.value;
      }
      get _ageText() {
        return this.inputAge.value;
      }
    
      _resetInput() {
        this.inputName.value = '';
        this.inputAge.value = '';
      }
    
      createInput(
        { key, type, placeholder, name } = {
          key: 'default',
          type: 'text',
          placeholder: 'default',
          name: 'default'
        }
      ) {
        this[key] = this.createElement('input');
        this[key].type = type;
        this[key].placeholder = placeholder;
        this[key].name = name;
      }
    
      createElement(tag, className) {
        const element = document.createElement(tag);
    
        if (className) element.classList.add(className);
    
        return element;
      }
    
      getElement(selector) {
        return document.querySelector(selector);
      }
    
      displayUsers(users) {
        // Delete all nodes
        while (this.userList.firstChild) {
          this.userList.removeChild(this.userList.firstChild);
        }
    
        // Show default message
        if (users.length === 0) {
          const p = this.createElement('p');
          p.textContent = 'Nothing to do! Add a user?';
          this.userList.append(p);
        } else {
          // Create nodes
          users.forEach(user => {
            const li = this.createElement('li');
            li.id = user.id;
    
            const checkbox = this.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.checked = user.complete;
    
            const spanUser = this.createElement('span');
    
            const spanAge = this.createElement('span');
            spanAge.contentEditable = true;
            spanAge.classList.add('editable');
    
            if (user.complete) {
              const strikeName = this.createElement('s');
              strikeName.textContent = user.name;
              spanUser.append(strikeName);
    
              const strikeAge = this.createElement('s');
              strikeAge.textContent = user.age;
              spanAge.append(strikeAge);
            } else {
              spanUser.textContent = user.name;
              spanAge.textContent = user.age;
            }
    
            const deleteButton = this.createElement('button', 'delete');
            deleteButton.textContent = 'Delete';
            li.append(checkbox, spanUser, spanAge, deleteButton);
    
            // Append nodes
            this.userList.append(li);
          });
        }
      }
    
      _initLocalListeners() {
        this.userList.addEventListener('input', event => {
          if (event.target.className === 'editable') {
            this._temporaryAgeText = event.target.innerText;
          }
        });
      }
    
      bindAddUser(handler) {
        this.form.addEventListener('submit', event => {
          event.preventDefault();
    
          if (this._nameText) {
            handler({
              name: this._nameText,
              age: this._ageText
            });
            this._resetInput();
          }
        });
      }
    
      bindDeleteUser(handler) {
        this.userList.addEventListener('click', event => {
          if (event.target.className === 'delete') {
            const id = event.target.parentElement.id;
    
            handler(id);
          }
        });
      }
    
      bindEditUser(handler) {
        this.userList.addEventListener('focusout', event => {
          if (this._temporaryAgeText) {
            const id = event.target.parentElement.id;
            const key = 'age';
    
            handler(id, { [key]: this._temporaryAgeText });
            this._temporaryAgeText = '';
          }
        });
      }
    
      bindToggleUser(handler) {
        this.userList.addEventListener('change', event => {
          if (event.target.type === 'checkbox') {
            const id = event.target.parentElement.id;
    
            handler(id);
          }
        });
      }
    }
    
    아키텍처의 마지막 파일은 디렉터입니다.컨트롤러는 의존항 주입 (DI) 을 통해 두 개의 의존항 (서비스와 보기) 을 수신합니다.이러한 의존 관계는 컨트롤러의 개인 변수에 저장된다.그 밖에 구조 함수는 보기와 서비스 사이에 현식 연결을 구축한다. 왜냐하면 컨트롤러는 유일하게 쌍방에 접근할 수 있는 요소이기 때문이다.
    파일 사용자입니다.제어기.js는 다음과 같습니다.
    /**
     * @class Controller
     *
     * Links the user input and the view output.
     *
     * @param model
     * @param view
     */
    class UserController {
      constructor(userService, userView) {
        this.userService = userService;
        this.userView = userView;
    
        // Explicit this binding
        this.userService.bindUserListChanged(this.onUserListChanged);
        this.userView.bindAddUser(this.handleAddUser);
        this.userView.bindEditUser(this.handleEditUser);
        this.userView.bindDeleteUser(this.handleDeleteUser);
        this.userView.bindToggleUser(this.handleToggleUser);
    
        // Display initial users
        this.onUserListChanged(this.userService.users);
      }
    
      onUserListChanged = users => {
        this.userView.displayUsers(users);
      };
    
      handleAddUser = user => {
        this.userService.add(user);
      };
    
      handleEditUser = (id, user) => {
        this.userService.edit(id, user);
      };
    
      handleDeleteUser = id => {
        this.userService.delete(id);
      };
    
      handleToggleUser = id => {
        this.userService.toggle(id);
      };
    }
    
    우리 프로그램의 마지막 점은 프로그램 시작기입니다.우리의 예에서 우리는 app.js라고 부른다.응용 프로그램은 서로 다른 요소를 만들어서 실행한다. UserService, UserView, UserController, app.js 와 같다.
    const app = new UserController(new UserService(), new UserView());
    
    본고의 첫 번째 글에서 우리는 웹 응용 프로그램을 개발했다. 이 응용 프로그램에서 프로젝트의 구조는 MVC 체계 구조를 따르고 MVC 체계 구조에서 빈약한 모델을 사용했으며 논리적인 책임은 서비스에 있다.
    매우 중요한 것은 이 글의 교수 방법은 프로젝트가 서로 다른 파일에서의 구조와 서로 다른 책임을 이해하고 보기가 모델/서비스와 컨트롤러에 어떻게 완전히 독립하는지를 이해하는 것이다.
    다음 기사에서는 TypeScript를 사용하여 JavaScript를 강화할 것입니다. 이것은 웹 응용 프로그램 개발에 더욱 강력한 언어를 제공할 것입니다.JavaScript를 사용한다는 사실 때문에 DOM 관리를 위해 지루하고 반복되는 코드가 많이 작성되었습니다. (Angular 프레임워크를 사용하면 최소화할 수 있습니다.)
    이 글의 GitHub 분기는 https://github.com/Caballerog/VanillaJS-MVC-Users입니다.

    좋은 웹페이지 즐겨찾기