基于Vite+Vue3+TS+AntDV4+Unocss+Pinia的项目开发底层(2024版)

发布于:2024-04-27 ⋅ 阅读:(22) ⋅ 点赞:(0)

这是《【Buff叠满】基于Vite+Vue3+TS+AntDV4+unocss+pinia的项目开发底层》的第二篇文章

上篇文章已经把底子打好,现在要开始起灶台摆桌子了。

写在开头

Vue3 + TypeScript + Vite项目开发底层,集成Eslint + Prettier + StyleLint + Husky + Lint-stahed + CommitLint规范检验,目标是搭建一套好用的前端开发常规底层,支撑10万+代码行项目高效开发。

项目源码:

分支 备注 进度
master 主分支
release/basic 基础底层(Vue3 + TypeScript + Vite + Eslint + Prettier + StyleLint + Husky + Lint-stahed + CommitLint) 已完成
release/advanced 高级版底层(unocss + ant-design-vue4.x + Vue-router + pinia + Axios + Less + unplugin-auto-import + vue-global-api) 已完成
release/pro Pro版底层(含有各种常用页面+业务组件) 计划中

环境

  • IDE:VSCode
  • NodeJs: 18+
  • 包管理工具: PNPM

路径别名和TSConfig配置

路径别名纯粹是想少写些代码,并让IDE知道对应的路径在哪,能正确跳过去,不报错

安装依赖

pnpm add @types/node -D

修改vite.config.ts文件

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import * as path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    // 设置别名
    alias: {
      '@': path.resolve(__dirname, 'src'),
      Assets: path.resolve(__dirname, 'src/assets'),
      Components: path.resolve(__dirname, 'src/components'),
      Utils: path.resolve(__dirname, 'src/utils'), // 工具类方法(新创建的)
    },
  },
})

修改tsconfig.json文件

tsconfig的更多配置项可参考

{
  "compilerOptions": {
    "target": "ESNext", // 将代码编译为最新版本的 JS
    "module": "ESNext", // 使用 ES Module 格式打包编译后的文件
    "moduleResolution": "node", // 使用 Node 的模块解析策略
    "lib": ["ESNext", "DOM", "DOM.Iterable"], // 引入 ES 最新特性和 DOM 接口的类型定义
    "skipLibCheck": true, // 跳过对 .d.ts 文件的类型检查
    "resolveJsonModule": true, // 允许引入 JSON 文件
    "isolatedModules": true, // 要求所有文件都是 ES Module 模块。
    "noEmit": true, // 不输出文件,即编译后不会生成任何js文件
    "jsx": "preserve", // 保留原始的 JSX 代码,不进行编译
    "strict": true, // 开启所有严格的类型检查
    "esModuleInterop": false, // 兼容ES模块, 允许使用 import 引入使用 export = 导出的内容
    "allowSyntheticDefaultImports": true, // 配合esModuleInterop使用
    "allowJs": true, //允许使用js
    "baseUrl": ".", //查询的基础路径
    "paths": {
      "@/*": ["src"],
      "Assets/*": ["src/assets/*"],
      "Components/*": ["src/components/*"],
      "Utils/*": ["src/utils/*"]
    } //路径映射,配合别名使用
  },
  //需要检测的文件
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  "references": [
    {
      "path": "./tsconfig.node.json"
    }
  ] //为文件进行不同配置
}

修改tsconfig.node.json文件

{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}

试一下

在utils下创建一个tools.ts文件

// 生成自增的长度为n的数组,从0开始
export const generateArray = function (n: number): number[] {
  return [...Array(n).keys()]
}

在App.vue中引入和使用

<script setup lang="ts">
import HelloWorld from 'Components/HelloWorld.vue' // 使用Components别名
import { generateArray } from 'Utils/tools' // 使用Utils别名
console.log(generateArray(20))
</script>
</script>

运行看看,没毛病

image.png

配置vue-global-api自动引入Vue API

Vue3开发几乎都离不开ref、reative、computed等等Vue API,每次都需要import { ref } from 'vue'其实有点浪费时间

antfu开发的vue-global-api就可以解决这个问题帮我们自动引入。

安装依赖

pnpm add vue-global-api -D

