Bootstrap

从零搭建vue3.0项目框架与基本封装

从零搭建vue3.0项目框架与基本封装

一、先卸载旧版本
如果没有旧版本可直接跳过此步骤

npm uninstall vue-cli -g

二、安装Vue CLI 3

npm install -g @vue/cli

三、创建项目
进入要创建项目的目录

vue create name

执行上述命令后,出现如下界面,选择配置,这里不选默认,选择自己配置
在这里插入图片描述
在这里插入图片描述
根据自己的需求,选择相应的配置,这里按"a",全选,回车。
在这里插入图片描述
这里选择3x,回车。
在这里插入图片描述
这里问你是否使用class风格的组件语法,输入y,回车。

在这里插入图片描述
这里问你是否使用babel做转义,输入y,回车。
在这里插入图片描述
这里是问你路由是否使用历史模式? 这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面,这里根据需要选择,我选了y。
在这里插入图片描述
这里是问你使用什么css预编译器? 我选择的 node-scss。
在这里插入图片描述
这里是问你选择哪种代码检测:
tslint: typescript格式验证工具
eslint w…: 只进行报错提醒; 不考虑项目【选这个】
eslint + A…: 不严谨模式;
eslint + S…: 正常模式;
eslint + P…: 严格模式;严谨一点【选这个】
在这里插入图片描述
这里是问你监测时机,这里选第一个,保存就监测,不要等提交在监测,那样问题会很多。
在这里插入图片描述
单元测试工具,这里随便了。
在这里插入图片描述
vue-cli 一般来讲是将所有的依赖目录放在package.json文件里
在这里插入图片描述
这里是问你是否在以后的项目中使用以上配置?这里我选择N。
ps:如何安装过程中有报错请百度自行解决,大多数是你的配置环境问题。

可能出现的node-sass安装问题解决办法
1、安装python2.7
1.1 打开https://www.python.org/download/releases/2.7/ 找到 Windows X86-64 MSI Installer (2.7.0) [1] (sig) 后下载后安装
1.2 配置环境变量, 把C:\Python27追加到变量名:path后
2、配置 win32-x64-67_binding.node
2.1 https://github.com/sass/node-sass/releases/download/v4.11.0/win32-x64-67_binding.node 下载后放到某个目录下(比如:D:\software\win32-x64-67_binding.node)
2.2 配置环境变量 变量名:SASS_BINARY_PATH 变量值:D:\software\win32-x64-67_binding.node(文件全路径)
2.3 执行npm rebuild node-sass 重构一下
3、进入vue工程 执行 npm install
4、安装成功之后 vue工程执行下执行 npm run serve。
看到如下界面说明启动成功。
在这里插入图片描述
四、UI框架安装
这里本人选择的 element-ui。
ps: element-ui 对vue3.0d的安装方式与2.0有所不同。在这里插入图片描述
安装命令

npm install element-plus --save

代码配置

import { createApp } from 'vue'
import App from './App.vue'
import './registerServiceWorker'
import router from './router'
import store from './store'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import locale from 'element-plus/lib/locale/lang/zh-cn'

createApp(App)
  .use(store)
  .use(ElementPlus, { locale })
  .use(router)
  .mount('#app')


五、封装axios
安装命令

npm install axios

封装
在src目录下创建request目录,创建interceptors.js与request.js。
在这里插入图片描述
interceptors.js该文件主要用来对请求与响应的拦截。

/**axios封装
 * 请求拦截、相应拦截、错误统一处理
 */
import Axios from "axios";
import router from "@/router";
import { ElMessage } from "element-plus";

const axios = Axios.create({
  baseURL: process.env.VUE_APP_URL, // url = base url + request url
  timeout: 5000, // request timeout
});

