Skip to content

前言 ​

这是一篇由保罗自己写代码总结出来的属于自己的“代码风格与实践”手册,在一度「借鉴」了不少类似项目规范的文章中,又给出了自己的观点。

本手册主要列举了 HTML、CSS、JavaScript、React JSX 和 Git Commit 的相关案例,如果你觉得有点意思,不妨可以继续往下看。

@Innei 提到,代码风格相关的东西建议配置一套 ESLint,可是这本规范手册里面不一定只是“约束”了代码风格,还包含了部分个人实践。例如使用图片的场景,应该默认让它保持原比例,而不是被拉伸。

代码风格方面,主要是使用 ESLint + AirBnb,也是是比较常规的方案,但 不推崇使用 Prettier,因为它有一些比较反人类的操作。

代码实践方面,则主要偏向于代码可读性、运行性能等方面综合考虑。

如果你觉得这里面的一些规则并不够合理,那么你可以大胆的和我提出来,也许我会好好思考一番。这个项目会长期提供维护,只是时间方面并不固定。

框架选型 ​

编写一个前端项目,技术和框架的选型是比较主要的,合适的选型可以减少实际开发产生的问题,提高代码的可读性和可维护性。

CSR 客户端渲染 ​

客户端渲染模式基本上就是 SPA(Single Page Application)单页应用,通过打包使用单或多个 JS 文件后,在客户端上以多种异步接口获取的形式展示页面(这种应用常常会有很多在局部组件内出现的加载动画)

这种渲染模式也往往是刚起步学习 Vue、React、Angular、Svelte、Solid 等前端框架时所接触的模式,这种开发模式使用广泛。

特点:

  • 服务器存储静态的 HTML、CSS、JS 文件
  • 客户端执行 JS 实现页面的首屏渲染,列表页会有加载效果

适用类型:

  • 在线协作、语音、会议
  • 图表、大屏数据等可视化展示
  • 单位转换、计算器、秒表等各种小工具
  • 各种行业领域业务的内容管理后台
  • 不考虑搜索引擎收录内容的页面

不适用的类型:

  • 个人博客、项目文档、企业官网等需要被搜索引擎收录内容的页面

像我的 小窝 后台项目,就是使用这种开发模式构建的。

SSR 服务端渲染 ​

服务端渲染模式包括传统的 MPA(Multi Page Application)多页面应用、SPA(Single Page Application)单页应用、以及 SSG(Static Site Generate)静态页面生成三种方案。

MPA ​

就是最传统的开发模式,常见于 PHP、Java 等纯后端实现的渲染前端页面。通过使用模板引擎,字符串拼接等方式将数据转换为视图的 HTML 内容,再由客户端执行 JS 代码进行「注水」,实现最终的交互功能。

其中的注水代码,往往由前端直接负责编写,不使用打包工具等前端生态链。

前面虽提出 CSR 适合编写各类工具、后台项目,但你依旧可以使用 SSR 框架实现 CSR 所呈现的功能和效果,只是我个人并不建议这样做。

特点:

  • 服务器存储静态的 HTML、CSS、JS 文件
  • 客户端执行 JS 实现页面的交互功能(注水)

适用类型:

  • 需要被搜索引擎收录内容的内容页面

不适用的类型:

  • 不考虑搜索引擎收录内容的页面

SPA ​

首屏渲染表现和 MPA 一致,但你可以使用前端的工作链和框架减少实际的开发成本。在开发模式上和 CSR 的形式有所重合,但最终呈现效果都可以比较的相似。

SSG 静态页面生成 ​

之所以把它单独归为一类,主要是它既不属于客户端渲染(非通过 JS 在客户端上后期生成 HTML),也不属于服务端渲染(服务器生成了 HTML),而是通过命令行的方式在开发机上生成静态的 HTML。

它和 SSR 服务端渲染的区别就是没有通过数据库等方式即时返回最新的内容。这种开发模式比较常见于需要快速实现文章、页面展示的场景。最常见的框架有 Hexo 和 GitHub 的 Jekyll。

特点:

  • 服务器存储静态的 HTML、CSS、JS 文件
  • 客户端执行 JS 实现页面的交互功能(注水)

适用类型:

  • 个人博客、企业网站等内容变化较少的展示性页面
  • 需要被搜索引擎收录内容的内容页面

不适用的类型:

  • 网站内容可被用户改变的因素较为丰富,例如微博、论坛等

使用 Hexo 博客框架再结合第三方的云服务,即可实现评论、点赞等需要后端的交互功能。

辅助工具 ​

不使用 Prettier ​

保罗在这里并不推荐直接使用「Prettier」进行代码的格式化,格式化后的效果可能无法满足你的个人意愿(需要换行区分的地方它去掉了,不需要换行的地方给你换行了)。

