【TS小项目】任务看板

111 阅读6分钟

项目概述

实时添加新项目,利用项目看板记录项目完成情况,通过拖拽实现项目的状态切换
其中所有功能采用模块化开发,旨在将开发的子模块可以任意解耦复用

项目演示

1.gif

项目基础配置

package.json

{
  "name": "newdemo",
  "version": "1.0.0",
  "description": "",
  "main": "index.html",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack serve --open",
    "dev": "webpack",
    "build": "webpack --config webpack.config.prod.js"
  },
  "keywords": [],
  "author": "Nicky",
  "license": "ISC",
  "devDependencies": {
    "clean-webpack-plugin": "^4.0.0",
    "ts-loader": "^9.4.2",
    "typescript": "^5.0.4",
    "webpack": "^5.82.1",
    "webpack-cli": "^5.1.1",
    "webpack-dev-ser-ver": "^4.15.0"
  }
}

技术栈

开发语言:typescript

ts编译文件tsconfig.json相关配置

{
    "compilerOptions":{
        "target": "es5" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
        "experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */,
        /* Modules */
        "module": "ES2015" /* Specify what module code is generated. */,
        "sourceMap": true /* Create source map files for emitted JavaScript files. */,
        "outDir": "./dist/" /* Specify an output folder for all emitted files. */,
        "removeComments": true /* Disable emitting comments. */,
        "noEmitOnError": true /* Disable emitting files if any type checking errors are reported. */,
        "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
        "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
        "strict": true /* Enable all strict type-checking options. */,
        "skipLibCheck": true /* Skip type checking all .d.ts files. */,
        "noUnusedParameters": true
    }
}

相关配置说明

  • target: 编译出的文件支持的JS语法版本 es3es5es6/es2015es2016es2017es2018es2019es2020es2021es2022, or esnext
  • module: 支持的模块化语法nonecommonjsamdumdsystemes6/es2015es2020es2022esnextnode16, or nodenext
    CommonJS if target is ES3 or ES5ES6/ES2015.
  • experimentalDecorators: 是否开启装饰器语法
  • sourceMap: 在生成JS文件时是否生成sourceMap
  • outDir: 编译生成的所有文件所在的输出文件夹
  • removeComments: 生成文件是否保留注释
  • noEmitOnError: TS编译过程校验错误时,是否生成编译后文件
  • esModuleInterop: 是否将模块文件采用分离文件形式编译生成
  • forceConsistentCasingInFileNames: 是否保证导入文件名的大小写
  • strict: 是否开启严格模式——更多的校验规则
  • skipLibCheck: 是否跳过声明文件的校验
  • noUnusedParameters: 开启则在编写过程提示,避免在函数中定义了参数但没有使用,导致编译报错

构建工具:webpack

webpack构建项目 开发(默认)环境 的webpack.config.js相关配置

注: 当执行 package.json定义的npm脚本或 直接使用 webpack-cli 执行 webpack 指令时启动默认识别 webpack.config.js配置文件

const path = require("path"); //引入node.js中的path模块

//采用node.js模块语法
module.exports = {
  mode: "development",
  entry: "./src/app.ts", //入口文件
  output: {
    filename: "bundle.js", //出口文件名称,名称具体规则看下
    path: path.resolve(__dirname, "dist"), //出口文件所在目录
    publicPath: "dist",
  },
  devServer: {
    static: "./",
  },
  devtool: "inline-source-map",
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: "ts-loader",
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: [".ts", ".js"],
  },
};

webpack构建项目 生产环境 采用的webpack.config.prod.js相关配置

const path = require("path"); //引入node.js中的path模块
const CleanPlugin = require("clean-webpack-plugin");

//采用的是node.js模块语法
module.exports = {
  mode: "production",
  entry: "./src/app.ts", //入口文件
  output: {
    filename: "bundle.js", //出口文件名称
    path: path.resolve(__dirname, "dist"), //出口文件所在目录
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: "ts-loader",
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: [".ts", ".js"],
  },
  plugins: [new CleanPlugin.CleanWebpackPlugin()],
};

