前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >在前端中理解MVC服务之TypeScript篇

在前端中理解MVC服务之TypeScript篇

作者头像
学前端
发布2020-04-07 15:35:23
2K0
发布2020-04-07 15:35:23
举报
文章被收录于专栏:前端之巅前端之巅

简介

这篇文章是系列文中的第二篇,旨在了解 MVC 体系结构如何创建前端应用程序。

通过开发一个网页应用来理解构建前端应用的方法,其中,使用JavaScript作为脚本语言,并转向使用JavaScript/TypeScript作为面向对象程序开发的语言

在这一篇文章中,将使用第一个版本的TypeScript来构建应用程序,因此,本文将上次的程序由VanillaJS迁移到TypeScript中,但是,了解应用程序的所有部分以及如何构建它才是本文的重中之重。最后,在最后一篇文章中,我们将转换我们的代码,将其与Angular框架集成。

  • 第 1 部分。了解前端的 MVC 服务:VanillaJS 点击直达
  • 第 2 部分。了解前端的 MVC 服务:TypeScript 点击直达
  • 第 3 部分。了解前端的 MVC 服务:Angular 点击直达

项目架构:

可以参照上次的文章,可以理解我们所需要构建的内容。

什么是MVC架构?

MVC 架构是一个具有三个层/部分的体系

  • Model -管理应用的数据,这些模型将是不可见的(缺乏功能),因为它们将被引用于服务。
  • View 模型的直观表示,即用户所看到的部分
  • Controller - Model与View中的链接

下面,我们列出了项目中的文件结构 该文件将作为一个画布,整个应用将使用 “元素动态构建”。此外,此文件将充当所有文件的加载程序,因为它们将在 HTML 文件本身中链接。最后,我们的文件体系结构由以下 TypeScript 文件组成:

  • user.model.ts — 用户的属性(模型)
  • user.controller.ts — 负责将模型加入视图的部分
  • user.service.ts — 管理对用户的所有操作
  • user.views.ts — 负责刷新和更改显示屏幕上的内容

HTML 文件如下:

代码语言:javascript
复制
<!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="css/style.min.css" />
  </head>

  <body>
    <div id="root"></div>
  </body>
  <script src="bundle.js"></script>
</html>

我们可以看到的是,只有一个调用的文件已链接,而这个文件bundle.js将在TypeScript转换到JS并最小化应用之后生成。

我们不会专注于构建应用的工具,因为我们将负责用gulpfile来执行项目所有的转换任务

在这种情况下我们决定使用gulp工具,当然,如果用webpack也是可以的。

如果你知道JS,你能够读懂它的代码的意思,并且你能够几乎完全的理解我们所执行的任务,在我们的这个案例中,我们使用browserity插件来打包、创建模块系统并执行TS到JS的转换。

代码语言:javascript
复制
const gulp = require('gulp');
const browserify = require('browserify');
const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const sourcemaps = require('gulp-sourcemaps');
const concat = require('gulp-concat');
const minifyCSS = require('gulp-minify-css');
const autoprefixer = require('gulp-autoprefixer');
const useref = require('gulp-useref');
const rename = require('gulp-rename');
const { server, reload } = require('gulp-connect');

gulp.task('watch', function() {
  gulp.watch('src/**/*.ts', gulp.series('browserify'));
  gulp.watch('src/**/*.html', gulp.series('html'));
  gulp.watch('src/**/*.css', gulp.series('css'));
});

gulp.task('html', function() {
  return gulp
    .src('src/*.html')
    .pipe(useref())
    .pipe(gulp.dest('dist'))
    .pipe(reload());
});

gulp.task('css', function() {
  return gulp
    .src('src/**/*.css')
    .pipe(minifyCSS())
    .pipe(autoprefixer('last 2 version', 'safari 5', 'ie 8', 'ie 9'))
    .pipe(concat('style.min.css'))
    .pipe(gulp.dest('dist/css'))
    .pipe(reload());
});

