普通视图

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

pnpm monorepo 联调:告别 --global 参数

作者 JinSo
2025年10月19日 15:08

前言

在之前的文章《pnpm monorepo 联调方案》中,我详细介绍了如何使用 pnpm linkpnpm link --global 来解决 monorepo 环境下的调试难题。

时间过去了一段时间,pnpm 也在不断演进。最近在使用过程中,我发现了一个有趣的变化:执行 pnpm link 时不再需要添加 --global 参数,同时 pnpm 会自动创建 pnpm-workspace.yaml 文件。这引起了我的好奇心,决定深入研究一下 pnpm 10.x 版本中 link 功能的最新变化。

pnpm 10.x 中关于 link 的变更

最大的变化就是 去除了 --global 参数

之前我们这样操作:

# 库中
pnpm link --global

# 项目中
pnpm link --global <pkg>

现在直接:

# 库中
pnpm link

# 项目中
pnpm link <pkg>

看起来是小改动,但其实是把之前的 pnpm link --global 的行为直接变成了 pnpm link 的默认行为。

从官方文档也能看出来,现在的 pnpm link 描述和之前 9.x 版本的 pnpm link --global 完全一样。

image0.png

image1.png

取消链接还是用 pnpm unlink

pnpm unlink <pkg>

实际使用体验

在库中执行 link

现在在基础库中执行 pnpm link,会直接链接到全局:

cd ~/packages/core
pnpm install
pnpm link

# 输出示例
❯ pnpm link
 WARN  The package @easy-editor/core, which you have just pnpm linked, has the following peerDependencies specified in its package.json:
  - mobx@^6.13.5
The linked in dependency will not resolve the peer dependencies from the target node_modules.
This might cause issues in your project. To resolve this, you may use the "file:" protocol to reference the local dependency.
√ The modules directory at "C:\\Users\\user\\AppData\\Local\\pnpm\\global\\5\\node_modules" will be removed and reinstalled from scratch. Proceed? (Y/n) · true
Recreating C:\\Users\\user\\AppData\\Local\\pnpm\\global\\5\\node_modules

在项目中链接库

在项目中链接也更直接了:

cd ~/projects/my-project
pnpm link @easy-editor/core

# 输出示例
❯ pnpm link @easy-editor/core
 WARN  The package @easy-editor/core, which you have just pnpm linked, has the following peerDependencies specified in its package.json:
  - mobx@^6.13.5
The linked in dependency will not resolve the peer dependencies from the target node_modules.
This might cause issues in your project. To resolve this, you may use the "file:" protocol to reference the local dependency.
√ The modules directory at "D:\\Programming\\EasyEditor\\EasyDashboard\\node_modules" will be removed and reinstalled from scratch. Proceed? (Y/n) · true
Recreating D:\\Programming\\EasyEditor\\EasyDashboard\\node_modules
dependencies:
    + @easy-editor/core 0.0.0 <- C:\\Users\\user\\AppData\\Local\\pnpm\\global\\5\\node_modules\\@easy-editor\\core

自动生成的 workspace 配置

这是个新发现,链接完成后,pnpm 会自动在项目根目录生成 pnpm-workspace.yaml

overrides:
  '@easy-editor/core': link:C:/Users/user/AppData/Local/pnpm/global/5/node_modules/@easy-editor/core

这样就能清楚地看到当前项目链接了哪些包,比之前透明多了。

多库联调的改进

之前需要给每个库都加 --global,现在直接:

# 在各个库中
cd ~/packages/core && pnpm link
cd ~/packages/designer && pnpm link
cd ~/packages/utils && pnpm link

# 在项目中
cd ~/projects/my-project
pnpm link @myorg/core
pnpm link @myorg/designer
pnpm link @myorg/utils

对于 workspace:* 这种依赖,pnpm 还是会自动处理,和之前一样智能。

前后对比

pnpm 9.x pnpm 10.x
全局链接 pnpm link --global pnpm link
项目链接 pnpm link --global <pkg> pnpm link <pkg>
workspace 配置 手动管理 自动生成
操作复杂度 需要记住加 --global 更简单直接

总结

pnpm 10.x 的这个改动虽然看起来不大,但确实让联调操作更简单了。不用再记那个 --global 参数,直接 pnpm link 就完事。

加上自动生成的 workspace 配置文件,整个链接状态也更透明了。如果你还在用旧版本,建议升级试试,体验确实有提升。


参考资料

  1. pnpm link | pnpm 官方文档
  2. "pnpm link --global" in v10 behaves differently from v9, breaking global linking.

UniApp 微信小程序开发使用心得

2025年10月9日 15:56

前言

UniApp 作为一款跨平台开发框架,为开发者提供了"一次开发,多端部署"的强大能力。在众多支持的平台中,微信小程序是最重要和最常用的目标平台之一。通过长时间的实践,我对 UniApp 开发微信小程序有了深入的理解和体会。

一、UniApp 基础认知与优势

1.1 UniApp 核心优势

  • 统一开发体验:使用 Vue.js 语法,降低了学习成本
  • 多端编译能力:一套代码可以发布到微信小程序、H5、App 等多个平台
  • 生态丰富:拥有庞大的插件市场和社区支持
  • 性能优化:框架层面做了大量性能优化工作

1.2 与原生小程序的对比

// UniApp 写法
<template>
  <view class="container">
    <text>{{ message }}</text>
    <button @click="handleClick">点击</button>
  </view>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello UniApp'
    }
  },
  methods: {
    handleClick() {
      uni.showToast({
        title: '点击成功'
      })
    }
  }
}
</script>

相比原生小程序,UniApp 提供了更接近 Vue 的开发体验,代码更加简洁易懂。

二、项目搭建与环境配置

2.1 开发环境准备

  1. HBuilderX:官方推荐的 IDE,集成了丰富的开发工具
  2. Node.js:确保版本在 12.0 以上
  3. 微信开发者工具:用于调试和预览小程序

2.2 项目初始化

# 通过 HBuilderX 创建项目
# 或者使用命令行
npm install -g @vue/cli
vue create -p dcloudio/uni-preset-vue my-project

2.3 配置文件详解

// manifest.json - 小程序配置
{
  "mp-weixin": {
    "appid": "your-appid",
    "setting": {
      "urlCheck": false,
      "es6": true,
      "postcss": true
    },
    "usingComponents": true
  }
}

三、核心开发技巧与最佳实践

3.1 页面与组件开发

3.1.1 页面生命周期管理

export default {
  // 页面加载
  onLoad(options) {
    console.log('页面加载', options)
  },
  
  // 页面显示
  onShow() {
    console.log('页面显示')
  },
  
  // 页面隐藏
  onHide() {
    console.log('页面隐藏')
  },
  
  // 页面卸载
  onUnload() {
    console.log('页面卸载')
  },
  
  // 下拉刷新
  onPullDownRefresh() {
    this.refreshData()
  },
  
  // 上拉加载
  onReachBottom() {
    this.loadMore()
  }
}

3.1.2 组件封装技巧

