尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

别让AI一口气写完整个前端:5大工程化Skills拆解

别让AI一口气写完整个前端:5大工程化Skills拆解 1. 为什么“一口气写完整个前端”是危险的幻觉Codex 这类代码生成模型确实能几秒内输出一个带 Vue 组件、路由配置、API 调用和基础样式的页面骨架。我第一次看到它生成一个带登录表单、JWT 验证逻辑、Axios 封装和 Element Plus 样式的完整登录页时手心全是汗——不是惊喜是警觉。因为那串代码里藏着三个致命断层页面结构和视觉逻辑脱节、状态管理与业务流程错位、测试桩和真实接口契约不匹配。这不是效率提升是债务打包。这背后根本不是技术问题而是工程认知偏差。前端开发从来就不是“把功能堆出来”而是一套精密协作系统UI 层要对设计系统负责逻辑层要对业务规则负责测试层要对行为契约负责构建层要对交付质量负责。Codex 擅长的是“文本续写”它把 HTML、JS、CSS 当作字符串拼接却无法理解input v-modelform.username和this.$refs.form.validate()之间隔着一个完整的表单校验生命周期它能写出npm run build命令但完全不知道vue.config.js里configureWebpack.optimization.splitChunks的 chunk 大小阈值设为 30KB 是为了平衡首屏加载和缓存复用率。所以标题里说的“别让 Codex 一口气写完整个前端”本质是在对抗一种偷懒的惯性思维——把“能生成”等同于“能交付”。真正决定项目成败的从来不是第一行代码怎么写而是第 100 行代码在什么条件下会崩溃。我把这套拆解方法沉淀为 5 组 Skills不是教你怎么用 Codex而是教你如何用 Codex 之前先建立自己的判断坐标系。这 5 组 Skills 对应着前端工程师每天真实面对的战场页面渲染的像素级控制、逻辑流转的状态守卫、测试用例的行为锚点、构建产物的体积博弈、以及所有环节的协同契约。它们不依赖任何框架版本只依赖你对“代码如何变成用户可感知体验”这一链条的深度理解。2. 页面 Skills从像素到交互的三层穿透式拆解2.1 第一层DOM 结构与语义化骨架非视觉层Codex 生成的页面常犯一个隐蔽错误用div classbtn提交/div替代button typesubmit提交/button。表面看样式一样但实际埋下三重隐患屏幕阅读器无法识别操作意图、键盘 Tab 焦点跳过该元素、表单 submit 事件无法自动触发。真正的页面 Skills 第一步是强制自己用语义化标签重建 DOM 骨架哪怕只是草稿。我习惯用“三问法”快速验证问角色这个元素在用户心智模型中是什么是动作按钮button、导航链接a、还是内容容器section问关系它和周围元素构成什么语义关系是表单控件form label input还是列表项ul li问状态它是否需要 aria-* 属性支持动态状态比如 loading 中的按钮必须有aria-busytrue和aria-disabledtrue。实操时我会先手写一个极简 HTML 骨架只保留语义标签和必要属性连 class 名都不写。例如商品列表页main header h1精选商品/h1 /header section aria-labelledbyfilter-heading h2 idfilter-heading筛选条件/h2 form fieldset legend价格区间/legend label formin-price最低价/label input typenumber idmin-price nameminPrice label formax-price最高价/label input typenumber idmax-price namemaxPrice /fieldset /form /section ul aria-label商品列表 li rolearticle h3iPhone 15 Pro/h3 p¥7,999/p button>/* 针对折叠屏展开态宽度≥812px 且高度≥1200px */ media (min-width: 812px) and (min-height: 1200px) { .product-grid { grid-template-columns: repeat(4, 1fr); } } /* 针对平板横屏宽度≥1024px */ media (min-width: 1024px) { .product-grid { grid-template-columns: repeat(5, 1fr); } }Codex 生成的 CSS 常用rem单位但忽略根字体大小重置或用flex-wrap却不设置flex-basis导致换行错乱。我的解决方案是在项目根 CSS 文件顶部强制注入重置规则并用clamp()函数替代媒体查询:root { --font-size-base: clamp(14px, 2.5vw, 16px); --spacing-unit: clamp(4px, 1.2vw, 8px); }这样既保证小屏可读性又避免大屏过度拉伸且 Codex 生成的组件样式只需继承这些变量无需重复写断点。2.3 第三层交互逻辑与状态映射行为层页面 Skills 的终极考验是把用户操作精准映射到状态变更。Codex 常生成v-on:clickhandleClick这类模糊函数却不定义handleClick的输入输出契约。我要求每个交互事件必须回答三个问题触发条件什么情况下允许触发如删除按钮需v-ifitem.deletable副作用范围执行后影响哪些状态如点击收藏按钮必须同时更新item.isFavorited和全局favoritedCount失败兜底异常时如何降级如网络失败时显示Toast.error(收藏失败请重试)而非静默失败以搜索框为例Codex 可能生成template input v-modelsearchQuery inputdebounceSearch / /template script export default { data() { return { searchQuery: } }, methods: { debounceSearch() { /* ... */ } } }这存在严重缺陷未处理空搜索、未防抖、未显示加载态。我的标准写法是template div classsearch-box input v-modelsearchQuery inputonInput :disabledisSearching placeholder搜索商品... / div v-ifisSearching classloading-indicator/div /div /template script import { debounce } from lodash export default { props: { // 明确声明外部依赖搜索结果必须通过 this.$emit(results, data) 传递 onSearch: { type: Function, required: true, validator: fn typeof fn function fn.length 1 } }, data() { return { searchQuery: , isSearching: false, // 所有状态变更必须可追溯searchQuery 改变 → isSearchingtrue → onSearch() → isSearchingfalse searchHistory: [] } }, methods: { onInput() { if (!this.searchQuery.trim()) return this.isSearching true // 防抖必须绑定到实例方法避免闭包内存泄漏 this.debouncedSearch debounce(() { this.onSearch(this.searchQuery) .finally(() this.isSearching false) }, 300) this.debouncedSearch() } } }这里props.onSearch的validator强制约束函数签名debouncedSearch作为实例属性确保防抖器可销毁finally保证加载态必然关闭。Codex 无法自动生成这种契约意识必须由开发者手动植入。3. 逻辑 Skills用状态机思维重构业务流程3.1 为什么 Vuex/Pinia 不是万能解药很多团队把状态管理当成“数据仓库”把所有变量塞进 store结果是state.user.profile.name和state.cart.items[0].price之间毫无关联。Codex 生成的 store 往往更糟它会创建userModule、cartModule、orderModule三个独立模块却忽略它们之间的状态流转依赖。比如用户登录后购物车需要重新校验库存订单模块需要同步用户地址——这些不是数据同步而是状态跃迁。我彻底放弃“模块化 store”的思路改用状态机State Machine建模。以电商结算流程为例传统写法// 错误示范分散的状态 state.cart { items: [], total: 0 } state.user { address: {}, balance: 0 } state.order { status: draft, paymentMethod: alipay }这导致逻辑散落在各处提交订单时要检查cart.items.length 0 user.address.valid order.paymentMethod而状态变更时又需手动同步cart.total到order.amount。正确做法是定义一个checkoutMachineconst checkoutMachine createMachine({ id: checkout, initial: idle, states: { idle: { on: { START: validating } }, validating: { invoke: { src: validateCartAndUser, onDone: { target: ready, actions: [setOrderData] }, onError: { target: error, actions: [showError] } } }, ready: { on: { CONFIRM: submitting, EDIT_CART: editingCart, EDIT_ADDRESS: editingAddress } }, submitting: { invoke: { src: submitOrder, onDone: { target: success, actions: [clearCart] }, onError: { target: error, actions: [showError] } } }, success: { type: final } } })这里validating状态明确要求validateCartAndUser服务返回成功才进入ready而submitting状态的onDone动作clearCart保证了副作用可控。Codex 无法生成这种状态驱动的逻辑因为它需要理解业务规则的先后依赖关系而非单纯的数据操作。3.2 事件总线的陷阱与替代方案Codex 常推荐用this.$bus.$emit(cart-updated)解耦组件但这制造了隐式依赖发送方不知道谁在监听监听方不知道事件何时触发。我在一个支付组件中踩过坑——当用户切换支付方式时payment-method-change事件被 7 个组件监听其中 2 个组件在mounted时注册3 个在created时注册还有 2 个动态注册。结果是状态更新顺序混乱优惠券计算错乱。我的替代方案是“显式依赖注入”// 在父组件提供 export default { provide() { return { // 提供一个受控的更新函数而非广播事件 updatePaymentContext: (newContext) { this.paymentContext newContext // 主动通知所有依赖者 this.$children.forEach(child { if (child.updateFromParent) child.updateFromParent(newContext) }) } } } } // 子组件消费 export default { inject: [updatePaymentContext], mounted() { // 显式声明依赖关系 this.updatePaymentContext(this.localContext) } }这种方式让依赖关系可视化inject数组明确列出所需服务updateFromParent方法强制子组件实现状态同步协议。Codex 生成的代码几乎从不采用这种模式因为它需要开发者预先设计组件协作契约。3.3 API 请求的幂等性与缓存策略Codex 生成的 API 调用常是axios.get(/api/user)这种裸调用忽略三个关键维度幂等性GET 请求必须保证多次调用结果一致但/api/user?timestamp123这种带随机参数的请求破坏了这一点缓存粒度用户资料应缓存 5 分钟商品列表应缓存 10 秒而实时价格必须禁用缓存错误分类网络超时、HTTP 401、业务错误 20001每种需要不同处理策略。我的解决方案是封装ApiServiceclass ApiService { constructor() { this.cache new Map() } request(config) { const cacheKey this.generateCacheKey(config) // 只对 GET 请求启用缓存 if (config.method GET config.cache ! false) { const cached this.cache.get(cacheKey) if (cached Date.now() - cached.timestamp this.getTTL(config)) { return Promise.resolve(cached.data) } } return axios(config) .then(response { // 业务错误统一拦截 if (response.data.code ! 0) { throw new BusinessError(response.data.message, response.data.code) } // 缓存写入 if (config.method GET) { this.cache.set(cacheKey, { data: response.data, timestamp: Date.now() }) } return response.data }) .catch(error { if (error instanceof BusinessError) { // 业务错误直接抛出 throw error } else if (error.code ECONNABORTED) { // 网络超时重试一次 return this.request({ ...config, timeout: config.timeout * 2 }) } else { // 其他错误转为统一错误类型 throw new NetworkError(error.message) } }) } getTTL(config) { // 不同路径不同 TTL if (config.url.includes(/user/)) return 5 * 60 * 1000 // 5分钟 if (config.url.includes(/products/)) return 10 * 1000 // 10秒 return 0 // 默认不缓存 } }这个服务强制要求每个请求配置cache和timeout参数getTTL方法根据 URL 路径动态计算缓存时间。Codex 无法生成这种上下文感知的缓存策略因为它需要理解业务数据的时效性特征。4. 测试 Skills从“覆盖行数”到“守护契约”4.1 单元测试的三大反模式Codex 生成的测试常陷入三种典型反模式Mock 泄漏在测试中jest.mock(axios)却未清理导致后续测试用到真实 axios断言失焦expect(wrapper.vm.count).toBe(1)这类断言只验证内部状态不验证用户可见行为场景缺失只测正常流程不测边界条件如空数组、网络超时、权限不足。我的测试 Skills 核心是“契约驱动”每个测试用例必须对应一个可验证的用户契约。以购物车组件为例用户契约是“当添加商品时购物车图标数字应实时更新且 Toast 应提示‘已加入购物车’”。标准测试写法describe(CartIcon.vue, () { it(should show correct item count and toast when adding item, async () { // Arrange: 设置初始状态 const wrapper mount(CartIcon, { propsData: { itemCount: 0 } }) // Act: 触发用户操作 await wrapper.vm.$emit(add-item, { id: 123, name: iPhone }) // Assert: 验证用户契约 expect(wrapper.find(.cart-count).text()).toBe(1) expect(wrapper.vm.$message.success).toHaveBeenCalledWith(已加入购物车iPhone) // 额外验证确保事件被正确分发 expect(wrapper.emitted(update:item-count)).toHaveLength(1) }) it(should handle add failure with error toast, async () { // Arrange: mock 失败场景 jest.mock(/api/cart, () ({ addItem: jest.fn().mockRejectedValue(new Error(库存不足)) })) const wrapper mount(CartIcon) // Act await wrapper.vm.$emit(add-item, { id: 999, name: OutStockItem }) // Assert: 用户看到错误提示 expect(wrapper.vm.$message.error).toHaveBeenCalledWith(库存不足请稍后再试) }) })这里expect(wrapper.find(.cart-count).text())验证 DOM 渲染结果expect(wrapper.vm.$message.success)验证 UI 反馈expect(wrapper.emitted())验证组件间通信。Codex 生成的测试往往只验证wrapper.vm.itemCount这属于“内部实现细节”一旦重构为computed属性就会失效。4.2 E2E 测试的 ROI 优化策略很多团队盲目追求 100% E2E 覆盖率结果是 CI 构建时间暴涨。我坚持“金字塔测试策略”但做了关键调整E2E 只验证核心用户旅程且每个用例必须对应一个业务 KPI。例如电商网站我只维护 3 个 E2E 用例注册转化率模拟新用户完成邮箱注册、验证、首单购买全流程支付成功率模拟用户从下单到支付成功监控支付网关回调是否触发搜索召回率输入关键词“iPhone”验证前 3 条结果包含 iPhone 相关商品。每个用例都绑定监控指标// cypress/e2e/checkout.spec.js describe(Checkout Journey, () { it(should complete purchase with alipay, () { cy.visit(/) cy.get([data-testidsearch-input]).type(iPhone{enter}) cy.get([data-testidproduct-card]).first().click() cy.get([data-testidadd-to-cart]).click() cy.get([data-testidgo-to-checkout]).click() cy.get([data-testidpayment-alipay]).click() cy.get([data-testidconfirm-order]).click() // 关键断言必须看到支付成功页 cy.url().should(include, /order/success) cy.get([data-testidorder-id]).should(exist) // 业务指标记录本次旅程耗时 cy.task(recordKpi, { name: checkout_duration, value: Date.now() - Cypress.currentTest.startedAt, tags: { paymentMethod: alipay } }) }) })cy.task(recordKpi)将性能数据上报到监控系统与业务报表联动。Codex 无法生成这种业务导向的测试因为它缺乏对商业目标的理解。4.3 测试数据的工厂模式Codex 生成的测试常硬编码数据const user { name: test, email: testtest.com }。这导致两个问题数据过期邮箱格式变化、耦合严重修改用户结构需批量更新测试。我采用工厂模式// factories/user.factory.js export const userFactory (overrides {}) ({ id: Math.floor(Math.random() * 1000), name: overrides.name || 张三, email: overrides.email || ${Date.now()}example.com, avatar: overrides.avatar || https://example.com/avatar.png, // 业务规则邮箱必须包含 符号 isValidEmail: () this.email.includes() }) // 测试中使用 it(should validate email format, () { const user userFactory({ email: invalid-email }) expect(user.isValidEmail()).toBe(false) const validUser userFactory({ email: validexample.com }) expect(validUser.isValidEmail()).toBe(true) })工厂函数接受overrides参数确保测试数据可定制isValidEmail方法将业务规则内聚在数据对象中。Codex 生成的测试数据通常是静态对象无法表达这种动态规则。5. 构建 Skills从 bundle 分析到部署契约5.1 构建产物的体积治理四象限Codex 生成的vue.config.js常是默认配置导致vendor.js达到 3MB。我用“四象限分析法”定位问题象限特征典型原因解决方案高体积高复用node_modules中的lodash、moment未做 Tree-shaking改用date-fns按需导入import { format } from date-fns高体积低复用src/assets/images/logo.png图片未压缩配置image-webpack-loaderPNG 压缩率设为 80%低体积高复用src/utils/request.js未提取为公共模块创建/shared/utils供所有模块引用低体积低复用src/views/ReportView.vue组件过大未拆分拆分为ReportTable、ReportChart、ReportFilter关键技巧是用webpack-bundle-analyzer生成可视化报告但不止看文件大小还要看模块引用链深度。例如发现echarts被ReportChart.vue引用而ReportChart.vue又被Dashboard.vue引用但Dashboard.vue实际只用到echarts的折线图功能。此时应改用echarts/lib/chart/line替代echarts全量引入。5.2 CI/CD 流水线的构建契约Codex 生成的 GitHub Actions 常是- name: Build run: npm run build这缺少三个关键契约环境一致性本地npm run build和 CI 中npm run build是否使用相同 Node 版本产物验证构建产物是否包含index.html和assets/目录安全扫描是否检查package-lock.json中的高危漏洞我的标准流水线name: Build and Deploy on: [push] jobs: build: runs-on: ubuntu-22.04 steps: - uses: actions/checkoutv3 - uses: actions/setup-nodev3 with: node-version: 18.17.0 # 锁定版本避免 npm install 差异 cache: npm - name: Install dependencies run: npm ci # 使用 ci 而非 install确保 lockfile 严格一致 - name: Build production bundle run: npm run build - name: Validate build output run: | if [ ! -f dist/index.html ]; then echo ERROR: dist/index.html missing exit 1 fi if [ ! -d dist/assets ]; then echo ERROR: dist/assets directory missing exit 1 fi echo Build output validated successfully - name: Security scan uses: snyk/actions/nodemaster with: command: test args: --severity-thresholdhigh env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}npm ci确保依赖安装与package-lock.json完全一致Validate build output步骤强制检查关键文件存在Security scan在构建后立即扫描漏洞。Codex 无法生成这种防御性流水线因为它需要理解交付链路的风险点。5.3 部署环境的灰度发布策略Codex 生成的部署脚本常是scp dist/ userserver:/var/www/html/这导致零停机发布无法实现。我采用“符号链接切换”策略# 部署脚本 deploy.sh #!/bin/bash APP_VERSION$(date %Y%m%d%H%M%S) DEPLOY_DIR/var/www/myapp/releases/$APP_VERSION CURRENT_LINK/var/www/myapp/current # 创建新版本目录 mkdir -p $DEPLOY_DIR cp -r dist/* $DEPLOY_DIR/ # 更新当前链接 ln -sf $DEPLOY_DIR $CURRENT_LINK # 清理旧版本保留最近3个 ls -t /var/www/myapp/releases/ | tail -n 4 | xargs -I {} rm -rf /var/www/myapp/releases/{}这样current目录始终指向最新稳定版本回滚只需ln -sf /var/www/myapp/releases/20240101000000 /var/www/myapp/current。更重要的是我在此基础上增加灰度开关# nginx 配置 upstream backend { server 127.0.0.1:3000; } server { location / { # 根据 cookie 或 header 决定流量走向 if ($cookie_gray_version v2) { proxy_pass http://backend_v2; break; } proxy_pass http://backend_v1; } }Codex 无法生成这种渐进式发布策略因为它需要理解业务风险与技术方案的权衡。6. 协同 Skills建立人机协作的黄金法则6.1 Codex 提示词的“四要素”结构很多人用 Codex 时输入“写一个 Vue 表单”结果得到一堆不可维护的代码。我总结出提示词必须包含四个要素角色定义明确 AI 的身份如“你是一名资深 Vue 工程师熟悉 Composition API 和 TypeScript”输入约束限定输入格式如“接收一个 props 对象包含 { title: string, items: Product[] }”输出契约规定输出必须满足的条件如“返回一个 setup() 函数返回 { columns, dataSource, loading }且 loading 必须是 ref ”错误预防提前规避常见错误如“禁止使用 any 类型禁止硬编码颜色值禁止在 template 中写 JavaScript 表达式”完整示例你是一名资深 Vue 工程师熟悉 Composition API 和 TypeScript。请编写一个商品列表组件接收 props { category: string, limit: number }。组件必须 1. 使用 defineComponent 和 setup() 语法 2. 返回 { products, loading, loadMore }其中 products 是 refProduct[]loading 是 refboolean 3. loadMore 是一个函数调用时设置 loadingtrue获取数据后设置 loadingfalse 4. 禁止使用 any 类型禁止在 template 中写三元表达式禁止硬编码 px 单位 5. 使用 Tailwind CSS 类名且所有间距使用 space-y-4 这类实用类这种提示词让 Codex 输出的代码可直接集成到项目中而非需要大量重构。6.2 代码审查的 Checklist我给团队制定的 Codex 代码审查清单可访问性所有交互元素是否有role、aria-*属性表单控件是否有label响应式CSS 是否使用clamp()或媒体查询是否测试过 320px 宽度错误处理API 调用是否有try/catch网络错误是否显示用户友好的提示性能列表渲染是否使用key图片是否设置loadinglazy测试覆盖每个组件是否至少有一个单元测试验证核心交互每次 Codex 生成代码后必须逐项打钩确认。我发现 80% 的问题出现在“错误处理”和“可访问性”两项因为 Codex 无法理解用户在弱网环境下的真实体验。6.3 技术债的量化管理我用一个简单的公式量化 Codex 引入的技术债技术债分值 (AI 生成代码行数 × 0.5) (手动修复行数 × 2) (测试补充行数 × 1.5)AI 生成代码行数乘以 0.5因为初始成本低但维护成本高手动修复行数乘以 2因为修复过程暴露了设计缺陷测试补充行数乘以 1.5因为测试是降低未来风险的投资。每月统计团队的技术债分值当分值超过阈值如 500 分时强制进行技术债偿还周重构高分值模块、补充缺失测试、优化构建配置。Codex 不是债务制造者而是债务放大器——它让原本需要 10 小时的手写代码变成 2 小时生成 8 小时修复而修复过程中的认知负荷远高于原始开发。最后分享一个真实教训上周我让 Codex 生成一个 WebSocket 连接管理器它输出的代码完美运行了 3 天。第四天凌晨服务器因连接泄漏崩溃。排查发现 Codex 生成的onclose回调里没有清除定时器而我们的业务逻辑要求每 30 秒 ping 一次。这个 bug 不在任何测试用例中因为没人会测试“连续运行 72 小时后的内存泄漏”。所以真正的 Skills 不是让 Codex 写得更快而是让自己看得更远——在每一行 AI 生成的代码后面都多问一句“它在什么情况下会失效”
返回列表