素材 - 浅尝 menorepo 多包架构 - pnpm workspace+vite+vue3+ts

3,505 阅读11分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情

前言

最近刚好接触了一个 yarn workspace 的项目,在学习了解该项目的同时又了解到了 pnpm,pnpm 号称是最好用的 npm 工具,而且也支持 workspace 模式的开发。

众所周知现在 menorepo 已经成为近两年以来最热的词汇之一了,趁着这个机会就搭建了一个这样的项目框架,可以在以后开发新项目的时候直接在新的项目中使用。同时在拆包的时候,可以把一些已经积累在一起的组件和工具进行累积,使每个子包都可以健壮起来。

这个项目结构会满足以下几个功能:

  1. 至少包含4个子包,4个子包分别是:main/utils/components/cli
  2. main 中存放项目主体,只涉及项目业务
  3. utils 中存放项目中使用到的 js 工具类,供 main 使用
  4. components 中存放可抽离为通用解决方案的组件,供 main 使用
  5. cli 是自己写的脚手架,支持使用命令行生成页面并配置 router/menu/store 或 生成 components。
  6. utils 和 components 两个子包中的内容尽量避免 反向引用 main 中的内容,充分解耦,功能更加单一。
  7. 对于 是否应该把 utils 和 components 发布出去这个问题,我的想法是在 cli 中执行 git 命令,还是使用这种架构方式,方便对组件和工具的可持续化集成,并在集成过后提交到 git 仓库。所以这个仓库就必须要有完善的管理机制。避免频繁的改动或者破坏性更新。
  8. 整个项目要支持 eslint 校验规范以及遵循 git commitMsg 提交规范。
  9. components 应该提供一份文档出来,技术选项为 vitepress。

开发过程

一、搭建 pnpm workspace 项目结构

安装 pnpm

npm i -g pnpm

下面有 pnpm 的中文文档地址。

配置 workspace

创建配置文件 pnpm-workspace.yaml

packages:
  - 'packages/**'

注意:第二行 - 后面跟着一个空格,因为这个空格,我搞了两天~~~

二、第一个子包: 创建 vite+vue3+ts 项目

1. 创建应用并安装依赖

# 使用 vite 创建项目
$ pnpm create vite main

# 安装依赖 
$ pnpm install

因为后续还要将 components 进行分包,所以共用一套 vue 环境,所以我把依赖放在了主包上,但是不知道为什么好像并没有生效. . .
将 main 中的 package.json 中的 依赖都复制到 跟目录中的 package.json 中

2. 启动应用

在根目录下的 package.json 中配置启动脚本,并执行:

// package.json
{
    ...
    "scripts": {
        "dev:main": "pnpm -r --filter @motorepo/main run dev",
        "build:main": "pnpm -r --filter @motorepo/main run build"
    },
    ...
}

// 本地开发:在根目录下执行
$ pnpm run dev:main

// 打包:在根目录下执行
$ pnpm run build:main

启动成功之后:

image.png

按照提示,浏览器中访问地址 http://127.0.0.1:5173/ ,查看项目即可。默认项目如下:

image.png

解决 ts 提示报错的问题

现在效果是可以看到了,但是打开代码以后,会发现代码上会被 ts 有两个错误提示:

  1. App.vue 中 html 部分标签会被提示

【JSX 元素隐式具有类型 “any“,因为不存在接口 “JSX.IntrinsicElements“】

但是后来把配置改回去没有复现,但是当时确实解决了:

a. 不使用严格的类型检查,即在 tsconfig.json 中设置 “strict”: false

    {
      "compilerOptions": {
        "strict": false
      }
    }

b. 在 tsconfig.json中设置 “noImplicitThis”: false

    {
      "compilerOptions": {
        "noImplicitAny": false, // 是否在表达式和声明上有隐含的any类型时报错
      }
    }
  1. HelloWorld.vue 中,引用变量会被提示

类型“{}”上不存在属性“count”

解决方案为在 main 的根目录下创建如下文件

/**
 * vue-file-import.d.ts 文件
 * 添加此文件用来解决ts提示 【类型“{}”上不存在属性“count”】 的问题
 */
declare module"*.vue" {
  import Vue from"vue";
  export default Vue;
}

3. 配置 router

安装依赖 vue-router

$ pnpm add vue-router -filter @motorepo/main

配置过程分为三步:

  1. 编辑一个 页面 (index.vue)
// /pages/main/index.vue 

