02. Vite快速入门:秒级启动的开发体验

Vite快速入门:秒级启动的开发体验

Vite Logo

如果你用过Webpack,一定经历过这样的痛苦:

  • npm run dev启动要等30秒
  • 改一行代码,热更新要等3秒
  • 配置文件写得想砸键盘

Vite就是为了终结这些痛苦而生的。它的名字来自法语"快"(vite),快到让你怀疑人生。

为什么Vite这么快?

传统打包工具的问题

Webpack的工作方式:

  1. 启动时,从入口文件开始
  2. 递归分析所有依赖
  3. 打包成一个(或几个)大文件
  4. 启动开发服务器

项目越大,依赖越多,打包越慢。

Vite的革命性做法

Vite利用了浏览器的原生ES模块能力:

开发环境

  1. 启动时不打包
  2. 浏览器请求哪个文件,就编译哪个文件
  3. 源码按需编译,秒级启动

生产环境
使用Rollup打包,优化输出

graph LR
    A[浏览器请求] --> B[Vite Dev Server]
    B --> C{已编译?}
    C -->|是| D[返回缓存]
    C -->|否| E[即时编译]
    E --> D

创建第一个Vite项目

# 创建Vue3项目
npm create vite@latest my-vue-app -- --template vue

# 或者用pnpm(推荐)
pnpm create vite my-vue-app --template vue

# 进入项目
cd my-vue-app

# 安装依赖
pnpm install

# 启动开发服务器
pnpm dev

输出:

  VITE v5.1.0  ready in 234 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

234毫秒!这就是Vite的速度。

项目结构

my-vue-app/
├── index.html          # 入口HTML
├── package.json        # 项目配置
├── vite.config.js      # Vite配置
├── public/             # 静态资源(不处理)
│   └── favicon.ico
└── src/                # 源代码
    ├── main.js         # 入口文件
    ├── App.vue         # 根组件
    └── assets/         # 资源文件
        └── vue.svg

index.html

Vite把index.html作为入口,而不是main.js


    <title>Vite + Vue</title>

    <div id="app"></div>

注意type="module",这告诉浏览器用ES模块方式加载。

main.js

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

App.vue


import { ref } from 'vue'

const count = ref(0)

  <div>
    <h1>Vite + Vue</h1>
    <button>
      Count: {{ count }}
    </button>
  </div>

h1 {
  color: #42b883;
}

Vite配置

基础配置

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],

  // 开发服务器
  server: {
    port: 3000,          // 端口
    open: true,          // 自动打开浏览器
    host: '0.0.0.0',     // 允许局域网访问
    cors: true           // 启用CORS
  },

  // 构建配置
  build: {
    outDir: 'dist',      // 输出目录
    sourcemap: true      // 生成sourcemap
  }
})

路径别名

import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

使用:

import MyComponent from '@/components/MyComponent.vue'
// 而不是
import MyComponent from '../../../components/MyComponent.vue'

环境变量

# .env.development
VITE_API_URL=http://localhost:3000/api

# .env.production
VITE_API_URL=https://api.example.com

使用:

const apiUrl = import.meta.env.VITE_API_URL

注意:只有VITE_前缀的变量才会暴露给客户端代码。

代理API请求

export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
})

CSS处理

CSS Modules


  <div>
    <h1>Hello</h1>
  </div>

.container {
  padding: 20px;
}
.title {
  color: blue;
}

预处理器

# 安装Sass
pnpm add -D sass

  <div class="container">
    <h1>Hello</h1>
  </div>

$primary-color: #42b883;

.container {
  padding: 20px;

  h1 {
    color: $primary-color;
  }
}

全局CSS

// vite.config.js
export default defineConfig({
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: <code>@import "@/styles/variables.scss";
      }
    }
  }
})

静态资源

引入方式

// 直接引入
import logo from '@/assets/logo.png'

// URL形式
const imgUrl = new URL('./assets/logo.png', import.meta.url).href

// 动态引入(Vite特有)
const modules = import.meta.glob('./assets/*.png')
// 返回: { './assets/a.png': () => import('./assets/a.png'), ... }

public目录

public/下的文件不会被处理,直接复制到输出目录:

<img src="/logo.png" />

访问:http://localhost:5173/logo.png

构建优化

代码分割

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vendor': ['vue', 'vue-router', 'pinia'],
          'ui': ['element-plus']
        }
      }
    }
  }
})

分析打包大小

pnpm add -D rollup-plugin-visualizer
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    vue(),
    visualizer({ open: true })
  ]
})

构建后会生成一个HTML文件,可视化显示每个模块的大小。

TypeScript支持

# 创建TS项目
pnpm create vite my-app --template vue-ts

Vite原生支持TypeScript,无需额外配置(但需要vue-tsc做类型检查)。

pnpm add -D vue-tsc
// package.json
{
  "scripts": {
    "build": "vue-tsc && vite build"
  }
}

常用插件

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'           // JSX支持
import vueDevTools from 'vite-plugin-vue-devtools'   // Vue DevTools
import compression from 'vite-plugin-compression'    // Gzip压缩
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    vueDevTools(),
    compression(),
    visualizer()
  ]
})