这是来自 AntFu 大佬的文章:为什么我不使用 Prettier

这是我加入的某技术群群友的视频:不推荐使用 Prettier

保罗也并不喜欢它自动处理的换行格式,他更喜欢根据实际功能将相同类型的参数或方法放在一行,来减少换行。而且过于依赖工具或许并不是一个好的选择。

jsx
// 格式化前
<DatePicker className="w-full" format="YYYY-MM-DD HH:mm" showTime
  disabledDate={disabledDate} disabledHours={disabledHours}
  minuteStep={15}
/>

// 格式化后
<DatePicker
  className="w-full"
  format="YYYY-MM-DD HH:mm"
  showTime
  disabledDate={disabledDate}
  disabledHours={disabledHours}
  minuteStep={15}
/>

让我比较难接受的还是它的 if 和 else 的规则,实际编写代码的时候这种换行规则比较紧凑,我的个人偏好还是希望增加一定的间距便于阅读。然而这种设定貌似并不能通过修改配置文件的方式进行修改。

js
const person = "Paul";

if (person === "Paul") {
  return "是一个前后端萌新程序员";
} else if (person === "Innei") {
  return "是一个在大厂实习过的大佬";
// } else if (person === "Jimmy") {
  // return "是我上家的后端前同事";
} else {
  return "我也不知道是谁";
}

这样写,读起来难道不是更清晰一些么?

js
const person = "Paul";

if (person === "Paul") {
  return "是一个前后端萌新程序员";
}
else if (person === "Innei") {
  return "是一个在大厂实习过的大佬";
}
// else if (person === "Jimmy") {
  // return "是我上家的后端前同事";
// }
else {
  return "我也不知道是谁";
}

如果要去掉多个条件,实际一个条件内的代码内容可能会很长,可读性就更差了。

js
const person = "Paul";

if (person === "Paul") {
  return "是一个前后端萌新程序员";
} else if (person === "Innei") {
  return "是一个在大厂实习过的大佬";
// } else if (person === "Jimmy") {
  // return "是我上家的后端前同事";
// } else {
  // return "我也不知道是谁";
}

善用强类型和辅助工具 ​

前端为什么现在都在写 TypeScript 了呢?这主要还是其「类型约束」的功能,可以减少很多潜在性的 Bug。编写 TypeScript 类型定义的过程中其实就形成了一本“字典”,可以让编辑器直接识别参数类型,检查参数是否合法。以下提供两个“潜在 Bug”的示例:

javascript
const enabled = false;

// 此处返回 false,和 React 处理方式不一样,如果使用 innerText 直接输出就是字符串 false 了
enabled && "<p>未开启功能</p>";
javascript
const enabled = -1;

if (enabled) {
  console.log("-1 隐式转换结果是 true,应该是 false 的");
}

编写 PHP 项目的时候也可以通过注释的方式来实现约束参数的类型,并且可以在注释中对函数功能进行描述,在编辑器可识别的情况下能大幅度的提高效率。如果直接编写普通注释,你还得新开一个文件才能查阅,就有些不便了。

编码习惯 ​

代码写的好不好,我认为并不是看技术栈新不新,使用到的 API 厉不厉害。哪怕是 jQuery 甚至原生写的项目,也可以写的很好。但新的语法糖还是建议使用,它的出现不就是为了解决可读性的问题么。

我对一个好的项目代码的评价是:良好的可读性、抽象性、扩展性,小功能不动用大框架,不安装多个相似功能的库,没有残留较多的无效代码。

抽象判断 ​

后台项目可能存在功能重合或相似的功能和页面,需要根据自我经验判断其可重用性。如果是同一个功能点的创建和编辑页面,大多数表单和功能都可复用的情况下,是完全可以合并成一个页面展示的,这样即便增加字段,也可以降低修改成本。

如果是两个毫不相干的功能点(登录日志、操作日志),它们接口不一致,但展示内容相似,我会选择编写两个页面去展示,因为对接的后端数据库可能不是同一张表,结构有些许差异,后期存在改动的可能性。

不使用奇怪的缩写命名 ​

众所周知,读代码时,一个好的命名很重要,宁愿写全也不要贪图一时爽快,一些奇怪的缩写建议不要使用!更不要使用无意义的 xxxOne、xxxTwo 或 xxxA、xxxB 这种模式命名,非常恶心!函数的名称也是,清晰的描述函数做了些什么比简短更重要!

javascript
// 👍 推荐的
const result;

// 👎 不推荐的
const rst; // 实际可能是指 Result
  • rst -> result
  • cbk -> callback
  • ap -> audioPermission
  • vp -> videoPermission
  • ...
javascript
// 👍 推荐的
const checkAudioPermission = () => {};
const startLoopPermissionAndDevices = () => {};

