使用vite搭建基于vue3.0+element-plus的后台管理系统

最近vite越来火爆了,2.0发布以后也趋于稳定,写了一个demo发现运行速度确实非常快,和使用webpack的vue-cli相比完全是降维打击,想着搭个架子,方便以后使用。小菜鸡的初次尝试,希望各位大神不吝赐教。

1、使用yarn初始化vite模板

yarn create @vitejs/app
之后会让你输入项目名称,选择框架等。这里我们输入名称为jianshu,框架选择vue,回车
然后进入项目中,输入yarn回车,安装依赖
cd jianshu && yarn
安装完成之后,使用yarn dev命令启动开发服务。
可以看到只花了477毫秒就启动完成了,对比vue-cli来说快了很多。我们看到vite默认端口是3000,而不是8080,这个可以在项目里配置。打开localhost:3000地址,可以看到vite默认的欢迎页面。

2、安装我们需要用到的插件

网络请求使用axios,css预处理使用sass,还有登录验证会用到的jd-md5加密,当然还有我们的element-plus,最后就是vue全家桶:vuex和vue-router。
yarn add axios sass js-md5 element-plus

由于默认安装vuex和vue-router还是3.x,和vue3.0搭配不上,这里我们单独安装这两个插件的最新的4.x版本

yarn add vuex@lastest
yarn add vue-router@lastest
全部安装完成后,来进行配置

3、插件的配置

①配置vuex

vuex4.x的语法和3.x差距比较大,有能力的同学可以自行翻看官方文档。在src目录下新建store文件夹,然后新建index.js。
这里我们只配置最基础的token,userInfo,storeInfo三个数据,全部存入localstorage中做持久化。
然后配置了set方法来设置/清空数据。集成了loginIn和loginOut的方法,方便调用。
import { createStore } from 'vuex';

export default createStore({
  state() {
    return {
      userInfo: JSON.parse(localStorage.getItem('userInfo')),
      storeInfo: JSON.parse(localStorage.getItem('storeInfo')),
      token: localStorage.getItem('token')
    }
  },
  mutations: {
    SetToken(state, data) {
      state.token = data.token;
      if(data){
        localStorage.setItem('token', data.token);
        localStorage.setItem('expire', data.expire)
      }else{
        localStorage.removeItem('token');
        localStorage.removeItem('expire');
      }
    },
    SetUser(state, data) {
      state.userInfo = data;
      if(data){
        localStorage.setItem('userInfo', JSON.stringify(data));
      }else{
        localStorage.removeItem('userInfo');
      }
    },
    SetStore(state, data){
      state.storeInfo = data;
      if(data){
        localStorage.setItem('storeInfo', JSON.stringify(data));
      }else{
        localStorage.removeItem('storeInfo');
      }
    },
    LoginIn(state, data) {
      this.commit('SetToken', data);
      this.commit('SetUser', data.user);
    },
    LoginOut() {
      this.commit('SetToken', '');
      this.commit('SetUser', '');
      this.commit('SetStore', '');
      location.href = '/';
    },
  }
});
然后在main.js里进行配置。默认的main.js是这样的
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')
不太方便我们配置插件,这里需要改造一下
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App);
app.mount('#app');
引入store
import store from './store';
...
app.use(store);

②配置网络请求axios

在src目录下新建plugins文件夹,然后新建axios.js。
引入axios和element-plus的loading组件,然后引入store。
import axios from 'axios';
import {ElLoading} from 'element-plus';
import store from '../store';
import {uiMsg, apiHost} from '../utils';
这里的uiMsg和apiHost是单独封装到utils里面,方便调用的
在src目录下新建utils文件夹,然后新建index.js,封装一些常用的工具函数。
import { ElMessage } from 'element-plus';
// api
// const apiHost = 'https://product.com/api'; //生产
const apiHost = 'http://dev.com/api'; //开发

// 消息提示
function uiMsg(msg, type = 'error', onClose) {
  ElMessage.closeAll();
  ElMessage({
    message: msg,
    type,
    duration: 1500,
    customClass: 'ui-msg-zindex',
    onClose: () => {
      onClose && onClose();
    }
  });
}

// 复制剪贴板
function uiCopy(str) {
  let copyDom = document.createElement('div');
  copyDom.innerText = str;
  copyDom.style.position = 'absolute';
  copyDom.style.top = '0px';
  copyDom.style.right = '-9999px';
  document.body.appendChild(copyDom);
  //创建选中范围
  let range = document.createRange();
  range.selectNode(copyDom);
  //移除剪切板中内容
  window.getSelection().removeAllRanges();
  //添加新的内容到剪切板
  window.getSelection().addRange(range);
  //复制
  let successful = document.execCommand('copy');
  copyDom.parentNode.removeChild(copyDom);
  try {
    uiMsg({
      msg: successful ? "复制成功!" : "复制失败,请手动复制内容",
      type: successful ? 'success' : 'error'
    })
  } catch (err) {}
}

// 深拷贝
function uiDeepCopy(obj, cache = []) {
  function find(list, f) {
      return list.filter(f)[0];
  }
  if (obj === null || typeof obj !== 'object') {
      return obj;
  }
  const hit = find(cache, (c) => c.original === obj);
  if (hit) {
      return hit.copy;
  }
  const copy = Array.isArray(obj) ? [] : {};
  cache.push({original: obj, copy});
  Object.keys(obj).forEach((key) => {
      copy[key] = uiDeepCopy(obj[key], cache);
  });
  return copy;
}