// post请求头
axios.defaults.headers.post["Content-Type"] = "application/json;charset=UTF-8";
// request interceptor
axios.interceptors.request.use(
  (config) => {
    // do something before request is sent
    if (sessionStorage.getItem("token")) {
      config.headers["Authorization"] =
        "bearer " + sessionStorage.getItem("token"); // 让每个请求携带自定义 token 请根据实际情况自行修改
    }
    return config;
  },
  (error) => {
    // do something with request error
    console.log(error); // for debug
    return Promise.reject(error);
  }
);
// http response 拦截器
axios.interceptors.response.use(
  (response) => {
    const data = response.data;
    // 根据返回的code值来做不同的处理(和后端约定)
    switch (data.code) {
      case 401:
        // 未登录 清除已登录状态
        if (router.history.current.name !== "login") {
          if (data.msg !== null) {
            ElMessage({
              message: data.msg,
              type: "error",
              duration: 5 * 1000,
            });
          } else {
            ElMessage({
              message: "未知错误,请重新登录",
              type: "error",
              duration: 5 * 1000,
            });
          }
          router.push("/login");
        }
        break;
      case 403:
        // 没有权限
        if (data.msg !== null) {
          ElMessage({
            message: data.msg,
            type: "error",
            duration: 5 * 1000,
          });
        } else {
          ElMessage({
            message: "未知错误,请重新登录",
            type: "error",
            duration: 5 * 1000,
          });
        }
        break;
      case 500:
        // 错误
        if (data.msg !== null) {
          ElMessage({
            message: data.msg,
            type: "error",
            duration: 5 * 1000,
          });
        } else {
          ElMessage({
            message: "未知错误,请重新登录",
            type: "error",
            duration: 5 * 1000,
          });
        }
        break;
      default:
        return data;
    }
    return data;
  },
  (err) => {
    // 返回状态码不为200时候的错误处理
    ElMessage({
      message: err.toString(),
      type: "error",
      duration: 5 * 1000,
    });
    return Promise.resolve(err);
  }
);
export default axios;

request.js该文件主要是对get,post,put,delete方法的封装。

import instance from './interceptors'
import { ElMessage } from 'element-plus'

export default class baseRequest {
  private url: any
  private params: any

  constructor (url: any, params: any) {
    this.url = url
    this.params = typeof params === 'undefined' ? {} : params
  }

  get () {
    return instance
      .get(this.url, { params: this.params })
      .then((res: any) => {
        if (res.code === 200) {
          return Promise.resolve(res.data)
        } else {
          ElMessage({
            message: res.message,
            type: 'error',
            duration: 5 * 1000
          })
          return Promise.resolve(false)
        }
      })
      .catch((e) => {
        ElMessage({
          message: e,
          type: 'error',
          duration: 5 * 1000
        })
        Promise.resolve(false)
      })
  }

  post () {
    return instance
      .post(this.url, this.params)
      .then((res: any) => {
        if (res.code === 200) {
          return Promise.resolve(res.data)
        } else {
          ElMessage({
            message: res.message,
            type: 'error',
            duration: 5 * 1000
          })
          Promise.resolve(false)
        }
      })
      .catch((e) => {
        ElMessage({
          message: e,
          type: 'error',
          duration: 5 * 1000
        })
        Promise.resolve(false)
      })
  }

  put () {
    return instance
      .put(this.url, this.params)
      .then((res: any) => {
        if (res.code === 200) {
          return Promise.resolve(res.data)
        } else {
          ElMessage({
            message: res.message,
            type: 'error',
            duration: 5 * 1000
          })
          Promise.resolve(false)
        }
      })
      .catch((e) => {
        ElMessage({
          message: e,
          type: 'error',
          duration: 5 * 1000
        })
        Promise.resolve(false)
      })
  }

  delete () {
    return instance
      .delete(this.url, { params: this.params })
      .then((res: any) => {
        if (res.code === 200) {
          return Promise.resolve(res.data)
        } else {
          ElMessage({
            message: res.message,
            type: 'error',
            duration: 5 * 1000
          })
          Promise.resolve(false)
        }
      })
      .catch((e) => {
        ElMessage({
          message: e,
          type: 'error',
          duration: 5 * 1000
        })
        Promise.resolve(false)
      })
  }

  upfile () {
    return instance
      .post(this.url, this.params, {
        headers: {
          'Content-Type': 'multipart/form-data',
          'X-Requested-With': 'XMLHttpRequest'
        }
      })
      .then((res: any) => {
        if (res.code === 200) {
          return Promise.resolve(res.data)
        } else {
          ElMessage({
            message: res.message,
            type: 'error',
            duration: 5 * 1000
          })
          Promise.resolve(false)
        }
      })
      .catch((e) => {
        ElMessage({
          message: e,
          type: 'error',
          duration: 5 * 1000
        })
        Promise.resolve(false)
      })
  }