<template>
  <div>index.vue</div>
</template>
  1. 编辑 router/index.ts
// /router/index.ts

import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'indexLayout',
    component: () => import('../pages/main/index.vue'), // 懒加载组件
  },
]

const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

export default router;
  1. 在 main.ts 中挂载 router
// main.ts

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

// 引入 router
import router from './router'

createApp(App).use(router).mount('#app');

这里只是把项目跑起来,并没有对 router 进行下一步的优化,后续的修改方向:
项目在后续的开发过程中肯定是按模块进行开发,后续 router 中使用 require 的方式遍历模块下的 router 文件,实现自动挂载模块 router。
这样的话在大团队中,每个人负责一个模块,各自维护自己模块的 router ,避免改动 根目录下的 router。
同样,store 也可以这么做。

4. 使用 pinia 作为状态管理,并实现数据持久化

安装依赖:

$ pnpm add pinia -filter @motorepo/main

配置过程也是三步:

  1. 编辑 pinia install 文件
// store/install.ts

// 引入 pinia
import { createPinia } from 'pinia';

// 持久化工具
// import piniaPluginPersist from 'pinia-plugin-persist';

// 实例化
const store = createPinia();

// 使用持久化工具
// 还需要再对应的 store 中开启 persist 字段
// store.use(piniaPluginPersist);

// 导出 store
export default store;

// #  注意:在其他模块中使用store的话,
// #  必须引入这个实例,
// #  不然无论如何都会失去响应式,
// #  而且和挂载在App上的store不是同一个
  1. 编辑 main 模块
// store/index.ts

import { defineStore } from 'pinia';
import pinia from '@/store/install';

const useMainStore = defineStore('main', {
  state: () => ({
    loading: false,
    token: '',
    currentScale: 0,
    userInfo: {
      username: '', // 用户名
      realname: '', // 真实姓名
      sex: '', // 性别
      phone: '', // 电话
      email: '', // 邮箱
      birth: '', // 出生日期
      avantor: '', // 头像
    },
  }),
  getters: {
    getUsername: (state) => state.userInfo?.username,
    getToken: (state) => state.token,
    getCurrentScale: (state) => state.currentScale,
  },
  // persist: {
  //   enabled: true,
  //   strategies: [
  //     {
  //       storage: localStorage,
  //     },
  //   ],
  // },
});

// 数据持久化
// 1. 保存数据
const instance = useMainStore(pinia);
instance.$subscribe((_, state) => {
  localStorage.setItem(
    'login-store',
    JSON.stringify({
      ...state,
    }),
  );
});
// 2. 获取保存的数据,先判断有无,无则用先前的
const old = localStorage.getItem('login-store');
if (old) {
  instance.$state = JSON.parse(old);
}

export { useMainStore };
  1. 在 main.ts 上挂载 pinia
// main.ts

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

// 引入 router
import router from './router'

// 引入 pinia
import pinia from '@/store/install';

createApp(App).use(router).use(pinia).mount('#app');
  1. 使用方法
import { ref } from 'vue'
import { useMainStore } from '../../store'
const mainStore = useMainStore()

// 获取
const username = ref(mainStore.userInfo.username)

// 更新
setTimeout(() => {
  mainStore.$patch({
    token: null,
    userInfo: { username: '中国人' },
  })
}, 1000)

5. 为路径设置别名 @

修改 vite.config.ts,添加如下配置:

// vite.config.ts
import { resolve } from 'path';

resolve: {
    alias: {
      '@': resolve(__dirname, 'src'), // 设置 `@` 指向 `src` 目录
    },
  },

添加完这个配置后ts会报错提示,path 找不到的问题
解决方案分为两步

  1. 安装 @type/node 依赖: pnpm add @types/node -filter @motorepo/main
  2. 修改 tsconfig.json ,在 compilerOptions 字段中增加配置: "types": ["vite/client", "node"]

除了上面的修改之外,tsconfig.ts中也需要配置 @ 别名。最终配置如下:

