最近比较忙,所以技术博客停滞了一会。当然也不是什么事都没做,稍微整理下以前的基础知识点,有兴趣的同学可以看一下:https://docs.wuwei.fun 。 好东西都是需要沉淀的,结合自己的经历,咱们来一起聊聊什么是 H5,及 Vue 的使用。
H5开发
谈论Vue
新架构Vue
实战Vue
Vue常见用法
Vue组件之间的传参
Vue常用生命周期
mounted
destroyed
activated
deactivated
Vue常用属性
props
data
components
methods
computed
watch
Vue-Router常用方法
Vuex的实现方式
Sass vue提供的scoped属性
工具类utils,防抖、节流、获取url后面的所有参数
axios实现get、post请求
请求服务器接口跨域
nginx支持gzip
实现一个公共toast组件
打包发布上线
1.H5开发 有个职业,叫前端开发工程师,然后随着手机App的普及,慢慢地,被人叫H5开发工程师。主要原因是互联网的入口发生了转变,之前大部分业务都集中在电脑
,各种网站,如电商官网、管理后台等等。截至今日,手机
才是互联网的最大入口。
根据《中国互联网报告》,手机网民已经超过8亿,人均每天上网三个多小时。毫不奇怪,手机应用软件(mobile application,简称 mobile App)的开发工程师供不应求,一直是 IT 招聘的热门。
表面上看,手机 App 都是同样的东西,就是手机上的应用程序,点击图标就能运行,但是它们的底层技术不一样。按照开发技术,App 可以分成三大类。
原生应用(native application,简称 Native App)
Web 应用(web application,简称 Web App)
混合应用(hybrid application,简称 hybrid App)
这三类 App 的技术模型都不一样,各有优缺点。企业一般会选择其中一种作为主要技术栈,构建自己的手机 App。
H5的特点:它是目前主流开发技术之一,容易上手,开发周期短、成本低、兼容传统 Web 开发。
H5 这个词,可以理解成混合 App 模型,只不过它特指混合 App 的前端部分
。 因为混合 App 的前端就是 HTML5 网页,所以简称 H5。这个词是国内独有的,基本上都是前端程序员在用,国外不用这个词,就直接叫混合 App。
2.谈论Vue 我是在2016年4月就开始接触并使用 vue 这个框架,当时版本 vue1.0。 当时写了第一篇文章《初识Vue.js》 ,有兴趣的同学可以看一下。
时隔3年多,再用 vue 去重构 h5 项目,会有额外的感受。 首先,感觉自己的眼光没错,尽管 vue 是国人开发的框架,作者尤雨溪,但是其双向绑定、上手easy、开发速度巨快等特点,备受国人喜爱。 其次,react 和 vue 是目前前端项目最热门的两门框架,我的三款app技术栈都是 react ,最近在做的云笔记客户端也用的react,按理说 react 应该比 vue 牛逼才对,其实不然。一款框架是否牛逼,取决于开发者自己的能力,react 和 vue 虽然是两个不同的框架,但是它们的底层原理都是很相似的,无非在上层堆砌了自己的概念上去。 所以我们无需去对比到底哪个框架牛逼,引用尤大的一句话:
说到底,就算你证明了 A 比 B 牛逼,也不意味着你或者你的项目就牛逼了… 比起争这个,不如多想想怎么让自己变得更牛逼吧。
vue 在我看来,是互联网公司不可多得的,想快、再快、更快出功能的框架
首选。因为其学习成本比 react 低很多,所以很容易上手,只要会写传递的html页面、css布局,会用简单的vue了。
3.新架构Vue 最近在用最新的 vue 搭 h5 项目,直接上菜:
涉及的框架及插件有: vue全家桶:vue、vuex、vue-router、vuex-router-sync; sass:node-sass、sass-loader; eslint规范:eslint、eslint-config-airbnb-base; 常用框架:axios、fastclick、reset css; px转rem:flexible、postcss-plugin-px2rem; webpack打包调试优化:compression-webpack-plugin、uglifyjs-webpack-plugin、vconsole-webpack-plugin;
涉及的官网: vue:https://cn.vuejs.org/v2/guide vuex:https://vuex.vuejs.org/zh vue router:https://router.vuejs.org/zh vuex-router-sync:https://github.com/vuejs/vuex-router-sync
node-sass:https://github.com/sass/node-sass sass-loader:https://github.com/webpack-contrib/sass-loader
eslint:https://eslint.bootcss.com/docs/about eslint-config-airbnb:https://www.npmjs.com/package/eslint-config-airbnb airbnb js rules:https://github.com/airbnb/javascript
axios:https://github.com/axios/axios fastclick:https://github.com/ftlabs/fastclick reset css:https://meyerweb.com/eric/tools/css/reset/
flexible:https://github.com/amfe/lib-flexible postcss-plugin-px2rem:https://www.npmjs.com/package/postcss-plugin-px2rem
compression-webpack-plugin:https://www.npmjs.com/package/compression-webpack-plugin uglifyjs-webpack-plugin:https://www.npmjs.com/package/uglifyjs-webpack-plugin vconsole-webpack-plugin:https://www.npmjs.com/package/vconsole-webpack-plugin
1.vue+vuex+vue-router+sass+eslint 安装最新vue-cli脚手架,当前使用的最新是@vue/cli 4.4.1
。 安装说明:https://cli.vuejs.org/zh/guide/installation.html
1 2 3 4 // 全局安装vue-cli脚手架npm install -g @vue/cli // 新建一个vue项目vue create app
选Manually select feature,自定义自己所需,选择Babel、Router、Vuex、CSS Pre-processors、Linter / Formatter。 继续Y,然后选择Sass/SCSS(with node-sass),然后选择ESLint + Airbnb config,接着选择Lint on save,一直回车,如图所示。
通过vue-cli脚手架,我们可以轻松将vue全家桶和sass、eslint搭建出来。让我们打开项目运行一下:
1 2 3 4 5 6 7 8 9 10 // 打开app目录cd app // 本地运行npm run serve // 运行成功App running at: - Local: http:// localhost:8080 / - Network: http:// 172.18 .72.244 :8080 /
Vue:MVVM框架,具有双向绑定的特性,可以轻松实现业务相关逻辑。 Vue-Router:Vue的路由机制,有hash和history两种模式,可以实现SPA单页面应用的跳转,使页面不会重载。 Vuex:Vue的状态容器,主要作用于兄弟组件之间的变量改变,后续会有详细说明。 Sass:Css预编译工具,防止样式混淆。 ESLint:JS代码规范,选择国外Airbnb爱彼迎的代码规则,这样团队开发风格会得到统一,能避免this指向或变量提升导致的常规错误。
接着使用VS Code,打开设置,在工作区设置中勾选启用eslint,和保存自动修复eslint语法。
2.导入vuex-router-sync、axios、fastclick,重置默认样式css 1 2 // 安装vuex-router-sync 、axios、fastclick npm install vuex-router-sync axios fastclick --save
在main.js引入sync,axios:
1 2 3 4 5 6 7 import Vue from 'vue' ;import fastClick from 'fastclick' ;import { sync } from 'vuex-router-sync' ;... fastClick.attach(document .body); sync(store, router); ...
更改App.vue,将其换成如下代码:
1 2 3 4 5 6 7 8 9 10 <template > <div id ="app" > <keep-alive > <router-view > </router-view > </keep-alive > </div > </template > <style lang ="scss" > @import "./styles/common.scss" ;</style >
使用keep-alive
Vue内置组件,用来对组件进行缓存,从而节省性能,由于是一个抽象组件,所以在v页面渲染完毕后不会被渲染成一个DOM元素。当组件在keep-alive内被切换时,组件的activated
、deactivated
这两个生命周期钩子函数会被执行。router-view>
组件是Vue-Router提供的组件,渲染路径匹配到的视图组件。 详细说明,可参考:https://router.vuejs.org/zh/api/#router-view。
在common.scss中引入reset.css,因为手机浏览器千差万别,每个厂家对其浏览器做了不同的定制和优化,因为我们需要重置css样式,将其保持一致,这样我们在写css样式的时候,风格和尺寸才能得到统一。 我们访问https://meyerweb.com/eric/tools/css/reset,将其重置css样式引入进来,也可以加入自己常用的公共css:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 html , body , div , span , applet, object , iframe ,h1 , h2 , h3 , h4 , h5 , h6 , p , blockquote , pre,a , abbr , acronym, address , big, cite , code ,del , dfn , em , img , ins , kbd , q , s, samp ,small, strike, strong , sub, sup , tt, var , b , u, i , center,dl , dt , dd , ol , ul , li ,fieldset , form , label , legend ,table , caption , tbody , tfoot , thead , tr , th , td ,article , aside , canvas , details , embed, figure , figcaption , footer , header , hgroup , menu , nav , output, ruby, section , summary ,time , mark , audio , video { margin : 0 ; padding : 0 ; border : 0 ; font-size : 100% ; font : inherit; vertical-align : baseline; } article , aside , details , figcaption , figure , footer , header , hgroup , menu , nav , section { display : block; } body { line-height : 1 ; } ol , ul { list-style : none; } blockquote , q { quotes : none; } blockquote :before, blockquote:after,q:before, q:after { content: '' ; content : none; } table { border-collapse : collapse; border-spacing : 0 ; } html { width : 100% ; height : 100% ; } body { width : 100% ; height : 100% ; } #app { width : 100% ; height : 100% ; } .hide { display : none; } .show { display : block; }
axios是一个基于 promise 的 http 库,后续在调用get、post请求的时候再来说明。 fastClick是为了解决移动端点击事件300ms延迟的问题,至于为什么会有这个问题,请自行百度即可。
3.新增vue.config.js vue.config.js 是一个可选的配置文件,如果项目的根目录中存在这个文件,那么它会被 @vue/cli-service 自动加载。 详细说明和配置:https://cli.vuejs.org/zh/config/#vue-config-js。 我们先新建一个vue.config.js,新增如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 const path = require ('path' );module .exports = { configureWebpack: { resolve: { alias: { '@' : path.resolve(__dirname, 'src' ), }, }, }, };
这段代码意思是配置webpack,将整个src目录,取个别名@。 这样的话,我们可以修改router.js的import:
1 2 3 4 5 import Home from '../views/Home.vue' ;import About from '../views/About.vue' ;... import Home from '@/views/Home.vue' ;import About from '@/views/About.vue' ;
4.新增.eslintrc.js airbnb规范有些确实不够灵活,因此我们可以将不需要的规则手动关掉。找到根目录的package.json:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ... "eslintConfig" : { "root" : true , "env" : { "node" : true }, "extends" : [ "plugin:vue/essential" , "@vue/airbnb" ], "rules" : { // 增加自己不需要的规则 "max-len" : "off" , ... } } ...
想关闭的规则,可以参考:https://eslint.bootcss.com/docs/rules/。
5.实现样式自适应 H5开发会根据蓝湖的标注去写对应页面,页面会根据手机尺寸的不同而做出相应的改变,但默认是以375px宽度的iPhone7设备,去设置样式。UI设计的时候,则会给出默认750px的宽度
,当基准值,去设计页面和切图。 想实现页面样式的适配,可通过rem的方式去实现。 我们可以严格按照蓝湖给的2倍px去写页面,然后通过动态计算,去转换成相应的rem值,这里推荐postcss-plugin-px2rem
插件。
参考地址:https://github.com/amfe/lib-flexible/tree/master, flexible下载地址:http://g.tbcdn.cn/mtb/lib-flexible/0.3.4/??flexible_css.js,flexible.js
首先引入amfe-flexible,到index.html:
1 2 3 4 5 6 7 8 <head > ... <title > app</title > <script > !function ( ) {var a="@charset \"utf-8\";html{color:#000;background:#fff;overflow-y:scroll;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}html *{outline:0;-webkit-text-size-adjust:none;-webkit-tap-highlight-color:rgba(0,0,0,0)}html,body{font-family:sans-serif}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td,hr,button,article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{margin:0;padding:0}input,select,textarea{font-size:100%}table{border-collapse:collapse;border-spacing:0}fieldset,img{border:0}abbr,acronym{border:0;font-variant:normal}del{text-decoration:line-through}address,caption,cite,code,dfn,em,th,var{font-style:normal;font-weight:500}ol,ul{list-style:none}caption,th{text-align:left}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:500}q:before,q:after{content:''}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}a:hover{text-decoration:underline}ins,a{text-decoration:none}" ,b=document .createElement("style" );if (document .getElementsByTagName("head" )[0 ].appendChild(b),b.styleSheet)b.styleSheet.disabled||(b.styleSheet.cssText=a);else try {b.innerHTML=a}catch (c){b.innerText=a}}();!function (a,b ) {function c ( ) {var b=f.getBoundingClientRect().width;b/i>540 &&(b=540 *i);var c=b/10 ;f.style.fontSize=c+"px" ,k.rem=a.rem=c}var d,e=a.document,f=e.documentElement,g=e.querySelector('meta[name="viewport"]' ),h=e.querySelector('meta[name="flexible"]' ),i=0 ,j=0 ,k=b.flexible||(b.flexible={});if (g){console .warn("将根据已有的meta标签来设置缩放比例" );var l=g.getAttribute("content" ).match(/initial\-scale=([\d\.]+)/ );l&&(j=parseFloat (l[1 ]),i=parseInt (1 /j))}else if (h){var m=h.getAttribute("content" );if (m){var n=m.match(/initial\-dpr=([\d\.]+)/ ),o=m.match(/maximum\-dpr=([\d\.]+)/ );n&&(i=parseFloat (n[1 ]),j=parseFloat ((1 /i).toFixed(2 ))),o&&(i=parseFloat (o[1 ]),j=parseFloat ((1 /i).toFixed(2 )))}}if (!i&&!j){var p=(a.navigator.appVersion.match(/android/gi ),a.navigator.appVersion.match(/iphone/gi )),q=a.devicePixelRatio;i=p?q>=3 &&(!i||i>=3 )?3 :q>=2 &&(!i||i>=2 )?2 :1 :1 ,j=1 /i}if (f.setAttribute("data-dpr" ,i),!g)if (g=e.createElement("meta" ),g.setAttribute("name" ,"viewport" ),g.setAttribute("content" ,"initial-scale=" +j+", maximum-scale=" +j+", minimum-scale=" +j+", user-scalable=no" ),f.firstElementChild)f.firstElementChild.appendChild(g);else {var r=e.createElement("div" );r.appendChild(g),e.write(r.innerHTML)}a.addEventListener("resize" ,function ( ) {clearTimeout (d),d=setTimeout (c,300 )},!1 ),a.addEventListener("pageshow" ,function (a ) {a.persisted&&(clearTimeout (d),d=setTimeout (c,300 ))},!1 ),"complete" ===e.readyState?e.body.style.fontSize=12 *i+"px" :e.addEventListener("DOMContentLoaded" ,function ( ) {e.body.style.fontSize=12 *i+"px" },!1 ),c(),k.dpr=a.dpr=i,k.refreshRem=c,k.rem2px=function (a ) {var b=parseFloat (a)*this .rem;return "string" ==typeof a&&a.match(/rem$/ )&&(b+="px" ),b},k.px2rem=function (a ) {var b=parseFloat (a)/this .rem;return "string" ==typeof a&&a.match(/px$/ )&&(b+="rem" ),b}}(window ,window .lib||(window .lib={})); </script > </head >
随后安装postcss-plugin-px2rem插件:
1 2 npm install --save postcss-plugin -px2rem
在vue.config.js配置文件中加入以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const px2rem = require('postcss-plugin-px2rem'); const postcss = px2rem({ rootValue: 75 , unitPrecision: 5 , propWhiteList: [], propBlackList: [], exclude: false , selectorBlackList: [], ignoreIdentifier: false , replace: true , mediaQuery: false , minPixelValue: 0 , }); ... module.exports = { ... css: { loaderOptions: { postcss: { plugins: [postcss ], }, }, }, ... };
6.vue优化 vue打包优化: 1.图片需要支持CDN
,否则图片全放在项目asset里面,本地打包的话,会越来越慢。 2.打包环境下去除console.log
,减少打包体积,这是引入uglifyjs-webpack-plugin的原因。 3.支持gzip
,这样一来,请求的H5资源体积越少,下载越快,用户体验越好,这是引入compression-webpack-plugin的原因。
vue调试优化: 1.引入vConsole,可以在手机app网页内进行调试,这是引入vconsole-webpack-plugin的原因。 2.引入refresh机制,可以在手机app网页内进行重新加载。
具体步骤如下:
1 2 npm install uglifyjs-webpack-plugin compression-webpack-plugin vconsole-webpack-plugin --save -dev
在vue.config.js配置文件中加入以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 ... const UglifyJsPlugin = require ('uglifyjs-webpack-plugin' );const CompressionPlugin = require ('compression-webpack-plugin' );const VConsolePlugin = require ('vconsole-webpack-plugin' );const productionGzipExtensions = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i ;const isProduction = process.env.NODE_ENV === 'production' ;... module .exports = { configureWebpack : { ... plugins : isProduction ? [ new UglifyJsPlugin({ uglifyOptions : { compress : { drop_debugger : true , drop_console : true , }, }, sourceMap : false , parallel : true , }), new CompressionPlugin({ filename : '[path].gz[query]' , algorithm : 'gzip' , test : productionGzipExtensions, threshold : 8192 , minRatio : 0.8 , deleteOriginalAssets : true , }), ] : [ new VConsolePlugin({ filter : [], enable : true , })], ... }, };
根据是否是打包环境,匹配加载对应的webpack插件,如果是打包环境,使用UglifyJsPlugin和CompressionPlugin插件,进行压缩优化;如果是开发环境使用VConsolePlugin,进行调试,打包环境是不需要出来VConsole的。 VConsole参考地址:https://github.com/Tencent/vConsole/blob/dev/README_CN.md
7.实现一个刷新按钮 有了VConsole,我们可以在真机app上进行调试看打印日志。VConsole主要作用于JsBridge
,与原生前端交互时用的,比如原生回调H5方法,传的参数是否正确。除此之外,我们还需要能有自主刷新当前页的功能,比如在和后端交互的时候,想重新加载新的请求,方便测试去做接口测试。那么我们该怎么实现呢? 我的思路是:在index.html里面加个refresh按钮,通过正则表达式区分是开发环境还是正式环境,如果是开发环境将其显示出来,正式环境将其隐藏。 实现如下,在index.html,增加refresh按钮:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <head > ... <style type ="text/css" > ._refresh { position : fixed; right : 0 ; bottom : 50px ; z-index : 999999 ; color : #fff ; border : 1px solid #eee ; width : 50px ; height : 50px ; background : url ('/refresh.png' ) no-repeat 0 0 ; background-color : rgba (255 , 255 , 255 , .8 ); background-size : 30px 30px ; background-position : center; border-radius : 5px ; display : none; } </style > ... </head > <body > ... <script > window .onload = function ( ) { const reg = /^(?=^.{3,255}$)[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$/ ; const current = window .location.host; if (!reg.test(current)) { document .getElementById('_refresh' ).style.display = 'block' ; } } </script > <div id ="_refresh" class ="_refresh" onclick ="location.reload()" > </div > </body >
最终效果如图:
4.实战Vue 1.Vue相关的基础知识:
Vue组件之间的传参
Vue常用生命周期
mounted
destroyed
activated
deactivated
Vue常用属性
props
data
components
methods
computed
watch
1.1.Vue组件之间的传参 1.父子组件,传参交互 Parent.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <template > <Child :msg ="msg" @change ="change" > </Child > </template > <script > import Child from '@/views/Child.vue' ;export default { data ( ) { return { msg : 'Hello World' , }; }, components : { Child, }, methods : { change (msg ) { this .msg = msg; }, } } </script >
Child.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <template > <div class ="child" > <h1 > {{ msg }} </h1 > <div class ="button" @click ="change()" > Change Words</div > </div > </template > <script > export default { props : ['msg' ], methods : { change ( ) { this .$emit('change' , 'Hello' ); }, }, }; </script >
msg值是通过父组件,传过去的,然后子组件通过props
属性进行接收,子组件通过$emit
去触发父组件的change事件,将值再传回给父组件,父组件再重新对msg赋值。
2.兄弟组件,传参交互 除开父子组件,其余都是兄弟组件。兄弟组件的传参,方式可以有很多。举例:从列表页跳入详情页,详情页想获取到列表页的数据。 1.从路由后面拼接传递
1 2 3 4 5 6 // 列表页this.$router .push('detail?token=642305366431891456&id=2' ); // 详情页getQuery('token' ) getQuery('id' )
2.通过localStorage传递
1 2 3 4 5 6 7 8 9 const objStr = JSON .stringify({ token : '642305366431891456' , id : 2 });localStorage .setItem('objStr' , objStr);const objStr = localStorage .getItem('objStr' );const obj = JSON .parse(objStr);obj.token obj.id
3.通过vuex传递
基本这两种情况可以满足大部分情况,但是还有一种特殊场景,那就是使用keep-alive
组件,使用keep-alive后,mounted 生命周期只会走一次,destroyed 生命周期不走。 举例:从列表页跳入详情页,详情页上做了操作一些,需要改变列表页的值。 场景再描述具体一点:列表有6枚勋章,3枚已领取,3枚未领取,进入3枚未领取的其中一个详情后,点击按钮变成已领取。所以这时候列表,变成4枚已领取,2枚未领取勋章。 这时候这个场景需要刷新下列表的接口,重新获取勋章状态,但是返回后,不走列表的 mounted 生命周期。 遇到这种情况的时候,需要用到vuex
,状态管理,后续会说vuex的使用方式。
1.2.Vue常用生命周期 1 2 3 4 5 6 7 8 9 10 11 12 mounted ( ) { console .log('mounted-------' ); }, destroyed ( ) { console .log('destroyed-------' ); }, activated ( ) { console .log('activated-------' ); }, deactivated ( ) { console .log('deactivated-------' ); },
mounted,可以理解成DOM结构生成后,会进入该生命周期,注意,这里只是生成个框,并不表示所有的DOM都渲染完毕,想等整个视图都渲染完毕,需要使用$nextTick
:
1 2 3 4 this.$nextTick(() => { })
mounted常用于掉接口,发起http请求。 destroyed常用于定时器timeout、interval的销毁。
当组件在keep-alive内被切换时,组件的activated
、deactivated
这两个生命周期钩子函数会被执行。类似于小程序的onShow
、onHide
生命周期,用于切换时需处理的逻辑。
1.3.Vue常用属性
props
data
components
methods
computed
watch
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 props :['msg' ]data ( ) { return { msg : 'Hello World' , }; }, components : { Child, } methods : { change ( ) { } } computed : { reversedMessage ( ) { return this .msg.split('' ).reverse().join('' ); }, } watch : { msg ( ) { console .log('msg--------change' ); } }
2.Vue-Router常用方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 /** * 认识 Vue Router 常见API: * push 跳转到指定页面,会向 history 添加新记录 * replace 跳转到指定页面,不会向 history 添加新记录 * go 跳去 history 记录中向前或者后退多少步 * forward 跳去 history 记录前进一步 * back 跳去 history 记录后退一步 */ // 常见路由跳转push、replace// 字符串方式跳转this.$router .push(`detail?token=642305366431891456 &id=1 `); // 对象方式跳转this.$router .push({ path: 'detail' , query: { token: '642305366431891456' , id: 1 }}); // 常见路由返回this.$router .back(); this.$router .go(-2 );
3.Vuex的实现方式 前面提到过用vuex的场景,就是使用keep-alive
组件后,mounted 生命周期只会走一次,destroyed 生命周期不走。或者在兄弟组件之间,两个或者更多组件都依赖同一个data中的变量而自动
改变时,那么就是用vuex的最佳机会。
就拿领取勋章举例,默认从接口请求获取到3枚已领取、3枚未领取。 1.初始化一个变量list 2.在列表中获取请求的结果,赋值到该list上,需要在mutations和actions中创建一个setList方法 3.在详情页中,需要点击按钮领取的时候,需要改变list中某一个的领取状态变成已领取,因此还需要创建一个setDetail方法 该业务写在store/modules/global.js中,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 // initial state const state = { list: [], }; // getters const getters = {};// actions const actions = { set List({ commit }, list) { commit('set_list', list); }, set Detail({ commit }, id) { commit('set_detail', id); }, }; // mutations const mutations = { set_list(state , list) { state .list = list; }, set_detail(state , id) { state .list = state .list.map((e) => { if (e.id === id) { e.flag = true; } return e; }); }, }; export default { namespaced: true, state , getters, actions, mutations, };
3.将上面的vuex实现放在store的一个module中,取名叫global
1 2 3 4 5 6 7 8 9 10 import Vue from 'vue' ;import Vuex from 'vuex' ;import global from './modules/global' ;Vue.use(Vuex); export default new Vuex.Store({ modules: { global , }, });
这是vuex如何初始化变量和定义方法,是不是还算简单? 在这里我们认识一下vuex的几个概念:
1 2 3 4 5 6 7 8 /** * 认识 Vuex 几个概念: * State 全局变量 一个store * Module 将这个store 分割,形成多个module * Getter store 的计算属性 * Mutation 改变store 状态,提交mutation * Action 分发Action,触发更新store */
下面我们来实现vuex如何使用? 想使用vuex的变量,用mapState
;想使用vuex的方法,用mapActions
。 List.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <template > <ul class ="ul" > <li class ="li" :class ="{ selected: item.flag }" v-for ="item in list" :key ="item.id" @click ="goDetail(item.id)" > {{ item.flag ? '已领取' : '未领取' }} </li > </ul > </template > <script > import { mapState, mapActions } from 'vuex' ;export default { computed : { ...mapState({ list : state => state.global.list, }), }, methods : { ...mapActions('global' , [ 'setList' , ]), getList ( ) { ajax.then((res ) => { const data = [ { id : 1 , flag : true }, { id : 2 , flag : true }, { id : 3 , flag : true }, { id : 4 , flag : false }, { id : 5 , flag : false }, { id : 6 , flag : false }, ]; this .setList(data); }); }, }, mounted ( ) { this .getList(); }, } </script >
Detail.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template > <button class ="button" @click ="getOne()" > 领取</button > </template > <script > import { mapActions } from 'vuex' ;export default { methods : { ...mapActions('global' , [ 'setDetail' , ]), getOne ( ) { const id = parseInt (getQuery('id' )); this .setDetail(id); }, }, }; </script >
最终的效果如下:
4.Sass vue提供的scoped属性 我一直认为scoped是scss提供的,其实不是,而是vue提供的。 参考地址:https://cn.vuejs.org/v2/guide/comparison.html#%E7%BB%84%E4%BB%B6%E4%BD%9C%E7%94%A8%E5%9F%9F%E5%86%85%E7%9A%84-CSS
4.1.scoped的由来 css一直有个令人困扰的作用域问题:即使是模块化编程下,在对应的模块的js中import css进来,这个css仍然是全局的。为了避免css样式之间的污染,vue中引入了scoped这个概念
。
在vue文件中的style标签上,有一个特殊的属性:scoped。当一个style标签拥有scoped属性时,它的CSS样式就只能作用于当前的组件
。通过设置该属性,使得组件之间的样式不互相污染。如果一个项目中的所有style标签全部加上了scoped,相当于实现了样式的模块化。
4.2.scoped的原理 vue中的 scoped 通过在DOM结构以及css样式自动添加一个唯一的属性
:data-v-hash
的方式,以保证唯一(而这个工作是由过PostCSS转译实现的),达到样式私有化模块化的目的。
总结一下scoped三条渲染规则:
给HTML的DOM节点加一个不重复data属性(形如:data-v-19fca230)来表示他的唯一性
在每句css选择器的末尾(编译后的生成的css语句)加一个当前组件的data属性选择器(如[data-v-19fca230])来私有化样式
如果组件内部包含有其他组件,只会给其他组件的最外层标签加上当前组件的data属性
举个例,转译前:
1 2 3 4 5 6 7 8 9 10 11 12 13 <style lang ="scss" scoped > .detail { background : blue; span { color :red; } } </style > <template > <div class ="detail" > <span > hello world !</span > </div > </template >
转译后:
1 2 3 4 5 6 7 8 9 10 11 12 13 <style lang ="css" > .detail [data-v-ff86ae42] { background : blue; } .detail span [data-v-ff86ae42] { color : red; } </style > <template > <div class ="detail" data-v-ff86ae42 > <span data-v-ff86ae42 > hello world !</span > </div > </template >
4.3.穿透scoped 在做项目中,通常会遇到这么一个问题,即:引用第三方组件时,需要在组件中局部修改第三方组件的样式,而又不想去除scoped属性造成组件之间的样式污染。那么有哪些解决办法呢?
不使用scoped 省略(个人不推荐)
在模板中使用两次style标签(推荐)
1 2 3 4 5 6 <style lang ="scss" > </style > <style lang ="scss" scoped > </style >
vue官网中提到:一个 .vue 文件可以包含多个style标签。所以上面的写法是没有问题的。
5.工具类utils,防抖、节流、获取url后面的所有参数 常用的方法,可以协助研发很好的使用常用方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 export function validateMobile (mobile ) { const re = /^(13[0-9]|14[579]|15[0-3,5-9]|16[6]|17[0135678]|18[0-9]|19[89])\d{8}$/ ; return re.test(mobile); } export function getQuery (key = null ) { const paramsUrl = (window .location.href).split('?' ); if (paramsUrl.length < 2 ) return key ? null : {}; const paramsArr = paramsUrl[1 ].split('&' ); const paramsData = {}; paramsArr.forEach((r ) => { const data = r.split('=' ); const [a, b] = data; paramsData[a.toLowerCase()] = b; }); if (key) return Object .prototype.hasOwnProperty.call(paramsData, key) ? paramsData[key] : null ; return paramsData; } export function debounce (fn, delay ) { delay = delay || 600 ; let timer; return function ( ) { const ctx = this ; const args = arguments ; if (timer) { clearTimeout (timer); } timer = setTimeout (() => { timer = null ; fn.apply(ctx, args); }, delay); }; } export function throttle (fn, delay ) { let last, timer; delay = delay || 600 ; return function ( ) { const ctx = this ; const args = arguments ; const now = +new Date (); if (last && now < last + delay) { clearTimeout (timer); timer = setTimeout (() => { last = now; fn.apply(ctx, args); }, delay); } else { last = now; fn.apply(ctx, args); } }; }
6.axios实现get、post请求 常用的axios方法,除了axios.get、axios.post外,还可以通过axios.create自定义http请求。
1 2 3 4 5 6 7 const serviceJson = axios.create({ timeout: 5000 , headers: { 'Api-Version': apiVersion , accessToken , }, });
接着我们可以使用interceptors,来拦截请求request和响应response。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 serviceJson.interceptors.request.use((config ) => { ... const { method } = config;switch (method) { case 'get' : ... break ; case 'post' : ... break ; default : break ; } console .log(config, '-----config' ); return config; ... });
1 2 3 4 5 6 7 serviceJson.interceptors.response.use((res ) => { if (res && res.status === 200 && res.data.code === 0 ) { return res.data.data; } console .error(`[http] error \n${res.config.url} \nmessage : ${res.data.message || 'encounter error' } ` ); return Promise .reject(res.data); });
最后我们可以使用这一个自定义http请求来处理所有get和post请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 getFAQ (params ) { return serviceJson({ method : 'get' , url : `/udata/udata/getdata?${params} ` , }); }, setInvCode (params ) { return serviceJson({ method : 'post' , url : 'user/setInvCode' , data : params, }); },
7.请求服务器接口跨域
上面的这个报错大家都不会陌生,报错是说没有访问权限(跨域问题)。本地开发项目请求服务器接口的时候,因为客户端的同源策略,导致了跨域的问题。那么该如何解决跨域问题呢?
7.1.本地开发环境跨域 如果是为了解决本地开发环境
的跨域问题,解决方案是使用 webpack 配置代理
。 在vue.config.js配置文件中加入以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 module.exports = { ... devServer: { disableHostCheck: true, proxy: { '^/(withdraw|user |x |task |sign |coin |feet |daily |auth |rank |reward |body )': { target: 'http://duoduo-api.tuji.com', }, '^/(udata)': { target: 'http://duoduo-test.tuji.com', }, '^/(zoubbActivity)': { target: 'http://172.17 .44 .171 :9011 ', }, }, }, ... }
然后通过axios,根据请求的前缀,自动proxy到对应的域名或IP下面。比如/user/invUInfo
,代理到http://duoduo-api.tuji.com
;/udata/udata/getdata
,代理到http://duoduo-test.tuji.com
;/zoubbActivity/getList
,代理到http://172.17.44.171:9011
。
7.2.线上正式环境跨域 如果是为了解决线上正式环境
的跨域问题,解决方案有两种,第一就是将前端的项目与后台接口的项目放在同一台服务器上
,这样一来就不会出现跨域问题。但这个方案有个弊端,如果前端项目需要访问其他服务器上的接口请求的话,还是会出现跨域问题,因此,最佳的解决方案是使用 nginx 配置反向代理
。
nginx反向代理主要通过proxy_pass
来配置,将你项目的域名填写到proxy_pass后面即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 server { listen 80 ; location ~ ^/(withdraw|user|x|task|sign|coin|feet|daily|auth|rank|reward|body) { proxy_pass http://duoduo-api.tuji.com; } location ~ ^/(udata) { proxy_pass http://duoduo-test.tuji.com; } location ~ ^/(zoubbActivity) { proxy_pass http://10.10.10.10:20186; } }
然后使用sudo nginx -t
,检查nginx语法是否正确,最后使用sudo nginx -s relod
,重启生效。
想了解更多关于代理方面的知识,强烈推荐姐妹篇《谁说前端不需要懂 Nginx》 和 《谁说前端需要懂 Nginx》 。
8.nginx支持gzip SPA单页应用,首屏由于一次性加载所有资源,所有首屏加载速度很慢,但是只要加载完成,尤其放在App的WebView缓存里面后,体验超级好
。解决这个问题非常有效的手段之一就是前后端开启gzip
(其他还有缓存、路由懒加载等等)。gzip其实就是帮我们减少文件体积,能压缩到30%左右,即100k的文件gzip后大约只有30k。
为了让浏览器支持gzip格式的访问,那前后端如何开启gzip呢?在前面的新架构中,前端其实已经开启了gzip的支持。 前端支持: 1.引入compression-webpack-plugin
1 npm install compression-webpack-plugin --save -dev
2.在vue.config.js配置文件中加入以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const CompressionPlugin = require ('compression-webpack-plugin' );const productionGzipExtensions = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i ;... configureWebpack : { ... plugins : [ new CompressionPlugin({ filename : '[path].gz[query]' , algorithm : 'gzip' , test : productionGzipExtensions, threshold : 8192 , minRatio : 0.8 , deleteOriginalAssets : true , }), ] } ...
详细的配置属性参考:https://www.npmjs.com/package/compression-webpack-plugin 说下常用的两个属性: threshold:表示文件大小多大就需要压缩成gzip包。上面的8192,指的是静态文件超过8kb(1024*8),就需要压缩了。 deleteOriginalAssets:表示打包后是否需要删除源文件,true只留下gzip文件,源文件被删除。
3.执行npm run build打包命令 这是deleteOriginalAssets:false的情况,两种类型的文件都存在:
至此,前端打包gzip的工作就全部结束了。
后端支持: 后端开启gzip,更简单,增加nginx配置gzip_static on;
,即可开启。 开启nginx gzip压缩后,网页、css、js等静态资源的大小会大大的减少,从而可以节约大量的带宽,提高传输效率,给用户快的体验。虽然会消耗cpu资源,但是为了给用户更好的体验是值得的。 nginx配置:
1 2 3 4 5 6 7 8 9 server { listen 80 ; location / { root /home/ww/data/html; index index .html; error_page 404 /index .html; gzip_static on ; // 开启gzip } }
加载文件速度前后对比: gzip开启前:
gzip开启后:
响应的header的对比: gzip开启前:
gzip开启后,新增Content-Encoding: gzip
:
9.实现一个公共toast组件 想实现公共的组件,在vue的定义中,叫插件
。 参考地址:https://cn.vuejs.org/v2/guide/plugins.html 那我们来自定义一个toast插件: 1.新增组件toast.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 <template > <transition name ="toast-pop" > <div class ="toast" v-show ="visible" :class ="customClass" :style ="{ 'padding': iconClass === '' ? '10px' : '20px' }" > <i class ="toast-icon" :class ="iconClass" v-if ="iconClass !== ''" > </i > <span class ="toast-text" :style ="{ 'padding-top': iconClass === '' ? '0' : '10px' }" > {{ message }} </span > </div > </transition > </template > <style lang ="scss" scoped > .toast { position : fixed; max-width : 80% ; border-radius : 5px ; background : rgba (0 , 0 , 0 , 0.7 ); color : #fff ; box-sizing : border-box; text-align : center; z-index : 1000 ; transition : opacity 0.3s linear; &.is-placetop { top : 50px ; left : 50% ; transform : translate (-50% , 0 ); } &.is-placemiddle { left : 50% ; top : 50% ; transform : translate (-50% , -50% ); } &.is-placebottom { bottom : 50px ; left : 50% ; transform : translate (-50% , 0 ); } &.toast-pop-enter , &.toast-pop-leave-active { opacity : 0 ; } .toast-icon { display : block; text-align : center; font-size : 56px ; } .toast-text { font-size : 28px ; display : block; text-align : center; line-height : 1.5 } } </style > <script type ="text/babel" > export default { props : { message : String , className : { type : String , default : '' , }, position : { type : String , default : 'middle' , }, iconClass : { type : String , default : '' , }, }, data ( ) { return { visible : false , }; }, computed : { customClass ( ) { const classes = []; switch (this .position) { case 'top' : classes.push('is-placetop' ); break ; case 'bottom' : classes.push('is-placebottom' ); break ; default : classes.push('is-placemiddle' ); } classes.push(this .className); return classes.join(' ' ); }, }, }; </script >
2.新增toast逻辑index.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 import Vue from 'vue' ;const ToastConstructor = Vue.extend(require ('./toast.vue' ).default);const toastPool = [];const getAnInstance = () => { if (toastPool.length > 0 ) { const instance = toastPool[0 ]; toastPool.splice(0 , 1 ); return instance; } return new ToastConstructor({ el : document .createElement('p' ), }); }; const returnAnInstance = (instance ) => { if (instance) { toastPool.push(instance); } }; const removeDom = (event ) => { if (event.target.parentNode) { event.target.parentNode.removeChild(event.target); } }; ToastConstructor.prototype.close = function ( ) { this .visible = false ; this .$el.addEventListener('transitionend' , removeDom); this .closed = true ; returnAnInstance(this ); }; const Toast = (options = {} ) => { const duration = options.duration || 3000 ; const instance = getAnInstance(); instance.closed = false ; clearTimeout (instance.timer); instance.message = typeof options === 'string' ? options : options.message; instance.position = options.position || 'middle' ; instance.className = options.className || '' ; instance.iconClass = options.iconClass || '' ; document .body.appendChild(instance.$el); Vue.nextTick(() => { instance.visible = true ; instance.$el.removeEventListener('transitionend' , removeDom); ~duration && (instance.timer = setTimeout (() => { if (instance.closed) return ; instance.close(); }, duration)); }); return instance; }; export default Toast;
3.通过Vue.prototype,添加Vue实例方法且暴露install方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import Toast from './toast' ;const install = (Vue) => { if (install.installed) return ; Vue.$toast = Vue.prototype.$toast = Toast; }; if (typeof window !== 'undefined' && window .Vue) { install(window .Vue); } export default { Toast, install, };
4.在main.js中使用Vue.use引入插件:
1 2 3 4 import Vue from 'vue' ;import plugins from './plugins' ;... Vue.use(plugins);
目录结构如下:
5.使用toast
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template > <button class ="button" @click ="showToast()" > toast提示</button > </template > <script > export default { methods : { showToast ( ) { this .$toast({ message : '手机号不能为空' , duration : 2000 , position : 'middle' , className : '' , iconClass : '' , }); }, } } </script >
通过this.$toast()
,即可实现如下效果:
10.打包发布上线 目前公司前端打包常见的有三种: 1.自己公司内部研发的发布系统; 2.瓦力 walle 部署发布; 3.Jenkins实现打包、发布、部署;
目前所在公司没有自己的发布系统,因此只能选择后面两种方案,walle 或 jenkins。 walle 后续在研究,jenins 其实蛮好的,可以参考我之前写过的文章《教你用Vue、GitLab、Jenkins、Nginx实现自动打包发布上线》 。