4

2021必知必会的vite+vue3项目最佳实践

 3 years ago
source link: https://zhuanlan.zhihu.com/p/357864871
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

2021必知必会的vite+vue3项目最佳实践

关注微信公众号:web前端学习圈,领取85G前端全套系统教程

2021学什么

2021第一更是尤大的Vite2,全新插件架构,丝滑的开发体验,和Vue3的完美结合。 2021年第一弹,村长将以Vite2+Vue3为主题开启大家的前端学习之旅。

v2-2f1937ba4ce417b757b3fdc4bdb45312_720w.jpg

vue3+vite2项目中常见任务实践:

  • 基建:配置、lint、测试、样式组织、服务封装、数据mock、UI库整合、路由、状态管理等
  • 常见业务编写和代码组织

创建Vite2项目

闲言碎语不必说,下面我们表一表好汉vite2

使用npm:

$ npm init @vitejs/app

按提示指定项目名称和模板,或直接指定

$ npm init @vitejs/app my-vue-app --template vue

Vite2主要变化

  • 配置选项变化:vue特有选项、创建选项、css选项、jsx选项等、别名行为变化:不再要求/开头或结尾
  • Vue支持:通过 @vitejs/plugin-vue插件支持
  • React支持
  • HMR API变化
  • 清单格式变化
  • 插件API重新设计

Vue支持

Vue的整合也通过插件实现,和其他框架一视同仁:

SFC定义默认使用setup script,语法比较激进,但更简洁,好评!

别名定义

不再需要像vite1一样在别名前后加上/,这和webpack项目配置可以保持一致便于移植,好评!

import path from 'path'

export default {
  alias: {
    "@": path.resolve(__dirname, "src"),
    "comps": path.resolve(__dirname, "src/components"),
  },
}

App.vue里面用一下试试

<script setup>
import HelloWorld from 'comps/HelloWorld.vue'
</script>

插件API重新设计

Vite2主要变化在插件体系,这样更标准化、易扩展。Vite2插件API扩展自Rollup插件体系,因此能兼容现存的Rollup插件,编写的Vite插件也可以同时运行于开发和创建,好评!

插件编写我会另开专题讨论,欢迎大家关注我。

Vue3 Jsx支持

vue3jsx支持需要引入插件:@vitejs/plugin-vue-jsx

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

注册插件,vite.config.js

import vueJsx from "@vitejs/plugin-vue-jsx";

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

用法也有要求,改造一下App.vue

<!-- 1.标记为jsx -->
<script setup lang="jsx">
import { defineComponent } from "vue";
import HelloWorld from "comps/HelloWorld.vue";
import logo from "./assets/logo.png"

// 2.用defineComponent定义组件且要导出
export default defineComponent({
  render: () => (
    <>
      <img alt="Vue logo" src={logo} />
      <HelloWorld msg="Hello Vue 3 + Vite" />
    </>
  ),
});
</script>

Mock插件应用

之前给大家介绍的vite-plugin-mock已经重构支持了Vite2。

npm i mockjs -S
npm i vite-plugin-mock cross-env -D

配置,vite.config.js

import { viteMockServe } from 'vite-plugin-mock'

export default {
  plugins: [ viteMockServe({ supportTs: false }) ]
} 

设置环境变量,package.json

{
  "scripts": {
    "dev": "cross-env NODE_ENV=development vite",
    "build": "vite build"
  },
}  

基础配置、样式处理、lint、测试、打包发布等参见我上一篇文章:vite工程化实践

项目基础架构

路由

安装vue-router 4.x

npm i vue-router@next -S 

路由配置,router/index.js

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

const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    { path: '/', component: () => import('views/home.vue') }
  ]
});

export default router

引入,main.js

import router from "@/router";
createApp(App).use(router).mount("#app"); 

别忘了创建home.vue并修改App.vue
路由用法略有变化,村长的视频教程

状态管理

安装vuex 4.x

npm i vuex@next -S 

Store配置,store/index.js

import {createStore} from 'vuex';

export default createStore({
  state: {
    couter: 0
  }
});

引入,main.js

import store from "@/store";
createApp(App).use(store).mount("#app"); 

用法和以前基本一样,村长的视频教程

样式组织

安装sass

npm i sass -D

styles目录保存各种样式

index.scss作为出口组织这些样式,同时编写一些全局样式

最后在main.js导入

import "styles/index.scss";

注意在vite.config.js添加styles别名

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) 

抽取成插件会更好,plugins/element3.js

// 完整引入
import element3 from "element3";
import "element3/lib/theme-chalk/index.css";

// 按需引入
// import { ElButton } from "element3";
// import "element3/lib/theme-chalk/button.css";

export default function (app) {
  // 完整引入
  app.use(element3)

  // 按需引入
  // app.use(ElButton);
}
<el-button>my button</el-button>

基础布局

我们应用需要一个基本布局页,类似下图,将来每个页面以布局页为父页面即可:

布局页面,layout/index.vue

