Vue实战之从零搭建Vite2+Vue3全家桶

109 阅读3分钟

前言

开启2021学习之旅,主要介绍学习vite2和vue3的实战过程。
本篇主要介绍从零开始搭建Vite2 + Vue3+Element-Plus + Vue-router4 + Vuex + Eslint。

vite介绍

 Vite (法语意为 "快速的",发音 /vit/)是一种全新的前端构建工具。
 由一个开箱即用的开发服务器 + 一套构建    指令组成。
 Vite 利用浏览器原生的 ES 模块支持和基于 esbuild 的依赖预打包来显著提升前端开发体验。

初始化项目

  # npm 6.x
  npm init @vitejs/app my-vue-app --template vue

  # npm 7+, 需要额外的双横线:
  npm init @vitejs/app my-vue-app -- --template vue

  # yarn
  yarn create @vitejs/app my-vue-app --template vue

别名设置

修改vite.config.js

  import path from 'path'
  export default defineConfig({
      ...,
      resolve: {
        alias: {
          '@': path.resolve(__dirname, 'src'),
          components: path.resolve(__dirname, 'src/components'),
          assets: path.resolve(__dirname, 'src/assets'),
          views: path.resolve(__dirname, 'src/views'),
          utils: path.resolve(__dirname, 'src/utils'),
          apis: path.resolve(__dirname, 'src/apis'),
        },
      }
  })

SCSS预处理

scss是一款css预处理语言,是sass的一个升级版本,SCSS 是 Sass 3 引入新的语法,其语法完全兼容 CSS3,并且继承了 Sass 的强大功能。

安装sass

  npm i -d sass node-sass sass-loader

引入SASS全局变量/样式

  # vite.config.js
  export default defineConfig({
      ...,
      css: {
        preprocessorOptions: {
          scss: {
            // @/ 是 src/ 的别名
            // 所以这里假设你有 `src/assets/scss/variables.scss` 这个文件
            additionalData: `@import "@/assets/scss/variables.scss";`
          },
        },
      }
  })

ElementUI

开始引入UI框架,这里使用Element适配vue3版本的element-plus

安装element

  npm i -s element-plus

按需加载组件

借助 vite-plugin-style-import实现按需加载组件,以达到减小项目体积的目

安装\textrm\color{red}{vite-plugin-style-import}vite-plugin-style-import

npm install vite-plugin-style-import -D

修改\textrm\color{red}{vite.config.js}vite.config.js

import styleImport from 'vite-plugin-style-import'
plugins: [
  vue(),
  styleImport({
    libs: [
      {
        libraryName: 'element-plus',
        esModule: true,
        ensureStyleFile: true,
        resolveStyle: (name) => {
          name = name.slice(3);
          return `element-plus/packages/theme-chalk/src/${name}.scss`;
        },
        resolveComponent: (name) => {
          return `element-plus/lib/${name}`;
        },
      },
    ],
  })
]

新建element.js文件

// 如果要使用.scss样式文件,则需要引入base.scss文件
// import 'element-plus/packages/theme-chalk/src/base.scss'
import 'element-plus/packages/theme-chalk/src/base.scss'
import {
  ElAlert,
  ElAside,
  ElAutocomplete,
  ElAvatar,
  ElBacktop,
  ElBadge,
  ElBreadcrumb,
  ElBreadcrumbItem,
  ElButton,
  ElButtonGroup,
  ElCalendar,
  ElCard,
  ElCarousel,
  ElCarouselItem,
  ElCascader,
  ElCascaderPanel,
  ElCheckbox,
  ElCheckboxButton,
  ElCheckboxGroup,
  ElCol,
  ElCollapse,
  ElCollapseItem,
  ElCollapseTransition,
  ElColorPicker,
  ElContainer,
  ElDatePicker,
  ElDialog,
  ElDivider,
  ElDrawer,
  ElDropdown,
  ElDropdownItem,
  ElDropdownMenu,
  ElFooter,
  ElForm,
  ElFormItem,
  ElHeader,
  ElIcon,
  ElImage,
  ElInput,
  ElInputNumber,
  ElLink,
  ElMain,
  ElMenu,
  ElMenuItem,
  ElMenuItemGroup,
  ElOption,
  ElOptionGroup,
  ElPageHeader,
  ElPagination,
  ElPopconfirm,
  ElPopover,
  ElPopper,
  ElProgress,
  ElRadio,
  ElRadioButton,
  ElRadioGroup,
  ElRate,
  ElRow,
  ElScrollbar,
  ElSelect,
  ElSlider,
  ElStep,
  ElSteps,
  ElSubmenu,
  ElSwitch,
  ElTabPane,
  ElTable,
  ElTableColumn,
  ElTabs,
  ElTag,
  ElTimePicker,
  ElTimeSelect,
  ElTimeline,
  ElTimelineItem,
  ElTooltip,
  ElTransfer,
  ElTree,
  ElUpload,
  ElInfiniteScroll,
  ElLoading,
  ElMessage,
  ElMessageBox,
  ElNotification,
} from 'element-plus';

