基于vite新建一个vue—ts 项目

285 阅读5分钟

基于vite新建一个vue—ts 项目

github 上新建一个项目 vue-ts-init

克隆到本地

git clone https://github.com/name/vue-ts-init.git

使用 pnpm 初始化项目

pnpm create vite vue-ts-init --template vue-ts

可以运行起来看看

cd vue-ts-init
pnpm install
pnpm run dev

工程化插件

增加.nvmrc 用来设置 node 版本 不用再执行 nvm use 切换 node 版本

设置路径别名

pnpm i @types/node -D

1、tsconfig.json 设置

"baseUrl": "",
"paths": {
    "@/*": ["./src/*"] // 此处映射是相对于"baseUrl"
}

2、vite.config.ts 设置路径

import { resolve } from "path";

resolve: {
    alias: [
        {
            find: "@",
            replacement: resolve(__dirname, "src"),
        },
    ],
},

3、找个模块测试一下

import HelloWorld from "@/components/HelloWorld.vue";

找不到模块“./App.vue”或其相应的类型声明

在 vite-env.d.ts 文件里新增以下对 vue 的声明

declare module "*.vue" {
	import type { DefineComponent } from "vue";
	const component: DefineComponent<{}, {}, any>;
	export default component;
}

安装 prettier + eslint

pnpm i @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier eslint-plugin-prettier prettier eslint-plugin-vue -D

安装 prettier + eslint

1、配置 .eslintrc.js

module.exports = {
    root: true,
    parser: 'vue-eslint-parser',
    parserOptions: {
        parser: '@typescript-eslint/parser',
    },
    extends: ['plugin:vue/vue3-recommended', 'plugin:prettier/recommended'],
    rules: {
        'vue/no-v-html': 0,
        'vue/v-on-event-hyphenation': 0,
        'vue/no-template-shadow': 0,
        'vue/multi-word-component-names': 0,
    },
}

2、配置 .eslintignore

/dist
/node_modules
tsconfig.json
*.svg
*.png
*.jpg
*.jpeg
*.scss
*.gif
*.webp
*.ttf
index.html
*.md

3、配置.prettierrc.js

module.exports = {
    singleQuote: true, // 使用单引号代替双引号
    printWidth: 200, // 超过最大值换行
    semi: false, // 结尾不用分号
    useTabs: true, // 缩进使用tab, 不使用空格
    tabWidth: 4, // tab 样式宽度
    bracketSpacing: true, // 对象数组, 文字间加空格 {a: 1} => { a: 1 }
    arrowParens: 'avoid', // 如果可以, 自动去除括号 (x) => x 变为 x => x
    proseWrap: 'preserve',
    htmlWhitespaceSensitivity: 'ignore',
    trailingComma: 'all',
}

4、配置.prettierignore

/dist
/node_modules
/deploy
*.yml
*.yaml
tsconfig.json
*.svg
*.png
*.jpg
*.jpeg
*.scss
*.gif
*.webp
*.ttf
index.html
*.md

5、重启编辑器,格式化文件

