Vue 技术栈从零搭建一个Vue3全家桶+webpack5基础架构项目

项目环境,nodejs 使用高版本12以上, npm 6以上
vue2的时候有一直用一个搭建好的基础结构,每次开新项目就拉去直接使用,vue3来了,升级了一下这个项目基础结构,系统的整理一下记录一下分享给大家。
完整项目 github 地址:vue3-project-template

项目使用的技术栈工具及大版本

  • 语言:javascript
  • 构建工具:webpack 5
  • 前端框架:vue 3
  • 路由:vue router 4
  • 状态管理:vuex 4
  • CSS 预编译处理:less
  • 网络请求工具:axios
  • 前端 UI 框架:element ui

项目搭建步骤

创建开发目录安装 webpack 工具及开发服务器

mkdir admin
cd admin
# 初始化项目生成 package.json
npm init -y
# 安装 webpack工具 及dev-server服务
npm i webpack webpack-cli webpack-dev-server webpack-merge --save-dev

当前我的安装成功后的版本:

+ webpack-merge@5.8.0
+ webpack-cli@4.10.0
+ webpack-dev-server@4.9.3
+ webpack@5.74.0
安装webpack 需要的一些 loader
# 安装 html 模板解析器
npm i html-webpack-plugin --save-dev
# 安装 css-loader style-loader less-loader
npm i css-loader style-loader url-loader less-loader --save-dev
# 安装 babel
npm i babel-loader @babel/core @babel/preset-env -D

基础项目文件结构

在项目根目录创建两个文件夹

mkdir public
mkdir src

在 public 目录创建一个 index.html 文件,复制一个 favicon.ico 文件过来

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Admin</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
安装 vue 全家桶
    # 最新稳定版的 vue
    npm install vue@next
    # 单文件组件@vue/compiler-sfc, vue-loader解析
    npm install -D @vue/compiler-sfc vue-loader
    # vue router 路由及 vuex
    npm install vue-router@4 vuex --save

当前我的本机环境安装后的版本号:

+ vue@3.2.36
+ @vue/compiler-sfc@3.2.37
+ vue-loader@17.0.0
+ vue-router@4.1.2
+ vuex@4.0.2
创建项目文件结构

先创建一个这样的目录结构备用

├── App.vue   		入口文件
├── api  			接口文件目录
├── assets 			静态资源目录
├── components  	组件目录
├── layout  		布局文件目录
├── main.js 		入口 js
├── router 			路由文件目录
├── store  			vuex 目录
└── utils  			项目工具类目录

src 目录结构如下图:
在这里插入图片描述

完善入口 main.js 文件和 App.vue 文件
main.js
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.mount('#app')

App.vue

<template>
  <div>
    <router-link to="/">首页</router-link>
    <router-link to="/about">About</router-link>
  </div>
  <router-view></router-view>
</template>

<style>
html,body{
  padding: 0;
  margin: 0;
  width: 100%;
}
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  padding: 0;
  margin: 0;
  width: 100%;
  height: 100%;
}
</style>

配置 webpack配置文件

在 src 的同级目录再创建一个 config 文件目录,创建一个 webpck.dev.js 配置文件

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');

module.exports = {
  entry: {
    main: './src/main.js',
  },
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: '[name].[chunkhash:8].build.js'
  },
  mode: "development",
  devtool: 'source-map',
  resolve: {
    // 快捷目录别名
    alias: {
      "@": path.resolve('./src'),
    },
    // 配置文件扩展名,引入的时候可以不需要加后缀名了
    extensions: [ '*', '.js', '.ts', '.vue', '.json']
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test:/\.js$/, 
        exclude:/node_modules/, //排除node_modules文件夹
        use:{
          loader:'babel-loader', //转换成es5
          options:{
            presets:['@babel/preset-env'], //设置编译的规则
          }
        }
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin(),
    new HtmlWebpackPlugin({
      template: './public/index.html',
      filename: 'index.html'
    })
  ]
};

添加两个启动脚本在 package.json 文件中
在 package.json 的 script 中添加两个脚本命令:

