前言
如何搭建一个简易脚手架。核心需求是输入项目命令,clone准备好的项目模板,拉到本地后,装一下依赖,就可以直接开发了。不用每次都花大量时间,去搭建项目规范和做必要的封装配置。
经过简单寻找后,发现没有符合自己预期的。于是大雄从0到1搭建一个具备完善规范的Vue3开发模板✨,并手把手带大家实现,本文你将会学到以下内容,上图👇,话不多说,我们直接开始!!
本文环境:
- win10
- 包管理工具:pnpm
- node版本:16.16
- vite版本:5.0.10
- vue版本:3.3.11
模板地址:https://github.com/1111-stu/vue3-template
项目规范搭建篇
前置包管理工具pnpm安装(已安装的小伙伴可跳过)
vue3项目推荐使用pnpm来作为包管理工具。若当前没安装过pnpm,执行下面的命令安装一下。
安装:
npm install pnpm -g
基本使用:
下载
pnpm install 包 //
pnpm i 包
pnpm add 包 // -S 默认写入dependencies
pnpm add -D // -D devDependencies
pnpm add -g // 全局安装
移除
pnpm remove 包 //移除包
pnpm remove 包 --global //移除全局包
更新
pnpm up //更新所有依赖项
pnpm upgrade 包 //更新包
pnpm upgrade 包 --global //更新全局包
可能遇到的问题:
安装完成后,如果pnpm命令可以在cmd执行,无法在vscode终端运行,参考:blog.csdn.net/weixin_4806…
解决步骤:
- vscode右键以管理员身份打开运行(不是管理员,会无权限更改)
- vscode终端输入命令get-ExecutionPolicy,若显示结果是Restricted,表示关闭命令功能。
get-ExecutionPolicy
3、输入命令set-ExecutionPolicy,输入参数RemoteSigned即可
set-ExecutionPolicy
RemoteSigned
示例见下图所示👇
Vite自定义模板
自定义选项前置知识了解,这里引用一下大佬的图片@吃炸鸡的前端,见下图所示👇
下面开始使用vite自定义基础模板配置,这里推荐一开始就选择Eslint和Prettier的预设,项目开发中途再加,一堆坑,实属没必要踩。详细配置选择,见下图👇。
我们使用git init来生成一个仓库。然后git add . ,git commit- m"" 来进行初次提交。
项目构建完成后,切换到对应目录,安装依赖,启动项目。
测试EsLint、Prettier的格式化功能
上文,我们基于Vite选择Eslint和Prettier的预设,下面我们测试一下EsLint的格式化效果
运行pnpm lint,发现没有任何变化(Eslint中没有添加很多的规则,导致通过了)。在运行pnpm format发现,发现确实帮我们做了格式化(根据.prettierrc.json文件中的内容格式化了)。
出现上述问题的原因是,基于Vite在选择Eslint和Prettier的预设时,并没有让两者联系起来,即没有把Prettier加入到Eslint中。下文我们就来构建两者的联系。
构建Prettier和Eslint的联系
插件安装
构建Prettier和Eslint的联系,需要安装一下下面的两个插件。
eslint-config-prettier //用于解决和 Prettier 冲突的 ESLint 的配置
eslint-plugin-prettier //启用 eslint-plugin-prettier
npm i eslint-config-prettier eslint-plugin-prettier -D
修改 ESLint 配置,使 Eslint 兼容 Prettier 规则
打开.eslintrc.cjs文件,放在extends配置项的末尾,因为extends中后引入的规则会覆盖前面的规则。
那么就可以在.prettierrc.json 中定义自己的代码风格校验。本地的prettier插件会根据这个文件来格式化,项目配置的prettier也会根据该文件来格式化。且eslint的风格与prettier风格冲突的地方会以prettier为主。
plugin:prettier/recommended
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting',
'plugin:prettier/recommended'
],
parserOptions: {
ecmaVersion: 'latest'
}
}
'
这时我们运行pnpm lint,可以看到已经帮我们格式化好了。下面我们只需要加入commitLint即可完成我们的规范搭建。
可能遇到的问题
如遇到以下的报错,分析日志信息,发现eslint-config-prettier出现了两个版本,猜测是其他插件包依赖的eslint-config-prettier版本是8.10.0,当前安装的是9.1.0。导致出现了版本冲突,删除9.1.0的eslint-config-prettier,装一下8.10.0就可以了。
ESLint couldn't determine the plugin "prettier" uniquely.
集成lint-staged和husky
lint-staged 是一个专门针对已放入 Git 暂存区的文件进行检查的工具
husky 能提供监听 Git 操作并执行脚本代码的能力
安装 lint-staged和husky
pnpm i lint-staged husky --save-dev
配置lint-staged
在package.json中添加下面的代码,匹配暂存区所有的js,vue文件,并执行命令。
"lint-staged": {
"*.{js,vue,jsx,tsx}": [
"pnpm lint",
"prettier --write",
"eslint --cache --fix",
"git add"
]
}
配置husky,实现在git 提交时执行 lint-staged
配置脚本钩子,实现在"pnpm i"后自动执行对应的hooks
在 package.json的"scripts"中配置快捷命令,用来在安装项目依赖时生成 husky 的相关文件,配置项postinstall或者prepare都可以。
{
// ...
"scripts": {
// ...
"postinstall": "husky install"
},
}
在 package.json 文件中,"postinstall" 是一个特殊的脚本钩子,它在 npm install(或等效的 pnpm i、yarn install)命令执行后自动运行。这个特性是由 npm(以及兼容的包管理器如 pnpm 和 yarn)提供的,旨在在安装包后自动执行某些任务。
当在项目中使用 pnpm i 命令安装依赖时,pnpm 会检查 package.json 文件中的 "scripts" 部分,特别是 "postinstall" 脚本。如果存在 "postinstall" 脚本,pnpm 将在所有包安装完成后自动执行该脚本中定义的命令。
在 package.json 中,"postinstall" 被设置为 "husky install"。这意味着每次执行 pnpm i 安装依赖后,pnpm 会自动执行 husky install 命令。这个命令的作用是安装 Husky,Husky是一个流行的 Git 钩子工具,用于在 Git 操作(如提交、推送等)时自动运行脚本。
"prepare" 和 "postinstall" 是 package.json 文件中定义的两个不同的 npm 生命周期钩子,它们在不同的时间点被触发,适用于不同的用途。下面是它们的主要区别:
- 触发时机:
-
- "prepare" : 这个钩子在几个关键场景中被触发,包括在本地执行 npm install(没有参数)后、在作为 git 依赖安装到其他项目之前、以及在运行 npm pack 和 npm publish 之前(在打包“npm pack”或发布“npm publish”你的库到 npm 之前,会执行 "prepare" 脚本。这样可以确保在打包或发布前运行必要的构建步骤或检查。)。
- "postinstall" : 这个钩子仅在 npm install 或等效命令(如 pnpm i 或 yarn install)执行后触发,不论是在本地安装项目依赖时,还是作为依赖安装到其他项目中时。
- 用途:
-
- "prepare" : 通常用于在发布包之前执行构建脚本或其他准备工作。例如,如果你的包需要编译或转换代码,或者需要在发布前进行某些自动化检查,那么 "prepare" 是合适的选择。
- "postinstall" : 通常用于在安装项目依赖后执行一些设置或配置工作。比如安装或配置工具,或执行一些只在项目初次安装依赖时需要的操作。
- 场景应用:
-
- 使用 "prepare" 当你的包需要在发布前进行构建或准备工作,或者当你的包被作为 git 依赖安装时。
- 使用 "postinstall" 适合在每次安装依赖时都需要执行的操作,如安装 Git 钩子或其他项目配置。
如果对npm script命令不太了解的小伙伴可以参考下方链接进行学习
juejin.cn/post/684490…
www.ruanyifeng.com/blog/2016/1…
配置完成后,执行pnpm i,会在项目根目录生成 .husky/_ 目录。
生成pre-commit文件
执行下面的命令, husky会 生成 git 操作的监听钩子脚本。
npx husky add .husky/pre-commit "npx lint-staged"
打开.husky/pre-commit 文件,可以看到以下内容,git commit时,这个脚本作为 Git 钩子运行,使用 lint-staged 来对暂存区中的文件执行代码检查。
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
到此你就完成了git commit时自动去触发eslint的检测和修复。修改代码后git add . git commit,就可以发现代码检测和修复生效了。
补充
值得注意的是,当前这种方式创建的项目,规则很少,适合自己自定义eslint规则。 因为刚初始化好的eslint规则只加入了一些推荐的eslint。(相对来说,踩坑较少)
如果自己需要自定义规则的话,可以在.prettierrc.json或者.eslintrc.cjs的rules中去添加自己的规则。.prettierrc.json定义的规则>.eslintrc.cjs中定义的规则。
如果你需要一套完善的git 提交规范校验,例如:运行 git commmit -m 'xxx' 时,用来检查 xxx 是否满足要求的提交规范, 下文我们一起来实现吧!!🐳
Git 提交规范建设校验commit格式
前置知识
commitlint是什么?
当我们运行 git commmit -m 'xxx' 时,用来检查 xxx 是否满足固定格式的工具。简单来说,就是制定提交规范
提交的格式,与常见的规范
- 提交格式 (注意冒号后面有空格)
git commit -m [optional scope]:
type :用于表明我们这次提交的改动类型,是新增了功能?还是修改了测试代码?又或者是更新了文档?
optional scope:一个可选的修改范围。用于标识此次提交主要涉及到代码中哪个模块。
description:一句话描述此次提交的主要内容,做到言简意赅。
- 常用的 type 类型
安装commitlint
pnpm i --save-dev @commitlint/config-conventional @commitlint/cli
生成commit-msg文件
文件中可以配置在 ****git commit 时对 commit 信息的校验指令
可手动创建文件再输入文件内容,但是建议使用命令创建,命令如下:
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'
上面命令执行成功后会在 .husky 目录下生成一个 commit-msg 文件,该文件的内容如下,表示在 git commit 前执行一下 npx --no -- commitlint --edit $1 指令。
当你进行 Git 提交时,这个 commit-msg 文件会被触发,它先初始化 Husky,然后使用commitlint 来检查你的提交信息是否符合预设的规范。
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit $1
项目根目录创建名为commitlint.config.js的文件,配置提交规范
module.exports = {
extends: ["@commitlint/config-conventional"],
// 定义规则类型
rules: {
// type 类型定义,表示 git 提交的 type 必须在以下类型范围内
"type-enum": [
2,
"always",
[
"feat", // 增加新功能
"fix", // 修复 bug
"del", // 删除功能
"update", // 更新功能
"docs", // 文档相关的改动
"style", // 不影响代码逻辑的改动,例如修改空格,缩进等
"build", // 构造工具或者相关依赖的改动
"refactor", // 代码重构
"revert", // 撤销,版本回退
"test", // 添加或修改测试
"perf", // 提高性能的改动
"chore", // 修改 src 或者 test 的其余修改,例如构建过程或辅助工具的变动
"ci", // CI 配置,脚本文件等改动
],
],
// subject 大小写不做校验
"subject-case": [0],
},
plugins: [
{
rules: {
"commit-rule": ({ raw }) => {
return [
/^[(feat|fix|del|update|docs|style|build|refactor|revert|test|perf|chore)].+/g.test(raw),
`commit备注信息格式错误,格式为 <[type] 修改内容>,type支持${types.join(",")}`,
];
},
},
},
],
};
可能遇到的问题
commitlint.config.js文件,提交时候ESlint会报错,解决方法是,根目录下新建.eslintignore文件,配置路径,忽略对这个文件的Eslint校验。
添加下面这行代码,为什么是.cjs?见下面的报错解决(如未遇到,改为.js就好)
commitlint.config.cjs
添加.eslintignore后,若git commit如果忽略校验没有奏效。检查package.json文件,
如果lint的配置存在--ignore-path,去掉--ignore-path就可以了。原因是:如果eslint后加了 --ignore-path 后,.eslintignore的配置会失效
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
改成:
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix"
参考解决方法:
www.cnblogs.com/zouzhongxin…
juejin.cn/post/700760…
其他忽略ESlint的校验的方法:blog.csdn.net/a7442358/ar…
如果出现下面的问题,重命名commitlint.config.js为commitlint.config.cjs
提供Commit提示信息,实现交互式commit,简化提交的负担
背景:
如果提交的type比较多,每次都需要翻看一下。那么接入commitizen、cz-customizable,实现交互式提交会是个不错的解决方法。交互效果见下图👇
安装commitizen、cz-customizable
commitizen: commit命令行提示
cz-customizable:自定义的Commitizen的交互信息
pnpm add commitizen cz-customizable -g
详细可参考:github.com/leonardoana…
配置信息
根目录新建一个.cz-config.cjs文件(一开始创建的是.js文件,提示使用cjs代替,于是修改为.cjs文件),在根目录创建的.cz-config.cjs 添加以下代码,自定义commit提示内容。
module.exports = {
types: [
{ value: 'feat', name: 'feat: 新功能' },
{ value: 'fix', name: 'fix: 修复bug' },
{ value: 'del', name: 'del: 删除功能'},
{value: 'update', name: 'update: 更新功能'},
{ value: 'docs', name: 'docs: 文档变更' },
{ value: 'style', name: 'style: 代码格式(不影响代码运行的变动)' },
{
value: 'refactor',
name: 'refactor: 重构(既不是增加feature,也不是修复bug)'
},
{ value: 'perf', name: 'perf: 性能优化' },
{ value: 'test', name: 'test: 测试用例变更' },
{ value: 'chore', name: 'chore: 构建过程或辅助工具的变动' },
{ value: 'revert', name: 'revert: 回退' },
{ value: 'build', name: 'build: 打包' },
//ci变动
{ value: 'ci', name: 'ci: CI 配置或脚本文件的改动' }
],
// override the messages, defaults are as follows
messages: {
type: '请选择提交类型:',
// scope: '请输入文件修改范围(可选):',
// used if allowCustomScopes is true
customScope: '请输入修改范围(可选):',
subject: '请简要描述提交(必填):',
body: '请输入详细描述(可选,待优化去除,跳过即可):',
// breaking: 'List any BREAKING CHANGES (optional):\n',
footer: '请输入要关闭的issue(待优化去除,跳过即可):',
confirmCommit: '确认使用以上信息提交?(y/n/e/h)'
},
allowCustomScopes: true,
// allowBreakingChanges: ['feat', 'fix'],
skipQuestions: ['body', 'footer'],
// limit subject length, commitlint默认是72
subjectLimit: 72
}
package.json添加以下内容:
"scripts" : {
...
"commit": "./node_modules/cz-customizable/standalone.js"
}
...
"config": {
"commitizen": {
"path": "./node_modules/cz-customizable"
},
"cz-customizable": {
"config": "./.cz-config.cjs"
}
}
}
配置完成后,输入git cz,出现下面的内容,说明接入成功了
后面的提交,使用git cz来代替git commit
husky hooks接入
输入git cz后,细心的小伙伴会发现,上文的ESlint、Prettier的校验没有生效,我们希望commitizen和ESlint、Prettier,是一起集成到husky的hooks里面,输入git cz自动执行配置的hooks。达到下图的效果
其实很简单,在 Husky 的 commit-msg 钩子中调用 commitizen。即在 .husky/commit-msg 文件中:添加下面的代码
npx cz --hook || true
随便找个文件修改一下,"git add ."、"git cz"查看效果,出现下图说明配置成功了。
其他可扩展的规范校验接入
- 集成Style-lint,提交时校验并格式化css、less、scss代码
- 接入Eslint-plugin-filenames,实现对文件夹文件名的命名校验
- 补充Prettier、Eslint自定义规则
项目基础配置
配置全局 scss 样式文件
安装saas
pnpm i sass -d
新增样式文件
在 src/assets 下新增 style 文件夹,用于存放全局样式文件,新建 main.scss, 设置一个用于测试的颜色变量
$primary-color: #007bff;
$warning-color: #ffc107;
如何将这个全局样式文件全局注入到项目中呢?vite.config.ts添加下方配置
css:{
preprocessorOptions:{
scss:{
additionalData:'@import "@/assets/style/main.scss";'
}
}
},
组件内使用
.title {
color: $primary-color;
}
配置路径别名
依赖安装
@types/node 是一个 NPM 包,它的作用是为 Node.js 环境中的 JavaScript 提供 TypeScript 类型定义。当在 TypeScript 项目中使用 Node.js 时,这个包非常重要
pnpm i @types/node -D
vite.config.ts:第一种配置
// path 模块提供了一些工具函数,用于处理文件与目录的路径
import { resolve } from 'path'
// 使用 defineConfig 工具函数,这样不用 jsdoc 注解也可以获取类型提示
import { defineConfig } from 'vite'
/** 当前执行 node 命令时文件夹的地址(工作目录) */
const root: string = process.cwd()
/** 路径拼接函数,简化代码 */
const pathResolve = (dir: string): string => resolve(root, dir)
export default defineConfig({
resolve: {
alias: [
// 设置 `@` 指向 `src` 目录
{ find: '@', replacement: pathResolve('src') },
// 设置 `@assets` 指向 `src/assets` 目录
{ find: '@assets', replacement: pathResolve('src/assets') },
// 设置 `@components` 指向 `src/components` 目录
{ find: '@components', replacement: pathResolve('src/components') },
// 设置 `@views` 指向 `src/views` 目录
{ find: '@views', replacement: pathResolve('src/views') },
// 设置 `@utils` 指向 `src/utils` 目录
{ find: '@utils', replacement: pathResolve('src/utils') }
// 可以根据需要添加更多的别名
],
},
})
vite.config.ts:第二种配置
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@assets': fileURLToPath(new URL('./src/assets', import.meta.url)),
'@components': fileURLToPath(new URL('./src/components', import.meta.url)),
'@views': fileURLToPath(new URL('./src/views', import.meta.url)),
'@utils': fileURLToPath(new URL('./src/utils', import.meta.url))
}
}
})
tsconfig.json声明paths
无论是使用配置一还是配置二,都需要在tsconfig.json声明paths
{
"compilerOptions": {
// ... 其他已有的配置
// 设置基础 URL 为项目根目录
"baseUrl": ".",
// 定义别名
"paths": {
"@/": ["./src/"],
"@assets/*": ["./src/assets/*"],
"@components/*": ["./src/components/*"],
"@views/*": ["./src/views/*"],
"@utils/*": ["./src/utils/*"]
}
},
// ... 其他 TypeScript 配置,如 include、exclude 等
}
请求封装
安装axios
pnpm i axios
实际使用中可以根据项目修改,比如RESTfulapi中可以自行添加put和delete请求,ResType也可以根据后端的通用返回值动态的去修改
简单二次封装
新增 service 文件夹,service 下新增 http.ts 文件、 api 文件夹。api文件夹下做接口做统一管理,按照模块来划分。详见下方文件树👇。
service
├── api
│ └── login
│ ├── login.ts
│ └── types.ts
└── http.ts
http.ts代码
//http.ts
import axios from 'axios'
// 设置请求头和请求路径
axios.defaults.baseURL = '/api'
axios.defaults.timeout = 10000
axios.defaults.headers.post['Content-Type'] = 'application/json;charset=UTF-8'
axios.interceptors.request.use(
(config) => {
const token = window.sessionStorage.getItem('token')
if (token) {
//@ts-ignore
config.headers.token = token
}
return config
},
(error) => {
return error
}
)
// 响应拦截
axios.interceptors.response.use((res) => {
if (res.data.code === 111) {
sessionStorage.setItem('token', '')
// token过期操作
}
return res
})
interface ResType<T> {
code: number
data?: T
msg: string
err?: string
}
interface Http {
get<T>(url: string, params?: unknown): Promise<ResType<T>>
post<T>(url: string, params?: unknown): Promise<ResType<T>>
upload<T>(url: string, params: unknown): Promise<ResType<T>>
download(url: string): void
}
const http: Http = {
get(url, params) {
return new Promise((resolve, reject) => {
axios
.get(url, { params })
.then((res) => {
resolve(res.data)
})
.catch((err) => {
reject(err.data)
})
})
},
post(url, params) {
return new Promise((resolve, reject) => {
axios
.post(url, JSON.stringify(params))
.then((res) => {
resolve(res.data)
})
.catch((err) => {
reject(err.data)
})
})
},
upload(url, file) {
return new Promise((resolve, reject) => {
axios
.post(url, file, {
headers: { 'Content-Type': 'multipart/form-data' },
})
.then((res) => {
resolve(res.data)
})
.catch((err) => {
reject(err.data)
})
})
},
download(url) {
const iframe = document.createElement('iframe')
iframe.style.display = 'none'
iframe.src = url
iframe.onload = function () {
document.body.removeChild(iframe)
}