main.ts中导入 vue-global-api 注册全局 API

// main.js
import 'vue-global-api'

.eslintrc.cjs中增加配置

// .eslintrc.js
module.exports = {
  extends: [
    'vue-global-api'
  ]
};

试一下

在HelloWorld.vue文件中,直接使用ref

<script setup lang="ts">
    const count = ref(0)
    console.log(count)
</script>

运行看看,没毛病!

image.png

配置路由

安装依赖

pnpm add vue-router

配置拦截器

src目录下创建config文件夹,里头创建interceptors文件夹用于存放各种拦截器,比如我们先创建router.ts用于路由守卫和拦截。

import { RouteLocationNormalized, NavigationGuardNext } from 'vue-router'

export async function routerBeforeEachFunc(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) {
  // 路由进入前的操作
  next()
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function routerAfterEachFunc(to: RouteLocationNormalized, from: RouteLocationNormalized) {
  // 路由进入后的操作
}

创建views

src目录下创建views文件夹,用于存放页面,页面结构跟之后的路由结构保持一致,便于维护。这里我们创建一个HomePage页面,里头新建index.vue文件。

<template>
  <div>HomePage</div>
</template>

<script setup lang="ts"></script>

注意,这里eslint会报错,说组件文件名需要是多个单词词组

image.png

emmm,我觉得没必要,所以我们可以在.eslintrc.cjs里配置下规则,不去检测这个问题

    "rules": {
        "vue/multi-word-component-names": 'off'
    }

创建routes

src目录下创建routes文件夹,用于存放所有路由。这里我们创建一个index.ts存放通用路由。

export default [
  {
    path: '/',
    name: 'Home',
    component: () => import('Views/HomePage/index.vue'), // 在vite.config.ts和tsconfig.json配置好路径别名Views
  },
  { path: '/home', redirect: '/' },
]

创建路由实例

src目录下创建plugins文件夹,用于存放所有的插件注入文件。这里我们创建一个router.ts文件用于创建router实例。

import { App } from 'vue'
import { routerBeforeEachFunc, routerAfterEachFunc } from 'Config/interceptors/router' // 在vite.config.ts和tsconfig.json配置好路径别名Config
import { RouteRecordRaw, createRouter, createWebHistory } from 'vue-router'

type RouterModule = {
  default: RouteRecordRaw[]
}

// 导入所有路由文件
let Routes: Array<RouteRecordRaw> = []
const modules = import.meta.glob('Routes/*.ts', { eager: true }) // 在vite.config.ts和tsconfig.json配置好路径别名Routes。eager: true表示同步导入
for (const path in modules) {
  const module: RouterModule = modules[path] as RouterModule
  Routes = Routes.concat(module.default)
}

// 创建路由实例
const router = createRouter({
  history: createWebHistory(),
  routes: Routes,
})

export async function setupRouter(app: App) {
  // 创建路由守卫
  router.beforeEach(routerBeforeEachFunc)
  router.afterEach(routerAfterEachFunc)

  app.use(router)

  // 路由准备就绪后挂载APP实例
  await router.isReady()
}

export default router

注册路由

在入口文件main.ts中使用导出的注册路由方法。

import { createApp } from 'vue'
import 'vue-global-api'
import './style.css'
import App from './App.vue'
import { setupRouter } from 'Plugins/router'

const app = createApp(App)
async function setupApp() {
  // 挂载路由
  await setupRouter(app)

  app.mount('#app')
}

setupApp()

安置route容器

修改App.vue文件

<template>
  <router-view />
</template>

试一下

运行看看,没毛病!

image.png

配置Ant-Design-Vue 4.x

这里我们选用作为组件库,并安装配置自动按需引入,提升开发效率,顺便引入配套的图标库@ant-design/icons-vue

安装依赖

// 安装Ant Design Vue 和 配套icons
pnpm add --save ant-design-vue@4.x @ant-design/icons-vue

// 安装组件自动引入插件
pnpm add -D unplugin-vue-components

修改vite.config.ts文件

// 这里代码经过简化,只保留跟章节有关代码,完整代码请看源码

// Ant Design Vue 4.x 自动按需引入组件
import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    Components({
      resolvers: [
        // Ant Design Vue 4.x 自动按需引入组件
        AntDesignVueResolver({
          importStyle: false, // css in js
        }),
      ],
    }),
  ],
})

