vite2 项目最佳实践

517 阅读5分钟

开始项目

vite2 主要变化

  • 配置项变化:vue特有选项、创建选项、css选项、jsx选项
  • 别名用法变化:不再需要 开始/结尾 处的斜线了
  • vue 支持:通过@vitejs/plugin-vue插件支持
  • react 支持
  • HMR API变化
  • 清单格式变化
  • 插件API重新设计

vue支持

和其他框架一视同仁,Vue的整合通过插件:@vitejs/plugin-vue实现

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

export default defineConfig({
  plugins: [vue()]
})

SFC定义默认使用setup script

<script setup>
import { ref } from 'vue'

defineProps({
  msg: String
})

const count = ref(0)
</script>

别名定义

不再需要像vite1一样在别名前后加上/

//vite.config.js
resolve:{
    alias: { 
      '@': path.resolve(__dirname, 'src'),
      'comps': path.resolve(__dirname, 'src/components')
    }
  }

App.vue中用一下试试

<script setup> 
import HelloWorld from 'comps/HelloWorld.vue'
</script>
  • vue3 setup script
    • defineProps 和 defineEmits:声明propsemits
    • defineExpose:暴露在当前 <script setup> 中声明的绑定,以便通过模板 ref 或者 $parent 链获取到的组件的公开实例可以访问
// HelloWorld.vue
defineProps({
  msg: String
})
const emit = defineEmits(['myclick'])
defineExpose({
  someMethod(){
    console.log('some message from helloworld')
  }
})

// App.vue
<HelloWorld msg="Hello Vue 3 + Vite" ref="hw" @myclick="onmyclick" />
<script setup>
import HelloWorld from 'comps/HelloWorld.vue'
import { ref } from 'vue'

const hw = ref(null)
const onmyclick = () => {
  console.log('myclick from helloworld')
  
  // HelloWorld中defineExpose 暴露
  hw.value.someMethod()
}
</script>

插件API重新设计

Vite 2 使用了一套完全重定义的,扩展了 Rollup 插件的接口。 插件开发指南

jsx支持

Vue 3 JSX 支持@vitejs/plugin-vue-jsx

npm i @vitejs/plugin-vue-jsx -D

仅限于单文件组件中解析 jsx 和 后缀名为jsx tsx的解析不是一回事

//vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'

export default defineConfig({
  plugins: [vue(),vueJsx()]
})

// Comp.vue
<script lang="jsx">
import { ref } from "vue" 
export default{
    setup(){
        const counter = ref(0)
        const onclick = ()=>{
            counter.value++
        }
        return () => (
            <>
                <div> Comp </div>
                <p onClick={onclick}>{counter.value}</p>
            </>
        )
    }
}
</script>

Mock插件应用

vite-plugin-mock

  • 插件安装
    • npm i mockjs -S
    • npm i vite-plugin-mock -D
    • 用到环境变量 :npm i cross-env
      • "dev": "cross-env NODE_ENV=development vite"
  • 配置vite.config.js
// vite.config.js
import { viteMockServe } from 'vite-plugin-mock' 
export default { 
    plugins: [ viteMockServe({ supportTs: false }) ] 
}

项目基础架构

路由 vue-router 4.x

  • 安装 vue-router 4.x
    • npm install vue-router@4

image.png

import { createRouter, createWebHashHistory } from "vue-router";

// 工厂函数来创建router实例
const router = createRouter({
    history: createWebHashHistory(),
    routes: [
        {
            path: '/',
            component: () => import('views/home.vue')
        }
    ]
});

export default router
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

createApp(App)
.use(router)
.mount('#app')

记得创建homg.vue,并修改App.vue
路由用法略有变化 ,vue-router v4.x

状态管理 vuex@next (vuex4.x)

  • 安装 vuex@next (vuex4.x)
    • npm install vuex@next --save
  • store配置store/index.js
import { createStore } from 'vuex';

export default createStore({
    state: {
        counter: 0
    },
    mutations:{
        add(state){
            state.counter++
        }
    }
});
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

createApp(App).use(router).use(store).mount('#app')

用法和以前基本一样,vuex 4.x

样式组织

  • 安装sass
    • npm i sass -D
  • styles目录保存各种样式
  • index.scss作为出口组织这些样式,同时编写一些全局样式
  • 最后在main.js导入