<template>
  <div class="app-wrapper">
    <!-- 侧边栏 -->
    <div class="sidebar-container"></div>
    <!-- 内容容器 -->
    <div class="main-container">
      <!-- 顶部导航栏 -->
      <navbar />
      <!-- 内容区 -->
      <app-main />
    </div>
  </div>
</template>

<script setup>
import AppMain from "./components/AppMain.vue";
import Navbar from "./components/Navbar.vue";
</script>

<style lang="scss" scoped>
@import "../styles/mixin.scss";

.app-wrapper {
  @include clearfix;
  position: relative;
  height: 100%;
  width: 100%;
}
</style>

别忘了创建AppMain.vueNavbar.vue

路由配置,router/index.js

{
  path: "/",
	component: Layout,
  children: [
    {
      path: "",
      component: () => import('views/home.vue'),
      name: "Home",
      meta: { title: "首页", icon: "el-icon-s-home" },
    },
  ],
},

动态导航

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

首先创建侧边栏组件,递归输出routes中的配置为多级菜单,layout/Sidebar/index.vue

<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 SidebarItem from "./SidebarItem.vue";
import { computed } from "vue";
import { useRoute } from "vue-router";
import { routes } from "@/router";
import variables from "styles/variables.module.scss";

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

注意:sass文件导出变量解析需要用到css module,因此variables文件要加上module中缀。

添加相关样式:

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

创建SidebarItem.vue组件,解析当前路由是导航链接还是父菜单:

通过路由匹配数组可以动态生成面包屑。

面包屑组件,layouts/components/Breadcrumb.vue

<template>
  <el-breadcrumb class="app-breadcrumb" separator="/">
      <el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
        <span
          v-if="item.redirect === 'noRedirect' || index == levelList.length - 1"
          class="no-redirect"
          >{{ item.meta.title }}</span>
        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
      </el-breadcrumb-item>
  </el-breadcrumb>
</template>

<script setup>
import { compile } from "path-to-regexp";
import { reactive, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";

const levelList = ref(null);
const router = useRouter();
const route = useRoute();

const getBreadcrumb = () => {
  let matched = route.matched.filter((item) => item.meta && item.meta.title);

  const first = matched[0];
  if (first.path !== "/") {
    matched = [{ path: "/home", meta: { title: "首页" } }].concat(matched);
  }

  levelList.value = matched.filter(
    (item) => item.meta && item.meta.title && item.meta.breadcrumb !== false
  );
}

const pathCompile = (path) => {  
  var toPath = compile(path);
  return toPath(route.params);
}

const handleLink = (item) => {
  const { redirect, path } = item;
  if (redirect) {
    router.push(redirect);
    return;
  }
  router.push(pathCompile(path));
}

getBreadcrumb();
watch(route, getBreadcrumb)

</script>

<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
  display: inline-block;
  font-size: 14px;
  line-height: 50px;
  margin-left: 8px;

  .no-redirect {
    color: #97a8be;
    cursor: text;
  }
}
</style>

别忘了添加依赖:path-to-regexp
注意:vue-router4已经不再使用path-to-regexp解析动态path,因此这里后续还需要改进。

数据封装

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

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

准备工作:

  • 安装axios:
npm i axios -S

添加配置文件:.env.development

VITE_BASE_API=/api 

请求封装,utils/request.js

import axios from "axios";
import { Message, Msgbox } from "element3";

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

// 请求拦截
service.interceptors.request.use(
  (config) => {
    // 模拟指定请求令牌
    config.headers["X-Token"] = "my token";
    return config;
  },
  (error) => {
    // 请求错误的统一处理
    console.log(error); // for debug
    return Promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
  /**
   * 通过判断状态码统一处理响应,根据情况修改
   * 同时也可以通过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">
  <el-table-column label="ID" prop="id"></el-table-column>
  <el-table-column label="账户名" prop="name"></el-table-column>
  <el-table-column label="年龄" prop="age"></el-table-column>
</el-table>

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

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

  // 获取列表
  function getList() {
    state.loading = true;
    return request({
      url: "/getUsers",
      method: "get",
    }).then(({ data, total }) => {
      // 设置列表数据
      state.list = data;
    }).finally(() => {
      state.loading = false;
    });
  }
  
  // 首次获取数据
  getList();

  return { state, getList };
}

list.vue中使用

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

分页处理,list.vue

<pagination
      :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, // 每页条数
  },
});
request({
  url: "/getUsers",
  method: "get",
  params: state.listQuery, // 在查询中加入分页参数
})

表单处理

用户数据新增、编辑使用el-form处理

可用一个组件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;
      });
    }
  });
  return { model };
}

配套视频演示

本文配套演示视频,喜欢看视频的小伙伴看这里: 「备战2021」Vite2 + Vue3项目最佳实践

本文相关源码点这里

原作者姓名:花果山技术团队
原出处:掘金
原文链接:2021必知必会的vite+vue3项目最佳实践


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK