从零开始构建一个 Vue 3 + Typescript + Vite2.0 项目

640 阅读1分钟

从零开始构建一个 Vue 3 + Typescript + Vite 项目

项目地址

一、创建基本项目

参考 vite 官网

// 使用 npm
npm init vite@latest

// 使用 yarn
yarn create vite

// 使用 pnpm
pnpm create vite

选择 vue ,再选择 vue-ts

二、 配置 eslint + prettier

1、安装依赖

// 1.安装 eslint 相关依赖
yarn add eslint eslint-plugin-vue  @vue/eslint-config-typescript -D

// 2.安装 prettier 相关依赖
yarn add prettier eslint-plugin-prettier @vue/eslint-config-prettier -D

// 3.安装解析器
yarn add @typescript-eslint/parser @typescript-eslint/eslint-plugin -D

2、配置 eslint

根目录下创建 .eslintrc.js 文件

// .eslintrc.js

module.exports = {
  root: true,
  env: {
    node: true,
  },
  extends: [
    'plugin:vue/vue3-essential',
    'eslint:recommended',
    '@vue/typescript/recommended',
    '@vue/prettier',
  ],
  parserOptions: {
    ecmaVersion: 2022,
    parser: '@typescript-eslint/parser',
    requireConfigFile: false,
    sourceType: 'module',
  },
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'vue/multi-word-component-names': [
      'error',
      {
        ignores: ['index', '404'],
      },
    ],
    'no-empty-function': 'off',
    '@typescript-eslint/no-empty-function': 'off',
    'no-unused-vars': 'off',
    '@typescript-eslint/no-unused-vars': ['error'],
  },
}

使用<script setup>语法糖时, definePropsdefineEmits等方法会报no-undef错误,需要在.eslintrc.js中添加以下代码。

// .eslintrc.js

module.exports = {
  // ... 其他配置
  globals: {
    defineProps: 'readonly',
    defineEmits: 'readonly',
    defineExpose: 'readonly',
    withDefaults: 'readonly',
  },
}

3、 配置 prettier

根目录下创建 .prettierrc 文件

{
  "semi": false,
  "singleQuote": true,
  "endOfLine": "auto",
  "printWidth": 100
}

如果配置完没有效果,重启 vscode 即可

三、设置 alias

// vite.config.ts

export default defineConfig({
  resolve: {
    alias: {
      '@': '/src/',
    },
  },
})
// tsconfig.json

{
  "baseUrl": ".",
  "paths": {
    "@/*": ["src/*"]
  }
}

四、基础目录结构

|-- vue3-vite-ts-template
    |-- public
    |-- src
        |-- api // 接口文件
        |-- assets // 静态文件
        |-- components // 全局组件
        |-- hooks // hooks
        |-- router // 路由
        |-- store // 状态管理器
        |-- styles // 全局样式
        |-- utils // 公共方法
        |-- views // 视图文件
        |-- App.vue
        |-- env.d.ts
        |-- main.ts
    |-- .env.development
    |-- .env.production
    |-- .env.staging
    |-- .eslintrc.js
    |-- .gitignore
    |-- .prettierrc
    |-- index.html
    |-- package.json
    |-- README.md
    |-- tsconfig.json
    |-- vite.config.ts
    |-- yarn.lock

五、添加 sass 和 全局样式

1、添加依赖

yarn add sass sass-loader -D

2、添加 全局样式

/src/styles 下创建 index.scss 文件,根据自己习惯修改,以下提供一个模板

// index.scss

html {
  box-sizing: border-box;
}

body {
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-family: PingFangSC-Regular, PingFang SC, Helvetica Neue, Helvetica, Hiragino Sans GB, Microsoft
      YaHei, Arial, sans-serif;
}

html,
body {
  height: 100%;
}

label {
  font-weight: 700;
}

#app {
  min-height: 100%;
}

*,
*:before,
*:after {
  box-sizing: inherit;
}

* {
  margin: 0;
  padding: 0;
}

a:focus,
a:active {
  outline: none;
}

a,
a:focus,
a:hover {
  cursor: pointer;
  color: inherit;
  text-decoration: none;
}

div:focus {
  outline: none;
}

.clearfix {
  &:after {
    visibility: hidden;
    display: block;
    font-size: 0;
    content: ' ';
    clear: both;
    height: 0;
  }
}