// vite.config.js 添加别名 styles
// 全局样式
import 'styles/index.scss'

UI库

花果山团队element3

  • 安装
    • npm i element3 -S
  • 完整引入,main.js
import Element3 from 'element3'
import 'element3/lib/theme-chalk/index.css'

createApp(App).use(Element3)
  • 按需引入,main.js
import "element3/lib/theme-chalk/button.css"; 
import { ElButton } from "element3"
createApp(App).use(ElButton)
// 完整引入element3
// import Element3 from 'element3'
// import 'element3/lib/theme-chalk/index.css'

// 按需加载
import { ElButton,ElInput } from "element3"
import 'element3/lib/theme-chalk/button.css'
import 'element3/lib/theme-chalk/input.css'

export default function(app){
    // 完整
    // app.use(Element3)

    // 按需引入
    app.use(ElButton)
    app.use(ElInput)
}

基础布局

基础布局页,将来每个页面以布局页为父页面即可:

布局页面 layouts/index.vue

  • 侧边栏 layouts/components/Sidebar/index.vue
  • 右侧内容容器
    • 顶部导航栏 layouts/components/Navbar.vue
      • 面包屑 layouts/components/Breadcrumb.vue
      • 右侧下拉菜单
    • 内容区 layouts/components/AppMain.vue

动态导航

侧边导航 sidebar

根据路由表动态生成侧边导航菜单

  • 创建侧边栏组件,递归输出routes中配置的多级菜单
<template>
  <el-scrollbar wrap-class="scrollbar-wrapper">
    <el-menu
      :default-active="activeMenu"
      :background-color="variables.menuBg"
      :text-color="variables.menuText"
      :unique-opened="false"
      :active-text-color="variables.menuActiveText"
      mode="vertical"
    >
      <sidebar-item
        v-for="route in routes"
        :key="route.path"
        :item="route"
        :base-path="route.path"
      />
    </el-menu>
  </el-scrollbar>
</template>

<script setup>
import variables from "styles/variables.module.scss";
import SidebarItem from "./SidebarItem.vue";
import { computed } from "@vue/runtime-core";
import { useRoute } from "vue-router";
import { routes } from "@/router";

const activeMenu = computed(() => {
  const route = useRoute();
  console.log(route);
  const { meta, path } = route;
  if (meta.activeMenu) {
    return meta.activeMenu;
  }
  return path;
});
</script>

sass文件导出变量解析需要用到css module,因此variables文件要加上module中缀,variables.module.scss 导出以便可以js访问变量值

  • 相关样式文件:

    • styles/variables.module.scss
    • styles/sidebar.scss
    • styles/index.scss 中引入
  • SidebarItem组件

    • 父菜单(含子路由)
    • 导航链接

面包屑

通过路由匹配数组可以动态生成面包屑。 面包屑组件,src\layouts\components\Breadcrumb.vue

数据封装

统一封装数据请求服务,有利于解决一下问题:

  • 统一配置请求
  • 请求、响应统一处理

准备工作:

  • 安装axiosnpm i axios -S
  • 添加配置文件:环境变量 .env.development VITE_BASE_API=/api
  • 请求封装,utils/request.js
import axios from "axios";
import { Message, Msgbox } from "element3";
import store from "@/store";

// 创建axios实例
const service = axios.create({
  // 在请求地址前面加上baseURL
  baseURL: import.meta.env.VITE_BASE_API,
  // 当发送跨域请求时携带cookie
  // withCredentials: true,
  timeout: 5000,
});