常见问题

Q: 为什么生产构建用Rollup?

A: Rollup生成的代码更干净、更小。开发时用原生ES模块追求速度,生产时用Rollup追求优化。

Q: 如何处理CommonJS模块?

A: Vite会自动转换,大多数情况无需处理。如果有问题:

export default defineConfig({
  optimizeDeps: {
    include: ['some-cjs-module']
  }
})

Q: 热更新不生效?

A: 检查文件是否被正确引入,确保没有语法错误。可以在浏览器控制台查看HMR状态。

Vite vs Webpack

特性 Vite Webpack
启动速度 ⚡ 秒级 🐢 项目大时慢
热更新 ⚡ 毫秒级 🐢 较慢
配置复杂度 📝 极简 😵 复杂
生态成熟度 🌱 快速发展 🌳 非常成熟
生产打包 Rollup Webpack

结论:新项目首选Vite,除非必须用Webpack生态的特定插件。

小结

你现在掌握了:

  • Vite为什么快(原生ES模块)
  • 创建和配置Vite项目
  • 路径别名、环境变量、代理
  • CSS处理(Modules、预处理器)
  • 静态资源引入
  • 构建优化技巧

下一期,我们正式进入Vue3基础。你会学到Vue3的核心概念:响应式、组件、模板语法。


练习任务

  1. 创建一个Vite + Vue3项目
  2. 配置@路径别名
  3. 配置开发服务器代理
  4. 使用Sass编写样式

下期预告:《Vue3基础:响应式系统和组件入门》—— ref和reactive怎么选?组件怎么通信?

Views: 23

01. 现代前端开发入门:从ES6到Vue3的第一步

现代前端开发入门:从ES6到Vue3的第一步

Modern Frontend

你刚决定学习前端开发,打开教程一看:ES6、Node.js、npm、Webpack、Vite、TypeScript...这些名词像天书一样砸过来。别慌,这篇文章帮你理清现代前端的"基础设施",为Vue3开发打下坚实基础。

现代前端开发三件套

在Vue3之前,你需要先掌握三个基础:

  1. ES6+语法 - 现代JavaScript的写法
  2. Node.js + npm - 包管理和构建工具的基础
  3. 模块化开发 - 代码组织方式

让我们一个个攻克。

ES6+必学语法

ES6(ECMAScript 2015)是JavaScript的重大升级,Vue3大量使用这些新特性。

let和const

// ❌ 旧写法
var name = 'Vue'
var name = 'React' // 可以重复声明,容易出bug

// ✅ 新写法
const app = 'Vue3' // 常量,不可重新赋值
let count = 0 // 变量,可以修改
count = 1 // OK
// app = 'React' // 报错!const不能重新赋值

规则:默认用const,需要修改时才用let,永远不用var

箭头函数

// ❌ 旧写法
const add = function(a, b) {
return a + b
}

// ✅ 新写法
const add = (a, b) => {
return a + b
}

// 更简洁:单行可以省略大括号和return
const add = (a, b) => a + b

// 单参数可以省略括号
const double = n => n * 2

关键区别:箭头函数没有自己的this,它会继承外层作用域的this。这在Vue中非常重要。

const obj = {
count: 0,
// ❌ 普通函数,this指向调用者
incrementOld: function() {
setTimeout(function() {
console.log(this.count) // undefined!
}, 1000)
},
// ✅ 箭头函数,this继承外层
incrementNew: function() {
setTimeout(() => {
console.log(this.count) // 0
}, 1000)
}
}

解构赋值

// 对象解构
const user = { name: 'Vue', version: 3, author: 'Evan' }
const { name, version } = user
console.log(name, version) // 'Vue' 3

// 重命名
const { name: framework } = user
console.log(framework) // 'Vue'

// 默认值
const { license = 'MIT' } = user
console.log(license) // 'MIT'

// 数组解构
const [first, second, ...rest] = [1, 2, 3, 4, 5]
console.log(first, second, rest) // 1 2 [3, 4, 5]

展开运算符

// 数组展开
const arr1 = [1, 2, 3]
const arr2 = [...arr1, 4, 5]
console.log(arr2) // [1, 2, 3, 4, 5]

// 对象展开(浅拷贝)
const defaults = { theme: 'light', lang: 'zh' }
const settings = { ...defaults, theme: 'dark' }
console.log(settings) // { theme: 'dark', lang: 'zh' }

// 合并对象
const merged = { ...obj1, ...obj2 }

模板字符串

const name = 'Vue'
const version = 3

// ❌ 旧写法
const msg = 'Welcome to ' + name + ' ' + version

// ✅ 新写法
const msg = <code>Welcome to ${name} ${version}

// 多行字符串
const html = `

${name}

`

简写属性

const name = 'Vue'
const version = 3

// ❌ 旧写法
const app = {
name: name,
version: version,
sayHello: function() {
return 'Hello'
}
}

// ✅ 新写法
const app = {
name, // 等同于 name: name
version, // 等同于 version: version
sayHello() { // 方法简写
return 'Hello'
}
}