// tsconfig.ts
{
  "compilerOptions": {
    "baseUrl": "./",
    "target": "ESNext",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "strict": false,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "lib": ["ESNext", "DOM"],
    "skipLibCheck": true,
    "types": ["vite/client", "node"],
    "paths": {
      "@/*": ["src/*"],
    },
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

变动主要有三个字段:
baseUrl:在设置 paths 字段的时候如果使用相对路径,则必须先配置 baseUrl 才行
types:上面解决引入 path 模块的时候增加的
paths: 具体配置

6. 配置 sass

其实 vite 中已经预置了 sass 解析器,所以只需要安装 sass 即可使用。

# 为 main 安装 sass
$ pnpm add sass -filter @motorepo/main

使用方法呢,直接在刚才的 index.vue 中使用就好了。

7. 安装 element-plus 并配置全局变量

element-plus 是专门为 vue3 做的 UI 组件库。

默认安装方法固然简单,但是真实的开发场景下,全局的 css 样式会包含:组件库css、全局默认css、iconfont、全局 scss 变量。可能还有其他的,如动画库等。因为是搭个架子所以先不放动画库了。

为 main 安装 element-plus 依赖。

$ pnpm add element-plus -filter @motorepo/main

main.ts 中安装 element-plus 并配置中文包

// 引入 elementPlus
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'

createApp(App)
  .use(ElementPlus, {
    locale: zhCn,
  })
  .use(router)
  .use(pinia)
  .mount('#app')

将所有的 css 样式整合到一个入口文件,然后统一挂载在 main.ts

// assets/styles/index.scss

@import './base';
@import './elment-reset';
// @import './iconfont/iconfont.css';

---
// assets/style/element-reset.scss  element样式并修改默认样式

@forward 'element-plus/theme-chalk/src/common/var.scss' with (
  $colors: (
    'white': #ffffff,
    'black': #000000,
    'primary': (
      // 'base': #409eff,
      'base': #333,
    ),
    'success': (
      'base': #67c23a,
    ),
    'warning': (
      'base': #e6a23c,
    ),
    'danger': (
      'base': #f56c6c,
    ),
    'error': (
      'base': #f56c6c,
    ),
    'info': (
      'base': #909399,
    ),
  )
);
@use 'element-plus/theme-chalk/src/index.scss' as *;

.el-button {
  border-radius: 0;
}

.el-menu--horizontal {
  border-bottom: none;
}

.el-dialog__footer,
.el-dialog__header {
  background: $white;
}

---
// assets/styles/base.scss

* {
  margin: 0;
  padding: 0;
}

html,
body,
#app {
  width: 100%;
  min-width: 1336px;
  height: 100%;
  background: $white;
  font-family: 'PingFang SC', '思源黑体CN', 'Mircrosoft YaHei', 'SF UI Text', Arial, sans-serif;
}

button,
input,
optgroup,
select,
textarea {
  font-family: 'PingFang SC', '思源黑体CN', 'Mircrosoft YaHei', 'SF UI Text', Arial, sans-serif;
}

// 列表
ul,
ol,
li {
  margin: 0;
  padding: 0;
  list-style: none;
}

/** 滚动条 */
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}

/** 滚动条轨道 */
::-webkit-scrollbar-track {
  background: none;
}

/** 滚动条上的滚动滑块 */
::-webkit-scrollbar-thumb {
  min-height: 28px;
  border-radius: 8px;
  background: rgba(0, 0, 0, 0.3);
  background-clip: padding-box;
}

/** 滚动条没有滑块的轨道部分 */
::-webkit-scrollbar-track-piece {
  opacity: 0;
}

::-webkit-scrollbar-thumb:hover {
  background: rgba(0, 0, 0, 0.4);
}

.fl {
  float: left;
}

.fr {
  float: right;
}

.ellips {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.page-main {
  background: #fff;
  min-height: calc(100% - 84px);
  padding: 24px;
}

// assets/styles/global-varible.scss
$headerHeight: 60px;
$footerHeight: 100px;
$mainWidth: 1336px;

$white: #eef7f2;
$blue: #3a89b0;
$black: #333;

如上四个 scss 文件,其中 index.scss 作为最终挂载文件,把其他的 scss 文件统一引入,最后挂载在 main.ts 中。挂载方法是在 main.ts 中加入如下代码:

// 引入所有的公共样式
import '@/assets/styles/index.scss';

global-varible.scss 则是全局变量,可见到里面设置了几个颜色和尺寸。需要挂载在 vite.config.ts 上。挂载方法是在 vite.config.ts 中增加如下代码:

 // 配置全局 scss 变量文件
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/assets/styles/global-variable.scss";`,
        charset: false,
      },
    },
  },

需要注意的是 配置全局变量的时候,用到了 路径别名 @,所以要先配置 路径别名。

至此,在 index.vue 中就可以使用 element 组件了。我写了几个 按钮:

image.png