// 请求拦截
service.interceptors.request.use(
  (config) => {
    // 指定请求令牌
    // if (store.getters.token) {
    // // 自定义令牌的字段名为X-Token,根据咱们后台再做修改
    // config.headers["X-Token"] = store.getters.token;
    // }
    config.headers["X-Token"] = "my token";
    return config;
  },
  (error) => {
    // 请求错误的统一处理
    console.log(error); // for debug
    return Promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
  /**
   * If you want to get http information such as headers or status
   * Please return  response => response
   */

  /**
   * 通过判断状态码统一处理响应,根据情况修改
   * 同时也可以通过HTTP状态码判断请求结果
   */
  (response) => {
    const res = response.data;

    // 如果状态码不是20000则认为有错误
    if (res.code !== 20000) {
      Message.error({
        message: res.message || "Error",
        duration: 5 * 1000,
      });

      // 50008: 非法令牌; 50012: 其他客户端已登入; 50014: 令牌过期;
      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
        // 重新登录
        Msgbox.confirm("您已登出, 请重新登录", "确认", {
          confirmButtonText: "重新登录",
          cancelButtonText: "取消",
          type: "warning",
        }).then(() => {
          store.dispatch("user/resetToken").then(() => {
            location.reload();
          });
        });
      }
      return Promise.reject(new Error(res.message || "Error"));
    } else {
      return res;
    }
  },
  (error) => {
    console.log("err" + error); // for debug
    Message({
      message: error.message,
      type: "error",
      duration: 5 * 1000,
    });
    return Promise.reject(error);
  }
);

export default service;

业务处理

结构化数据展示

使用el-table展示结构化数据,配合el-pagination做数据分页。

文件组织结构如下:

  • list.vue展示列表
  • edit.vuecreate.vue编辑或创建
  • 内部复用detail.vue处理
  • model中负责数据业务处理

list.vue中的数据展示

<el-table
v-loading="loading"
:data="list"
border
fit
highlight-current-row
style="width: 100%"
>
    <el-table-column align="center" label="ID" prop="id"></el-table-column>
    <el-table-column align="center" label="账户名" prop="name">
    </el-table-column>
    <el-table-column align="center" label="年龄" prop="age">
    </el-table-column>
</el-table>

listloading数据的获取逻辑,可以使用compsition-api提取到userModel.js

export function useList() {
  // 列表数据
  const state = reactive({
    loading: true, // 加载状态
    list: [], // 列表数据
    total: 0,
    listQuery: {
      page: 1,
      limit: 5,
    },
  });

  // 获取列表
  function getList() {
    state.loading = true;

    return request({
      url: "/getUsers",
      method: "get",
      params: state.listQuery,
    })
      .then(({ data, total }) => {
        // 设置列表数据
        state.list = data;
        state.total = total;
      })
      .finally(() => {
        state.loading = false;
      });
  }

  // 删除项
  function delItem(id) {
    state.loading = true;

    return request({
      url: "/deleteUser",
      method: "get",
      params: { id },
    }).finally(() => {
      state.loading = false;
    });
  }

  // 首次获取数据
  getList();

  return { state, getList, delItem };
}

list.vue中使用:

import { useList } from "./model/userModel";
const { state, getList, delItem } = useList();

分页处理,list.vue

<pagination
      v-show="total > 0"
      :total="total"
      v-model:page="listQuery.page"
      v-model:limit="listQuery.limit"
      @pagination="getList"
    ></pagination>

分页需要的数据也在userModel中处理

const state = reactive({
    total: 0,
    listQuery: {
      page: 1,
      limit: 5,
    },
  });

表单处理

用户数据新增、编辑使用el-form处理 用一个组件detail.vue来处理,区别在于初始化时是否获取信息回填到表单。 detail.vue

<el-form ref="form" :model="model" :rules="rules">
      <el-form-item prop="name" label="用户名">
        <el-input v-model="model.name"></el-input>
      </el-form-item>
      <el-form-item prop="age" label="用户年龄">
        <el-input v-model.number="model.age"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button @click="submitForm" type="primary">提交</el-button>
      </el-form-item>
    </el-form>

数据处理同样可以提取到userModel中处理。

export function useItem(isEdit, id) {
  const model = ref(Object.assign({}, defaultData));

  // 初始化时,根据isEdit判定是否需要获取玩家详情
  onMounted(() => {
    if (isEdit && id) {
      // 获取玩家详情
      request({
        url: "/getUser",
        method: "get",
        params: { id },
      }).then(({ data }) => {
        model.value = data;
      });
    }
  });

  const updateUser = () => {
    return request({
      url: "/updateUser",
      method: "post",
      data: model.value,
    });
  };

  const addUser = () => {
    return request({
      url: "/addUser",
      method: "post",
      data: model.value,
    });
  };

  return { model, updateUser, addUser };
}

源码