// 👎 不推荐的
const checkAP = () => {};
const startCheckPD = () => {};

多写注释 ​

编写大多数的业务场景,都需要一部分的逻辑思维能力,如果某个功能长期没有得到维护,等到你需要迭代的时候发现这里的代码逻辑全部要重新捋一捋,不仅仅是一点时间的消耗,如果代码质量不高,更是对自己的一种折磨。而且在快速完成迭代的过程中总会存在“技术债务”,也就是留坑。

遇到这种情况就只能通过多写注释的方式来改善,这样不仅对自己的代码负责,方便自己的后期维护,也能让同事更快的熟悉上手这块的逻辑。

当然这样做的缺点也是有的,提高了你在公司的可替代性,但是这种习惯对于一个开源项目来说,能让更多人理解代码并参与到你的项目中。就算你不写注释,代码一行嵌套好几个三元表达式什么的,能让其他人看到完全发麻,那下一个接手的也很大可能会进行小重构,折磨的是接手的人,又不是老板和公司。都是打工人,相互折磨也没有什么意义不是么

我并不鼓励每一个函数和过程都写大量的注释,如果在熟悉该函数本身用途及整体执行过程的情况下,只需要对部分比较绕的逻辑进行注释补充即可。如果一个并不复杂的功能处处都是注释,是不是反倒说明这块写的不够好呢?

不生产认知偏差 ​

后端某个接口返回的状态一般可能有很多种,而前端实际展示只 需要“合并同类项”把相同的效果放在一起,就会产生将后端值或其类型进行二次修改和重新转换的情况。

INITED // 可视为初始化成功
TRIGGERED // 被出发,也可视为初始化成功
STARTED // 进行中
ENDED // 结束了
FAILED // 也可视为结束了

简化之后就只有 init started ended 三个状态了,而如果直接覆盖后端接口原有的值,就有可能产生「认知偏差」,另一个人使用到该 API 或相似 API 返回的值,但未作转换,就会因不了解原始状态和新状态的生成条件,写出了错误的判断条件,从而导致实际展示效果不一致的情况。

如果实在需要这样的转换,我会选择以扩展原有返回值的情况下实现该功能。例如在 TypeScript 的 interface 上也可以轻松使用 extends 完成原始值的继承。

javascript
// Before
{
  status: "TRIGGERED"
}

// After
{
  status: "TRIGGERED",
  parsedStatus: "init"
}
typescript
interface IDetail {
  status: "INITED" | "TRIGGERED" | "STARTED" | "ENDED" | "FAILED"
}

interface IModifiedDetail extends IDetail {
  parsedStatus: "init" | "started" | "ended"
}

这样即便是使用 API 返回的原始值,或是新值,是否存在认知偏差,同一个接口多个前端来写,都不会影响到实际的展示效果。如果代码已经被多个位置使用,没法快速进行修改和调整,我会在捋清楚之后写下注释,免得到时候自己又要来迭代或修改需要使用到这个逻辑的时候,再次被反复折磨消耗时间。

不编写重复代码 ​

如果一个函数被多处使用,体积还不小,那就不应该写多次了。万一后面需要调整,很容易产生漏网之鱼不说,分别修改起来也比较麻烦。遇到这种问题,可以作为一个业务模块的通用函数写在 utils 一类的目录里面。

如果是 1-2 个组件(如 PC + 移动版)需要转换原有的数据类型变为组件接受的形式(例如 Select 组件的 value 和 label),逻辑并不复杂则并不强求抽出来(就是一个 map 函数能解决的事)。在我的 小窝 后台项目里,像这样功能的函数会写在一起,它们有个共同特点就是一行代码并不能解决问题。

  • 获取返回年份区间数组
  • 将枚举值转换为指定字符串
  • 根据属性拼接 Url / 字符串
  • ...

UI 组件也是一个大头,因此一个新需求的设计前期就应该考虑到项目现有的交互设计,从产品处开始避免将相同的交互产生新“变体”,这样无论是 UI 设计师还是前端开发的角度,都会节省大量不必要的精力开销,项目体积也能因更好的复用性减少膨胀速度。

不安装功能类似的库 ​

这样的事情很常见,例如一个项目内同时使用了多个相似的 UI 库、时间处理库等等,这些都会影响到项目实际打包的体积,体积越大,用户访问所需时间也会越长。况且一个库的功能可能覆盖的非常周全,代价就是其本身的体积也很大。

适当的功能容错 ​

像是 Intl.DateTimeFormat().resolvedOptions().timeZone 这个 API 就比较独特,一般情况下会返回一个带地区的时区信息(Asia/Shanghai),但在少数情况下,这里会返回一些让我们没法预料的值(Etc-GMT-8)。在没法保证系统环境和具体文档说明的情况下,这里需要提供一定的容错能力。