可见,主题颜色已经变化,且修改后的 button 样式也已经生效。

三、第二个子包:第三方工具解耦独立配置安装

第二个子包我选择放置一些开发工具类的内容。
我将它命名为 utils,作为示例使用的话,封装了一个 el-message 。

开发过程很简单,使用有序列表简单描述一下,然后粘贴出示例代码:

  1. packages 中执行命令 mkdir utils && cd utils && npm init -y
  2. 修改 utils 包的 package.json 中的 name 字段,改为 @menorepo/utils
  3. 在 utils 包的根路径下创建 index.ts 作为导出所有工具的出口
  4. 在 utils 包中创建 src 路径,用来存放工具
  5. 在 src 中新建 message 路径,开发 message 工具,message 其实是基于 element-plus 的 ElMessage 和ElMessageBox 进行的简单封装。所以也要安装 element-plus。
  6. 为 main 包安装 utils 包 pnpm add @menorepo/utils -filter @menorepo/main
  7. 在 main 中的页面中使用 message

image.png

// message.ts

import { ElMessage, ElMessageBox } from 'element-plus';

const base = (msg: string) => ({
  duration: 4000,
  showClose: true,
  message: msg,
  dangerouslyUseHTMLString: true,
});

export default class Message {
  static error(msg: string) {
    return ElMessage({
      type: 'error',
      ...base(msg),
    });
  }

  static success(msg: string) {
    return ElMessage({
      type: 'success',
      ...base(msg),
    });
  }

  static warning(msg: string) {
    return ElMessage({
      type: 'warning',
      ...base(msg),
    });
  }

  static info(msg: string) {
    return ElMessage({
      type: 'info',
      ...base(msg),
    });
  }