<!-- 自定义组件示例 -->
<template>
  <view class="custom-card">
    <slot name="header"></slot>
    <view class="content">
      <slot></slot>
    </view>
    <slot name="footer"></slot>
  </view>
</template>

<script>
export default {
  name: 'CustomCard',
  props: {
    padding: {
      type: [String, Number],
      default: 20
    }
  }
}
</script>

3.2 数据管理与状态共享

3.2.1 Vuex 状态管理

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    userInfo: null,
    token: ''
  },
  mutations: {
    SET_USER_INFO(state, info) {
      state.userInfo = info
    },
    SET_TOKEN(state, token) {
      state.token = token
    }
  },
  actions: {
    async login({ commit }, payload) {
      try {
        const res = await api.login(payload)
        commit('SET_TOKEN', res.token)
        commit('SET_USER_INFO', res.userInfo)
        return res
      } catch (error) {
        throw error
      }
    }
  }
})

export default store

3.2.2 全局数据共享

// utils/globalData.js
class GlobalData {
  constructor() {
    this.data = {}
  }
  
  set(key, value) {
    this.data[key] = value
  }
  
  get(key) {
    return this.data[key]
  }
  
  remove(key) {
    delete this.data[key]
  }
}

export default new GlobalData()

3.3 网络请求封装

// utils/request.js
class Request {
  constructor() {
    this.baseURL = 'https://api.example.com'
    this.timeout = 10000
  }
  
  // 请求拦截
  interceptRequest(config) {
    // 添加 token
    const token = uni.getStorageSync('token')
    if (token) {
      config.header = {
        ...config.header,
        'Authorization': `Bearer ${token}`
      }
    }
    return config
  }
  
  // 响应拦截
  interceptResponse(response) {
    const { data, statusCode } = response
    if (statusCode === 200) {
      return data
    } else if (statusCode === 401) {
      // token 过期处理
      uni.redirectTo({
        url: '/pages/login/login'
      })
    } else {
      uni.showToast({
        title: data.message || '请求失败',
        icon: 'none'
      })
      throw new Error(data.message)
    }
  }
  
  request(options) {
    return new Promise((resolve, reject) => {
      const config = this.interceptRequest({
        url: this.baseURL + options.url,
        timeout: this.timeout,
        ...options
      })
      
      uni.request({
        ...config,
        success: (res) => {
          try {
            const data = this.interceptResponse(res)
            resolve(data)
          } catch (error) {
            reject(error)
          }
        },
        fail: (err) => {
          uni.showToast({
            title: '网络错误',
            icon: 'none'
          })
          reject(err)
        }
      })
    })
  }
}

export default new Request()

四、性能优化策略

4.1 渲染性能优化

4.1.1 虚拟列表实现

<template>
  <scroll-view 
    class="virtual-list" 
    :scroll-y="true" 
    @scroll="onScroll"
    :scroll-top="scrollTop"
  >
    <view class="placeholder" :style="{ height: topPlaceholderHeight + 'px' }"></view>
    <view 
      v-for="item in visibleItems" 
      :key="item.id" 
      class="list-item"
    >
      {{ item.name }}
    </view>
    <view class="placeholder" :style="{ height: bottomPlaceholderHeight + 'px' }"></view>
  </scroll-view>
</template>

<script>
export default {
  data() {
    return {
      allItems: [],
      visibleItems: [],
      itemHeight: 50,
      containerHeight: 500,
      scrollTop: 0
    }
  },
  computed: {
    topPlaceholderHeight() {
      return this.startIndex * this.itemHeight
    },
    bottomPlaceholderHeight() {
      return (this.allItems.length - this.endIndex) * this.itemHeight
    }
  },
  methods: {
    onScroll(e) {
      const scrollTop = e.detail.scrollTop
      this.updateVisibleItems(scrollTop)
    },
    updateVisibleItems(scrollTop) {
      const visibleCount = Math.ceil(this.containerHeight / this.itemHeight)
      this.startIndex = Math.floor(scrollTop / this.itemHeight)
      this.endIndex = this.startIndex + visibleCount
      
      this.visibleItems = this.allItems.slice(
        this.startIndex, 
        Math.min(this.endIndex, this.allItems.length)
      )
    }
  }
}
</script>

4.1.2 图片懒加载优化

<template>
  <view class="image-list">
    <view 
      v-for="item in imageList" 
      :key="item.id" 
      class="image-item"
    >
      <image 
        :src="item.loaded ? item.url : defaultImage" 
        :data-src="item.url"
        @load="onImageLoad"
        mode="aspectFill"
        lazy-load
      />
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      imageList: [],
      defaultImage: '/static/images/placeholder.png'
    }
  },
  methods: {
    onImageLoad(e) {
      const src = e.currentTarget.dataset.src
      const index = this.imageList.findIndex(item => item.url === src)
      if (index !== -1) {
        this.$set(this.imageList[index], 'loaded', true)
      }
    }
  }
}
</script>

4.2 网络性能优化

4.2.1 请求缓存机制

// utils/cache.js
class CacheManager {
  constructor() {
    this.cache = new Map()
    this.ttl = 5 * 60 * 1000 // 5分钟缓存
  }
  
  set(key, data) {
    this.cache.set(key, {
      data,
      timestamp: Date.now()
    })
  }
  
  get(key) {
    const item = this.cache.get(key)
    if (!item) return null
    
    // 检查是否过期
    if (Date.now() - item.timestamp > this.ttl) {
      this.cache.delete(key)
      return null
    }
    
    return item.data
  }
  
  clear() {
    this.cache.clear()
  }
}

export default new CacheManager()

4.2.2 请求防抖与节流

// utils/debounce.js
export function debounce(func, wait) {
  let timeout
  return function(...args) {
    clearTimeout(timeout)
    timeout = setTimeout(() => func.apply(this, args), wait)
  }
}

// utils/throttle.js
export function throttle(func, wait) {
  let timeout
  return function(...args) {
    if (!timeout) {
      timeout = setTimeout(() => {
        func.apply(this, args)
        timeout = null
      }, wait)
    }
  }
}

五、常见问题与解决方案

5.1 跨端兼容性问题

5.1.1 条件编译处理

// #ifdef MP-WEIXIN
// 微信小程序特有代码
wx.doSomething()
// #endif

// #ifdef H5
// H5 特有代码
window.doSomething()
// #endif

// #ifndef APP-PLUS
// 非 App 端代码
console.log('非 App 端')
// #endif

5.1.2 平台差异处理

// utils/platform.js
export const isWechat = () => {
  // #ifdef MP-WEIXIN
  return true
  // #endif
  return false
}

export const showToast = (options) => {
  // #ifdef MP-WEIXIN
  uni.showToast({
    ...options,
    icon: options.icon || 'none'
  })
  // #endif
  
  // #ifdef H5
  // H5 端自定义 toast
  // #endif
}

5.2 内存泄漏防范

5.2.1 定时器清理

export default {
  data() {
    return {
      timer: null
    }
  },
  methods: {
    startTimer() {
      this.timer = setInterval(() => {
        // 定时任务
        this.updateData()
      }, 1000)
    }
  },
  beforeDestroy() {
    // 清理定时器
    if (this.timer) {
      clearInterval(this.timer)
      this.timer = null
    }
  }
}