pnpm eslint --fix ./src/*

6、在 package.json 中去掉 "type": "module", 因为 .eslintrc.js中 “module.exports=”导出方式不是module默认的export default

使用 vue-router

1、安装

pnpm i vue-router

2、配置路由

1、在src目录下新建 router/index.ts

import {createRouter, createWebHistory} from 'vue-router'

const routes = [
    { path: '/', component: ()=> import('@/components/HelloWorld.vue') },
    { path: '/login', component: ()=> import('@/pages/login.vue') },
    { path: '/account', component: ()=> import('@/pages/account.vue') },
    { path: '/:pathMatch(.*)', redirect: '/'  },
]

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

//导航守卫
router.beforeEach((_to, _from, next)=>{
    next()
})
export default router

2、在main.ts 引入路由并使用

import router from "@/router"
app.use(router)
3、在app.vue页面中加入
<router-view></router-view>
4、新增pages/account.vue
<template>
	<h1>account 页面</h1>
	<button @click="toLogin">toLogin</button>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router'

const router = useRouter()
function toLogin() {
	router.push({
		path: '/login',
	})
}
</script>

5、新增pages/login.vue
<template>
	<h1>login 页面</h1>
	<button @click="toAccount">toAccount</button>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router'

const router = useRouter()
function toAccount() {
	router.push({
		path: '/account',
	})
}
</script>

使用状态管理器 pinia

安装

pnpm i pinia

main.ts 中使用

import { createPinia } from 'pinia'

app.use(createPinia())

新建stores/index.ts

import { defineStore, acceptHMRUpdate } from 'pinia'

export const useStore = defineStore('counter', {
	state: () => {
		return { count: 0 }
	},
	// 也可以这样定义
	// state: () => ({ count: 0 })
	getters: {
		double: state => state.count * 2,
	},
	actions: {
		increment() {
			this.count++
		},
	},
})

if (import.meta.hot) {
	import.meta.hot.accept(acceptHMRUpdate(useStore, import.meta.hot))
}

使用sass

pnpm install sass -D

使用 autoprefiexer 给 web 项目自动增加 css 前缀,兼容各种浏览器

安装

pnpm install autoprefixer -D
pnpm install postcss -D

在vite.config.ts 中配置

import autoprefixer from 'autoprefixer'

// <https://vitejs.dev/config/>
export default defineConfig({
	css: {
		postcss: {
			plugins: [
				autoprefixer({
					overrideBrowserslist: ['Chrome > 40', 'ff > 31', 'ie 11'],
				}),
			],
		},
	},
})

在页面中测试一下

.filter-1 {
	filter: blur(1px);
}

运行结果:
.filter-1[data-v-e17ea971] {
    -webkit-filter: blur(1px);
    filter: blur(1px);
}

使用 @vitejs/plugin-legacy 兼容旧版浏览器js

安装

pnpm i @vitejs/plugin-legacy terser@^5.4.0 -D

在vite.config.js中新增以下配置

import legacy from '@vitejs/plugin-legacy'

plugins: [
    legacy({
        targets: ['cover 99.5%'],
    }),
],

使用组件库 ant-design-vue

安装

pnpm i ant-design-vue

使用

import { Button as AButton } from 'ant-design-vue'

<a-button type="primary">Primary Button</a-button>

备注:这里安装的版本是 ant-design-vue 4.0.7 然后按钮点击的时候报错:

useConfigInject.js:64 Uncaught TypeError: Cannot read properties of undefined (reading 'value')
    at ReactiveEffect.fn (useConfigInject.js:64:83)
    at ReactiveEffect.run (reactivity.esm-bundler.js:178:19)
    at get value [as value] (reactivity.esm-bundler.js:1140:33)
    at showWave (useWave.js:7:21)
    at HTMLButtonElement.onClick (index.js:51:13)

升级一下就可以了 这个问题ant-design-vue已经修复了

pnpm i ant-design-vue@next

然后安装的这个版本啊 是"ant-design-vue": "4.0.0-rc.6"

在软件开发中,版本号通常由三个部分组成:主版本号、次版本号和修订版本号。有时,还会包括预发布版本号或构建元数据。在你提到的版本号 "4.0.0-rc.6" 中,"rc.6" 表示预发布版本,其中 "rc" 代表 "Release Candidate",意味着这是一个发布候选版本。数字 "6" 表示该候选版本的第六个构建或修订。

使用 tailwindcss 快速添加样式

安装

pnpm install -D tailwindcss postcss autoprefixer
pnpx tailwindcss init

在根目录得到一个 tailwind.config.js配置文件

配置

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./src/**/*.{vue}"],
  theme: {
    extend: {},
  },
  plugins: [],
}

在common.scss添加以下代码

@tailwind base;
@tailwind components;
@tailwind utilities;

使用husky和lint-staged进行代码提交前的检查

安装

pnpm dlx husky-init && pnpm install

pnpm install --save-dev lint-staged

在.husky/per-commit 中添加

npx lint-staged

在package.json

"lint-staged": {
    "*.{vue,ts}": "eslint --cache --fix",
    "*.md": "prettier --list-different"
}

执行命令观察结果

git add . 
git commot -m 'husky'

基于axios封装请求

安装

pnpm i axios

创建实例

import axios, { AxiosError, AxiosInstance } from 'axios'