  static confirm(msg: string, resovleFn: () => void) {
    return ElMessageBox.confirm(msg, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
      .then(() => {
        resovleFn();
      })
      .catch(() => Message.info('哦,原来是点错啦!'));
  }

  static delete(msg: string, resovleFn: () => void) {
    return ElMessageBox.confirm(msg, '删除', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
      .then(() => {
        resovleFn();
      })
      .catch(() => Message.info('哦,原来是点错啦!'));
  }
}

核心点是 第6点。

四、第三个子包:公共组件进行解耦

公共组件其实是一个可持续集成的一个子包,可以在合适的时候直接发布到 npm 上。供 menorepo 模式的工程使用。

开发流程很是简单粗暴:

  1. 创建一个子包 components 件并初始化:
$ cd packages && mkdir components && cd components && npm init -y

记得修改包名 : @menorepo/components

  1. 开发一个组件并导出

image.png

项目结构如上图所示。

源码粘一下:

// src/button/delete-button.vue

<template>
  <el-button type="danger"> 删除 </el-button>
</template>

<script lang="ts" setup>
// import {ElButton}

</script>

---
// src/button/index.ts 

export * from './delete-button.vue'
import type { App } from 'vue'
import DeleteButton from './delete-button.vue'

DeleteButton.install = (app: App) => {
  app.component('delete-botton', DeleteButton)
}

export { DeleteButton }
export default DeleteButton

---
// index.ts 

export * from './src/button'

---
// tsconfig.json

{
  "compilerOptions": {
    "jsx": "preserve", // 可解决 *.vue 组件第一行,ts 提示异常的问题。
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

  1. main 包中安装 子包 并开始消费组件

安装: $ pnpm add @menorepo/components -filter @menorepo/main

消费:(还是在 index.vue中测试)

// main/src/pages/index.vue

<template>
  <div class="page">
    <div>index.vue</div>
    <div>{{ username }}</div>
    <el-button type="primary">默认按钮</el-button>

    <!-- 消费 components 包中的组件 -->
    <p><DeleteButton /></p>  
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useMainStore } from '../../store'
import { DeleteButton } from '@menorepo/components' // 引入components 包中的组件
import { Message } from '@menorepo/utils'

const mainStore = useMainStore()

const username = ref(mainStore.userInfo.username)

Message.success('123123')

setTimeout(() => {
  mainStore.$patch({
    token: null,
    userInfo: { username: '中国人' },
  })
}, 1000)
</script>

<style lang="scss">
.page {
  background-color: pink;
}
</style>

至此,多包模式已经可以告一段落。

计划中的三个包已经完成,并可以互相调用。
这时候回头去和传统的单包开发模式进行对比,我发现了几点好处:

  • components 和 utils 两个包独立出来,可以独立发布,独立维护,大大的提高了复用性。
  • 项目模块清晰明了,main 包更专注于业务逻辑,而和业务耦合程度较低的内容都可以独立出去。
  • 一个仓库中可以存放多个项目,项目文档维护成本降低。
  • 其他的还没发现 . . .

五、第四个子包:脚手架工具的封装

这个脚手架的功能主要是有两个目的:

  • 项目初期,提高项目的搭建效率,使用命令行的方式,迅速根据产品将项目的所有页面搭建起来,开发人员的目光直指业务逻辑,不再会为了搭建开发环境浪费时间。
  • 保证整个项目的大结构统一,降低后来介入人员的学习成本。

总归一句话,解放双手,开心摸鱼。

1. 思路

2. 开发流程

六、代码规范及git规范的配置

1. eslint 安装及配置

# 根目录安装 eslint 
$ pnpm add eslint -D -w

# 初始化 eslint, 会根据配置生成 .eslintrc.js
$ pnpm eslint --init

You can also run this command directly using 'npm init @eslint/config'.
√ How would you like to use ESLint? · problems    
√ What type of modules does your project use? · esm
√ Which framework does your project use? · vue
√ Does your project use TypeScript? · No / Yes
√ Where does your code run? · browser, node
√ What format do you want your config file to be in? · JavaScript
The config that you've selected requires the following dependencies:

eslint-plugin-vue@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest
√ Would you like to install them now? · No / Yes
√ Which package manager do you want to use? · pnpm

在 package.json 中的 script 中增加命令:

#  eslint . 为指定lint当前项目中的文件 
# --ext 为指定 lint 哪些后缀的文件 
# --fix 开启自动修复

"lint": "eslint . --ext .vue,.js,.ts,.jsx,.tsx --fix"

然后执行 pnpm lint 就可以看到lint结果了。

在执行lint的时候,会发现少了三个依赖,会逐次提示,具体安装如下:

$ pnpm add eslint-plugin-vue@latest -D -w
$ pnpm add @typescript-eslint/eslint-plugin@latest -D -w
$ pnpm add @typescript-eslint/parser -D -w

果不其然,好几个问题:(其实总共有10个问题的,已经处理了3个,下面都会有解决记录)

  5:36  error    Don't use `{}` as a type. `{}` actually means "any non-nullish value".
- If you want a type meaning "any object", you probably want `Record<string, unknown>` instead.
- If you want a type meaning "any value", you probably want `unknown` instead.- If you want a type meaning "empty object", you probably want `Record<string, never>` instead  @typescript-eslint/ban-types
  5:40  error    Don't use `{}` as a type. `{}` actually means "any non-nullish value".
- If you want a type meaning "any object", you probably want `Record<string, unknown>` instead.
- If you want a type meaning "any value", you probably want `unknown` instead.- If you want a type meaning "empty object", you probably want `Record<string, never>` instead  @typescript-eslint/ban-types
  5:44  warning  Unexpected any. Specify a different type   @typescript-eslint/no-explicit-any

E:\@self\motorepo\packages\utils\src\http\http.ts
  63:39  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
  66:39  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
  69:40  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
  72:42  warning  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any

✖ 7 problems (2 errors, 5 warnings)
  1. 解析 vue 文件失败,报错为 提示 vue 文件第一个 尖括号 < 就报错了。

修改 .eslintrc.js 中。parser 字段改为: vue-eslint-parser 。

  1. 提示 vue 文件应该以驼峰方式命名 Component name "index" should always be multi-word

是因为 eslint 规定的,要检查组件名称,将这个规则改掉就好了。
修改 .eslintrc.js 中的 rule 字段,增加如下规则:

"vue/multi-word-component-names": [
    "error",
    {
      ignores: ["index"], //需要忽略的组件名
    },
  ],
  1. 提示 不要 以 {} 作为一个 类型

增加 rules 配置

"@typescript-eslint/ban-types": [
    "error",
    {
        "extendDefaults": true,
        "types": {
        "{}": false
        }
    }
]
  1. 提示 不要以 any 作为类型

了解过另外一个项目组,他们组的要求是不允许使用 any 作为类型推断使用,这个有点严苛了,在开发过程中难免会用到 any
所以我选择将其关掉了
增加 rules 配置

"@typescript-eslint/no-explicit-any": ["off"],

至此,执行 pnpm lint 已经没有报错了!

image.png

因为现在代码比较少,后面如果有新的报警或报错,可以根据提示内容,再进行配置 .eslintrc.js 这个配置文件或者改代码。

2. prettier 安装及配置

安装 prettier

$ pnpm add prettier -D -w

在根目录下创建 .prettier.js

module.exports = {
    // 一行的字符数,如果超过会进行换行,默认为80
    printWidth: 80, 
    // 一个tab代表几个空格数,默认为80
    tabWidth: 2, 
    // 是否使用tab进行缩进,默认为false,表示用空格进行缩减
    useTabs: false, 
    // 字符串是否使用单引号,默认为false,使用双引号
    singleQuote: true, 
    // 行位是否使用分号,默认为true
    semi: false, 
    // 是否使用尾逗号,有三个可选值"<none|es5|all>"
    trailingComma: "none", 
    // 对象大括号直接是否有空格,默认为true,效果:{ foo: bar }
    bracketSpacing: true
}

package.json中的script中添加以下命令

{
    "scripts": {
        "format": "prettier --write "./**/*.{html,vue,ts,js,json,md}"",
    }
}

执行 pnpm format 可看到 git 记录中有好多文件都发生了变化,虽然有些看起来没变,但实际上有变化,而且跟计划中的是一致的。

image.png

image.png

image.png

husky 配置 git hooks

基于新版本的 husky 进行配置的: husky ^7.0.1,我装的是当前最新版本 8.0.1。

安装 并 初始化 husky

# 安装
$ npm add husky -D -w

# 初始化 husky 目录
# 这里没有查询 pnpm 命令,因为只加了个命令,所以就用 npm 了,理论上,手动增加一个也行。
$ npm set-script prepare "husky install"

# 执行新增的命令
$ pnpm prepare

# 添加 pre-commit 钩子配置
$ npx husky add .husky/pre-commit "npx lint-staged"

commit 时校验代码,并 format 代码

不得不提一嘴,程序开发真的越了解越知其精湛。
我也是在配置结束以后才有这个感觉的,husky 其实是把 git 的几个阶段都进行了代理,使开发人员在执行 git 命令的时候走 husky 的逻辑,将命令劫持,先执行开发人员自定义的一些操作。
正因如此,才能实现现在的操作。
我尝试了大家常用的 lint-staged 的配置方案,但是在 lint-staged 中配置执行命令,感觉会把 package.json 和 husky 的配置耦合在一起,为了解耦我尝试了几个方法,最终使用了下面的方法配置成功。

可以看到,网上的一些配置方案中,大家都是选择在 packege 中增加一个 lint-staged 字段,并且在里面配置 eslint 和 prettier 的相关命令,然后 在 script 中再增加 命令 "lint": "lint-staged" ,再然后在husky中执行 npx lint-staged

说真的,绕来绕去把我都给绕懵了,不知道在干嘛!

我考虑了一下,既然在 husky 中执行了 lint-staged,对应的是 package.json 中的 script 指令,所以 理论上是可以把 上面已经测试过没问题的 lint 和 format 命令放在 pre-commit 脚本里面就好了:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# npx lint-staged
pnpm run lint && pnpm run format

如上所示,直接将 npx lint-staged 指令换成 pnpm run lint && pnpm run format
注意:只能这么写,顺序也不能乱,不然虽然控制台报错,依旧能commit成功,eslint 检查报错不会给 pre-commit 钩子捕获。


commitMsg 校验

  • 格式:git commit -m '类型: 描述性文字'

    类型概念
    build编译相关的修改,例如发布版本、对项目构建或者依赖的改动
    ci持续集成修改
    docs文档修改
    feat新特性、新功能
    fix修改bug
    perf优化相关,比如提升性能、体验
    refactor代码重构
    revert回滚到上一个版本
    style代码格式修改, 注意不是 css 修改
    test测试用例修改
    chore其他修改,比如改变构建流程、或者增加依赖库、工具等
  • 安装

$ pnpm add  commitlint @commitlint/config-conventional -D -w
  • 配置 package.json中配置commitlint
{
    // ...
    "commitlint": {
        "extends": [
            "@commitlint/config-conventional"
        ]
    }
}
  • 添加钩子
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'

git commit 就会触发提交规范的校验啦。

结语

以上就是所有内容了。

本文包含了如何新建一个 vite+ts+vue3 项目,以及日常开发中遇到的一些 ts 异常提示的解决方案。

懒人推进社会?干饭!!!

参考文档

文档相关

bug 相关

eslint 相关配置