"build": "webpack --config config/webpack.dev.js",
"dev": "webpack serve --config config/webpack.dev.js",

这里,运行两个命令都可以成功了。
打包和开发都可以跑起来了。

测试一下打包

npm run build 

在这里插入图片描述
运行一下开发模式

npm run dev

在这里插入图片描述
到这里一个最简单的 webpack + vue 开发环境配置就完成了,可以解析.vue单文件组件,可以解析 CSS,也可以正常启动开发和打包。

接下来继续完善

添加 less 解析

# 安装 less-loader
npm i install less less-loader --save-dev

webpack 中配置添加 less-loader

{
   test: /\.less$/,
   use: [
     'style-loader',
     'css-loader',
     'less-loader'
   ]
 }

测试一下,在 App.vue 中随便写一点 less 样式,页面中文字变成红色,less 添加完成。

<style lang="less" scoped>
  body{
    div {
      color: red;
    }
  }
</style>

安装前端 UI 框架,element-plus

element-plus

npm install element-plus --save

如果你对打包后的文件大小不是很在乎,那么使用完整导入会更方便。
这个自己评估项目,如果你这个项目你预估会非常多的模块,是一个中大型级后台,那么推荐按需引入,文件体积能少一点还是少一点,
如果是小后台,就几个小模块,懒的麻烦完整引入不在乎文件体积大小,自己判断就行。

修改一下 main.js,
添加引入 element-plus

import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

在 app 实例后添加

app.use(ElementPlus)

当前 main 文件

import { createApp } from 'vue'
import App from './App.vue'

import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

const app = createApp(App)

app.use(ElementPlus)

app.mount('#app')

可以 App.vue 文件中添加一个按钮组件测试一下,看一下UI 渲染是否成功了。

<el-button type="primary">Primary</el-button>

在这里插入图片描述
渲染成功,安装完成。

后台管理界面框架 + vue router 路由完善

建立页面基础结构

src 目录下创建一个 pages 目录,创建几个基础路由页面,404错误页面,登录成功的首页,登录页面。

├── common
│   └── 404.vue
├── home
│   └── index.vue
└── login
    └── index.vue

在 home/index.vue 中随便写点东西

<template>
  <h3>Home page</h3>
</template>
layout 目录后台管理界面结构

src目录中创建一个 layout 目录,在 layout 目录下创建后台管理界面,基本结构如下图:
左为菜单项目,右:头部,面包屑,主路由内容区

在这里插入图片描述
在 layout 中建立以下文件结构,可以根据需求自己调整

├── breadcrumb
│   └── index.vue
├── header
│   └── index.vue
└── sidebar
    └── index.vue
├── index.vue
完善 router 路由

在 src 下的 router 目录中开始完善路由,让界面渲染出来跑起来。

├── common.js   # 公共页面
└── index.js    # 入口路由

我们采用 index.js 入口引入其它模块路由

index.js
import {
  createRouter,
  createWebHashHistory,
} from 'vue-router'

import { commonRoutes } from "./common"; // 公共路由
// import { imageRoutes } from "./image/index"; // 业务模块

const routes = [
  ...commonRoutes,
  // ...imageRoutes
]
const router = createRouter({
  history: createWebHashHistory(),
  routes
})

export default router
common.js

import Layout from "@/layout/index.vue";
import Home from "@/pages/home/index.vue";

export const commonRoutes = [
  {
    path: "/login",
    name: "登录",
    component: () => import("@/pages/login/index.vue"),
  },
  {
    path: "/",
    name: "home",
    component: Layout,
    meta: {
      title: '运营中心'
    },
    redirect: "/home",
    children: [
      {
        path: "home",
        name: "Home-Index",
        meta: {
          title: '控制台'
        },
        component: () => Home,
      }
    ],
  },
  {
    path: '/404',
    name: '404',
    component: Layout,
    meta: {
      title: 'Error'
    },
    children: [
      {
        path: "",
        name: "404page",
        meta: {
          title: '404'
        },
        component: () => import('@/pages/common/404.vue')
      }
    ],

  },
  {
      path: '/:pathMatch(.*)',
      redirect: '/404'
  }
];