Promise和async/await

// Promise
fetch('/api/users')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error))

// async/await(推荐)
async function getUsers() {
try {
const response = await fetch('/api/users')
const data = await response.json()
console.log(data)
} catch (error) {
console.error(error)
}
}

// 并行请求
async function getAll() {
const [users, posts] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json())
])
return { users, posts }
}

数组方法

const numbers = [1, 2, 3, 4, 5]

// map:转换每个元素
const doubled = numbers.map(n => n * 2) // [2, 4, 6, 8, 10]

// filter:筛选元素
const evens = numbers.filter(n => n % 2 === 0) // [2, 4]

// find:查找元素
const found = numbers.find(n => n > 3) // 4

// reduce:累积计算
const sum = numbers.reduce((acc, n) => acc + n, 0) // 15

// some/every:判断
const hasEven = numbers.some(n => n % 2 === 0) // true
const allPositive = numbers.every(n => n > 0) // true

// 链式调用
const result = numbers
.filter(n => n > 2)
.map(n => n * 10)
.reduce((acc, n) => acc + n, 0) // 120

可选链和空值合并

const user = {
profile: {
name: 'Vue'
// avatar 可能不存在
}
}

// ❌ 旧写法(容易报错)
const avatar = user && user.profile && user.profile.avatar

// ✅ 可选链
const avatar = user?.profile?.avatar // undefined,不会报错

// ❌ 旧写法
const name = user.name || 'Anonymous' // 空字符串也会用默认值

// ✅ 空值合并
const name = user.name ?? 'Anonymous' // 只有null/undefined才用默认值

Node.js和npm

什么是Node.js?

Node.js让JavaScript可以脱离浏览器运行。它主要用于:

  1. 运行构建工具(Vite、Webpack)
  2. 服务端开发(Express、Nest)
  3. 运行脚本

下载安装:https://nodejs.org/

npm基础命令

# 初始化项目
npm init -y

# 安装依赖
npm install vue # 安装到dependencies
npm install -D vite # 安装到devDependencies
npm install -g pnpm # 全局安装

# 版本管理
npm install vue@3 # 指定版本
npm install vue@latest # 最新版本

# 运行脚本
npm run dev # 开发服务器
npm run build # 构建生产版本

# 其他
npm update # 更新依赖
npm outdated # 检查过时依赖
npm audit # 安全检查

package.json

{
"name": "my-vue-app",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.0"
},
"devDependencies": {
"vite": "^5.0.0"
}
}

pnpm:更快的替代品

# 安装pnpm
npm install -g pnpm

# 使用(命令基本相同)
pnpm install
pnpm add vue
pnpm dev

pnpm优点:

  • 更快(硬链接,节省磁盘空间)
  • 更严格(避免幽灵依赖)
  • 更安全(更好的依赖管理)

ES模块

import/export

// utils.js - 导出
export const PI = 3.14159

export function add(a, b) {
return a + b
}

export default class Calculator {
// ...
}

// main.js - 导入
import Calculator, { PI, add } from './utils.js'
import * as utils from './utils.js'

动态导入

// 懒加载
const module = await import('./heavy-module.js')

// Vue路由懒加载
const routes = [
{
path: '/about',
component: () => import('./views/About.vue')
}
]

开发环境推荐

VSCode必备插件

  1. Vue - Official - Vue语法高亮和智能提示
  2. ESLint - 代码规范检查
  3. Prettier - 代码格式化
  4. Volar - Vue3专用(已集成到Vue - Official)

推荐配置

// .vscode/settings.json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}

小结

你现在掌握了:

  • ES6+核心语法(箭头函数、解构、展开、async/await)
  • Node.js和npm基础操作
  • ES模块的import/export
  • 开发环境配置

下一期,我们将学习Vite - Vue3官方推荐的构建工具。它快得让你怀疑人生,配置简单到让你感动流泪。


练习任务

  1. 用解构赋值提取对象属性
  2. 用map/filter/reduce处理数组
  3. 用async/await改写Promise链式调用
  4. 创建一个npm项目,安装vue依赖

下期预告:《Vite快速入门:秒级启动的开发体验》—— 为什么Vite这么快?如何配置Vite项目?

Views: 13

Git实战宝典:从翻车现场到绝地求生

Git实战宝典:从翻车现场到绝地求生

学了前四期,你已经掌握了Git的理论和最佳实践。但现实世界不是童话,翻车才是常态。

这一期,我们直面那些让新手崩溃、让老手头秃的真实场景。每个问题都有救,只要你知道正确的方法。

紧急救援手册

场景1:commit后发现写错了

情况A:还没push

# 修改最后一次提交
git commit --amend -m "正确的提交信息"

# 或者补充文件
git add forgotten-file.txt
git commit --amend --no-edit

情况B:已经push了

# 如果只有你在这个分支工作
git commit --amend -m "正确的提交信息"
git push --force-with-lease

# 如果有别人也在用这个分支,不要用amend!
# 而是创建新的修复提交
git revert HEAD
git push