const components = [
  ElAlert,
  ElAside,
  ElAutocomplete,
  ElAvatar,
  ElBacktop,
  ElBadge,
  ElBreadcrumb,
  ElBreadcrumbItem,
  ElButton,
  ElButtonGroup,
  ElCalendar,
  ElCard,
  ElCarousel,
  ElCarouselItem,
  ElCascader,
  ElCascaderPanel,
  ElCheckbox,
  ElCheckboxButton,
  ElCheckboxGroup,
  ElCol,
  ElCollapse,
  ElCollapseItem,
  ElCollapseTransition,
  ElColorPicker,
  ElContainer,
  ElDatePicker,
  ElDialog,
  ElDivider,
  ElDrawer,
  ElDropdown,
  ElDropdownItem,
  ElDropdownMenu,
  ElFooter,
  ElForm,
  ElFormItem,
  ElHeader,
  ElIcon,
  ElImage,
  ElInput,
  ElInputNumber,
  ElLink,
  ElMain,
  ElMenu,
  ElMenuItem,
  ElMenuItemGroup,
  ElOption,
  ElOptionGroup,
  ElPageHeader,
  ElPagination,
  ElPopconfirm,
  ElPopover,
  ElPopper,
  ElProgress,
  ElRadio,
  ElRadioButton,
  ElRadioGroup,
  ElRate,
  ElRow,
  ElScrollbar,
  ElSelect,
  ElSlider,
  ElStep,
  ElSteps,
  ElSubmenu,
  ElSwitch,
  ElTabPane,
  ElTable,
  ElTableColumn,
  ElTabs,
  ElTag,
  ElTimePicker,
  ElTimeSelect,
  ElTimeline,
  ElTimelineItem,
  ElTooltip,
  ElTransfer,
  ElTree,
  ElUpload,
];

const plugins = [
  ElInfiniteScroll,
  ElLoading,
  ElMessage,
  ElMessageBox,
  ElNotification,
];

const option = { size: 'small', zIndex: 3000 }
export default (app) => {
  // element全局配置
  app.config.globalProperties.$ELEMENT = option
  components.forEach((component) => {
    app.component(component.name, component);
  });

  plugins.forEach((plugin) => {
    app.use(plugin);
  });
};

修改main.js

import useElement from '@/utils/element.js';
const app = createApp(App)
useElement(app)
app.mount('#app')

注意事项

  # 如果报错 Error: @use rules must be written before any other rules.

  # 修改vite.config.js
  - additionalData: `@import "assets/scss/variables.scss";`
  + additionalData: `@use "assets/scss/variables.scss" as *;`

vue-router

Vue Router 是 Vue的路由管理器,它和 Vue.js 的核心深度集成,是Vue的核心插件之一。

安装vue-router

  npm i -s vue-router@4.0.10

创建router目录及文件

新建router目录

src目录下新建router目录文件夹

新建index.js

router目录下新建index.js

  // 引入vue-router对象
  import { createRouter, createWebHistory } from "vue-router";
  import Layout from '@/layout'
  /**
  * 定义路由数组
  */
  const routes = [
    {// 404路由
      name: '404',
      path: '404',
      component: () => import('/@/views/error/404.vue')
    },
    {// 401路由
      name: '401',
      path: '401',
      component: () => import('@/views/error/401.vue'),
      hidden: true
    },
    {
      name: 'home',
      path: "home",
      component: () => import("/@/views/home/home.vue"),
    }
  ];

  /**
  * 创建路由
  */
  const router = createRouter({
    // hash模式:createWebHashHistory,
    // history模式:createWebHistory
    history: createWebHistory("/"),
    // history:createWebHashHistory(),
    routes,
  });

  /**
  * 路由守卫
  */
  router.beforeEach((guard) => {
    beforeEach.checkAuth(guard, router);
  });

  /**
  * 路由错误回调
  */
  router.onError((handler) => {
    console.log("error:", handler);
  });

  /**
  * 输出对象
  */
  export default router

修改main.js

import { createApp } from 'vue'
import App from './App.vue'
import useElement from '@/utils/element.js';
import router from '@/router/index.js'

