STAY HUNGRY , STAY FOOLISH.

求知若饥,虚心若愚。

       浏览:

何为H5?三年后,为何还用Vue?

最近比较忙,所以技术博客停滞了一会。当然也不是什么事都没做,稍微整理下以前的基础知识点,有兴趣的同学可以看一下: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。

app

H5的特点:它是目前主流开发技术之一,容易上手,开发周期短、成本低、兼容传统 Web 开发。

H5 这个词,可以理解成混合 App 模型,只不过它特指混合 App 的前端部分。 因为混合 App 的前端就是 HTML5 网页,所以简称 H5。这个词是国内独有的,基本上都是前端程序员在用,国外不用这个词,就直接叫混合 App。


2.谈论Vue

我是在2016年4月就开始接触并使用 vue 这个框架,当时版本 vue1.0。
当时写了第一篇文章《初识Vue.js》,有兴趣的同学可以看一下。

vue1.0

时隔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 项目,直接上菜:

web

涉及的框架及插件有:
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_create_save


通过vue-cli脚手架,我们可以轻松将vue全家桶和sass、eslint搭建出来。让我们打开项目运行一下:

vue-cli

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_show


Vue:MVVM框架,具有双向绑定的特性,可以轻松实现业务相关逻辑。
Vue-Router:Vue的路由机制,有hash和history两种模式,可以实现SPA单页面应用的跳转,使页面不会重载。
Vuex:Vue的状态容器,主要作用于兄弟组件之间的变量改变,后续会有详细说明。
Sass:Css预编译工具,防止样式混淆。
ESLint:JS代码规范,选择国外Airbnb爱彼迎的代码规则,这样团队开发风格会得到统一,能避免this指向或变量提升导致的常规错误。

接着使用VS Code,打开设置,在工作区设置中勾选启用eslint,和保存自动修复eslint语法。

vscode_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-aliveVue内置组件,用来对组件进行缓存,从而节省性能,由于是一个抽象组件,所以在v页面渲染完毕后不会被渲染成一个DOM元素。当组件在keep-alive内被切换时,组件的activateddeactivated这两个生命周期钩子函数会被执行。
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
/* http://meyerweb.com/eric/tools/css/reset/ 
v2.0 | 20110126
License: none (public domain)
*/
/* Reset CSS -------------------- Start */
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;
}
/* HTML5 display-role reset for older browsers */
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;
}
/* Reset CSS -------------------- End */
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>
// lib-flexible适配方案
!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
// 安装webpack-plugin插件
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>

最终效果如图:

refresh


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(() => {
// Code that will run only after the
// entire view has been rendered
})

mounted常用于掉接口,发起http请求。
destroyed常用于定时器timeout、interval的销毁。

当组件在keep-alive内被切换时,组件的activateddeactivated这两个生命周期钩子函数会被执行。类似于小程序的onShowonHide生命周期,用于切换时需处理的逻辑。


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 = {
setList({ commit }, list) {
commit('set_list', list);
},
setDetail({ 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>

最终的效果如下:

get


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>
/* local styles */
</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);
}

// 获取url拼接的所有参数
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;
}

// 防抖,多次点击,只在600ms后执行
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);
};
}

// 节流,多次点击,每600ms执行一次
// 函数防抖是某一段时间内只执行一次,而函数节流是间隔时间执行。
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':
// 自定义公共get的配置
...
break;
case 'post':
// 自定义公共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.请求服务器接口跨域

cros

上面的这个报错大家都不会陌生,报错是说没有访问权限(跨域问题)。本地开发项目请求服务器接口的时候,因为客户端的同源策略,导致了跨域的问题。那么该如何解决跨域问题呢?

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的情况,两种类型的文件都存在:
change

至此,前端打包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开启前:

no_gzip

gzip开启后:

yes_gzip


响应的header的对比:
gzip开启前:

gzip_before

gzip开启后,新增Content-Encoding: gzip

gzip_after


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);

目录结构如下:

toast_dir

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(),即可实现如下效果:
toast


10.打包发布上线

目前公司前端打包常见的有三种:
1.自己公司内部研发的发布系统;
2.瓦力 walle 部署发布;
3.Jenkins实现打包、发布、部署;

目前所在公司没有自己的发布系统,因此只能选择后面两种方案,walle 或 jenkins。 walle 后续在研究,jenins 其实蛮好的,可以参考我之前写过的文章《教你用Vue、GitLab、Jenkins、Nginx实现自动打包发布上线》