TypeScript 或 泪水
Source: Dev.to
另请参阅: Backend Quality Gates
后端 linters 捕获异步陷阱。类型检查器防止运行时崩溃。现在轮到前端了。
问题
JavaScript 静默失败。这就是整个问题的简短概括。
- 你用错误的参数调用函数 → 它仍然会运行。
- 你访问一个不存在的属性 → 它仍然会运行。
- 你忘记处理
null→ 它仍然会运行。
一切都在运行。却没有任何效果。用户抱怨。你根本不知道原因。
“这没问题。一切都好。”
— 开玩笑的,这是一团混乱。
和后端一样,这些甚至还没有进行测试。我们只是在检查代码是否 明显 破损,才去费心运行真正的测试。标准仍然低得离谱。至少让我们把它踩过去吧。
为什么 JavaScript 需要类型
JavaScript 没有类型。这是一个设计决策。它是错误的。
function processUser(user) {
return user.name.toUpperCase();
}
user 是什么? 一个对象?一个字符串?一个 Promise?JavaScript 并不知道。JavaScript 并不在乎。JavaScript 认为一切皆有可能。
JavaScript 是个乐观主义者。你不应该如此。
进入 TypeScript(严格模式)
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
现在编译器会对你大喊:
// TypeScript rejects this
function processUser(user) { // Error: implicit 'any'
return user.name.toUpperCase();
}
// TypeScript accepts this
function processUser(user: User): string {
return user.name.toUpperCase();
}
这令人恼火吗? 是的。
它能在用户之前捕获错误吗? 同样是的。这就是权衡。
我使用 strict: true 以及 noUncheckedIndexedAccess。后者尤其让人恼火,因为它假设数组访问可能返回 undefined——而实际上确实可能,所以你需要处理它。
使用 ESLint 进行代码检查
TypeScript 能捕获类型错误。ESLint 则捕获其他所有问题:
- 未使用的变量
- 格式不一致
- 危险的写法
- 生产环境中遗忘的
console.log
我使用 Airbnb 风格指南。它观点鲜明、规则严格且经过实战检验。成千上万的工程师已经为这些规则争论过,我就不必再去争论了。
// eslint.config.js
import { configs, extensions, plugins } from 'eslint-config-airbnb-extended';
export default [
plugins.stylistic,
plugins.importX,
plugins.react,
plugins.reactA11y,
plugins.reactHooks,
plugins.typescriptEslint,
...extensions.base.typescript,
...extensions.react.typescript,
...configs.react.recommended,
...configs.react.typescript,
];
一次导入,数百条规则。 TypeScript 支持、React Hooks、可访问性、导入顺序——全部预配置。
no-explicit-any 是关键规则。它关闭了逃生舱口:你不能随意使用 any 并假装拥有类型安全。要么正确地为东西标注类型 要么 构建失败。
有些人觉得这很限制。那些人在凌晨 2 点调试生产问题。我在凌晨 2 点看 Netflix。选择不同而已。
Source: …
使用 Storybook 记录组件状态
这里有个有趣的游戏:重构组件、运行应用、随意点击、发布。三个月后,发现你在一个很少访问的页面上弄坏了加载状态。
组件有许多状态:
- 正常路径
- 错误状态
- 加载状态
- 空状态
- 边缘情况
你不可能每次都手动测试所有状态。你不会,我也不会,没人会。
Storybook 能记录每一种状态:
// Button.stories.tsx
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Click me',
},
};
export const Loading: Story = {
args: {
isLoading: true,
children: 'Loading...',
},
};
export const Disabled: Story = {
args: {
disabled: true,
children: 'Nope',
},
};
在 CI 中运行 build-storybook。如果任何组件在任何状态下无法渲染,构建就会失败。你破坏了某些东西——在它发布之前先修复它。
额外收获: AI 可以读取这些 stories,了解存在哪些状态,并生成相应的处理代码。真正被使用的文档。
One Job per Check
当出现错误时,你能准确知道是哪一个。
.frontend-quality:
stage: quality
image: node:lts-slim
cache:
key: npm-frontend-${CI_COMMIT_REF_SLUG}
paths:
- frontend/node_modules
- ~/.npm
before_script:
- cd frontend
- npm ci --prefer-offline
allow_failure: false
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
eslint:
extends: .frontend-quality
script:
- npm run lint
typecheck:
extends: .frontend-quality
script:
- npm run typecheck
storybook:build:
extends: .frontend-quality
script:
- npm run build-storybook --quiet
artifacts:
paths:
- frontend/storybook-static
expire_in: 1 week
when: always
三个作业。 同一阶段, 并行运行。
- 隐藏作业(
.frontend-quality)以点号开头,GitLab 会把它当作 模板(不会直接执行)。 - 使用
npm ci而不是npm install。ci更快、更严格,并且严格使用 lockfile,不会出现意外。 --prefer-offline– 在可能的情况下使用缓存的包。网络慢时,缓存快。
并行执行
这三个作业会同时运行:
- ESLint 失败? 你会立刻看到。
- TypeScript 失败? 你会同时看到两个错误,能够一起修复。
- Storybook 构建失败? 你也会看到。
Storybook 构件 会保留已构建的 Storybook。when: always 即使构建失败也会保存它——这对调试哪个组件出问题非常有帮助。
当某个作业失败时,你可以在流水线视图中准确看到是哪一个。无需滚动日志,也不必猜测。
| 作业 | 失败意味着 |
|---|---|
eslint | 样式问题或危险的代码模式 |
typecheck | 类型错误 |
storybook:build | 组件在 story 中无法渲染 |
这些并不能验证代码是否实现了预期功能——这需要 测试 来完成。这里仅仅是验证代码 没有明显的错误。
标准很低,但你会惊讶于有多少项目连这个都达不到。
Full CI Configuration
stages:
- quality
.frontend-quality:
stage: quality
image: node:lts-slim
cache:
key: npm-frontend-${CI_COMMIT_REF_SLUG}
paths:
- frontend/node_modules
- ~/.npm
before_script:
- cd frontend
- npm ci --prefer-offline
allow_failure: false
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
eslint:
extends: .frontend-quality
script:
- npm run lint
typecheck:
extends: .frontend-quality
script:
- npm run typecheck
storybook:build:
extends: .frontend-quality
script:
- npm run build-storybook --quiet
artifacts:
paths:
- frontend/storybook-static
expire_in: 1 week
when: always
复制、粘贴、适配。它能工作。
前端代码会以各种奇怪的方式失效:静默错误、运行时异常、“在我的机器上可以运行”,但在 Safari 上却不行,组件在传入意外属性前渲染正常。
我无法手动捕捉所有这些问题。我的注意力持续时间并不长,谁的都不是。
所以我把显而易见的事情自动化:
- 类型必须显式声明
- 代码必须遵循一致的模式
- 组件必须在渲染时不崩溃
机器会捕捉我遗漏的地方。流水线会阻止我后悔的提交。
后端也是同样的道理:一次写规则,永久执行。
接下来: 安全 – 敬请期待 – 依赖是别人的代码,而别人也会犯错。