ul li {
  list-style-type: none;
}

// main-container global css
.app-container {
  padding: 16px;
}

main.ts 引入

import { createApp } from 'vue'
import App from './App.vue'

import './styles/index.scss'

const app = createApp(App)

app.mount('#app')

3、添加 全局 mixin

/src/styles 下创建 mixin.scss 文件,根据自己习惯添加

// mixin.scss

// 单行省略 、 多行省略
@mixin ellipsis($rowNum: 1) {
  @if ($rowNum == 1) {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  } @else {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: $rowNum;
    overflow: hidden;
    word-break: break-all;
  }
}

vite.config.js 引入

// vite.config.js

export default defineConfig({
  // ... 其他配置
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/styles/mixin.scss";`,
      },
    },
  },
})

六、配置环境

根目录下创建 .env.development.env.production.env.staging

# .env.development 开发环境

# just a flag
ENV = 'development'

# 命名规范 `VITE_为前缀的变量才会暴露给经过vite`处理的代码

# base api
VITE_APP_BASE_API = 'http://xxxx.xxx'

# .env.production 生产环境

# just a flag
ENV = 'production'
NODE_ENV =  'production'

# base api
VITE_APP_BASE_API = 'http://xxxx.xxx'

# .env.staging  测试环境

# just a flag
ENV = 'staging'
NODE_ENV =  'production'

# base api
VITE_APP_BASE_API = 'http://xxxx.xxx'

// package.json
{
  "build:stage": "vite build --mode staging", //测试环境 打包
  "build": "vite build" //生产环境 打包
}

七、添加接口 Api

这里还是使用 axios 来处理,有个 vue-request看上去还可以,之后再研究

1、安装依赖

yarn add axios

2、封装 axios

import axios, { AxiosRequestConfig } from 'axios'

const service = axios.create({
  baseURL: import.meta.env.VITE_APP_BASE_API as string, // url = base url + request url
  timeout: 1000 * 30, // request timeout
})

// request interceptor
service.interceptors.request.use(
  (config) => {
    // do something before request is sent

    // if (store.getters.token) {
    // let each request carry token
    // ['X-Token'] is a custom headers key
    // please modify it according to the actual situation
    // config.headers['X-Token'] = getToken()

    return config
  },
  (error) => {
    console.log(error) // for debug
    return Promise.reject(error)
  }
)

// response interceptor
service.interceptors.response.use(
  (response) => {
    return response
  },
  (error) => {
    console.log(error) // for debug
    return Promise.reject(error)
  }
)

/* 接口统一返回格式 */
interface ResponseData<T> {
  code: number
  success: boolean
  msg: string
  data?: T
}

const request = <T = any>(config: AxiosRequestConfig): Promise<ResponseData<T>> => {
  return new Promise((resolve, reject) => {
    service
      .request<ResponseData<T>>(config)
      .then((res) => resolve(res.data))
      .catch((err) => reject(err))
  })
}

export default request

3、创建 api 文件(示例)

/src/api/types 下创建 user.d.ts

// user.d.ts

/* 登录 */
export interface loginParams {
  account: string
  password: string
}
export interface loginResult {
  token: string
}

/src/api 下创建 user.ts

// user.ts

import request from '@/utils/request'

import { loginParams, loginResult } from './types/user'

/* 登录 */
export function login(data: loginParams) {
  return request<loginResult>({
    url: '/xxx/xxx/xxx',
    method: 'post',
    data,
  })
}

八、添加路由

参考 vue-router 官网

1、添加依赖

yarn add vue-router@4

yarn add nprogress // 加载进度条 非必须

2、配置路由

/src/router 下创建 index.tsroutes.ts
/src/views 下创建 dashboard/index.vue404.vue

// index.ts
import { createRouter, createWebHashHistory } from 'vue-router'
import routes from './routes'

/**
 * 路由模式一共有三种:
 *    createWebHistory
 *    createWebHashHistory
 *    createMemoryHistory
 * 这里使用的是路由的 hash 模式
 */
const router = createRouter({
  history: createWebHashHistory(),
  routes: routes,
})

export default router
// routes.ts
import { RouteRecordRaw } from 'vue-router'

/**
 * 固定路由
 */
