普通视图

发现新文章,点击刷新页面。
昨天以前首页

打造 AI 驱动的 Git 提交规范助手:基于 React + Express + Ollama+langchain 的全栈实践

作者 ohyeah
2026年1月10日 16:50

在现代软件开发中,高质量的 Git 提交信息不仅是团队协作的基础,更是项目可维护性、可追溯性和工程文化的重要体现。然而,许多开发者(尤其是初学者)常常忽略提交信息的规范性,导致提交日志混乱、难以理解,甚至影响 Code Review 和故障排查效率。

为了解决这一痛点,本文将带你从零构建一个 AI 驱动的 Git 提交规范助手 —— git-differ。该项目结合了前端(React + Tailwind CSS + Axios)、后端(Node.js + Express)以及本地大模型(Ollama + DeepSeek-R1:8B),通过分析 git diff 内容,自动生成符合 Conventional Commits 规范的提交信息。

我们将深入剖析项目的代码结构、技术选型与关键知识点,并围绕 跨域处理、LangChain 集成、自定义 React Hook 封装、Express 路由设计 等核心内容展开详细讲解。


一、项目整体架构与技术栈

git-differ 是一个典型的前后端分离全栈应用:

  • 前端:运行于浏览器(如 http://localhost:5173),使用 React 构建 UI,Tailwind CSS 实现响应式样式,Axios 发起 HTTP 请求。
  • 后端:运行于 Node.js 环境(http://localhost:3000),基于 Express 框架提供 RESTful API。
  • AI 引擎:本地部署 Ollama 服务(http://localhost:11434),加载开源大模型 deepseek-r1:8b,通过 LangChain 进行提示工程与输出解析。

整个数据流如下:

用户输入 git diff → 前端发送 POST /chat → 后端接收并调用 Ollama → AI 生成 commit message → 返回前端展示

这种架构不仅解耦清晰,还便于后续扩展(如支持多模型、历史记录、配置管理等)。


二、前端实现:React + 自定义 Hook + Axios 模块化

1. 主组件 App.jsx:UI 与逻辑分离

import { useEffect } from "react"
import { chat } from './api/axios'
import { useGitDiff } from "./hooks/useGitDiff"

export default function App(){
  const { loading, content } = useGitDiff('hello')

  return (
    <div className="flex">
      {loading ? 'loading...' : content}
    </div>
  )
}

主组件极其简洁,仅负责渲染状态。真正的业务逻辑被封装在 useGitDiff 自定义 Hook 中,体现了 “组件只负责 UI” 的最佳实践。

“use开头 封装响应式业务 副作用等 从组件里面剥离 组件只负责UI”

这种模式极大提升了代码的可读性与复用性。未来若需在多个页面使用 AI 生成 commit message 功能,只需调用 useGitDiff 即可。


2. API 层:Axios 模块化封装

// src/api/axios.js
import axios from 'axios'

// 创建axios实例 统一进行配置
const service = axios.create({
  baseURL: 'http://localhost:3000',
  headers: {
    'Content-Type': 'application/json'
  },
  timeout: 60000
})

export const chat = (message) => {
  return service.post('/chat', {
    message
  })
}

这里通过 axios.create() 创建了一个 专用的 HTTP 客户端实例,统一配置:

  • baseURL:避免在每个请求中重复写后端地址;
  • headers:确保请求体为 JSON 格式;
  • timeout:设置超时时间,防止请求卡死。

“模块化 在api目录下管理所有的请求”

封装了api请求,在其他组件(如:useGitDiff.js)中只需要模块化导入chat 即可以发起请求,这种组织方式是大型 React 项目的标准做法,便于维护和测试。


3. 自定义 Hook:useGitDiff —— 封装副作用与状态

// src/hooks/useGitDiff.js
import { useState, useEffect } from "react"
import { chat } from "../api/axios"

export const useGitDiff = (diff) => {
  const [content, setContent] = useState('')
  const [loading, setLoading] = useState(false)

  useEffect(() => {
    (async () => {
      if(!diff) return 
      setLoading(true)
      const { data } = await chat(diff)
      setContent(data.reply)
      setLoading(false)
    })()
    // 立即执行异步函数  避免顶层async
  }, [diff])

// 将外部需要的loading状态和content内容 return
  return {
    loading, // 加载状态 用户体验
    content // llm得出的commit message
  }
}

该 Hook 接收 diff 字符串作为依赖,当其变化时自动发起请求。关键点包括:

  • 使用 立即执行异步函数(IIFE)useEffect 中处理异步逻辑;
  • 通过 setLoading 提供加载状态反馈;
  • 依赖数组 [diff] 确保只在 diff 变化时触发请求;
  • 返回结构化对象,便于解构使用。

“通过立即执行函数 执行异步函数”

这种写法规避了 useEffect 的回调函数不能直接使用 async/await 的限制。


三、后端实现:从零构建一个健壮的 Express AI 服务

后端是整个 git-differ 项目的中枢神经。它不仅要接收前端请求、调用本地大模型,还需保证安全性、稳定性、可维护性与协议规范性。以下我们将结合完整代码,逐层剖析其技术内涵。

1. 基础服务初始化:Express 应用骨架

import express from 'express'
import cors from 'cors'

const app = express()
app.use(express.json())
app.use(cors())

app.listen(3000, () => {
  console.log('server is running on port 3000')
})

这段看似简单的代码,实际上完成了现代 Web API 服务的三大基础配置:

express():创建应用实例

  • Express 是 Node.js 生态中最流行的 Web 框架,其核心思想是 “中间件管道”
  • app 是一个可配置、可扩展的 HTTP 服务器容器,后续所有路由、中间件都挂载于此。

app.use(express.json()):请求体解析中间件

  • 默认情况下,Express 不会自动解析请求体req.bodyundefined)。
  • express.json() 是一个内置中间件,用于解析 Content-Type: application/json 的请求体,并将其转换为 JavaScript 对象。
  • 若省略此中间件,req.body 将无法获取前端发送的 { message: "..." } 数据,导致后续逻辑失败。

💡 最佳实践:应在所有路由定义之前注册全局中间件,确保所有请求都能被正确解析。

app.use(cors()):跨域资源共享(CORS)支持

  • 前端开发服务器(如 Vite,默认端口 5173)与后端(3000)构成跨域请求(协议、域名或端口不同)。

  • 浏览器出于安全考虑,会拦截跨域请求的响应,除非服务器明确允许。

  • cors() 中间件自动处理 OPTIONS 预检请求,并在响应头中添加:

    Access-Control-Allow-Origin: *
    Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
    Access-Control-Allow-Headers: Content-Type
    
  • 在生产环境中,应限制 origin 为可信域名(如 origin: ['https://your-app.com']),避免开放全站跨域。


2. 路由设计:RESTful API 与 HTTP 语义

app.get('/hello', (req, res) => {
  res.send('hello world')
})

app.post('/chat', async (req, res) => {
  // ... 业务逻辑
})

📌 路由方法与资源操作语义

  • GET /hello:用于健康检查或简单测试,无副作用,符合幂等性。

  • POST /chat:用于提交 git diff 内容并获取 AI 生成结果。使用 POST 是因为: 请求包含复杂数据体(diff 文本可能很长);

📌 请求与响应对象(req / res

  • req.body:由 express.json() 解析后的请求体数据;
  • res.status(code).json(data):设置 HTTP 状态码并返回 JSON 响应;
  • res.send():返回纯文本(不推荐用于 API,应统一使用 JSON)。

3. 输入验证:防御性编程的第一道防线

const { message } = req.body
if (!message || typeof message !== 'string') {
  return res.status(400).json({
    error: 'message 必填,必须是字符串'
  })
}
  • 永远不要信任客户端输入,后端稳定性 是第一位。即使前端做了校验,后端也必须二次验证。

  • 此处检查:

    • message 是否存在(防止 nullundefined);
    • 类型是否为字符串(防止传入对象、数组等非法类型)。
  • 返回 400 Bad Request 状态码,明确告知客户端请求格式错误。

  • 错误信息清晰具体,便于前端调试。


4. AI 集成:LangChain 链式调用与错误隔离

import { ChatOllama } from '@langchain/ollama'
import { ChatPromptTemplate } from '@langchain/core/prompts'
import { StringOutputParser } from '@langchain/core/output_parsers'

const model = new ChatOllama({
  baseUrl: 'http://localhost:11434',
  model: 'deepseek-r1:8b',
  temperature: 0.1 // 
})

🔗 LangChain 核心抽象:链(Chain)

LangChain 的核心思想是将 LLM 调用过程模块化、可组合。本项目使用了典型的三段式链:

  1. Prompt Template:结构化输入

    const prompt = ChatPromptTemplate.fromMessages([  ['system', `你是一个专业的 Git 提交信息生成器。请根据以下 git diff 内容,生成一条简洁、符合 Conventional Commits 规范的 commit message。不要解释,只输出 commit message`],
      ['human', '{input}']
    ])
    
    • 将原始 message 注入到预设对话模板中;
    • 支持多轮对话上下文(未来可扩展);
    • 确保提示词格式符合模型预期。
  2. Model:大模型调用

    • ChatOllama 是 LangChain 对 Ollama API 的封装;
    • baseUrl 指向本地 Ollama 服务;
    • temperature: 0.1 降低随机性,使输出更确定(适合生成规范文本)。
  3. Output Parser:标准化输出

    new StringOutputParser()
    
    • 强制将模型返回的复杂对象(如 { content: "...", role: "assistant" })转换为纯字符串;
    • 避免前端处理不必要的元数据;
    • 保证 API 响应结构稳定:{ reply: "string" }

⚠️ 错误处理:隔离 AI 不稳定性

try {
  const result = await chain.invoke({ input: message })
  res.json({ reply: result })
} catch (err) {
  res.status(500).json({ error: '调用大模型失败' })
}
  • 大模型调用可能因网络、内存、模型加载失败等原因抛出异常;
  • 使用 try/catch 捕获所有同步/异步错误;
  • 返回 500 Internal Server Error,避免服务崩溃;

5. 工程化考量:可维护性与可观测性

  • 日志记录console.log('正在调用大模型') 提供基本执行追踪;
  • 超时控制:虽未显式设置,但 Ollama 客户端和 Axios 均有默认超时;
  • 依赖解耦:AI 逻辑封装在路由内,未来可提取为独立 service 层;

总结:一个合格的后端 API 应具备什么?

维度 本项目实现
协议合规 正确使用 HTTP 方法、状态码、Content-Type
输入安全 严格校验请求体格式与类型
错误处理 区分客户端错误(4xx)与服务端错误(5xx)
跨域支持 通过 CORS 中间件解决开发跨域问题
AI 集成 使用 LangChain 实现可维护的提示工程
可扩展性 模块化结构,便于未来增加新功能

这才是一个面向生产、面向读者的后端实现应有的深度与广度。


四、AI 部分:Ollama 与本地大模型部署

1. 为什么选择 Ollama?

  • 开源、轻量、支持多种模型(Llama, DeepSeek 等);
  • 提供类 OpenAI 的 API 接口(/api/chat),便于集成;
  • 可在消费级 GPU 或 CPU 上运行(需足够内存)。

2. 模型选择:deepseek-r1:8b

  • r1 表示 Reasoning 版本,推理能力更强;

  • 8b 为 80 亿参数,平衡性能与资源消耗;

  • 需通过命令下载:

    ollama pull deepseek-r1:8b
    ollama run deepseek-r1:8b  # 测试
    

“ollama帮助我们像openai一样的api 接口 http://localhost:11434”

3. 性能提示

  • 首次加载模型较慢(需加载到内存);
  • temperature: 0.1 降低随机性,使输出更确定、规范;
  • 若响应慢,可考虑量化版本(如 q4_K_M)。

五、总结

通过 git-differ 项目,我们完整实践了一个 AI 增强型开发者工具 的全栈开发流程:

  • 前端:React 自定义 Hook + Axios 模块化;
  • 后端:Express 路由 + CORS + 错误处理;
  • AI:Ollama + LangChain 提示工程 + 输出解析。

这不仅解决了“如何写好 Git 提交信息”的实际问题,更展示了 本地大模型在开发者工具链中的落地场景。随着开源模型能力不断提升,类似工具将极大提升个人与团队的开发效率与代码质量。

最终目标:让新手也能像高手一样,写出清晰、规范、有价值的 Git 提交记录。


现在,启动你的 AI Git 助手,让每一次提交都成为工程艺术的一部分!

构建现代 React 登录表单:从 ESM 懒加载到 TailwindCSS 响应式设计

作者 ohyeah
2026年1月6日 15:35

在现代前端开发中,构建一个高效、可维护且用户体验良好的登录页面,早已不是简单地堆砌 HTML 表单元素。它融合了模块化架构、状态管理、UI 库集成、响应式布局以及性能优化等多方面技术。本文将以一个典型的 React 登录组件为例,深入剖析其背后的技术逻辑,涵盖 ESM(ECMAScript Module)的懒加载机制React 的函数式状态管理受控组件的设计哲学,以及 TailwindCSS 的响应式与状态驱动样式体系


一、模块导入方式的演进:为何不再需要 import React from 'react'

早期 React 项目中,开发者习惯于在每个 JSX 文件顶部写上:

import React, { useState } from 'react';

这是因为 JSX 语法在编译阶段会被转换为 React.createElement() 调用,因此必须显式引入 React 对象。然而,自 React 17 起,官方引入了新的 JSX 转换机制(由 Babel 7.9+ 和 TypeScript 4.1+ 支持),使得编译器可以直接生成对 jsxjsxs 函数的调用,而无需依赖全局的 React 对象。

这意味着如今只需按需引入所需 Hooks 或 API:

import { useState } from 'react';
import { Lock, Mail, EyeOff, Eye } from 'lucide-react';

这种写法不仅更简洁,也更符合 按需引入(Tree Shaking) 的理念——只打包实际使用的代码,减少最终 bundle 体积。


二、ESM 与 CJS:懒加载的本质差异

许多开发者误以为使用 ESM(import 语法)就天然具备“懒加载”能力。实际上,静态 import 并非懒加载。真正的懒加载(Lazy Loading)是指 仅在需要时才加载模块,这在大型应用中对首屏性能至关重要。

静态导入 vs 动态导入

  • 静态导入(Static Import)

    import _ from 'lodash'; // 模块在构建时就被打包,随主 bundle 加载
    

    无论是否执行到相关逻辑,该模块都会被包含在初始资源中。

  • 动态导入(Dynamic Import)

    button.addEventListener('click', async () => {
      const _ = await import('lodash');
      _.debounce(...);
    });
    

    此时,lodash 会被 Webpack 或 Vite 等构建工具拆分为独立 chunk,仅在用户点击按钮时才发起网络请求并执行

相比之下,CommonJS(CJS)的 require() 是同步且立即执行的,无法实现运行时按需加载。因此,ESM 的动态 import() 是实现真正懒加载的关键,这也是现代前端工程化推崇 ESM 的核心原因之一。


三、状态管理:单一状态对象 vs 多个 useState

在登录表单中,通常涉及多个字段:邮箱、密码、是否记住我。传统做法可能是为每个字段声明一个 useState

const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [rememberMe, setRememberMe] = useState(false);

但随着字段增多,状态分散、更新逻辑重复的问题会逐渐显现。更优雅的方式是使用 单一状态对象

const [formData, setFormData] = useState({
  email: '',
  password: '',
  rememberMe: false
});

配合一个通用的事件处理器:

const handleChange = (e) => {
  const { name, value, type, checked } = e.target;
  setFormData(prev => ({
    ...prev,
    [name]: type === 'checkbox' ? checked : value
  }));
};

这里有几个关键点:

  1. name 属性作为状态键:HTML 表单元素的 name 与状态对象的 key 一一对应,实现自动映射。
  2. 区分输入类型:通过 type 判断是否为复选框(checkbox),从而决定取 value 还是 checked
  3. 函数式更新:使用 setFormData(prev => ...) 确保状态基于最新值更新,避免闭包导致的 stale state 问题。

这种抽象极大提升了代码的可扩展性——新增字段只需在初始状态中添加 key,无需修改事件处理逻辑。


四、UI 状态驱动:密码可见性切换

密码输入框常提供“显示/隐藏”功能。其实现完全遵循 React 的 数据驱动视图 原则:

const [showPassword, setShowPassword] = useState(false);

// 在 input 中动态设置 type
<input type={showPassword ? "text" : "password"} ... />

// 切换按钮
<button onClick={() => setShowPassword(!showPassword)}>
  {showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>

图标库选用 lucide-react,它提供轻量、一致且可定制的 SVG 图标,完美契合现代 UI 设计。通过状态 showPassword 控制 inputtype 和图标组件的渲染,整个交互逻辑清晰、无副作用。


五、TailwindCSS:声明式样式与响应式设计

本例中的样式完全由 TailwindCSS 实现,其优势在于 原子化、组合式、响应式 的类名系统。

核心布局

<div className="min-h-screen bg-slate-50 flex items-center justify-center p-4">
  • min-h-screen:确保容器至少占满整个视口高度(100vh),避免内容过少时布局塌陷。
  • flex items-center justify-center:水平垂直居中,经典登录页布局。
  • p-4:移动端内边距;结合 md:p-10 实现 移动端优先(Mobile First) 的响应式策略。

卡片容器

<div className="max-w-md bg-white rounded-3xl shadow-xl shadow-slate-200/60 border-slate-100 p-8 md:p-10">
  • max-w-md:限制最大宽度,保证在大屏设备上不会过度拉伸。
  • shadow-xl shadow-slate-200/60:使用带透明度的颜色(/60 表示 60% 不透明度),营造柔和阴影。
  • rounded-3xl:大圆角提升现代感。

表单间距与交互反馈

<form className="space-y-6"> <!-- 子元素间垂直间距 -->
<input className="focus:outline-none focus:ring-2 focus:ring-indigo-600/20 focus:border-indigo-600" />
  • space-y-6:自动为子元素添加 margin-top,避免手动设置间距。
  • focus:...:聚焦时移除默认轮廓,并添加品牌色(indigo)的 ring 和 border,提升可访问性与视觉反馈。

父子状态联动:groupgroup-focus-within

<div className="relative group">
  <div className="group-focus-within:text-indigo-600">...</div>
  <input ... />
</div>

当 input 获得焦点时,其父级 .group 触发 focus-within 状态,进而使内部图标颜色变为品牌色。这种 状态穿透 能力是 TailwindCSS 强大伪类系统的体现。


六、未来扩展:加载状态与表单提交

虽然当前 handleSubmit 为空,但已预留 isLoading 状态:

const [isLoading, setIsLoading] = useState(false);

在真实场景中,提交时应:

  1. 设置 setIsLoading(true)
  2. 调用登录 API
  3. 根据结果跳转或提示错误
  4. 最终 setIsLoading(false)

同时,可将提交按钮设为:

<button 
  type="submit" 
  disabled={isLoading}
  className="... disabled:opacity-50 disabled:cursor-not-allowed"
>
  {isLoading ? '登录中...' : '登录'}
</button>

利用 Tailwind 的 disabled: 变体自动应用禁用样式,提升用户体验。


结语

一个看似简单的登录页面,实则蕴含了现代前端开发的诸多最佳实践:

  • 模块化:通过 ESM 按需引入与动态导入优化加载性能;
  • 状态管理:以数据为中心,抽象通用逻辑;
  • UI 工程:借助 TailwindCSS 实现声明式、响应式、状态驱动的样式;
  • 用户体验:受控组件、实时反馈、加载状态等细节打磨。

这些技术并非孤立存在,而是相互协同,共同构建出高性能、高可维护性的现代 Web 应用。掌握其底层逻辑,方能在复杂业务中游刃有余。

❌
❌