开发实战[CLI-3.x]
安装配置
下面已vuetao 为项目名称进行说明
1. 安装脚手架
npm install -g @vue/cli # OR yarn global add @vue/cli
2. 创建项目:vue create vuetao
从官方拿过来一张图,为啥呢(他的好看),上下方向键移动,空格键选中,看自己需要了,一般 Babel、Router、Vuex、Css Pre-processors、Linter / Formatter 这几个为必选的了,下面的两个测试用的自己选择。
3. 项目配置- vue.config.js
- 编译相关配置
// 基础路径:按自己的项目修改 线上: './.' 本地: '/' let publicPath = process.env.NODE_ENV === 'development' ? '/' : './.' module.exports = { publicPath, // https://cli.vuejs.org/zh/config/#lintonsave lintOnSave: true, outputDir: 'dist',// 编译后文件的目录名称 assetsDir: 'static', // 编译后资源文件的目录 //生产环境的 source map,可以看到打包前的源码,一般在线上有问题需要排查的时候需要 productionSourceMap: false, // 代理配置 devServer: { disableHostCheck: true,// 这个不用配置,我这边内网穿透的时候加的 port: 8085, publicPath, proxy: { '/api': { target: process.env.VUE_APP_BASE_URL,// 这个是你域名地址我加在env文件中调用了 ws: true, changeOrigin: true, pathRewrite: { '^/api': '/api/' } } } }, }
- 全局css 配置
module.exports = { css: { loaderOptions: { // 设置 scss 公用变量文件 sass: { prependData: `@import '~@/assets/style/public.scss';` } } }, }
- 删除prefetch 降低带宽压力
module.exports = { chainWebpack: config => { config.plugins .delete('prefetch') .delete('preload') } } /** * 删除懒加载模块的 prefetch preload,降低带宽压力 * https://cli.vuejs.org/zh/guide/html-and-static-assets.html#prefetch * https://cli.vuejs.org/zh/guide/html-and-static-assets.html#preload * 而且预渲染时生成的 prefetch 标签是 modern 版本的,低版本浏览器是不需要的 */
- alias 设置
module.exports = { chainWebpack: config => { config.resolve.alias .set('@', resolve('src')) .set('@v', resolve('src/views')) .set('~', resolve('public')) } }
上面单个示例合起来就是咱们需要配置的文件
使用CDN优化
打包编译速度减少项目体积 | 此处配置参考d2Admin 作者
1. 根目录创建cdn 依赖文件
// dependencies-cdn.js module.exports = [{ name: 'vue', library: 'Vue', js: 'https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js', css: '' }, { name: 'vue-router', library: 'VueRouter', js: 'https://cdn.jsdelivr.net/npm/vue-router@3.1.3/dist/vue-router.js', css: '' }, { name: 'vuex', library: 'Vuex', js: 'https://cdn.jsdelivr.net/npm/vuex@3.1.2/dist/vuex.js', css: '' }, { name: 'axios', library: 'axios', js: 'https://cdn.jsdelivr.net/npm/axios@0.19.0/dist/axios.min.js', css: '' }, { name: 'better-scroll', library: 'BScroll', js: 'https://cdn.jsdelivr.net/npm/better-scroll@1.15.2/dist/bscroll.min.js', css: '' }, { name: 'element-ui', library: 'ELEMENT', js: 'https://cdn.jsdelivr.net/npm/element-ui@2.13.0/lib/index.js', css: 'https://cdn.jsdelivr.net/npm/element-ui@2.13.0/lib/theme-chalk/index.css' }, { name: 'nprogress', library: 'NProgress', js: 'https://cdn.jsdelivr.net/npm/nprogress@0.2.0/nprogress.min.js', css: 'https://cdn.jsdelivr.net/npm/nprogress@0.2.0/nprogress.css' } ]
2. vue.config.js 配置
const cdnDependencies = require('./dependencies-cdn') // cdn 依赖文件 // 设置不参与构建的库 let externals = { 'vue': 'Vue', 'vue-router': 'VueRouter', 'vuex': 'Vuex', 'axios': 'axios', 'element-ui': 'ELEMENT' } cdnDependencies.forEach(packages => { externals[packages.name] = packages.library }) // 引入文件的 cdn 链接 const cdn = { css: cdnDependencies.map(e => e.css).filter(e => e), js: cdnDependencies.map(e => e.js).filter(e => e) } module.exports = { chainWebpack: config => { //添加 CDN 参数到 htmlWebpackPlugin 配置中 config.plugin('html').tap(args => { if (process.env.NODE_ENV === 'production') { args[0].cdn = cdn } else { args[0].cdn = [] } return args }) } }
3. html 中引入cdn 链接
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" /> <!-- 使用 CDN 加速的 CSS 文件,配置在 vue.config.js 下 --> <% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.css) { %> <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="preload" as="style"> <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet"> <% } %> <!-- 使用 CDN 加速的 JS 文件,配置在 vue.config.js 下 --> <% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %> <link href="<%= htmlWebpackPlugin.options.cdn.js[i] %>" rel="preload" as="script"> <% } %> </head> <body> <noscript> <strong> 很抱歉,没有 JavaScript 支持。 </strong> </noscript> <div id="app"> xxx </div> <!-- 使用 CDN 加速的 JS 文件,配置在 vue.config.js 下 --> <% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %> <script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script> <% } %> </body> </html>
开启GzipGzip 压缩
本来这些都是在项目使用的时候遇到需要优化的时候才会去寻求解决的,都是需要配置的我就当放一起一次性写出来了,以后查看也方便。
1. 安装插件 compression-webpack-plugin
npm install compression-webpack-plugin -D
2. vue.config.js 配置
文件开启Gzip,也可以通过服务端(如:nginx)(https://github.com/webpack-contrib/compression-webpack-plugin)
const CompressionPlugin = require('compression-webpack-plugin') // Gzip module.exports = { configureWebpack: config => { let pluginsPro = [ new CompressionPlugin({ filename: '[path].gz[query]', algorithm: 'gzip', test: new RegExp('\.(' + ['js', 'css'].join('|') + ')$'), threshold: 10240, deleteOriginalAssets: false, // 不删除源文件 minRatio: 0.8 }) ] const configNew = {} // 非开发环境 if (process.env.NODE_ENV !== 'development') { config.plugins = [ ...pluginsPro] } return configNew } }
顺带再加个webpack 去掉console和简要的文档说明,和这个压缩在同一个地方,我就直接在上面的配置中加了
const CompressionPlugin = require('compression-webpack-plugin') // Gzip module.exports = { configureWebpack: config => { let pluginsPro = [ new CompressionPlugin({ filename: '[path].gz[query]', algorithm: 'gzip', test: new RegExp('\.(' + ['js', 'css'].join('|') + ')$'), threshold: 10240, deleteOriginalAssets: false, // 不删除源文件 minRatio: 0.8 }) ] const configNew = {} // 非开发环境 if (process.env.NODE_ENV !== 'development') { configNew.externals = externals config.optimization.minimizer[0].options.terserOptions.compress.warnings = false config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true config.optimization.minimizer[0].options.terserOptions.compress.drop_debugger = true config.optimization.minimizer[0].options.terserOptions.compress.pure_funcs = ['console.log'] config .optimization.splitChunks = { chunks: 'all', cacheGroups: { libs: { name: 'chunk-libs', test: /[\/]node_modules[\/]/, priority: 10, chunks: 'initial' // only package third parties that are initially dependent }, elementUI: { name: 'chunk-elementUI', // split elementUI into a single package priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app test: /[\/]node_modules[\/]_?element-ui(.*)/ // in order to adapt to cnpm }, commons: { name: 'chunk-commons', test: resolve('src/components'), // can customize your rules minChunks: 2, // minimum common number priority: 5, reuseExistingChunk: true } } } config.optimization.runtimeChunk = 'single' config.plugins = [...config.plugins, ...pluginsPro] } return configNew } }
路由配置
- 路由钩子配置(路由拦截权限判断处理)
- 路由文件配置
- 公共路由文件配置(404,重定向,路由刷新配置等)
- 动态路由配置 (一般是权限控制后端控制菜单路由,其他动态匹配了)
路由钩子配置
// src/router/index.js import Vue from 'vue' import VueRouter from 'vue-router' // 进度条-需要安装插件,不用的话需要删除 import NProgress from 'nprogress' import 'nprogress/nprogress.css' // 路由数据 import routes from './routes' // 静态路由文件,包括公共路由,404 路由 if (process.env.NODE_ENV === 'development') { Vue.use(VueRouter)// 开发环境不用cdn,不配置cdn的话直接使用不用判断 } // 路由白名单 let WhiteList = routes.map(item=>item.path) // 导出路由 在 main.js 里使用 const router = new VueRouter({ routes }) // 路由钩子,访问路由 router.beforeEach((to, from, next) => { // 开始进度条 NProgress.start() // 这地方可对权限什么的进行判断 if(!WhiteList.includes(to.path)) {// 白名单内的不需要校验权限相关直接next //登陆判断 暂时以是否存在token作为示例 const token = util.cookies.get('token') // token这边存在cokkies 具体封装cokkies下面专门说明 if(token) { // 这边就可以对通过登陆的用户进行动态路由权限的校验处理 // 主要使用 router.addRoutes(array) 权限路由设计处理有点多,这地方不做说明了下面专门写 next() }else { if (to.path === '/login') { // 如果是登录页面路径,就直接next() next() }else { // 不然就跳转到登录; next({ name: 'login', query: { redirect: to.fullPath } }) NProgress.done() } } }else { next() } }) // 路由钩子,访问之后 router.afterEach(to => { // 结束进度条 NProgress.done() }) export default router
公共路由元配置
// src/router/routes.js 这个js 中会设计到相关的一些组件文件,我就不写出来了 const frameIn = [ { path: '/', redirect: { name: 'index' }, component: () => import(/* webpackChunkName: 'layout' */ '@/layout'),// 主体布局组件 children: [ { path:'index', name:'index', meta: { auth: true }, component:()=>import('@/system/index') }, // 刷新页面 { path: 'refresh', name: 'refresh', hidden: true, component: { beforeRouteEnter(to, from, next) { next(vm => vm.$router.replace(from.fullPath)) }, render: h => h() } }, // 页面重定向 { path: 'redirect/:route*', name: 'redirect', hidden: true, component: { beforeRouteEnter(to, from, next) { next(vm => vm.$router.replace(JSON.parse(from.params.route))) }, render: h => h() } } ] } ] const frameOut = [ { path:'/login', name:'login', component: () => import('@/system/login') } ] const errorPage = [ { path:'*', name:'404', component: () => import('@/system/error/404') } ] export default = [ ...frameIn, ...frameOut, ...errorPage ]
路由文件定义,主要是为了后端权限控制返回的时候好匹配,也为了防止组件多了不易查找
// src/router/routerMap.js const demo = { // 为了防止名称重复,这个地方命名可以以:内容形式+后端控制器+方法名去命名,如下 list_demo_page1: () => import(/* webpackChunkName: 'demo' */ '@/views/demo/page1'), list_demo_page2: () => import(/* webpackChunkName: 'demo' */ '@/views/demo/page2'), list_demo_page3: () => import(/* webpackChunkName: 'demo' */ '@/views/demo/page3') } export default file => { return { ...demo,// 上面定义几个这地方引入几个,一般一个大模块定义一个 }[file] || null }
路由跳转
<template> <div> <!-- 页面中-大体相当于a标签 --> <router-link to ="/home">返回首页</router-link> <!-- 事件操作跳转 --> <button @click="handleClickReturn">返回首页</button> </div> </template> <script> export default { methods: { handleClickReturn() { this.$router.push('/home') // 跳转路由 this.$router.replace('/home') // 路由替换导航后不会留下 history 记录 } } } </script>
路由传参于接收
总的来说有两种,一种是 query另一种是params 效果类型于 get和post 的区别,url 中看到于看不到传递的参数
<!-- 页面中-传参 命名的路由 --> <router-link :to="{ name: 'user', params: { id: 123 }}">User</router-link> <!-- 带查询参数,下面的结果为 /register?plan=private --> <router-link :to="{ path: 'register', query: { id: '122' }}">Register</router-link> <!--事件操作-传参 --> <button @click="handleClickReturn">返回首页</button> <script> export default { methods: { handleClickReturn() { this.$router.push('/home') // 跳转路由 this.$router.replace('/home') // 路由替换 } } } </script> // 接收页面- 需要注意的是使用 route 没有r this.$route.query.id this.$route.params.id
动态匹配
// 定义路由 router.js { path: "/navigate/:name", name: "navigate", component: () => import(/* webpackChunkName: 'navigate' */ "@/views/navigate") } 实现如: navigate/page1 navigate/page2 navigate/page3 ... 不同路由跳转到同一个文件,实现同一文件不同数据的渲染 // 路由跳转 this.$router.push({ name: "navigate", params: { name: 'page1' } }); // 接收页面 两种方法 // 第一种: 路由导航钩子 beforeRouteEnter(to, from, next) { next(vm => { to.params.name = vm.name; }); } // 第二种: 监听路由变化执行函数 created () { // 组件创建完后获取数据, // 此时 data 已经被 observed 了 this.fetchData() }, watch: { // 如果路由有变化,会再次执行该方法 '$route': 'fetchData' }, methods: { fetchData(e) { console.log(e) } }
组件
全局组件
- 组件定义
创建公共全局组件文件 /src/components/index.js
// 异步加载组件 const vueFiles = require.context('./tao-component', true, /component.vue$/) vueFiles.keys().forEach(key => { const component = vueFiles(key).default Vue.component(component.name, component) }) const jsFiles = require.context('./tao-component', true, /component.js$/) jsFiles.keys().forEach(key => { const component = jsFiles(key).default Vue.component(component.name, component) }) // 遍历 tao-component 目录下的所有 compoent.js 或 component.vue 文件注册为组件 // 需要注意的一点是组件中的name一定要定义并与文件夹名保持一致作为组件的名称
- 示例 tao-hello 组件
创建文件 src/components/tao-component/tao-hello/component.vue <template> <div>Hello Tao</div> </template> <script> export default { name:'tao-hello' } </script>
- 全局注册
main.js 中引入刚才创建的文件夹 // 组件 import '@/components'
- 使用
<tao-hello />
组件传值及插槽
- 父组件传值 子组件接收
//父组件 传递值不做限制,以子组件接收的类型为基准 <tao-hello :title="父组件传递" /> // 或者 <tao-hello v-bind="父组件传递" /> <!-- 子组件 文件 src/components/tao-component/tao-hello/component.vue --> <template> <div> <h1>Hello Tao</h1> <small>{{title}}</small> </div> </template> <script> export default { name:'tao-hello' , props: { // 可以用数据如: props:['title'] title: { type: String, default:null } } } </script>
- 子组件给父组件传值与父组件接收
// 子组件传递 <template> <div> <h1>Hello Tao</h1> <small>{{title}}</small> <button @click="handleClick">累加</button> </div> </template> <script> export default { name:'tao-hello' , props: { // 可以用数据如: props:['title'] title: { type: String, default:null } }, data() { return { count: 0 } }, methods: { handleClick() { this.count++ this.$emit('addEvent', this.count) // 子组件通过$emit 触发事件传递数据 } } </script> // 父组件接收-通过事件监听接收 <tao-hello :title="父组件传递" @addEvent="handleClickChi"/> <script> export default { name:'tao-hello-parent' , methods: { handleClickChi(e) { console.log(e) } } </script>
- 插槽
https://cn.vuejs.org/v2/guide/components-slots.html
// 子组件 <template> <div> <h1>Hello Tao</h1> <small>{{title}}</small> <button @click="handleClick">累加</button> <slot name="header"></slot> <slot></slot> </div> </template> <script> export default { name:'tao-hello' , props: { // 可以用数据如: props:['title'] title: { type: String, default:null } }, data() { return { count: 0 } }, methods: { handleClick() { this.count++ this.$emit('addEvent', this.count) // 子组件通过$emit 触发事件传递数据 } } </script> <!-- 注意 v-slot 只能添加在 <template> 上 --> <tao-hello :title="父组件传递" @addEvent="handleClickChi"> <template v-slot:header> <p>这里是具名插槽-header</p> </template> <template> <p>默认插槽</p> </template> </tao-hello> <!-- 另一种写法 可以使用子组件中的数据 --> <tao-hello :title="父组件传递" @addEvent="handleClickChi" v-slot="slotProps"> <p>{{slotProps}}</p> <!-- 注意默认插槽的缩写语法不能和具名插槽混用 --> </tao-hello> <script> export default { name:'tao-hello-parent' , methods: { handleClickChi(e) { console.log(e) } } </script>
深层组件传值与接收值
场景:a,b,c 三个文件 传值 a->b->c(c->b->a)
<!--page-a -->
<page-a :title="title" @_setData="handleSetData"/> <script> data() { return { title: 'xx' } }, methods: { handleSetData(e) { this.$emit('c页面传递', e) } } </script> <!--page-b --> <page-b v-bind="$attrs" v-on="$listeners"/> <script> created() { console.info(this.$attrs, this.$listeners) } </script> <!-- page-c --> <div> <button @click="handleClick">add</button> </div> <script> created() { console.log(this.$attrs) }, methods: { handleClick() { this.$emit('_setData', 'xx') } } </script>
内置组件妙用
介绍:使用 is 去绑定不同的组件,可根据js 动态切换组件
<component :is="which_to_show" /> <script> components: { demoPageOne:() => import('@/views/demo/page1') }, data() { return { which_to_show:'demoPageOne' } } </script>
keep-alive
比如当前列表页面需要详情,编辑,新增... 等每个都是一个组件,可能会打开很多次,这个时候就可以用keep-alive
<!--路由缓存 --> <keep-alive> <component :is="which_to_show" ref="ruleFomOperate" /> </keep-alive> <button @click="handleClickOpe('add')">add</button> <button @click="handleClickOpe('detail')">detail</button> <script> components: { demoPageOne:() => import('@/views/demo/page1'), demoPageTwo:() => import('@/views/demo/page2') }, data() { return { which_to_show:'demoPageOne' } }, methods: { // 根据操作类型的不同加载不同的组件 handleClickOpe(type) { if(type === 'add') { this.which_to_show = 'demoPageOne' }else { this.which_to_show = 'demoPageTwo' } } } </script>
还有应用场景如:动态表单构建,动态首页配置等等
状态管理
- state -- 辅助函数 -- mapState -- 状态树
- getter -- 辅助函数 -- mapGetters -- 从状态数中获得状态
- action -- 辅助函数 -- mapActions --
- mutations
请求响应-axios
封装
// src/api/service.js import axios from 'axios' import { Message } from 'element-ui' // 记录和显示错误 function errorLog(err) { // 打印到控制台 if (process.env.NODE_ENV === 'development') { console.log(err) } // 显示提示 Message({ message: err.message, type: 'error', duration: 5 * 1000 }) } const service = axios.create() // 不用可省略此处 service.interceptors.request.use(config => { // 可在此设置加载等待loading 当然不用loading可以不写这部分 }); service.interceptors.response.use( async response => { // 加了loading可在此数据响应后关闭 if (response.status === 200) { const dataAxios = response.data if (dataAxios.code === 200) { // 正常返回数据 return dataAxios } else { errorLog(dataAxios) return dataAxios } } else { return Promise.reject(response.msg) } }, error => { if (error && error.response) { // 对常用的http 状态码进行判断 switch (error.response.status) { case 400: error.message = '请求错误' break case 401: error.message = '未授权,请登录' break case 403: error.message = '拒绝访问' break case 404: error.message = `请求地址出错: ${error.response.config.url}` break case 408: error.message = '请求超时' break case 500: error.message = '服务器内部错误' break case 501: error.message = '服务未实现' break case 502: error.message = '网关错误' break case 503: error.message = '服务不可用' break case 504: error.message = '网关超时' break case 505: error.message = 'HTTP版本不受支持' break default: break } }else if(error.message.includes('timeout')) { // 请求超时状态 error.message = '请求超时,请检查你的网络是否正常!' } errorLog(error) return Promise.reject(error) } ) export function request(config) { let configDefault = { headers: { token: 'you-token', from: 'web', 'Content-Type': 'application/json' }, timeout: 5000, baseURL: process.env.VUE_APP_API,// 基础请求路径 data: {} } return service(Object.assign(configDefault, config)) }
API请求响应
// 自动匹配modules 下的所有.js 文件 src/api/index.js import { request } from './service' const files = require.context('./modules', false, /.js$/) const apiGenerators = files.keys().map(key => files(key).default) let api = {} apiGenerators.forEach(generator => { const apiInstance = generator({ request }) for (const apiName in apiInstance) { if (apiInstance.hasOwnProperty(apiName)) { api[apiName] = apiInstance[apiName] } } }) export default api // modules 下的接口文件示例,modules 下的接口文件名称最好和你的页面文件夹名称一致,便于管理 src/api/modules/test.js ... export default ({ request }) => ({ USER_LOGIN(data) { return request({ url: '/login/cellphone', method: 'post', data }) } })
注册插件
由于api 文件整个系统中会多地方调用,比较常用的可以使用 Vue.use() 注册为插件全局使用,需要注意的是必须提供 install方法
//src/utils/api.js import api from '@/api' export default { install(Vue, options) { Vue.prototype.$api = api } } // main.js import pluginApi from '@/utils/api' Vue.use(pluginApi)
UI 框架 Element-ui 学习总结
安装配置 文档地址
自定义主题
直接通过浏览器中修改
调整好你的主题色配置后下载
本地项目中新建文件 element-variables.scss
需要注意的是 字体路径一定是需要的
最后引入刚才新增的文件,原先的css 样式文件就可以去掉了