场景2:提交到错误的分支

# 你在main分支提交了,但应该在feature分支

# 1. 撤销当前提交,保留改动
git reset HEAD~1 --soft

# 2. 切换到正确的分支
git checkout feature-branch

# 3. 重新提交
git commit -m "feat: xxx"

场景3:需要撤回多个提交

# 撤回最近3个提交,保留改动在工作区
git reset HEAD~3

# 撤回最近3个提交,保留改动在暂存区
git reset HEAD~3 --soft

# 撤回最近3个提交,丢弃所有改动(危险!)
git reset HEAD~3 --hard

场景4:误删分支

# 查找被删分支的最后一个提交
git reflog

# 找到类似这样的记录
# abc1234 HEAD@{5}: checkout: moving from feature-xxx to main

# 重建分支
git checkout -b feature-xxx abc1234

场景5:误删文件

# 删除了工作区文件,想恢复
git checkout -- deleted-file.txt
# 或
git restore deleted-file.txt

# 删除了文件并已提交
git checkout HEAD~1 -- deleted-file.txt
git commit -m "restore: 恢复误删文件"

场景6:需要撤销已push的提交

# 方法1:revert(推荐,不改写历史)
git revert 
git push

# 方法2:reset + force push(危险,仅限个人分支)
git reset --hard 
git push --force-with-lease

revert创建一个新提交来撤销改动,不改写历史,适合已push的提交。reset回退指针,会改写历史。

场景7:merge出错想重来

# 合并过程中发现冲突太多,想放弃
git merge --abort

# 已经合并完成但想撤销
git reset --hard HEAD~1

场景8:需要合并特定的几个提交

# 方法1:cherry-pick
git cherry-pick   

# 方法2:交互式rebase
git rebase -i 
# 在编辑器中把不需要的提交标记为drop

历史改写

修改历史提交信息

# 修改最近3个提交的信息
git rebase -i HEAD~3

# 把要修改的提交前面的pick改成reword
# 保存后Git会逐个让你编辑提交信息

合并历史提交

git rebase -i HEAD~3

# 把后面几个提交的pick改成squash
# 保存后编辑合并后的提交信息

删除历史提交

git rebase -i HEAD~5

# 把要删除的提交改成drop

从历史中删除敏感文件

# 使用BFG Repo-Cleaner(快)
bfg --delete-files secrets.yml
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force

# 使用git filter-repo(现代推荐)
pip install git-filter-repo
git filter-repo --path secrets.yml --invert-paths

# 使用git filter-branch(慢,已废弃)
git filter-branch --force --index-filter \
  'git rm --cached --ignore-unmatch secrets.yml' \
  --prune-empty --tag-name-filter cat -- --all

重要:改写历史后通知所有协作者重新clone!

大文件处理

问题:仓库太大

# 查看哪些文件占用空间
git rev-list --objects --all | \
  git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | \
  sed -n 's/^blob //p' | \
  sort --numeric-sort --key=2 | \
  tail -20

解决方案1:Git LFS

# 安装Git LFS
brew install git-lfs  # Mac
apt install git-lfs   # Ubuntu

# 初始化
git lfs install

# 跟踪大文件
git lfs track "*.psd"
git lfs track "*.zip"
git lfs track "assets/**"

# 查看跟踪规则
git lfs track

# 提交跟踪规则
git add .gitattributes
git commit -m "chore: 配置Git LFS"

解决方案2:清理历史

# 从历史中彻底删除大文件
git filter-repo --path huge-file.zip --invert-paths

# 清理垃圾
git reflog expire --expire=now --all
git gc --prune=now --aggressive

性能优化

clone太慢

# 浅克隆(只克隆最近的历史)
git clone --depth 1 https://github.com/user/repo.git

# 单分支克隆
git clone --single-branch --branch main https://github.com/user/repo.git

# 后续获取完整历史
git fetch --unshallow

仓库太大

# 清理无用对象
git gc

# 更激进的清理
git gc --aggressive --prune=now

# 清理reflog
git reflog expire --expire=now --all
git gc --prune=now

status太慢

# 禁用文件监控(大仓库)
git config core.fsmonitor false
git config core.untrackedCache false

子模块

添加子模块

git submodule add https://github.com/user/lib.git libs/lib

克隆包含子模块的仓库

# 方法1:克隆时自动初始化
git clone --recursive https://github.com/user/repo.git

# 方法2:克隆后手动初始化
git clone https://github.com/user/repo.git
cd repo
git submodule init
git submodule update

更新子模块

# 更新到最新
git submodule update --remote

# 更新所有子模块
git submodule foreach git pull origin main

删除子模块

# 删除子模块(Git 1.8.3+)
git submodule deinit libs/lib
git rm libs/lib
rm -rf .git/modules/libs/lib

常见错误

error: Your local changes would be overwritten

# 情况1:想保留本地修改
git stash
git pull
git stash pop

# 情况2:丢弃本地修改
git reset --hard
git pull

fatal: refusing to merge unrelated histories

# 两个仓库历史不相关,需要强制合并
git pull origin main --allow-unrelated-histories

