从零开始用vite构建vue3+TS项目。
初始化项目
npm init vite@latest
项目名称:vite-vue3-ts
cd vite-vue3-ts
npm install
npm run dev
创建目录结构
在src
目录下面创建这些文件夹
文件名 | 用途 |
---|---|
api | 存放请求相关文件 |
service | axios请求封装 |
router | 路由管理 |
store | 仓储状态管理 |
styles | 公共样式 |
utils | 工具函数 |
views | 路由页面 |
配置路由Router
安装
npm install vue-router@4 -S
初始化路由实例
创建src/router/index.ts
文件,使用路由懒加载,优化访问性能。
import { createRouter, createWebHistory, createWebHashHistory, RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('../views/home/index.vue') // 建议进行路由懒加载,优化访问性能
},
{
path: '/login',
name: 'login',
component: () => import('../views/login/index.vue')
},
{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ "../views/about/index.vue"),
}
]
const router = createRouter({
// history: createWebHistory(), // 使用history模式
history: createWebHashHistory(), // 使用hash模式
routes
})
export default router
在main.ts里面引入router
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from "./router/index";
const app = createApp(App);
app.use(router);
app.mount("#app");
使用router
在App.vue 文件中使用router-view 组件,路由匹配到组件会通过router-view 组件进行渲染
<template>
<router-view></router-view>
</template>
<script setup lang="ts">
import {useRouter} from 'vue-router'
let router = useRouter()
</script>
<style scoped>
</style>
安装vuex
npm install vuex@next --save
配置相关
Vue组件中的$sotre属性的类型声明
useStore组合式函数的类型声明
目录
├─ store
│ ├─ index.ts
│ └─ interface.ts
创建src/store/interface.ts
export default interface State {
count: number
foo: string
}
创建src/store/index.ts
文件,初始化
// index.ts 文件
import { createStore, Store, useStore as baseUseStore } from 'vuex'
import State from './interface'
// 定义类型InjectionKey。
// InjectionKey在将商店安装到Vue应用程序时提供类型。
// 将类型传递InjectionKey给useStore方法。
import { InjectionKey } from 'vue'
// 定义 injection key
export const key: InjectionKey<Store<State>> = Symbol('key')
// 创建store实例
export const store = createStore<State>({
state() {
//存放数据和data类似
return {
count: 0,
foo: 'Hi'
}
},
getters: {
//相当于计算属性
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
//vuex中用于发起异步请求
},
modules: {
//拆分模块
}
})
// 定义自己的useStore组合式函数
export function useStore() {
return baseUseStore(key)
}
在main.ts里面引入store
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from "./router";
import { store, key } from './store'
const app = createApp(App);
app.use(router);
app.use(store, key);
app.mount("#app");
配置vite.confing.ts
配置模块路径别名
安装类型声明文件,否则会报错找不到path
npm install --save-dev @types/node
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
})
//tsconfig.json
{
"compilerOptions": {
// 路径配置
"paths": {
"@/*": [
"./src/*"
]
}
}
}
//tsconfig.node.josn
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
CSS 样式管理
这里我使用.scss
和 .sass
安装相应的预处理器依赖:
npm install -D sass
创建相关样式文件:
在 styles
文件下搭建样式目录结构
index.scss
:组织统一导出
variables.scss
: 全局 Sass变量
common.scss
: 全局公共样式
可选:
less安装
npm install -D less
stylus安装
npm install -D stylus
可以根据自己喜好来安装
index.scss 文件
@import './variables.scss';
@import './common.scss';
...
在main.ts里面引入样式
import { createApp } from 'vue'
import './styles/index.scss'
...
注意:
在组件里使用样式全局变量的时,需要在组件里引入定义的样式变量才能正常使用
<template>
<h1> 标题 </h1>
</template>
<script lang="ts" setup>
</script>
<style lang="scss" scoped>
@import '@/styles/variables.scss';
h1 {
color: $color;
}
</style>
但是每个单文件组件都引入是麻烦的,我就采取变量注入全局的方式
// vite.config.ts
...
css: {
preprocessorOptions: {
scss: {
// 注入样式变量(根据自己需求注入其他)
additionalData: `@import "./src/styles/variables.scss";`
}
}
},
...
这样的话就可以在组件直接使用变量
...
<style lang="scss" scoped>
h1 {
color: $color;
}
</style>
安装Element Plus
npm install element-plus --save
选择按需自动导入
需要安装unplugin-vue-components
和 unplugin-auto-import
这两款插件
npm install -D unplugin-vue-components unplugin-auto-import
然后在vite.config.ts文件配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
//自动导入
import AutoImport from "unplugin-auto-import/vite"
import Components from "unplugin-vue-components/vite"
import { ElementPlusResolver } from "unplugin-vue-components/resolvers"
import { resolve } from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
...
})
到这,我们就配置好了按需自动导入。
注意:
在vscode中直接使用ElMessage
会报错
需要在tsconfig.json
中 include 引入 auto-imports.d.ts
这个element-plus 自动引入组件的ts文件
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noEmit": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"auto-imports.d.ts"
],
"exclude": ["node_modules"],
"references": [{ "path": "./tsconfig.node.json" }]
}
这样就可以直接在组件使用ElMessage
,不需要再次引入。
要使用命令的方式创建element组件时,样式会无法自动引入。
import { ElMessage } from "element-plus"
ElMessage.warning("warning")
使用unplugin-element-plus
为 Element Plus 按需引入样式
import { ElButton } from "element-plus"
// ↓ ↓ ↓ ↓ ↓ ↓
import { ElButton } from "element-plus"
import "element-plus/es/components/button/style/css"
这个插件其实就是把你需要的组件的css或者sass文件自动引入进来
安装
npm install unplugin-element-plus -D
vite.config.ts
配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ElementPlus from 'unplugin-element-plus/vite'
//自动导入
import AutoImport from "unplugin-auto-import/vite"
import Components from "unplugin-vue-components/vite"
import { ElementPlusResolver } from "unplugin-vue-components/resolvers"
import { resolve } from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
ElementPlus({
useSource: true
}),
...
],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
...
})
注意:
unplugin-vue-components
按需导入的依赖项更新导致页面无限次重载
vite.config.ts
配置
//思路:排除重载的依赖项。
import fs from 'fs'
const optimizeDepsElementPlusIncludes = ["element-plus/es"];
fs.readdirSync("node_modules/element-plus/es/components").map((dirname) => {
fs.access(
`node_modules/element-plus/es/components/${dirname}/style/css.mjs`,
(err) => {
if (!err) {
optimizeDepsElementPlusIncludes.push(
`element-plus/es/components/${dirname}/style/css`
);
}
}
);
});
export default defineConfig({
...
optimizeDeps: {
include: optimizeDepsElementPlusIncludes,
},
...
})
elementPlus图标
方法一:自动引入
安装
npm install @element-plus/icons-vue unplugin-icons @iconify-json/ep -D
vite.config.ts
配置
import {resolve} from 'path'
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import {ElementPlusResolver} from 'unplugin-vue-components/resolvers'
import Icons from 'unplugin-icons/vite'
import IconsResolver from 'unplugin-icons/resolver'
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [
// 自动导入element-plus组件
ElementPlusResolver(),
// 自动导入图标组件
IconsResolver({
prefix: 'Icon'
})
],
}),
Components({
resolvers: [
// 自动导入element-plus组件
ElementPlusResolver(),
// 自动导入图标组件
IconsResolver({
enabledCollections: ['ep']
})
],
}),
// 自动导入图标组件
Icons({
autoInstall: true,
})
]
})
使用
无需再次引入,在标识的图标名前追加前缀IEp即可使用,这个官网有点坑没有明说,我是看了源站代码发现里面使用时前都加了i-ep-
比如: <i-ep-add-location />
<template>
<IEpCompass />
<IEpPlus />
<el-icon><IEpCirclePlus /></el-icon>
</template>
方法二:全部注册引入
创建src/element-plus/icons.ts
文件
import { App } from 'vue'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
export default function (app: App): void {
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
}
main.ts引入
import { createApp } from 'vue'
import './styles/index.scss'
import App from './App.vue'
import router from "./router/index";
import { store, key } from './store'
//按需导入组件库
import Icons from './element-plus/icons'
const app = createApp(App);
app.use(router);
app.use(store, key);
app.use(Icons)
app.mount("#app");
使用
<template>
<el-icon><Search /></el-icon>
<el-icon><Link /></el-icon>
</template>
以上两种引入方式任选一种就OK了。
安装axios
npm install axios --save
目录
├─ store
│ ├─ index.ts
│ └─ config.ts
创建src/service/config.ts
文件
开发环境判断
let BASE_URL = ''
const TIME_OUT = 5000
if (process.env.NODE_ENV === 'development') {
BASE_URL = '开发环境'
} else if (process.env.NODE_ENV === 'production') {
BASE_URL = '生产环境'
} else {
BASE_URL = '测试环境'
}
export { BASE_URL, TIME_OUT }
创建src/service/index.ts
文件
axios请求取消,拦截
import axios, { AxiosRequestConfig, AxiosResponse, Method } from 'axios';
import { BASE_URL, TIME_OUT } from './config'
const service = axios.create({
baseURL: BASE_URL,
timeout: TIME_OUT, // 超时时间
withCredentials: true,
validateStatus: function (status) {
return status >= 200 && status < 300; // 默认值
},
});
// 定义接口
interface PendingType {
url?: string;
method?: Method | string;
params: any;
data: any;
cancel: any;
}
// 取消重复请求
const pending: Array<PendingType> = [];
const CancelToken = axios.CancelToken;
// 移除重复请求
const removePending = (config: AxiosRequestConfig) => {
for (const key in pending) {
const item: number = +key;
const list: PendingType = pending[key];
// 当前请求在数组中存在时执行函数体
if (list.url === config.url && list.method === config.method && JSON.stringify(list.params) === JSON.stringify(config.params) && JSON.stringify(list.data) === JSON.stringify(config.data)) {
// 执行取消操作
list.cancel('操作太频繁,请稍后再试');
// 从数组中移除记录
pending.splice(item, 1);
}
}
};
/**
* 请求拦截器
*/
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
removePending(config);
config.cancelToken = new CancelToken((c:any) => {
pending.push(
{
url: config.url,
method: config.method,
params: config.params,
data: config.data,
cancel: c
}
);
});
// 添加请求头以及其他逻辑处理
const token = localStorage.getItem('token')
if(token && config.headers){
config.headers.token = token
}
return config;
},
(error: any) => {
Promise.resolve(error);
}
);
/**
* 响应拦截器
*/
service.interceptors.response.use(
(response: AxiosResponse) => {
removePending( response.config );
const res = response.data;
// 后端status错误判断
if ( res.code === 200 ) {
return Promise.resolve(res.data);
} else {
// 错误状态码处理
return Promise.reject(res.data);
}
},
(error: any) => {
// Http错误状态码处理
return Promise.reject(error);
}
);
export default service;
Api请求封装
目录
├─ api
│ ├─ model
│ ├ └─ dataTypes.ts
│ └─ LoginApi.ts
dataTypes.ts
//登录
export interface Login {
mobile:string,
password:string
}
LoginApi.ts
import request from '@/service/index';
import { Login } from './model/dataTypes'
/**
* 登录
* @param {string} url 请求连接
* @param {Object} params 请求参数
* @param {Object} header 请求需要设置的header头
*/
export default class LoginApi {
static login (data: Login) {
return request({
url: '/api/member/loginPw',
method: 'post',
data: data
});
}
}
组件使用
<template>
<el-form
ref="ruleFormRef"
:model="ruleForm"
status-icon
label-width="120px"
class="demo-ruleForm"
>
<el-form-item label="用户名" prop="pass">
<el-input v-model="ruleForm.mobile" type="text" autocomplete="off" />
</el-form-item>
<el-form-item label="密码" prop="checkPass">
<el-input
v-model="ruleForm.password"
type="password"
autocomplete="off"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm"
>Submit</el-button
>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { reactive } from 'vue'
import LoginApi from '@/api/loginApi'
const ruleForm = reactive({
mobile: '18888888883',
password: '111111'
})
const submitForm = () => {
let data = {
...ruleForm
}
LoginApi.login(data).then((res) => {
console.log(res)
ElMessage({
message: '登录成功',
type: 'success'
})
})
}
</script>
跨域
vite.config.ts
配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import Icons from 'unplugin-icons/vite'
import IconsResolver from 'unplugin-icons/resolver'
import ElementPlus from 'unplugin-element-plus/vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
...
// https://vitejs.dev/config/
export default defineConfig({
...
server: {
//服务器主机名
host: "localhost",
//端口号
port: 3000,// 不知为何更改会有问题
//设为 true 时若端口已被占用则会直接退出,
//而不是尝试下一个可用端口
strictPort: true,
cors: true, // 默认启用并允许任何源
open: false, // 在服务器启动时自动在浏览器中打开应用程序
//https.createServer()配置项
https: false,
//反向代理配置,注意rewrite写法,开始没看文档在这里踩了坑
proxy: {
'^/api': {
target: 'http://api.huhaowb.com/api', //代理接口
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})
VS Code插件安装
- Vue 3 Snippets
- Vue VSCode Snippets
- ESLint
- Prettier ESLint
- Vue Language Features (Volar)
- TypeScript Vue Plugin (Volar)
- Vue Volar extension Pack
- Prettier - Code formatter
配置eslint
安装相关依赖
npm i eslint eslint-plugin-vue eslint-define-config -D
npm i prettier eslint-plugin-prettier @vue/eslint-config-prettier -D
npm i @vue/eslint-config-typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser -D
配置文件 .eslintrc.js
const { defineConfig } = require('eslint-define-config')
module.exports = defineConfig({
root: true,
/* 指定如何解析语法。*/
parser: 'vue-eslint-parser',
/* 优先级低于parse的语法解析配置 */
parserOptions: {
parser: '@typescript-eslint/parser',
//模块化方案
sourceType: 'module'
},
env: {
browser: true,
es2021: true,
node: true,
// 解决 defineProps and defineEmits generate no-undef warnings
'vue/setup-compiler-macros': true
},
plugins: ['prettier'],
extends: [
// https://github.com/vuejs/eslint-plugin-vue
'plugin:vue/vue3-essential',
// https://github.com/vuejs/eslint-config-standard
'standard',
'plugin:vue/vue3-recommended',
'eslint:recommended',
'plugin:@typescript-eslint/recommended', // typescript-eslint推荐规则,
'prettier',
'airbnb-base',
'plugin:prettier/recommended',
'./.eslintrc-auto-import.json' // 这是unplugin-auto-import/vite相关配置,自动引入vue相关api
],
// 解决 error Component name "index" should always be multi-word vue/multi-word-component-names
overrides: [
{
files: ['src/views/**/*.vue'],
rules: {
'vue/multi-word-component-names': 0
}
}
],
rules: {
'prettier/prettier': 'error',
// 禁止使用 var
'no-var': 'error',
semi: 'off',
// 优先使用 interface 而不是 type
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
'@typescript-eslint/no-explicit-any': 'off', // 可以使用 any 类型
'@typescript-eslint/explicit-module-boundary-types': 'off',
// 解决使用 require() Require statement not part of import statement. 的问题
'@typescript-eslint/no-var-requires': 0,
// https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/ban-types.md
'@typescript-eslint/ban-types': [
'error',
{
types: {
// add a custom message to help explain why not to use it
Foo: "Don't use Foo because it is unsafe",
// add a custom message, AND tell the plugin how to fix it
String: {
message: 'Use string instead',
fixWith: 'string'
},
'{}': {
message: 'Use object instead',
fixWith: 'object'
}
}
}
],
// 禁止出现未使用的变量
'@typescript-eslint/no-unused-vars': [
'error',
{ vars: 'all', args: 'after-used', ignoreRestSiblings: false }
],
'vue/html-indent': 'off',
// 关闭此规则 使用 prettier 的格式化规则,
'vue/max-attributes-per-line': ['off'],
// 优先使用驼峰,element 组件除外
// 'vue/component-name-in-template-casing': [
// 'error',
// 'PascalCase',
// {
// ignores: ['/^el-/', '/^router-/'],
// registeredComponentsOnly: false,
// },
// ],
// 强制使用驼峰
// camelcase: ["error", { properties: "always" }],
// 优先使用 const
'prefer-const': [
'error',
{
destructuring: 'any',
ignoreReadBeforeAssign: false
}
]
}
})
.eslintignore
不会对以下文件进行代码风格检查
build/*.js
src/assets
node_modules
public
dist
auto-import.d.ts
components.d.ts
.prettierrc
具体规则官网自行查找,prettier主要是格式化代码
useTabs:使用tab缩进还是空格缩进,选择false;
tabWidth:tab是空格的情况下,是几个空格,选择2个;
printWidth:当行字符的长度,推荐80,也有人喜欢100或者120;
singleQuote:使用单引号还是双引号,选择true,使用单引号;
trailingComma:在多行输入的尾逗号是否添加,设置为 none;
semi:语句末尾是否要加分号,默认值true,选择false表示不加;
{
"useTabs": false,
"tabWidth": 2,
"printWidth": 80,
"singleQuote": true,
"trailingComma": "none",
"semi": false
}
.prettierignore
对以下文件不会格式化
build/*.js
src/assets
node_modules
public
dist
auto-import.d.ts
components.d.ts
EditorConfig 插件
.editorconfig
自定义文件是用来定义编辑器的编码格式规范,编辑器的行为会与 .editorconfig 文件中定义的一致,并且其优先级比编辑器自身的设置要高。
EditorConfig 插件会读取 .editorconfig 文件中定义的内容,应用于编辑器。
# http://editorconfig.org
root = true
[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行首的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行
[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false
配置环境变量
在项目根目录下新建两个配置文件:
.env.development:开发环境下的配置文件,执行npm run dev命令,会自动加载.env.development文件.
.env.production:生产环境下的配置文件,执行npm run build命令,会自动加载.env.production文件
.env.development文件:
NODE_ENV = 'development'
VITE_BASE_URL='/api'
.env.production文件:
ENV = 'production'
VITE_BASE_URL = 'http://xxxxxx/api/'
这里的VITE_BASE_URL是项目上线后需要请求的服务器接口。
使用全局变量
vue-cli引用为:
process.env.变量名
vite引用为:
import.meta.env.变量名
配置axios时使用全局baseUrl:
const service = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
timeout: 5000
})