vite + vue3 + setup + pinia + ts 項目實戰(zhàn)

介紹

一個使用 vite + vue3 + pinia + ant-design-vue + typescript 完整技術(shù)路線開發(fā)的項目,秒級開發(fā)更新啟動、新的vue3 composition api 結(jié)合 setup縱享絲滑般的開發(fā)體驗、全新的 pinia狀態(tài)管理器和優(yōu)秀的設計體驗(1k的size)、antd無障礙過渡使用UI組件庫 ant-design-vue、安全高效的 typescript類型支持、代碼規(guī)范驗證、多級別的權(quán)限管理~

前言

前兩天接到了一個需求,就是把原來的一個項目的主要功能模塊和用戶模塊權(quán)限系統(tǒng)抽出來做一個新后臺項目,并迭代新增一些新功能,看起來好像也沒啥東西

拿到源碼看了下項目,好家伙,原項目是個微應用項目,主應用用戶模塊是react技術(shù)棧,子應用模塊是vue2技術(shù)棧,這直接 CV大法看樣子是不行了??,我這要做的畢竟是個單頁面應用,確定一個技術(shù)路線即可,具體看下代碼邏輯并跑起來看看

跑起來試了下,兩個項目基本都是1分鐘左右啟動,看代碼vue項目整個業(yè)務邏輯代碼都擰在一塊寫了

想到之前問老大要源碼的時候,說那個是老項目了,重新搭一個寫應該會快點

這話沒毛病啊,話不多說,直接開整,這次直接上 vite + vue3

emoji

特性

  • ?腳手架工具:高效、快速的 Vite
  • ??前端框架:眼下最時髦的 Vue3
  • ??狀態(tài)管理器:vue3新秀 Pinia,猶如 react zustand般的體驗,友好的api和異步處理
  • ??開發(fā)語言:政治正確 TypeScript
  • ??UI組件:antd開發(fā)者無障礙過渡使用 ant-design-vue,熟悉的配方熟悉的味道
  • ??css樣式:lesspostcss
  • ??代碼規(guī)范:EslintPrettierCommitlint
  • ??權(quán)限管理:頁面級、菜單級、按鈕級、接口級
  • ?依賴按需加載:unplugin-auto-import,可自動導入使用到的vuevue-router等依賴
  • ??組件按需導入:unplugin-vue-components,無論是第三方UI組件還是自定義組件都可實現(xiàn)自動按需導入以及TS語法提示

項目目錄

├── .husky                              // husky git hooks配置目錄
    ├── _                               // husky 腳本生成的目錄文件
    ├── commit-msg                      // commit-msg鉤子,用于驗證 message格式
    ├── pre-commit                      // pre-commit鉤子,主要是和eslint配合
├── config                              // 全局配置文件
    ├── vite                            // vite 相關(guān)配置
    ├── constant.ts                     // 項目配置
    ├── themeConfig.ts                  // 主題配置
├── dist                                // 默認的 build 輸出目錄
├── mock                                // 前端數(shù)據(jù)mock
├── public                              // vite項目下的靜態(tài)目錄
└── src                                 // 源碼目錄
    ├── api                             // 接口相關(guān)
    ├── assets                          // 公共的文件(如image、css、font等)
    ├── components                      // 項目組件
    ├── directives                      // 自定義 指令
    ├── enums                           // 自定義 常量(枚舉寫法)
    ├── hooks                           // 自定義 hooks
    ├── layout                          // 全局布局
    ├── router                          // 路由
    ├── store                           // 狀態(tài)管理器
    ├── 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格式規(guī)范
├── .env                                // 環(huán)境變量
├── .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

開發(fā)

項目初始化

如果使用vscode編輯器開發(fā)vue3,請務必安裝Volar插件與vue3配合使用更佳(與原本的Vetur不兼容)

使用 vite cli 快速創(chuàng)建項目

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

安裝相關(guān)依賴

推薦使用新一代 pnpm 包管理工具,性能和速度以及 node_modules依賴管理都很優(yōu)秀

建議配合 .npmrc 配置使用

# 提升一些依賴包至 node_modules
# 解決部分包模塊not found的問題
# 用于配合 pnpm
shamefully-hoist = true

# node-sass 下載問題
# sass_binary_site="https://npm.taobao.org/mirrors/node-sass/"

代碼規(guī)范

工具:huskyeslintprettier

具體使用方式,網(wǎng)上很多,我在之前另一篇文章也有說過,這里不再贅述~

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