配置说明

  • mode: 模式分为 none | development | production, 此属性告诉webpack构建时使用什么环境的内置优化方案;
    • development下,会具备:Source Maps, 调试,热加载等功能;如果用cli执行项目,webpack -d 就表示 默认采用 了 --devtool等功能############
    • production下,会压缩代码,不具备热加载等优化执行降低调试性的操作
  • entry:
    • 定义单入口文件entry: string | [string],通常适用于开发一个库文件,一个功能模块,而非一个大型项目
    • 定义多个入口对象entry: { <entryChunkName> string | [string] } | {};详见:webpack.js.org/concepts/en… ; 场景相见:webpack.js.org/concepts/en…
  • output:编译后的文件输出配置
    • 尽管可能有多个入口文件,输出配置 存在一个对象
    • [name].bundle.js表示输出文件采用 入口文件名 区分编译后文件
    • [id].bundle.js表示输出文件采用 内部块id 区分编译后文件
    • [contenthash].bundle.js表示输出文件采用 内容生成哈希值 区分编译后文件
    module.exports = {
      entry: {
        app: './src/app.js',
        search: './src/search.js',
      },
      output: {
        filename: '[name].js',
        path: __dirname + '/dist',
      },
    };
    // writes to disk: ./dist/app.js, ./dist/search.js
    
  • devServer: 这个配置的前提是采用插件webpack-dev-server;此属性 配置具体说明 在⬇️相关插件处
  • devtool: 选择 某种  SourceMap 便于开发调试,不同的SourceMap对构建性能也有所影响;此处采用的 inline-source-map是将SourceMap内联到原始文件中而不创建新文件的方式,且浏览器调试过程能够看到编写的原始代码
  • module: 决定项目中不同模块的处理方式
    • rules:不同模块的规则集合
      • test:设定测试规则。表示此rule适用于通过此规则的所有模块
      • exclude:设定排除规则。排除所有符合此规则的模块
      • include:设定包含规则。包含所有符合此规则的模块
      • resource:与以上三个设置不包容,设置前三项会使此规则失效
      • use:指定用于 符合规则模块 的loaders,数组形式:
        use: [
          'style-loader',  //字符串loader
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
            },
          },
          {
            loader: 'less-loader',
            options: {
              noIeCompat: true,
            },
          },
        ],
        
  • resolve: 模块中的resolve级别比非模块 resolve高;用于补充设置解析文件的更多设置;如resolve.alias设置importrequire 模块的简称
    resolve: {
      alias: {
        Utilities: path.resolve(__dirname, 'src/utilities/'),
        Templates: path.resolve(__dirname, 'src/templates/'),
      },
    },
    
    //xx.js
    //简化引入:
    import Utility from 'Utilities/utility';
    

webpack 插件库:webpack.js.org/awesome-web…

webpack加载器:ts-loader

此插件属于 webpack的 语言规则&框架类加载器,下载⏬方式:

yarn add ts-loader --dev

npm install ts-loader -D

在webpack中的相关配置

module: {
  rules: [
    // all files with a `.ts`, `.cts`, `.mts` or `.tsx` extension will be handled by `ts-loader`
    { test: /.([cm]?ts|tsx)$/, loader: "ts-loader" }
  ]
}

webpack插件:webpack-dev-server

此插件适用于 开发场景(mode:development)下,用于启动服务器,因此下载⏬方式:

npm install --save-dev webpack-dev-server

npm install webpack-dev-server -D

在webpack中的相关配置说明

devServer: {
  static: './',
},
  • devServer: 默认在localhost:8080上, 此插件启动的 服务器路径 结构如下:http://[devServer.host]:[devServer.port]/[output.publicPath]/[output.filename]

webpack三方插件:clean-webpack-plugin

清理 项目构建后的 输出文件夹内容 ,包括所有 未使用 的静态文件,在项目rebuild后都会清理掉

入口文件app.ts

对于webpack构建的项目,设置了 入口文件 为app.ts,输入文件为 单一文件bundle.js (见⬆️配置),因此 webpack 会将此入口文件的导入模块一起进行打包