5.2.2 事件监听器清理

export default {
  mounted() {
    // 添加事件监听
    uni.$on('customEvent', this.handleCustomEvent)
  },
  beforeDestroy() {
    // 移除事件监听
    uni.$off('customEvent', this.handleCustomEvent)
  },
  methods: {
    handleCustomEvent(data) {
      // 处理事件
    }
  }
}

六、调试与测试经验

6.1 调试工具使用

6.1.1 控制台调试

// 开发环境调试信息
if (process.env.NODE_ENV === 'development') {
  console.log('调试信息:', data)
}

// 自定义日志工具
class Logger {
  static info(...args) {
    if (process.env.NODE_ENV === 'development') {
      console.info('[INFO]', ...args)
    }
  }
  
  static error(...args) {
    console.error('[ERROR]', ...args)
  }
}

6.1.2 网络请求监控

// utils/requestMonitor.js
class RequestMonitor {
  constructor() {
    this.requests = []
  }
  
  addRequest(config) {
    const request = {
      id: Date.now(),
      url: config.url,
      method: config.method,
      startTime: Date.now(),
      status: 'pending'
    }
    this.requests.push(request)
    return request.id
  }
  
  updateRequest(id, status, response) {
    const request = this.requests.find(req => req.id === id)
    if (request) {
      request.status = status
      request.endTime = Date.now()
      request.duration = request.endTime - request.startTime
      request.response = response
    }
  }
}

6.2 单元测试实践

// test/utils.test.js
import { debounce, throttle } from '@/utils'

describe('工具函数测试', () => {
  test('防抖函数', (done) => {
    let count = 0
    const fn = debounce(() => {
      count++
    }, 100)
    
    fn()
    fn()
    fn()
    
    setTimeout(() => {
      expect(count).toBe(1)
      done()
    }, 150)
  })
})

七、用户体验优化

7.1 加载状态管理

<template>
  <view class="page">
    <loading v-if="loading" />
    <error v-else-if="error" :message="errorMessage" @retry="retry" />
    <content v-else :data="data" />
  </view>
</template>

<script>
export default {
  data() {
    return {
      loading: false,
      error: false,
      errorMessage: '',
      data: null
    }
  },
  methods: {
    async loadData() {
      this.loading = true
      this.error = false
      
      try {
        const data = await api.getData()
        this.data = data
      } catch (err) {
        this.error = true
        this.errorMessage = err.message
      } finally {
        this.loading = false
      }
    }
  }
}
</script>

7.2 交互反馈优化

// utils/feedback.js
export class Feedback {
  static async confirm(title, content) {
    return new Promise((resolve) => {
      uni.showModal({
        title,
        content,
        success: (res) => {
          resolve(res.confirm)
        }
      })
    })
  }
  
  static toast(title, icon = 'none') {
    uni.showToast({
      title,
      icon
    })
  }
  
  static loading(title = '加载中...') {
    uni.showLoading({
      title
    })
  }
  
  static hideLoading() {
    uni.hideLoading()
  }
}

八、发布与运维经验

8.1 版本管理策略

// package.json 版本管理
{
  "version": "1.2.3",
  "scripts": {
    "build:mp-weixin": "uni-build --platform mp-weixin",
    "build:prod": "uni-build --mode production"
  }
}

8.2 自动化部署

#!/bin/bash
# deploy.sh
echo "开始构建微信小程序..."

# 安装依赖
npm install

# 构建项目
npm run build:mp-weixin

# 上传到微信开发者工具
# 这里可以集成微信开发者工具的命令行工具

echo "构建完成"

九、安全与权限管理

9.1 数据安全

// utils/security.js
class Security {
  // 数据加密
  static encrypt(data) {
    // 实现加密逻辑
    return encryptedData
  }
  
  // 数据解密
  static decrypt(encryptedData) {
    // 实现解密逻辑
    return decryptedData
  }
  
  // 敏感信息脱敏
  static maskPhone(phone) {
    return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
  }
}

9.2 权限控制

// utils/auth.js
class Auth {
  static checkPermission(permission) {
    const userPermissions = uni.getStorageSync('permissions') || []
    return userPermissions.includes(permission)
  }
  
  static async requestPermission(permission) {
    // 请求权限逻辑
  }
}

十、总结与展望

通过长时间的 UniApp 微信小程序开发实践,我深刻体会到跨平台开发的优势和挑战。UniApp 为开发者提供了强大的工具和生态支持,但在实际项目中仍需要关注平台差异、性能优化和用户体验等关键问题。

10.1 最佳实践总结

  1. 合理使用条件编译:针对不同平台做差异化处理
  2. 重视性能优化:特别是列表渲染和网络请求优化
  3. 建立完善的错误处理机制:提升应用稳定性
  4. 注重用户体验:提供流畅的交互和及时的反馈

10.2 未来发展趋势

随着微信小程序生态的不断完善和 UniApp 框架的持续优化,跨平台开发将成为更多团队的选择。未来需要关注:

  1. 性能进一步提升:框架层面的优化将持续进行
  2. 生态更加丰富:插件和组件库将更加完善
  3. 开发体验优化:工具链和调试能力将持续改进

通过不断学习和实践,相信 UniApp 在微信小程序开发领域会有更广阔的应用前景。开发者需要保持对新技术的敏感度,持续优化开发流程和代码质量,才能在激烈的市场竞争中脱颖而出。

重新思考CSS Reset:normalize.css vs reset.css vs remedy.css,在2025年该如何选?

作者 ErpanOmer
2025年10月9日 11:44

what-is-css.png

我带团队Review一个新项目的启动代码时,有一个文件我一定会仔细看,那就是CSS Reset

它虽然不起眼,但却像我们整个CSS架构的地基。地基打不好,上面的楼盖得再漂亮,也容易出问题,后期维护成本会非常高。

从十多年前 reset.css 横空出世,到后来normalize.css 成为事实标准,再到近几年出现的一些新方案,CSS Reset的理念,其实也在不断演进。

但现在都2025年10月了,IE早已入土为安,主流浏览器对标准的支持也空前一致。我们还有必要像十年前那样做重置样式吗?

今天,我就想聊聊我对这几个主流方案的看法,以及在我们团队的当前项目中,我是如何选择的。


