一、前言
本文基于开源项目:
最近有不少小伙伴私聊广东靓仔,能不能出一期关于vue日常项目开发的文章,广东靓仔整理了以往开发过的vue项目,结合目前业界主流方案,因此有了这篇文章。
二、现有方案
目前业界有很多现成的解决方案,广东靓仔列举了几个:
Ant Design Pro
D2 Admin截图
soybean-admin截图
以上都是比较稳定的管理系统解决方案,有兴趣的小伙伴可以去看看~下面我们一起来梳理梳理,vue实际开发的一些需要考虑到的点。
三、正文
在使用vue开发我们项目的时候,一般的都是采用现有的开源项目,然后进行一些改造(二次开发),来满足我们的业务需求,这种方案效率是最高的,成本也是最低的。
下面开始讲讲vue实际项目开发需要关注的模块,具体内容如下所示:
使用Vue CLI 3快速创建项目
脚手架,不是本文要讲的重点,随便看看即可~
全局安装
npm install -g @vue/cli or yarn global add @vue/cli
新建项目
vue create my-project
最后启动项目,看到如下效果:
自定义Webpack和Babel配置
webpack.config.js代码如下:
let path = require('path'); let webpack = require('webpack'); /* html-webpack-plugin插件,webpack中生成HTML的插件, 具体可以去这里查看https://www.npmjs.com/package/html-webpack-plugin */ let HtmlWebpackPlugin = require('html-webpack-plugin'); /* webpack插件,提取公共模块 */ let CommonsChunkPlugin = webpack.optimize.CommonsChunkPlugin; let config = { //入口文件配置 entry: { index: path.resolve(__dirname, 'src/js/page/index.js'), vendors: ['vue', 'vue-router','vue-resource','vuex','element-ui','element-ui/lib/theme-default/index.css'] // 需要进行单独打包的文件 }, //出口文件配置 output: { path: path.join(__dirname, 'dist'), //输出目录的配置,模板、样式、脚本、图片等资源的路径配置都相对于它 publicPath: '/dist/', //模板、样式、脚本、图片等资源对应的server上的路径 filename: 'js/[name].js', //每个页面对应的主js的生成配置 chunkFilename: 'js/[name].asyncChunk.js?'+new Date().getTime() //chunk生成的配置 }, module: { //加载器 rules: [ { test: /\.vue$/, loader: 'vue-loader', options: { loaders: { scss: 'vue-style-loader!css-loader!sass-loader', // <style lang="scss"> sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax' // <style lang="sass"> } } }, { test: /\.html$/, loader: "raw-loader" }, { test: /\.css$/, loader: 'style-loader!css-loader' }, { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader", options: { presets: ["es2015","stage-0"], plugins: ['syntax-dynamic-import'] } }, { test: /\.scss$/, loader: 'style-loader!css-loader!sass-loader' }, { test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/, loader: 'file-loader' }, { //图片加载器,雷同file-loader,更适合图片,可以将较小的图片转成base64,减少http请求 //如下配置,将小于8192byte的图片转成base64码 test: /\.(png|jpg|gif)$/, loader: 'url-loader?limit=8192&name=images/[hash].[ext]' } ] }, //插件 plugins: [ //webpack3.0的范围提升 new webpack.optimize.ModuleConcatenationPlugin(), //打包生成html文件,并且将js文件引入进来 new HtmlWebpackPlugin({ filename: path.resolve(__dirname, 'dist/html/index.html'), //生成的html存放路径,相对于path template: path.resolve(__dirname, 'src/html/index.html'), //ejs模板路径,前面最好加上loader用于处理 inject: 'body', //js插入的位置,true/'head'/'body'/false hash: true }), //提取功能模块 new CommonsChunkPlugin({ name: 'vendors', // 将公共模块提取,生成名为`vendors`的chunk minChunks: 2, //公共模块被使用的最小次数。配置为2,也就是同一个模块只有被2个以外的页面同时引用时才会被提取出来作为common chunks // children:true //如果为true,那么公共组件的所有子依赖都将被选择进来 }), ], //使用webpack-dev-server,启动热刷新插件 devServer: { contentBase: path.join(__dirname, "/"), host: 'localhost', //建议写IP地址,开发时候电脑的ip地址。localhost我不知道是幻觉还是怎样,有时候热刷新不灵敏 port: 9090, //默认9090 inline: true, //可以监控js变化 hot: true//热启动 }, //搜索路径变量 resolve: { alias: { vue: 'vue/dist/vue.js' }, extensions:['.js','.scss','.vue','.json']// 可以不加后缀, 直接使用 import xx from 'xx' 的语法 } }; module.exports = config;
设计一个高扩展性的路由
根据页面展示结构进行抽象,结合业务模块进行的合理层级划分
router.js代码如下:
import Vue from 'vue'; import Router from 'vue-router' import NProgress from 'nprogress' import 'nprogress/nprogress.css' import NotFound from '../views/404'; Vue.use(Router); const router = new Router({ mode: 'history', routes: [ {path:'/',redirect: '/user/login'}, { path: '/user', component: {render: h=>h("router-view")}, children: [{ path: 'login', name: 'index', component: () => import( /* webpackChunkName: "user" */ '../views/User/Login') }, { path: 'register', name: 'news', component: () => import( /* webpackChunkName: "user" */ '../views/User/Register') }, { path: '*', name: '404', component:NotFound } ] }] }) router.beforeEach((to,form,next)=>{ NProgress.start(); next(); }); router.afterEach(() => { NProgress.done(); }); export default router
可动态改变的页面布局
方案一:
定义好数据格式,一个页面可以把它划分成多个组件来构成,例如一个基本的布局:
header,main,footer。那么就可以划分成三个组件,为这三个组件添加样式,属性,事件。
{ header:{ style:{}, property:{}, event:{} }, main:{ style:{}, property:{}, event:{} } }
当数据添加进去,生成的页面就应该根据这些数据来渲染
方案二:
定义模板,根据需要切换
var hdInput = { template: "<div><input/></div>" }; var hdTextarea = { template: "<div><textarea></textarea></div>" }; new Vue({ el: "#hdcms", components: {hdInput,hdTextarea}, data:{ formType:"hdInput" } });
将菜单和路由结合
具体方案:
1.前端在本地写好路由表,以及每个路由对应的角色,也就是哪些角色可以看到这个菜单/路由;
2.登录的时候,向后端请求得到登录用户的角色(管理者、普通用户);
3.利用路由拦截,根据取到的用户角色,跟本地的路由表进行对比,过滤出用户对应的路由,并利用路由进行左侧菜单渲染一、本地写好路由表
router/index.js
//代码位置:router/index.js { path: '', component: layout, //整体页面的布局(包含左侧菜单跟主内容区域) children: [{ path: 'main', component: main, meta: { title: '首页', //菜单名称 roles: ['user', 'admin'], //当前菜单哪些角色可以看到 icon: 'el-icon-info' //菜单左侧的icon图标 } }] }
二、用户登录,获取用户的角色
获取到用户角色,存放进localStorage,然后跳转主页
//代码位置:src/components/reLoad.vue // axios.post('/temp',this.formModel).then(res=>{}) // 我暂时就不模拟了,直接取 let getUserRole = this.formModel.user === 'admin' ? 'admin' : 'user' localStorage.setItem('userRole', getUserRole) this.$router.push({ path: '/main' })
三、路由拦截beforeEach,并过滤出角色对应的路由表
关键技术点addRoutes
//代码位置:src/permission.js router.beforeEach((to, from, next) => { // 取到用户的角色 let GetRole = localStorage.getItem("userRole") // 如果登录了 if (GetRole !== 'unload') { next() //next()方法后的代码也会执行 // 1.如果路由表 没根据角色进行筛选,就筛选一次 if (!addRouFlag) { addRouFlag = true // 2.根据用户的角色、和需要动态展示的路由,生成符合用户角色的路由 var getRoutes = baseRoleGetRouters(permissionRouter, GetRole.split(",")) // 3.利用global属性,让渲染菜单的组件sideMeuns.vue重新生成左侧菜单 global.antRouter = fixedRouter.concat(getRoutes) // 4.将生成好的路由addRoutes router.addRoutes(fixedRouter.concat(getRoutes)) // 5.push之后,会重新进入到beforeEach的钩子里,直接进入第一个if判断 router.push({ path: to.path }) } } else { // 用户没登录,跳转到登录页面 if (to.path === '/') { next() } else { next('/') } } })
精细化的权限设计
权限控制是后台管理系统比较常见的需求,我们需要对某些页面的添加权限控制,可以在路由管理中的权限做一些校验。
一、权限校验函数
getCurrentAuthority()函数用于获取当前用户权限,一般来源于后台数据check()函数用于权限的校验匹配isLogin()函数用于检验用户是否登录
/** *权限校验函数 * /src/utils,/auth.js演示使用路由管理用户权限 **/ // 获取当前用户权限 export function getCurrentAuthority(){ return ["user"]; } //权限校验 export function check(authority){ const current getCurrentAuthority(); return current.some(item =authority.includes(item)); } //登录检验 export function isLogin(){ const current getcurrentAuthority(); return current ¤t[0]!="guest"; }
二、路由配置元信息
路由配置元信息meta:{ authority: ["admin"] }
/** * 路由配置元信息 * /src/router/index.js */ const routes = // 省略部分代码 { path:"/" meta:authority:["user","admin"]} component:()= import(/*webpackChunkName:"Layout"*/"../layouts/BasicLayout") //省略部分代码 }, { path:"/403", name:"403", hideInMenu:true, component:()= import(/*webpackChunkName:"exception"*/"@/views/Exception/403") } ];
三、路由守卫router.beforeEach中判断
/**登出于形到物权限 * /src/router/index.js */ import findLast from "lodash/findLast"; import {check,isLogin} from "../utils/auth"; import {notification} from "ant-design-vue"; // 路由守卫判断权限 router.beforeEach((to,from,next)=>{ if (to.path I==from.path){ NProgress.start() } const record findLast(to.matched,record => record.meta.authority); if (record && !check(record.meta.authority)){ if (lisLogin()&&to.path !=="/user/login"){ next({ path:"/user/login" }) } else if(to.path1 !== "/403"){ notification.error({ message:"403", description:"你设有访间权限,请联系管理员" }) next({ path:"/403" }) } NProgress.done(); } next(); })
使用ECharts、Antv等其他第三方库
根据项目要求,按需引入
使用第三方的开源库,可以提高效率~
使用Mock数据进行开发
一、安装:
npm i mockjs -D
-D: 仅仅在开发环境上使用
二、项目引入:
在 main.js 文件中引入mock:import '@/mock'
三、创建mock文件夹
// 引入随机函数 import { Random } from 'mockjs' // 引入Mock const Mock = require('mockjs') const userListData = Mock.mock({ 'data|10': [ { id: () => Random.id(), nickName: () => Random.cword('零一二三四五六七八九十', 3), phone: () => Random.integer(11111111111, 99999999999), tgCount: () => Random.integer(0, 200), earnings: () => Random.float(2000, 10000, 0, 2), }, ], }) function userList(res) { return { code: 200, data: userListData.data, message: '获取成功', total: 20, size: 10, user_count: 20, shop_count: 20, } } const shopListData = Mock.mock({ 'data|10': [ { shop_id: () => Random.id(), shop_name: () => Random.cword('零一二三四五六七八九十', 3), address: () => Random.city(true), shop_tel: () => Random.integer(11111111111, 99999999999), open_date: () => Random.date(), earnings: () => Random.float(2000, 10000, 0, 2), }, ], }) function shopList(res) { return { code: 200, data: shopListData.data, message: '获取推广店铺成功', total: 20, size: 10, earnings_count: 20000, shopCount: 20, } } export default { userList, shopList, }
四、定义访问的方法,接口,请求方式,请求参数
import http from '../../plugins/http' export const getUserList = (params) => { return http.get('/api/cuz/userList') } export const getShopListById = (id) => { return http.get(`/api/cuz/shopList/${id}`) }
五、拦截匹配在api中定义的请求,并对此返回模拟出的假数据
// 引入mockjs import Mock from 'mockjs' // 引入模板函数类 import ratings from './modules/ratings' import cuz from './modules/cuz' // Mock函数 const { mock } = Mock // 设置延时 Mock.setup({ timeout: 400, }) // 使用拦截规则拦截命中的请求,mock(url, post/get, 返回的数据); mock(/\/api\/ratings\/list/, 'post', ratings.list) mock(/\/api\/cuz\/userList/, 'get', cuz.userList) mock(/\/api\/cuz\/shopList/, 'get', cuz.shopList)