! [rejected] main -> main (non-fast-forward)

# 远程有新提交,先拉取
git pull --rebase origin main
git push origin main

fatal: Authentication failed

# 检查SSH密钥
ssh -T git@github.com

# 检查远程URL
git remote -v

# 切换到SSH
git remote set-url origin git@github.com:user/repo.git

# 或使用token
git remote set-url origin https://@github.com/user/repo.git

fatal: not a git repository

# 当前目录不在Git仓库中
# 初始化仓库
git init

# 或进入正确的目录
cd /path/to/repo

调试技巧

查看某行代码的修改历史

# 查看文件的每一行是谁在什么时候改的
git blame filename.txt

# 只看某几行
git blame -L 10,20 filename.txt

# 查看某行代码的完整历史
git log -p -S "特定代码" filename.txt

找出引入bug的提交

# 二分查找
git bisect start
git bisect bad          # 当前版本有bug
git bisect good v1.0    # v1.0版本正常
# Git会自动跳到中间提交,你测试后标记
git bisect good/bad
# 重复直到找到
git bisect reset

查看reflog(操作日志)

# 查看所有操作记录
git reflog

# 找到误删的提交
git checkout 

最佳实践总结

DO

  1. 频繁提交:小步快跑,每个逻辑改动一个提交
  2. 写好提交信息:让未来的自己和队友能看懂
  3. 先pull再push:避免不必要的冲突
  4. 使用分支:不要在main上直接开发
  5. 定期备份:重要分支推送到远程

DON'T

  1. 不要push --force:除非你确定自己在做什么
  2. 不要commit敏感信息:密码、密钥一旦push就很难彻底清除
  3. 不要在公共分支rebase:会改写历史,坑队友
  4. 不要忽略.gitignore:node_modules、.env必须忽略
  5. 不要提交大文件:使用Git LFS

Git速查表

# 初始化
git init                          # 初始化仓库
git clone                    # 克隆仓库

# 日常操作
git status                        # 查看状态
git add                     # 添加到暂存区
git commit -m "message"           # 提交
git push                          # 推送
git pull                          # 拉取

# 分支操作
git branch                  # 创建分支
git checkout              # 切换分支
git checkout -b           # 创建并切换
git merge                 # 合并分支
git branch -d             # 删除分支

# 撤销操作
git checkout --             # 撤销工作区修改
git reset HEAD              # 撤销暂存
git commit --amend                # 修改最后一次提交
git revert                # 撤销提交(安全)
git reset --hard          # 回退到指定提交(危险)

# 历史查看
git log --oneline --graph         # 图形化日志
git blame                   # 查看每行修改记录
git reflog                        # 操作历史

# 暂存工作
git stash                         # 暂存当前工作
git stash pop                     # 恢复暂存

系列总结

恭喜你完成了整个Git系列!

第一期:Git基础 - 从安装到日常操作
第二期:分支管理 - 在平行宇宙中开发
第三期:团队协作 - Pull Request与代码审查
第四期:自动化 - 钩子、CI/CD与效率工具
第五期:实战宝典 - 从翻车到救命

你现在已经是Git高手了。但记住:Git只是工具,真正重要的是团队协作的规范和习惯。工具再好,用不好也是白搭。

保持学习,保持实践,保持分享。Git的世界还有很多宝藏等你探索。


进阶资源


恭喜!你已经掌握了Git的全部核心技能。去征服代码世界吧!

Views: 56

Git钩子与自动化:让代码质量自动守护

Git钩子与自动化:让代码质量自动守护

前三期我们掌握了Git的基础操作、分支管理和团队协作。现在,让我们把效率提升到下一个层次——自动化。

Git钩子(Hooks)是Git在特定事件发生时自动执行的脚本。比如,每次commit前自动检查代码格式,每次push前自动运行测试,每次merge后自动部署。

这一期,我们深入Git的自动化世界。

Git钩子基础

钩子存放在.git/hooks/目录:

ls -la .git/hooks/

你会看到很多.sample文件,这是示例。去掉.sample后缀就激活了。

钩子类型

客户端钩子(本地执行):

  • pre-commit:commit前,检查代码
  • prepare-commit-msg:commit message生成后,编辑器启动前
  • commit-msg:commit message写完后,检查格式
  • post-commit:commit完成后,发通知
  • pre-push:push前,跑测试
  • post-checkout:切换分支后,安装依赖

服务端钩子(远程执行):

  • pre-receive:push到达服务器时
  • update:每个分支更新时
  • post-receive:push完成后

第一个钩子

创建一个简单的pre-commit钩子,检查是否有console.log

# .git/hooks/pre-commit
#!/bin/bash

# 检查暂存区的JS文件
files=$(git diff --cached --name-only --filter=ACM | grep '\.js$')

if [ -z "$files" ]; then
    exit 0
fi

# 检查是否有console.log
if grep -n "console\.log" $files; then
    echo "❌ 发现console.log,请移除后再提交"
    exit 1
fi

exit 0
chmod +x .git/hooks/pre-commit