试一下

删除src目录下的style.css文件,删除main.ts中对此文件的引入,删除App.vue中的所有style,在Antdv文档中找一个Layout示例代码复制进HomePage/index.vue文件

<template>
  <a-layout class="layout">
    <a-layout-header>
      <div class="logo" />
      <a-menu v-model:selectedKeys="selectedKeys" theme="dark" mode="horizontal" :style="{ lineHeight: '64px' }">
        <a-menu-item key="1">nav 1</a-menu-item>
        <a-menu-item key="2">nav 2</a-menu-item>
        <a-menu-item key="3">nav 3</a-menu-item>
      </a-menu>
    </a-layout-header>
    <a-layout-content style="padding: 0 50px">
      <a-breadcrumb style="margin: 16px 0">
        <a-breadcrumb-item>Home</a-breadcrumb-item>
        <a-breadcrumb-item>List</a-breadcrumb-item>
        <a-breadcrumb-item>App</a-breadcrumb-item>
      </a-breadcrumb>
      <div :style="{ background: '#fff', padding: '24px', minHeight: '280px' }">Content</div>
    </a-layout-content>
    <a-layout-footer style="text-align: center"> Ant Design ©2018 Created by Ant UED </a-layout-footer>
  </a-layout>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const selectedKeys = ref<string[]>(['2'])
</script>
<style scoped>
.site-layout-content {
  padding: 24px;
  min-height: 280px;
  background: #fff;
}

#components-layout-demo-top .logo {
  float: left;
  margin: 16px 24px 16px 0;
  width: 120px;
  height: 31px;
  background: rgb(255 255 255 / 30%);
}

.ant-row-rtl #components-layout-demo-top .logo {
  float: right;
  margin: 16px 0 16px 24px;
}

[data-theme='dark'] .site-layout-content {
  background: #141414;
}
</style>

运行看看,没毛病!(浏览器默认的8px margin)

image.png

此时,根目录下会出现一个components.d.ts文件,里面就放着自动按需导入的组件列表,直呼高级! (注意:Antdv的icon需要按需自行引入)

image.png

配置Axios

安装依赖

pnpm add axios

配置拦截器

在config/interceptors文件夹下新建axios.ts文件,存放请求拦截器。

import type { AxiosRequestConfig, AxiosResponse } from 'axios'
export function requestSuccessFunc(requestObj: AxiosRequestConfig) {
  // 自定义请求拦截逻辑,可以处理权限,请求发送监控等
  // ...

  return requestObj
}

export function requestFailFunc(requestError: AxiosRequestConfig) {
  // 自定义发送请求失败逻辑,断网,请求发送监控等
  // ...

  return Promise.reject(requestError)
}

export function responseSuccessFunc(responseObj: AxiosResponse) {
  // 自定义响应成功逻辑,全局拦截接口,根据不同业务做不同处理,响应成功监控等
  // ...

  return responseObj
}

export function responseFailFunc(responseError: AxiosResponse) {
  // 响应失败,可根据 responseError.status 来做监控处理
  // ...

  return Promise.reject(responseError)
}

配置axios默认config

在config文件夹下新建index.ts文件,存放各种配置,这里我们写上axios的简单配置。

// axios 默认配置
export const AXIOS_DEFAULT_CONFIG = {
  timeout: 50000,
  maxContentLength: 2000,
}

创建axios实例

在plugins文件夹下新建axios.ts文件

import axios from 'axios'
import { AXIOS_DEFAULT_CONFIG } from 'Config/index'

import { requestSuccessFunc, requestFailFunc, responseSuccessFunc, responseFailFunc } from 'Config/interceptors/axios'

// 创建axios实例
const axiosInstance = axios.create(AXIOS_DEFAULT_CONFIG)

// 注入请求拦截
axiosInstance.interceptors.request.use(requestSuccessFunc, requestFailFunc)
// 注入失败拦截
axiosInstance.interceptors.response.use(responseSuccessFunc, responseFailFunc)

