浅尝Vite

接上一篇Webpack优化之后,现在和小伙伴们一起来聊一聊Vite,聊的主要内容是为什么选用Vite和Vite的使用规则。

我第一次使用Vite是去年4月份的时候,当时上海正在封控中,同时公司开始筹备做一个CMS系统。经过多轮的项目评审和调研后,最后大家一拍即合决定用Vue3+Vite+Typescript+Element-plus+Pinia。之所以选择这样的技术栈原因如下:

  1. 历史项目上分析,公司现存的两个项目都在用Vue2+Webpack构建,但是随着后期功能的不断增加,打包速度变的会越来越慢,在现有功能上由于类型的不规范定义,维护起来也很吃力。
  2. 对比Vite和Webpack的优缺点,这里先说说这两者的打包原理,webpack执行过程是:根据配置查找入口>逐层识别依赖>分析/转换/编译/输出代码>打包后的代码,其原理是从入口文件开始,逐层递归识别依赖,构建依赖图谱,转换成AST抽象树,处理代码,最终转换成浏览器能识别的代码。Vite的原理是基于浏览器的ES Module特性,碰见import就会发送一个HTTP请求去加载文件。Vite启动一个connect 服务器拦截这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再以ESM格式返回返回给浏览器。整个过程中没有对文件进行打包编译。结合它们两者的工作原理,得出Vite的优点是:启动快热更新快
  3. Vue3语法结合TypeScript规范,可以构建易维护、可扩展的项目。

接下来说说如何一步步开始搭建项目。

  • 使用Vite创建项目
  • 配置根目录下的vite.config.ts文件,可以参考下面代码片段的一些基本且实用的配置。
ts 复制代码
//vite.config.ts
import { defineConfig, ConfigEnv, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import ElementPlus from 'unplugin-element-plus/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import OptimizationPersist from 'vite-plugin-optimize-persist'
import PkgConfig from 'vite-plugin-package-config'
const path = require('path')
const envConfig = require('./env.config')
// https://vitejs.dev/config/
// @ts-ignore
export default defineConfig((mode: ConfigEnv) => {
  // @ts-ignore
  return {
    server: {
      host: '0.0.0.0', // resolve vite use `--host` to expose
      port: '8080',
      open: true
    },
    resolve: {
      alias: [
        {
          find: '@',
          replacement: resolve(__dirname, 'src')
        }
      ]
    },
    css: {
      // css预处理器
      preprocessorOptions: {
        scss: {
          // 引入 var.scss 这样就可以在全局中使用 var.scss中预定义的变量了
          additionalData: '@use "./src/styles/variables.scss" as *;'
        }
      }
    },
    build: {
      chunkSizeWarningLimit: true,
      brotliSize: false,
      rollupOptions: {
        output: {
          manualChunks(id) {
            // 将pinia的全局库实例打包进vendor,避免和页面一起打包造成资源重复引入
            if (id.includes(path.resolve(__dirname, '/src/store/index.ts'))) {
              return 'vendor'
            }
          }
        }
      }
    },
    plugins: [
      vue(),
      ElementPlus({
        // 引入的样式的类型,可以是css、sass、less等,
        importStyle: 'css',
        useSource: true
      }),
      AutoImport({
        resolvers: [ElementPlusResolver()]
      }),
      Components({
        resolvers: [ElementPlusResolver()]
      }),
      createSvgIconsPlugin({
        // 指定需要缓存的图标文件夹
        iconDirs: [resolve(process.cwd()), 'src/icons/svg'],
        // 指定格式
        symbolId: 'icon-[dir]-[name]'
      }),
      PkgConfig(),
      OptimizationPersist()
    ]
  }
})
  • 配置环境变量

这里以dev环境为例,其他环境如法炮制。首先在根目录下新建.env.dev文件,变量的定义需要以VITE_APP为前缀,参考如下代码片段。

.env.dev 复制代码
# 开发环境变量
VITE_APP_API_ADDRESS='//keeper-api-dev.***.ink/'
VITE_APP_STORAGE_PREFIX='***.cms.dev'

如何引用环境变量

ts 复制代码
//引用api_address环境变量
import.meta.env.VITE_APP_API_ADDRESS

最后在打包命令时配上相应的模式参数,这里的参数一一对应根目录下的env.mode文件配置。

json 复制代码
"scripts": {
  "dev": "vite",
  "build": "vue-tsc --noEmit && vite build",
  "build:dev": "vue-tsc --noEmit && vite build --mode dev",
  "build:test": "vue-tsc --noEmit && vite build --mode test",
  "build:pre": "vue-tsc --noEmit && vite build --mode pre",
  "build:prod": "vue-tsc --noEmit && vite build --mode production",
  "preview": "vite preview",
  "lint": "eslint . --ext .vue,.js,.ts,.jsx,.tsx --fix"
},
  • 安装和配置路由

安装

bash 复制代码
npm i vue-router@4

基本用法和配置和vue2项目没有太大差别,这里重点介绍路由安全守卫的逻辑,具体如下:

ts 复制代码
import router from '@/router'
import { getToken, setToken } from '@/utils/cookies'
import { whiteList } from '@/config/white-list'
import { userStore } from '@/store/modules/user'
import { permissionStore } from '@/store/modules/permission'
import { appStore } from '@/store/modules/app'
import tfrMessage from '@/utils/tfrMessage'
const $tfrMessage = tfrMessage

router.beforeEach(async (to, from, next) => {
  const useUserStore = userStore()
  const usePermissionStore = permissionStore()
  const useAppStore = appStore()
  const token = getToken()
  if (token) {
    if (whiteList.indexOf(to.path) !== -1) {
      next()
    } else {
      try {
        if (JSON.stringify(useUserStore.user) === '{}') {
          // 检查用户是否已获得权限角色
          await useUserStore.getInfoHttp()
          useAppStore.token = token // 刷新后从设置token
          usePermissionStore.setRoutes()
          usePermissionStore.dynamicRoutes.forEach(v => {
            router.addRoute(v)
          })
          next({ ...to, replace: true })
        } else {
          next()
        }
      } catch (e) {
        setToken('')
        next(`/login?redirect=${to.path}`)
      }
    }
  } else if (whiteList.indexOf(to.path) !== -1) {
    next()
  } else {
    $tfrMessage({
      message: 'Please Sign In',
      type: 'error'
    })
    next(`/login?redirect=${to.path}`)
  }
})

router.afterEach(() => {
  // NProgress.done()
})
  • 状态管理工具 Pinia

安装

bash 复制代码
npm i pinia

main.ts

ts 复制代码
import { createApp } from 'vue'
import { createPinia } from 'pinia'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)