  downfile () {
    return instance
      .post(this.url, this.params, { responseType: 'blob' })
      .then((res: any) => {
        const fileReader = new FileReader()
        fileReader.onload = function (e: any) {
          try {
            const jsonData = JSON.parse(e.target.result) // 说明是普通对象数据,后台转换失败
            if (jsonData.code) {
              ElMessage({
                message: jsonData.message,
                type: 'error',
                duration: 5 * 1000
              })
              Promise.resolve(false)
            }
          } catch (err) { // 解析成对象失败,说明是正常的文件流
            const url = window.URL.createObjectURL(res)
            const eleLink = document.createElement('a')
            eleLink.href = url
            eleLink.download = 'application.yml'
            document.body.appendChild(eleLink)
            eleLink.click()
            window.URL.revokeObjectURL(url)
          }
        }
        fileReader.readAsText(res)
      })
      .catch((e) => {
        ElMessage({
          message: e,
          type: 'error',
          duration: 5 * 1000
        })
        Promise.resolve(false)
      })
  }
}



如何使用?
在src目录下创建api文件夹,用来做项目的模块化管理。
在这里插入图片描述
这里创建了user.js,定义userApi 导出后,即可在vue.js文件中使用。

import baseRequest from "@/request/request.js";
// import qs from "qs";

const userApi = {
  /**
   * 获取数据来源-无参数
   */
  source() {
    let url = "/tmc/import/sourcea";
    let baseRequestFun = new baseRequest(url);
    return baseRequestFun.get();
  },
  /**
   * 获取数据来源-有参数
   */
  source2(params) {
    let url = "/tmc/import/sourcea";
    let baseRequestFun = new baseRequest(url,params);
    return baseRequestFun.get();
  },
};
export default userApi;

六、路由封装
路由封装包括登录权限请求等一系列操作;需要用到vuex;
如何配置vueX,请参考这篇文章

为了美观,我们使用NProgress在页面跳转时候在浏览器顶部添加进度条。

安装命令

npm install --save nprogress

使用nprogress
在src目录下创建permission.js文件,permission主要负责全局路由守卫和登录判断。
在这里插入图片描述
简单使用nprogress,在permission.js文件中引用NProgress 与router 。

import NProgress from "nprogress";
import "nprogress/nprogress.css";
import router from "@/router";

router.beforeEach((to, from, next) => {
  NProgress.start();
  next();
});

router.afterEach(() => {
  NProgress.done();
});

在main.js中引入permission。

import { createApp } from "vue";
import App from "./App.vue";
import "./registerServiceWorker";
import router from "./router";
import store from "./store";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import "./permission"; // permission control

createApp(App).use(store).use(ElementPlus).use(router).mount("#app");

这里补充一个知识点:
一个js文件代表一个js模块 ,ES6引入外部模块分两种情况:
1.导入外部的变量或函数等;

import {firstName, lastName} from './test';

2.导入外部的模块,并立即执行

//执行test.js但不导入任何变量
import './test'

接下来,配置全局路由守卫和登录的逻辑判断。
我们修改一下permission.js中的内容

import NProgress from "nprogress"; // progress bar
import "nprogress/nprogress.css"; // progress bar style
import router, { resetRouter, constantRoutes } from "@/router";
import Home from "@/views/Layout";
import { loginApi } from "@/api/login/login";
import store from "./store";

// NProgress 的一些配置
NProgress.configure({ showSpinner: false });
//不需要权限验证的路径
const whiteList = ["/login", "/auth-redirect"];

router.beforeEach(async (to, from, next) => {
  // 开始NProgress
  NProgress.start();
  // 获取token
  const hasToken = sessionStorage.getItem("token");
  if (hasToken) {
    if (to.path === "/" || to.path === "/login") {
      // 如果已经登录那么重定向到首页
      next({ path: "/index" });
      //NProgress结束
      NProgress.done();
    } else {
      // 获取角色,这里角色是存在vuex中的
      const hasRoles = store.getters.roleId;
      if (hasRoles) {
        next();
      } else {
        try {
          //拿到用户名,通过用户名去查询用户角色
          let name = sessionStorage.getItem("name");
          //查询用户角色
          const roleId = await loginApi.getRoleId(name);
          // 将角色保存到vuex中
          store.dispatch("setROLEID", roleId);
          //根据角色获取对应的路由信息
          const accessRoutes = await loginApi.getRouter(roleId);
          //添加路由,这里是自己定义的添加路由的方法,可以根据自己公司定义的前后台传参方式进行自定义编写
          await controlRouter(accessRoutes);
          //hack方法以确保addRoute是完整的
          //设置replace:true,这样导航将不会留下历史记录
          next({ ...to, replace: true });
        } catch (error) {
          //删除令牌并转到登录页面重新登录
          next(`/login?redirect=${to.path}`);
          NProgress.done();
        }
      }
    }
  } else {
    /* 没有token 也可以登录的页面*/
    if (whiteList.indexOf(to.path) !== -1) {
      // 在免登录白名单中,直接进入
      next();
    } else {
      //没有访问权限的其他页面将重定向到登录页面
      next(`/login?redirect=${to.path}`);
      NProgress.done();
    }
  }
});