import { ProjectInput } from "./components/project-input";  //输入组件
import { ProjectClass } from "./components/project-list";   //项目组件

const proInput = new ProjectInput();
const actList = new ProjectClass("进行中");
const finList = new ProjectClass("已完成");

服务器启动页面index.html

  <head>
    <!-- ... -->
    <title>项目管理</title>
    <!-- ... -->
    <script src="dist/bundle.js" defer></script>
  </head>

说明

  • 由于 ts配置 语法为es5,因此 webpack 打包生成的 并非支持 模块语法的文件,因此script标签 需要 采用type=module页面就能正确引用
  • 设置defer属性,页面会先完成解析再进行外链脚本的获取执行,不会阻塞页面;默认情况下 页面执行到 脚本语句时 优先 获取并执行,会导致页面的阻塞;除此还有async属性,当设置此属性,页面解析过程会 同时 获取脚本文件,获取到后会 优先 执行,概率阻塞

项目功能

输入校验

TS功能逻辑

//校验方法
export interface ValidateOptions {
  value: string | number;
  required?: boolean; //等价于 required: boolean | undefined
  minLength?: number;
  maxLength?: number;
  min?: number;
  max?: number;
}

export function validate(opt: ValidateOptions): boolean {
  let isValid = true;
  if (opt.required) {
    isValid = isValid && opt.value.toString().trim().length !== 0;
  }
  if (opt.minLength != null && typeof opt.value === "string") {
    isValid = isValid && opt.value.length >= opt.minLength;
  }
  if (opt.maxLength != null && typeof opt.value === "string") {
    isValid = isValid && opt.value.length <= opt.maxLength;
  }
  if (opt.min != null && typeof opt.value === "number") {
    isValid = isValid && opt.value >= opt.min;
  }
  if (opt.max != null && typeof opt.value === "number") {
    isValid = isValid && opt.value <= opt.max;
  }
  return isValid;
}

TS语法说明

  • interface 定义 接口类型 关键字。纯类型结构声明,不包含 任何具体实现,可被 类实现,可以继承融合
  • 可选属性 [property]?:不是 必要结构,因此 require?:boolean 表示 require:boolean | undefined
  • != null : !===为类型判断操作符,会执行 类型转换,复杂类型会转为基础类型, 因此这个表达式 可以表示为 !== null || !== undefined;而!=====是严格相等操作符,不会执行类型转换
  • trim:方法从 字符串 的 两端 清除空格,返回一个 新的字符串,而不修改原始字符串。此上下文中的 空格 是指所有的 空白字符(空格、tab、不换行空格等)以及所有 行终止符 字符(如 LF、CR 等)。

公共装饰器功能

何为装饰器

官方定义:随着类在TypeScript和ES6中的引入,现存在一些场景需要额外的特性来支持注释或修改类和类成员。装饰器提供了一种为 类声明 和 成员 添加注释和元编程 的方法。

为何引入装饰器

在 未采用 箭头函数 的普通函数声明时,其调用指向的 this 可能引发异常,在调用方法时采用手动bind 可解决这类异常,也可以直接定义装饰器方法,在函数执行时 让装饰器 自动 对其进行bind

装饰器的使用

  1. 配置TS支持装饰器的使用: 在上述tsconfig.json配置信息中,可以看到 "experimentalDecorators": true;将 experimentalDecorators 这个字段设置为 true,则表示 开启 装饰器语法的支持
  2. ts中编写相关文件
    export function autoBind(_: any, _2: any, descriptor: PropertyDescriptor) {
      const originalMethod = descriptor.value;
      const options = {
        configurable: true,
        get() {
          const boundFn = originalMethod.bind(this);
          return boundFn;
        },
      };
      return options;
    }
    
    注:装饰器可以用在 类声明、方法、访问器、属性或参数上,不同位置的装饰器 具备不同的传入参数,此项目编写的 是针对 类方法进行设置的装饰器