在 main 文件中添加路由引入

import router from './router/index'

app.use(router)

路由搭建好之后,继续把 layout 布局内容完善一下

完善 layout结构
App.vue

去掉多余的演示数据

<template>
  <router-view></router-view>
</template>

<style>
  html,body{
    padding: 0;
    margin: 0;
    width: 100%;
  }
  #app {
    font-family: Avenir, Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    padding: 0;
    margin: 0;
    width: 100%;
    height: 100%;
  }
</style>

layout 的 index.vue
<template>
  <div class="common-layout">
    <el-container>
      <el-aside class="sidebar " >
        <Sidebar></Sidebar>
      </el-aside>
      <el-container>
        <el-header>
          <Header></Header>
        </el-header>
        <el-main>
          <Breadcrumb></Breadcrumb>
          <div class="layout-content">
            <div class="layout-main">
              <router-view />
            </div>
          </div>
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>

<script setup>
  import Sidebar from "./sidebar/index.vue"
  import Header from "./header/index.vue"
  import Breadcrumb from './breadcrumb/index.vue'
</script>

<style lang="less" scoped>
  .common-layout{
    height: 100vh;
  }
  .el-header{
    padding: 0;
  }
  .el-main{
    padding: 0;
    background-color: #f2f2f2;
  }
  .sidebar{
    width: 230px;
    height: 100vh;
    background-color: #001529;
    overflow: hidden;
    -webkit-box-shadow: 2px 0 6px rgb(0 21 41 / 35%);
    box-shadow: 2px 0 6px rgb(0 21 41 / 35%);
    transition: all .2s;
  }
  .sidebar-hide{
    width: 70px;
  }
  .layout-content{
    padding: 15px;
  }
  .layout-main{
    background-color: #fff;
    padding: 15px;
    min-height: 40vh;
  }
</style>
header/index.vue
<template>
  <div class="header">
    <div class="trigger">
      <el-icon >
        <DArrowLeft />
      </el-icon>
    </div>
    <div class="f5">
      <el-icon><RefreshRight /></el-icon>
    </div>

    <el-popover placement="bottom" :width="160" trigger="click">
      <template #reference>
        <div class="header-right">
          <el-avatar size="small" :icon="UserFilled" />
          <span class="name">Admin</span>
          <el-icon><ArrowDown /></el-icon>
        </div>
      </template>
      <div class="person" @click="exit">
        <!-- 暂无扩展内容 -->
        <span>退出登录</span>
        <el-icon><Close /></el-icon>
      </div>
    </el-popover>

  </div>
</template>

<script setup>
  import { UserFilled } from '@element-plus/icons-vue'

  function exit() {
    consolelog('exit localStroge clear')
  }


</script>

<style lang="less" scoped>
  @uiColor: #409eff;
  .header {
    position: relative;
    height: 60px;
    padding: 0;
    background: #fff;
    -webkit-box-shadow: 0 1px 4px rgb(0 21 41 / 8%);
    box-shadow: 0 1px 4px rgb(0 21 41 / 8%);
  }
  .trigger{
    height: 60px;
    line-height: 60px;
    vertical-align: top;
    padding: 0 22px;
    display: inline-block;
    cursor: pointer;
    -webkit-transition: all .3s,padding 0s;
    transition: all .3s,padding 0s;
  }
  .f5{
    height: 60px;
    line-height: 60px;
    vertical-align: top;
    display: inline-block;
  }
  .header-right{
    float: right;
    height: 100%;
    margin-left: auto;
    overflow: hidden;
    display: flex;
    align-items: center;
    padding-right: 20px;
    cursor: pointer;
  }
  .header-right:hover{
    color: @uiColor;
  }
  .header-right .name{
    padding: 0 8px;
  }
  .person{
    display: flex;
    justify-content: end;
    align-items: center;
    cursor: pointer;
    span{
      padding-right: 5px;
    }
  }