reset.css

  • 它的原理:非常暴力直接——抹平所有浏览器默认样式margin, padding, font-size, line-height...通通归零,h1pulli在外观上变得一模一样,所有元素都回到最原始、最裸的状态。

  • 代码片段感受一下

    /* http://meyerweb.com/eric/tools/css/reset/ 
       v2.0 | 20110126
       License: none (public domain)
    */
    
    html, body, div, span, applet, object, iframe,
    h1, h2, h3, h4, h5, h6, p, blockquote, pre,
    a, abbr, acronym, address, big, cite, code,
    del, dfn, em, img, ins, kbd, q, s, samp,
    small, strike, strong, sub, sup, tt, var,
    b, u, i, center,
    dl, dt, dd, ol, ul, li,
    fieldset, form, label, legend,
    table, caption, tbody, tfoot, thead, tr, th, td,
    article, aside, canvas, details, embed, 
    figure, figcaption, footer, header, hgroup, 
    menu, nav, output, ruby, section, summary,
    time, mark, audio, video {
            margin: 0;
            padding: 0;
            border: 0;
            font-size: 100%;
            font: inherit;
            vertical-align: baseline;
    }
    /* HTML5 display-role reset for older browsers */
    article, aside, details, figcaption, figure, 
    footer, header, hgroup, menu, nav, section {
            display: block;
    }
    body {
            line-height: 1;
    }
    ol, ul {
            list-style: none;
    }
    blockquote, q {
            quotes: none;
    }
    blockquote:before, blockquote:after,
    q:before, q:after {
            content: '';
            content: none;
    }
    table {
            border-collapse: collapse;
            border-spacing: 0;
    }
    
  • 优点:提供了一个绝对干净、可预测,非常适合那些需要从零开始、高度定制视觉风格的网站。

  • 2025年的缺点

    1. 太粗暴了:它移除了很多有用的默认样式。比如,你写了一个<ul>,却发现前面的项目符号没了,还得自己手动加回来。
    2. 破坏了语义化:一个<h1>在视觉上和<p>毫无区别,这在开发初期,会削弱HTML语义化的默认视觉反馈。
    3. 调试困难:当你在DevTools里审查一个元素时,你看到的样式,和它本该有的默认样式天差地别,这会增加调试的心智负担。

在2025年,对于绝大多数项目,我不推荐再使用这种粗暴的Reset样式。


normalize.css

screenshot-20251009-114007.png

  • 原理:与reset.css完全相反——保留有用的浏览器默认样式,只修复已知的浏览器不一致和Bug。它不在重置,而是修正。

  • 代码片段感受一下

    /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
    /* Document
       ========================================================================== */
    
    /**
     * 1. Correct the line height in all browsers.
     * 2. Prevent adjustments of font size after orientation changes in iOS.
     */
    
    html {
      line-height: 1.15; /* 1 */
      -webkit-text-size-adjust: 100%; /* 2 */
    }
    
    /* Sections
       ========================================================================== */
    
    /**
     * Remove the margin in all browsers.
     */
    
    body {
      margin: 0;
    }
    
    /**
     * Render the `main` element consistently in IE.
     */
    
    main {
      display: block;
    }
    
    /**
     * Correct the font size and margin on `h1` elements within `section` and
     * `article` contexts in Chrome, Firefox, and Safari.
     */
    
    h1 {
      font-size: 2em;
      margin: 0.67em 0;
    }
    
    /* Grouping content
       ========================================================================== */
    
    /**
     * 1. Add the correct box sizing in Firefox.
     * 2. Show the overflow in Edge and IE.
     */
    
    hr {
      box-sizing: content-box; /* 1 */
      height: 0; /* 1 */
      overflow: visible; /* 2 */
    }
    
    /**
     * 1. Correct the inheritance and scaling of font size in all browsers.
     * 2. Correct the odd `em` font sizing in all browsers.
     */
    
    pre {
      font-family: monospace, monospace; /* 1 */
      font-size: 1em; /* 2 */
    }
    
    /* Text-level semantics
       ========================================================================== */
    
    /**
     * Remove the gray background on active links in IE 10.
     */
    
    a {
      background-color: transparent;
    }
    
    /**
     * 1. Remove the bottom border in Chrome 57-
     * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
     */
     
     /*  大部分已省略,完整的版本可以查看👉 https://github.com/necolas/normalize.css/blob/8.0.1/normalize.css */
    
  • 优点

    1. 保留了元素的默认语义化样式,h1就是比h2大。
    2. 只修复问题,代码注释清晰,像一本浏览器修复手册。
    3. 它成为了过去十年里,包括Bootstrap、Ant Design在内,无数框架和组件库的基石。
  • 2025年的缺点

    1. 过于保守:它只修复不一致,但并没有提供一些我们现代开发中普遍认为更好的默认值。比如,它就没有设置box-sizing: border-box;
    2. 部分规则已过时:它里面的一些修复,是针对我们现在根本不需要支持的、非常古老的浏览器版本的(比如旧版IE)。

normalize.css在今天,依然是一个安全、稳妥的选择。它不会犯错,但我觉得,它有点不够看了😫。


最佳选择:remedy.css

  • 原理:在normalize.css的基础上,再往前走一步。它不仅修正了不一致,还提供了一套我们现代Web开发中,普遍认为 更好的默认样式

  • 核心特性

    1. 开箱即用的border-box

      *, ::before, ::after {
        box-sizing: border-box;
      }
      

      这几乎是所有现代CSS项目的第一行代码,它帮你写好了。

    2. 更好的响应式媒体元素

      img, picture, video, canvas, svg {
        display: block;
        max-width: 100%;
      }
      

      这能天然地防止图片、视频等媒体元素撑破布局,是响应式设计的基础。

    3. 更平滑的字体渲染和滚动

      html {
        -moz-text-size-adjust: none;
        -webkit-text-size-adjust: none;
        text-size-adjust: none;
        scroll-behavior: smooth;
      }
      
    4. 更友好的可用性/无障碍默认值

      [disabled] {
        cursor: not-allowed;
      }
      
  • 优点:它像一个经验丰富的老手,把你开新项目时,那些不得不写的、或者最好要写的样板代码,都提前帮你准备好了。

  • 缺点:它带有一定的主观性。比如,它默认移除了所有元素的margin,统一用padding来控制间距,这需要你适应它的理念。

对于我们团队的新项目,尤其是那些需要快速启动的中后台项目,remedy.css或者类似的现代Reset方案(比如modern-css-reset),已经成为了我的首选


选择与建议🤞

Reset 类型 哲学思想 适用场景 在2025年的建议
reset.css 简单粗暴的重置 高度定制视觉、几乎没有原生HTML元素的UI 不推荐❌
normalize.css 保留并修正 任何项目,尤其是需要保持浏览器原生感的 安全,但略显保守👍
remedy.css 现代最佳实践 所有新项目,尤其是中后台、需要快速启动的项目 强烈推荐首选👍👍👍
自己定义 量身定制 大型项目、有完整设计系统的团队 终极方案,成本高🤔

CSS Reset 只有权衡,没有什么可选,不可选。

但在2025年,我们权衡的基点,已经从如何抹平IE的差异,变成了如何以一个更现代、更高效、更符合最佳实践的基点,来开始我们的工作

所以,下次当你的新项目npm init之后,别再下意识地npm install normalize.css了。

或许,remedy.css会给你一个更好的开始。

祝大家国庆愉快🙌

项目代码提交检测机制实现

作者 galenjx
2025年10月9日 11:16

概述

为提高了代码质量和团队协作效率。通过 Git hooks 和工具链的配合,实现了代码提交检测机制:

  1. 代码质量保证:通过 ESLint 检测代码规范和质量问题
  2. 提交信息规范:通过自定义检查确保提交信息格式统一
  3. 自动化流程:在提交时自动执行检查和格式化
  4. 错误拦截:检测不通过时阻止提交,确保代码库质量

