项目基础配置
工欲善其事必先利其器,一个好的项目架子(目录结构以及代码风格,即规范)就是一套教程。
规范是为了使得团队成员协作更为轻松而存在的,应当在个人习惯以及团队协作之间取得平衡,有红线、也有金线。
统一的规范也是使得团队发挥出 1 + 1 > 2 能力的基础。
必须遵循两个基本原则:
- 少数服从多数(协商讨论);
 - 用工具统一风格(lint 工具)。
 
常用工具:
- eslint
 - editorconfig
 - husky[pre-commit]
 - vscode plugins
 - 贯彻执行
 
代码相关
关键字:简单、可读、好维护
一切为了简单、可读,好维护。
策略模式
使用策略模式优化 if-else,换汤(switch)不换药(if),策略(map)更有效
// bad
const { channel } = this.state;
let chal = "";
switch (channel) {
  case "weixin":
    chal = 1;
    break;
  case "weixinGroup":
    chal = 2;
    break;
  case "qq":
    chal = 3;
    break;
  case "qrCode":
    chal = 4;
    break;
}
// good
const { channel } = this.state;
const channelCodeMap = {
  weixin: 1,
  weixinGroup: 2,
  qq: 3,
  chal: 4,
};
const chal = channelCodeMap[channel] || "";
此处是根据 channel 拿取对应 code,如果是根据后端 code 显示文案同理,如果是根据后端 code 进行不同操作也是同理,只不过将键值对的值更换为我们抽象的方法而已,将不同策略抽取出去,策略对象只负责选取策略,分离可变与不可变,即为策略模式。
想要深入戳此处:
定义常量
倘若一个字符串或是一个数字在项目中反复被使用(单个文件除外),就应当抽离出配置文件,而非在业务代码中写满意义不明的 number 与 string
// bad
const { channel } = this.state;
const channelCodeMap = {
  weixin: 1,
  weixinGroup: 2,
  qq: 3,
  chal: 4,
};
const chal = channelCodeMap[channel] || "";
// good
// constants.js
export const CHANNEL_CODES = {
  WEIXIN: 1,
  WEIXIN_GROUP: 2,
  QQ: 3,
  CHAL: 4,
};
// index.js
import { CHANNEL_CODES } from "path2config/constants";
const { channel } = this.state;
const channelCodeMap = {
  weixin: CHANNEL_CODES.WEIXIN,
  weixinGroup: CHANNEL_CODES.WEIXIN_GROUP,
  qq: CHANNEL_CODES.WEIXIN_QQ,
  chal: CHANNEL_CODES.WEIXIN_CHAL,
};
const chal = channelCodeMap[channel] || "";
使用 async/await
除开极其复杂的异步流程(极少情况,我没碰到过)需要使用 Promise,绝大多数情况 async/await 就可以 hold 住,并且少了括号嵌套,可读性极佳。
// bad
/* 获取用户信息后获取用户提现信息 */
getWithdrawInfo = () => {
  getUserInfo()
    .then((res) => {
      return getWithdrawInfo(res.userId);
    })
    .then((withdrawInfo) => {
      this.setState({
        withdrawInfo,
      });
    })
    .catch((err) => {
      console.log(err);
    });
};
// good
/* 获取用户信息后获取用户提现信息 */
getWithdrawInfo = async () => {
  const userInfo = await getUserInfo();
  const withdrawInfo = await getWithdrawInfo(userInfo.userId);
  this.setState({
    withdrawInfo,
  });
};
单一职责
一个方法做一件事,生命周期函数力求清晰简单,内部不要有业务逻辑,简单调用业务方法即可。
bad patter 就是在生命周期内写一大堆业务逻辑,看得头皮发麻,此处不做示例。
// good
componentDidMount() {
    // 获取账户信息
    this.getAccountInfo();
    // 获取提现配置信息
    this.getWithdrawConfig();
    // 获取收听时长
    this.getListenData();
}
类名拼接
使用 classnames 库(声明式)拼接复杂类名,而非各种拼接字符串(过程式)。
// bad
// 这个例子写的太累了。。。
const Button = (props) => {
  const { size, type, openPrefixCl, prefixCl } = props;
  let btnCls = "test-btn";
  if (size === "large") {
    btnCls += " test-btn__large";
  } else if (size === "medium") {
    btnCls += " test-btn__medium";
  } else {
    btnCls += " test-btn__small";
  }
  if (type === "primary") {
    btnCls += " test-btn__primary";
  } else if (type === "warning") {
    btnCls += " test-btn__warning";
  } else if (type === "danger") {
    btnCls += " test-btn__danger";
  } else {
    btnCls += " test-btn__default";
  }
  if (openPrefixCl === 1) {
    // 这个例子放在这里不合理 只是为了说明classnames的好处
    btnCls += ` ${prefixCl}`;
  }
  return <button className={btnCls}>点我</button>;
};
// good
import cx from "classnames";
const sizeClsMap = {
  small: "test-btn__large",
  medium: "test-btn__medium",
  large: "test-btn__small",
};
const typeClsMap = {
  primary: "test-btn__primary",
  warning: "test-btn__warning",
  danger: "test-btn__danger",
};
const Button = (props) => {
  const { size, type, openPrefixCl, prefixCl } = props;
  const sizeCl = sizeClsMap[size] || "test-btn__medium";
  const typeCl = typeClsMap[type] || "test-btn__default";
  const btnCls = cx({
    "test-btn": true,
    [typeCl]: true,
    [sizeCl]: true,
    [prefixCl]: openPrefixCl === 1,
  });
  return <button className={btnCls}>点我</button>;
};
(伪)计算属性
通过 get 关键字,抽离计算逻辑,根据 state 与 props 计算出衍生值。
// 使用 getters 封装 render 所需要的状态或条件的组合
// 对于返回 boolean 的 getter 使用 is- 前缀命名
// bad
render() {
    const { age } = this.state;
    const { school } = this.props;
    return (
        <>
        {
           age > 18 && (school === 'A' || school === 'B')
            ? <VipComponent />
            : <NormalComponent />
        }
        </>
    )
}
// good
get isVIP() {
    const { age } = this.state;
    const { school } = this.props;
    return age > 18 && (school === 'A' || school === 'B')
    }