</style>
breadcrumb/index.vue
<template>
  <div class="breadcrumb">
    <el-breadcrumb separator="/">
    <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
    <el-breadcrumb-item
      v-for="(item, index) in listBread"
      :key="index + '-'+item.path">
      <span v-if="index == 0">{{ item.meta.title }}</span>
      <router-link v-else :to="item.path">{{ item.meta.title }}</router-link>
    </el-breadcrumb-item>
  </el-breadcrumb>
  </div>
</template>

<script setup>
import { ref } from "@vue/reactivity";
import { watchEffect } from "@vue/runtime-core";
import { useRoute } from "vue-router";

const listBread = ref([]);
const route = useRoute();

// 监听路由变化
watchEffect(() => {
  listBread.value = route.matched;
});

</script>

<style lang="less" scoped>
  .breadcrumb{
    padding: 20px;
    background-color: #fff;
  }
</style>
sidebar/index.vue

这里放菜单信息,是动态接口返回还是本机固定路由,
自由控制

<template>
  <div class="logo-hd">
    <h2><span class="big">A</span><span class="sub-tit">dmin</span></h2>
  </div>
  <!-- 这里放菜单信息 -->
  <el-menu
    class="menu"
    active-text-color="#409eff"
    background-color="#001529"
    text-color="#fff"
    default-active="1"
    :unique-opened="true"
    :default-openeds="['1', '2']">
    <el-sub-menu
      v-for="(item, index) in menuList"
      :key="item.id"
      :index="'m-' + index"
    >
      <template #title>
        <el-icon :size="14" >
          <component :is="item.meta.icon "></component>
        </el-icon>
        <span class="menu-name">{{ item.name }}</span>
      </template>
      <el-menu-item
        v-for="(child, childIndex) in item.children"
        :key="child.id"
        :index="index + '-' + childIndex"
      >
        <router-link :to="child.url" class="menu-name">{{ child.name }}</router-link>
      </el-menu-item>
    </el-sub-menu>
  </el-menu>
</template>

<script setup>
  import { reactive } from 'vue'
  const menuList = reactive([
    {
      id: 1,
      name: "首页",
      url: "/home",
      meta: {
        icon: "House"
      },
      children: [
        { id: 3, name: "运营中心", url: "/home" },
      ],
    }
  ])
</script>

<style lang="less" scoped>
  @menuColor: #409eff;
  .logo-hd{
    text-align: center;
    color: #fff;
  }
  h2{
    font-size: 20px;
  }
  .big{
    font-size: 24px;
    font-weight: 500;
    color: #f60;
  }
  .sidebar-hide .sub-tit{
    display: none;
  }


  .menu{
    border-right: 0;
    transition: all .2s;
  }
  .menu-name{
    color: #fff;
  }

  .el-menu-vertical-demo:not(.el-menu--collapse) {
    width: 200px;
    min-height: 400px;
  }
  .is-opened .el-menu-item{
    background-color: #000c17 ;
  }
  .is-opened .is-active{
    background-color: @menuColor !important ;
  }
  .el-menu-item{
    font-size: 12px;
    --el-menu-sub-item-height: 40px;
    --el-menu-active-color: @menuColor;
    a{
      text-decoration: none;
      color: hsla(0,0%,100%,.65);
    }
    .router-link-active{
      color: #fff;
    }
  }
  .is-active{
    background-color: @menuColor;

  }
  .is-active:hover{
    background-color: @menuColor;
  }
  .el-menu-item:hover{
    a{
      color: #fff;
    }
  }


</style>

到这里页面结构已经渲染出来了,如图示:

在这里插入图片描述

login.vue

再来写一个登录界面

<template>
  <div class="login" :style="'background-image: url(' + bgpic + ')'">
    <el-form class="form-signin" >
      <h3 class="">管理平台</h3>
      <div class="item">
        <el-input placeholder="用户名" v-model="userInfo.username"></el-input>
      </div>
      <div class="item">
        <el-input show-password placeholder="密码" v-model="userInfo.pass">
        </el-input>
      </div>
      <div class="act-btn">
        <el-button type="primary" @click="login">登录</el-button>
      </div>
    </el-form>
  </div>
