前言
这是一个用vue做的单页面管理系统,这里只是介绍架子搭建思路
前端架构
沿用Vue全家桶系列开发,主要技术栈:vue2.x+vue-router+vuex+element-ui1.x+axios
工程目录
项目浅析
webpack打包配置
webpack的配置主要是vue-cli生成的,经过一些简化修改如下
webpack.config
1 const path = require('path'); 2 const webpack = require('webpack'); 3 const cssnext = require('postcss-cssnext'); 4 const atImport = require('postcss-import'); 5 const cssvariables = require('postcss-css-variables'); 6 const ExtractTextPlugin = require('extract-text-webpack-plugin'); 7 const HtmlWebpackPlugin = require('html-webpack-plugin'); 8 const CopyWebpackPlugin = require('copy-webpack-plugin'); 9 10 const devSrc = 'http://localhost:8099/static/'; 11 const devOutputPath = '../dist/static'; 12 const prodSrc = './static/'; 13 const prodOutputPath = '../dist/static'; 14 15 const Util = require('./util') 16 17 const PATH_DIST = { 18 font: 'font/', 19 img: 'image/', 20 css: 'css/', 21 js: 'js/' 22 }; 23 const isProduction = process.env.NODE_ENV === 'production'; //环境,dev、production 24 console.log('isProduction',isProduction) 25 const host = isProduction ? prodSrc : devSrc; 26 const outputPath = isProduction ? prodOutputPath : devOutputPath; 27 const extractElementUI = new ExtractTextPlugin(PATH_DIST.css + 'element.css' + (isProduction ? '?[contenthash:8]' : '')); 28 const extractCSS = new ExtractTextPlugin(PATH_DIST.css + 'app.css' + (isProduction ? '?[contenthash:8]' : '')); 29 30 module.exports = function (env) { 31 let Config = { 32 entry: { 33 element: ['element-ui'], 34 vue: ['vue', 'axios', 'vue-router', 'vuex'], 35 app: './src/main.js' 36 }, 37 output: { 38 path: path.resolve(__dirname, outputPath), 39 publicPath: host, 40 filename: PATH_DIST.js + '[name].js' + (isProduction ? '?[chunkhash:8]' : '') 41 }, 42 module: { 43 rules: [ 44 { 45 test: /\.vue$/, 46 loader: 'vue-loader', 47 options: { 48 loaders: { 49 scss:Util.generateSassResourceLoader(), 50 sass:Util.generateSassResourceLoader(), 51 css: extractCSS.extract({ 52 use: 'css-loader!postcss-loader', 53 fallback: 'vue-style-loader' 54 }) 55 } 56 } 57 }, 58 { 59 test: function (path) { 60 if (/\.css$/.test(path) && (/element-ui/).test(path)) { 61 return true; 62 } else { 63 return false; 64 } 65 }, 66 loader: extractElementUI.extract({ 67 use: 'css-loader!postcss-loader' 68 }) 69 }, 70 { 71 test: function (path) { 72 if (/\.css$/.test(path) && !(/element-ui/).test(path)) { 73 return true; 74 } else { 75 return false; 76 } 77 }, 78 loader: extractCSS.extract({ 79 use: 'css-loader!postcss-loader' 80 }) 81 }, 82 { 83 test: /\.js$/, 84 loader: 'babel-loader', 85 exclude: /node_modules/ 86 }, 87 { 88 test: /\.(woff|svg|eot|ttf)\??.*$/, //字体文件 89 loader: 'file-loader', 90 options: { 91 publicPath:'../font/', 92 outputPath:PATH_DIST.font, 93 name: '[name].[ext]' 94 } 95 }, 96 { 97 test: /\.(gif|jpg|png)\??.*$/, //图片 98 loader: 'file-loader', 99 options: { 100 name: PATH_DIST.img + '[name].[ext]' 101 } 102 }, 103 { 104 test: /\.scss$/, 105 use: Util.generateSassResourceLoader() 106 }, 107 { 108 test: /\.sass/, 109 use: Util.generateSassResourceLoader() 110 }, 111 112 ] 113 }, 114 plugins: [ 115 new webpack.optimize.CommonsChunkPlugin({ 116 name: ['element', 'vue'] 117 }), 118 extractElementUI, 119 extractCSS, 120 new webpack.LoaderOptionsPlugin({ 121 options: { 122 postcss: function () { 123 return [atImport({ 124 path: [path.resolve(__dirname, '../src')] 125 }), cssnext, cssvariables]; 126 } 127 }, 128 minimize: isProduction 129 }), 130 new HtmlWebpackPlugin({ 131 title: 'JD唯品会运营后台', 132 template: 'index.html', 133 filename: '../index.html', 134 inject: false, 135 chunks: ['element', 'vue', 'app'] 136 }), 137 new webpack.DefinePlugin({ 138 'process.env.NODE_ENV': isProduction ? '"production"' : '"development"' 139 }) 140 ], 141 performance: { 142 hints: isProduction ? 'warning' : false 143 }, 144 devtool: isProduction ? false : '#eval-source-map', 145 resolve: { 146 alias: { 147 'src': path.resolve(__dirname, '../src'), 148 'scss':path.resolve(__dirname,'../src/scss/'), 149 'config':path.resolve(__dirname, '../src/config/'), 150 } 151 } 152 }; 153 154 if (isProduction) { 155 Config.plugins = Config.plugins.concat([ 156 new webpack.optimize.UglifyJsPlugin({ 157 sourceMap: true, 158 compress: { 159 warnings: false 160 } 161 }) 162 ]); 163 } else { 164 Config.devServer = { 165 historyApiFallback: true, 166 publicPath: '/static/', 167 disableHostCheck: true, 168 noInfo: true, 169 hot: true, 170 host: 'localhost', 171 port: 8099, 172 watchOptions: { 173 poll: false, 174 ignored: ['node_modules/**', 'config/**', 'common/**', 'dist/**'] 175 }, 176 headers: { 177 'Access-Control-Allow-Origin': '*' 178 } 179 }; 180 } 181 return Config; 182 };
config用到的util
1 let path = require('path') 2 let ExtractTextPlugin = require('extract-text-webpack-plugin') 3 function resolveResouce(name) { 4 let src = path.resolve(__dirname, '../src/assets/' + name); 5 return src; 6 } 7 let cssLoader = { 8 loader: 'css-loader', 9 options: { 10 minimize: process.env.NODE_ENV === 'production', 11 sourceMap: process.env.NODE_ENV !== 'production' 12 } 13 } 14 exports.generateSassResourceLoader = function (options) { 15 16 let loaders = [ 17 'css-loader', 18 'postcss-loader', 19 'sass-loader', { 20 loader: 'sass-resources-loader', 21 options: { 22 // it need a absolute path 23 resources: [resolveResouce('mixin.scss')] 24 } 25 } 26 ]; 27 return ExtractTextPlugin.extract({ 28 use: loaders, 29 fallback: 'vue-style-loader' 30 }) 31 }
src文件夹下的confg放一些东西
ajax.js配置axios
1 import axios from 'axios' 2 import qs from 'qs' 3 import store from './../store' 4 import { Message } from 'element-ui' 5 6 7 function getCookie(name) { 8 var arr, reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)"); 9 if (arr = document.cookie.match(reg)){ 10 return unescape(arr[2]); 11 } 12 else{ 13 return null; 14 } 15 } 16 17 const X_CSRF_TOKEN = getCookie("pmsTp_token"); 18 19 const service = axios.create({ 20 timeout: 30000, // 请求超时时间 21 }) 22 23 service.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded '; 24 25 //请求拦截 26 service.interceptors.request.use((config) => { 27 if (config.data&&config.data.noLoading) { 28 delete config.data.noLoading; 29 }else{ 30 store.commit(STORE_TYPE.IS_LOADING,true); 31 } 32 //config.headers.X_CSRF_TOKEN = X_CSRF_TOKEN 33 //在发送请求之前做某件事 34 if(config.method === 'post'){ 35 config.data = qs.stringify(config.data); 36 } 37 return config; 38 },(error) =>{ 39 console.log("错误的传参", 'fail'); 40 return Promise.reject(error); 41 }); 42 //返回拦截 43 service.interceptors.response.use((res) =>{ 44 store.commit(STORE_TYPE.IS_LOADING,false); 45 if (res.data.code ==-300) { 46 window.VM&&VM.$router.push({name:'redownload'}); 47 return Promise.reject(res); 48 } 49 //对响应数据做些事 50 if(res.data.code<0){ 51 if (res.data&&res.data.msg) { 52 Message({ 53 message: res.data.msg, 54 type: 'error', 55 showClose:true, 56 duration: 5 * 1000 57 }) 58 59 return Promise.reject(res); 60 } 61 } 62 return res; 63 }, (error) => { 64 store.commit(STORE_TYPE.IS_LOADING,false); 65 console.log("网络异常", 'fail'); 66 return Promise.reject(error); 67 }); 68 69 export default service
directive.js 一些自定义指令(暂时只是权限控制)
1 let install = (Vue, options= {}) => { 2 3 //权限指令 4 Vue.directive("permission", { 5 bind: function(el, binding) { 6 let permission = binding.value 7 let btnCodeList = localStorage.getItem('btnCodeList') || []; 8 if (btnCodeList.indexOf(permission) < 0 ) { 9 el.remove() 10 } 11 } 12 }); 13 14 } 15 16 export default { 17 install 18 }
mixin.js 只是定义一些全局用的方法
1 let jqMixin = { 2 methods: { 3 goto(path) { 4 if (path && typeof path === 'string') { 5 if (/^\//.test(path)) { 6 this.$router.push(path); 7 } else { 8 this.$router.push([this.$route.fullPath, path].join('/')); 9 } 10 } else if (typeof path === 'number') { 11 this.$router.go(path); 12 } 13 }, 14 logout(){ 15 window.location.href = window.location.protocol + '//' + window.location.host + '/' + "logout"; 16 }, 17 login(){ 18 window.location.reload(); 19 }, 20 queryVauleByMap(map, value, returnKey = "name", key = "id") { 21 for (var i = map.length - 1; i >= 0; i--) { 22 if (map[i][key] == value) { 23 return map[i][returnKey] 24 } 25 } 26 }, 27 formatDate(val, format) { 28 if (!val) return ''; 29 let d; 30 if (typeof val == 'number') { 31 d = new Date(val); 32 } else if (val instanceof Date) { 33 d = val; 34 } 35 var o = { 36 "m+": d.getMonth() + 1, //月份 37 "d+": d.getDate(), //日 38 "h+": d.getHours(), //小时 39 "i+": d.getMinutes(), //分 40 "s+": d.getSeconds(), //秒 41 "q+": Math.floor((d.getMonth() + 3) / 3), //季度 42 "S": d.getMilliseconds() //毫秒 43 }; 44 if (/(y+)/.test(format)) 45 format = format.replace(RegExp.$1, (d.getFullYear() + "").substr(4 - RegExp.$1.length)); 46 for (var k in o) 47 if (new RegExp("(" + k + ")").test(format)) 48 format = format.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); 49 return format; 50 }, 51 getStringfy(obj) { 52 let str = ''; 53 for (let key in obj) { 54 str += ('&' + key + '=' + obj[key]); 55 } 56 return str.substr(1); 57 }, 58 copyObj(obj) { 59 var o = {} 60 for (var i in obj) { 61 if (Object.prototype.toString.call(obj[i]) == '[object Object]' || Object.prototype.toString.call(obj[i]) == '[object Array]' ) { 62 o[i] = this.copyObj(obj[i]); 63 }else{ 64 o[i] = obj[i]; 65 } 66 } 67 return o; 68 }, 69 handleSizeChange(val) { 70 this.$store.commit(this.paramType,{ 71 pageIndex:1, 72 pageSize:val, 73 }); 74 this.$store.dispatch(this.queryType); 75 }, 76 handleCurrentChange(val) { 77 this.$store.commit(this.paramType,{ 78 pageIndex:val, 79 }); 80 this.$store.dispatch(this.queryType); 81 }, 82 clear() { 83 for (let key in this.queryParam) { 84 if (typeof this.queryParam[key] == "string") { 85 this.queryParam[key] = "" 86 } else { 87 this.queryParam[key] = -1 88 } 89 } 90 }, 91 }, 92 } 93 let install = (Vue, options = {}) => { 94 Vue.mixin(jqMixin); 95 }; 96 97 export default { 98 install 99 };
constant.js 一些定死的枚举值
1 const CONSTANT = { 2 3 } 4 export default CONSTANT
src文件夹下的router放路由配置
structure.js 定义路由
1 const structure = [{ 2 path: '/index', 3 name: 'index', 4 // redirect: '/scheduleList', //默认重定向跳转配置,暂时不开启 5 title: '首页', 6 icon: 'fa-home', 7 component: require('./../page/index/index.vue'), 8 children: [] 9 },{ 10 path: '/redownload', 11 name: 'redownload', 12 // redirect: '/scheduleList', //默认重定向跳转配置,暂时不开启 13 title: '重新登录', 14 icon: 'fa-home', 15 component: require('./../page/redownload.vue'), 16 children: [] 17 }, { 18 path: '', 19 name: 'promotions', 20 title: '促销活动', 21 children: [{ 22 path: '/promotions/fullMinus', 23 name: 'promotions/fullMinus', 24 title: '满减促销活动', 25 component: require('./../page/promotions/list.vue'), 26 children: [{ 27 path: '/viewPromotions/fullMinus/:actId/:actNo', 28 name: 'viewPromotions/fullMinus', 29 title: '查看满减促销活动', 30 component: require('./../page/promotions/view.vue') 31 }] 32 },{ 33 path: '/promotions/discount', 34 name: 'promotions/discount', 35 title: '折扣促销活动', 36 component: require('./../page/promotions/list.vue'), 37 children: [{ 38 path: '/viewPromotions/discount/:actId/:actNo', 39 name: 'viewPromotions/discount', 40 title: '查看折扣促销活动', 41 component: require('./../page/promotions/view.vue') 42 }] 43 },{ 44 path: '/promotions/buyFree', 45 name: 'promotions/buyFree', 46 title: '买免促销活动', 47 component: require('./../page/promotions/list.vue'), 48 children: [{ 49 path: '/viewPromotions/buyFree/:actId/:actNo', 50 name: 'viewPromotions/buyFree', 51 title: '查看买免促销活动', 52 component: require('./../page/promotions/view.vue') 53 }] 54 },{ 55 path: '/promotions/nOptional', 56 name: 'promotions/nOptional', 57 title: 'N元任选促销活动', 58 component: require('./../page/promotions/list.vue'), 59 children: [{ 60 path: '/viewPromotions/nOptional/:actId/:actNo', 61 name: 'viewPromotions/nOptional', 62 title: '查看N元任选促销活动', 63 component: require('./../page/promotions/view.vue') 64 }] 65 },{ 66 path: '/promotions/syncAct', 67 name: 'promotions/syncAct', 68 title: '同步结果页', 69 component: require('./../page/promotions/syncAct.vue'), 70 children: [] 71 },{ 72 path: '/promotions/innerpro', 73 name: 'promotions/innerpro', 74 title: '内部工具页面', 75 component: require('./../page/promotions/innerpro.vue'), 76 children: [] 77 }] 78 }]; 79 export default { 80 structure: structure 81 };
受之前angular的配置影响,这里配置的children属性是基于业务逻辑下的子页面,并不是vue-router的children含义,因此需要遍历structure对象将路由配置全部提取出来
因此有个index.js
index.js生成配置路由
1 import Vue from 'vue' 2 import VueRouter from 'vue-router' 3 import structure from './structure.js'; 4 import store from './../store'; 5 6 Vue.use(VueRouter) 7 let getRoutes = ((items = [], fbreadcrumbs = []) => { 8 let routes = []; 9 let structureRoutes = []; 10 items.forEach(item => { 11 let route = {} 12 let breadcrumbs = item.title ? fbreadcrumbs.concat({ 13 title: item.title, 14 path: item.path, 15 }) : fbreadcrumbs; 16 if (item.path || item.path === '') { 17 route.path = item.path; 18 route.name = item.name; 19 route.title = item.title; 20 route.meta = { 21 breadcrumbs: breadcrumbs, 22 }; 23 if (item.component) { 24 route.component = item.component; 25 } 26 if (item.redirect) { 27 route.redirect = item.redirect; 28 } 29 if (item.children && item.children.length) { 30 routes = routes.concat(getRoutes(item.children, breadcrumbs)); 31 } 32 routes.push(route); 33 } else { 34 if (item.children && item.children.length) { 35 routes = routes.concat(getRoutes(item.children, breadcrumbs)); 36 } 37 } 38 39 }); 40 return routes; 41 }) 42 43 const router = new VueRouter() 44 router.afterEach(route => { 45 store.commit(STORE_TYPE.COMMON_BREADCRUMBS_UPDATE, route.meta.breadcrumbs); 46 }); 47 let routerData = getRoutes(structure.structure); 48 router.addRoutes(routerData) 49 export default { 50 router: router 51 }
src文件夹下的store放公共或者自己喜欢放的数据
type.js 一些行为的事件名字(很鸡肋)
1 let STORE_TYPE = { 2 IS_LOADING: 'IS_LOADING', 3 COMMON_PERMISSION: 'COMMON_PERMISSION', 4 COMMON_BREADCRUMBS_UPDATE: 'COMMON_BREADCRUMBS_UPDATE', 5 DEPT_LIST: 'DEPT_LIST', 6 USER_INFO: 'USER_INFO', 7 USER_CSRF_TOKEN: 'USER_CSRF_TOKEN', 8 USER_PERMISSION: 'USER_PERMISSION', 9 /* 10 promotions 11 */ 12 PROMOTIONS_DATA_INIT: 'PROMOTIONS_DATA_INIT', 13 PROMOTIONS_PARAM: 'PROMOTIONS_PARAM', 14 PROMOTIONS_PARAM_START: 'PROMOTIONS_PARAM_START', 15 PROMOTIONS_CACHEPARAM: 'PROMOTIONS_CACHEPARAM', 16 PROMOTIONS_LISTDATA: 'PROMOTIONS_LISTDATA', 17 PROMOTIONS_REMOVE: 'PROMOTIONS_REMOVE', 18 PROMOTIONS_ACTIVITYINFO: 'PROMOTIONS_ACTIVITYINFO', 19 PROMOTIONS_GOODSINFO: 'PROMOTIONS_GOODSINFO', 20 PROMOTIONS_GOODSPARAM: 'PROMOTIONS_GOODSPARAM', 21 PROMOTIONS_SYNCSTATUS: 'PROMOTIONS_SYNCSTATUS', 22 PROMOTIONS_SYNC: 'PROMOTIONS_SYNC', 23 PROMOTIONS_BATCHSYNC: 'PROMOTIONS_BATCHSYNC', 24 PROMOTIONS_SYNCACT: 'PROMOTIONS_SYNCACT', 25 26 27 }; 28 if (window && !window.STORE_TYPE) { 29 window.STORE_TYPE = STORE_TYPE; 30 } 31 export default { 32 STORE_TYPE 33 };
index.js 暴露给外部的vuex对象
1 import Vue from 'vue' 2 import Vuex from 'vuex' 3 import './types' 4 import common from './modules/common' 5 import user from './modules/user' 6 import promotions from './modules/promotions' 7 8 Vue.use(Vuex) 9 10 export default new Vuex.Store({ 11 modules: { 12 common, 13 user, 14 promotions, 15 } 16 })
modules 放置各个vuex对象定义的文件夹
这儿只帖个common.js的定义吧
1 import axios from 'config/ajax.js' 2 3 const state = { 4 isLoading: false, 5 permission: {}, 6 dept_list:[], 7 breadcrumbs: [], 8 } 9 10 const getters = { 11 } 12 13 const mutations = { 14 [STORE_TYPE.COMMON_PERMISSION](state, data) { 15 if (JSON.stringify(state.permission) == '{}') { 16 state.permission = data; 17 } 18 }, 19 [STORE_TYPE.IS_LOADING](state, data) { 20 state.isLoading = data; 21 }, 22 [STORE_TYPE.DEPT_LIST](state, data) { 23 state.dept_list = data; 24 }, 25 [STORE_TYPE.COMMON_BREADCRUMBS_UPDATE](state, breadcrumbs) { 26 state.breadcrumbs = breadcrumbs; 27 }, 28 } 29 30 const actions = { 31 [STORE_TYPE.COMMON_PERMISSION]({ 32 commit 33 }) { 34 return axios.get("/common/getUserPermissions").then(res => { 35 if (res.data.data) { 36 commit(STORE_TYPE.COMMON_PERMISSION, res.data.data); 37 return res.data.data; 38 } 39 }); 40 }, 41 42 [STORE_TYPE.DEPT_LIST]({ 43 commit 44 }) { 45 return axios.get("/common/getDepartment").then(res => { 46 if (res.data.data) { 47 commit(STORE_TYPE.DEPT_LIST, res.data.data.children); 48 return res.data.data.children; 49 } 50 }); 51 }, 52 53 } 54 55 export default { 56 state, 57 getters, 58 mutations, 59 actions 60 }
全局vue对象模板 app.vue
1 239 10 11 34 354 567 8
废话了这么多,下面就来生成vue对象吧
main.js如下
import Vue from 'vue' import App from './App.vue' import store from './store/index.js' import router from './router/index.js' import ElementUI from 'element-ui' import jqMixin from './config/mixin.js' import jqDirective from './config/directive.js' import LazyRender from 'vue-lazy-render' import './config/constant.js' import './config/ajax.js' import '../element-ui/index.css' //element-ui默认主题UI import '../font-awesome/css/font-awesome.min.css' //font-awesome字体插件: http://fontawesome.io/icons/ import './assets/ui.scss' Vue.config.devtools = true; Vue.config.productionTip = false; Vue.use(ElementUI) Vue.use(jqMixin) Vue.use(jqDirective) Vue.use(LazyRender) localStorage.removeItem('newRoutePath'); localStorage.removeItem('oldRoutePath'); store.dispatch(STORE_TYPE.COMMON_PERMISSION).then((user) => { localStorage.setItem('btnCodeList', user.btnCodeList); window.VM = new Vue({ el: '#app', store: store, router: router.router, render: h => h(App), watch:{ $route:function (newVal,val) { localStorage.setItem('newRoutePath',newVal.path); localStorage.setItem('oldRoutePath',val.path); } } }) },(data)=>{ window.VM = new Vue({ el: '#app', store: store, router: router.router, render: h => h(App), }) // VM.$router.push({name:'redownload'}) }).catch(e=>{ console.log(e); })
剩下的就是page文件夹放置各个页面的业务代码了,这儿就贴了
源码都在这https://github.com/houpeace/vueProject 喜欢就去看看吧,记得加星哟~