vite + vue3 + setup + pinia + ts 项目实战

33,230 阅读11分钟

介绍

一个使用 vite + vue3 + pinia + ant-design-vue + typescript 完整技术路线开发的项目,秒级开发更新启动、新的vue3 composition api 结合 setup纵享丝滑般的开发体验、全新的 pinia状态管理器和优秀的设计体验(1k的size)、antd无障碍过渡使用UI组件库 ant-design-vue、安全高效的 typescript类型支持、代码规范验证、多级别的权限管理~

前言

前两天接到了一个需求,就是把原来的一个项目的主要功能模块和用户模块权限系统抽出来做一个新后台项目,并迭代新增一些新功能,看起来好像也没啥东西

拿到源码看了下项目,好家伙,原项目是个微应用项目,主应用用户模块是react技术栈,子应用模块是vue2技术栈,这直接 CV大法看样子是不行了👀,我这要做的毕竟是个单页面应用,确定一个技术路线即可,具体看下代码逻辑并跑起来看看

跑起来试了下,两个项目基本都是1分钟左右启动,看代码vue项目整个业务逻辑代码都拧在一块写了

想到之前问老大要源码的时候,说那个是老项目了,重新搭一个写应该会快点

这话没毛病啊,话不多说,直接开整,这次直接上 vite + vue3

emoji

特性

  • ✨脚手架工具:高效、快速的 Vite
  • 🔥前端框架:眼下最时髦的 Vue3
  • 🍍状态管理器:vue3新秀 Pinia,犹如 react zustand般的体验,友好的api和异步处理
  • 🏆开发语言:政治正确 TypeScript
  • 🎉UI组件:antd开发者无障碍过渡使用 ant-design-vue,熟悉的配方熟悉的味道
  • 🎨css样式:lesspostcss
  • 📖代码规范:EslintPrettierCommitlint
  • 🔒权限管理:页面级、菜单级、按钮级、接口级
  • ✊依赖按需加载:unplugin-auto-import,可自动导入使用到的vuevue-router等依赖
  • 💪组件按需导入:unplugin-vue-components,无论是第三方UI组件还是自定义组件都可实现自动按需导入以及TS语法提示

项目目录

├── .husky                              // husky git hooks配置目录
    ├── _                               // husky 脚本生成的目录文件
    ├── commit-msg                      // commit-msg钩子,用于验证 message格式
    ├── pre-commit                      // pre-commit钩子,主要是和eslint配合
├── config                              // 全局配置文件
    ├── vite                            // vite 相关配置
    ├── constant.ts                     // 项目配置
    ├── themeConfig.ts                  // 主题配置
├── dist                                // 默认的 build 输出目录
├── mock                                // 前端数据mock
├── public                              // vite项目下的静态目录
└── src                                 // 源码目录
    ├── api                             // 接口相关
    ├── assets                          // 公共的文件(如image、css、font等)
    ├── components                      // 项目组件
    ├── directives                      // 自定义 指令
    ├── enums                           // 自定义 常量(枚举写法)
    ├── hooks                           // 自定义 hooks
    ├── layout                          // 全局布局
    ├── router                          // 路由
    ├── store                           // 状态管理器
    ├── utils                           // 工具库
    ├── views                           // 页面模块目录
        ├── login                       // login页面模块
        ├── ...
    ├── App.vue                         // vue顶层文件
    ├── auto-imports.d.ts               // unplugin-auto-import 插件生成
    ├── components.d.d.ts               // unplugin-vue-components 插件生成
    ├── main.ts                         // 项目入口文件
    ├── shimes-vue.d.ts                 // vite默认ts类型文件
    ├── types                           // 项目type类型定义文件夹
├── .editorconfig                       // IDE格式规范
├── .env                                // 环境变量
├── .eslintignore                       // eslint忽略
├── .eslintrc                           // eslint配置文件
├── .gitignore                          // git忽略
├── .npmrc                              // npm配置文件
├── .prettierignore                     // prettierc忽略
├── .prettierrc                         // prettierc配置文件
├── index.html                          // 入口文件
├── LICENSE.md                          // LICENSE
├── package.json                        // package
├── pnpm-lock.yaml                      // pnpm-lock
├── postcss.config.js                   // postcss
├── README.md                           // README
├── tsconfig.json                       // typescript配置文件
└── vite.config.ts                      // vite

开发

项目初始化