</template>

<script >
import { reactive } from 'vue'
import { ElMessage } from "element-plus";
import { useRouter } from 'vue-router'

export default {
  name: 'login',
  data () {
    return {
      username: '',
      pass: '',
      roundPic: [
        'https://s.cn.bing.net/th?id=OHR.MangroveDay_ZH-CN5590436101_1920x1080.jpg',
        'https://s.cn.bing.net/th?id=OHR.FourTigresses_ZH-CN4095017352_1920x1080.jpg'
      ],
      bgpic: '',
    }
  },
  created () {
    this.bgpic = this.roundPic[parseInt(Math.random() * this.roundPic.length)]
  },
  setup() {
    const router = useRouter()
    const userInfo = reactive({
      username: '',
      pass: '',
    })


    function login() {
      if (userInfo.username == '' || userInfo.pass == '') {
        ElMessage.error('账号或者密码不能为空!')
        return
      }
      let params = {
        username: userInfo.username,
        password: userInfo.pass
      }

      localStorage.setItem('token', '登录成功')
      // 跳转到首页
      router.push('/')
    }

    return {
      userInfo,
      login
    }

  },
}
</script>

<style>
html,
body {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
}

body {
  display: -ms-flexbox;
  display: flex;
  -ms-flex-align: center;
  align-items: center;
  background-color: #dadada;
}

.login {
  width: 100%;
  height: 100%;
  background-repeat: no-repeat;
  background-position: center;
  background-size: cover;
}

.item {
  display: flex;
  justify-content: space-between;
  padding: 5px 0;
}

.form-signin {
  width: 350px;
  min-height: 240px;
  padding: 15px 45px 45px;
  position: fixed;
  top: 50%;
  left: 50%;
  z-index: 2;
  margin-left: -225px;
  margin-top: -160px;
  background-color: #fff;
}
.act-btn{
  padding-top: 10px;
}
.act-btn button {
  width: 100%;
}

</style>

在这里插入图片描述

axios 网络请求

封装网络请求
# 安装 axios 和 events
npm install axios events --save-dev

在 src/utils 目录下,建一个 http 文件夹再创建一个 request.js 文件

/*
 * @Description: axios
 * @Author: oscar
 * @Date: 2021-12-09 21:19:39
 */
import { ElMessage } from "element-plus";

import * as axios from "axios";
import * as EventEmitter from 'events';

class Request extends EventEmitter {
  constructor() {
    super();
    this.interceptors();
  }
  interceptors() {
    // 请求拦截器
    axios.interceptors.request.use(
      // 发送请求之前
      config => {
        // 头部设置 签名
        config.headers.sign = '' // 略,根据后端协商自行完善
        // 头部设置 token
        config.headers.token = '' // 略,根据后端协商自行完善
        return config;
      },
      error => {
        // 请求错误
        return Promise.reject(error);
      }
    );

    // 响应拦截器
    axios.interceptors.response.use(
      response => {
        const code = response.status;
        // 根据自己的业务代码进行响应拦截
        if ((code >= 200 && code < 300) || code === 304) {
          const res = response.data
          // 成功的事件回调,可以略,可以全局的去做一些业务处理
          this.emit("Success", res);
          return Promise.resolve(res);
        } else {
          console.log(response)
          // 响应错误逻辑处理 5xx 4xx 等等
          this.emit("Error", response);
          return Promise.reject(response);
        }
      },
      error => {
        // 响应错误逻辑处理
        console.log(error);
        // 接口异常了,全局的去针对业务做一些配置处理,不需要可以去掉
        this.emit("Error");
        return Promise.reject(error);
      }
    );
  }

  get(url, params) {
    return axios({
      method: 'get',
      url,
      params
    });
  }
  post(url, data) {
    return axios({
      method: 'post',
      url,
      data
    });
  }
  delete(url, data) {
    return axios({
      method: 'delete',
      url,
      data
    });
  }
  put(url, data) {
    return axios({
      method: 'put',
      url,
      data
    });
  }
  patch(url, data) {
    return axios({
      method: 'patch',
      url,
      data
    });
  }
}