TS相关模块说明

  • 自定义的autoBind装饰器方法 接收参数 说明:
    1. 类构造函数
    2. 当前成员名称
    3. 当前成员的属性描述符
  • _: 由于ts配置为 严格模式,未引用 的函数传参会报错,对于 不使用 的传参采用此标识可以让 TS忽略校验
  • PropertyDescriptor 属性描述符,一种对象类型,包含该属性的相关配置:
    除常规定义的 value 外,还有三个特殊的特性(attributes),也就是所谓的“标志”:
    • writable — 默认为 true,则值可以被修改,否则它是只可读的。
    • enumerable — 默认为 true,则会被在循环中列出,否则不会被列出。
    • configurable — 默认为 true,则此属性可以被删除,这些特性也可以被修改,否则不可以。
  • get: 通常设置的属性为 数据属性,增删改都直接操作该属性;而 getter/setter 这类 访问器属性 是用于 获取/设置 值,而非直接操作 数据本身;
    注意: 一个 存在get/set 的 访问器属性描述符 和普通的 数据描述符 相比没有 value 和 writable描述符
  • bind:在函数 执行时 创建并执行新的方法,此方法指向 提供的值
    const module = {
      x: 42,
      getX: function() {
        return this.x;
      }
    };
    
    const unboundGetX = module.getX;
    console.log(unboundGetX()); //undefined
    
    const boundGetX = unboundGetX.bind(module);
    console.log(boundGetX()); // 42
    

项目状态管理功能

状态结构设置

//项目类型
export enum ProjectStatus {
  Active,
  Finished,
}
export class Project {
  constructor(
    public id: string,
    public title: string,
    public description: string,
    public people: number,
    public state: ProjectStatus
  ) {}
}

状态模块设置

import { Project, ProjectStatus } from "../models/project-model";

//状态管理
type Listener<T> = (item: T[]) => void;
class State<T> {
  protected listeners: Listener<T>[] = [];
  addListener(listenerFn: Listener<T>) {
    this.listeners.push(listenerFn);
  }
}
export class ProjectState extends State<Project> {
  private projects: Project[] = [];
  private static instance: ProjectState;
  private constructor() {
    super();
  }
  //..
  static getInstance() {
    if (this.instance) return this.instance;
    this.instance = new ProjectState();
    return this.instance;
  }
  switchState(projectId: string, newState: ProjectStatus) {
    const _pro = this.projects.find((item) => item.id === projectId);
    if (_pro && _pro.state !== newState) {
      _pro.state = newState;
    }
  }
}

//项目唯一的状态实例
export const projectState = ProjectState.getInstance();

状态模块相关说明

  • <T>泛型:在定义环节弱化类型限制,但能更好表达输入输出关系,使得执行阶段能提供有效类型校验
  • protected,private类的 可访问性修饰符
    • protected 修饰的属性方法可以被继承的子类和自身访问,不可被实例访问
    • private 修饰的属性方法仅可被自身访问,不可被子类,实例访问
  • static:静态属性只能由类本身调用,类似于对象中不可枚举的属性值
  • 单例模式
    • 一个项目或应用中只 实例化 一次,全局 可以访问 相同实例;用于状态管理
    • 设置私有构造器,对外无法new实例,只能内部操作,确保此实例的唯一性
    • 设置一个 存放实例的 私有静态属性,外部无法访问修改,通过内部获取并进行管理操作;
    • 创建一个只能类调用的静态方法,用于 拦截并创建 实例
  • super:子类调用父类的构造器;获取父类属性方法
  • find:返回数组中第一个满足测试条件的元素;没有满足条件返回undefined

项目信息输入

HTML模版

<template id="project-input">
  <form class="user-input">
    <div class="form-control">
      <label for="title">标题</label>
      <input type="text" id="title" />
    </div>
    <div class="form-control">
      <label for="description">描述</label>
      <textarea id="description" rows="3"></textarea>
    </div>
    <div class="form-control">
      <label for="people">人员</label>
      <input type="number" id="people" step="1" min="0" max="10" />
    </div>
    <button type="submit" id="submit">添加项目</button>
  </form>