1. 检测机制架构

采用两层检测机制来确保代码质量和提交规范:

git commit
    ↓
┌─────────────────────────────────────┐
│  Pre-commit Hook                    │
│  ├── Prettier 代码格式化            │
│  ├── ESLint 代码质量检测            │
│  └── 重新暂存文件                   │
└─────────────────────────────────────┘
    ↓
┌─────────────────────────────────────┐
│  Prepare-commit-msg Hook            │
│  └── 提交信息格式检查               │
└─────────────────────────────────────┘
    ↓
┌─────────────────────────────────────┐
│  提交完成                           │
└─────────────────────────────────────┘

2. Git Hooks 实现原理

2.1 Yorkie 工具

项目使用 yorkie 包来管理 Git hooks,配置在 package.json 中:

{
  "devDependencies": {
    "yorkie": "^2.0.0"
  },
  "gitHooks": {
    "pre-commit": "lint-staged",
    "prepare-commit-msg": "node ./scripts/git-hooks/prepare-commit-msg.mjs"
  }
  "lint-staged": {
    "*.{js,vue,json}": [
      "prettier --write",
      "eslint",
      "git add ."
    ]
  }
}

2.2 Hook 文件位置

实际的 Git hook 文件位于:

  • .git/hooks/pre-commit
  • .git/hooks/prepare-commit-msg

2.3 Yorkie Runner

yorkie 通过 node_modules/yorkie/src/runner.js 执行配置的命令:

const fs = require('fs')
const path = require('path')
const execa = require('execa')

const cwd = process.cwd()
const pkg = fs.readFileSync(path.join(cwd, 'package.json'))
const hooks = JSON.parse(pkg).gitHooks

const hook = process.argv[2]
const command = hooks[hook]

console.log(` > running ${hook} hook: ${command}`)
try {
  execa.shellSync(command, { stdio: 'inherit' })
} catch (e) {
  process.exit(1)
}

2.4 eslint配置示例

ESLint 配置位于 .eslintrc.js 文件中:

module.exports = {
  // ... 其他配置
  rules: {
    // 不允许出现未使用变量
    'no-unused-vars': 'error',
    // 其他规则...
  }
};

会被检测到的错误:

// ❌ 错误:声明了但未使用
const unusedVariable = 'hello';
const anotherVar = 123;

// ❌ 错误:函数参数未使用
function unusedParam(param1, param2) {
  console.log('hello');
  // param1 和 param2 都没有使用
}

2.5 提交信息标准格式

项目要求提交信息必须符合以下格式:

type(可选 scope): 本次代码做了哪些事情,前面要有一个空格

https://project.feishu.cn/xxx

支持的提交类型

  • feature - 新功能
  • bugfix - 修复bug
  • task - 任务
  • docs - 文档
  • style - 样式
  • refactor - 重构
  • performance - 性能优化
  • test - 测试
  • chore - 杂务
  • revert - 回滚
  • config - 配置
  • fix - 修复
  • feat - 功能

3. 提交信息格式检查

3.1 提交代码检查

通过"pre-commit": "lint-staged"自动检测提交的代码

3.2 提交信息检查

3.2.1 检查文件

提交信息检查由以下文件实现:

  • scripts/git-hooks/prepare-commit-msg.mjs - 主检查文件
  • scripts/git-hooks/messge-checker.mjs - 检查逻辑实现

3.2.2 检查过程

messge-checker.mjs

import Readline from 'n-readlines';

const rules = [
  {
    example: 'type(可选 scope): 本次代码做了哪些事情,前面要有一个空格',
    checker(line) {
      const commitRE =
        /^(feature|bugfix|task|docs|style|refactor|performance|test|chore|revert|config|fix|feat)(\(.+\))?(:) .*$/;
      return commitRE.test(line);
    },
    special(line) {
      const commitRE = /^([A-Za-z]+)(\(utils\)) .*$/;
      return commitRE.test(line);
    }
  },
  {
    example: '必须是空行',
    checker: function (line) {
      return line.trim().length === 0;
    }
  },
  {
    example: 'https://project.feishu.cn/brm/story/detail/15260602',
    checker: function (line) {
      const commitRE = /^https:\/\/project.feishu.cn\//;
      return commitRE.test(line) || line.includes('.feishu.cn/base/');
    }
  }
];

function commitMessageChecker(fileName, debug) {
  const liner = new Readline(fileName);

  let i = 0;
  let isSpecial = false;
  let line;
  const ret = {
    code: 0,
    msg: []
  };
  debug && console.log('--------------------------------------------------------------');
  while ((line = liner.next())) {
    // docs 类型的提交,只需要校验第一行
    if (
      `${line}`.startsWith('docs') ||
      `${line}`.startsWith('fix') ||
      `${line}`.startsWith('feat') ||
      `${line}`.startsWith('config')
    ) {
      if (rules[i] && !rules[i].checker(`${line}`)) {
        ret.code = -1;
        ret.msg.push(`第${i + 1}行格式不对,规范为:${rules[i].example}`);
      }
      return ret;
    }

    // 忽略注释行
    if (`${line}`.startsWith('#')) {
      continue;
    }

    debug && console.log(`${line}`);

    if (rules[i] && !rules[i].checker(`${line}`)) {
      ret.code = -1;
      ret.msg.push(`第${i + 1}行格式不对,规范为:${rules[i].example}`);
    }

    // 特殊的提交,只需要校验第一行
    if (rules[i] && rules[i].special && rules[i].special(`${line}`)) {
      isSpecial = true;
      break;
    }

    i++;
  }
  debug && console.log('--------------------------------------------------------------');

  // console.log(`isSpecial = ${isSpecial}, i = ${i}`)
  // 不是utils域,就必须要3行
  if (!isSpecial && i !== rules.length) {
    ret.code = -1;
    ret.msg.push(`commit message必须为${rules.length}行,而本次有${i}行`);
  }

  return ret;
}

export default commitMessageChecker;

// 另外一个实现切分文件多行的方法,这里没有采用主要是担心兼容性不够
/*
const data = fs.readFileSync('file.txt', 'UTF-8');
const lines = data.split(/\r?\n/);
lines.forEach((line) => {
    console.log(line);
});
*/

prepare-commit-msg

import chalk from 'chalk';
import checker from './messge-checker.mjs';

const msgPath = './.git/COMMIT_EDITMSG';

const ret = checker(msgPath, true);

if (ret.code !== 0) {
  console.error(
    `  ${chalk.bgRed.white(' ERROR ')} ${chalk.red('commit message 格式错误,信息如下:')}\n\n${ret.msg
      .map((m) => {
        return `  ${chalk.green(m)}`;
      })
      .join('\n\n')}\n\n${chalk.red('  详细规范 README.md #commit message 规范')}`
  );

  process.exit(1);
}

4. 检测触发流程

4.1 完整执行流程

git commit
    ↓