gulp.task('images', function() {
  gulp.src('src/**/*.jpg').pipe(gulp.dest('dist'));
  return gulp.src('src/**/*.png').pipe(gulp.dest('dist'));
});

gulp.task('serve', () => {
  server({
    name: 'Dev Game',
    root: './dist',
    port: 5000,
    livereload: true,
  });
});

gulp.task('browserify', function() {
  return browserify({
    entries: './src/app.ts',
  })
    .plugin('tsify')
    .bundle()
    .on('error', function(err) {
      console.log(err.message);
    })
    .pipe(source('bundle.js'))
    .pipe(buffer())
    .pipe(sourcemaps.init({ loadMaps: true }))
    .pipe(sourcemaps.write('./'))
    .pipe(gulp.dest('dist'))
    .pipe(reload());
});

gulp.task(
  'default',
  gulp.series(['browserify', 'html', 'css', 'images', gulp.parallel('serve', 'watch')]),
);

Models (Anemic 贫血模式)

这个示例中的第一个生成Class是Model,它由Class属性和生成随机ID组成 user.model.ts 模型将具有以下字段:

  • id 唯一值
  • name 用户名
  • age 用户年龄
  • complete bool值,可以知道此条数据是否有用 使用TS构建Class.但,构造函数接受个纯对象,该对象将通过Window从用户数据输入中提供,此对象需要有一个Interface接口,以便任何纯对象都不能实例化,而是满足定义的接口的对象。User UserLocalStorage UserDtouser.model.ts 文件中写下以下代码:
代码语言:javascript
复制
/**
 * @class Model
 *
 * Manages the data of the application.
 */

export interface UserDto {
  name: string;
  age: string;
  complete: boolean;
}

export class User {
  public id: string;
  public name: string;
  public age: string;
  public complete: boolean;

  constructor(
    { name, age, complete }: UserDto = {
      name: null,
      age: null,
      complete: false
    }
  ) {
    this.id = this.uuidv4();
    this.name = name;
    this.age = age;
    this.complete = complete;
  }

  uuidv4(): string {
    return (([1e7] as any) + -1e3 + -4e3 + -8e3 + -1e11).replace(
      /[018]/g,
      (c: number) =>
        (
          c ^
          (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
        ).toString(16)
    );
  }
}

Service

对用户执行的操作在服务中执行。该服务允许Model为贫血模式,因为所有的逻辑负载都在其中。

在此特定情况下,我们将使用数组来存储所有用户,并生成与读取、修改、创建和删除 (CRUD) 用户关联的四种方法。

应该注意的是,服务使用模型,实例化从类提取的对象。这是因为只存储数据,而不是存储数据的原型。从后端到前端的数据也是如此 , 它们没有实例化其类。

我们类的构造函数如下所示:

代码语言:javascript
复制
constructor() {
  const users: UserDto[] = JSON.parse(localStorage.getItem('users')) || [];
  this.users = users.map(user => new User(user));
}

提示:

我们定义了一个名为“类变量”的Class变量,该变量在所有用户从纯对象转换为类的原型对象后存储它们。

在服务中我们必须定义的是我们想要创建的每个操作。使用 TypeScript 如下所示:

代码语言:javascript
复制
add(user: User) {
    this.users.push(new User(user));

    this._commit(this.users);
  }

  edit(id: string, userToEdit: User) {
    this.users = this.users.map(user =>
      user.id === id
        ? new User({
            ...user,
            ...userToEdit
          })
        : user
    );

    this._commit(this.users);
  }

  delete(_id: string) {
    this.users = this.users.filter(({ id }) => id !== _id);

    this._commit(this.users);
  }