const dialogMessage = (message) => {
  if (!message) {
    console.error('empty message')
    return
  }
  ElMessage.error(message)
}

let request = new Request();

request.on('Success', function(data) {
  console.log('Success:', data );
});

request.on('Error', function(data) {
  console.log('Error:', data );
});

export default request;

创建接口文件

在 src/api 目录下创建一个 demo.js 文件

// 接口测试 DEMO

import request from "@/utils/http/request.js"

//  测试 GET
export const getDemo = (params) => {
  let url = 'http://jsonplaceholder.typicode.com/comments'
  return request.get(url, params)
}
使用方法

随便找个地方来测试一下,
这里使用前面用到的首页组件来测试一下。

# 引入接口文件
import { getDemo } from '@/api/demo'

# 调用对应的方法,传入参数
const param = {
 	postId: 1
}
getDemo(param).then( res=> {
  	state.result = res
}).catch( err=> {

})

完整示例代码如下:

<template>
  <h3>Home page</h3>
  <el-button @click="getTest">Get Request Test</el-button>
  <div v-if="state.result && state.result.length > 0">
    <div class="item" v-for="item in state.result" :key="item.id">id: {{ item.id }}, name: {{ item.name }}, email: {{item.email}}</div>
  </div>
</template>

<script setup>
  import { getDemo } from '@/api/demo'
  import { reactive } from "vue"
  const state = reactive({
    loading: false,
    result: []
  })
  function getTest() {

    const param = {
      postId: 1
    }
    getDemo(param).then( res=> {
    // 成功后这里处理业务逻辑
      state.result = res
    }).catch( err=> {
    // 失败了这里处理业务逻辑

    })
  }

</script>

点击 Get Request Test 按钮,会发出一个网络请求

在这里插入图片描述
axios封装成功。

vuex 状态管理

做一个登录的小功能来测试一下 vuex
在 api/demo.js 中添加一个获取用户信息的接口

// 获取用户资料
export const getUser = (params) => {
  let url = 'https://jsonplaceholder.typicode.com/users/1'
  return request.get(url, params)
}

在 src/store 下创建以下文件,随自己的心意组织,项目小可以只创建一个文件搞定。
先有思想把内容组织一下这样后期有扩展添加也不会显的乱。

├── common			// 公共的数据,全项目都会常用到的
│   └── menu.js		// 比如菜单
├── index.js 		// 入口的 vuex文件,在这里引入其它模块
└── module			// 模块包
    └── user.js		// user用户模块,有其它业务模块可以继续添加

文件内容

store/index.js
import { createStore } from "vuex";
import user from "@/store/module/user"; // 引入业务模块下的用户模块
import common from "@/store/common/menu"; // 引入公共模块下的菜单模块

export default createStore({
  modules: {
    user,
    common
  },
});

store/common/menu.js
// 不写,演示用

export default {
  state: {
    isCollapse: false, // 控制菜单展开与折叠
    menuList: []
  },
  mutations: {
    change(state, status) {
      state.isCollapse  = Boolean(status)
    }
  },
  actions: {},
  getters: {},
  modules: {},
};

store/module/user.js
import { getUser } from '@/api/demo'
export default {
  namespaced: true,
  state: {
    user: JSON.parse(localStorage.getItem('USERINFO')) || {} , // 用户数据
  },
  mutations: {
    // 修改用户信息
    changeUser(state, data) {
      localStorage.setItem('USERINFO', JSON.stringify(data))
      state.user  = data
    },
    // 退出清空缓存数据
    exitUser() {
      localStorage.clear()
    }
  },
  actions: {
    // 获取用户信息
    login({ commit }, params) {
      return new Promise((resolve, reject) => {
        getUser(params).then(res => {
          // 更新用户资料
          commit('changeUser', res)
          resolve(res)
        }).catch(error => {
          reject(error)
        })
      })
    },

    // 用户退出
    userLogout({ commit }) {
      return new Promise((resolve, reject) => {
        // 根据业务需要,是否通知服务端,清空token有效期
        commit('exitUser')
        resolve({})
      })
    }
  },
  getters: {},
  modules: {},
};