export { apiHost, uiMsg, uiCopy, uiDeepCopy }
初始化axios,使用requestCount来优化多个请求同步进行时,loading闪烁的问题。
let http = axios.create({
  baseURL: apiHost,
  timeout: 6000,
  headers: {
    'Content-Type': 'application/json;charset=UTF-8;'
  }
});

let loading,
  requestCount = 0;

const ShowLoading = ()=>{
  if(requestCount === 0 && !loading){
    loading = ElLoading.service({
      background: 'rgba(0,0,0,.7)'
    });
  }
  requestCount++;
}
const HideLoading = ()=>{
  requestCount--;
  if(requestCount === 0){
    loading.close();
  }
}
因为我们后台接口大部分都是使用jwt认证,所以需要在request拦截器中给请求加上token。
然后在response拦截器中加入token过期失效的处理,这里可以根据实际情况修改判断条件。
http.interceptors.request.use(config=>{
  ShowLoading();
  if (store.state.token) {
    config.headers['Authorization'] = 'Bearer ' + store.state.token;
  }
  config.headers.post['Content-Type'] = 'application/json';
  return config;
});
http.interceptors.response.use(response=>{
  HideLoading();
  return response.data;
}, error=>{
  if(error.response.status === 401){
    // 401 token过期,退出登录
    uiMsg('登录已过期,请重新登录', null, ()=>{
      store.dispatch('LoginOut');
    });
  }else{
    return Promise.reject(error)
  }
});
封装get和post请求。加入函数节流处理,添加接口状态判断和错误消息提示。
/**
 * get方法,对应get请求
 * @param {String} url [请求的url地址]
 * @param {Object} params [请求时携带的参数]
 */
 function get(url, params) {
  return new Promise((resolve) => {
    let _timestamp = new Date().getTime();
    http.get(url, { params }).then(res => {
      if(new Date().getTime() - _timestamp < 200){
        setTimeout(() => {
          if (res.code === 200) {
            resolve(res);
          } else {
            res.msg && uiMsg(res.msg);
          }
        }, 200);
      }else{
        if (res.code === 200) {
          resolve(res);
        } else {
          res.msg && uiMsg(res.msg);
        }
      }
    }).catch(error => {});
  })
}

/**
 * post方法,对应post请求
 * @param {String} url [请求的url地址]
 * @param {Object} params [请求时携带的参数]
 */
 function post(url, params) {
  return new Promise((resolve) => {
    let _timestamp = new Date().getTime();
    http.post(url, params).then(res => {
      if(new Date().getTime() - _timestamp < 200){
        setTimeout(() => {
          if (res.code === 200) {
            resolve(res);
          } else {
            res.msg && uiMsg(res.msg);
          }
        }, 200);
      }else{
        if (res.code === 200) {
          resolve(res);
        } else {
          res.msg && uiMsg(res.msg);
        }
      }
    })
    .catch(error => {});
  })
}

export {get, post};
完成后在main.js中进行配置,同时引入utils里面的工具函数,挂载到vue全局上。
import {get, post} from './plugins/axios';
import {uiMsg, uiCopy, uiDeepCopy} from './utils';
...
app.config.globalProperties.$uiMsg = uiMsg;
app.config.globalProperties.$uiCopy = uiCopy;
app.config.globalProperties.$uiDeepCopy = uiDeepCopy;
app.config.globalProperties.$get = get;
app.config.globalProperties.$post = post;

③配置vue-router

在scr目录下新建router目录,然后新建index.js。
引入vue-router和vuex。
import {createRouter, createWebHistory} from 'vue-router';
import store from '../store';
首先配置不需要权限控制的页面。在src目录下新建views目录,然后分别新建登录:/login/index.vue,404:/error/notFound.vue,无权限:/error/noPermission.vue页面。这里我们使用异步加载引入。
// 不需要权限的页面
const constantRoutes = [
  {
    // 登录
    path: '/login',
    name: 'login',
    component: ()=>import('../views/login/index.vue')
  },
  {
    // 404
    path: '/:pathMatch(.*)',
    name: 'notFound',
    component: ()=>import('../views/error/notFound.vue')
  },
  {
    // 无权限
    path: '/noPermission',
    name: 'noPermission',
    component: ()=>import('../views/error/noPermission.vue')
  }
];
然后来配置普通页面。随意配置三个示例页面。
const asyncRoutes = {
  path: '/',
  name: 'main',
  component: ()=>import('../views/main.vue'),
  children: [
    {
      // 首页
      path: '/',
      name: 'home',
      component: ()=>import('../views/home/index.vue')
    },
    {
      // 用户列表
      path: '/userList',
      name: 'userList',
      component: ()=>import('../views/setting/userList.vue')
    },
    {
      // 权限设置
      path: '/permission',
      name: 'permission',
      component: ()=>import('../views/setting/permission.vue')
    }
  ]
}
然后来初始化router。
const router = createRouter({
  history: createWebHistory('/'),
  routes: constantRoutes
});

router.addRoute(asyncRoutes);
我们在router跳转中,加入token判断

router.beforeEach((to, from, next)=>{
  // 登录判断
  if(store.state.token){
    if(to.path === '/login'){
      next({path: '/'});
    }else{
      // 权限判断
      next();
    }
  }else{
    if(to.path === '/login'){
      next();
    }else{
      next({path: '/login'})
    }
  }
});
跳转完成后,将滚动条位置重置。
router.afterEach(to => {
  window.scrollTo(0, 0);
});
最后导出router
export default router;
然后到main.js中配置
import router from './router';
...
app.use(router);

④配置elemet-plus。

在plugins文件夹下新建element.js。
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';

export default app=>{
  app.use(ElementPlus);
}
然后在main.js中进行配置。
import installElementPlus from './plugins/element';
...
installElementPlus(app);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容