2023-10-13 关于 Code 我的一些改变


本来想写《使用 React.js 后我有哪些改变?》,写完后发现我的改变不仅仅是 React.js 相关,还有一些代码周边的内容。

1. File Structure: 放弃按代码类型组织,选择按使用频度组织

使用 React.js 之前,我是按照代码类型进行组织文件结构的。HTML 放在 views 文件夹,JS 放在 scripts 文件夹,CSS 放在 styles 文件夹。文件结构如下:

├── scripts
│   ├── index.js
│   ├── login.js
│   └── settings.js
├── styles
│   ├── index.css
│   ├── login.css
│   └── settings.css
└── views
    ├── index.html
    ├── login.html
    └── settings.html

使用 React.js 之后,我开始按代码被调用、被修改的频度组织文件结构。登录页需要的 HTML JS CSS 都放在 login 文件夹,管理页需要的 HTML JS CSS 都放在 settings 文件夹。文件结构如下:

├── index
│   ├── index.css
│   ├── index.js
│   └── index.jsx
├── login
│   ├── login.css
│   ├── login.js
│   └── login.jsx
└── settings
    ├── settings.css
    ├── settings.js
    └── settings.jsx

这样做的好处是:

  1. 每次新增 features 或者修改 bugs 的 diff changesets 更集中;
  2. 在编辑器中进行文件夹跳跃更少,手指和大脑负担更小;
  3. 已有功能的修改、重构、拆分、删除更简单。

传统的 MVC 三层架构项目也可以按代码使用频度整理文件架构。

TJ

old style

├── controllers
│   ├── index.controller.js
│   ├── login.controller.js
│   └── settings.controller.js
├── services
│   ├── index.service.js
│   ├── login.service.js
│   └── settings.service.js
└── views
    ├── index.htm
    ├── login.htm
    └── settings.htm

new style

├── index
│   ├── index.controller.js
│   ├── index.htm
│   └── index.service.js
├── login
│   ├── login.controller.js
│   ├── login.htm
│   └── login.service.js
└── settings
    ├── settings.controller.js
    ├── settings.htm
    └── settings.service.js

2. 代码重复利用单位从 class 变小的 function

使用 React.js 之前,我使用 class 组织数据和功能,properties 和 methods 放在 class 中,并通过 class 使用。代码如下:

// account.js
export class Account {
  name = "";
  age = 0;

  constructor({ name, age }) {
    Account.validateAccount({ name, age });
    this.name = name;
    this.age = age;
  }

  getAccount() {
    return {
      name: this.name,
      age: this.age,
    };
  }

  setAccount({ name, age }) {
    Account.validateAccount({ name, age });
    this.name = name;
    this.age = age;
  }

  static validateAccount({ name, age }) {
    if (name.length <= 0) {
      throw new Error("Name is required");
    }

    if (age < 0) {
      throw new Error("Age must be greater than 0");
    }
  }

  introduce() {
    return `Hello, my name is ${this.name} and I am ${this.age} years old`;
  }
}

// hello.js
import { Account } from "account.js"

const tj = new Account({ name: "TJ", age: 31 });
tj.introduce()

使用 React.js 之后,我使用 module 组织数据和功能,数据就是最基本的变量,功能就是最基本的函数,它们可以被单独使用。代码如下:

// account.js
import z from "zod";
export const AccountSchema = z.object({
    name: z.string().min(1),
    age: z.number().min(0),
});

export const introduce = async ({ name, age }) => {
    return `Hello, my name is ${name} and I am ${age} years old`;
};

// hello.js
import { AccountSchema, introduce } from "account.js"

const tj = AccountSchema.parse({ name: "TJ", age: 31 });
introduce(tj)

这样做的好处是:

  1. plaintext object 比 class instance object 更容易理解,没有魔法
  2. function 比 class method 更容易管理、拆分、测试,可以避免写出巨型代码块。
  3. 代码行数少,阅读代码更轻松。

CSS 代码重复利用单位也从 BEM class 变小到 utility class

TJ

3. Coding: 放弃模版文件和注解等非标准特性,使用编程语言的标准特性

使用 React.js 之前,我会使用一些模版技术。代码如下:

// settings.vue
<script>
const items = [{ message: 'Foo' }, { message: 'Bar' }]
</script>

<template>
    <h1 class="title">Settings</h1>
    <li v-for="item in items">
      {{ item.message }}
    </li>
</template>

<style>
.title {
  color: red;
  font-weight: bold;
}
</style>

使用 React.js 之后:

// settings.jsx
import "./settings.css";

export function Settings() {
  const items = [{ message: "Foo" }, { message: "Bar" }];
  return (
    <div>
      <h1 class="title">Settings</h1>
      <ul>
        {items.map((item) => (
          <li>{item.message}</li>
        ))}
      </ul>
    </div>
  );
}

没有特殊语法,没有特殊文件类型,所有都是普通的 JS 代码。 import "./settings.css" 是 normal JS code, .map() 也是 normal JS code。

使用 React.js 之前,我会使用一些基于注解的框架。代码如下:

// OcpiListFilter.ts

import { IsDateString, IsInt, IsOptional } from 'class-validator';
import { Type } from 'class-transformer';

export class OcpiListFilter {
  @IsOptional()
  @IsDateString()
  dateFrom: string;

  @IsOptional()
  @IsDateString()
  dateTo: string;

  @IsOptional()
  @IsInt()
  @Type(() => Number)
  offset: number;

  @IsOptional()
  @IsInt()
  @Type(() => Number)
  limit: number;
}

使用 React.js 之后:

import z from 'zod';

const OcpiListFilterSchema = z.object({
    dateFrom: z.string().date().optional(),
    dateTo: z.string().date().optional(),
    offset: z.number().int().optional(),
    limit: z.number().int().optional(),
});

没有注解,没有黑魔法,只是最普通函数(function)。

总结

其实是很简单的几件小事,几句话就说清楚了。但是要展示 before and after 的代码,占用了很大篇幅,😂