基本类型
number/string/boolean
undefined/null
symbol/bigint
引用/对象类型
Object/Array/Function
其它内置或自定义类型
6种判断方式
更新数组的7个
push / pop / unshift / shift
splice
sort / reverse
遍历元素
forEach
map / filter / reduce
find / findIndex
every / some
其它
slice
concat
join
includes
indexOf
函数调用时, 是将实参变量的数据拷贝一份赋值给形参变量
只是实参变量数据可能是基本类型值 ==> 值传递
也可能是引用类型的值(也就是地址值) ==> 引用传递/值传递
注意下面的代码, 准确的说不是将a内存的地址赋值给b, 而是将a中保存的地址值赋值给b
var a = {}
var b = a
var a1 = 2
var a2 = {}
function fn (x) {
}
fn(a1) // 值传递
fn(a2) // x = a2 拷贝a2内存中保存的值(地址值)传递给x
作用域
作用域链
变量声明提升
)
函数声明提升
)
预解析/处理
是什么?
如何产生?
作用?
区别产生闭包与使用闭包及释放闭包?
应用?
写一个简单的闭包程序
function fn1() {
var a = 2;
var b = 3;
function fn2() {
a++;
console.log(a);
}
return fn2;
}
var f = fn1();
f();
f();
f = null;
作用:
instanceof内部如何判断?
自定义instanceof功能函数
/*
自定义instanceof工具函数:
语法: myInstanceOf(obj, Type)
功能: 判断obj是否是Type类型的实例
实现: Type的原型对象是否是obj的原型链上的某个对象, 如果是返回true, 否则返回false
*/
function myInstanceOf(obj, Type) {
// 得到原型对象
let protoObj = obj.__proto__
// 只要原型对象存在
while(protoObj) {
// 如果原型对象是Type的原型对象, 返回true
if (protoObj === Type.prototype) {
return true
}
// 指定原型对象的原型对象
protoObj = protoObj.__proto__
}
return false
}
基于构造函数的继承
// 父类型
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.fn = function () {}
Person.prototype.sayHello = function () {
console.log(`我叫${this.name}, 年方${this.age}`)
}
// 子类型
function Student(name, age, price) {
// this.name = name
// this.age = age
// 借用父类型的构造函数
Person.call(this, name, age) // 相当于执行this.Person(name, age)
this.price = price
}
// 让子类的原型为父类的实例
Student.prototype = new Person()
// 让原型对象的构造器为子类型
Student.prototype.constructor = Student
// 重写方法
Student.prototype.sayHello = function () {
console.log(`我叫${this.name}, 年方${this.age}, 身价: ${this.price}`)
}
const s = new Student('tom', 23, 14000)
s.sayHello()
s.fn()
基于ES6的类的继承
// 父类
class Person2 {
constructor (name, age) {
this.name = name
this.age = age
}
fn () {}
sayHello () {
console.log(`我叫${this.name}, 年方${this.age}`)
}
}
// 子类
class Teacher extends Person2 {
constructor (name, age, course) {
super(name, age)
this.course = course
}
// 重写父类的方法
sayHello () {
console.log(`我叫${this.name}, 年方${this.age}, 课程:${this.course}`)
}
}
const t = new Teacher('bb', 34, 'CC')
t.sayHello()
t.fn()
封装:
继承
多态: 多种形态
在JS中对象的释放(回收)是靠浏览器中的垃圾回收器来回收处理的
垃圾回调器
如何判断对象是垃圾对象呢?
垃圾回收机制1:引用计数法
为0
就垃圾对象,引用数大于0
就不是垃圾对象垃圾回收机制2:标记-清除法
可达
的对象标记起来,不可达
的对象当成垃圾回收V8垃圾回收机制: 分代回收
将堆分为两个空间,一个叫新生代区,一个叫老生代区
对象初始创建时都在新生代, 经过几次收集后还在存在, 就会转移到老生代存储
新生代区 | 老生代区 | |
---|---|---|
大小 | 较小(1-8M), 包含2个子空间 | 较大 |
对象的特点 | 存活周期短 | 存活周期长 |
回收器 | 副垃圾回收器 | 主垃圾回收器 |
核心算法 | Scavenge算法 | Mark-Sweep && Mark-Compact算法 |
新生代 (以空间换时间)
- 1、标记活动对象和非活动对象(根据可达性)
- 2、复制
from-space
的活动对象到to-space
中并进行排序- 3、清除
from-space
中的非活动对象- 4、将
from-space
和to-space
进行角色互换,以便下一次的Scavenge算法
垃圾回收
老生代
Mark-Sweep算法(标记清理)
标记阶段:对老生代对象进行第一次扫描,对活动对象进行标记
清理阶段:对老生代对象进行第二次扫描,清除未标记的对象,即非活动对象
问题: 清除非活动对象之后,留下了很多零零散散的空位
, 不利于对象分配空间
Mark-Compact算法(标记整理)
内存溢出
运行程序需要分配的内存超过了系统能给你分配的最大剩余内存
抛出内存溢出的错误,程序中断运行
演示代码
const arr = []
for (let index = 0; index < 100000000; index++) {
arr[index] = new Array(1000)
}
内存泄漏
理解: 当程序中的某个内存数据不再需要使用, 而由于某种原因, 没有被释放
常见情况:
意外的全局变量
function fn () {a = new Array(100000)}
fn()
没有及时清除的定时器
this.intervalId = setInterval(() => {}, 1000)
// clearInterval(this.intervalId)
没有及时解绑的监听
this.$bus.$on('xxx', this.handle)
// this.$bus.$off('xxx')
没有及时释放的闭包
JS内部是通过事件循环机制来实现单线程异步执行执行的
分析事件循环机制
宏任务与微任务
宏队列与微队列
整体的执行顺序
script(整体代码)
所有微队列中的微任务
宏队列中的第一个宏任务
所有微队列中的微任务
后面3-4循环处理
/*
xhr + promise 封装一个异步ajax请求的通用函数 简洁版
ajax ('xxx.json')
*/
function ajax(url) {
return new Promise((resolve, reject) => {
// 创建一个XHR对象
const xhr = new XMLHttpRequest()
// 初始化一个异步请求(还没发请求)
xhr.open('GET', url, true)
// 绑定状态改变的监听
xhr.onreadystatechange = function () {
/*
ajax引擎得到响应数据后
将xhr的readyState属性指定为4
将响应数据保存在response / responseText属性上
调用此回调函数
*/
// 如果状态值不为4, 直接结束(请求还没有结束)
if (xhr.readyState !== 4) {
return
}
// 如果响应码在200~~299之间, 说明请求都是成功的
if (xhr.status>=200 && xhr.status<300) {
// 指定promise成功及结果值
resolve(JSON.parse(xhr.responseText))
} else { // 请求失败了
// 指定promise失败及结果值
reject(new Error('request error staus '+ request.status))
}
}
xhr.send()
})
}
配置通用的基础路径和超时
显示请求进度条
成功返回的数据不再是response, 而直接是响应体数据response.data
统一处理请求错误, 具体请求也可以选择处理或不处理
每个请求自动携带userTempId的请求头: 在请求拦截器中实现
如果当前有token, 自动携带token的请求头
对token过期的错误进行处理
基本执行顺序
内部原理: 通过promise.then的链式调用将这4个任务串连越来, 依次执行并进行数据传递
Promise.resolve(config)
.then((config) => { // 请求拦截器
// 显示进度/携带token/userTempId
return config
})
.then((config) => { // 使用xhr发ajax请求
return new Promise((resolve, reject) => {
// 根据config创建xhr对象发送异步ajax
// 如果请求成功(响应状态码200--299之间)
const response = {
data: JSON.parse(xhr.responseText),
status: xhr.statusCode
}
resolve(response)
// 如果请求失败
reject(new Error('请求失败 code ' + xhr.statusCode))
})
})
.then( // 响应拦截器
response => {
return response.data
},
error => {
throw error
}
)
.then(result => { // 特定请求的回调
}).catch(error => {
})
存储方式:
cookie: 会话/持久化两种
sessionStorage
localStorage
数据库(很少用)
区别:
---------------------sessionStorage VS localStorage
刷新在/ 关闭不在 关闭还在
---------------------cookie VS sessionStorage与localStorage
- 容量 小
- 请求时是否自动携带 会
- API易用性 不好用
- 浏览器是否可禁用 可
/*
自定义函数对象的call方法
*/
function call (fn, obj, ...args) {
// 如果传入的是null/undefined, this指定为window
if (obj===null || obj===undefined) {
// obj = window
return fn(...args)
}
// 给obj添加一个方法: 属性名任意, 属性值必须当前调用call的函数对象
obj.tempFn = fn
// 通过obj调用这个方法
const result = obj.tempFn(...args)
// 删除新添加的方法
delete obj.tempFn
// 返回函数调用的结果
return result
}
/*
自定义函数对象的bind方法
重要技术:
闭包
call()
三点运算符
*/
function bind (fn, obj, ...args) {
return function (...args2) {
return call(fn, obj, ...args, ...args2)
}
}
1. 事件高频触发处理的问题
如果更新界面 => 界面更新卡顿
如果发送ajax请求 => 发送了很多没必要的请求
2. 解决办法
函数节流
函数防抖
3. 区别:
当事件高频发生很多次时, 防抖只执行最后一次, 而节流执行少量几次
4. 应用场景
节流: 窗口调整(resize)/ 页面滚动(scroll)/ OM元素的拖拽功能实现(mousemove)
防抖: 输入搜索联想功能
/*
用于产生节流函数的工具函数
*/
function throttle (callback, delay) {
// 用于保存处理事件的时间, 初始值为0, 保证第一次会执行
let start = 0
// 返回事件监听函数 ==> 每次事件发生都会执行
return function (event) {
console.log('---throttle')
// 发生事件的当前时间
const current = Date.now()
// 与上一次处理事件的时差大于delay的时间
if (current-start>delay) {
// 执行处理事件的函数
callback.call(event.target, event)
// 保证当前时间
start = current
}
}
}
/*
用于产生防抖函数的工具函数
*/
function debounce (callback, delay) {
// 返回事件监听函数 ==> 每次事件发生都会执行
return function (event) {
console.log('---debounce')
// 如果还有未执行的定时器, 清除它
if (callback.timeoutId) {
clearTimeout(callback.timeoutId)
}
// 启动延时delay的定时器, 并保证定时器id
callback.timeoutId = setTimeout(() => {
// 执行处理事件的函数
callback.call(event.target, event)
// 删除保存的定时器id
callback.timeoutId = null
}, delay);
}
}
/*
数组扁平化: 取出嵌套数组(多维)中的所有元素放到一个新数组(一维)中
如: [1, [3, [2, 4]]] ==> [1, 3, 2, 4]
*/
/*
方法一: 递归 + reduce() + concat() + some()
*/
function flatten1 (array) {
return array.reduce((pre, item) => {
if (Array.isArray(item) && item.some((cItem => Array.isArray(cItem)))) {
return pre.concat(flatten1(item))
} else {
return pre.concat(item)
}
}, [])
}
/*
方法二: ... + some() + concat()
*/
function flatten2 (arr) {
// 只要arr是一个多维数组(有元素是数组)
while (arr.some(item => Array.isArray(item))) {
// 对arr进行降维
arr = [].concat(...arr)
}
return arr
}
/*
自定义new工具函数
语法: newInstance(Fn, ...args)
功能: 创建Fn构造函数的实例对象
实现: 创建空对象obj, 调用Fn指定this为obj, 返回obj
*/
function newInstance(Fn, ...args) {
// 创建一个新的对象
const obj = {}
// 执行构造函数
const result = Fn.apply(obj, args) // 相当于: obj.Fn()
// 如果构造函数执行的结果是对象, 返回这个对象
if (result instanceof Object) {
return result
}
// 给obj指定__proto__为Fn的prototype
obj.__proto__ = Fn.prototype
// 如果不是, 返回新创建的对象
return obj
}
/*
1). 大众乞丐版
问题1: 函数属性会丢失
问题2: 循环引用会出错
*/
export function deepClone1(target) { // 从后台获取的数据都可以用
return JSON.parse(JSON.stringify(target))
}
/*
获取数据的类型字符串名
*/
function getType(data) {
return Object.prototype.toString.call(data).slice(8, -1) // -1代表最后一位
}
/*
2). 面试基础版本
解决问题1: 函数属性还没丢失
*/
function deepClone2(target) {
const type = getType(target)
if (type==='Object' || type==='Array') {
const cloneTarget = type === 'Array' ? [] : {}
for (const key in target) {
if (target.hasOwnProperty(key)) {
cloneTarget[key] = deepClone2(target[key])
}
}
return cloneTarget
} else { // 基本类型 / function
return target
}
}
/*
3). 面试加强版本
解决问题2: 循环引用正常
*/
function deepClone3(target, map = new Map()) {
const type = getType(target)
if (type==='Object' || type==='Array') {
// 从map容器取对应的clone对象
let cloneTarget = map.get(target)
// 如果有, 直接返回这个clone对象
if (cloneTarget) {
return cloneTarget
}
cloneTarget = type==='Array' ? [] : {}
// 将clone产生的对象保存到map容器
map.set(target, cloneTarget)
for (const key in target) {
if (target.hasOwnProperty(key)) {
cloneTarget[key] = deepClone3(target[key], map)
}
}
return cloneTarget
} else {
return target
}
}
deepClone3(obj)
/*
4). 面试加强版本2(优化遍历性能)
数组: while | for | forEach() 优于 for-in | keys()&forEach()
对象: for-in 与 keys()&forEach() 差不多
*/
function deepClone4(target, map = new Map()) {
const type = getType(target)
if (type==='Object' || type==='Array') {
let cloneTarget = map.get(target)
if (cloneTarget) {
return cloneTarget
}
if (type==='Array') {
cloneTarget = []
map.set(target, cloneTarget)
target.forEach((item, index) => {
cloneTarget[index] = deepClone4(item, map)
})
} else {
cloneTarget = {}
map.set(target, cloneTarget)
Object.keys(target).forEach(key => {
cloneTarget[key] = deepClone4(target[key], map)
})
}
return cloneTarget
} else {
return target
}
}
/*
冒泡排序的方法
*/
function bubbleSort (array) {
// 1.获取数组的长度
var length = array.length;
// 2.反向循环, 因此次数越来越少
for (var i = length - 1; i >= 0; i--) {
// 3.根据i的次数, 比较循环到i位置
for (var j = 0; j < i; j++) {
// 4.如果j位置比j+1位置的数据大, 那么就交换
if (array[j] > array[j + 1]) {
// 交换
// const temp = array[j+1]
// array[j+1] = array[j]
// array[j] = temp
[array[j + 1], array[j]] = [array[j], array[j + 1]];
}
}
}
return arr;
}
const products = [{price: 23, sales: 103}, {price: 22, sales: 101}]
/*
根据销量升序
*/
products.sort((p1, p2) => { // 比较函数 ==> 返回数值 如果大于o, p2放在左边
return p1.sales - p2.sales
})
更新DOM或Style
注意:
哪些操作会导致重排
哪些操作会导致重绘
例子代码
var s = document.body.style;
s.padding = "2px"; // 回流+重绘
s.border = "1px solid red"; // 再一次 回流+重绘
s.color = "blue"; // 重绘
s.backgroundColor = "#ccc"; // 重绘
s.fontSize = "14px"; // 再一次 回流+重绘
document.body.appendChild(document.createTextNode('abc!'));// 添加node,再一次 回流+重绘
如何减少重排次数
递归组件: 组件内部有自己的子组件标签
应用场景: 用于显示树状态结构的界面
注意: 递归组件必须有name
编码: 实现一个简单的可开关的树状结构界面的 Tree 组件
-
{{item.text}}
根据组件间关系分类
另一种分类方式
props
v-model
a t t r s 与 attrs与 attrs与listeners
作用域插槽(子向父传递)
自定义事件
全局事件总线
v-model
.sync
$refs, $children, $parent
provide与inject
插槽
vuex
vue自定义事件
实现子组件向父组件通信
相关语法:
父组件中绑定自定义事件监听:
child.$on(‘eventName’, callback)
子组件中分发事件
this.$emit(‘eventName’, 2)
应用:
elment-ui的组件的事件监听语法都用的是自定义事件
我们项目中的组件也用了不少自定义事件
全局事件总线
大佬的图文解释
简洁表达
对象: 通过Object.defineProperty()添加setter方法来监视属性数据的改变 + 订阅-发布
数组: 重写更新数组元素的一系列方法 + 订阅-发布
详细表达(主要说对象的)
初始化
更新
Vue数据响应式原理结构图
这里是有案例的解释
创建了一个vm对象
state中的数据都是vm的data数据(是响应式的)
组件中读取的state数据本质读取的就是data中的数据
一旦更新了state中的数据, 所有用到这个数据的组件就会自动更新
new Vue({
data: {
home: {
categoryList: [],
xxx: {}
},
user: {
userInfo: {}
}
}
})
window.addEventListener('beforeunload', () => { // 当页面刷新时, 页面卸载前的事件回调
sessionStorage.setItem('CART_LIST_KEY',
JSON.stringify(this.$store.state.shopCart.cartList))
})
window.removeEventListener('beforeunload')
cartList: JSON.parse(sessionStorage.getItem('CART_LIST_KEY')) || [],
如果注册没有指定/:xxx的点位, 而跳转时通过params配置携带的参数数据, 刷新时就会丢失
因为url中没有携带的参数数据路径
前一个项目没这个问题, 后一个项目有问题
3.1.0版本(2019.8)没这个问题, 3.1.0这后才有这个问题
解决:
办法1: 在每次push时指定回调函数或catch错误
push('/xxx', () => {}) ===>
push('/xxx').catch(() => {})
办法2: 重写VueRouter原型上的push方法 (比较好)
const originPush = VueRouter.prototype.push
VueRouter.prototype.push = function (location, onComplete, onAbort) {
console.log('push()', onComplete, onAbort)
// 判断如果没有指定回调函数, 通过call调用源函数并使用catch来处理错误
if (onComplete===undefined && onAbort===undefined) { // 使用的新语法
return originPush.call(this, location).catch(() => {})
} else { // 如果有指定任意回调函数, 通过call调用源push函数处理
return originPush.call(this, location, onComplete, onAbort)
}
}
扩展问题
声明式路由跳转之所有没有问题, 是因为默认传入了成功的空回调函数
区别:
history: 路由路径不#, 刷新会携带路由路径, 默认会出404问题, 需要配置返回首页
404:
解决:
开发环境: 如果是脚手架项目本身就配置好
==> webpack ==> devServer: {historyApiFallback
: true}
当使用 HTML5 History API 时, 所有的 404
请求都会响应 index.html
的内容
生产环境打包运行:
配置nginx
location / {
try_files $uri $uri/ /index.html; # 所有404的请求都返回index页面
}
hash: 路由路径带#, 刷新不会携带路由路径, 请求的总是根路径, 返回首页, 没有404问题
原理:
在全局前置守卫中, 强制跳转到登陆页面时携带目标路径的redirect参数
if (userInfo.name) {
next()
} else {
// 如果还没有登陆, 强制跳转到login
next('/login?redirect='+to.path) // 携带目标路径的参数数据
}
在登陆成功后, 跳转到redirect参数的路由路径上
await this.$store.dispatch('login', {mobile, password})
// 成功了, 跳转到redirect路由 或 首页
const redirect = this.$route.query.redirect
this.$router.replace(redirect || '/')
导航守卫是什么?
导航守卫分类
全局守卫: 针对任意路由跳转
全局前置守卫
router.beforeEach((to, from, next) => {
// ...
})
全局后置守卫
router.afterEach((to, from) => {})
路由独享守卫
前置守卫
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {}
},
{
path: '/ff',
component: Foo,
},
组件守卫: 只针对当前组件的路由跳转
进入
beforeRouteEnter (to, from, next) {
// 不能使用this, this是undefined
next((comp) => { // 延迟到组件对象创建后才执行
// comp就是组件对象
})
},
更新:
beforeRouteUpdate (to, from, next) {}
离开
beforeRouteLeave (to, from, next) {}
1. 虚拟DOM的key的作用?
1). 简单的说: key是虚拟DOM对象的标识, 在更新显示时key起着极其重要的作用
2). 详细的说: 当列表数组中的数据发生变化生成新的虚拟DOM后, 进行新旧虚拟DOM的diff比较
a. 有一个对应的key
item数据没变, 直接使用原来的真实DOM
item数据变了, 对原来的真实DOM进行数据更新
b. 没有一个对应的key
根据item数据创建新的真实DOM显示
2. key为index的问题
1). 添加/删除/排序 => 产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低
2). 如果item界面还有输入框 => 产生错误的真实DOM复用 ==> 不仅效率低, 界面效果也有问题
注意: 如果不存在添加/删除/排序操作, 用index没有问题
3. 解决:
使用item数据的标识数据作为key, 比如id属性值
一个较轻的普通JS对象 ==> 真实DOM是一个较重的对象 => 轻重看对象内部属性多少
虚拟DOM对象有时也称为虚拟节点对象(vNode), 它包含了用于生成一个真实DOM/Node的必要信息, 比如:
标签结构:
- abc1
- abc2
虚拟DOM/节点
{
tagName: 'ul',
props: {
id: 'list',
},
children: [
{tagName: 'li', props: {}, children: ['abc1'], key: '1'},
{tagName: 'li', props: {}, children: ['abc2'], key: "2"},
]
}
目标:
比较的结果是要确定: 哪些原来真实DOM可以复用(但内部内容可能要更新), 要创建哪些真实DOM
处理流程
只做同层比较 => 虚拟DOM也是一个倒立树状态结构, 只进行同层比较, 这样比较次数少,效率高
确定要比较的新旧虚拟节点
没有key: 依次比较 ==> 多出的虚拟DOM, 直接创建新的真实DOM
有key: 找同名的key比较 ==> 没有找到, 直接创建新的真实DOM
先比较标签名
如果不同, 直接创建新的真实DOM
如果相同, 复用原来对应的真实DOM => 如果数据内容有变化, 更新真实DOM内部内容
简单流程: 同层比较 => 比较key => 比较标签名 => 复用
是什么?
官方描述: 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM
在数据更新后, 调用nextTick(callback),在callback中才可以读取到更新后的DOM
使用场景?
更新数据后, 想操作更新后的DOM
项目功能1: 使用swiper实现动态轮播
项目功能2: 动态显示input输入框后, 需要自动获取焦点
原理?
更新数据后, DOM不会立即更新, 而是在所有数据都变化完后, 将DOM更新作为一个异步任务统一执行
理解Tick: 取出队列中的一个回调任务到调用栈中执行就是一个tick, 有时也指定队列中的一个回调任务
nextTick(callback): 将callback指定为下一个异步回调任务执行
为什么要先更新数据再调用nextTick()呢?
第一个数据更新才触发将DOM更新放入任务队列
这样nextTick指定的callback就是放在DOM更新任务之后
这样callback中才能读取到更新后的DOM
nextTick中用的是哪个异步技术呢?
简单说: 优先使用微任务, 如果不支持才选择宏任务
详细说: promise => MutationObserver => setImmediate => setTimeout
首页
商品搜索列表
商品详情
购物车
登陆与注册
订单交易/结算
支付
个人中心/订单列表
vue
vue-router
vuex
vee-validate
vue-lazyload
axios
mockjs
nprogress
uuidjs
swiper
qrcode
lodash
- 自定义事件
Pagination组件 ==> 内部改变当前页码后分事件通知父组件, 并传递出最新的页码
- 全局事件总线
Search组件与Header组件 ==> Search组件内部清除关键字条件时, 通过全局事件总线分发事件通知Header组件清除输入
1). 配置通用的基础路径和超时
2). 显示请求进度条
3). 成功返回的数据不再是response, 而直接是响应体数据response.data
4). 统一处理请求错误, 具体请求也可以选择处理或不处理
5). 每次请求都携带一个userTempId请求头, 数据值在state中
6). 每次请求(已登陆)都携带一个token请求头, 数据值在state中
7). 对token失效的401错误, 进行处理
实现静态组件: 模板/样式写好
设计从外部接收的数据: props
设计内部的数据: data
设计基于props和data的计算属性数据: computed
根据props和data数据和computed进行动态显示
更新数据, 更新界面, 通知父组件
可以以Pagination组件为例分析
先动态请求显示一级列表, 鼠标移入某个分类项显示对应的二三级列表(可能要请求)
点击某个分类项, 跳转到搜索页面, 携带分类条件参数
- 使用编程式导航代替声明式导航
- router-link太多 ==> 创建很多组件对象 ==> 占用内存大, 效率低
- 优化事件处理效率
- 利用事件委托: event.target
- 理解事件委托与事件冒泡
- 如何携带点击的分类的数据?
- event.target得到a标签
- 利用自定义的data标签属性来保存分类信息
- 对mouseEnter高频事件进行节流处理
- 使用lodash的throttle进行节流处理
- 对lodash库实现按需引入
- 准备各种搜索条件
- category1Id: '', // 一级分类ID
- category2Id: '', // 二级分类ID
- category3Id: '', // 三级分类ID
- categoryName: '', // 分类名称
- keyword: '', // 关键字
- trademark: '', // 品牌 "ID:品牌名称"
- props: [], // 商品属性的数组: ["属性ID:属性值:属性名"] 示例: ["2:6.0~6.24英寸:屏幕尺寸"]
- order: '1:desc', // 排序方式 1: 综合,2: 价格 asc: 升序,desc: 降序 示例: "1:desc"
- pageNo: 1, // 当前页码
- pageSize: 10, // 每页数量
删除搜索关键字条件
清除输入框中的关键字 ===> 全局事件总线
删除分类条件/关键字条件
直接发请求 ==> 有问题: 地址栏上的条件参数没有删除
- 解决: 重新跳转到search, 不再携带要删除条件参数, search组件监视路由的变化, 发请求获取数据
- 购物车数据是保存在后台的, 标识是什么?
- 未登陆: 标识为用户临时ID(userTempId)
- 第一次访问时前台利用uuid库生成的唯一字符串, 保存保存在local中
- 每次请求时通过请求头自动携带它(利用请求拦截器)
- 登陆: 登陆用户的token对应的userId
- 用户请求登陆时, 服务器端生成并返回给浏览器, 浏览器收到后自动保存到local中
- 每次请求时通过请求头自动携带它(利用请求拦截器)
- 进入购物车页面 ==> 请求获取购物车列表显示
- 修改购物项数量
- 提交请求时, 携带商品的skuid和数量(变化的)
- 对点击进行节流限制
- 删除购物项(一个/多个)
- 请求接口, 携带一个skuId或多个skuId的数组
- 参数: skuId的数组 [2,3]
- 勾选购物项(一个/多个)
- 请求接口, 携带一个skuId或多个skuId的数组 和 是否勾选的标识数据(0/1)
注册流程
前台: 输入注册需要的相关信息(用户名/密码/…), 进行前台表单校验, 如果不通过, 提示错误
前台: 发送注册的ajax请求(post), 携带注册接口需要的相关数据(用户名/密码/…)
后台: 获取到注册请求携带的参数, 去数据库中判断是否已经存在
前台: 接收到响应
登陆流程
自动登陆流程
问题: 编程式路由跳转到当前路由, 参数不变, 会报出错误
3.1.0版本(2019.8)没这个问题, 3.1.0这后才有这个问题
3.1.0之前: 返回值为undefined
解决: 重写VueRouter原型上的push方法 (比较好)
扩展问题
商品管理功能
分类查询
品牌管理
平台属性管理
SPU管理
SKU管理
权限管理
权限数据(用户/角色/菜单)管理 CRUD
权限(路由(页面)/按钮的)控制
其它功能:
优惠管理
订单管理
客户管理
数据报表可视化
vue
vue-router
vuex
element-ui
axios
nprogress
lodash
echarts
自定义事件
CategorySelector
SpuForm
v-model
el-input
el-select
.sync
控制SpuForm的隐藏
Dialog/Drawer
$attrs与v-bind
$listeners与v-on
封装HintButton
ref
SpuForm/SkuForm数据初始化加载
插槽
默认插槽
通过标签体向子组件传入标签结构
Table / Form/ Upload/...
命名插槽
通过标签体向子组件传入多种不同的标签结构
Dialog / Upload
作用域插槽
决定父组件传递什么标签结构的数据在子组件中
el-table-column
vuex
集中管理状态数据
多模块编程
user
routes
app
- HintButton
- 带标题提示的按钮
- elementUI组件: ToolTip / Button
- 使用$attrs与$listeners
- CategorySelector
- 通过Select动态显示三级分类列表
- 当选择某个分类时, 分发事件通知父组件, 并携带分类ID和级别值
注意: 与前台项目的登陆接口不同的返回的数据只有token, 没有用户信息
模板项目: vue-admin-template 二次开发
element-ui快速搭建项目界面
对element-ui实现按需引入打包
按需引入打包bug: 显示PopConfirm组件的背景是透明的(不是白色)
原因: 白色背景的样式是PopOver组件提供, 而我们没有对PopOver进行按需引入打包,
最终就没有它的样式, 那背景就是透明的
解决: 引入并注册PopOver
scoped的作用和原理
作用: scoped样式只能影响当前组件和子组件的根标签
原理: 加了scoped后产生了哪些变化
标签: 当前组件的所有原生标签和子组件的根标签都添加了同一个且唯一的data自定义属性
选择器: 在选择器的最右边加上了当前data自定义属性的选择器
==> 只有可能匹配上当前组件和子组件的根标签(子组件的子标签没有这个属性)
.box .title[data-v-6777eaa2] {
color: red;
}
deep的作用和原理
deep的作用: 让我们的样式在scoped下也可以影响子组件内部子标签的样式
使用:
原生css: >>>
css预编译器: /deep/ (vue-cli3之前) 或 ::v-deep (vue-cli3及之后)
原理:
data自定义属性选择器就会从最右侧转移到deep声明的位置
==> 对子组件的内部子标签没有当前data属性的条件限制 ==> 那就可以匹配了
.box {
::v-deep .title {
color: red;
}
}
.box[data-v-6777eaa2] .title {
color: red;
}
应用:
让抽屉组件Drawer形成垂直滚动
修改轮播组件Carousel的分页指示器样式
8. 深克隆技术的理解和应用
功能: 平台属性修改取消 / 权限控制中
说清楚功能和数据结构
区别浅克隆与深克隆
如何编码实现深克隆
扩展: 自定义深克隆
9. n e x t T i c k 与 nextTick与 nextTick与set两个方法的理解和应用
功能: 列表项动态显示输入框并自动获得焦点
给响应式对象添加新属性: 使用$set()添加edit属性为true ==> 动态显示input
界面更新后执行: $nextTick()指定回调在Input DOM更新之后执行 => 自动获取焦点
扩展: $nextTick()的原理
10. 路由权限控制的实现
路由全局前置守卫
动态添加路由: addRoutes()
基于后台数据的权限控制(user/role/permission)
给用户分配了对应的角色
给角色分配了权限(按钮和路由)
权限控制的2个级别: 路由权限和按钮权限
初始化时先只注册不需要登陆或所有用户都可见的常量路由(Login/Home)
登陆请求成功后/刷新访问项目: 根据token获取用户和权限数据, 并根据权限数据生成权限路由并动态注册
全局前置守卫: 当有token, 但还没有用户信息就发请求获取用户信息和权限数据
根据路由权限数据来从所有异步路由的数组中过滤出当前用户权限路由
动态添加注册用户的权限路由: router.addRoutes()
将用户的权限路由与常量路由合并用来显示左则导航菜单
11. 路由权限控制中的2个bug
bug1: 模板项目自身代码的问题
描述: 在权限路由上刷新 页面是空白的
原因: 动态添加注册的路由只能在后面的路由跳转才可见, 当次跳转看不到
而next()是放行当次路由跳转, 自然就看不到刚动态注册的权限路由
解决:
// next() // 放行, 没有重新跳转, 看不到最新添加的动态路由
// next(to.path) // 重新跳转到目标路由, 但丢失了参数(如果有的话)
next({...to}) // 重新跳转到目标路由, 且参数不会丢失
NProgress.done() // 结束进度条
bug2: 我们代码的问题
描述: 如果先用一个A用户登陆, 退出后用B用户登陆, 结果只能看到部分有权限的路由
原因: 我们在过滤总的异步路由数组中, 过滤掉了内部部分子路由, 另一个用户登陆看不到总的路由数组了
解决: 深拷贝然后再进行过滤 ==> 不去改变总的异步路由数组
12. 按钮权限控制的实现
按钮权限数据:
在哪? vuex的user模块的state中 ==> 映射到getters中了 butttons
结构: ['按钮1权限值', '按钮2权限值'] ==> ["btn.Attr.add", "btn.Trademark.add"]
如何判断当前用户是否有某个按钮的权限?
定义判断的函数, 接收特定按钮的权限值, 返回是否有权限的布尔值
function hasBtnPermission (btnPer) {
return store.getters.buttons.includes(btnPer)
}
将判断的函数挂载到Vue的原型上, 让所有组件都可见
Vue.prototype.$hasBP = hasBtnPermission
在权限组件中利用$hasBP来判断是否显示某个功能按钮
v-if="$hasBP('btn.Attr.update')" 权限值去菜单管理列表中查看
13. 说一个记忆深刻的开发过程中的问题
HintButton封装的bug
1. 删除table中的一行, 下一行的hintButton会自动显示文本提示
原因: 没有table的遍历的key, 用了index作为key, 下一个数据会复用上一个被删除数据的真实DOM
解决: row-key属性给table的遍历指定key为id
2. 关闭确定框后, hint-button上的文本提示又会显示
原因: 按钮在确定框后自动获取焦点 => 显示文本提示
解决: 在点击按钮时, 让其失去焦点 event.target.blur()
问题: 如果点击按钮中图标, 不可以
原因: target此时是图标标签i, 而不是button
解决: event.currentTarget.blur()
14. 详细说说你实现的某个业务功能的过程
商品平台属性管理
一个平台属性包含一个平台属性名和多个平台属性值, 每个属性值都是包含属性值名称和其它属性的对象
先根据选择的某个3级分类ID, 请求所有对应的所有平台属性的列表
点击添加或某个分类的修改按钮进入相同的添加修改界面
点击修改进入时, 要保存要修改的平台属性对象
问题: 输入修改平台属性值名称后, 不能取消
原因: 修改界面和列表界面共用一个属性对象
解决: 保存的不能是列表中的平台属性对象的引用或浅拷贝, 必须是它的深拷贝对象
在编辑平台属性值名称时, 需要实现点击从查看模式变为编辑模式, 也就是从span变为input
设计: 给每个平台属性值对象添加一个edit属性为true
问题: 通过row.edit=true后, input没有显示
原因: 新添加的属性不是响应式的
解决: 通过$set给row添加edit属性, 值为true
设计: 显示input时, 自动获取焦点
问题: 通过ref得到input后, 调用focus方法 => input是undefined的错误
原因: dom还没有更新==>也就是input还没有产生
解决: 调用$nextTick()在回调函数中获取input调用focus => 回调是在dom更新之后执行的
点击保存发送请求添加或更新平台属性, 在发送请求前, 需要对收集的数据进行一些处理
过滤掉属性值名称为空的属性值对象
删除属性值对象中的edit属性
15. 说说你项目中echarts的使用和遇到的问题
- 如何动态显示图表
- echarts:
- 在mounted设置option ==> 如果是静态数据就已经可以了
- 监视数据改变, 当数据时, 再重新设置带数据option
- vue-echarts:
- 只需要给v-chart指定动态option属性, 通过调用getOption得到配置对象
- 动态显示图表不能正常显示
- 原因: 初始data数据是undefined reportData: {}
- 解决: 给data对应的数据一个空数组的默认值
- 图表显示后不能自适应大小
- 原因: 窗口大小改变, 且父元素大小改变时, 图表没有重新绘制
- 解决:
- echarts: 给window绑定resize监听, 在回调中调用chart对象的resize()方法
- vue-echarts: 只需要指定autoresize属性即可
- tooltip提示框有时会自动隐藏超出容器区域的部分
- 解决一: confine: true, // 将 tooltip 框限制在容器的区域内
- 问题: 档住了鼠标
- 解决二: position (point, params, dom, rect, size) 返回位置坐标 [x, -40]
前台移动 WEB 应用
1. 有哪些功能模块?
首页
搜索
分类
值得买
用户
商品详情
购物车
下单支付
2. 使用了哪些库?
vue
vue-router
vuex
vant ui
axios
mockjs
nprogress
uuidjs
lodash
amfe-flexible
postcss-pxtorem
3. 说说项目的适配如何实现的
- 实现rem适配
-
- 下载依赖包
yarn add amfe-flexible postcss [email protected]
说明:
amfe-flexible: 将页面宽度
postcss-pxtorem不能下载最新版本, 与postcss不适配
-
- 配置postcss-pxtorem
postcss.config.js
module.exports = {
plugins: {
'postcss-pxtorem': {
rootValue: 37.5, // 设计图页面宽度为375, 划分成10份, 指定1rem=37.5
propList: ['*'],
},
},
};
-
- 在入口js中加载amfe-flexible
main.js
import 'amfe-flexible'
4. 项目可说的一些功能技术点
- 移动端rem适配,使用amfe-flexible和postcss-pxtorem插件
- 整体界面使用vant ui库, 并实现按需引入打包
- 通过深度作用域选择器修改vant组件内部样式
- 使用axios请求后台接口, 并对xios进行二次封装, 使用NProgress显示请求进度提示
- 重写路由器的push和replace方法, 解决路由重复跳转报错的bug
- 配置路由的滚动行为, 路由跳转总能停留在顶部, 返回时能停留在原来的位置
- 利用路由的meta配置保存是否显示底部tab的标识, 来控制tab的显示隐藏
- 实现推荐页面与分类频道页的切换显示, 并解决频道标题不能正常选中的相关Bug
- 封装搜索框组件,使用v-model和$attrs&$listeners实现组件间通信
- 使用vant ui的Image组件实现图片懒加载
- 值得买界面: 实现小单元格的自定义轮播效果
- 值得买界面: 使用vue-waterfall-easy插件实现瀑布流分页加载效果
5. 说一个记忆深刻的开发过程中的问题
问题: 首页选中某个频道显示对应的列表页面后, 刷新显示的列表正确, 但会自动选中推荐项
原因:
推荐的是静态的, 初始时就会渲染
其它频道列表的在初始时不存在, 只有请求得到列表数据后才会产生渲染
导致的问题 =>
用来存储标识哪个tab显示的navId在初始值为当前对应的分类Id值
但由于初始只有推荐的, navId会自动被赋值为推荐的的标识name值-1
等到频道列表的产生时, 不可能再选中对应的频道了
Vue项目优化
1. Vue代码层面优化
- v-for 遍历列表
- 指定非下标的唯一key, 尽量不用index, 如果只用于展示就没关系
- 不同时使用 v-if
- 图片资源懒加载
- 使用vue-lazyload element-ui/vant-ui Image组件本身就有懒加载的功能
- 路由组件懒加载 ==> 预加载
- 第三方插件的按需引入打包
- element-ui / vant /lodash
- 对高频事件进行节流或防抖处理
- 及时销毁事件监听
- 大数组优化
- 冻结响应式数据
- 虚拟列表
2. webpack配置层面优化
- 浏览器兼容处理
- JS: @babel/polyfill => core-js配置useBuiltIns: ‘usage’
- CSS: 给C3样式自动添加浏览器厂商前缀 => autoprefixer => postcss-loader
- 拆分打包与压缩
- 资源预加载(prefetch)
- 生产环境时不生成 SourceMap
- 文件名hash化=>利用浏览器缓存
- 代码Tree Shaking
3. 基础的Web技术层面的优化
- 对打包文件开启 Gzip压缩
- 静态资源(css/js/img)使用CDN引入
TS + Vue3
TS
1. 区别TS与JS?(说说TS的特点)
-
强类型, 声明变量时可以指定特定类型, 编码时可以有更友好的提示(错误或补全), 易于写出更健壮的程序
-
TS支持JS的所有语法特性, 也扩展了一些新的数据类型
- 新的基本类型: 联合类型, 元组, 枚举
- 新的复杂类型: 接口, 泛型
-
TS浏览器是不能直接运行的, 需要编译为JS才能运行
2. 说说你对接口的理解
- 接口是对状态(属性)或行为(方法)的抽象(描述)
- 接口可以用来约束一个对象/函数/类
3. 说说你对泛型的理解
- 泛型: 代表不确定的类型
- 泛型可以用在函数/接口/类上
- 什么时候需要泛型呢?
- 定义函数/接口/类时, 要操作数据类型不确定
- 泛型的3个操作
- 定义泛型类型
- 函数: 定义函数的函数名的右侧
- 接口: 定义接口的接口名的右侧
- 类: 定义类的类名的右侧
- 使用泛型: T
- 函数: 参数/返回值/函数体
- 接口: 接口体内
- 类: 类体内
- 指定泛型对应的具体类型: <具体类型名>
- 函数: 调用函数时函数名的右侧
- 接口: 定义实现类时接口名的右侧
- 类: 创建实例时类名的右侧
Vue3
Vue3 比 Vue2 有什么优势
-
更好的代码组织和逻辑抽离
- 设计
composition API
来代替 option API
- 更便于可复用功能代码的封装提取, 代码可阅读性更高
-
体积更小
- 将功能以多个函数提供出来, 我们会进行按需引入使用
- 引入
tree-shaking
, 可以在打包压缩时, 将无用模块“摇掉”
-
性能更好/更快
-
使用proxy代替defineProperty来实现数据劫持
-
diff算法优化: 静态虚拟节点添加静态标记, 不进行diff比较
-
静态提升: 静态虚拟节点提升到render的外面, 缓存起来, 不创建新的
-
更好的 TS 支持
- 对TS的类型检查更友好
-
更好的脚手架工具vite
- 启动运行快了很多
Vue3 生命周期
组合API VS 选项API
-
composition API 优点:
- 更好的代码组织
- 更好的逻辑复用
- 更好的类型推导
-
如何选择:
- 不建议共用,会引起混乱
- 小型项目,业务逻辑简单,用 Options API
- 中大型项目,逻辑复杂,用 Composition API
-
选项式 API(Options API
)
-
所有方法都写在 methods 中,如果 data 中数据越来越多,找数据会非常困难
{{ count }}
-
组合式 API(Composition API
)
-
逻辑会清晰,可以让功能的代码集中抽取到一个函数中进行逻辑复用
{{ num }}
常用的组合API
- 启动函数
- setup()
- 响应式: 核心
- ref()
- reactive()
- computed()
- watch()
- 响应式: 工具
- toRefs()
- 生命周期勾子
- onMounted()
- onBeforeUnmount()
比较Vue2与Vue3的响应式(重要)
1) vue2的响应式
- 核心:
- 对象: 通过defineProperty对对象的已有属性值的读取和修改进行劫持(监视/拦截)
- 数组: 通过重写数组更新数组一系列更新元素的方法来实现元素修改的劫持
Object.defineProperty(data, 'count', {
get () {},
set () {}
})
- 问题
- 对象直接新添加的属性或删除已有属性, 界面不会自动更新
- 直接通过下标替换/添加元素或更新length, 界面不会自动更新
2) Vue3的响应式
- 核心:
- 通过Proxy(代理): 拦截对data任意属性的任意(13种)操作, 包括属性值的读写, 属性的添加, 属性的删除等…
- 通过 Reflect(反射): 动态对代理对象的相应属性进行特定的操作
- 文档:
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
const p = new Proxy(data, {
// 拦截读取属性值
get (target, prop) {
return Reflect.get(target, prop)
},
// 拦截设置属性值或添加新属性
set (target, prop, value) {
return Reflect.set(target, prop, value)
},
// 拦截删除属性
deleteProperty (target, prop) {
return Reflect.deleteProperty(target, prop)
}
})
p.name = 'tom'
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Proxy 与 Reflecttitle>
head>
<body>
<script>
const user = {
name: "John",
age: 12
};
/*
proxyUser是代理对象, user是被代理对象
后面所有的操作都是通过代理对象来操作被代理对象内部属性
*/
const proxyUser = new Proxy(user, {
get(target, prop) {
console.log('劫持get()', prop)
return Reflect.get(target, prop)
},
set(target, prop, val) {
console.log('劫持set()', prop, val)
return Reflect.set(target, prop, val); // (2)
},
deleteProperty (target, prop) {
console.log('劫持delete属性', prop)
return Reflect.deleteProperty(target, prop)
}
});
// 读取属性值
console.log(proxyUser===user)
console.log(proxyUser.name, proxyUser.age)
// 设置属性值
proxyUser.name = 'bob'
proxyUser.age = 13
console.log(user)
// 添加属性
proxyUser.sex = '男'
console.log(user)
// 删除属性
delete proxyUser.sex
console.log(user)
script>
body>
html>
pinia VS vuex
-
Pinia 没有 mutations
, 在actions
中可以直接同步更新state或异步更新state
-
Pinia中可以包含多个store, 而且相互独立, 且不进行合并, 没有模块的嵌套结构
-
无需手动注册 store,创建出的store直接就可以使用
-
更好的 TypeScript
支持, 提示补全很到位
vue-router V4的变化
- 创建路由器有变化
- new Router 变成 createRouter
- createWebHistory()与createWebHashHistory() 取代了 ‘history’ 与 ‘hash’
- 动态添加路由
- 以前可以一次添加多个: router.addRoutes(routes)
- 现在只能一次添加一个: router.addRoute(route)
- 通配路由的path变了
- 以前的path: *
- 现在的path: /:pathMatch(.*)
vue3 & TS 语法
-
声明接收props
// 定义接口, 约束prop
interface Props {
count: number;
updateCount(val: number): void;
}
defineProps()
-
原生事件
-
原生标签上绑定
-
组件标签上绑定: 组件内部没有声明为自定义事件
-
自定义事件
// 声明事件
const emit = defineEmits<{
(e: 'xxx', val: string): void
(e: 'click', val: object): void
(e: 'increment', val: number): void
}>()
// ts 中分发事件
emit('xxx', 'abc')
// 模板中分发事件
$emit('increment', 5)
-
全局事件总线
- vue本身不再提供事件总线的API: 没有$on方法了
- 需要使用
mitt
或 pubsub-js
第三方工具包
-
v-model的本质
-
原生标签上: 动态value和原生的input监听
-
组件标签上: 动态modelValue(默认)和自定义的input监听
-
向外暴露方法
-
组件内部的方法默认在外部是不能调用的
-
可以通过 defineExpose暴露
defineExpose({
borrowMoney
})
-
通过ref得到组件对象
// 使用ref标识子组件
// 定义ref
const sonRef = ref | null>(null)
// 通过ref得到子组件对象, 调用其暴露的方法
sonRef.value?.borrowMoney(num)
微信小程序音乐播放器项目
1. 有哪些功能模块?
首页
视频
个人中心
登录
搜索
推荐歌曲
歌曲播放
2. 使用了哪些api/库?
api:
wx.request() 发送请求
wx.navigateTo() 保留当前页面,跳转到应用内的某个页面。但是不能跳到 tabbar 页面。
wx.redirectTo() 关闭当前页面,跳转到应用内的某个页面。但是不允许跳转到 tabbar 页面。
wx.switchTab() 跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面
wx.reLaunch() 关闭所有页面,打开到应用内的某个页面
wx.showToast() 显示消息提示框
wx.showLoading() 显示 loading 提示框
wx.showModal() 显示模态对话框
wx.hideLoading() 关闭提示框
wx.setNavigationBarTitl() 动态设置当前页面的标题
wx.setStorageSync() 将数据存储在本地缓存中指定的 key 中
wx.setStorage() 将数据存储在本地缓存中指定的 key 中。
wx.removeStorageSync() 从本地缓存中移除指定 key
wx.removeStorage() 从本地缓存中移除指定 key
wx.getStorage() 异步获取当前storage的相关信息
wx.getStorageSync() 从本地缓存中同步获取指定 key 的内容。
wx.createVideoContext() 视频播放对象的获取
VideoContext.play() 播放
VideoContext.pause() 暂停
VideoContext.stop() 停止
VideoContext.seek() 跳到指定位置
wx.getBackgroundAudioManager() 背景音频对象的获取
BackgroundAudioManager.play()播放音乐
BackgroundAudioManager.pause()暂停音乐
BackgroundAudioManager.stop()停止音乐
BackgroundAudioManager.onPlay()监听背景音频播放事件
BackgroundAudioManager.onPause()监听背景音频暂停事件
BackgroundAudioManager.onEnded()监听背景音频自然播放结束事件
BackgroundAudioManager.onStop()监听背景音频停止事件
BackgroundAudioManager.onTimeUpdate()监听背景音频播放进度更新事件
wx.login() 获取登录凭证
wx.getUserProfile() 获取用户信息
库:
pubsub-js.js 实现页面与页面之间的通信
moment.js 格式化日期
3. 说说项目的适配如何实现的
- 小程序本身非常支持flex布局,默认使用rpx进行适配
1px=2rpx
4. wx.request的封装
wx.request 是一个异步的方法
success回调函数的作用域发生了改变,所以this的指向不是当前函数,另外使用promise可以解决异步嵌套的问题
config.js文件:
export default {
host:'http://localhost:3000'
}
request.js文件:
import config from './config.js'
export default (url, data = {}, method = 'GET') => {
return new Promise((resolve, reject) => {
wx.request({
url: config.host + url,
data,
method,
success: res => {
resolve(res.data)
},
fail: err => {
reject(err)
}
})
})
}
5. 页面之间如何实现通信
缓存的方式:
wx.setStorage()
wx.setStorageSync()
应用:可以缓存cookie的信息,实现异步请求携带cookie数据
全局唯一实例对象
getApp()
应用:缓存音乐播放/暂停的状态及音乐id数据,从而实现音乐监听相关操作
插件
pubsub-js
应用:自动播放下一曲
eventChannel 事件通道
wx.navigateTo({
url: 'test?id=1',
events: {
// 为指定事件添加一个监听器,获取被打开页面传送到当前页面的数据
acceptDataFromOpenedPage: function(data) {
console.log(data)
},
someEvent: function(data) {
console.log(data)
}
...
},
success: function(res) {
// 通过eventChannel向被打开页面传送数据
res.eventChannel.emit('acceptDataFromOpenerPage', { data: 'test' })
}
})
Page({
onLoad: function(option){
console.log(option.query)
const eventChannel = this.getOpenerEventChannel()
eventChannel.emit('acceptDataFromOpenedPage', {data: 'test'});
eventChannel.emit('someEvent', {data: 'test'});
// 监听acceptDataFromOpenerPage事件,获取上一页面通过eventChannel传送到当前页面的数据
eventChannel.on('acceptDataFromOpenerPage', function(data) {
console.log(data)
})
}
})
6. 微信小程序中事件的理解
事件是视图层到逻辑层的通讯方式。
事件可以将用户的行为反馈到逻辑层进行处理。
事件可以绑定在组件上,当达到触发事件,就会执行逻辑层中对应的事件处理函数。
事件对象可以携带额外信息,如 id, dataset, touches。
事件分为冒泡事件和非冒泡事件:
冒泡事件:当一个组件上的事件被触发后,该事件会向父节点传递。
非冒泡事件:当一个组件上的事件被触发后,该事件不会向父节点传递。
事件处理函数中的event参数中可以携带相关的参数,如:id/data-
id的方式携带的数据的类型最终为string类型
data-的方式可以直接存储数值类型的数据,使用的时候无需转换
7. 自定义组件的理解
微信小程序中自定义组件通常存放在components目录中
自定义组件component和页page的区别在于xxx.js文件
组件的js文件中代码:
Component({
data: {}, 自身使用的数据
properties: { }, 某个页面传递进来的数据
methods: { } 当前组件所需的方法
})
页page的js文件中代码:
Page({
data:{},
onLoad(){},
....
})
8. 模版的理解
WXML提供模板(template),可以在模板中定义代码片段,然后在不同的地方调用。
定义模板
使用 name 属性,作为模板的名字。然后在内定义代码片段
{{index}}: {{msg}}
Time: {{time}}
使用模板
使用 is 属性,声明需要的使用的模板,然后将模板所需要的 data 传入
Page({
data: {
person: {
name:'小甜甜',
age:20
}
}
})
页面中使用模版:
1. 先引入模版的结构文件
2. 然后引入模版的样式文件
在页面的.wxss文件中引入
@import 'item.wxss'
9. 生命周期的理解
微信小程序的生命周期分为:
App的生命周期/Page的生命周期/Component的生命周期
App的生命周期如下:
onLaunch(){} 生命周期回调——监听小程序初始化。
onShow(){} 生命周期回调——监听小程序启动或切前台。
onHide(){} 生命周期回调——监听小程序切后台
Page的生命周期
onLoad(){} 生命周期回调—监听页面加载
onShow(){} 生命周期回调—监听页面显示
onReady(){} 生命周期回调—监听页面初次渲染完成
onHide(){} 生命周期回调—监听页面隐藏
onUnload(){} 生命周期回调—监听页面卸载
Component的生命周期
created(){} 组件生命周期函数-在组件实例刚刚被创建时执行,注意此时不能调用 setData )
attached(){} 组件生命周期函数-在组件实例进入页面节点树时执行)
ready(){} 组件生命周期函数-在组件布局完成后执行)
moved(){} 组件生命周期函数-在组件实例被移动到节点树另一个位置时执行)
detached(){} 组件生命周期函数-在组件实例被从页面节点树移除时执行)
10. WXML语法
数据绑定使用 Mustache 语法(双大括号)将变量包起来
{{msg}}
组件属性(需要在双引号之内)
控制属性(需要在双引号之内)
关键字(需要在双引号之内)
true:boolean 类型的 true,代表真值。
false: boolean 类型的 false,代表假值。
特别注意:不要直接写 checked="false",其计算结果是一个字符串,转成 boolean 类型后代表真值
列表渲染
wx:for
在组件上使用 wx:for 控制属性绑定一个数组,即可使用数组中各项的数据重复渲染该组件。
默认数组的当前项的下标变量名默认为 index,数组当前项的变量名默认为 item
{{index}}: {{item.message}}
使用 wx:for-item 可以指定数组当前元素的变量名,
使用 wx:for-index 可以指定数组当前下标的变量名:
{{idx}}: {{itemName.message}}
block wx:for
类似 block wx:if,也可以将 wx:for 用在 标签上,以渲染一个包含多节点的结构块。例如:
{{index}}:
{{item}}
wx:key
如果列表中项目的位置会动态改变或者有新的项目添加到列表中,并且希望列表中的项目保持自己的特征和状态(如 input 中的输入内容,switch 的选中状态),需要使用 wx:key 来指定列表中项目的唯一的标识符。
wx:key 的值以两种形式提供
字符串,代表在 for 循环的 array 中 item 的某个 property,该 property 的值需要是列表中唯一的字符串或数字,且不能动态改变。
保留关键字 *this 代表在 for 循环中的 item 本身,这种表示需要 item 本身是一个唯一的字符串或者数字。
11. uni-app框架的理解
uni-app 是一个使用 Vue.js (opens new window)开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)、快应用等多个平台。
微信小程序相关面试题
1. 谈谈你对微信小程序的理解
1. 2017年度百度百科十大热词之一
2. 张小龙对其的定义是无需安装,用完即走,实际上是需要安装的,只不过小程序的体积特别小, 下载速度很快,用户感觉不到下载的过程
3. 2017年1月9日0点上线
4. 没有DOM,组件化开发
5. 体积小,单个压缩包体积不能大于2M,否则无法上线
6. 小程序的四个重要的文件
a) *.js
b) *.wxml ---> view结构 ----> html
c) *.wxss ---> view样式 -----> css
d) *. json ----> view 数据 -----> json文件
7. 小程序适配方案: rpx (responsive pixel响应式像素单位)
2. 微信小程序的分包
1 什么要分包
1. 小程序要求压缩包体积不能大于2M,否则无法发布
2. 实际开发中小程序体积如果大于2M就需要使用分包机制进行发布上传
3. 分包后可解决2M限制,并且能分包加载内容,提高性能
4. 分包后单个包的体积不能大于2M
5. 分包后所有包的体积不能大于20M
2 分包形式
1. 常规分包
2. 独立分包
3. 分包预下载
3 常规分包
1. 开发者通过在 app.json subpackages 字段声明项目分包结构
2. 特点:
a) 加载小程序的时候先加载主包,当需要访问分包的页面时候才加载分包内容
b) 分包的页面可以访问主包的文件,数据,图片等资源
c) 主包:
3. 主包来源: 除了分包以外的内容都会被打包到主包中
4. 通常放置启动页/tabBar页面
4 独立分包
1. 设置independent为true
2. 特点:
a) 独立分包可单独访问分包的内容,不需要下载主包
b) 独立分包不能依赖主包或者其他包的内容
3. 使用场景
a) 通常某些页面和当前小程序的其他页面关联不大的时候可进行独立分包
b) 如:临时加的广告页 || 活动页
5 分包预下载
1. 配置
a) app.json中设置preloadRule选项
b) key(页面路径): {packages: [预下载的包名 || 预下载的包的根路径])}
4. vue和小程序的区别
vue是渐进式的框架
小程序是一种全新的连接用户与服务的方式,它可以在微信内被便捷地获取和传播
1. vue的生命周期和小程序的生命周期对比
2. vue中的数据代理和小程序中的是否有数据代理进行对比
3. 组件化的对比
4. vue中的组件通信,小程序中的页面之间通信
5. vue中的路由和小程序的路由对比
6. vue组件中的方法定义位置和小程序中的方法定义位置对比
7. 异步请求的方式(axios/wx.request)
8. vue需要ie8以上浏览器,小程序在安卓/ios系统上
5. 小程序部分常见面试题
提高微信小程序的应用速度的常见方式有哪些?
1. 提高页面加载速度
2. 用户行为预测
3. 减少默认data的大小
4. 组件化方案
5. 分包预下载
小程序与原生App相比优缺点?
优点:
基于微信平台开发,微信本身自带的流量是最大的优势,无需安装,只要打开微信就能用,不占用用户手机内存,体验好,开发周期短,一般最多一个月可以上线完成,开发所需的资金少,所需资金是开发原生APP一半不到,小程序名称是唯一性的,在微信的搜索里权重很高
容易上手,只要之前有HTML+CSS+JS基础知识,写小程序基本上没有大问题;当然如果了解ES6+CSS3则完全可以编写出即精简又动感的小程序;
基本上不需要考虑兼容性问题,只要微信可以正常运行的机器,就可以运行小程序;
发布、审核高效,基本上上午发布审核,下午就审核通过,升级简单,而且支持灰度发布;
开发文档比较完善,开发社区比较活跃;
新增webview组件,可以展示网页
支持插件式开发,一些基本功能可以开发成插件,供多个小程序调用;
缺点:
1.局限性很强,(比如压缩体积不能超过2M。样式单一。小程序的部分组件已经是成型的了,样式不可以修改。例如:幻灯片、导航。)只能依赖于微信依托于微信,无法开发后台管理功能。
2.不利于推广推广面窄,不能分享朋友圈,只能通过分享给朋友,附近小程序推广。其中附近小程序也受到微信的限制
3.后台调试麻烦,因为API接口必须https请求,且公网地址,也就是说后台代码必须发布到远程服务器上;当然我们可以修改host进行dns映射把远程服务器转到本地,或者开启tomcat远程调试;不管怎么说终归调试比较麻烦。
4.前台测试有诸多坑,最头疼莫过于模拟器与真机显示不一致
5.小程序中对js使用做了很多限制,不能使用:new Function,eval,Generator,不能操作cookie,不能操作DOM;
原生App优点:
1、原生的响应速度快
2、对于有无网络操作时,譬如离线操作基本选用原生开发
3、需要调用系统硬件的功能(摄像头、方向传感器、重力传感器、拨号、GPS、语音、短信、蓝牙等功能)
4、在无网络或者若网的情况下体验好。
缺点:
开发周期长,开发成本高
需要下载
简述微信小程序原理?
答:微信小程序采用JavaScript、WXML、WXSS三种技术进行开发,从技术讲和现有的前端开发差不多,但深入挖掘的话却又有所不同。
JavaScript:首先JavaScript的代码是运行在微信App中的,并不是运行在浏览器中,因此一些H5技术的应用,需要微信App提供对应的API支持,而这限制住了H5技术的应用,且其不能称为严格的H5,可以称其为伪H5,同理,微信提供的独有的某些API,H5也不支持或支持的不是特别好。
WXML:WXML微信自己基于XML语法开发的,因此开发时,只能使用微信提供的现有标签,HTML的标签是无法使用的。
WXSS:WXSS具有CSS的大部分特性,但并不是所有的都支持,而且支持哪些,不支持哪些并没有详细的文档。
微信的架构,是数据驱动的架构模式,它的UI和数据是分离的,所有的页面更新,都需要通过对数据的更改来实现。
小程序分为两个部分webview和appService。其中webview主要用来展现UI,appService有来处理业务逻辑、数据及接口调用。它们在两个进程中运行,通过系统层JSBridge实现通信,实现UI的渲染、事件的处理
分析下微信小程序的优劣势?
优势:
1、无需下载,通过搜索和扫一扫就可以打开。
2、良好的用户体验:打开速度快。
3、开发成本要比App要低。
4、安卓上可以添加到桌面,与原生App差不多。
5、为用户提供良好的安全保障。小程序的发布,微信拥有一套严格的审查流程, 不能通过审查的小程序是无法发布到线上的。
劣势:
1、限制较多。页面大小不能超过1M。不能打开超过5个层级的页面。
2、样式单一。小程序的部分组件已经是成型的了,样式不可以修改。例如:幻灯片、导航。
3、推广面窄,不能分享朋友圈,只能通过分享给朋友,附近小程序推广。其中附近小程序也受到微信的限制。
4、依托于微信,无法开发后台管理功能。
小程序的发布流程(开发流程)
注册微信小程序账号
获取微信小程序的 AppID
下载微信小程序开发者工具
创建demo项目
去微信公众平台配置域名
手机预览
代码上传
提交审核
小程序发布
webview中的页面怎么跳回小程序中
首先,需要在你的html页面中引用一个js文件。
然后为你的按钮标签注册一个点击事件:
$(".kaiqi").click(function(){undefined
wx.miniProgram.redirectTo({url: '/pages/indexTwo/indexTwo'})
});
这里的redirectTo跟小程序中的wx.redirectTo()跳转页面是一样的,会关闭当前页跳转到页面。
你也可以替换成navigateTo,跳转页面不会关闭当前页。
使用webview直接加载要注意哪些事项?
一、必须要在小程序后台使用管理员添加业务域名;
二、h5页面跳转至小程序的脚本必须是1.3.1以上;
三、微信分享只可以都是小程序的主名称了,如果要自定义分享的内容,需小程序版本在1.7.1以上;
四、h5的支付不可以是微信公众号的appid,必须是小程序的appid,而且用户的openid也必须是用户和小程序的。
小程序授权登录流程。
(授权,微信登录获取code,微信登录,获取 iv , encryptedData 传到服务器后台,如果没有注册,需要注册。)
(授权,微信登录获取code,微信登录,获取 iv , encryptedData 传到服务器后台,如果没有注册,需要注册。)
小程序支付如何实现?
1、小程序注册,要以公司的以身份去注册一个小程序,才有微信支付权限;
2、绑定商户号。
3、在小程序填写合法域
4.调用wx.login()获取appid
5.调用
wx.requestPayment(
{
'timeStamp': '',//时间戳从1970年1月1日00:00:00至今的秒数,即当前的时间
'nonceStr': '',//随机字符串,长度为32个字符以下。
'package': '',//统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=*
'signType': 'MD5',//签名类型,默认为MD5,支持HMAC-SHA256和MD5。注意此处需与统一下单的签名类型一致
'paySign': '',//签名,具体签名方案参见微信公众号支付帮助文档;
'success':function(res){},//成功回调
'fail':function(res){},//失败
'complete':function(res){}//接口调用结束的回调函数(调用成功、失败都会执行)
})
小程序还有哪些功能?
客服功能,录音,视频,音频,地图,定位,拍照,动画,canvas
微信小程序与H5的区别?
第一条是运行环境的不同
传统的HTML5的运行环境是浏览器,包括webview,而微信小程序的运行环境并非完整的浏览器,是微信开发团队基于浏览器内核完全重构的一个内置解析器,针对小程序专门做了优化,配合自己定义的开发语言标准,提升了小程序的性能。
第二条是开发成本的不同
只在微信中运行,所以不用再去顾虑浏览器兼容性,不用担心生产环境中出现不可预料的奇妙BUG
第三条是获取系统级权限的不同
系统级权限都可以和微信小程序无缝衔接
第四条便是应用在生产环境的运行流畅度
长久以来,当HTML5应用面对复杂的业务逻辑或者丰富的页面交互时,它的体验总是不尽人意,需要不断的对项目优化来提升用户体验。但是由于微信小程序运行环境独立
小程序怎么实现下拉刷新?
方式1:
通过在 app.json 中, 将 "enablePullDownRefresh": true, 开启全局下拉刷新。
或者通过在 组件 .json , 将 "enablePullDownRefresh": true, 单组件下拉刷新。
方式2:
scroll-view :使用该滚动组件 自定义刷新,通过 bindscrolltoupper 属性, 当滚动到顶部/左边,会触发 scrolltoupper事件,所以我们可以利用这个属性,来实现下拉刷新功能。
HTML + CSS
1. 标签语义化的意义
- 开发者更容易理解,减少差异化,方便团队开发和维护
- 机器更容易理解结果(搜索爬虫、方便其他设备解析(读屏幕软件、盲人设备、移动设备)
2. 写页面结构应该注意什么
- 尽可能少的使用没有语义的 div 和 span 元素
- 块级元素和内联元素的嵌套一定要符合 web 标准,比如内联元素就是不能嵌套块级元素
3. HTML5 新特性
- 新的语义化元素:article 、footer 、header 、nav 、section
- 表单增强,新的表单控件:calendar 、date 、time 、email 、url 、search
- 新的 API:音频(用于媒介回放的 video 和 audio 元素)、图形(绘图 canvas 元素)
- 新的 API:离线,通过创建 cache manifest 文件,创建应用程序缓存
- 新的 API:本地存储,localStorage-没有时间限制的数据存储,sessionStorage-session 数据存储(关闭浏览器窗口数据删除)
- 新的 API:实时通讯,设备能力
4. CSS3 新特性
-
CSS3实现圆角(border-radius),阴影(box-shadow),
-
对文字加特效(text-shadow、),线性渐变(gradient),旋转(transform)
-
transform:rotate(9deg) scale(0.85,0.90) translate(0px,-30px) skew(-9deg,0deg);//旋转,缩放,定位,倾斜3.
-
增加了更多的CSS选择器 多背景 rgba
-
在CSS3中唯一引入的伪元素是::selection.
-
媒体查询,多栏布局
5. 盒子模型
页面渲染时,dom 元素所采用的 布局模型。可通过box-sizing
进行设置。根据计算宽高的区域可分为:
content-box
(W3C 标准盒模型)
border-box
(IE 盒模型)
padding-box
margin-box
(浏览器未实现)
6. BFC
块级格式化上下文,是一个独立的渲染区域,让处于 BFC 内部的元素与外部的元素相互隔离,使内外元素的定位不会相互影响。
IE 下为 Layout,可通过 zoom:1 触发
-
触发条件:
- 根元素
position: absolute/fixed
display: inline-block / table
float
元素
ovevflow
!== visible
-
规则:
- 属于同一个 BFC 的两个相邻 Box 垂直排列
- 属于同一个 BFC 的两个相邻 Box 的 margin 会发生重叠
- BFC 中子元素的 margin box 的左边, 与包含块 (BFC) border box 的左边相接触 (子元素 absolute 除外)
- BFC 的区域不会与 float 的元素区域重叠
- 计算 BFC 的高度时,浮动子元素也参与计算 - 文字层不会被浮动层覆盖,环绕于周围
-
应用:
- 阻止
margin
重叠
- 可以包含浮动元素 —— 清除内部浮动(清除浮动的原理是两个
div
都位于同一个 BFC 区域之中)
- 自适应两栏布局
- 可以阻止元素被浮动元素覆盖
7. 选择器优先级
!important
> 行内样式 > #id
> .class
> tag
> * > 继承 > 默认
- 选择器 从右往左 解析
8. 去除浮动影响,防止父级高度塌陷
- 通过增加尾元素清除浮动
:after /
: clear: both
- 创建父级 BFC
- 父级设置高度
9. link 与 @import 的区别
link
功能较多,可以定义 RSS,定义 Rel 等作用,而@import
只能用于加载 css
- 当解析到
link
时,页面会同步加载所引的 css,而@import
所引用的 css 会等到页面加载完才被加载
@import
需要 IE5 以上才能使用
link
可以使用 js 动态引入,@import
不行
10. CSS 预处理器(Sass/Less/Postcss)
CSS 预处理器的原理: 是将类 CSS 语言通过 Webpack 编译 转成浏览器可读的真正 CSS。在这层编译之上,便可以赋予 CSS 更多更强大的功能,常用功能:
- 嵌套
- 变量
- 循环语句
- 条件语句
- 自动前缀
- 单位转换
- mixin 复用
11. 单行文本溢出
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
12. 多行文本溢出
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2; /*2*/
-webkit-box-orient: vertical;
13. flex布局
-
Flex是Flexible Box的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性。
- 布局的传统解决方案,基于盒状模型,依赖display属性 + position属性 + float属性。它对于那些特殊布局非常不方便,比如,垂直居中就不容易实现。
-
分清主轴和交叉轴
- flex-direction 为 row 时,主轴是横向的,相反的为 column 时,主轴是纵向的。
-
简单的分为容器属性和元素属性
-
容器的属性:
flex-direction:决定主轴的方向(即子item的排列方法)
flex-wrap:决定换行规则
flex-flow:flex-direction和
flex-wrap的简写形式,默认值为
row nowrap
justify-content:对其方式,水平主轴对齐方式
align-items:对齐方式,竖直轴线方向
-
项目的属性(元素的属性):
flex:是flex-grow和flex-shrink、flex-basis的简写,默认值为0 1 auto。
order属性:定义项目的排列顺序,顺序越小,排列越靠前,默认为0
flex-grow属性:定义项目的放大比例,即使存在空间,也不会放大
flex-shrink属性:定义了项目的缩小比例,当空间不足的情况下会等比例的缩小,如果定义个item的flow-shrink为0,则为不缩小
flex-basis属性:定义了在分配多余的空间,项目占据的空间。
14. 圣杯布局
- 圣杯特点简单记为:有头、有尾、包三列,圣杯布局中间有container大容器包裹着左、中、右三列区域
- 左、中、右是独立的三个区域,都处于一个层级
15. 双飞翼布局
- 左、中、右是独立的三个区域,中间区域属于最上面的层级
移动端
1. 基础知识
-
1英寸(inch) = 2.54厘米(cm)
-
IPhone 6 的屏幕分辨率为 750 * 1334 设备独立像素为 375 * 667
-
物理像素:由屏幕制造商决定,屏幕生产后无法修改
-
css像素:单位是px,它是为 Web 开发者创造的
-
设备独立像素的出现,使得即使在【高清屏】下,也可以让元素有正常的尺寸,让代码不受到设备的影响,它是设备厂商根据屏幕特性设置的,无法更改。
-
1个位图像素对应1个物理像素,图片才能得到完美清晰的展示
-
pc端视口:默认宽度和浏览器窗口的宽度一致,也被称为初始包含块document.documentElement.clientWidth
-
移动端视口:
-
布局视口:一般是980px左右,布局视口经过压缩后,横向的宽度用css像素表达就不再是375px了,而是980px
-
视觉视口:用户可见的区域,它的绝对宽度永远和设备屏幕一样宽
-
理想视口:布局视口宽度 与 屏幕等宽(设备独立像素),靠meta标签实现
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
2. 适配
1.viewport 适配
- 方法:拿到设计稿之后,设置布局视口宽度为设计稿宽度,然后直接按照设计稿给宽高进行布局即可。
- 一般适用于:计图稿宽度 < 375
2.rem适配
- 方案一:(百度)
- 设置完美视口
- 通过js设置根字体大小 = *( 当前设备横向独立像素值 100) / 设计稿宽度
- 编写样式时,直接以rem为单位,值为:设计值 / 100
- 增加 JS 代码进行实时适配
- 方法二:(淘宝、搜狐、唯品会)
- 设置完美视口
- 通过js设置根字体大小 = 当前设备横向独立像素值 / 10
- 编写样式时,直接以rem为单位,值为:设计值 / (设计稿宽度 / 10)
- 增加 JS 代码进行实时适配
3.vw适配
vw和vh是两个相对单位
- 1vw = 等于布局视口宽度的1%
- 1vh = 等于布局视口高度的1%
4.1px物理像素边框
高清屏幕下 1px 对应更多的物理像素,所以 1 像素边框看起来比较粗,解决方法如下
方法一
使用媒查询:
@media screen and (-webkit-min-device-pixel-ratio:2){
#demo{
border: 0.5px solid black;
}
}
或
@media screen and (-webkit-min-device-pixel-ratio:2){
#demo2::after{
transform:scaleY(0.5);
}
}
方法二
根据dpr扩大布局视口,例如dpr为n则布局视口改为原来的n倍,则元素尺寸均变为原来的n分之一,为了保证元素尺寸比例不变,扩大根字体为原来的n倍,但整个过程中边框一直用px作为单位,不用rem。
-
rem 页面布局
-
元素的边框设置为 1px
-
通过 viewport 中的 initial-scale 将布局视口扩大n倍,这样页面元素就比原来缩小了n倍
var viewport = document.querySelector('meta[name=viewport]')
var scale = 1 / window.devicePixelRatio
viewport.setAttribute('content', 'width=device-width,initial-scale=' + scale);
- 重新设置根元素字体
var fontSize = parseInt(document.documentElement.style.fontSize);
document.documentElement.style.fontSize = fontSize * window.devicePixelRatio + 'px'
3. 移动端事件
- touchstart 元素上触摸开始时触发
- touchmove 元素上触摸移动时触发
- touchend 手指从元素上离开时触发
- touchcancel 触摸被打断时触发
4. 移动端中touchstart,touchend,click执行顺序
- touchstart
- touchend
- click,浏览器在 click 后会等待约300ms去判断用户是否有双击行为,如果300ms内没有再一次click,那么就判定这是一次单击行为
5. 点击穿透
-
touch 事件结束后会默认触发元素的 click 事件
方法一:阻止默认行为
方法二:使背后元素不具备click特性,用touchXxxx代替click
方法三:让背后的元素暂时失去click事件,300毫秒左右再复原,属性pointer-events: none;
方法四:让隐藏的元素延迟300毫秒左右再隐藏