  toggle(_id: string) {
    this.users = this.users.map(user =>
      user.id === _id ? new User({ ...user, complete: !user.complete }) : user
    );

    this._commit(this.users);
  }

它仍有待定义负责存储在我们的数据存储中执行的操作的方法

代码语言:javascript
复制
bindUserListChanged(callback: Function) {
  this.onUserListChanged = callback;
}

_commit(users: User[]) {
  this.onUserListChanged(users);
  localStorage.setItem('users', JSON.stringify(users));
}

此方法调用在创建 zervice 时绑定的函数,如方法的定义中所示。我已经可以告诉你,callbackbindUserListChanged是从视图产生的功能,并负责刷新屏幕上的用户列表。

user.service.ts 文件如下所示:

代码语言:javascript
复制
import { User, UserDto } from '../models/user.model';

/**
 * @class Service
 *
 * Manages the data of the application.
 */
export class UserService {
  public users: User[];
  private onUserListChanged: Function;

  constructor() {
    const users: UserDto[] = JSON.parse(localStorage.getItem('users')) || [];
    this.users = users.map(user => new User(user));
  }

  bindUserListChanged(callback: Function) {
    this.onUserListChanged = callback;
  }

  _commit(users: User[]) {
    this.onUserListChanged(users);
    localStorage.setItem('users', JSON.stringify(users));
  }

  add(user: User) {
    this.users.push(new User(user));

    this._commit(this.users);
  }

  edit(id: string, userToEdit: User) {
    this.users = this.users.map(user =>
      user.id === id
        ? new User({
            ...user,
            ...userToEdit
          })
        : user
    );

    this._commit(this.users);
  }

  delete(_id: string) {
    this.users = this.users.filter(({ id }) => id !== _id);

    this._commit(this.users);
  }

  toggle(_id: string) {
    this.users = this.users.map(user =>
      user.id === _id ? new User({ ...user, complete: !user.complete }) : user
    );

    this._commit(this.users);
  }
}

Views

视图是模型的可视表示形式。我们决定动态创建整个视图,而不是创建 HTML 内容并注入它(就像在许多框架中所做的那样)。应该做的第一件事是通过 DOM 方法缓存视图的所有变量,如视图构造函数所示:

代码语言:javascript
复制
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();
}

视图的下一个最相关的点是View与Service (将通过Controller发送)的结合。例如,bindAddUseraddUser接收一个驱动程序函数作为参数,该参数将执行服务中描述的操作。在方法中,将定义每个视图控件的 。请注意,从视图中,我们可以访问用户提供的所有数据,这些数据通过函数连接。 bindXXXEventListenerhandler


代码语言:javascript
复制
bindAddUser(handler: Function) {
  this.form.addEventListener('submit', event => {
    event.preventDefault();

    if (this._nameText) {
      handler({
        name: this._nameText,
        age: this._ageText
      });
      this._resetInput();
    }
  });
}

bindDeleteUser(handler: Function) {
  this.userList.addEventListener('click', event => {
    if ((event.target as any).className === 'delete') {
      const id = (event.target as any).parentElement.id;

      handler(id);
    }
  });
}

bindEditUser(handler: Function) {
  this.userList.addEventListener('focusout', event => {
    if (this._temporaryAgeText) {
      const id = (event.target as any).parentElement.id;
      const key = 'age';

      handler(id, { [key]: this._temporaryAgeText });
      this._temporaryAgeText = '';
    }
  });
}

bindToggleUser(handler: Function) {
  this.userList.addEventListener('change', event => {
    if ((event.target as any).type === 'checkbox') {
      const id = (event.target as any).parentElement.id;

      handler(id);
    }
  });
}

视图的其他代码将处理文档的 DOM。user.view.ts文件如下所示:

代码语言:javascript
复制
import { User } from '../models/user.model';

/**
 * @class View
 *
 * Visual representation of the model.
 */

