vite构建vue3+TS项目

从零开始用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

引入
Element Plus-快速开始

选择按需自动导入
需要安装unplugin-vue-componentsunplugin-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
})

vite构建vue3+TS项目代码


  目录