store/modules/app.ts

js 复制代码
/**
 * 一般在容器中做这4件事
 *    1. 定义容器并导出
 *    2. 使用容器中的state
 *    3. 修改容器中的state
 *    4. 使用容器中的action
 */
import { defineStore } from 'pinia'
import getDevice from '@/utils/device'

/**
 * 1. 定义容器并导出
 * 参数一: 容器ID, 唯一, 将来 Pinia 会把所有的容器挂载到根容器
 * 参数二: 选项对象
 * 返回值: 函数, 调用的时候要空参调用, 返回容器实例
 */
export const appStore = defineStore({
  id: 'app',
  /**
   * 类似组件的 data, 用于存储全局的的状态
   * 注意:
   *    1.必须是函数, 为了在服务端渲染的时候避免交叉请求导致的数据交叉污染
   *    2.必须是箭头函数, 为了更好的 TS 类型推导
   */
  state: () => {
    return {
      device: getDevice(), // desktop ipad mobile
      token: ''
    }
  },
  /**
   * 类似组件的 computed, 用来封装计算属性, 具有缓存特性
   */
  getters: {},
  /**
   * 类似组件的 methods, 封装业务逻辑, 修改state
   * 注意: 里面的函数不能定义成箭头函数(函数体中会用到this)
   */
  actions: {}
})

layout/index.ts

ts 复制代码
<template>
  <div v-if="device !== 'mobile'" class="common-layout">
    <el-aside :width="`${menuWidth}`">
      <SideBar />
    </el-aside>
    <el-container :style="{ paddingLeft: menuWidth, height: '100%' }">
      <el-header :style="{ left: menuWidth }">
        <PlatformControl />
      </el-header>
      <el-main>
        <div class="main_content">
          <router-view />
        </div>
      </el-main>
    </el-container>
  </div>
  <div v-else class="common-layout">
    <MobileSideBar />
    <el-main :style="{ paddingTop: mobileMainPaddingTop + 'px' }">
      <router-view />
    </el-main>
  </div>
</template>

<script setup lang="ts">
import SideBar from '@/layout/component/sidebar/index.vue'
import MobileSideBar from '@/layout/component/sidebar/mobile.vue'
import PlatformControl from '@/components/PlatformControl/index.vue'
import { appStore } from '@/store/modules/app'
import { menuStore } from '@/store/modules/menu'
import { storeToRefs } from 'pinia'
const useMenuStore = menuStore()
// 通过storeToRefs转换为响应式对象解构可正常使用
const { menuWidth } = storeToRefs(menuStore())
const { mobileMainPaddingTop } = storeToRefs(menuStore())
const { device } = storeToRefs(appStore())
// useUserStore.setMenuWidth(routeName)
useMenuStore.setMenuWidth()

device.value === 'mobile' && useMenuStore.setMobileMainPaddingTop()
</script>

写在最后

到此项目搭建基本完成,期望能对大家有所帮助,不断学习,共同进步。参考资料:

Vite 官网 vitejs.dev/config/

Vue Router router.vuejs.org/zh/installa...

Pinia pinia.vuejs.org/zh/introduc...

项目地址 gitee.com/chongchonge...

相关推荐
正小安2 天前
Vite系列课程 | 11. Vite 配置文件中 CSS 配置(Modules 模块化篇)
前端·vite
学前端的小朱3 天前
Echarts实现大屏可视化
websocket·echarts·nodejs·vue3·vite·koa·cors
正小安5 天前
Vite 系列课程|1课程道路,2什么是构建工具
前端·vite
三天不学习7 天前
Vue3+Vite 环境变量和模式配置详解
前端·javascript·vue.js·vite·vue环境变量
潜心专研的小张同学8 天前
解决 Vite 项目启动时端口重复问题的总结
前端·vue.js·vite
沐晨爸爸9 天前
pinia和pinia-plugin-persistedstate的配置和用法
vue.js·vite
Java陈序员15 天前
一个开源免费中后台模版!
vue.js·typescript·vite
码上有料19 天前
Vite插件搞定图片预加载
前端·javascript·vite
前端界的CV大师19 天前
基于vue3+vite实现的文件在线预览功能【缝合怪】
前端·vue.js·vite