现在,如果有console.log,commit会被拒绝。

Husky:现代钩子管理

直接编辑.git/hooks/有几个问题:

  • 钩子不会被Git跟踪,队友无法共享
  • 每次clone后需要手动设置
  • 脚本管理混乱

Husky解决了这些问题。

安装

npm install husky --save-dev

# 初始化(自动创建.husky目录)
npx husky init

配置pre-commit

# 创建pre-commit钩子
echo "npm test" > .husky/pre-commit

现在每次commit前会自动运行测试。测试失败,commit就被拒绝。

配置commit-msg

验证提交信息格式:

# .husky/commit-msg
#!/bin/bash

msg=$(cat $1)

# 检查是否符合约定式提交
if ! echo "$msg" | grep -qE "^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{1,}"; then
    echo "❌ 提交信息格式错误"
    echo "格式: (): "
    echo "示例: feat(auth): 添加OAuth登录"
    exit 1
fi
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'

lint-staged:只检查暂存文件

全量检查太慢?lint-staged只检查你改动的文件。

npm install lint-staged --save-dev
// package.json
{
  "lint-staged": {
    "*.js": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.css": [
      "stylelint --fix",
      "prettier --write"
    ],
    "*.{json,md}": [
      "prettier --write"
    ]
  }
}
# .husky/pre-commit
npx lint-staged

流程:

  1. git commit触发pre-commit
  2. lint-staged检查暂存的文件
  3. ESLint/Prettier自动修复
  4. 修复后的文件重新add
  5. commit继续

commitlint:提交信息规范

npm install @commitlint/cli @commitlint/config-conventional --save-dev
// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      [
        'feat',
        'fix',
        'docs',
        'style',
        'refactor',
        'perf',
        'test',
        'build',
        'ci',
        'chore',
        'revert'
      ]
    ],
    'subject-case': [2, 'always', 'lower-case']  // 主题小写
  }
};

现在,不符合规范的提交会被拒绝:

git commit -m "添加功能"
# ❌ 提交失败:必须以type开头

git commit -m "feat: 添加用户登录功能"
# ✅ 提交成功

完整工作流配置

一个现代化的前端项目配置:

// package.json
{
  "scripts": {
    "prepare": "husky install",
    "lint": "eslint . --ext .js,.ts,.tsx",
    "format": "prettier --write .",
    "test": "jest"
  },
  "lint-staged": {
    "*.{js,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,scss,json,md}": [
      "prettier --write"
    ]
  },
  "devDependencies": {
    "@commitlint/cli": "^18.0.0",
    "@commitlint/config-conventional": "^18.0.0",
    "eslint": "^8.0.0",
    "husky": "^9.0.0",
    "lint-staged": "^15.0.0",
    "prettier": "^3.0.0"
  }
}
# .husky/pre-commit
npx lint-staged

# .husky/commit-msg
npx --no -- commitlint --edit $1

# .husky/pre-push
npm test
// commitlint.config.js
module.exports = { extends: ['@commitlint/config-conventional'] };

一次clone后,所有开发者都会自动拥有相同的检查规则。

Git别名:效率提升

配置常用别名,减少输入:

# 基础别名
git config --global alias.st status
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commit

# 查看日志
git config --global alias.lg "log --oneline --graph --all"
git config --global alias.last 'log -1 HEAD'

# 撤销
git config --global alias.unstage 'reset HEAD --'
git config --global alias.undo 'checkout HEAD --'

# 有用的别名
git config --global alias.stash-all 'stash save --include-untracked'
git config --global alias.push-force 'push --force-with-lease'

使用:

git st          # = git status
git lg          # 美观的日志图
git unstage file.txt  # 取消暂存

自定义Git命令

创建git-xxx脚本,就能用git xxx调用:

# ~/bin/git-publish
#!/bin/bash
# 发布当前分支到远程

branch=$(git branch --show-current)
git push -u origin $branch
chmod +x ~/bin/git-publish
export PATH=$PATH:~/bin

# 使用
git publish

自动部署

post-receive钩子(服务端)

在服务器上配置自动部署:

# /var/git/project.git/hooks/post-receive
#!/bin/bash

TARGET="/var/www/project"
GIT_DIR="/var/git/project.git"

while read oldrev newrev ref
do
    BRANCH=$(git rev-parse --symbolic --abbrev-ref $ref)

    if [ "$BRANCH" = "main" ]; then
        echo "Deploying main branch..."
        git --work-tree=$TARGET --git-dir=$GIT_DIR checkout -f main
        cd $TARGET
        npm install --production
        pm2 restart project
        echo "Deploy complete."
    fi
done

现在,push到main分支会自动部署。

GitHub Actions自动部署

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - run: npm ci
      - run: npm run build

      - name: Deploy to server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.SSH_KEY }}
          script: |
            cd /var/www/project
            git pull
            npm ci --production
            pm2 restart project

Vercel/Netlify自动部署

最简单的方式:连接GitHub仓库,自动部署。

  • push到main → 自动构建 → 自动部署
  • PR预览 → 每个PR都有独立预览链接
  • 回滚 → 点一下就回退