export default axiosInstance

试一下

在HomePage/index.vue文件中,发一个请求

// 测试接口请求
import $api from 'Plugins/axios'
$api({
  url: window.location.href,
  method: 'get',
}).then((res) => {
  console.log(res)
})

运行看看,没毛病!

image.png

请求API管理

Axios配置好后,马上把API也封装好。

配置接口默认config

在config/index.ts中加入接口配置,目前只有mockBaseUrl配置,以后业务需要就会多起来。

// API 默认配置
export const API_DEFAULT_CONFIG = {
  mockBaseUrl: '/API', // 本地开发mock接口地址前缀
}

封装API

在plugins下新建api.ts文件,封装API

import axios from './axios'
import { API_DEFAULT_CONFIG } from 'Config/index'
import type { AxiosRequestConfig } from 'axios'

// 根据当前环境设置baseUrl
const mockBaseUrl = import.meta.env.DEV ? API_DEFAULT_CONFIG.mockBaseUrl : ''

// 封装API请求
const API = (option: AxiosRequestConfig) => {
  option['url'] = mockBaseUrl + option.url
  if (option.method?.toLowerCase() === 'get') {
    option['params'] = option.data
  }

  return axios(option)
}

export default API

管理API

在src目录下新建api文件夹分模块存放api接口,例如新建一个user.ts文件存放user相关接口。

import type { AxiosRequestConfig } from 'axios'
import $api from 'Plugins/api'

/** 登录 POST */
interface LoginDto {
  username: string
  password: string
}

// 这里约定所有的接口方法名前加个“$”前缀,跟普通方法名区分开
export async function $authLogin(data: LoginDto, options?: AxiosRequestConfig) {
  return $api({
    url: '/user/login',
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    data,
    ...(options || {}),
  })
}

配置开发环境端口和代理

在vite.config.ts中,加入serve配置。

// 这里代码经过简化,只保留跟章节有关代码,完整代码请看源码
export default defineConfig({
  server: {
    port: 3000, // 本地开发服务端口
    proxy: {
      '/API': {
        target: 'http://127.0.0.3:62000', // 要代理的地址
        changeOrigin: true,
        followRedirects: true, // Cookie支持重定向
        rewrite: (path) => path.replace(/^\/API/, ''),
      },
    },
  },

试一下

在HomePage/index.vue文件中,发一个请求

// 测试接口请求
import { $authLogin } from 'API/user' // 这里记得配置路径映射别名
$authLogin({
  username: 'test',
  password: 'test',
}).then((res) => {
  console.log(res)
})

运行看看,虽然500(因为就没后端服务),但是没毛病!

image.png

配置Pinia

安装依赖

pnpm add pinia

创建pinia实例

在src目录下新建store文件夹,里面创建1个index.ts文件

import { createPinia } from 'pinia'
import type { App } from 'vue'

// 创建store实例
const store = createPinia()

// 挂载到app上
export function setupStore(app: App<Element>) {
  app.use(store)
}

export { store }

在 main.js 中引用

import { createApp } from 'vue'
import 'vue-global-api'
import App from './App.vue'
import { setupRouter } from 'Plugins/router'
import { setupStore } from 'Store/index'

const app = createApp(App)
async function setupApp() {
  // 挂载pinia状态管理
  setupStore(app)

  // 挂载路由
  await setupRouter(app)

  app.mount('#app')
}

setupApp()

创建1个store

在src/store下创建modules文件夹用于存放不同模块的store,比如user.ts存放用户相关的数据

import { defineStore } from 'pinia'
import { $authLogin } from 'API/user'

// 第一个参数是该 store 的唯一 id
const userStore = defineStore('user', {
  state: () => {
    return {
      isLogined: 0, // 0 未知  1  登录  2 未登录
      username: 'test',
      email: 'test@test.com',
    }
  },
  actions: {
    authLogin() {
      return $authLogin().then(
        (res) => {
          this.isLogined = 1
          Object.assign(this.username, res.data.username)
          return res
        },
        (rej) => {
          this.isLogined = 2
          console.log('未登录')
          return rej
        }
      )
    },
  },
  // other options...
})

export default userStore

使用store

在HomePage/index.vue文件中,引用user store

// 测试Store
import useUserStore from 'Store/modules/user'
const userStore = useUserStore()

console.log(userStore.username)

userStore.authLogin().then((res) => {
  console.log(res)
})

运行看看,没毛病!

image.png

配置Unocss(可选)

尝一口antfu大佬做的Unocss,真香!如果你问Unocss是什么,可以查阅这篇文章。因为是推荐,这里不再介绍如何安装使用,有需要点击上述链接学习即可!

配置Iconify(可选)

结合unocss,自动按需引入图标,贼优雅。

安装依赖

pnpm add @unocss/preset-icons @iconify/json unplugin-icons -D

修改tsconfig.json文件

创建uno.config.ts,会被自动导入

import { defineConfig, presetIcons, presetUno, presetAttributify } from 'unocss'
import UnocssIcons from '@unocss/preset-icons'

export default defineConfig({
  presets: [
    presetUno(),
    presetAttributify(),
    presetIcons({
      // 图标默认样式
      extraProperties: {
        display: 'inline-block',
        height: '1em',
        width: '1em',
      },
      /* options */
    }),
    UnocssIcons(),
  ],
})

修改tsconfig.json文件

// 这里代码经过简化,只保留跟章节有关代码,完整代码请看源码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// unocss
import Unocss from 'unocss/vite'

// Icons 自动按需引入图标库
import Icons from 'unplugin-icons/vite'
import IconsResolver from 'unplugin-icons/resolver'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    Components({
      resolvers: [
        IconsResolver(),
      ],
    }),
    Unocss(),
    Icons({ autoInstall: true }), // 自动安装
  ],
})