router.afterEach(() => {
  //完成进度条
  NProgress.done();
});

//路由器数据拼接,addRoutes方法在vue3中已经废弃了,所以这里使用的addRoute
export function controlRouter(accessRoutes) {
  for (let i = 0; i < accessRoutes.length; i++) {
    console.log(accessRoutes[i]);
    let sumMenu = {
      path: accessRoutes[i].MENU_URL,
      component: Home,
      name: accessRoutes[i].MENU_NAME,
      iconCls: accessRoutes[i].MENU_ICON,
      redirect: accessRoutes[i].MENU_REDIRECT,
    };
    if (accessRoutes[i].children) {
      sumMenu["children"] = [];
      for (let j = 0; j < accessRoutes[i].children.length; j++) {
        //是否是iframe页面
        if (accessRoutes[i].children[j].MENU_REPORT === "true") {
          const MENU_REPORT_PATH = accessRoutes[i].children[j].MENU_REPORT_PATH;
          sumMenu["children"][j] = {
            props: () => {
              return {
                path: MENU_REPORT_PATH,
              };
            },
            path: `${accessRoutes[i].children[j].MENU_PATH}`,
            iconCls: accessRoutes[i].children[j].MENU_ICON,
            component: (resolve) =>
              require(["./../public/static/report/test"], resolve),
            name: accessRoutes[i].children[j].MENU_NAME,
          };
        } else {
          sumMenu["children"][j] = {
            path: accessRoutes[i].children[j].MENU_PATH,
            iconCls: accessRoutes[i].children[j].MENU_ICON,
            component: () =>
              import(`@/views/${accessRoutes[i].children[j].MENU_URL}`),
            name: accessRoutes[i].children[j].MENU_NAME,
          };
        }
      }
    }
    //this.$router不是响应式的,所以手动将路由元注入路由对象
    resetRouter();
    router.options.routes = constantRoutes;
    router.options.routes.push(sumMenu);
    router.addRoute(sumMenu); //添加到路由
  }
}


这里贴出router.js文件

import { createRouter, createWebHistory } from "vue-router";
import Layout from "@/views/Layout";
// 该 js 实现了逻辑的第一步,配置两个路由
// 公共的路由
export const constantRoutes = [
  {
    path: "/login",
    component: () => import("@/views/login/index"),
    hidden: true, // 因为无需在侧边栏显示,可以用这个字段来控制隐藏
  },
  {
    path: "/index",
    component: Layout, // 这是一个框架组件,顶部和侧边栏会固定加载
    redirect: "/homePage", // 重定向的就是中间的内容部分
    children: [
      {
        path: "/homePage",
        component: () => import("@/views/homePage"),
        name: "homePage",
        meta: { title: "homePage", icon: "homePage" },
      },
    ],
  },
];

// 先把公共路由添加进路由实例,动态的路由手动添加
const router = createRouter({
  history: createWebHistory(),
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRoutes,
});

export function resetRouter() {
  const newRouter = createRouter({
    history: createWebHistory(),
    scrollBehavior: () => ({ y: 0 }),
    routes: constantRoutes,
  });
  router.resolve = newRouter.resolve; // the relevant part
}

export default router;

七、页面布局封装

页面布局主要分成Header、Aside、Main、Bottom四部分,根据项目需要可以去掉Bottom部分。

这里重点讲一下Aside侧边栏封装动态菜单。