┌─────────────────────────────────────┐
│  .git/hooks/pre-commit              │
│  ↓                                  │
│  yorkie runner                      │
│  ↓                                  │
│  lint-staged                        │
│  ↓                                  │
│  prettier --write [files]           │
│  ↓                                  │
│  eslint [files] ← 这里检测未使用变量 │
│  ↓                                  │
│  git add .                          │
└─────────────────────────────────────┘
    ↓
┌─────────────────────────────────────┐
│  .git/hooks/prepare-commit-msg      │
│  ↓                                  │
│  yorkie runner                      │
│  ↓                                  │
│  prepare-commit-msg.mjs             │
│  ↓                                  │
│  messge-checker.mjs ← 检查提交信息   │
└─────────────────────────────────────┘
    ↓
┌─────────────────────────────────────┐
│  提交完成                           │
└─────────────────────────────────────┘

5. 错误处理机制

5.1 ESLint 错误处理

当 ESLint 检测到错误时:

src/pages/header/guideDialogs/audioTranscriptionModal.vue
  231:8  error  'SearchSelect' is defined but never used  no-unused-vars
  373:7  error  'searchCompanyByKeyword' is assigned a value but never used  no-unused-vars

✖ 2 problems (2 errors, 0 warnings)
  • 显示具体的错误位置和类型
  • 返回非零退出码
  • 阻止提交继续执行

5.2 提交信息错误处理

当提交信息格式错误时:

 ERROR  commit message 格式错误,信息如下:

  第1行格式不对,规范为:type(可选 scope): 本次代码做了哪些事情,前面要有一个空格

  详细规范 README.md #commit message 规范
  • 使用 chalk 库美化错误信息
  • 显示具体的错误位置和规范要求
  • 调用 process.exit(1) 强制终止

Git 提交规范与 Git Flow 最佳实践分享

作者 tangzzzfan
2025年9月29日 23:34

在日常的协作开发中,Git作为我们最重要的代码版本控制工具,扮演着核心角色。然而,我们发现团队在使用Git时,出现了一些不规范的操作,例如提交信息不清晰、提交粒度过大、主线分支存在无效提交等。这些问题不仅影响了代码的可追溯性和可维护性,也增加了代码审查的难度,降低了团队的协作效率。

本次分享旨在为大家提供一份详细的Git提交规范与Git Flow最佳实践指南。Git Flow 每个团队可能都有自己的规范,这次分享只讲如何通过规范我们的操作,让每一次提交都有迹可循,让每一次代码合并都清晰明了,从而提升开发效率,保障代码质量。

一、Git Commit 消息规范

良好的Commit消息是项目文档的一部分,它能帮助我们快速理解每次提交的目的、内容和影响。

1.1 常见问题与危害

  • 问题1: 提交消息简单,如 "update", "fix", "no update" 等。
    • 危害: 无法快速了解本次提交的具体内容和目的;导致代码回溯困难;影响问题排查效率。

1.2 规范准则

我们建议采用 Angular 团队的 Commit Message 约定,它结构化且清晰明了。

提交消息格式:

<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
  • type (必需): 本次提交的类型,用于说明提交的性质。
    • feat: 新增功能 (feature)
    • fix: 修复 bug
    • docs: 文档变更 (documentation)
    • style: 代码格式或风格调整 (不影响代码运行的变动,如空格、分号等)
    • refactor: 代码重构 (既不是新增功能也不是修复 bug 的代码变动)
    • perf: 性能优化 (performance)
    • test: 增加或修改测试
    • build: 构建过程或辅助工具的变动 (如 CI/CD 配置、依赖更新)
    • ci: CI/CD 相关的改动
    • chore: 其他不属于以上类型的提交 (如构建过程、辅助工具的变动)
    • revert: 撤销之前的提交
  • scope (可选): 本次提交影响的范围,例如 users, auth, component-name, api 等。这有助于更精细地定位改动。
  • subject (必需): 提交的简短描述,不超过 50 个字符。
    • 使用现在时、祈使句,例如 "fix: add user validation" 而不是 "fix: added user validation"。
    • 首字母小写,句末不加句号。
  • body (可选): 详细描述本次提交,解释为什么进行这项改动,以及解决了什么问题,或实现了什么功能。可以分点说明。
  • footer (可选): 放置一些元数据,例如关联的 Issue ID,或 Breaking Changes 信息。
    • 例如: Closes #123, Refs #456, BREAKING CHANGE: ...

1.3 示例

feat(user): add new user registration API

This commit introduces a new API endpoint for user registration.
It includes:
- Validation for email and password
- Hashing of passwords
- User data persistence to database

Closes #789
fix(auth): correct token refresh logic

Previously, the token refresh mechanism was failing under high load
due to a race condition. This fix ensures that only one refresh
request is active at a time for a given user session.

1.4 这样做的好处

  • 可读性: 快速理解每次提交的目的,无需深入代码。
  • 可追溯性: 方便回溯代码历史,定位问题和查找功能实现。
  • 自动化: 可以基于 Commit Type 自动生成 CHANGELOG,或者触发 CI/CD 流程。
  • 团队协作: 统一的规范减少沟通成本,提高团队协作效率。

二、Git Flow 工作流管理

Git Flow是一种成熟且广泛使用的分支管理模型,它定义了明确的分支角色和合并策略,有助于大型团队和复杂项目的协作。

2.1 常见问题与危害

  • 问题2: 提交的代码一次性很多,让 review 的人很麻烦。
    • 危害: 代码审查效率低下;隐藏潜在Bug;难以理解改动的上下文;回滚风险高。
  • 问题3: 在 main/develop 的主线分支上,有很多无效提交信息节点, 比如 merge xx into develop。
    • 危害: 污染主线分支历史;难以通过主线历史了解项目演进;难以进行有效的代码回溯。

2.2 Git Flow 核心分支

Git Flow主要包含以下几类分支:

  • main (或 master) 分支: 生产环境代码,保持永远可发布状态。只接受 release 分支或 hotfix 分支的合并。
  • develop 分支: 预发布环境代码,包含所有已开发完成并通过测试的功能,是集成各功能的主分支。
  • feature 分支: 功能开发分支,从 develop 分支创建,用于开发新功能。完成后合并回 develop
  • release 分支: 发布分支,从 develop 分支创建,用于准备发布新版本。在此分支上进行 Bug 修复、版本号升级等操作。完成后合并到 maindevelop
  • hotfix 分支: 热修复分支,从 main 分支创建,用于紧急修复生产环境的Bug。完成后合并到 maindevelop

