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}
/>

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

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 甚至原生写的项目,也可以写的很好。但新的语法糖还是建议使用,它的出现不就是为了解决可读性的问题么。

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

抽象判断

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

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

不使用奇怪的缩写命名

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

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 组件的 valuelabel),逻辑并不复杂则并不强求抽出来(就是一个 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 代码中一般将它设置在 strokefill 属性上。

位图资源的处理

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

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

如果不考虑兼容性,可直接采用 webp 格式,无需从 jpgpng 之间做选择。当然如果你使用了 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