作者:羊先生
转发链接:https://segmentfault.com/a/1190000022512358
完整的架构配置
const path = require('path');const UglifyJsPlugin = require('uglifyjs-webpack-plugin') // 去掉注释const CompressionWebpackPlugin = require('compression-webpack-plugin'); // 开启压缩const { HashedModuleIdsPlugin } = require('webpack');function resolve(dir) { return path.join(__dirname, dir)}const isProduction = process.env.NODE_ENV === 'production';// cdn预加载使用const externals = { 'vue': 'Vue', 'vue-router': 'VueRouter', 'vuex': 'Vuex', 'axios': 'axios', "element-ui": "ELEMENT"}const cdn = { // 开发环境 dev: { css: [ 'https://unpkg.com/element-ui/lib/theme-chalk/index.css' ], js: [] }, // 生产环境 build: { css: [ 'https://unpkg.com/element-ui/lib/theme-chalk/index.css' ], js: [ 'https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js', 'https://cdn.jsdelivr.net/npm/[email protected]/dist/vue-router.min.js', 'https://cdn.jsdelivr.net/npm/[email protected]/dist/vuex.min.js', 'https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js', 'https://unpkg.com/element-ui/lib/index.js' ] }}module.exports = { lintOnSave: false, // 关闭eslint productionSourceMap: false, publicPath: './', outputDir: process.env.outputDir, // 生成文件的目录名称 chainWebpack: config => { config.resolve.alias .set('@', resolve('src')) // 压缩图片 config.module .rule('images') .test(/.(png|jpe?g|gif|svg)(?.*)?$/) .use('image-webpack-loader') .loader('image-webpack-loader') .options({ bypassOnDebug: true }) // webpack 会默认给commonChunk打进chunk-vendors,所以需要对webpack的配置进行delete config.optimization.delete('splitChunks') config.plugin('html').tap(args => { if (process.env.NODE_ENV === 'production') { args[0].cdn = cdn.build } if (process.env.NODE_ENV === 'development') { args[0].cdn = cdn.dev } return args }) config .plugin('webpack-bundle-analyzer') .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin) }, configureWebpack: config => { const plugins = []; if (isProduction) { plugins.push( new UglifyJsPlugin({ uglifyOptions: { output: { comments: false, // 去掉注释 }, warnings: false, compress: { drop_console: true, drop_debugger: false, pure_funcs: ['console.log']//移除console } } }) ) // 服务器也要相应开启gzip plugins.push( new CompressionWebpackPlugin({ algorithm: 'gzip', test: /.(js|css)$/,// 匹配文件名 threshold: 10000, // 对超过10k的数据压缩 deleteOriginalAssets: false, // 不删除源文件 minRatio: 0.8 // 压缩比 }) ) // 用于根据模块的相对路径生成 hash 作为模块 id, 一般用于生产环境 plugins.push( new HashedModuleIdsPlugin() ) // 开启分离js config.optimization = { runtimeChunk: 'single', splitChunks: { chunks: 'all', maxInitialRequests: Infinity, minSize: 1000 * 60, cacheGroups: { vendor: { test: /[/]node_modules[/]/, name(module) { // 排除node_modules 然后吧 @ 替换为空 ,考虑到服务器的兼容 const packageName = module.context.match(/[/]node_modules[/](.*?)([/]|$)/)[1] return `npm.${packageName.replace('@', '')}` } } } } }; // 取消webpack警告的性能提示 config.performance = { hints: 'warning', //入口起点的最大体积 maxEntrypointSize: 1000 * 500, //生成文件的最大体积 maxAssetSize: 1000 * 1000, //只给出 js 文件的性能提示 assetFilter: function (assetFilename) { return assetFilename.endsWith('.js'); } } // 打包时npm包转CDN config.externals = externals; } return { plugins } }, pluginOptions: { // 配置全局less 'style-resources-loader': { preProcessor: 'less', patterns: [resolve('./src/style/theme.less')] } }, devServer: { open: false, // 自动启动浏览器 host: '0.0.0.0', // localhost port: 6060, // 端口号 https: false, hotOnly: false, // 热更新 proxy: { '^/sso': { target: process.env.VUE_APP_SSO, // 重写路径 ws: true, //开启WebSocket secure: false, // 如果是https接口,需要配置这个参数 changeOrigin: true } } }}
We're sorry but doesn't work properly without JavaScript enabled. Please enable it to continue.
new CompressionWebpackPlugin({ algorithm: 'gzip', test: /.(js|css)$/, // 匹配文件名 threshold: 10000, // 对超过10k的数据压缩 deleteOriginalAssets: false, // 不删除源文件 minRatio: 0.8 // 压缩比})
安装cnpm i uglifyjs-webpack-plugin -D
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')new UglifyJsPlugin({ uglifyOptions: { output: { comments: false, // 去掉注释 }, warnings: false, compress: { drop_console: true, drop_debugger: false, pure_funcs: ['console.log'] //移除console } }})
chainWebpack: config => { // 压缩图片 config.module .rule('images') .test(/.(png|jpe?g|gif|svg)(?.*)?$/) .use('image-webpack-loader') .loader('image-webpack-loader') .options({ bypassOnDebug: true })}
devServer: { open: false, // 自动启动浏览器 host: '0.0.0.0', // localhost port: 6060, // 端口号 https: false, hotOnly: false, // 热更新 proxy: { '^/sso': { target: process.env.VUE_APP_SSO, // 重写路径 ws: true, //开启WebSocket secure: false, // 如果是https接口,需要配置这个参数 changeOrigin: true } }}
在vscode中插件安装栏搜索 Path Intellisense 插件,打开settings.json文件添加 以下代码 "@": "${workspaceRoot}/src",按以下添加
{ "workbench.iconTheme": "material-icon-theme", "editor.fontSize": 16, "editor.detectIndentation": false, "guides.enabled": false, "workbench.colorTheme": "Monokai", "path-intellisense.mappings": { "@": "${workspaceRoot}/src" }}
在项目package.json所在同级目录下创建文件jsconfig.json
{ "compilerOptions": { "target": "ES6", "module": "commonjs", "allowSyntheticDefaultImports": true, "baseUrl": "./", "paths": { "@/*": ["src/*"] } }, "exclude": [ "node_modules" ]}
如果还没请客官移步在vscode中使用别名@按住ctrl也能跳转对应路径
在根目录新建
# 开发环境NODE_ENV='development'VUE_APP_SSO='http://http://localhost:9080'
NODE_ENV = 'production' # 如果我们在.env.test文件中把NODE_ENV设置为test的话,那么打包出来的目录结构是有差异的VUE_APP_MODE = 'test'VUE_APP_SSO='http://http://localhost:9080'outputDir = test
NODE_ENV = 'production'VUE_APP_SSO='http://http://localhost:9080'
"scripts": { "build": "vue-cli-service build", //生产打包 "lint": "vue-cli-service lint", "dev": "vue-cli-service serve", // 开发模式 "test": "vue-cli-service build --mode test", // 测试打包 "publish": "vue-cli-service build && vue-cli-service build --mode test" // 测试和生产一起打包 }
router/index.js文件
import Vue from 'vue';import VueRouter from 'vue-router'Vue.use(VueRouter)import defaultRouter from './defaultRouter'import dynamicRouter from './dynamicRouter';import store from '@/store';const router = new VueRouter({ routes: defaultRouter, mode: 'hash', scrollBehavior(to, from, savedPosition) { // keep-alive 返回缓存页面后记录浏览位置 if (savedPosition && to.meta.keepAlive) { return savedPosition; } // 异步滚动操作 return new Promise((resolve, reject) => { setTimeout(() => { resolve({ x: 0, y: 0 }) }, 200) }) }})// 消除路由重复警告const selfaddRoutes = function (params) { router.matcher = new VueRouter().matcher; router.addRoutes(params);}// 全局路由拦截router.beforeEach((to, from, next) => { const { hasRoute } = store.state; // 防止路由重复添加 if (hasRoute) { next() } else { dynamicRouter(to, from, next, selfaddRoutes) }})export default router;
dynamicRouter.js
import http from '@/http/request';import defaultRouter from './defaultRouter'import store from '@/store'// 重新构建路由对象const menusMap = function (menu) { return menu.map(v => { const { path, name, component } = v const item = { path, name, component: () => import(`@/${component}`) } return item; })}// 获取路由const addPostRouter = function (to, from, next, selfaddRoutes) { http.windPost('/mock/menu') // 发起请求获取路由 .then(menu => { defaultRouter[0].children.push(...menusMap(menu)); selfaddRoutes(defaultRouter); store.commit('hasRoute', true); next({ ...to, replace: true }) })}export default addPostRouter;
defaultRouter.js 默认路由
const main = r => require.ensure([], () => r(require('@/layout/main.vue')), 'main')const index = r => require.ensure([], () => r(require('@/view/index/index.vue')), 'index')const about = r => require.ensure([], () => r(require('@/view/about/about.vue')), 'about')const detail = r => require.ensure([], () => r(require('@/view/detail/detail.vue')), 'detail')const error = r => require.ensure([], () => r(require('@/view/404/404.vue')), 'error');const defaultRouter = [ { path: "/", component: main, // 布局页 redirect: { name: "index" }, children:[ { path: '/index', component: index, name: 'index', meta: { title: 'index' } }, { path: '/about', component: about, name: 'about', meta: { title: 'about' } }, { path: '/detail', component: detail, name: 'detail', meta: { title: 'detail' } } ] }, { path: '/404', component: error, name: '404', meta: { title: '404' } }]export default defaultRouter;
import axios from "axios";import merge from 'lodash/merge'import qs from 'qs'/** * 实例化 * config是库的默认值,然后是实例的 defaults 属性,最后是请求设置的 config 参数。后者将优先于前者 */const http = axios.create({ timeout: 1000 * 30, withCredentials: true, // 表示跨域请求时是否需要使用凭证});/** * 请求拦截 */http.interceptors.request.use(function (config) { return config;}, function (error) { return Promise.reject(error);});/** * 响应拦截 */http.interceptors.response.use(response => { // 过期之类的操作 if (response.data && (response.data.code === 401)) { // window.location.href = ''; 重定向 } return response}, error => { return Promise.reject(error)})/** * 请求地址处理 */http.adornUrl = (url) => { return url;}/** * get请求参数处理 * params 参数对象 * openDefultParams 是否开启默认参数 */http.adornParams = (params = {}, openDefultParams = true) => { var defaults = { t: new Date().getTime() } return openDefultParams ? merge(defaults, params) : params}/** * post请求数据处理 * @param {*} data 数据对象 * @param {*} openDefultdata 是否开启默认数据? * @param {*} contentType 数据格式 * json: 'application/json; charset=utf-8' * form: 'application/x-www-form-urlencoded; charset=utf-8' */http.adornData = (data = {}, openDefultdata = true, contentType = 'json') => { var defaults = { t: new Date().getTime() } data = openDefultdata ? merge(defaults, data) : data return contentType === 'json' ? JSON.stringify(data) : qs.stringify(data)}/** * windPost请求 * @param {String} url [请求地址] * @param {Object} params [请求携带参数] */http.windPost = function (url, params) { return new Promise((resolve, reject) => { http.post(http.adornUrl(url), qs.stringify(params)) .then(res => { resolve(res.data) }) .catch(error => { reject(error) }) })}/** * windJsonPost请求 * @param {String} url [请求地址] * @param {Object} params [请求携带参数] */http.windJsonPost = function (url, params) { return new Promise((resolve, reject) => { http.post(http.adornUrl(url), http.adornParams(params)) .then(res => { resolve(res.data) }) .catch(error => { reject(error) }) })}/** * windGet请求 * @param {String} url [请求地址] * @param {Object} params [请求携带参数] */http.windGet = function (url, params) { return new Promise((resolve, reject) => { http.get(http.adornUrl(url), { params: params }) .then(res => { resolve(res.data) }) .catch(error => { reject(error) }) })}/** * 上传图片 */http.upLoadPhoto = function (url, params, callback) { let config = {} if (callback !== null) { config = { onUploadProgress: function (progressEvent) { //属性lengthComputable主要表明总共需要完成的工作量和已经完成的工作是否可以被测量 //如果lengthComputable为false,就获取不到progressEvent.total和progressEvent.loaded callback(progressEvent) } } } return new Promise((resolve, reject) => { http.post(http.adornUrl(url), http.adornParams(params), config) .then(res => { resolve(res.data) }) .catch(error => { reject(error) }) })}export default http;
const Mock = require('mockjs')// 获取 mock.Random 对象const Random = Mock.Random// mock新闻数据,包括新闻标题title、内容content、创建时间createdTimeconst produceNewsData = function () { let newsList = [] for (let i = 0; i < 3; i++) { let newNewsObject = {} if(i === 0){ newNewsObject.path = '/add/article'; newNewsObject.name = 'add-article'; newNewsObject.component = 'modules/add/article/article'; } if(i === 1){ newNewsObject.path = '/detail/article'; newNewsObject.name = 'detail-article'; newNewsObject.component = 'modules/detail/article/article' } if(i === 2){ newNewsObject.path = '/edit/article'; newNewsObject.name = 'edit-article'; newNewsObject.component = 'modules/edit/article/article' } newsList.push(newNewsObject) } return newsList;}Mock.mock('/mock/menu', produceNewsData)
pluginOptions: { // 配置全局less 'style-resources-loader': { preProcessor: 'less', patterns: [resolve('./src/style/theme.less')] }}
安装cnpm i webpack -D
const { HashedModuleIdsPlugin } = require('webpack');configureWebpack: config => { const plugins = []; plugins.push( new HashedModuleIdsPlugin() )}
安装cnpm i webpack-bundle-analyzer -D
chainWebpack: config => { config .plugin('webpack-bundle-analyzer') .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)}
点击获取完整代码github:https://github.com/hangjob/vue-admin
《怎样为你的 Vue.js 单页应用提速》
《聊聊昨晚尤雨溪现场针对Vue3.0 Beta版本新特性知识点汇总》
《【新消息】Vue 3.0 Beta 版本发布,你还学的动么?》
《Vue真是太好了 壹万多字的Vue知识点 超详细!》
《Vue + Koa从零打造一个H5页面可视化编辑器——Quark-h5》
《深入浅出Vue3 跟着尤雨溪学 TypeScript 之 Ref 【实践】》
《手把手教你深入浅出vue-cli3升级vue-cli4的方法》
《Vue 3.0 Beta 和React 开发者分别杠上了》
《手把手教你用vue drag chart 实现一个可以拖动 / 缩放的图表组件》
《Vue3 尝鲜》
《总结Vue组件的通信》
《手把手让你成为更好的Vue.js开发人员的12个技巧和窍门【实践】》
《Vue 开源项目 TOP45》
《2020 年,Vue 受欢迎程度是否会超过 React?》
《尤雨溪:Vue 3.0的设计原则》
《使用vue实现HTML页面生成图片》
《实现全栈收银系统(Node+Vue)(上)》
《实现全栈收银系统(Node+Vue)(下)》
《vue引入原生高德地图》
《Vue合理配置WebSocket并实现群聊》
《多年vue项目实战经验汇总》
《vue之将echart封装为组件》
《基于 Vue 的两层吸顶踩坑总结》
《Vue插件总结【前端开发必备】》
《Vue 开发必须知道的 36 个技巧【近1W字】》
《构建大型 Vue.js 项目的10条建议》
《深入理解vue中的slot与slot-scope》
《手把手教你Vue解析pdf(base64)转图片【实践】》
《使用vue+node搭建前端异常监控系统》
《推荐 8 个漂亮的 vue.js 进度条组件》
《基于Vue实现拖拽升级(九宫格拖拽)》
《手摸手,带你用vue撸后台 系列二(登录权限篇)》
《手摸手,带你用vue撸后台 系列三(实战篇)》
《前端框架用vue还是react?清晰对比两者差异》
《Vue组件间通信几种方式,你用哪种?【实践】》
《浅析 React / Vue 跨端渲染原理与实现》
《10个Vue开发技巧助力成为更好的工程师》
《手把手教你Vue之父子组件间通信实践讲解【props、$ref 、$emit】》
《1W字长文+多图,带你了解vue的双向数据绑定源码实现》
《深入浅出Vue3 的响应式和以前的区别到底在哪里?【实践】》
《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》
《基于Vue/VueRouter/Vuex/Axios登录路由和接口级拦截原理与实现》
《手把手教你D3.js 实现数据可视化极速上手到Vue应用》
《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【上】》
《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【中】》
《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【下】》
《Vue3.0权限管理实现流程【实践】》
《后台管理系统,前端Vue根据角色动态设置菜单栏和路由》
《13个精选的React JS框架》
《深入浅出画图讲解React Diff原理【实践】》
《【React深入】React事件机制》
《Vue 3.0 Beta 和React 开发者分别杠上了》
《手把手深入Redux react-redux中间件设计及原理(上)【实践】》
《手把手深入Redux react-redux中间件设计及原理(下)【实践】》
《前端框架用vue还是react?清晰对比两者差异》
《为了学好 React Hooks, 我解析了 Vue Composition API》
《【React 高级进阶】探索 store 设计、从零实现 react-redux》
《写React Hooks前必读》
《深入浅出掌握React 与 React Native这两个框架》
《可靠React组件设计的7个准则之SRP》
《React Router v6 新特性及迁移指南》
《用React Hooks做一个搜索栏》
《你需要的 React + TypeScript 50 条规范和经验》
《手把手教你绕开React useEffect的陷阱》
《浅析 React / Vue 跨端渲染原理与实现》
《React 开发必须知道的 34 个技巧【近1W字】》
《三张图详细解说React组件的生命周期》
《手把手教你深入浅出实现Vue3 & React Hooks新UI Modal弹窗》
《手把手教你搭建一个React TS 项目模板》
《全平台(Vue/React/微信小程序)任意角度旋图片裁剪组件》
《40行代码把Vue3的响应式集成进React做状态管理》
《手把手教你深入浅出React 迷惑的问题点【完整版】》
《React可用于哪些Web开发场景?具体怎么做?》
作者:羊先生
转发链接:https://segmentfault.com/a/1190000022512358