如果使用vscode编辑器开发vue3,请务必安装Volar插件与vue3配合使用更佳(与原本的Vetur不兼容)

使用 vite cli 快速创建项目

yarn create vite project-name --template vue-ts

安装相关依赖

推荐使用新一代 pnpm 包管理工具,性能和速度以及 node_modules依赖管理都很优秀

建议配合 .npmrc 配置使用

# 提升一些依赖包至 node_modules
# 解决部分包模块not found的问题
# 用于配合 pnpm
shamefully-hoist = true

# node-sass 下载问题
# sass_binary_site="https://npm.taobao.org/mirrors/node-sass/"

代码规范

工具:huskyeslintprettier

具体使用方式,网上很多,我在之前另一篇文章也有说过,这里不再赘述~

a Vite2 + Typescript + React + Antd + Less + Eslint + Prettier + Precommit template

主要就是自动化的概念,在一个合适的时机完成规定的事

  • 结合VsCode编辑器(保存时自动执行格式化:editor.formatOnSave: true
  • 配合Git hooks钩子(commit前或提交前执行:pre-commit => npm run lint:lint-staged

注意

针对不同系统 commitlint安装方式有所不同 commitlint,安装错误可能会无效哦~

# Install commitlint cli and conventional config
npm install --save-dev @commitlint/{config-conventional,cli}
# For Windows:
npm install --save-dev @commitlint/config-conventional @commitlint/cli

功能

vue能力支持

模板语法配合jsx语法,使用起来非常方便、灵活~

一些必须的插件

{
    // "@vitejs/plugin-legacy": "^1.6.2", // 低版本浏览器兼容
    "@vitejs/plugin-vue": "^1.9.3", // vue 支持
    "@vitejs/plugin-vue-jsx": "^1.2.0", // jsx 支持
}

状态管理器 Pinia

vue新一代状态管理器,用过 react zustand的同学应该会有很熟悉的感觉(zustand状态管理器

Pinia是一个围绕Vue 3 Composition API的封装器。因此,你不必把它作为一个插件来初始化,除非你需要Vue devtools支持、SSR支持和webpack代码分割的情况

  • 非常轻量化,仅有 1 KB
  • 直观的API使用,符合直觉,易于学习
  • 模块化设计,便于拆分状态
  • 全面的TS支持
// ... 引入相关依赖

interface IUserInfoProps{
  name: string;
  avatar: string;
  mobile: number;
  auths: string[]
}

interface UserState {
  userInfo: Nullable<IUserInfoProps>;
}

// 创建 store
export const useUserStore = defineStore({
  id: 'app-user', // 唯一 ID,可以配合 Vue devtools 使用
  state: (): UserState => ({
    // userInfo
    userInfo: null,
  }),
  getters: {
    getUserInfo(): Nullable<IUserInfoProps> {
      return this.userInfo || null;
    },
  },
  actions: {
    setUserInfo(info: Nullable<IUserInfoProps>) {
      this.userInfo = info ?? null;
    },
    resetState() {
      this.userInfo = null;
    },

    /**
     * @description: fetchUserInfo
     */
    async fetchUserInfo(params: ReqParams) {
      const res = await fetchApi.userInfo(params);
      if (res) {
        this.setUserInfo(res);
      }
    },
  },
})

组件中使用

// TS 类型推断、异步函数使用都很方便
import { useHomeStore } from '/@/store/modules/home';

const store = useHomeStore();
const userInfo = computed(() => store.getUserInfo);

onMounted(async () => {
  await store.fetchInfo(); // 异步函数
  // ...
});

UI组件按需加载、自动导入

了解基本概念:vite 自带按需加载(针对js),我们这里主要针对样式做按需加载处理

方案一:vite-plugin-style-import

import styleImport from 'vite-plugin-style-import'

// 
plugins:[
  styleImport({
    libs: [
      {
        libraryName: 'ant-design-vue',
        esModule: true,
        resolveStyle: (name) => {
          return `ant-design-vue/es/${name}/style/index`
        },
      }
    ]
  })
]

方案二:unplugin-vue-components

推荐使用 unplugin-vue-components 插件

该插件只需在 vite plugin中添加对应 AntDesignVueResolver 即可,也支持自定义的 components 自动注册,很方便

import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
import Components from 'unplugin-vue-components/vite';

// vite.config.ts plugins 添加如下配置
export default defineConfig({
  plugins: [
    Components({
      resolvers: [
        AntDesignVueResolver(), // ant-design-vue
        // ElementPlusResolver(), // Element Plus
        // VantResolver(), // Vant
      ]
    })
  ]
})

当然这里如果没有你使用的对应的UI框架的 Resolver加载器,也没关系,也支持自定义配置

Components({
  resolvers: [
    // example of importing Vant
    (name) => {
      // where `name` is always CapitalCase
      if (name.startsWith('Van'))
        return { importName: name.slice(3), path: 'vant' }
    }
  ]
})

另一强悍功能:该插件不仅支持UI框架组件的按需导入,也支持项目组件的自动按需导入

具体表现就是:如我们使用 ant-design-vue的 Card组件或我们自己定义的 components/Icon 等其他组件时,我们不用导入,直接用即可,插件会为我们自动按需导入,结合 TS语法提示,开发效率杠杠的~

配置如下:

Components({
  // allow auto load markdown components under `./src/components/`
  extensions: ['vue'],

  // allow auto import and register components
  include: [/\.vue$/, /\.vue\?vue/],

  dts: 'src/components.d.ts',
})

需要在src目录下添加 components.d.ts文件配合使用,该文件会被插件自动更新

  • components.d.ts 作用

直接的作用是:在项目下生成对应.d.tstype类型文件,用于语法提示与类型检测通过

  • 注意

"unplugin-vue-components": "^0.17.2"

当前版本已知问题:issues 174

对于 ant-design-vuenotification / message 组件,当在 js中使用时,该插件不会执行自动导入能力(样式不会被导入)

最终效果是:message.success('xx')可以创建 DOM元素,但是没有相关样式代码

因为该插件的设计原理是根据 vue template 模板中的组件使用进行处理的,函数式调用时插件查询不到

解决方案:

  • 改用vite-plugin-style-import 插件
  • 手动全局引入 message组件样式,import 'ant-design-vue/es/message/style'
  • 在vue组件的 template中手动添加 <a-message /> 供插件索引依赖时使用

依赖按需自动导入

  • unplugin-auto-import

vue相关 defineComponentcomputedwatch等模块依赖根据使用,插件自动导入,你无需关心 import,直接使用即可

该插件默认支持:

  • vue
  • vue-router
  • vue-i18n
  • @vueuse/head
  • @vueuse/core
  • ...

当然你也可以自定义配置 unplugin-auto-import

用法如下:

import AutoImport from 'unplugin-auto-import/vite'

export default defineConfig({
  // ...
  plugins: [
    AutoImport({
      imports: [
        'vue',
        'vue-router',
        'vue-i18n',
        '@vueuse/head',
        '@vueuse/core',
      ],
      dts: 'src/auto-imports.d.ts',
    })
  ]
})

需要在src目录下添加 auto-imports.d.ts文件配合使用,该文件会被插件自动更新

最终效果为:

ref方法我们可以直接使用并有相应的TS语法提示,而不需要手动的去 import { ref } from 'vue'

自定义主题

自定义主题设置参考官方文档配置即可,两种常规方式

  1. 按需加载配合 webpack/vite loader属性修改变量
  2. 全量引入,配合 variables.less自定义样式覆盖框架主题样式

这里我们采用第一种方法通过loader配置配合按需加载食用

vite项目下,请手动安装 less,pnpm add less -D

css: {
  preprocessorOptions: {
    less: {
      modifyVars: { 'primary-color': 'red' },
      javascriptEnabled: true, // 这是必须的
    },
  },
}

注意:在使用了 unplugin-vue-components进行按需加载配置后,相关 less变量设置需要同步开启 importStyle: 'less'unplugin-vue-components issues 160

AntDesignVueResolver({ importStyle: 'less' }) // 这里很重要

mock数据

  • vite-plugin-mock 插件

vite plugin配置

viteMockServe({
  ignore: /^\_/,
  mockPath: 'mock',
  localEnabled: true,
  prodEnabled: false,
  // 开发环境无需关心
  // injectCode 只受prodEnabled影响
  // https://github.com/anncwb/vite-plugin-mock/issues/9
  // 下面这段代码会被注入 main.ts
  injectCode: `
      import { setupProdMockServer } from '../mock/_createProductionServer';

      setupProdMockServer();
      `,
})

根目录下创建 _createProductionServer.ts文件

import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer';

// 批量加载
const modules = import.meta.globEager('./**/*.ts');

const mockModules: any[] = [];
Object.keys(modules).forEach((key) => {
  if (key.includes('/_')) {
    return;
  }
  mockModules.push(...modules[key].default);
});

/**
 * Used in a production environment. Need to manually import all modules
 */
export function setupProdMockServer() {
  createProdMockServer(mockModules);
}

这样mock目录下的非 _开头文件都会被自动加载成mock文件

如:

import Mock from 'mockjs';

const data = Mock.mock({
  'items|30': [
    {
      id: '@id',
      title: '@sentence(10, 20)',
      account: '@phone',
      true_name: '@name',
      created_at: '@datetime',
      role_name: '@name',
    },
  ],
});

export default [
  {
    url: '/table/list',
    method: 'get',
    response: () => {
      const items = data.items;
      return {
        code: 0,
        result: {
          total: items.length,
          list: items,
        },
      };
    },
  },
];

配置好代理直接请求 /api/table/list 就可以得到数据了

Proxy代理

import proxy from './config/vite/proxy';

export default defineConfig({
  // server
  server: {
    hmr: { overlay: false }, // 禁用或配置 HMR 连接 设置 server.hmr.overlay 为 false 可以禁用服务器错误遮罩层
    // 服务配置
    port: VITE_PORT, // 类型: number 指定服务器端口;
    open: false, // 类型: boolean | string在服务器启动时自动在浏览器中打开应用程序;
    cors: false, // 类型: boolean | CorsOptions 为开发服务器配置 CORS。默认启用并允许任何源
    host: '0.0.0.0', // 支持从IP启动访问
    proxy,
  },
})

proxy 如下

import {
  API_BASE_URL,
  API_TARGET_URL,
} from '../../config/constant';
import { ProxyOptions } from 'vite';

type ProxyTargetList = Record<string, ProxyOptions>;

const ret: ProxyTargetList = {
  // test
  [API_BASE_URL]: {
    target: API_TARGET_URL,
    changeOrigin: true,
    rewrite: (path) => path.replace(new RegExp(`^${API_BASE_URL}`), ''),
  },
  // mock
  // [MOCK_API_BASE_URL]: {
  //   target: MOCK_API_TARGET_URL,
  //   changeOrigin: true,
  //   rewrite: (path) => path.replace(new RegExp(`^${MOCK_API_BASE_URL}`), '/api'),
  // },
};

export default ret;

环境变量 .env

我这边是把系统配置放到 config/constant.ts 管理了

为了方便管理不同环境的接口和参数配置,可以使用环境变量 .env,如.env.development.env.production

配合 vite + import.meta.env使用还是很方便的

详细使用方式见vite + vue3 + ts 使用总结

包依赖分析可视化

插件:rollup-plugin-visualizer

import visualizer from 'rollup-plugin-visualizer';

visualizer({
  filename: './node_modules/.cache/visualizer/stats.html',
  open: true,
  gzipSize: true,
  brotliSize: true,
})

代码压缩

插件:vite-plugin-compression

import compressPlugin from 'vite-plugin-compression';

compressPlugin({
  ext: '.gz',
  deleteOriginFile: false,
})

Chunk 拆包

如果想把类似 ant-design-vue这样的包依赖单独拆分出来,也可以手动配置 manualChunks属性

// vite.config.ts
build: {
  rollupOptions: {
    output: {
      manualChunks: configManualChunk
    }
  }
}
// optimizer.ts
const vendorLibs: { match: string[]; output: string }[] = [
  {
    match: ['ant-design-vue'],
    output: 'antdv',
  },
  {
    match: ['echarts'],
    output: 'echarts',
  },
];

export const configManualChunk = (id: string) => {
  if (/[\\/]node_modules[\\/]/.test(id)) {
    const matchItem = vendorLibs.find((item) => {
      const reg = new RegExp(`[\\/]node_modules[\\/]_?(${item.match.join('|')})(.*)`, 'ig');
      return reg.test(id);
    });
    return matchItem ? matchItem.output : null;
  }
};

兼容处理

插件:@vitejs/plugin-legacy

兼容不支持 <script type="module">特性的浏览器,或 IE浏览器

// Native ESM
legacy({
  targets: ['defaults', 'not IE 11']
})

// IE11
// 需要 regenerator-runtime
legacy({
  targets: ['ie >= 11'],
  additionalLegacyPolyfills: ['regenerator-runtime/runtime']
})

效果图

首页

vite-vue3-4


包依赖分析可视化,部分截图

vite-vue3-2


开启压缩、开启兼容后生产打包的产物

vite-vue3-1

路由和布局

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

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

// main.ts
app.use(router); // 挂载后可全局使用实列,如模板中 <div @click="$router.push('xx')"></div>

用法如下:

// router.config.ts
import BasicLayout from '/@/layouts/BasicLayout/index.vue'; // 基本布局
import BlankLayout from '/@/layouts/BlankLayout.vue'; // 空布局
import type { RouteRecordRaw } from 'vue-router';

const routerMap: RouteRecordRaw[] = [
  {
    path: '/app',
    name: 'index',
    component: BasicLayout,
    redirect: '/app/home',
    meta: { title: '首页' },
    children: [
      {
        path: '/app/home',
        component: () => import('/@/views/home/index.vue'),
        name: 'home',
        meta: {
          title: '首页',
          icon: 'liulanqi',
          auth: ['home'],
        },
      },
      {
        path: '/app/others',
        name: 'others',
        component: BlankLayout,
        redirect: '/app/others/about',
        meta: {
          title: '其他菜单',
          icon: 'xitongrizhi',
          auth: ['others'],
        },
        children: [
          {
            path: '/app/others/about',
            name: 'about',
            component: () => import('/@/views/others/about/index.vue'),
            meta: { title: '关于', keepAlive: true, hiddenWrap: true },
          },
          {
            path: '/app/others/antdv',
            name: 'antdv',
            component: () => import('/@/views/others/antdv/index.vue'),
            meta: { title: '组件', keepAlive: true, breadcrumb: true },
          },
        ],
      },
    ]
  }
  ...
]

权限

  • 支持页面和菜单级别的权限管理、路由管理
  • 支持按钮级别的权限管理
  • 支持接口级别的权限管理

几个关键词:router.addRoutes动态路由、v-auth指令、axios拦截

使用 router.beforeEach 全局路由钩子

核心逻辑如下,详情见仓库代码 router/permission.ts

// 没有获取,请求数据
await permissioStore.fetchAuths();
// 过滤权限路由
const routes = await permissioStore.buildRoutesAction();
// 404 路由一定要放在 权限路由后面
routes.forEach((route) => {
  router.addRoute(route);
});
// hack 方法
// 不使用 next() 是因为,在执行完 router.addRoute 后,
// 原本的路由表内还没有添加进去的路由,会 No match
// replace 使路由从新进入一遍,进行匹配即可
next({ ...to, replace: true });

使用v-auth指令控制按钮级别的权限

function isAuth(el: Element, binding: any) {
  const { hasPermission } = usePermission();

  const value = binding.value;
  if (!value) return;
  if (!hasPermission(value)) {
    el.parentNode?.removeChild(el);
  }
}

axios拦截

axios请求拦截器 interceptors.request.use 添加

// 接口权限拦截
const store = usePermissioStoreWithOut();
const { url = '' } = config;
if (!WhiteList.includes(url) && store.getIsAdmin === 0) {
  if (!store.getAuths.includes(url)) {
    return Promise.reject('没有操作权限');
  }
}

总结

在开始使用 vite + vue3的时候,也是边踩坑边学习开发的过程,好在现在社区比较活跃,很多问题都有对应的解决方案,配合文档和github issue一起食用基本ok,该项目也是参考了 vue-vben-admin的一些实现和代码管理,本文作为 vue3使用学习记录~

使用过之后会发现 vue3vue2有着完全不同的开发体验,现在的 vue3TS有着极好的支持,开发效率和质量上上升了一个层次啊,而且也支持 JSX语法,类似 React的形式开发也是可行的,当然,配合 vue模板使用时,也有着极大的灵活性,可自行根据场景定制自己的代码,在结合目前的 script setup开发,直接爽到起飞呀~

在使用 vue3composition api开发模式时,一定要摒弃之前的 options api的开发逻辑,配和 hooks可以自由组合拆分代码,灵活性极高,方便维护管理,不会再出现 vue2时代的整个代码都拧在一起的情况

一句话:vite + vue3 + setup + ts + vscode volar 插件,谁用谁知道,爽的一批~

仓库地址:github.com/JS-banana/v…

emoji

参考