const staticRoutes: RouteRecordRaw[] = [
  {
    path: '/',
    redirect: '/dashboard',
  },
  {
    path: '/404',
    component: () => import('@/views/404.vue'),
  },
]

/**
 * 权限路由
 */
const asyncRoutes: RouteRecordRaw[] = [
  {
    path: '/dashboard',
    component: () => import('@/views/dashboard/index.vue'),
    meta: {
      title: '首页',
    },
  },
  // ... 其他具体路由页面
]

/**
 * 结尾路由
 */
const endRoutes: RouteRecordRaw[] = [
  // 没有匹配地址,跳转到 404,要放在最后
  {
    path: '/:pathMatch(.*)*',
    name: 'notFound',
    redirect: '/404',
    meta: {
      hidden: true,
    },
  },
]

const routes: RouteRecordRaw[] = [...staticRoutes, ...asyncRoutes, ...endRoutes]

export default routes

3、配置全局路由守卫

/src 下添加全局路由守卫 permission.ts

import router from './router'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css' // progress bar style

NProgress.configure({ showSpinner: false }) // NProgress Configuration

router.beforeEach(async (to, from, next) => {
  NProgress.start()

  // do something

  next()
})

router.afterEach(() => {
  NProgress.done()
})

如果 nprogress 提示无法找到模块的声明文件,在 npm 上下载一个就行

yarn add @types/nprogress -D

4、在 main.ts 引入

// main.ts
import { createApp } from 'vue'
import App from './App.vue'

// router
import router from '@/router'
import './permission'
app.use(router)

app.mount('#app')

5、配置路由入口

<!-- app.vue -->

<router-view v-slot="{ Component }">
  <component :is="Component" />
</router-view>

九、添加状态管理器(pinia)

参考 pinia 官网

1、添加依赖

yarn add pinia

2、配置 pinia(示例)

/src/store 下创建 index.ts

// index.ts

import { createPinia } from 'pinia'
const store = createPinia()

export default store

/src/store 下创建 user.ts

// user.ts

import { defineStore } from 'pinia'

import { login } from '@/api/user'
import { loginParams } from '@/api/types/user'

// useUserStore could be anything like useUser, useCart
// the first argument is a unique id of the store across your application
export const useUserStore = defineStore('user', {
  state: () => {
    return {
      userName: 'Admin',
      type: 0,
      token: '',
    }
  },
  getters: {
    isAdmin(state) {
      return state.type === 0
    },
  },
  actions: {
    login(account: string, password: string) {
      const params: loginParams = {
        account,
        password,
      }

      login(params).then((res) => {
        if (res?.data) {
          this.userName = '张三'
          this.type = 1
          this.token = res.data.token
        }
      })
    },
  },
})

3、在 main.ts 引入

// main.ts

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// store
import store from '@/store'
app.use(store)

app.mount('#app')

4、调用示例

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/store/user'

const store = useUserStore()
// 属性
const { userName, isAdmin } = storeToRefs(store)
// 方法
store.login('1515123445', '*******')
</script>

<template>
  <div>
    <p>userName:{{ userName }}</p>
    <p>isAdmin:{{ isAdmin }}</p>
  </div>
</template>

5、数据持久化

插件 pinia-plugin-persist 可以辅助实现数据持久化功能。

  1. 安装
yarn add pinia-plugin-persist
  1. 使用
// src/store/index.ts

import { createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist'

const store = createPinia()
store.use(piniaPluginPersist)

export default store
// user.ts

import { defineStore } from 'pinia'

import { loginParams, login } from '@/api/user'

// useUserStore could be anything like useUser, useCart
// the first argument is a unique id of the store across your application
export const useUserStore = defineStore('user', {
  state: () => {
    return {
      userName: 'Admin',
      type: 0,
    }
  },
  // ... 其他配置
  persist: {
    enabled: true, // 开启数据缓存
    strategies: [
      {
        storage: localStorage, //持久化存储位置
        paths: ['userName'], //持久化字段
      },
    ],
  },
})

十、打包配置

1、打包时删除console和debugger

// vite.config.js

import { defineConfig } from 'vite'

export default defineConfig({
  //...
  build: {
    minify: 'terser',
    terserOptions: {
      compress:{
        drop_console: true,
        drop_debugger: true,
      }
    },
  },
})