</template>

模版解析

  • template:标签内的 HTML元素在 页面加载时 不会渲染,之后可通过JS对其进行运行时渲染;此标签 通常搭配一个用于装载内部HTML元素的 额外HTML元素,作为 后续渲染的 选择容器
    • 有效获取此内部元素的方式是 获取其 HTML子元素 ,而HTML内容
    const container = document.getElementById("container");
    const template = document.getElementById("template");
    
    function clickHandler(event) {
      event.target.append(" — Clicked this div");
    }
    
    const firstClone = template.content.cloneNode(true);
    firstClone.addEventListener("click", clickHandler);  //无效
    container.appendChild(firstClone);
    
    const secondClone = template.content.firstElementChild.cloneNode(true);
    secondClone.addEventListener("click", clickHandler);  //有效
    container.appendChild(secondClone);
    
  • form:页面表单主控件,包含各种提交信息的交互元素
    • input:设置各种输入交互的元素
    • button:比input设置灵活的按钮元素。如 进行 表单提交,属性type设置为buttonsubmit类型 的按钮 会触发 表单提交的默认状态,影响页面的正常交互
    • label:在 用户层面 进行补充说明的标识。和inputtextarea这样的表单控件搭配——可以提供更清晰的 语义关联 和 扩大选中区域关联控件的label选中也会触发控件的选中)
      • label 和 表单控件的关联方式for=表单控件的name
      <label for="cheese">Do you like cheese?</label>
      <input type="checkbox" name="cheese" id="cheese">
      

TS功能

对于表单组件来说,需要的功能为:获取&绑定输入信息 以及 表单验证

export class ProjectInput extends CommonComponent<HTMLDivElement,HTMLFormElement> {
  private _title: HTMLInputElement;
  private _description: HTMLInputElement;
  private _people: HTMLInputElement;

  constructor() {
    super("project-input", "app", true, "user-input");
    this._title = this._element.querySelector("#title") as HTMLInputElement;
    this._description = this._element.querySelector(
      "#description"
    ) as HTMLInputElement;
    this._people = this._element.querySelector("#people") as HTMLInputElement;

    this._configure();
  }
  _configure() {
    //设置form的提交事件
    this._element.addEventListener("submit", this._submitHandler);
  }
  _renderContent(): void {}

  @autoBind
  private _submitHandler(e: Event) {
    e.preventDefault();
    const userInput = this._gatherInputs();
    if (Array.isArray(userInput)) {
      const [title, des, people] = userInput;
      projectState.addProject(title, des, people);
      console.log(title, des, people);
      this._clearInputs();
    }
  }

  private _gatherInputs(): [string, string, number] | void {
    const title = this._title.value;
    const description = this._description.value;
    const people = this._people.value;

    const titleValidate: _vv.ValidateOptions = {
      value: title,
      required: true,
    };
    const desValidate: _vv.ValidateOptions = {
      value: description,
      required: true,
      minLength: 5,
      maxLength: 100,
    };
    const peopleValidate: _vv.ValidateOptions = {
      value: +people,
      required: true,
      min: 1,
    };
    if (
      _vv.validate(titleValidate) &&
      _vv.validate(desValidate) &&
      _vv.validate(peopleValidate)
    ) {
      return [title, description, +people];
    } else {
      throw new Error("输入无效");
    }
  }
  private _clearInputs() {
    this._title.value = "";
    this._description.value = "";
    this._people.value = "";
  }
}