使用

我们可以在网站上搜寻我们想要的Icon。

image.png

点击某个Icon,然后切到Unocss,即可便捷复制代码应用到项目中

image.png

有2种使用方式,一种是以class名的方式,一种是标签名的方式

    <div class="i-openmoji:home-button" />
    <i-openmoji:home-button />

打开控制台可以看到,class名的方式,Icon会以background的形式展现,标签名的方式就会被转换成SVG。

image.png

注意 只有class名的方式会带上extraProperties自定默认样式

配置Less(可选)

CSS预编译器少不了,Less、Scss、stylus。但每个团队可能选择不一样,这里简单演示Less安装。因为Vite自带less-loader能力,所以安装less依赖就行。

安装依赖

pnpm add less -D

使用

// template
<div class="my-card">
  <div class="card-item">Card 1</div>
  <div class="card-item">Card 2</div>
  <div class="card-item">Card 3</div>
</div>

// style
<style scoped lang="less">
.my-card {
  display: flex;
  justify-content: space-between;

  .card-item {
    max-width: 300px;
    height: 300px;
    font-size: 32px;
    text-align: center;
    color: #fff;
    background-color: #333;
    flex: 1;
    line-height: 300px;
  }
}
</style>

运行看看,没毛病!

image.png

打包优化

最后把打包产物优化下,首先build看看现状

pnpm run build

image.png

可以看到所有资源都在assets下。这里我主要思路是把资源分在不同目录下,把console跟debugger行去掉。

至于文件压缩,gzip我认为交给服务器就行了,打包阶段压缩会加大打包产物总体积,增加打包和传输上线时间,而且服务器压缩是按需+缓存的,图片压缩也同理,处理方式是设计压缩好再交付给前端,保证质量。

修改vite.config.ts文件

// 这里代码经过简化,只保留跟章节有关代码,完整代码请看源码
export default defineConfig({
  build: {
    target: 'ESNext',
    minify: 'esbuild',
    // rollup 配置
    rollupOptions: {
      output: {
        chunkFileNames: 'js/[name]-[hash].js', // 引入文件名的名称
        entryFileNames: 'js/[name]-[hash].js', // 包的入口文件名称
        assetFileNames: '[ext]/[name]-[hash].[ext]', // 资源文件像 字体,图片等
      },
    },
  },
  esbuild: {
    drop: [
      'console', // 如果线上需要打印,就把这行注释掉
      'debugger',
    ],
  },
})

参考


网站公告

今日签到

点亮在社区的每一天
去签到