Vue 3 + TypeScript + Vite
待办
- pnpm 简单,最开始直接安装就好
- eslint
- prettier
- husky 提交 lint
- commit 提交规范
- vue-router
- vue-router 添加类型(变动较大在另个分支)
- '@' 别名
- rollup-plugin-visualizer 打包可视化工具,看到打包大小
- vite-plugin-compression 优化打包体积
- env 环境变量
- 支持 jsx
- pinia 持久化
- vue-router 添加守卫
- mitt 发布订阅添加
- lodash
- unocss 对比 tailwindcss 后使用 unocss
- icon 解决方案
- css 引入重置
- 项目设置阿里npm源
- scss 引入
- unplugin-auto-import 消除部分通用 import 引入
- vite-plugin-cdn-import 包使用 cdn 减少构建体积(非必须)
- axios 封装
- 请求 hooks 封装
- element-plus 及基础样式修改,推荐样式覆盖解决方案,看看与unocss有冲突没有
- i18n 多语言解决方案
- [] 登录 and todo demo
额外分支
- [] 兼容性腻子脚本添加
- yapi 接口生成
- [] vue 最佳实践学习(长期)
eslint + prettier + stylelint
eslint
pnpm create @eslint/config
eslint.config.js
中随便加个 ignores
,根据自己目录规则加
{ ignores: ['build', 'dist', 'node_modules', 'script'] },
package.json
添加一个命令测试一下
"lint": "eslint . --fix"
prettier
-
eslint-plugin-prettier
该eslint插件可以将prettier的代码修复功能集成到eslint的检查工作流中,对代 码检查过程中报出的问题进行自动修复。 -
eslint-config-prettier
它可以实现eslint和prettier的相互配合而避免检查规则冲突的情况 -
vue-eslint-parser
解析.vue
文件 解决lint中遇到的Parsing error: Unexpected token
问题
pnpm add prettier eslint-plugin-prettier eslint-config-prettier -D
eslint.config.js
添加插件eslint-plugin-prettier
import globals from 'globals';
import pluginJs from '@eslint/js';
import tseslint from 'typescript-eslint';
import pluginVue from 'eslint-plugin-vue';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
export default [
{ files: ['**/*.{js,mjs,cjs,ts,vue}'] },
{
languageOptions: {
globals: { ...globals.browser, ...globals.node }
}
},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
...pluginVue.configs['flat/essential'],
eslintPluginPrettierRecommended,
{
rules: {
// 本来eslint的规则应该加在这里,等下要装prettier,需要覆盖,所以这里没有
// 'vue/no-unused-vars': 'error',
}
}
];
git 提交 lint
安装 husky
pnpm add husky -D
初始化
pnpm exec husky init
安装 lint-staged
eslint
是检测是全部,lint-staged
只检测 git
暂存区的代码。
pnpm add lint-staged -D
package.json
添加配置
"lint-staged": {
"*.{js,ts,vue}": [
"eslint --fix",
"prettier --write"
],
"*.{cjs,json}": [
"prettier --write"
],
"*.{vue,html}": [
"eslint --fix",
"prettier --write"
],
"*.{scss,css}": [
"stylelint --fix --allow-empty-input",
"prettier --write"
]
}
其他
自己能保证你代码没问题,后续务必会把错误修改了这种特殊情况,才能忽略掉lint提交代码
git commit -m "feat: 测试git提交lint包含了lint失败的内容" -n
commit 提交规范
- 'feat', // 新增功能
- 'fix', // 修复缺陷
- 'docs', // 文档变更
- 'style', // 代码格式(不影响功能,例如空格、分号等格式修正)
- 'refactor', // 代码重构(不包括 bug 修复、功能新增)
- 'perf', // 性能优化
- 'test', // 添加疏漏测试或已有测试改动
- 'build', // 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)
- 'ci', // 修改 CI 配置、脚本
- 'revert', // 回滚 commit
- 'chore' // 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)
commitlint
安装 @commitlint/cli
和对应规则 @commitlint/config-conventional
pnpm add @commitlint/config-conventional @commitlint/cli -D
添加.husky\commit-msg
执行脚本
npx --no -- commitlint --edit $1
package.json
添加配置
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
}
测试一下,随便提交一个 commit 确实被拦截住了
一个提交小工具 git-cz
vue-router
pnpm add vue-router@4
略
vue-router 添加类型
改动太大不确定能和权限结合起来不,单独开了个分支,auto-router
详情:uvr.esm.is/
rollup-plugin-visualizer
安装
pnpm add rollup-plugin-visualizer -D
vite.config.ts
添加配置
import { visualizer } from 'rollup-plugin-visualizer';
...
plugins: [
vue(),
visualizer({
gzipSize: true,
brotliSize: true
})
]
...
vite-plugin-compression 压缩 并开启加速
当前端资源过大时,服务器请求资源会比较慢。前端可以将资源通过Gzip压缩使文件体积减少大概60%左右,能不
能无脑开启gzip
压缩呢,最后解答
安装
pnpm add vite-plugin-compression -D
vite.config.ts
添加配置
import viteCompression from 'vite-plugin-compression';
...
plugins: [
vue(),
viteCompression(),
visualizer({
gzipSize: true,
brotliSize: true
})
]
...
nginx 配置
光是打包有了gz包还不行,得服务器支持才行比如nginx
的配置如下
http
、server
或location
块中添加:
gzip on;
gzip_static on;
gzip_buffers 4 16k;
gzip_comp_level 8;
gzip_types application/javascript text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; #压缩文件类型
gzip_vary on;
测试压缩是否生效
你可以使用浏览器的开发者工具或者命令行工具(如curl)来检查响应头,确认是否包含Content-Encoding: gzip,以验证gzip压缩是否生效。
解答最开始的问题:虽然gzip压缩可以提高传输效率,但它也会增加服务器的CPU负载。因此,在启用gzip压缩
时,需要权衡传输速度和服务器性能之间的关系。所以才有vite-plugin-compression
中配置threshold
阈值的
一个配置项,可以设置压缩文件的最小体积,只有大于这个体积的资源才会被压缩。
env 环境变量
vite自带的env功能
新建.env
、.env.development
、.env.test
、.env.production
文件,分别对应不同的环境变量
其中.env
是始终生效,与其他环境变量merge在src\vite-env.d.ts
中添加下面定义,作用
让import.meta.env
具有类型
interface ImportMetaEnv {
readonly VITE_APP_API_BASE_URL: string;
readonly VITE_APP_API_HOST: string;
}
修改package.json
,添加对应环境的启动命令
"dev": "vite",
"dev:test": "vite --mode test",
"dev:prod": "vite --mode production",
"build": "vue-tsc -b && vite build --mode production",
"build:dev": "vue-tsc -b && vite build --mode development",
"build:test": "vue-tsc -b && vite build --mode test",
添加config文件
存在的意义,不一定所有的config都需要放在环境变量中,这里作为系统的config文件,在项目中使用,可以理解 为环境变量在业务中间的一个防腐层
src\config\index.ts
添加
export type AppConfig = {
apiBaseUrl: string;
};
const { VITE_APP_API_BASE_URL } = import.meta.env;
const config: AppConfig = {
apiBaseUrl: VITE_APP_API_BASE_URL
};
export default config;
业务中使用应该使用这个config
文件
jsx 支持
为什么要用jsx,因为vue
是模板语法,注定在写具有大量逻辑代码的时候你的模板语法不会很好看
安装
@vitejs/plugin-vue-jsx
vite.config.ts
plugins
添加
import vueJsx from '@vitejs/plugin-vue-jsx';
plugins: [
vue(),
vueJsx(),
...
]
eslint 支持jsx 配置
eslint.config.js
添加eslint.config.js
{
languageOptions: {
...
parserOptions: {
...
ecmaFeatures: {
jsx: true
}
}
}
},
demo
基本使用
<script lang="jsx">
export default {
render() {
return (
<div class="app">
<h2>我是标题</h2>
<p>我是内容, 哈哈哈</p>
</div>
);
}
};
</script>
<style lang="less" scoped></style>
组合式组件
<template>
<jsx />
</template>
<script lang="jsx" setup>
import { ref } from 'vue';
import About from './About.vue';
const counter = ref(0);
const increment = () => {
counter.value++;
};
const decrement = () => {
counter.value--;
};
const jsx = () => (
<div>
<h2>当前计数:{counter.value}</h2>
<button onClick={increment}> +1 </button>
<button onClick={decrement}> -1 </button>
<About />
</div>
);
</script>
<style lang="less" scoped></style>
setup 中使用
<script lang="jsx">
import { ref } from 'vue';
import About from './About.vue';
export default {
setup() {
const counter = ref(0);
const increment = () => {
counter.value++;
};
const decrement = () => {
counter.value--;
};
return () => (
<div class="app">
<h2>当前计数:{counter.value}</h2>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<About />
</div>
);
}
};
</script>
<style lang="less" scoped></style>
具体语法
看官网吧 cn.vuejs.org/guide/extra…
lodash
安装
按需引入使用Tree-shaking
Tree-shaking主要依赖于ES6的模块特性,即模块必须是ESM(ES Module)。这是因为ES6模块的依赖关系是确定 的、静态的,与运行时的状态无关,从而可以进行静态分析。
而lodash本身不是ES6模块,要么需要如下方式使用import get from "lodash/get"
才能支持按需引入,多个包
的时候就不好看所以选择lodash-es
pnpm install lodash-es
pnpm install @types/lodash-es -D
pinia 持久化
安装
pnpm add pinia pinia-plugin-persistedstate
src\store\index.ts
import type { App } from 'vue';
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
const store = createPinia();
store.use(piniaPluginPersistedstate);
export function setupStore(app: App<Element>) {
app.use(store);
}
export { store };
main.ts
const app = createApp(App);
// 配置 store
setupStore(app);
配合ts使用
src\store\modules\user.ts
import { defineStore } from 'pinia';
type UserInfo = any;
interface State {
token: string | null;
user: UserInfo | null;
}
export const useUserStore = defineStore('user', {
// 为了完整类型推理,推荐使用箭头函数
state: (): State => {
return {
token: null,
user: null
};
},
persist: true
});
vue-router 添加守卫
src\router\guard\index.ts
import { useUserStore } from '@/store/modules/user';
import { Router } from 'vue-router';
export function setupRouterGuard(router: Router) {
createAuthGuard(router);
}
const createAuthGuard = (router: Router) => {
router.beforeEach(async to => {
const { isLogin } = useUserStore();
if (
// 检查用户是否已登录
!isLogin &&
// ❗️ 避免无限重定向
to.name !== 'Login'
) {
// 将用户重定向到登录页面
return { name: 'Login' };
}
});
};
src\router\index.ts
// 配置路由器
export function setupRouter(app: App<Element>) {
app.use(router);
setupRouterGuard(router);
}
mitt
src\utils\mitt\index.ts
import mitt, { Emitter } from 'mitt';
import { onBeforeUnmount, onMounted } from 'vue';
import { MittEvents } from './types';
export const emitter: Emitter<MittEvents> = mitt<MittEvents>();
// emitter.emit('foo', '123');
// emitter.on('foo', val => {
// console.log(val);
// });
// emitter.off('foo');
// emitter.all.clear()
/**
* emitter.on hooks的调用方法
*/
export const useSubscribe: typeof emitter.on = (...arg: Parameters<typeof emitter.on>) => {
const subscribeKey = arg[0];
const handler = arg[1];
onMounted(() => {
emitter.on(subscribeKey, handler);
});
onBeforeUnmount(() => {
emitter.off(subscribeKey as keyof MittEvents);
});
};
/**
* emitter.emit hooks的调用方法
*/
export const usePublish: () => typeof emitter.emit = () => {
const publish: typeof emitter.emit = (...arg: Parameters<typeof emitter.emit>) => {
emitter.emit(...arg);
};
return publish;
};
src\utils\mitt\types.ts
/**
* 为方便管理 用事件,必须再这里声明
* 举例:appInit、userLogin,至少“主”,“动”两个单词组成
*/
export type MittEvents = {
appInit: string;
userLogin?: number;
};
unocss
安装
pnpm add unocss -D
uno.config.ts
import { defineConfig, presetUno } from 'unocss';
export default defineConfig({
presets: [presetUno()],
shortcuts: {
'wh-full': 'w-full h-full'
},
rules: []
});
main.ts
import 'virtual:uno.css';
vite.config.ts
import UnoCSS from 'unocss/vite';
...
plugins: [
...
UnoCSS(),
...
]
插件
class 可分组
pnpm add @unocss/transformer-variant-group -D
uno.config.ts
最终
import { defineConfig, presetUno } from 'unocss';
import transformerVariantGroup from '@unocss/transformer-variant-group';
export default defineConfig({
presets: [presetUno()],
shortcuts: {
'wh-full': 'w-full h-full'
},
rules: [],
transformers: [transformerVariantGroup()]
});
icon 解决方案
方案一 unocss icon
安装
pnpm add @iconify/utils -D
uno.config.ts
引入配置
import { defineConfig, presetUno, presetIcons } from 'unocss';
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders';
export default defineConfig({
presets: [
presetUno(),
presetIcons({
warn: true,
prefix: ['i-'],
extraProperties: {
display: 'inline-block'
},
collections: {
base: FileSystemIconLoader('./src/assets/svg')
}
})
],
...
});
添加svg资源
src\assets\svg
下引入svg文件,这里的svg文件名就会是后面icon用到的名字
<div class="i-base:close"></div>
未来应该的发展
应该有个icon解决平台的方案,而不是在本地项目维护
缺点
抽象成组件有问题,因为class是拼出来的,没法起作用
方案二 vite-plugin-svg-icons
安装
pnpm add vite-plugin-svg-icons -D
vite.config.ts
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
plugins: [
...
createSvgIconsPlugin({
// Specify the icon folder to be cached
iconDirs: [path.resolve(process.cwd(), 'src/assets/svg')],
// Specify symbolId format
symbolId: 'icon-[dir]-[name]'
}),
...
]
main.ts
import 'virtual:svg-icons-register';
svg组件
src\components\IconSvg\index.vue
<template>
<svg aria-hidden="true" :style="getStyle">
<use :href="symbolId" :fill="color" />
</svg>
</template>
<script>
import { defineComponent, computed } from 'vue';
export default defineComponent({
name: 'SvgIcon',
props: {
prefix: {
type: String,
default: 'icon'
},
name: {
type: String,
required: true
},
size: {
type: [Number, String],
default: 24
},
color: {
type: String,
default: '#333'
}
},
setup(props) {
const symbolId = computed(() => `#${props.prefix}-${props.name}`);
const getStyle = computed(() => {
const { size } = props;
let s = `${size}`;
s = `${s.replace('px', '')}px`;
return {
width: s,
height: s
};
});
return { symbolId, getStyle };
}
});
</script>
css 引入重置
安装
pnpm add @unocss/reset
src\main.ts
import '@unocss/reset/tailwind.css';
设置npm 阿里源
根目录添加.npmrc
registry=https://registry.npmmirror.com
vscode配置
在项目目录下添加.vscode配置文件夹
会和本地配置merge,方便统一管理
一个编辑器小插件
Code Spell Checker 可以检测单词拼写
vite-plugin-cdn-import
项目打包大小的优化,因为比如vue,axios,element-plus,等我们长期都是使用一个版本的,每次打包这些内容 都不需要更新的,也不需要打进包里面的,且单独走cdn,还可以加速访问,减少服务器压力
pnpm add vite-plugin-cdn-import -D
默认是使用的是jsdelivr
vite.config.ts
...
importToCDN({
modules: [
'vue',
{
name: 'vue-demi',
var: 'VueDemi',
path: 'lib/index.iife.min.js'
},
'vue-router',
'axios',
'dayjs',
// {
// name: 'vue-i18n',
// var: 'VueI18n',
// path: 'dist/vue-i18n.global.min.js'
// },
{
name: 'pinia',
var: 'Pinia',
path: 'dist/pinia.iife.min.js'
}
]
}),
最后使用下来,打包速度快了一丢丢,因为现在这个template比较小看不出来,速度的差别,因为没有把第三方包 打进来,所以肯定是快了的
其次加载的数据,也是因为项目比较小,甚至总体加载完资源的速度还变慢了,虽然有资源走cdn了,但是请求数 量多上去了,而影响整体加载的因素除了跟大小还跟http请求数量有关系,所以这个功能看自己用不用,用也可 以,不用也行
unplugin-auto-import 消除部分通用 import 引入
安装
pnpm add unplugin-auto-import -D
vite.config.ts
import AutoImport from 'unplugin-auto-import/vite';
plugins: [
AutoImport({
//要想在项目中优雅地使用自动导入,还要解决以下两个编码的问题:
//TS 类型丢失,会导致 TS 编译报错
//插件会在项目根目录生成类型文件 auto-imports.d.ts ,确保该文件在 tsconfig 中被 include
dts: true,
imports: ['vue', 'vue-router'],
//插件会在项目根目录生成类型文件 .eslintrc-auto-import.json ,确保该文件在 eslint 配置中被 extends:
// .eslintrc.js
// module.exports = {
// extends: [
// './.eslintrc-auto-import.json',
// ],
// }
eslintrc: {
enabled: true
}
})
];
预热
先要跑一次项目生成对应的声明文件auto-imports.d.ts
,..eslintrc-auto-import.json
解决ts报错
tsconifg
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "types/**/*.ts", "auto-imports.d.ts"],
解决eslint报错
eslint.config.js
import fs from 'fs';
const loadJSON = path => JSON.parse(fs.readFileSync(new URL(path, import.meta.url)));
const autoImports = loadJSON('./.eslintrc-auto-import.json');
export default [
...
{
languageOptions: {
globals: { ...globals.browser, ...globals.node, ...autoImports.globals },
parser: parserVue,
parserOptions: {
parser: tseslint.parser,
ecmaFeatures: {
jsx: true
}
}
}
},
...
]
然后你页面上就可以不用引入比如ref
了,同时eslint也正常,ts类型也正常跳转
i18n 多语言解决方案
vue-i18n
太多了,略,简单使用没问题,要配合ts,要不报错,还要全局t函数有提示就搞了很久
vscode i18n Ally
请求 hooks 封装
使用vue-hooks-plus
常用hooks:useBoolean、useUrlState、useToggle、useDebounce、useThrottle、useSize、useRequest
axios 封装
src\utils\request\index.ts
import axios, { AxiosRequestConfig } from 'axios';
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
export const axiosInstance = axios.create({
timeout: 10000
});
axiosInstance.interceptors.request.use(
config => {
return config;
},
error => {
return Promise.reject(error);
}
);
axiosInstance.interceptors.response.use(
response => {
if (response?.status === 200) {
return Promise.resolve(response.data);
} else {
return Promise.reject(response);
}
},
error => {
if (error?.message?.includes?.('timeout')) {
console.log('timeout');
} else {
console.log(error);
}
Promise.reject(error);
}
);
export const request = <ResponseType = unknown>(
url: string,
options?: AxiosRequestConfig<unknown>
): Promise<ResponseType> => {
return new Promise((resolve, reject) => {
axiosInstance({
url,
...options
})
.then(res => {
resolve(res.data);
})
.catch(err => reject(err));
});
};
element-plus 及基础样式修改,推荐样式覆盖解决方案
由于是ui框架,没在主分支,在单独的element-plus
分支上
安装官网安装,我这里采取的全局使用,然后走cdn的方式
样式覆盖
添加 src/assets/style/element/index.scss
@use 'sass:map';
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'white': #ffffff,
'black': #000000,
'primary': (
'base': #409eff
),
'success': (
'base': #67c23a
),
'warning': (
'base': #e6a23c
),
'danger': (
'base': #f56c6c
),
'error': (
'base': #f56c6c
),
'info': (
'base': #909399
)
),
// 正确的样式覆盖方式
$button-border-radius:
(
'large': 2px,
'default': 2px,
'small': 2px
)
);
@use 'element-plus/theme-chalk/src/index.scss' as *;
入口main.ts
替换原本的引入element的样式
import '@/assets/style/element/index.scss';
vite.config.ts
mixin有冲突,additionalData 使用函数的方式来
preprocessorOptions: {
scss: {
additionalData: (source: string, filename: string) => {
if (filename.includes('style/element/index.scss')) {
return source;
}
return '@import "@/assets/style/mixin.scss";' + source;
}
// additionalData: '@import "@/assets/style/mixin.scss";'
}
}
}
注意看覆盖文件中的,这是推荐的覆盖基本样式的方式
$button-border-radius: (
'large': 2px,
'default': 2px,
'small': 2px
);
yapi 接口生成
已开发在yapi-2-typescript 分支