简介
在开发中尤其是团队协作开发中,我们经常会遇到一些重复性问题,由于解决问题后没有做好文档记录,或者没有通过一个有效的渠道进行分享,导致再次遇到同样问题时,还要花费时间经历去查找资料,有时可能还解决不了。
为了避免上诉情况,以后开发中解决问题后,可以通过此文档进行记录,然后分享团队开发人员。
以下是对文档具体内容的一个分类:
- 基础知识篇:常用的基础技术点,不同的业务场景需要对应不同的配置,对这些技术点掌握不牢固,每次使用时都要翻阅资料去查找示例。
- 技术分享: 开发中使用的第三方、好用的方法、使用过程遇到的问题。
- 问题记录:记录开发中遇到的问题,写清楚问题场景、解决方案、解决问题资料链接等。
目的
- 通过对问题的记录,总结,验证的过程来,有助于我们更深入的理解基础技术知识。
- 通过新技术的分享,来丰富我们的技术栈。
- 问题文档完善后,便于团队人员学习查阅,提高团队开发效率。
Vue
基础知识篇
一、指令
1、事件修饰符
在事件处理程序中调用 event.preventDefault()
或 event.stopPropagation()
是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。
为了解决这个问题,Vue.js 为 v-on 提供了事件修饰符。之前提过,修饰符是由 点
开头的指令后缀来表示的.
常用的修饰符:
.stop
.prevent
.capture
.self
.once
.passive
<!-- 默认事件继续传播 -->
<div class="parent-box" @click="console('2 - 会执行\n --------结束')">
<div class="child-box"
@click="console('开始------\n1 - 会执行')"
>
默认
<div>
</div>
<!-- .stop 阻止单击事件继续传播 -->
<div class="parent-box" @click="console('2 - 不会执行')">
<div class="child-box" @click.stop="console('1-阻止单击事件继续传播')">
.stop
</div>
</div>
<!-- .prevent 阻止浏览器事件默认行为 -->
<div class="parent-box">
<a
href="https://www.baidu.com"
@click.prevent="console('1 - 会输出,不在跳转百度链接')"
>
prevent
</a>
</div>
<!-- .native 给组件绑定原生事件 -->
<div class="parent-box">
native
<EventItem
@clickChild="console('1 - 子组件事件')"
@click.native="console('2 - 给子组件绑定事件')"
></EventItem>
</div>
<!-- capture 拦截器,拦截当前事件并有限执行,然后事件继续向下传递 ,
可以通过.stop阻止传递-->
<div class="parent-box" @click="console('2 - 会执行')">
capture
<EventItem @click.native.capture="console('1 - 子组件事件不会执行')"> </EventItem>
</div>
<div class="parent-box" @click="console('2 - 不会执行')">
capture.stop
<EventItem @click.native.capture.stop="console('1 - 子组件事件不会执行')"> </EventItem>
</div>
<!-- .passive 会告诉浏览器你不想阻止事件的默认行为,这个
.passive 修饰符尤其能够提升移动端的性能。 -->
<div class="parent-box" @scroll.passive="console('2 - 不会影响视图的滚动行为')">
passive
</div>
二、Class 与 Style 绑定
1、组件上使用 class
-
当在一个自定义组件上使用
class
property 时,这些class
将被添加到该组件的根元素上面。例如,如果你声明了这个组件:
Vue.component('my-component', { template: '<p class="foo bar">Hi</p>' })
然后在使用它的时候添加一些 class:
<my-component class="baz boo"></my-component>
HTML 将被渲染为:
<p class="foo bar baz boo">Hi</p>
-
class 优先及情况:
- 默认 父组件
>
子组件,带scoped
属性时,scoped
内的样式优先,这个元素上已经存在的 class 会被覆盖。
三、渲染
1、v-if
-
因为 v-if 是一个指令,所以必须将它添加到一个元素上。但是如果想切换多个元素呢?此时可以把一个
<template>
元素当做不可见的包裹元素,并在上面使用 v-if。最终的渲染结果将不包含<template>
元素。<template v-if="ok"> <h1>Title</h1> <p>Paragraph 1</p> <p>Paragraph 2</p> </template>
-
避免 v-if 和 v-for 用在一起
当 Vue 处理指令时,v-for 比 v-if 具有更高的优先级,所以这个模板:
<ul> <li v-for="user in users" v-if="user.isActive" :key="user.id" > {{ user.name }} </li> </ul>
将会经过如下运算:
this.users.map(function (user) { if (user.isActive) { return user.name } })
因此哪怕我们只渲染出一小部分用户的元素,也得在每次重渲染的时候遍历整个列表,不论活跃用户是否发生了变化。
通过将其更换为在如下的一个计算属性上遍历:
computed: { activeUsers: function () { return this.users.filter(function (user) { return user.isActive }) } } <ul> <li v-for="user in activeUsers" :key="user.id" > {{ user.name }} </li> </ul>
我们将会获得如下好处:
- 过滤后的列表只会在 users 数组发生相关变化时才被重新运算,过滤更高效。
- 使用 v-for=“user in activeUsers” 之后,我们在渲染的时候只遍历活跃用户,渲染更高效。
- 解耦渲染层的逻辑,可维护性 (对逻辑的更改和扩展) 更强。
2、key的作用
key
的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素
-
用 key 管理可复用的元素,每次切换时,输入框都将被重新渲染。
<template v-if="loginType === 'username'"> <label>Username</label> <input placeholder="Enter your username" key="username-input"> </template> <template v-else> <label>Email</label> <input placeholder="Enter your email address" key="email-input"> </template>
-
触发过渡,完整地触发组件的生命周期钩子
<transition> <span :key="text">{{ text }}</span> </transition>
当 text 发生改变时, 总是会被替换而不是被修改,因此会触发过渡。
同理,key属性被用在组件上时,当key改变时会引起新组件的创建和原有组件的删除,此时组件的生命周期钩子就会被触发。
-
key 列表渲染 (参考文档)
推荐使用数据的唯一标识作为key,比如id,身份证号,手机号等等,通常这些数据由后端提供。
后续操作不破坏原来数据顺序的话,使用index作为key也没有任何问题。
3、动态组件
- 不同组件之间进行动态切换,比如在一个多标签的界面里,可以通过 Vue 的 元素加一个特殊的
is
attribute 来实现。
<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component v-bind:is="currentTabComponent"></component>
- 结合
keep-alive
使用, 可以实现组件的缓存:
<keep-alive>
<component v-bind:is="currentTabComponent" class="tab">
</component>
</keep-alive>
四、API
1、model
允许一个自定义组件在使用 v-model
时定制 prop
和 event
。默认情况下,一个组件上的 v-model
会把 value
用作 prop
且把 input
用作 event
,但是一些输入类型比如单选框和复选框按钮可能想使用 value prop
来达到不同的目的。使用 model
选项可以回避这些情况产生的冲突。
<template>
<div>
<div class="mycheck-box">{{ value }}-{{ checked }}</div>
<input v-model="email" placeholder="Enter your email address" />
</div>
</template>
<script>
export default {
name: 'MyCheck',
model: {
prop: 'checked',
event: 'change',
},
props: {
// this allows using the `value` prop for a different purpose
value: String,
// use `checked` as the prop which take the place of `value`
checked: {
type: String,
default: '',
},
},
data() {
return {
email: '',
}
},
watch: {
email(val) {
this.$emit('change', val)
},
},
created() {
this.email = this.checked
},
methods: {
checkClick() {
this.$emit('change', 4)
},
},
}
<my-checkbox v-model="foo" value="some value"></my-checkbox>
上述代码相当于:
<my-checkbox
:checked="foo"
@change="val => { foo = val }"
value="some value">
</my-checkbox>
五、路由
对于大多数单页面应用,都推荐使用官方支持的 vue-router 库。更多细节可以移步 vue-router 文档。
以下是我们项目中实际用到的技术点:
1、导航守卫
正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。
-
全局前置守卫
你可以使用 router.beforeEach 注册一个全局前置守卫:const router = new VueRouter({ mode: 'history', base: pkg.path, routes, }) // 结合中间件使用,在跳转路由前通过中间件拦截,常用于用户登录,角色权限验证等逻辑 router.beforeEach((to, from, next) => { console.log('to:', to, 'from', from) if (!to.meta.middleware) { return next() } const middleware = to.meta.middleware const context = { to, from, next, store, } return middleware[0]({ ...context, next: middlewarePipeline(context, middleware, 1), }) })
-
全局后置钩子
你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:router.afterEach((to, from) => { // 调整滚动位置 if (to.meta.noCache == true) { window.scrollTo(0, -100) } // 全局修改浏览器title if (to.meta.title) { document.title = to.meta.title } })
-
组件内的守卫
const Foo = { template: `...`, beforeRouteEnter(to, from, next) { // 在渲染该组件的对应路由被 confirm 前调用 // 不!能!获取组件实例 `this` // 因为当守卫执行前,组件实例还没被创建 }, beforeRouteLeave(to, from, next) { // 导航离开该组件的对应路由时调用 // 可以访问组件实例 `this` } }
我们一般获取数据都是在 vue 的
create
、mounted
、activated
三个钩子函数内进行,但也存在一些特殊情况,需要在组件渲染前用到一些数据判断逻辑,这时候们就可以通过beforeRouteEnter
进行数据获取,实例:beforeRouteEnter(to, from, next) { mainRequest .getToken({ code: ' ' }) .then((res) => { if (res.error_no == 0) { // Toast('cuow') next((vm) => vm.setData(null, res)) } }) .catch((err) => { Toast('cuow') next() }) },
2、路由原信息
定义路由的时候可以配置 meta 字段
{
path: '/vue-example',
name: 'VueExample',
component: '...'
meta: {
title: 'Vue 基础', // 导航标题
middleware: [auth], // 中间件登录验证
noCache: true, // 是否开启 keep-alive
},
}
3、滚动行为
使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 vue-router 能做到,而且更好,它让你可以自定义路由切换时页面如何滚动。
注意: 这个功能只在支持 history.pushState 的浏览器中可用。
当创建一个 Router 实例,你可以提供一个 scrollBehavior 方法:
const router = new VueRouter({
routes: [...],
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
}
})
技术分享
一、Vant
组件库
1、覆盖默认样式
通过自定义样式类或者直接重写组件样式类名时来覆盖默认样式时,一定要使用 scoped
,保证样式独立,参考下面的示例:
<template>
<van-button class="my-button">按钮</van-button>
</template>
<style scoped>
/** 覆盖 Button 最外层元素的样式 */
.my-button {
width: 200px;
}
/** 覆盖 Button 内部子元素的样式 */
.my-button .van-button__text {
color: red;
}
/* 重写 */
.van-button--normal {
font-size: 30px;
}
/* 某些样式直接覆盖不了,可以通过keep 进行穿透 */
/keep/.van-picker-column {
ul li {
font-size: 30px;
}
}
}
</style>
2、桌面端适配
Vant 是一个面向移动端的组件库,因此默认只适配了移动端设备,这意味着组件只监听了移动端的 touch 事件,没有监听桌面端的 mouse 事件。
如果你需要在桌面端使用 Vant,可以引入我们提供的 @vant/touch-emulator,这个库会在桌面端自动将 mouse 事件转换成对应的 touch 事件,使得组件能够在桌面端使用。
# 安装模块
npm i @vant/touch-emulator -S
或
yarn add @vant/touch-emulator
// 在main.js 引入模块后自动生效
import '@vant/touch-emulator'
3、安全区适配
iPhone X 等机型底部存在底部指示条,指示条的操作区域与页面底部存在重合,容易导致用户误操作,因此我们需要针对这些机型进行安全区适配。Vant 中部分组件提供了 safe-area-inset-top 或 safe-area-inset-bottom 属性,设置该属性后,即可在对应的机型上开启适配,如下示例:
<!-- 在 head 标签中添加 meta 标签,并设置 viewport-fit=cover 值 -->
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover"
/>
<!-- 开启顶部安全区适配 -->
<van-nav-bar safe-area-inset-top />
<!-- 开启底部安全区适配 -->
<van-number-keyboard safe-area-inset-bottom />
自定义组件适配安全区域,示例:
.main-content {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
4、浏览器适配
Vant 默认使用 px 作为样式单位,设计稿的尺寸是375,所以为了适配vant和自定义样式的统一性,统一通过PostCSS 对样式单位进行转换。
- 移动端app
h5
项目,使用的是 Viewport 布局,配置如下:// postcss.config.js module.exports = { plugins: { 'postcss-px-to-viewport': { viewportWidth: 375, }, } }
- 企业微信
h5
项目,使用的是 Rem 布局适配,配置如下:// postcss.config.js - postcss-pxtorem module.exports = { plugins: { 'postcss-pxtorem': { rootValue: 37.5, } } }
- 如果设计稿的尺寸不是 375,而是 750 或其他大小,可以将 rootValue 配置调整为:
// postcss.config.js module.exports = { plugins: { // postcss-pxtorem 插件的版本需要 >= 5.0.0 'postcss-pxtorem': { rootValue({ file }) { return file.indexOf('vant') !== -1 ? 37.5 : 75; }, propList: ['*'], }, }, }
二、moment
时间过滤器
JavaScript 日期处理类库,用于日期格式转换的,Moment 被设计为在浏览器和 Node.js 中都能工作。
详细内容可参考官方文档
Vue 项目安装
npm install moment
或
yarn add moment
Vue 引用
import moment from 'moment'
使用示例:
- 日期格式化
/** * @description: 格式化日期 YYYYMMDD 年月日 * @param {*} value * @param {*} formater * @return {*} * @author: lrp * @Date: 2022-06-20 15:26:22 */ export function shortTime(value,formater = 'YYYYMMDD'){ return moment(value).format(formater) }
- 获取周日期
/** * @description: 获取本周日期 周一~周天,并格式化日期 * @param {*} formater * @return {* [start,end]}返回本周日期区间 * @author: lrp * @Date: 2022-06-27 09:51:11 */ export function weekDate(formater = 'YYYYMMDD'){ moment.locale('zh-cn',{ week:{ dow:1 } }) let start = moment().startOf('week').format(formater) let end = moment().endOf('week').format(formater) // let start= moment().isoWeekday(1).format(formater); // 星期一 // let end = moment().isoWeekday(7).format(formater); // 星期日 return [start,end] }
问题记录
一、UI 异常问题
1、ios 12 iframe无法滚动
商城项目用iframe展示法律条款及一些协议,页面里采用flex布局,分三块,上面标题、中间iframe展示,底部下一步操作按钮。
经测试,各个手机都很正常,唯有一个ios12的手机,iframe无法滚动。
解决问题关键点在于:-webkit-overflow-scrolling: touch
参考:https://www.jianshu.com/p/af837464ba56
具体解决方案:
.iframe-detail-box {
touch-action: none;
.iframe-detail-body-box {
height: 100%;
-webkit-overflow-scrolling: touch;
overflow-y: scroll;
iframe {
height: 100%;
width: 1px;
min-width: 100%;
*width: 100%;
}
}
}