在 mian.js 中引入 vuex

在项目入口文件 main.js中添加两行

import store from './store/index'
app.use(store)
使用 vuex
login.vue 登录
# 引入 vuex
import { useStore } from 'vuex'

在setup中实例化

let store = useStore()
// 调用 vuex 中的 action 登录
store.dispatch('user/login', params ).then((res) => {
   if(res.id) {
     router.push('/')
   }
 })

完整的 login.vue登录页面的 JS代码

import { reactive } from 'vue'
import { ElMessage } from "element-plus";
import { useRouter } from 'vue-router'
import { getUser } from '@/api/demo'
import { useStore } from 'vuex'

export default {
  name: 'login',
  data () {
    return {
      username: '',
      pass: '',
      roundPic: [
        'https://s.cn.bing.net/th?id=OHR.MangroveDay_ZH-CN5590436101_1920x1080.jpg',
        'https://s.cn.bing.net/th?id=OHR.FourTigresses_ZH-CN4095017352_1920x1080.jpg'
      ],
      bgpic: '',
    }
  },
  created () {
    this.bgpic = this.roundPic[parseInt(Math.random() * this.roundPic.length)]
  },
  setup() {
    let store = useStore()
    const router = useRouter()
    const userInfo = reactive({
      username: '',
      pass: '',
    })


    function login() {
      if (userInfo.username == '' || userInfo.pass == '') {
        ElMessage.error('账号或者密码不能为空!')
        return
      }
      let params = {
        username: userInfo.username,
        password: userInfo.pass
      }

      // 调用 vuex 中的 action 登录
      store.dispatch('user/login', params ).then((res) => {
        if(res.id) {
          router.push('/')
        }
      }).catch(err => {
        console.error(err)
      })
    }

    return {
      userInfo,
      login
    }

  },
}

点击登录,跳转到首页并缓存了登录信息。

router 路由守卫

在 src 目录下添加一个 permission.js 文件

# 添加一个进度条组件
npm install nprogress --save-dev
import NProgress from 'nprogress' // Progress 进度条
import router from './router'
import 'nprogress/nprogress.css'
import { ElMessageBox } from "element-plus";
const whiteList = ['/login'] // 不重定向白名单


router.beforeEach((to, from, next) => {
  NProgress.start()
  let userinfo = localStorage.getItem('USERINFO');
  // 白名单不校验是否有权限
  if ( whiteList.includes(to.path) ) {
    next()
  } else {
    if (userinfo) {
      next();
    } else {
      ElMessageBox.alert('非法访问,请返回登录', 'Error', {
        confirmButtonText: '确定',
        center: true,
        callback: () => {
            next({
              path: '/login'
            });
        },
      })
    
    }
  }
});

router.afterEach(() => {
  NProgress.done() // 结束Progress
})

main.js 入口中引入

// 权限路由过滤
import './permission'

这样没有登录成功的用户除了白名单路由,访问其它路由都会弹出警告提示跳转回登录页面。
在这里插入图片描述

总结

至此,已经完成了基础的一个前端项目

  • npm项目初始化
  • 添加 webpack
  • 添加 vue全家桶
  • 添加 element ui 前端UI框架
  • 封装 axios 网络请求
  • 组织使用了 vuex 状态管理

到这里,只是一个最最简单的基础框架,并不能直接用到生产中。

完整项目 github 地址:vue3-project-template

待优化完成事项

可以根据实际情况对代码进行调整

  • webpack 的配置文件区分生产测试环境
  • 接口文件使用反向代理
  • webpack 配置打包编译优化
  • vue 业务常用的业务组件封装
  • axios 请求根据实际业务请求进行判断处理,错误异常拦截处理
  • 组件缓存的相关处理
  • 菜单路由由后端接口在登录中返回前端进行动态路由渲染
  • 自定义指令进行按钮级别的权限控制
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值