<template>
  <div>
    <el-menu
      :default-active="$route.path"
      class="el-menu-vertical-demo"
      router
      collapse-transition
      show-timeout
      hide-timeout
      background-color="#2189d5"
      text-color="#fff"
      active-text-color="#ffd04b"
      :collapse="isCollapse"
    >
      <template v-for="(item, index) in $router.options.routes">
        <template v-if="!item.hidden">
          <el-sub-menu :index="index+''" :key="index">
            <template #title
              ><i :class="item.iconCls" style="color: white"></i>
              <span>{{ item.name }}</span>
            </template>
            <template v-for="child in item.children">
              <el-menu-item
                :index="child.path"
                :key="child.path"
                v-if="!child.hidden"
                class="el-menu-item-icon"
              >
                <i :class="child.iconCls" style="color: white"></i>
                <span>{{ child.name }}</span>
              </el-menu-item>
            </template>
          </el-sub-menu>
        </template>
      </template>
    </el-menu>
  </div>
</template>

<script>
export default {
  name: "Aside",
  data() {
    return {
      isCollapse: false,
    };
  },
};
</script>

<style>
.el-menu-vertical-demo:not(.el-menu--collapse) {
  width: 200px;
  min-height: 400px;
}
</style>

在这里插入图片描述
上述代码中只封装了二级菜单,如果想无限级封装可以使用递归函数来实现。

最后这里贴出路由请求的出参结构

就是这行代码的出参

 const accessRoutes = await loginApi.getRouter(roleId);
[{
MENU_ICON: "el-icon-date",
MENU_ID: 45,
MENU_NAME: "今日动态",
MENU_ORDER: "1",
MENU_REDIRECT: "/query",
MENU_REPORT: "false",
MENU_TYPE: "2",
MENU_URL: "todayNews",
PARENT_ID: "0",
children:[{
MENU_ICON: "el-icon-date"
MENU_ID: 140
MENU_NAME: "门诊量(医生)"
MENU_ORDER: "1"
MENU_PATH: "/outpatientDoc"
MENU_REPORT: "false"
MENU_TYPE: "2"
MENU_URL: "todayNews/OutpatientDoc"
PARENT_ID: "45"
},
{MENU_ICON: "el-icon-document",
MENU_ID: 129,
MENU_NAME: "门诊量(科室)(皕杰)",
MENU_ORDER: "1",
MENU_PATH: "/bijie",
MENU_REPORT: "true",
MENU_REPORT_PATH: "http://192.168.17.152:8089/report/ReportEmitter?rpt=NewItem/NewItemNewItem2/AKS_econ_mzsjhz.brt",
MENU_TYPE: "1",
MENU_URL: "bijie.vue",
PARENT_ID: "45"}]
}]

八、vue.config配置
vue-cli 3.0 创建的项目,已经干掉了原有的webpack配置,取而代之的是,vue.config.js

module.exports = {
  // 输出文件目录
  outputDir: "dist", // 默认dist
  // 用于嵌套生成的静态资产(js,css,img,fonts)目录
  assetsDir: 'assets',
  // 指定生成的 index.html 的输出路径 (相对于 outputDir)。也可以是一个绝对路径
  indexPath: "index.html", // Default: 'index.html'
  filenameHashing: true,
  // 构建多页时使用
  pages: undefined,
  // eslint-loader是否在保存的时候检查
  lintOnSave: true,
  // 是否使用包含运行时编译器的Vue核心的构建
  runtimeCompiler: false,
  // 默认情况下 babel-loader 会忽略所有 node_modules 中的文件。如果你想要通过 Babel 显式转译一个依赖,可以在这个选项中列出来
  transpileDependencies: [],
  // 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。
  productionSourceMap: false,
  // css相关配置
  css: {
  },
  //开发模式反向代理配置,生产模式请使用Nginx部署并配置反向代理
  devServer: {
    port: 8085,
    proxy: {
      "/api": {
        //本地服务接口地址
        //target: "http://192.168.3.169:8080",
        target: "http://localhost:8083/admin",
        //远程演示服务地址,可用于直接启动项目
        ws: true,
        pathRewrite: {
          "^/api": '',
        },
      },
    },
  },
  // PWA 插件相关配置
  pwa: {},
  // 第三方插件配置
  pluginOptions: {
    // ...
  },
};

九、总结
到这里项目的基本就结束了,作为一个小白初次搭建项目肯定会有不足,后续会对其进行不断的完善,最后附上项目源码

;