Git bisect:定位问题提交

当项目出现bug,但不知道是哪个提交引入的:

# 开始二分查找
git bisect start

# 标记当前提交有问题
git bisect bad

# 标记某个旧提交是正常的
git bisect good v1.0.0

# Git会自动跳到中间的提交
# 你测试后标记
git bisect good  # 或 git bisect bad

# 重复直到找到问题提交
# Git会告诉你哪个提交有问题

# 结束
git bisect reset

自动化:

# 自动运行测试脚本来判断
git bisect run npm test

Git worktree:多分支并行开发

需要同时在多个分支工作,但不想频繁切换?

# 创建worktree
git worktree add ../project-feature feature-branch

# 现在有两个工作目录
# ../project (main分支)
# ../project-feature (feature-branch)

# 在project-feature中工作
cd ../project-feature
# 修改、提交...

# 完成后删除worktree
git worktree remove ../project-feature

小结

你现在掌握了Git自动化的核心技能:

  • Git钩子原理和常用钩子
  • Husky + lint-staged + commitlint现代工作流
  • 配置Git别名提升效率
  • 服务端自动部署
  • GitHub Actions CI/CD
  • Git bisect定位问题
  • Git worktree多分支并行

下一期,我们将进入实战环节——常见坑与解决方案。你会学到如何处理那些让新手崩溃的场景。


练习任务

  1. 为你的项目配置Husky + lint-staged
  2. 配置commitlint,尝试提交不规范信息(应该被拒绝)
  3. 配置一个Git别名,一键查看美化日志
  4. 用git bisect找一个故意引入的bug

下期预告:《Git实战:常见坑与解决方案》—— 误删分支怎么办?提交到错误分支怎么救?大文件怎么处理?

Views: 19

Git团队协作:让代码审查成为日常

Git团队协作:让代码审查成为日常

前两期我们掌握了Git的基础操作和分支管理。但Git的真正威力在于协作——让一群人同时开发,而不会互相踩脚。

这一期,我们来聊聊团队协作的最佳实践。

Fork vs Clone

Clone:直接克隆仓库,有写权限

git clone git@github.com:myteam/project.git

适用于团队成员,对仓库有直接写权限。

Fork:在你的账号下创建仓库副本

  1. 在GitHub/GitLab页面点击Fork
  2. 克隆你fork的仓库
    git clone git@github.com:yourname/project.git

    适用于开源贡献者,对原仓库没有写权限。

Pull Request工作流

Pull Request(PR)是GitHub的叫法,GitLab叫Merge Request(MR)。本质相同:请求把你的分支合并到目标分支。

流程

graph TB
    A[从main创建feature分支] --> B[开发并提交]
    B --> C[推送到远程]
    C --> D[在GitHub创建PR]
    D --> E[代码审查]
    E --> F{通过?}
    F -->|是| G[合并PR]
    F -->|否| H[修改代码]
    H --> B
    G --> I[删除feature分支]

实战示例

# 1. 确保main是最新的
git checkout main
git pull origin main

# 2. 创建功能分支
git checkout -b feature/add-dark-mode

# 3. 开发...
git add .
git commit -m "feat: 添加暗黑模式支持"

# 4. 推送到远程
git push -u origin feature/add-dark-mode

# 5. 去GitHub/GitLab页面创建PR

创建PR时,填写:

  • 标题:简洁描述改动(如"feat: 添加暗黑模式支持")
  • 描述:详细说明改了什么、为什么改、怎么测试
  • 审查者:指定审查代码的人
  • 标签:如enhancement、bug、documentation

好的PR长什么样?

标题格式(约定式提交):

  • feat::新功能
  • fix::bug修复
  • docs::文档更新
  • refactor::重构
  • test::测试
  • chore::杂务(依赖更新、配置等)

描述模板

## 改动描述
添加了暗黑模式支持,用户可以在设置中切换主题。

## 改动原因
用户反馈夜间使用时太刺眼,需要暗黑模式。

## 如何测试
1. 登录系统
2. 进入设置页面
3. 点击"切换主题"
4. 验证页面颜色变化

## 截图
![暗黑模式效果](screenshot.png)

## 相关Issue
Closes #123

一个PR只做一件事

  • 不要把多个不相关的改动塞进一个PR
  • 小PR更容易审查,更快合并
  • 理想大小:200-400行代码

代码审查

代码审查(Code Review)是团队协作的核心。它不仅能发现bug,还能传播知识、统一风格。

审查者看什么?

  1. 正确性:代码是否实现了需求?
  2. 可读性:变量名、函数名是否清晰?逻辑是否易懂?
  3. 性能:有没有明显的性能问题?
  4. 安全:有没有SQL注入、XSS等安全隐患?
  5. 测试:有没有足够的测试覆盖?
  6. 风格:是否符合团队的编码规范?

审查评论类型

必须修改(Blocking)

🛑 这里有个bug,变量未定义就会使用

建议修改(Non-blocking)

💡 建议:这里用Array.filter会更简洁

提问