2.3 最佳实践规范

  1. 提交粒度小而精 - 如何界定提交,确保每次 commit 是独立或有效的:

    • 核心原则: 每次提交只做一件事,并且这件事是完整的、可测试的、有意义的。 一个提交应该是一个原子操作,它引入的改动不应该依赖于后续的提交才能正常工作。
    • 操作指南:
      • 逻辑拆分: 将一个大的功能点拆解成多个小的、独立的逻辑单元进行提交。例如:
        • 数据模型修改 (feat(model): add user profile fields)
        • API 接口定义 (feat(api): define user profile endpoints)
        • 业务逻辑实现 (feat(service): implement user profile update logic)
        • UI 界面展示 (feat(ui): display user profile screen)
        • Bug 修复 (fix(login): correct password validation)
        • 优化 (perf(image): optimize image loading)
      • 避免半成品提交: 不提交那些无法编译、无法运行、功能不完整的代码。即使是小的修改,也应确保提交后代码库处于可用状态。
      • 上下文一致性: 一个提交中的所有改动都应围绕一个主题。例如,不要在一个提交中同时修改用户模块和订单模块。
      • Reviewer 视角: 在提交前,想象一下你的 Reviewer 看到这个提交时,是否能轻松理解你做了什么、为什么这么做,以及这些改动的影响。如果一个提交需要 Reviewer 花大量时间去梳理,那它可能太大了。
    • 解决问题2: 确保每次提交都是一个经过深思熟虑、逻辑独立的单元,极大地提高了代码审查的效率和质量,降低了 Bug 引入的风险。
    • 好处:
      • 高效 Review: Reviewer 可以专注于一个特定改动点,快速理解并给出反馈。
      • 精确定位问题: 当出现问题时,可以通过 git blamegit bisect 精确到引入问题的具体提交,而不是一大坨改动。
      • 轻松回滚/Cherry-Pick: 如果某个功能有问题,可以轻松地回滚某个提交,或者将某个提交 Cherry-Pick 到其他分支。
      • 方便单元测试: 小粒度提交意味着每个提交都可能对应一个功能点或修复,更容易编写和运行单元测试来验证其正确性。
      • 个人测试保障 (重点): 在提交代码给别人之前,小粒度的提交能极大地增加你进行充分自测的可能性。 当你只改动一小部分代码并提交时,你更有能力全面地验证这部分改动的功能是否正常,是否引入了新的问题,甚至可以为这部分改动编写简单的单元测试。这确保了你在将代码提交给 Reviewer 时,已经进行了初步的验证,极大地提升了你作为开发者的“靠谱性”,减少了因提交未验证代码而引发的返工和延误。
  2. 主线分支的提交信息原则:

    • 原则: maindevelop 分支上的提交历史应该是清晰、有意义的,能够反映项目的重要里程碑和功能演进。我们应避免将零碎的、中间过程的提交直接暴露在主线分支上。
    • 解决问题3: 避免主线分支出现 merge xx into develop 这类冗余信息。
    • 操作:
      • 使用 git merge --no-ff: 在将 featurehotfixrelease 分支合并到 developmain 时,始终使用 --no-ff (no-fast-forward) 选项。这会强制创建一个合并提交,保留分支合并的历史记录,而不是简单地将分支指针向前移动。
      • 使用 git rebase (谨慎): 在将 feature 分支合并回 develop 之前,可以使用 git rebase develop 来将 feature 分支的提交“整理”到 develop 分支的最新提交之后,保持提交历史的线性。这会改写历史,因此只在本地未推送的私有分支上使用。一旦推送到远程,切勿 rebase 已共享的分支。
      • Squash 提交: 当一个 feature 分支上有多个零碎的提交,但这些提交共同实现一个完整功能时,可以在合并到 develop 之前,使用 git rebase -i 将这些提交 squash (合并) 成一个或几个逻辑清晰的提交。
      • 合并提交消息: 合并回主线分支时,确保合并提交的消息清晰地描述了合并的内容,例如 "feat(login): complete user login module"。
    • 好处: 保持主线分支历史的整洁和可读性;方便通过主线提交历史快速了解项目功能演进;易于版本管理和发布。

2.4 这样做的好处

  • 结构清晰: 明确的分支职责,使得项目结构一目了然。
  • 并行开发: 允许多个功能并行开发,互不干扰。
  • 版本控制: 有序的版本发布流程,降低发布风险。
  • 故障隔离: 热修复流程能快速响应生产环境问题,且不影响正在进行的开发。
  • 历史整洁: 规范合并策略,保持主线分支历史的干净和有意义。

三、实际案例:SPM 组件化开发中的多仓库改动

在 SPM (Swift Package Manager) 组件化开发中,一个业务功能的实现往往涉及多个独立的 SPM 包 (即多个 Git 仓库)。例如,实现一个“用户注册”功能,可能需要改动以下几个仓库:

  1. NetworkingKit (网络层): 增加注册 API 请求和响应模型。
  2. AuthCore (认证核心): 增加用户注册的业务逻辑处理,如密码加密、Token 生成等。
  3. UserUI (用户界面): 增加注册界面和交互逻辑。
  4. AppMain (主应用): 集成 UserUI,并调用 AuthCore 进行注册。

假设我们要实现“用户注册”功能,这是一个相对复杂的功能,我们可以将其拆解为多个独立的、有意义的提交。

3.1 提交粒度拆解与规范化提交示例

目标功能: 实现用户注册,包括邮箱和密码。

开发分支: 我们在 AppMain 仓库中从 develop 拉取 feature/user-registration 分支。同时,在 NetworkingKit, AuthCore, UserUI 仓库中也分别创建对应的 feature/user-registration 分支。

提交流程 (以 AppMain 中的 feature/user-registration 为主线,其他组件库同步进行):

