最近比较忙,所以技术博客停滞了一会。当然也不是什么事都没做,稍微整理下以前的基础知识点,有兴趣的同学可以看一下: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组件之间的传参
- Vue-Router常用方法
- Vuex的实现方式
- Sass vue提供的scoped属性
- 工具类utils,防抖、节流、获取url后面的所有参数
- axios实现get、post请求
- 请求服务器接口跨域
- nginx支持gzip
- 实现一个公共toast组件
- 打包发布上线
- Vue常见用法
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
// 全局安装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搭建出来。让我们打开项目运行一下:
// 打开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
// 安装vuex-router-sync、axios、fastclick
npm install vuex-router-sync axios fastclick --save
在main.js引入sync,axios:
import Vue from 'vue';
import fastClick from 'fastclick';
import { sync } from 'vuex-router-sync';
...
fastClick.attach(document.body);
sync(store, router);
...
更改App.vue,将其换成如下代码:
<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:
/* 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,新增如下代码:
const path = require('path');
module.exports = {
configureWebpack: {
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
},
};
这段代码意思是配置webpack,将整个src目录,取个别名@。 这样的话,我们可以修改router.js的import:
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:
...
"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:
<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插件:
// 安装
npm install --save postcss-plugin-px2rem
在vue.config.js配置文件中加入以下代码:
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网页内进行重新加载。
具体步骤如下:
// 安装webpack-plugin插件
npm install uglifyjs-webpack-plugin compression-webpack-plugin vconsole-webpack-plugin --save-dev
在vue.config.js配置文件中加入以下代码:
...
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按钮:
<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
<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
<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.从路由后面拼接传递
// 列表页
this.$router.push('detail?token=642305366431891456&id=2');
// 详情页
getQuery('token')
getQuery('id')
2.通过localStorage传递
// 列表页
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常用生命周期
mounted() {
console.log('mounted-------');
},
destroyed() {
console.log('destroyed-------');
},
activated() {
console.log('activated-------');
},
deactivated() {
console.log('deactivated-------');
},
mounted,可以理解成DOM结构生成后,会进入该生命周期,注意,这里只是生成个框,并不表示所有的DOM都渲染完毕,想等整个视图都渲染完毕,需要使用$nextTick
:
this.$nextTick(() => {
// Code that will run only after the
// entire view has been rendered
})
mounted常用于掉接口,发起http请求。 destroyed常用于定时器timeout、interval的销毁。
当组件在keep-alive内被切换时,组件的activated
、deactivated
这两个生命周期钩子函数会被执行。类似于小程序的onShow
、onHide
生命周期,用于切换时需处理的逻辑。
1.3.Vue常用属性
- props
- data
- components
- methods
- computed
- watch
// 接受父组件传过来的参数
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常用方法
/**
* 认识 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中,
// 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
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的几个概念:
/**
* 认识 Vuex 几个概念:
* State 全局变量 一个store
* Module 将这个store分割,形成多个module
* Getter store 的计算属性
* Mutation 改变store状态,提交mutation
* Action 分发Action,触发更新store
*/
下面我们来实现vuex如何使用?
想使用vuex的变量,用mapState
;想使用vuex的方法,用mapActions
。
List.vue:
<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:
<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属性
举个例,转译前:
<style lang="scss" scoped>
.detail {
background: blue;
span{
color:red;
}
}
</style>
<template>
<div class="detail">
<span>hello world !</span>
</div>
</template>
转译后:
<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标签(推荐)
<style lang="scss">
/*添加要覆盖的样式*/
</style>
<style lang="scss" scoped>
/* local styles */
</style>
vue官网中提到:一个 .vue 文件可以包含多个style标签。所以上面的写法是没有问题的。
5.工具类utils,防抖、节流、获取url后面的所有参数
常用的方法,可以协助研发很好的使用常用方法:
// 验证手机号
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请求。
const serviceJson = axios.create({
timeout: 5000,
headers: {
'Api-Version': apiVersion,
accessToken,
},
});
接着我们可以使用interceptors,来拦截请求request和响应response。
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;
...
});
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请求。
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配置文件中加入以下代码:
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后面即可:
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
npm install compression-webpack-plugin --save-dev
2.在vue.config.js配置文件中加入以下代码:
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配置:
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:
<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:
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方法
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引入插件:
import Vue from 'vue';
import plugins from './plugins';
...
Vue.use(plugins);
目录结构如下:
5.使用toast
<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实现自动打包发布上线》。