TS涉及知识点解析

  • TS 关键字 as
    • 类型断言,当TS推断的类型过于宽泛时,如果 明确 知道变量类型,可以进行断言 跳过类型校验
    • 另一种 断言方式(不推荐,和 JSX语法 搭配可能造成歧义):
      const inputEle = <HTMLInputElement>document.getElementById('user-output')!;
      
  • 装饰器使用
    • 结合上述 装饰器功能 的描述,此处装饰器 目的是:执行方法时,通过 getter拦截,自动 设置调用方法的 this指向
    • 使用方式:@[定义的装饰器方法名]
  • preventDefault
    • 事件实例 调用,用于 阻止 事件的 默认行为。例如 submit表单提交事件 默认会 触发信息提交,页面刷新,由于 不需要 此行为,调用此方法。
  • isArray
    • Array.isArray(value)校验value是否是数组类型
    • 同样可以使用 instanceof,差别在于此方法无法识别框架页的数组原型;
    const iframe = document.createElement("iframe");
    document.body.appendChild(iframe);
    const xArray = window.frames[window.frames.length - 1].Array;
    const arr = new xArray(1, 2, 3); // [1, 2, 3]
    
    Array.isArray(arr); // true
    arr instanceof Array; // false
    
  • 解构的概念[title, des, people] = userInput
    • 数组解构是顺序抽取数组元素进行匹配赋值;因此 以上解构等效于: title=userInput[0], des=userInput[1], people=userInput[2]
  • 输入校验,super用法 参见上述 知识点

项目生成

项目拖拽 子接口定义

//拖拽
export interface Draggable {
  dragStartHandler(event: DragEvent): void;
  dragEndHandler(event: DragEvent): void;
}
export interface DragTarget {
  dragOverHandler(event: DragEvent): void;
  dropHandler(event: DragEvent): void;
  dragLeaveHandler(event: DragEvent): void;
}

TS相关模块

export class ProjectItem
  extends CommonComponent<HTMLUListElement, HTMLLIElement>
  implements Draggable
{
  private project: Project;

  get persons() {
    return this.project.people === 1
      ? "单人项目"
      : `${this.project.people} 人群体项目`;
  }

  constructor(hostId: string, project: Project) {
    super("single-project", hostId, false, project.id);
    this.project = project;
    this._configure();
    this._renderContent();
  }

  @autoBind
  dragStartHandler(event: DragEvent): void {
    event.dataTransfer!.setData("text/plain", this.project.id);
    event.dataTransfer!.effectAllowed = "move";
    console.log(event);
  }
  @autoBind
  dragEndHandler(_: DragEvent): void {
    console.log("dragEND".toUpperCase());
  }

  _configure(): void {
    this._element.addEventListener("dragstart", this.dragStartHandler);
    this._element.addEventListener("dragend", this.dragEndHandler);
  }
  _renderContent(): void {
    this._element.querySelector("h2")!.textContent = this.project.title;
    this._element.querySelector("h3")!.textContent = this.persons;
    this._element.querySelector("p")!.textContent = this.project.description;
  }
}

TS语法说明

  • drag和drop相关事件
    • drag 事件在用户 拖动元素 或选择的文本时,每隔 几百毫秒 就会被触发一次

    • dragover 事件在可拖动的元素或者被选择的文本被拖进一个有效的放置目标时(每几百毫秒)触发。

    • dragenter 事件在可拖动的元素或者被选择的文本进入一个有效的放置目标时触发。

    • dragleave 事件在拖动的元素或选中的文本离开一个有效的放置目标时被触发。

    • dragstart 事件在用户开始拖动元素或被选择的文本时调用

    • dragend 事件在拖放操作结束时触发(通过释放鼠标按钮或单击 escape 键)

    • drop 事件在元素或选中的文本被放置在有效的放置目标上时被触发。

  • HTMLUListElement , HTMLLIElement
    • 比常规的 HTMLElement 接口定义的属性之外,HTMLUListElement接口类型还有为操作无序列表<ul>元素提供了特殊的属性,HTMLLIElement接口类型有用于操作列表元素的特定属性和方法。
  • 拖拽事件的dataTransfer
    • 推拽事件触发 过程用于 携带拖拽元素的 对象信息
  • textContent:
    获取HTML元素下的所有内容,包括<script><style>元素, 区别于 innerText 只能取到可识别的文字信息;区别于innerHTML获得HTML元素, 而textContent的值并非是HTML元素格式,因此更容易操作
  • !作用:
    TS 无法通过 逻辑识别 某个元素是否为空,作为开发者明确的前提下,元素后接!,TS明确 非空判断