const app = createApp(App)
useElement(app)
app.use(router)
app.mount('#app')

vuex

安装vuex

npm i vuex@4 -s

新建store目录及文件

新建store目录

在src目录下新建store目录

新建index.js文件

store目录下新建index.js文件

import { createStore, Store } from 'vuex';
import user from './modules/user';
import getters from './getters'

const store = createStore({
  modules: { user },
  getters
});

export default store

新建modules目录和module文件

// src/store/modules/user.js
const state = {
  name: 'hello vue3',
  age: 18
}

const mutations = {
  SET_NAME: (state, name) => {
    state.name = name
  },
  SET_AGE: (state, age) => {
    state.age = age
  }
}

const actions = {
  setName({ commit }, name) {
    commit('SET_NAME', name)
  },
  setAge({ commit }, age) {
    commit('SET_AGE', age)
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

修改main.js

import { createApp } from 'vue'
import App from './App.vue'
import useElement from '@/utils/element.js';
import router from '@/router/index.js'
import i18n from '@/i18n/index.js'
import store from '@/store/index.js' //++++

const app = createApp(App)
app.use(router)
app.use(store)                       //++++
app.use(i18n)
useElement(app)
app.mount('#app')


国际化

通过vue-i18n实现国际化

安装vue-i18n

npm i vue-i18n@next -s

增加i18n配置文件

src目录下新建i18n目录,新建cn.js、en.js多语言配置文件

  // src/i18n/cn.js
  export default {
    message: {
      hello: '你好,欢迎使用Vue3'
    }
  };




  // src/i18n/en.js
  export default {
    message: {
      hello: 'Hello Vue3'
    }
  };

增加i18n入口文件

i18n目录下新建index.js

// src/i18n/index.js
import { createI18n } from 'vue-i18n';
import cn from './cn.js';
import en from './en.js';
const messages = {
  en: {
    ...en
  },
  'zh-cn': {
    ...cn
  }
}
const i18n = createI18n({
  locale: localStorage.getItem('lang') || 'zh-cn',
  messages
});
export default i18n;

修改main.js

import { createApp } from 'vue'
import App from './App.vue'
import useElement from '@/utils/element.js';
import router from '@/router/index.js'
import i18n from '@/i18n/index.js'

const app = createApp(App)
app.use(router)
app.use(i18n)
useElement(app)
app.mount('#app')

ESLint

编码过程中,代码规范很重要,采用ESLint可以避免很多编码错误,提高代码可读性,这里采用Airbnb JavaScript 这套代码规范。

安装eslint

npm i -d eslint eslint-config-airbnb-base eslint-plugin-import eslint-plugin-vue

增加.eslintrc.js配置文件

# src目录下新建.eslintrc.js文件,参考如下配置:
module.exports = {
  extends: ['plugin:vue/vue3-essential', 'airbnb-base'],
  parserOptions: {
    sourceType: 'module',
    ecmaVersion: 2020,
  },
  plugins: ['vue'],
  rules: {
    ...
  },
};

配置.eslintignore文件

# 配置ESLint忽略文件,根目录创建.eslintignore文件,内如根据需求添加配置,例如:
/node_modules
/dist

http请求工具

这里采用\textrm\color{red}{axios}axios来进行http请求调用

安装axios

  npm install axios -s

封装axios请求,新建request.js

  import axios from 'axios'
  import {
    ElLoading,
    ElMessage
  } from 'element-plus';
  //创建axios的一个实例 
  const instance = axios.create({
    baseURL: import.meta.env.VITE_APP_URL, //接口统一域名
    timeout: 5000, //设置超时
    headers: {
      'Content-Type': 'application/json;charset=UTF-8;',
    }
  })
  let loading;
  //正在请求的数量
  let requestCount = 0
  //显示loading
  const showLoading = () => {
    if (requestCount === 0 && !loading) {
      loading = ElLoading.service({
        text: "Loading  ",
        background: 'rgba(0, 0, 0, 0.7)',
        spinner: 'el-icon-loading',
      })
    }
    requestCount++;
  }
  //隐藏loading
  const hideLoading = () => {
    requestCount--
    if (requestCount == 0) {
      loading.close()
    }
  }
  // instance.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
  // instance.defaults.headers.get['Content-Type'] = 'application/x-www-form-urlencoded';
  // instance.defaults.transformRequest = [function (data) {
  //   let ret = ''
  //   for (let it in data) {
  //     ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
  //   }
  //   return ret
  // }]
  //请求拦截器 
  instance.interceptors.request.use((config) => {
    showLoading()
    // 每次发送请求之前判断是否存在token,如果存在,则统一在http请求的header都加上token,不用每次请求都手动添加了
    const token = window.localStorage.getItem('token');
    token && (config.headers.Authorization = token)
    //若请求方式为post,则将data参数转为JSON字符串
    if (config.method === 'POST') {
      config.data = JSON.stringify(config.data);
    }
    return config;
  }, (error) =>
    // 对请求错误做些什么
    Promise.reject(error));

  //响应拦截器
  instance.interceptors.response.use((response) => {
    hideLoading()
    //响应成功
    return response.data;
  }, (error) => {
    console.log(error)
    //响应错误
    if (error.response && error.response.status) {
      const status = error.response.status
      switch (status) {
        case 400:
          message = '请求错误';
          break;
        case 401:
          message = '请求错误';
          break;
        case 404:
          message = '请求地址出错';
          break;
        case 408:
          message = '请求超时';
          break;
        case 500:
          message = '服务器内部错误!';
          break;
        case 501:
          message = '服务未实现!';
          break;
        case 502:
          message = '网关错误!';
          break;
        case 503:
          message = '服务不可用!';
          break;
        case 504:
          message = '网关超时!';
          break;
        case 505:
          message = 'HTTP版本不受支持';
          break;
        default:
          message = '请求失败'
      }
      ElMessage.error(message);
      return Promise.reject(error);
    }
    return Promise.reject(error);
  });

  const axios = ({
    method,
    url,
    data,
    config
  }) => {
    method = method.toLowerCase();
    if (method == 'post') {
      return instance.post(url, data, { ...config })
    } else if (method == 'get') {
      return instance.get(url, {
        params: data,
        ...config
      })
    } else if (method == 'delete') {
      return instance.delete(url, {
        params: data,
        ...config
      })
    } else if (method == 'put') {
      return instance.put(url, data, { ...config })
    } else {
      console.error(`错误的请求方式:${method}`)
      return false
    }
  }
  export default axios

封装api请求

  import request from '@/api/user.js'

  // 用户登录
  export function login(data) {
    return request({
      url: '/api/login',
      method: 'post',
      data,
      config: {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
        },
        timeout: 10000
      }
    })
  }

  // 获取用户信息
  export function getInfo(data) {
    return request({
      url: '/api/userinfo',
      method: 'get',
      params: data
    })
  }

vue组件中使用

  import { login } from '@/api/user'

  // 请求登录
  function onLogin() {
    login({ username: 'admin', pwd: 'admin' }).then(res => {
      // 业务逻辑
    })
  }

icon图标管理

这里采用\textrm\color{red}{ vite-plugin-svg-icons} vite-plugin-svg-icons插件来对svg图标进行管理

vite-plugin-svg-icons特征

  • 预加载,在项目运行时就生成所有图标,只需要操作一次dom
  • 高性能 内置缓存,仅当文件被修改时才会重新生成

安装vite-plugin-svg-icons

  # 安装要求
  # node version: >=12.0.0
  # vite version: >=2.0.0

  npm i vite-plugin-svg-icons -d

修改vite.config.js

import viteSvgIcons from 'vite-plugin-svg-icons'
plugins: [
  vue(),
  viteSvgIcons({
    // 配置路劲在你的src里的svg存放文件
    iconDirs: [path.resolve(process.cwd(), 'src/icons')],
    symbolId: 'icon-[name]'
  })
]

封装SvgIcon组件

创建svgIcon公共组件

src/components目录下新建SvgIcon目录,SvgIcon目录下新建index.vue

  <template>
    <svg aria-hidden="true" class="svg-icon">
      <use :xlink:href="symbolId" :fill="color" />
    </svg>
  </template>

  <script>
  import { defineComponent, computed } from 'vue'

  export default defineComponent({
    name: 'SvgIcon',
    props: {
      prefix: {
        type: String,
        default: 'icon'
      },
      name: {
        type: String,
        required: true
      },
      color: {
        type: String,
        default: 'white'
      }
    },
    setup(props) {
      const symbolId = computed(() => `#${props.prefix}-${props.name}`)
      return { symbolId }
    }
  })
  </script>
  <style scoped>
  .svg-icon {
    width: 1em;
    height: 1em;
    fill: currentColor;
    vertical-align: middle;
  }
  </style>

icons目录结构

# src/icons

- password.svg
- username.svg
- ...

修改main.js

 import 'virtual:svg-icons-register' 
 import SvgIcon from '@/components/SvgIcon/index.vue' 

 const app = createApp(App)
 app.component('svg-icon', SvgIcon)

vue组件中使用svgIcon

<template>
...
 <svg-icon name="password"  />
...
</template>