Step 1: NetworkingKit 仓库修改 - 定义注册 API 请求/响应

  • 修改内容:NetworkingKit 中增加 RegisterRequestRegisterResponse 结构体,并添加相应的 Endpoint 定义。
  • NetworkingKit 仓库提交:
    • 命令行:
      # 确保你在 NetworkingKit 仓库的 feature/user-registration 分支
      git add Sources/NetworkingKit/API/Auth/RegisterEndpoint.swift
      git add Sources/NetworkingKit/Models/Auth/RegisterModels.swift
      git commit -m "feat(networking): add user registration API models and endpoint" -m "This commit defines the request and response models for user registration, along with its API endpoint definition."
      # 推送到远程分支
      git push origin feature/user-registration
      
    • SourceTree:
      1. 在 SourceTree 中,切换到 NetworkingKit 仓库的 feature/user-registration 分支。

      2. 在“文件状态”区域,选择 Sources/NetworkingKit/API/Auth/RegisterEndpoint.swiftSources/NetworkingKit/Models/Auth/RegisterModels.swift

      3. 点击“暂存选中项”或“暂存全部”。

      4. 在“提交信息”文本框中输入:

        feat(networking): add user registration API models and endpoint
        
        This commit defines the request and response models for user registration, along with its API endpoint definition.
        

        (通常 SourceTree 会有一个较小的文本框用于 subject,下方有较大的文本框用于 body,请分开填写)

      5. 点击“提交”按钮。

      6. 点击“推送”按钮,将分支推送到远程。 `.

        注意: SourceTree 通常不会有 pre-commit 钩子直接的视觉反馈,但如果你的项目配置了 Git Hook 脚本,它会在你点击“提交”时自动运行检查,如果失败会弹窗提示。

Step 2: AuthCore 仓库修改 - 实现注册业务逻辑

  • 修改内容:AuthCore 中实现调用 NetworkingKit 的注册 API,并处理响应,包括密码加密、Token 存储等。
  • AuthCore 仓库提交:
    • 命令行:
      # 确保你在 AuthCore 仓库的 feature/user-registration 分支
      # (可能需要先更新 NetworkingKit 依赖到最新,以便使用新定义的API)
      git add Sources/AuthCore/Services/AuthService.swift
      git add Sources/AuthCore/Models/AuthToken.swift
      git commit -m "feat(auth): implement user registration business logic" -m "This commit adds the core business logic for user registration, including password hashing, API call integration with NetworkingKit, and JWT token handling."
      git push origin feature/user-registration
      
    • SourceTree: 类似上述步骤,在 AuthCore 仓库中进行操作。

Step 3: UserUI 仓库修改 - 实现注册界面

  • 修改内容:UserUI 中创建注册视图控制器,包含邮箱和密码输入框,以及注册按钮,并连接 AuthCore 进行注册。
  • UserUI 仓库提交:
    • 命令行:
      # 确保你在 UserUI 仓库的 feature/user-registration 分支
      # (可能需要先更新 AuthCore 依赖到最新)
      git add Sources/UserUI/Registration/RegistrationViewController.swift
      git add Sources/UserUI/Registration/RegistrationViewModel.swift
      git commit -m "feat(user-ui): create user registration screen" -m "This commit adds the UI components and view model for the user registration screen, integrating with AuthCore for submission."
      git push origin feature/user-registration
      
    • SourceTree: 类似上述步骤,在 UserUI 仓库中进行操作。

Step 4: AppMain 仓库修改 - 集成注册功能

  • 修改内容:AppMain 中将 UserUI 的注册界面集成到应用导航流程中。
  • AppMain 仓库提交:
    • 命令行:
      # 确保你在 AppMain 仓库的 feature/user-registration 分支
      # (此时,你需要确保所有 SPM 依赖都已更新到各自 feature/user-registration 分支的最新版本)
      git add Sources/AppMain/AppCoordinator.swift
      git commit -m "feat(app): integrate user registration flow" -m "This commit integrates the new user registration UI from UserUI into the main application navigation flow."
      git push origin feature/user-registration
      
    • SourceTree: 类似上述步骤,在 AppMain 仓库中进行操作。

3.2 案例总结与思考

这个案例演示了在一个跨多个 SPM 包的业务功能开发中,如何将一个大的功能拆分成多个小而独立的提交。

  • 独立的 Commit: 每个 Commit 都完成了各自仓库中一个独立的、可验证的逻辑单元。例如,在 NetworkingKit 中提交后,NetworkingKit 本身仍然是一个功能完整且可用的包。
  • 清晰的 Commit Message: 每个 Commit 都遵循了规范,清晰地说明了 type, scope, subject,并且在 body 中提供了详细的描述。
  • 个人测试:NetworkingKit 提交后,开发者可以立即运行 NetworkingKit 的单元测试或进行简单的 API 调用测试,确保新增的 API 模型和 Endpoint 是正确的。同样,在 AuthCore 提交后,可以运行 AuthCore 的单元测试来验证业务逻辑。这种“提交即验证”的模式,大大提升了提交代码的可靠性。
  • 跨仓库协作: 虽然涉及到多个仓库,但清晰的提交粒度和规范,使得每个仓库的改动都能独立审查和验证,最终在 AppMain 仓库中完成集成。

四、线下规范操作验证方法与工具

为了帮助大家更好地实践这些规范,我提出以下线下验证方法和工具:

4.1 定义 Commit 消息检查工具 (Swift 项目适用)

虽然像 commitlint 这样的工具通常基于 JavaScript 生态,但我们可以在 Swift 项目中实现类似的 Commit 消息规范检查。

  1. Git Hooks (自定义脚本): Git 提供了 pre-commitcommit-msg 等钩子。你可以编写 Swift 脚本或 Shell 脚本,将其放置在 .git/hooks/ 目录下(并移除 .sample 后缀,确保可执行权限),以在提交前自动检查 Commit 消息是否符合规范。
    • commit-msg Hook: 这个钩子最适合用于检查 Commit 消息本身。当执行 git commit 时,Git 会将 Commit 消息的临时文件路径作为参数传递给 commit-msg 脚本。你可以在脚本中读取这个文件,然后使用正则表达式或其他逻辑来验证 Commit 消息的格式。如果验证失败,脚本以非零退出码退出,Commit 就会被阻止。
    • 示例 commit-msg 脚本:
      #!/bin/sh
      # .git/hooks/commit-msg
      
      COMMIT_MSG_FILE=$1
      COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
      
      # 简单的正则检查,确保以 type(scope): subject 格式开头
      if ! echo "$COMMIT_MSG" | grep -Eq "^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?: .{1,50}"; then
        echo "Error: Commit message does not follow the conventional commit format."
        echo "Please use format like: <type>(<scope>): <subject>"
        echo "Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert"
        exit 1
      fi
      
      # 可以添加更多复杂的 Swift 脚本逻辑
      # 例如:
      # swift run MyCommitChecker "$COMMIT_MSG_FILE"
      
  2. CI/CD 阶段检查: 在你的 CI/CD 流程中增加一个步骤,专门用于检查提交的 Commit 消息。即使本地没有强制检查,CI 也能捕获不规范的提交,从而避免其合并到主线分支。这是一种更强力的保障机制。

4.2 实践演练

  1. 创建一个沙盒项目: 建立一个独立的 Git 仓库,作为大家的练习场。
  2. 模拟开发流程: 在沙盒项目中,大家可以尝试:
    • 创建 feature 分支,开发并进行多次小粒度提交。
    • 按照规范撰写 Commit 消息,体验 Git Hooks 或 CI/CD 检查的校验。
    • 尝试合并 feature 分支到 develop 分支,使用 git merge --no-ff
    • 模拟 Bug 修复,创建 hotfix 分支,合并到 maindevelop
    • 体验 git rebase -isquash 提交 (注意只在私有分支使用)。
    • 刻意制造冲突并解决:在沙盒项目中,让两个人修改同一个文件的同一行代码,制造冲突。然后尝试解决冲突,体验命令行或 SourceTree 的冲突解决流程,并确保所有冲突标记都被正确移除。

4.3 团队 Code Review

  • 将 Commit 消息规范和提交粒度作为 Code Review 的重要内容。
  • Reviewer 在审查代码的同时,也要审查 Commit 消息和提交历史。
  • 通过相互监督和帮助,共同提升团队的 Git 使用水平。

五、总结与展望

规范的 Git 提交与工作流管理,并非为了增加我们的工作负担,而是为了让开发过程更加高效、透明和可控。它能帮助我们:

  • 提升代码质量: 清晰的提交历史和精细的提交粒度,有助于发现和修复问题。
  • 降低维护成本: 快速理解代码变更,简化代码回溯和问题排查。
  • 增强团队协作: 统一的规范减少沟通障碍,提高团队整体效率。
  • 促进个人成长: 养成良好的提交习惯,提升专业素养。

我鼓励大家从现在开始,在日常工作中积极实践这些规范。初期可能会有一些不适应,但随着时间的推移,你会发现这些努力是值得的。

❌
❌