项目介绍
1.项目根目录文件
2.源码子目录结构
3.api目录
4.assets目录
5.components目录
6.mixins目录
7.permission目录
8.router目录
9.store目录
10.styles目录
11.utils目录
项目文件介绍
1.安装element-ui组件实现按需加载
// 1.1.npm i element-ui -S // 1.2.在babel.config.js中配置 module.exports = { plugins: [ [ 'component', { libraryName: 'element-ui', styleLibraryName: 'theme-chalk' } ], ] } // utils下新建文件Element.js按需加载组件 import Vue from 'vue' // 按需引入 import { Button, } from 'element-ui' // 注册组件,之后就可以在所有组件中使用 Vue.use(Button) /* 注意:列如消息类组件,如:Message,Notification,MessageBox等 不能像上面一样注册使用,否则还没有使用该组件该组件就会被初始化 */ // 解决:绑定到vue原型上 Vue.prototype.$message = Message Vue.prototype.$notify = Notification Vue.prototype.$confirm = MessageBox // 再组件使用时直接this调用即可 this.$message.success('注册成功') // 如果想使用MessageBox中的确认弹框 this.$confirm.confirm()
2.封装二次请求使用起来更方便,在utils中新建request.js中写入
// 安装第三方请求工具axios npm install axios // request.js import axios from 'axios' // 处理javascript大数字的问题 // 安装 npm i json-bigint import jsonBig from 'json-bigint' import store from '@/store/index' // element-ui的加载组件和消息提示组件 import { message, Loading } from 'element-ui' // 是我们项目的配置文件,项目的接口和其他的配置放在utils目录中的config.js中 import { config, loaddingConfig } from '@/utils/config' // 创建一个新的axios对象供我们使用 const request = axios.create({ baseURL: `${config.baseURL}${config.prot}`, timeout: config.timeout }) // 请求拦截器 // 注意在单独的js中使用Loading和在组件中使用有一点不同 /* 1.在组件中使用 const loading = this.$loading({ lock: true, text: 'Loading', spinner: 'el-icon-loading', background: 'rgba(0, 0, 0, 0.7)' }); 关闭加载 loading.close() 2.在单独的js文件中使用 Loading.service({ lock: true, text: 'Loading', spinner: 'el-icon-loading', background: 'rgba(0, 0, 0, 0.7)' }) 关闭加载状态 Loading.service().close() */ request.interceptors.request.use(config => { // 开始加载 Loading.service(loaddingConfig) if (store.getters.getuserInfo.token) { // 每次登陆的时候将token插入到请求头中 config.headers.authorization = store.getters.getuserInfo.token } return config }, error => { // 取消失败终止 Loading.service().close() return Promise.reject(error) }) // 处理大数字的问题 request.defaults.transformResponse = [function (data) { try { return jsonBig.parse(data) } catch (err) { return data } }] // 响应拦截器 request.interceptors.response.use(response => { // 请求完毕取消加载 Loading.service().close() return response }, error => { Loading.service().close() // 处理请求错误的函数 errorMsg(error.response) return Promise.reject(error) }) // 异常处理 function errorMsg (error) { if (error) { return } switch (error.status) { case 400: message.error('亲,您查看的资料出现了错误') break case 401: message.error('请检查token,可能已经过期,需要重新登陆') tokenOverdue(error) break case 403: message.error('抱歉您的权限还需要升级') break case 404: message.error('资源被狗狗调走了') break case 408: message.error('请求超时,请重试') break case 500: message.error('可爱的服务器好像奔溃了') break case 502: message.error('请仔细检查您的网络') break case 504: message.error('您的网络很慢,已经超时,请重试') break case 503: message.error('当前服务不支持') break default: message.error('与服务器的连接断开了') break } } // token的过期处理 function tokenOverdue (error) { // 如果有token的话重新存储,刷新 if (error.data.token) { // 将刷新的token重新存储到本地即可 const userInfo = { ...store.getters.getuserInfo, token: error.data.token } // 将新的token重新存储到本地, store.commit('SET_USERINFO', userInfo) // 将错误的请求再重新发送刷新token return request(error.config) } } // 导出请求接口封装函数 export default (method, url, data = null) => { method = method.toUpperCase() if (method === 'POST') { return request.post(url, data) } else if (method === 'GET') { return request.get(url, { params: data }) } else if (method === 'DELETE') { return request.delete(url, { params: data }) } else if (method === 'PUT') { return request.put(url, data) } }
3.创建api根目录,里面当接口文件
// user.js文件
import request from '@/utils/request'
// 用户登陆 export const userLogin = params => request('GET', '/admin/user/login', params) // 用户注册 export const userRegister = params => request('GET', '/admin/user/register', params) // 获取用户的权限数据 export const getUserAuth = params => request('GET', '/admin/user/user_auth', params) // 获取用户头像 export const getUserImg = params => request('GET', '/admin/user/user_img', params) // 获取用户登陆时的头像 export const getLoginImg = params => request('GET', '/admin/user/login_img', params) // 修改用户资料 export const editUserInfo = data => request('POST', '/admin/user/edit_user', data)
4.require.context(path, Boolean, file)动态加载组件
1.为什么使用:下一步components目录中的组件要引入使用,那么就存在一个问题,如果我们一个页面要引入很多组件那么就会有很多的import。
此时可以通过使用require.context(path, Boolean, file) 之后就可以通过组件的name属性来调用组件
// 在components目录下新建 cptsRegister.js // cptsRegister.js import Vue from 'vue' function capitalizeFirstLetter (str) { return str.charAt(0).toUpperCase() + str.slice(1) } // 用来匹配.vue的前缀函数 function validateFileName (str) { return /^S+.vue$/.test(str) && str.replace(/^S+/(w+).vue$/, (res, $1) => capitalizeFirstLetter($1)) } const requireComponent = require.context('./', true, /.vue$/) // 遍历匹配到的文件夹及文件名,并且遍历得到每一个 requireComponent.keys().forEach(filePath => { // 得到每一个.vue文件中的属性方法和组件的name值 const componentConfig = requireComponent(filePath) // 得到文件名的前半部分index const fileName = validateFileName(filePath) // 判断如果是以index开头命名的就返回组件的name值来注册组件,否则就使用文件名来注册 const componentName = fileName.toLowerCase() === 'index' ? capitalizeFirstLetter(componentConfig.default.name) : fileName Vue.component(componentName, componentConfig.default || componentConfig) }) // components目录下的cheking目录的vue <template> <div class="container"> 我是一个组件 </div> </template> <script> export default { name: 'Checking', } </script> // 其他组件中使用,直接使用,无需引入和注册,但是必须和组件的name名字保持一致,是不是很棒 <Checking />
5.处理一下过滤器把
// 在utils目录下创建filters.js文件,我们一次性注册掉所有的过滤器 // filter.js // 事件格式化第三方插件 import moment from 'moment' const filters = { relativeTime (value) { return moment(value).startOf('second').fromNow() }, formatTime (value, format = 'YYYY-MM-DD HH:mm:ss') { return moment(value).format(format) }, statusFilter (status) { const statusText = ['审核通过', '草稿', '待审核', '审核失败'] if (status === undefined) return [] if (status.length) { status.forEach(item => { item.statusContent = statusText[item.status] }) } return status } } export default filters // 之后在main.js中一次性注册 import filterRegister from './utils/filters' for (const key in filterRegister) { Vue.filter(key, filterRegister[key]) }
6.图片懒加载( vue-lazyload )
// 安装 vue-lazyload 插件 npm i vue-lazyload // 在main.js中引入使用 import VueLazyload from 'vue-lazyload' // 引入图片懒加载的配置文件 import { lazyLoad } from './utils/config' // 注册使用 Vue.use(VueLazyload, lazyLoad) // 在组件中img标签上的src属性替换成v-lazy="reqquire('../../assets/images')", 注意图片的地址必须是网络地址,如果是本地地址需要跳过require()导入使用 // config中的配置 // 图片懒加载的配置 export const lazyLoad = { preLoad: 1.3, // 加载中显示的图片 loading: 'https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2843662159,2317606805&fm=16&gp=0.jpg', // 加载失败显示的图片 error: 'https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=104394466,2625116464&fm=11&gp=0.jpg', // 尝试加载一次 attempt: 1 }
7.路由切换进度条(nprogress)
// 安装进度条插件 npm i nprogress // 在全局路由导航中使用,在目录permission下的index.js中使用 import router from '@/router' // 导入进度条 import nprogress from 'nprogress' router.beforeEach((to, from, next) => { // 开启进度条 nprogress.start() }) router.afterEach((to, from, next) => { // 关闭进度条 nprogress.done() }) // main.js中引入样式文件 // 引入滚动条的样式文件 import 'nprogress/nprogress.css' // 引入全局路由导航拦截 import './permission/index' // 自定义滚动条的颜色 #nprogress .bar { background: #c3f909 !important; }
8.看一下路由的设置
import Vue from 'vue' import VueRouter from 'vue-router' // 公共路由 export const publicRoutesMap = [ { path: '/login', name: 'login', component: () => import(/* webpackChunkName: "user" */ '@/views/Login') }, { path: '/register', name: 'register', component: () => import(/* webpackChunkName: "user" */ '@/views/Register') }, ] // 权限路由 // 需要的权限路由,动态挂载的路由 export const asyncRoutesMap = [ { path: '*', redirect: '/404', hidden: true, meta: { roles: ['user'] } }, { path: '/404', name: 'four', component: () => import('../views/Error/foruAndFour.vue'), meta: { roles: ['user'] } } ]
9.在store目录下的modules目录新建permission.js文件
// 把公共路由和权限的路由导出来 import router, { publicRoutesMap, asyncRoutesMap } from '@/router' // 定义一个函数用来筛选后端返回来的权限数据,如果筛选成功的话返回true,否则false function hasPerMission (roles, route) { if (route && route.meta.roles) { return roles.some(role => route.meta.roles.indexOf(role) >= 0) } else { return true } } const permission = { state: { routers: publicRoutesMap, addRouters: [] }, mutations: { SET_ROUTERS (state, routers) { state.addRouters = routers state.routers = publicRoutesMap.concat(routers) } }, actions: { generateRoutes ({ state, commit }, data) { // 返回一个promise回调 return new Promise((resolve, reject) => { // 遍历权限数组 const accessedRoutes = asyncRoutesMap.filter(v => { // 如果包含admin,说明就是管理员直接进入即可 if (data.indexOf('admin') >= 0) return true // 之后就是调用hasPerMission函数对象权限动态路由和后台返回的用户权限进行严格匹配 if (hasPerMission(data, v)) { // 判断是否有权限路由是否有子路由,有子路由继续遍历 if (v.children && v.children.length > 0) { v.children = v.children.filter(child => { // 对权限子路由和后台返回的用户权限数据,在进行匹配,匹配成功返回 if (hasPerMission(data, child)) { return child } // 失败返回false return false }) // 并且要把权限的父路由返回来,不光要把权限子路由返回, // 无论权限子路有还是没有,都应该把权限父路由返回来 return v } else { // 否则说明没有子路由,直接把父路由返回 return v } } // 如果权限验证没有匹配项,直接返回false return false }) // 将返回的动态路由存储到公共路由中 commit('SET_ROUTERS', accessedRoutes) resolve() }) } }, getters: { // 只要权限路由数组发生变化就重新计算 addRouters (state) { return state.routers } } } export default permission
10.在全局路由导航中进行拦截当天添加
import router from '@/router' import store from '@/store' // 导入进度条 import nprogress from 'nprogress' const whiteList = ['/login', '/register'] // 不重定向白名单 router.beforeEach((to, from, next) => { // 开启进度条 nprogress.start() // 检查token if (store.getters.getuserInfo.token) { // 有token的话去登录页,直接拦截到首页,因为我们需要用户点击按钮退出,不希望通过浏览器的back按钮 if (to.path === '/login') { next() } else { // 如果没有用户的权限数据,去拉取一下用户的权限数据 if (store.getters.roles.length === 0) { store.dispatch('getUserAuth', store.getters.getuserInfo.token).then(res => { // 调用该方法对用户的权限进行一次筛选 store.dispatch('generateRoutes', res).then(() => { router.addRoutes(store.getters.addRouters) if (from.path !== '/login') { next({ ...to, replace: true }) } else { next() } }) }).catch(error => { // 验证失败重新登陆 next({ path: '/login' }) }) } else { next() } } } else { // 需要重定向的白名单 if (whiteList.indexOf(to.path) !== -1) { next() } else { next('/login') } } }) router.afterEach((to, from, next) => { // 关闭进度条 nprogress.done() })
11.看一下mixins目录中的文件
// 写法和vue一模一样,在需要使用的vue组件中混入使用即可 // mixins目录下的index.js export const publicLogic = { data () { return { msg: "我是mixins的数据" } }, methods: {}, created () {}, watch : { }, mounted () {} } // 在其他组件中混入 <template> </template> <script> import { publicLogic } from '@/mixins/index' export default { // 通过mixins书信混入 mixins: [publicLogic], data () { return { } }, created () { console.log(this.msg) // 即可拿到混入的数据 } } </script>
12.pc端的滚动条插件(vue-happy-scroll)
// 安装使用 npm i vue-happy-scroll import { HappyScroll } from 'vue-happy-scroll' // 在组件中注册 components: { HappyScroll } <happy-scroll :min-length-v="0.2" color="rgba(3,253,244,1)" size="10" hide-horizontal> // ...需要滚动的y元素 </happy-scroll> // 先取消浏览器默认的滚动 body,html { overflow-y: hidden; overflow-x: hidden; }
13.store目录具体结构
// store下的index.js import Vue from 'vue' import Vuex from 'vuex' // 把用户权限验证模块引入 import permission from './modules/permission' // 用户存储 import user from './modules/user' // 公共计算属性 import getters from './getters' Vue.use(Vuex) export default new Vuex.Store({ modules: { permission, user }, getters }) // store目录下的getters.js const getters = { getuserInfo: state => state.user.userInfo, roles: state => state.user.roles, navBars: state => state.user.navBars, cache: state => state.user.cache } export default getters // store目录下的modules目录下的js文件,都是一个一个的模块 // user.js 用来存储放置用户的逻辑操作 // 引入api接口 import { userLogin, getUserAuth } from '@/api/user' const user = { state: { // 需要往本地存储的用户信息包含token userInfo: JSON.parse(window.sessionStorage.getItem('userInfo')) || {}, // 用户的权限信息 roles: [], // 用户的导航栏数据 navBars: JSON.parse(window.sessionStorage.getItem('nav_bar')) || { navBarArr: [], index: 0 }, // 缓存组件 cache: [] }, mutations: { SET_USERINFO (state, userInfo) { state.userInfo = userInfo window.sessionStorage.setItem('userInfo', JSON.stringify(state.userInfo)) }, // 存储用户的navBar导航栏数据 SET_USERNAVBAR ({ navBars }, navBar) { if (navBars.navBarArr.length === 0) { navBars.navBarArr.push(navBar) } else { const navItem = navBars.navBarArr.find((item, index) => { if (item.path === navBar.path) { navBars.index = index return true } }) if (!navItem) { navBars.navBarArr.push(navBar) navBars.index = navBars.navBarArr.length - 1 } } window.sessionStorage.setItem('nav_bar', JSON.stringify(navBars)) }, // 动态添加缓存组件 ADD_CACHE ({ cache }, name) { if (cache.includes(name)) return cache.push(name) }, // 动态删除缓存组件 REMOVE_CACHE ({ cache }, name) { const index = cache.indexOf(name) if (index === -1) return cache.splice(index, 1) } }, actions: { // 获取用户信息 userLogin ({ commit }, userInfo) { return new Promise((resolve, reject) => { userLogin(userInfo).then(res => { if (res.data.code === 200) { commit('SET_USERINFO', res.data.userInfo) } resolve(res.data) }).catch(error => { reject(error) }) }) }, // 获取用户权限 getUserAuth ({ state }, token) { return new Promise((resolve, reject) => { getUserAuth({ token }).then(res => { state.roles.push(...res.data) resolve(res.data) }).catch(error => { reject(error) }) }) } } } export default user // permission.js 用来存储放置用户的权限方法
14.vue.config.js配置文件
const CompressionPlugin = require('compression-webpack-plugin') const path = require('path') const BundleAnalyzer = require('webpack-bundle-analyzer').BundleAnalyzerPlugin module.exports = { // 是否触发eslint检查 lintOnSave: false, // 是否使用包含运行时编译器的 Vue 构建版本, 但是这会让你的应用额外增加 10kb 左右 runtimeCompiler: false, publicPath: '/', // 打包文件的出口 outputDir: 'dist', // 放置生成的css和js和img和fonts的目录 assetsDir: 'static', // 数组存放html的路径 indexPath: 'index.html', productionSourceMap: false, /* 默认情况下,生成的静态资源在它们的文件名中包含了 hash 以便更好的控制缓存, 前提是保证index.html是vue cli自动生成的,如果无法保证的话就设置为false */ filenameHashing: true, chainWebpack: config => { // ============压缩图片 start============ config.module.rule('images') .test(/.(png|jpe?g|gif|svg)(?.*)?$/) .use('url-loader') .loader('url-loader') .options({ limit: 10240, outputPath: 'static/images' }) .end() // ============压缩图片 end============ }, configureWebpack: config => { return { plugins: [ // 压缩js和css和html new CompressionPlugin({ test: /.js$|.html$|.css/, threshold: 10240, deleteOriginalAssets: false }), // 图形化展示打包后的详情 new BundleAnalyzer() ], performance: { // 关闭webpack的性能提示 hints:'warning', //入口起点的最大体积 maxEntrypointSize: 50000000, //生成文件的最大体积 maxAssetSize: 30000000, //只给出 js 文件的性能提示 assetFilter: function(assetFilename) { return assetFilename.endsWith('.js'); } }, // 指定不打包的第三方包,那么这些包需要在html页面通过cdn引入 externals: { 'vue': 'Vue', 'vuex': 'Vuex', 'vue-router': 'VueRouter', 'axios': 'axios', "moment": "moment", 'echarts': 'echarts' } } }, } // index.html <script src="https://cdn.bootcss.com/vue/2.6.11/vue.runtime.min.js" crossorigin="anonymous" > </script> <script src="https://cdn.bootcss.com/vue-router/3.1.3/vue-router.min.js" crossorigin="anonymous" > </script> <script src="https://cdn.bootcss.com/vuex/3.1.2/vuex.min.js" crossorigin="anonymous" > </script> <script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js" crossorigin="anonymous" > </script> <script src="https://cdn.bootcss.com/moment.js/2.24.0/moment.min.js" crossorigin="anonymous" > </script> <script src="https://cdn.bootcss.com/echarts/3.7.1/echarts.min.js" crossorigin="anonymous" > </script>
15.babel.config.js
module.exports = { presets: [ '@vue/cli-plugin-babel/preset' ], plugins: [ [ 'component', { libraryName: 'element-ui', styleLibraryName: 'theme-chalk' } ], /* 把es6转换成es5的时候,babel会需要一些辅助函数,如果多个源码文件都依赖这些辅助函数 那么这些辅助函数会出现很多次,造成代码的沉余,为了不让这些辅助函数的代码重复注销 是 babel-plugin-transform-runtime插件可以做到让他们只出现一次,将这些辅助函数帮到 一个单独的模块 babel-runtime 中,这样做能减小项目文件的大小。 */ "@babel/plugin-transform-runtime" ] }