function createRequestInstance(url: string): AxiosInstance {
	const instance = axios.create({
		timeout: 1000 * 60 * 10,
		withCredentials: false,
		baseURL: `${url}/`,
	})
	return instance
}
//创建一个axios实例
const request = createRequestInstance('http://10.10.24.58:3000')

export default request

新增错误class

新建error.ts

import { AxiosError } from 'axios'

//自定义错误返回类型(具体可以跟后端同学商量决定)
export type ErrorResponse = {
	status: number
	code: number
	msg: string
}

class AxRequestError extends Error {
	data: ErrorResponse | undefined

	raw: AxiosError

	isUnAuthorized = false

	isServerError = false

	errCode?: number

	isNetError = false

	message: string

	constructor(status: number, message: string, raw: AxiosError, data?: ErrorResponse) {
		super(message) // ES6 要求,子类的构造函数必须执行一次 super 函数,否则会报错。
		this.data = data
		this.raw = raw
		this.isUnAuthorized = status === 401 //一般是身份验证不通过,token过期
		this.isServerError = status >= 500
		this.errCode = data?.code
		this.message = `${message || raw?.message || data?.msg || ''}`
		if (message.includes('getaddrinfo ENOTFOUND') || message === 'Network Error') {
			this.message = 'network error'
			this.isNetError = true
		} else if (['no token present in request'].includes(message)) {
			this.message = 'login expired'
		}
	}
}

export default AxRequestError

新增错误处理器 handleError.ts

import { AxiosError } from 'axios'
import AxRequestError, { ErrorResponse } from './error'

export function handelError(error: AxiosError | AxRequestError): AxRequestError {
	const err = error instanceof AxRequestError ? error : new AxRequestError(error.response?.status || 1, error.message, error, error.response?.data as ErrorResponse)
	return err
}

请求拦截器

在instance.ts 实例文件中新增请求拦截器,添加用户身份验证、多语言、自定义headers等设置

//伪方法 具体根据业务逻辑实现
const getToken = () => {
	return ''
}
//请求拦截器
request.interceptors.request.use(config => {
	const token = getToken() 
	const headers = config.headers || {}
	config.headers = {
		...headers,
		Authorization: `${headers.Authorization || token || ''}`,
		language: 'zh', //适用于多语言环境,
	} 
	return config
})

响应拦截器

//响应拦截器
request.interceptors.response.use(undefined, (err: AxRequestError) => {
	err = handelError(err)
	if (err.isUnAuthorized) {
		//todo退出登录
	}
	//其他错误处理

	return Promise.reject(err)
})

公共请求方法

import { AxiosResponse, AxiosRequestConfig } from 'axios'
import request from './instance'

/* 导出封装的请求方法以及公共方法 */
const $api = {
	get<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R> {
		return request.get(url, config)
	},
	post<T = any, R = AxiosResponse<T>>(url: string, data?: object, config?: AxiosRequestConfig): Promise<R> {
		return request.post(url, data, config)
	},
	put<T = any, R = AxiosResponse<T>>(url: string, data?: object, config?: AxiosRequestConfig): Promise<R> {
		return request.put(url, data, config)
	},
	delete<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R> {
		return request.delete(url, config)
	},
	patch<T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R> {
		return request.patch(url, data, config)
	},

	async test(): Promise<AxiosResponse> {
		const res = await request.get('/test')
		return res
	},
}

export default $api

在main.ts绑定公共请求方法

import $api from '@/service/requestList'

app.config.globalProperties.$api = $api

配置生产环境和开发环境

新增env.ts

import { isUndef } from '@/utils/is'

// 正式环境
export const ENV = {
	APP_API_BASE: 'http://10.10.24.58:3000',
	IS_DEV: '',
	NODE_ENV: 'production',
	IS_TEST: '',
}

// 测试环境
export const ENV_DEV = {
	...ENV,
	APP_API_BASE: 'http://10.10.24.58:3001',
	IS_DEV: 'true',
	NODE_ENV: 'development',
	IS_TEST: 'true',
}

export type EnvKey = keyof typeof ENV

const isTestEnv = process.env.NODE_ENV === 'development' || ['test.xxx.com'].includes(location.host)