如果一个表单这里是必填项,可以留空并提醒出来,或是自动填写成第一个允许的值,防止因获取值异常导致业务整体无法继续进行的问题。

不残留无效内容 ​

一个项目的日常迭代经常需要 UI 上的调整。比较小的改动可能只需要修改 CSS 即可解决,但较大的改动可能就涉及到整个组件的变化,很有可能还会大改生成的 DOM 结构,弃用部分样式、图标、图片等资源。

这些资源特别容易残留在项目内,如果组件设计的初期没考虑到良好的扩展性,样式未 使用 Less 嵌套 和 CSS Modules 的模式编写,想要找到删改涉及到的具体样式信息就特别艰难。

或许干脆直接不要编写 CSS 文件,而是采用原子 CSS 框架。Tailwind 采用全局的简短类名形式,再结合 TreeShaking 过滤未使用项,就能较好解决这样的问题。但我也不是在这里单推 Tailwind,毕竟一些编写伪元素(像是::before 和 :last-child)等复杂的选择器实现,你依旧还是得编写原生 CSS 不是么。

只要从一开始设计组件的时候就要考虑到此类问题,在后期修改的时候就会变得更轻松一些。我会习惯性的在完成每次删改前,在项目内检索一遍涉及到的样式和图片等资源,并将它们一并删除,减少项目的打包体积。

多做重构 ​

对于大多数新手来说,都没法确保自己初次编写的代码是符合框架或团队标准的,在这种情况下就很容易出现不合理的抽象、架构以及过程。在时间允许的条件下,直接重写部分代码的“性价比”明显不如直接重构更合理。

切换新的技术,升级依赖库,使用语法糖,发现遗留 Bug... 都是重构中无意之间可能解决的问题。如果项目里面已经遇到了大量不应该重复的代码,你就可以开始着手思考解决方案了。

预先处理图片和媒体资源 ​

设计师提供的素材类型多且复杂,但我们可以大概归纳为两种类型:

  • 矢量图(svg)一般用于图标或插画
  • 位图(jpg / png)一般用于背景,插图

矢量图资源的处理 ​

矢量图可以使用 张鑫旭的压缩工具 压缩一波,然后再插入到项目里面,确保没有冗余代码(空格换行,无效属性和注释字符等)的同时减少存储占用,提高访问效率。

如果是用于图标的矢量图,单色图标可直接使用 currentColor 这样的 CSS 属性应用颜色,而不是单独维护多个颜色的图标文件,在 SVG 代码中一般将它设置在 stroke 和 fill 属性上。

位图资源的处理 ​

需要透明度的图片才使用 png 格式,否则都默认采用 jpg 格式,png 本身的格式特性会比 jpg 占用更大的空间。

图片本身的尺寸也得保证不会过大,在无 PPI 缩放屏幕上实际显示 500 像素的图片,兼顾到像 MacBook 或手机这样的高分屏设备,可以保留到 1000 像素。图片本身也最好使用 TinyPNG 一类的压缩工具处理一下。

如果不考虑兼容性,可直接采用 webp 格式,无需从 jpg 和 png 之间做选择。当然如果你使用了 OSS 或 CDN 一类的服务托管图片资源,那就更轻松了,可以自动根据客户端浏览器压缩和替换格式,提升访问速度。

关于图片优化的详情也可参考我的文章:谈谈 Web 图片浏览体验与优化

SVG 图标的引入 ​

可以直接作为一个 ReactElement 存在一个 .tsx 文件夹内,将多个图标进行导出,或者使用打包器提取指定目录下的所有 SVG 文件,作为页面全局的一部分,再使用 SVG 的 use 标签引用,这种方式也支持使用 color: currentColor 进行自定义填色。

html
<!-- 定义 -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:link="http://www.w3.org/1999/xlink">
  <symbol fill="none" viewBox="0 0 14 16" id="icon-security-fill">
    <path d="..." stroke="currentColor"></path>
    <path fill-rule="evenodd" clip-rule="evenodd" d="..." fill="currentColor"></path>
  </symbol>
</svg>

<!-- 使用 -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:link="http://www.w3.org/1999/xlink">
  <use href="#icon-security-fill"></use>
</svg>

SVG 插画的引入 ​

就是最纯粹的引入就行了,不玩任何套路。记得存放前走一波压缩工具!

tsx
import homeIllust from "@/images/home/HomeIllust.svg";

...

<img src={homeIllust} alt="Home Illust" />

参考文章 ​

本文档的部分规范,借鉴了他人经验,感谢他们为我提供了帮助。

开源声明 ​

本项目采用 MIT 形式开源,使用了如下技术进行构建:

  • VitePress