❓ 为什么要用递归?迭代是不是更合适?

称赞

👍 这个抽象做得很好!

作为被审查者

  1. 不要抵触批评:审查的是代码,不是你这个人
  2. 解释而非辩解:解释为什么这样写,而不是为错误找借口
  3. 及时响应:不要让PR挂着一周不动
  4. 自己先审查一遍:push前自己diff一下,避免低级错误
# 提交前自查
git diff main...HEAD  # 查看当前分支的所有改动

解决PR冲突

PR期间main可能有了新提交,导致你的PR冲突:

# 方法1:merge
git checkout feature/add-dark-mode
git fetch origin
git merge origin/main
# 解决冲突...
git push

# 方法2:rebase(推荐,保持线性历史)
git checkout feature/add-dark-mode
git fetch origin
git rebase origin/main
# 解决冲突...
git push --force-with-lease  # 注意:需要force push

注意--force-with-lease--force更安全,它会在远程有新提交时拒绝push。

Code Owner

大项目可以配置CODEOWNERS文件,自动指定审查者:

# .github/CODEOWNERS

# 前端代码由前端组审查
/frontend/ @myteam/frontend

# API代码由后端组审查
/api/ @myteam/backend

# 安全相关由安全组审查
**/auth* @myteam/security

保护分支

防止直接push到main分支,强制走PR流程:

GitHub设置:

  1. Settings → Branches → Add rule
  2. Branch name pattern: main
  3. 勾选:
    • Require a pull request before merging
    • Require approvals(需要几个人审批)
    • Require status checks to pass before merging(CI必须通过)

这样,任何人(包括管理员)都不能直接push到main,必须通过PR。

CI/CD集成

PR可以触发自动测试,只有测试通过才能合并:

# .github/workflows/test.yml
name: Test

on:
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm test
      - run: npm run lint

这样每次PR都会自动跑测试,测试失败就无法合并。

多人协作常见问题

问题1:Push被拒绝

! [rejected] main -> main (non-fast-forward)

原因:远程有新提交,你本地没有。

解决

git pull --rebase origin main
git push origin main

问题2:别人的分支怎么更新?

# 获取所有远程分支
git fetch --all

# 查看同事的分支
git branch -r

# 基于同事的分支创建本地分支
git checkout -b feature-xxx origin/feature-xxx

问题3:如何同步Fork?

# 1. 添加上游仓库
git remote add upstream git@github.com:original-owner/project.git

# 2. 获取上游更新
git fetch upstream

# 3. 合并到本地main
git checkout main
git merge upstream/main

# 4. 推送到你的fork
git push origin main

问题4:不小心push了敏感信息!

# 立即删除敏感文件
git rm --cached config/secrets.yml
git commit -m "chore: 移除敏感文件"
git push

# 历史中还有!需要彻底清除
# 使用BFG Repo-Cleaner(推荐)
bfg --delete-files secrets.yml
git push --force

# 或用git filter-branch(慢)
git filter-branch --force --index-filter \
  'git rm --cached --ignore-unmatch config/secrets.yml' \
  --prune-empty --tag-name-filter cat -- --all
git push --force

重要:改写历史后,所有协作者需要重新clone仓库!

协作规范

Git提交规范

使用约定式提交(Conventional Commits):

(): 

<footer>

示例:

feat(auth): 添加OAuth2.0登录支持

- 支持Google登录
- 支持GitHub登录
- 添加登录状态持久化

Closes #456

好处:

  • 自动生成changelog
  • 自动决定版本号(feat是minor,fix是patch)
  • 清晰的历史记录

提交信息模板

配置提交模板:

git config commit.template ~/.gitmessage

~/.gitmessage内容:

# :  (50 chars)
# |- - - - - - - - - - - - - - - - - - - - - ->|

#  (72 chars)
# |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ->|

# Type:
#   feat     新功能
#   fix      Bug修复
#   docs     文档
#   style    格式(不影响代码运行)
#   refactor 重构
#   test     测试
#   chore    构建/工具

分支保护策略

graph LR
    A[feature分支] -->|PR| B[develop分支]
    C[bugfix分支] -->|PR| B
    B -->|PR| D[release分支]
    D -->|测试通过| E[main分支]
    F[hotfix分支] -->|PR| E
    E -->|backport| B

小结

你现在掌握了团队协作的核心技能:

  • Fork vs Clone的使用场景
  • Pull Request完整流程
  • 代码审查的最佳实践
  • 解决PR冲突
  • 配置分支保护和CI/CD
  • 多人协作常见问题解决

下一期,我们将进入高级主题——Git钩子和自动化。你会学到如何在commit/push时自动检查代码、运行测试、部署应用。


练习任务

  1. 在GitHub创建一个测试仓库,尝试完整的PR流程
  2. 邀请朋友审查你的PR,练习代码审查对话
  3. 配置一个简单的GitHub Actions,自动运行测试
  4. 尝试在PR中制造冲突并解决

下期预告:《Git高级:钩子与自动化》—— pre-commit怎么配置?如何自动部署?Husky和lint-staged怎么用?

Views: 30