render() {
    return (
        <>
        {this.isVIP ? <VipComponent /> : <NormalComponent />}
        </>
    )
}
为什么是(伪),因为通过这种效果模拟出来的计算属性和 Vue 提供的计算属性有本质区别。
对于 Vue 计算属性,Vue 官方文档中存在解释:”我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的依赖进行缓存的。只在相关依赖发生改变时它们才会重新求值。”
假使存在两个 state 属性 A.B,计算属性只依赖 A,不依赖 B。倘若 B 变化,计算属性不会重新计算。
但在 React 中,依旧会执行 render,所以 get 没有缓存,只是个语法糖。
降低 jsx 复杂度
render(jsx) 方法复杂到一定程度时,需要合理拆分成不同子组件或子方法(单一职责)。
bad pattern 不作展示,想像一个 200 行的 render 方法并且 jsx 中充斥着各种判断判断逻辑即可。
以一个模态框组件为例,模态框 Header 以及 Footer 基本都是可配置的,会存在许多判断,将其抽离分而治之可以更干净。
只要合理抽象方法,代码基本不会有太大问题。
renderModalHeader = () => {};
renderModalContent = () => {};
renderModalFooter = () => {};
render() {
    return (
      <div className="test-modal--container">
        {this.renderModalHeader()}
        {this.renderModalContent()}
        {this.renderModalFooter()}
      </div>
    );
  }
其他
- 
if 判断语句不通过尽早返回,避免一大堆逻辑后来个 else,很多时候直接 return 更清晰
 - 
在合适的场景使用 reduce/map/filter/some/every(声明式) 等方法替换 for 循环(过程式)
 - 
配置 webpack alias 避免导入模块相对路径过长的问题
 
// bad
import { Modal } from "../../../components/";
// good
import { Modal } from "@components";
- 公共组件需要有 propTypes 以及 defaultPropTypes 静态属性
 
看了 propTypes 属性就基本了解了组件的使用方式,代码即文档,如果 typescript 更好(interface)
- 相同的组件功能使用 HOC / render props/hooks 优化(高阶组件需要注意属性覆盖与静态方法丢失)