// const isTestEnv = false
export function getProcessEnv(key: EnvKey): string | void {
	if (isTestEnv) {
		if (!isUndef(ENV_DEV[key])) {
			return ENV_DEV[key]
		}
		return ''
	}
	if (!isUndef(ENV[key])) {
		return ENV[key]
	}
}

在instance.ts中引入

import { getProcessEnv } from '@/global/env'
const request = createRequestInstance(getProcessEnv('APP_API_BASE') || '')

测试一下

import $api from '@/service/request'

$api.get('/test').then(res => {
	console.info('res:', res)
})
$api.test().then(res => {
	console.info('res:', res)
})

使用vite-plugin-svg-icons插件展示svg矢量图

安装

pnpm install 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: [resolve(__dirname, 'src/assets/icons')],
		}),
    ],

在main.ts 中引用

import 'virtual:svg-icons-register'

新建svgIcon公共组件

<template>
	<svg aria-hidden="true" :width="width" :height="height">
		<use :href="symbolId" :fill="color" />
	</svg>
</template>

<script setup lang="ts">
import { computed } from 'vue'

const props = defineProps({
	name: {
		type: String,
		required: true,
	},
	color: {
		type: String,
		default: '#333',
	},
	size: {
		type: String,
		default: '',
	},
})
const symbolId = computed(() => `#icon-${props.name}`)

const size = props.size.split(' ')

let width: string
let height: string
if (size.length === 1) {
	;[width] = size
	height = width
} else if (size.length === 2) {
	;[width, height] = size
} else {
	throw new Error(`invalid size for svg: ${props.name}`)
}
</script>

新建global/allGlobalComponents.ts 注册全局组件

// 引入项目中的全部全局组件
import SvgIcon from '@/components/svgIcon.vue'

// 组装成一个对象
const allGlobalComponents: any = { SvgIcon }

// 对外暴露插件对象,在main.ts中引入后,直接自动调用install方法 注册插件
export default {
	install(app: any) {
		// 循环注册所有的全局组件
		Object.keys(allGlobalComponents).forEach(componentName => {
			app.component(componentName, allGlobalComponents[componentName])
		})
	},
}

在main.ts 中使用

import allGlobalComponents from '@/global/allGlobalComponents'

app.use(allGlobalComponents)

测试一下 在icons中新增一个bold.svg

<SvgIcon name="bold" color="blue" size="100"></SvgIcon>

i18n添加多语言

安装

pnpm i vue-i18n@9

新建global/i18n

en.ts

export default {
	test: 'Hello',
}

zh.ts

export default {
	test: '你好',
}

index.ts

import { createI18n } from 'vue-i18n'
import en from './en'
import zh from './zh'
import { SystemLang } from '@/global/enum'

const localLang = localStorage.SYSTEM_LANG

let curSystemLang
if (localLang) {
	curSystemLang = ['zh', 'zh-cn'].includes(localLang) ? SystemLang.ZH : SystemLang.EN
}

const defaultLang = curSystemLang ? curSystemLang : ['zh', 'zh-cn'].includes(window.navigator.language.toLocaleLowerCase()) ? SystemLang.ZH : SystemLang.EN

const messages = {
	en,
	zh,
}

const i18n = createI18n({
	messages,
	locale: defaultLang,
	//fallbackLocale: 'en', // 设置回退语言环境
})

export function setCurSystemLang(curSystemLang: SystemLang) {
	i18n.global.locale = curSystemLang
}

export const curLang = i18n.global.locale

export const $i18n = i18n.global.t.bind(i18n.global)

export default i18n

在main.ts中引入并使用

import i18n, { $i18n } from '@/global/i18n'

app.use(i18n)

app.config.globalProperties.$t = $i18n

在页面中使用

<h1 class="text-3xl font-bold underline">{{ i18n.global.locale }}: {{ $t('test') }} </h1>
<h1 class="text-3xl font-bold underline">{{ test }}</h1>

import i18n, { $i18n, setCurSystemLang } from '@/global/i18n'
import { SystemLang } from '@/global/enum'

const test = computed(() => $i18n('test'))

function changeLang() {
	setCurSystemLang(SystemLang.EN)
}