主要就是自動化的概念,在一個合適的時機完成規(guī)定的事

  • 結(jié)合VsCode編輯器(保存時自動執(zhí)行格式化:editor.formatOnSave: true
  • 配合Git hooks鉤子(commit前或提交前執(zhí)行:pre-commit => npm run lint:lint-staged

注意

針對不同系統(tǒng) 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 支持
}

狀態(tài)管理器 Pinia

vue新一代狀態(tài)管理器,用過 react zustand的同學應該會有很熟悉的感覺

Pinia是一個圍繞Vue 3 Composition API的封裝器。因此,你不必把它作為一個插件來初始化,除非你需要Vue devtools支持、SSR支持和webpack代碼分割的情況

  • 非常輕量化,僅有 1 KB
  • 直觀的API使用,符合直覺,易于學習
  • 模塊化設計,便于拆分狀態(tài)
  • 全面的TS支持
// ... 引入相關(guān)依賴

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

interface UserState {
  userInfo: Nullable<IUserInfoProps>;
}

// 創(chuàng)建 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 類型推斷、異步函數(shù)使用都很方便
import { useHomeStore } from '/@/store/modules/home';

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

onMounted(async () => {
  await store.fetchInfo(); // 異步函數(shù)
  // ...
});

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加載器,也沒關(guān)系,也支持自定義配置

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

另一強悍功能:該插件不僅支持UI框架組件的按需導入,也支持項目組件的自動按需導入

具體表現(xiàn)就是:如我們使用 ant-design-vue的 Card組件或我們自己定義的 components/Icon 等其他組件時,我們不用導入,直接用即可,插件會為我們自動按需導入,結(jié)合 TS語法提示,開發(fā)效率杠杠的~

配置如下:

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中使用時,該插件不會執(zhí)行自動導入能力(樣式不會被導入)

最終效果是:message.success('xx')可以創(chuàng)建 DOM元素,但是沒有相關(guān)樣式代碼

因為該插件的設計原理是根據(jù) vue template 模板中的組件使用進行處理的,函數(shù)式調(diào)用時插件查詢不到

解決方案:

  • 改用vite-plugin-style-import 插件
  • 手動全局引入 message組件樣式,import 'ant-design-vue/es/message/style'
  • 在vue組件的 template中手動添加 <a-message /> 供插件索引依賴時使用

依賴按需自動導入

  • unplugin-auto-import

vue相關(guān) defineComponentcomputedwatch等模塊依賴根據(jù)使用,插件自動導入,你無需關(guān)心 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'

自定義主題

自定義主題設置參考官方文檔配置即可,兩種常規(guī)方式

  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進行按需加載配置后,相關(guān) less變量設置需要同步開啟 importStyle: 'less'unplugin-vue-components issues 160

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

mock數(shù)據(jù)

  • vite-plugin-mock 插件

vite plugin配置

viteMockServe({
  ignore: /^\_/,
  mockPath: 'mock',
  localEnabled: true,
  prodEnabled: false,
  // 開發(fā)環(huán)境無需關(guān)心
  // injectCode 只受prodEnabled影響
  // https://github.com/anncwb/vite-plugin-mock/issues/9
  // 下面這段代碼會被注入 main.ts
  injectCode: `
      import { setupProdMockServer } from '../mock/_createProductionServer';

      setupProdMockServer();
      `,
})

根目錄下創(chuàng)建 _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 就可以得到數(shù)據(jù)了

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 為開發(fā)服務器配置 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;

環(huán)境變量 .env

我這邊是把系統(tǒng)配置放到 config/constant.ts 管理了

為了方便管理不同環(huán)境的接口和參數(shù)配置,可以使用環(huán)境變量 .env,如 .env、.env.local、.env.development、.env.production

配合 dotenv庫 使用還是很方便的

包依賴分析可視化

插件: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

包依賴分析可視化,部分截圖

[圖片上傳失敗...(image-1df5e5-1642403544292)]


開啟壓縮、開啟兼容后生產(chǎn)打包的產(chǎn)物

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: '關(guān)于', keepAlive: true, hiddenWrap: true },
          },
          {
            path: '/app/others/antdv',
            name: 'antdv',
            component: () => import('/@/views/others/antdv/index.vue'),
            meta: { title: '組件', keepAlive: true, breadcrumb: true },
          },
        ],
      },
    ]
  }
  ...
]

權(quán)限

  • 支持頁面和菜單級別的權(quán)限管理、路由管理
  • 支持按鈕級別的權(quán)限管理
  • 支持接口級別的權(quán)限管理

幾個關(guān)鍵詞:router.addRoutes動態(tài)路由、v-auth指令、axios攔截

使用 router.beforeEach 全局路由鉤子

核心邏輯如下,詳情見倉庫代碼 router/permission.ts

// 沒有獲取,請求數(shù)據(jù)
await permissioStore.fetchAuths();
// 過濾權(quán)限路由
const routes = await permissioStore.buildRoutesAction();
// 404 路由一定要放在 權(quán)限路由后面
routes.forEach((route) => {
  router.addRoute(route);
});
// hack 方法
// 不使用 next() 是因為,在執(zhí)行完 router.addRoute 后,
// 原本的路由表內(nèi)還沒有添加進去的路由,會 No match
// replace 使路由從新進入一遍,進行匹配即可
next({ ...to, replace: true });

使用v-auth指令控制按鈕級別的權(quán)限

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 添加

// 接口權(quán)限攔截
const store = usePermissioStoreWithOut();
const { url = '' } = config;
if (!WhiteList.includes(url) && store.getIsAdmin === 0) {
  if (!store.getAuths.includes(url)) {
    return Promise.reject('沒有操作權(quán)限');
  }
}

總結(jié)

在開始使用 vite + vue3的時候,也是邊踩坑邊學習開發(fā)的過程,好在現(xiàn)在社區(qū)比較活躍,很多問題都有對應的解決方案,配合文檔和github issue一起食用基本ok,該項目也是參考了 vue-vben-admin的一些實現(xiàn)和代碼管理,本文作為 vue3使用學習記錄~

使用過之后會發(fā)現(xiàn) vue3vue2有著完全不同的開發(fā)體驗,現(xiàn)在的 vue3TS有著極好的支持,開發(fā)效率和質(zhì)量上上升了一個層次啊,而且也支持 JSX語法,類似 React的形式開發(fā)也是可行的,當然,配合 vue模板使用時,也有著極大的靈活性,可自行根據(jù)場景定制自己的代碼,在結(jié)合目前的 script setup開發(fā),直接爽到起飛呀~

在使用 vue3composition api開發(fā)模式時,一定要摒棄之前的 options api的開發(fā)邏輯,配和 hooks可以自由組合拆分代碼,靈活性極高,方便維護管理,不會再出現(xiàn) vue2時代的整個代碼都擰在一起的情況

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

倉庫地址:https://github.com/JS-banana/vite-vue3-ts

emoji

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容