interface Input {
  key: string;
  type: string;
  placeholder: string;
  name: string;
}
export class UserView {
  private app: HTMLElement;
  private form: HTMLElement;
  private submitButton: HTMLElement;
  private inputName: HTMLInputElement;
  private inputAge: HTMLInputElement;
  private title: HTMLElement;
  private userList: HTMLElement;
  private _temporaryAgeText: string;

  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 }: Input = {
      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: string, className?: string) {
    const element = document.createElement(tag);

    if (className) element.classList.add(className);

    return element;
  }

  getElement(selector: string): HTMLElement {
    return document.querySelector(selector);
  }

  displayUsers(users: User[]) {
    // 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') as HTMLInputElement;
        checkbox.type = 'checkbox';
        checkbox.checked = user.complete;

        const spanUser = this.createElement('span');

        const spanAge = this.createElement('span') as HTMLInputElement;
        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 as any).className === 'editable') {
        this._temporaryAgeText = (event.target as any).innerText;
      }
    });
  }

  bindAddUser(handler: Function) {
    this.form.addEventListener('submit', event => {
      event.preventDefault();

      if (this._nameText) {
        handler({
          name: this._nameText,
          age: this._ageText
        });
        this._resetInput();
      }
    });
  }

  bindDeleteUser(handler: Function) {
    this.userList.addEventListener('click', event => {
      if ((event.target as any).className === 'delete') {
        const id = (event.target as any).parentElement.id;

        handler(id);
      }
    });
  }

  bindEditUser(handler: Function) {
    this.userList.addEventListener('focusout', event => {
      if (this._temporaryAgeText) {
        const id = (event.target as any).parentElement.id;
        const key = 'age';

        handler(id, { [key]: this._temporaryAgeText });
        this._temporaryAgeText = '';
      }
    });
  }

  bindToggleUser(handler: Function) {
    this.userList.addEventListener('change', event => {
      if ((event.target as any).type === 'checkbox') {
        const id = (event.target as any).parentElement.id;

        handler(id);
      }
    });
  }
}

Controllers

在我们的项目中,最后一个文件就是Controller,它将通过依赖注入(DI)来接受其具有的ServiceView服务项 这些依赖项存储在控制器中的私有变量中。此外,构造函数使视图和服务之间的显式连接,因为控制器是唯一可以访问双方的元素。 user.controller.ts文件如下所示:

代码语言:javascript
复制
import { User } from '../models/user.model';
import { UserService } from '../services/user.service';
import { UserView } from '../views/user.view';

/**
 * @class Controller
 *
 * Links the user input and the view output.
 *
 * @param model
 * @param view
 */
export class UserController {
  constructor(private userService: UserService, private 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: User[]) => {
    this.userView.displayUsers(users);
  };

  handleAddUser = (user: User) => {
    this.userService.add(user);
  };

  handleEditUser = (id: string, user: User) => {
    this.userService.edit(id, user);
  };

  handleDeleteUser = (id: string) => {
    this.userService.delete(id);
  };

  handleToggleUser = (id: string) => {
    this.userService.toggle(id);
  };
}

app.ts

我们应用程序的最后一点是应用程序启动器。在我们的案例中,我们称它为app.ts ,应用程序通过创建不同的元素来执行:UserServiceUserViewUserController

app.ts文件中所示:

代码语言:javascript
复制
import { UserController } from './controllers/user.controller';
import { UserService } from './services/user.service';
import { UserView } from './views/user.view';

const app = new UserController(new UserService(), new UserView());

总结

在第二篇文章中,我们开发了一个 Web 应用程序,其中项目的结构遵循 MVC 体系结构,其中使用了贫血模型,逻辑的责任在于Service。

了解不同文件中具有不同职责的项目结构以及视图如何完全独立于Model/Service和Controller非常重要。还必须注意,在本文中,我们将应用程序从 JavaScript 迁移到 TypeScript,从而允许我们获取类型化代码,帮助开发人员最大限度地减少错误并了解其每个部分的作用。

在本系列的下一篇文章中,我们将将 TypeScript 代码迁移到 Angular。这种迁移到框架将意味着我们不必处理使用 DOM 的复杂性和重复性。

这篇文章的GitHub分支位于 https://github.com/Caballerog/TypeScript-MVC-Users

本文来源于 Medium 作者:Carlos Caballero 转载请注明出处!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-12-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一起学前端 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是MVC架构?
  • Models (Anemic 贫血模式)
  • Service
  • 提示:
  • Views
  • Controllers
  • app.ts
  • 总结
相关产品与服务
数据保险箱
数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档