Canvas 是 HTML5 提供的一个用于展示绘图效果的标签. Canvas 原意为画布, 在 HTML 页面中用于展示绘图效果. 最早 Canvas 是苹果提出的一个方案, 今天已经在大多数浏览器中实现。
canvas 的使用领域
游戏
大数据可视化数据
banner 广告
多媒体
模拟仿真
远程操作
图形编辑
判断浏览器是否支持 canvas 标签
var canvas = document.getElementById('canvas')
if (canvas.getContext) {
console.log('你的浏览器支持Canvas!')
} else {
console.log('你的浏览器不支持Canvas!')
}
canvas 的基本用法
1、使用 canvas 标签, 即可在页面中开辟一格区域,可以设置其宽高,宽高为 300 和 150
<canvas></canvas>
2、获取 dom 元素 canvas
canvas 本身不能绘图. 是使用 JavaScript 来完成绘图. canvas 对象提供了各种绘图用的 api。
var cas = document.querySelector('canvas')
3、通过 cas 获取上下文对象(画布对象!)
var ctx = cas.getContext('2d')
4、通过 ctx 开始画画(设置起点 设置终点 连线-描边 )
ctx.moveTo(10, 10)
ctx.lineTo(100, 100)
ctx.stroke()
绘制线条
设置开始位置: context.moveTo( x, y )
设置终点位置: context.lineTo( x, y )
描边绘制: context.stroke()
填充绘制: context.fill()
闭合路径: context.closePath()
canvas 还可以设置线条的相关属性,如下:
CanvasRenderingContext2D.lineWidth 设置线宽.
CanvasRenderingContext2D.strokeStyle 设置线条颜色.
CanvasRenderingContext2D.lineCap 设置线末端类型,'butt'( 默认 ), 'round', 'square'.
CanvasRenderingContext2D.lineJoin 设置相交线的拐点, 'miter'(默认),'round', 'bevel',
CanvasRenderingContext2D.getLineDash() 获得线段样式数组.
CanvasRenderingContext2D.setLineDash() 设置线段样式.
CanvasRenderingContext2D.lineDashOffset 绘制线段偏移量.
封装一个画矩形的方法
function myRect(ctxTmp, x, y, w, h) {
ctxTmp.moveTo(x, y)
ctxTmp.lineTo(x + w, y)
ctxTmp.lineTo(x + w, y + h)
ctxTmp.lineTo(x, y + h)
ctxTmp.lineTo(x, y)
ctxTmp.stroke()
}
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
myRect(ctx, 50, 50, 200, 200)
绘制矩形
fillRect( x , y , width , height) 填充以(x,y)为起点宽高分别为 width、height 的矩形 默认为黑色
stokeRect( x , y , width , height) 绘制一个空心以(x,y)为起点宽高分别为 width、height 的矩形
clearRect( x, y , width , height ) 清除以(x,y)为起点宽高分别为 width、height 的矩形 为透明
绘制圆弧
绘制圆弧的方法有
CanvasRenderingContext2D.arc()
CanvasRenderingContext2D.arcTo()
6 个参数: x,y(圆心的坐标),半径,起始的弧度(不是角度 deg),结束的弧度,(bool 设置方向 ! )
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
ctx.arc(100, 100, 100, 0, degToArc(360))
ctx.stroke()
// 角度转弧度
function degToArc(num) {
return (Math.PI / 180) * num
}
绘制扇形
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
ctx.arc(300, 300, 200, degToArc(125), degToArc(300))
// 自动连回原点
ctx.closePath()
ctx.stroke()
function degToArc(num) {
return (Math.PI / 180) * num
}
制作画笔
声明一个变量作为标识
鼠标按下的时候,记录起点位置
鼠标移动的时候,开始描绘并连线
鼠标抬起的时候,关闭开关
点击查看效果图
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
var isDraw = false
// 鼠标按下事件
cas.addEventListener('mousedown', function () {
isDraw = true
ctx.beginPath()
})
// 鼠标移动事件
cas.addEventListener('mousemove', function (e) {
if (!isDraw) {
// 没有按下
return
}
// 获取相对于容器内的坐标
var x = e.offsetX
var y = e.offsetY
ctx.lineTo(x, y)
ctx.stroke()
})
cas.addEventListener('mouseup', function () {
// 关闭开关了!
isDraw = false
})
手动涂擦
原理和画布相似,只不过用的是clearRect()方法。
点击查看效果图
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
ctx.fillRect(0, 0, 600, 600)
// 开关
var isClear = false
cas.addEventListener('mousedown', function () {
isClear = true
})
cas.addEventListener('mousemove', function (e) {
if (!isClear) {
return
}
var x = e.offsetX
var y = e.offsetY
var w = 20
var h = 20
ctx.clearRect(x, y, w, h)
})
cas.addEventListener('mouseup', function () {
isClear = false
})
刮刮乐
首先需要设置奖品和画布,将画布置于图片上方盖住,
随机设置生成奖品。
当手触摸移动的时候,可以擦除部分画布,露出奖品区。
点击查看效果图
<div>
<img src="./images/2.jpg" alt="" />
<canvas width="600" height="600"></canvas>
</div>
css
img {
width: 600px;
height: 600px;
position: absolute;
top: 10%;
left: 30%;
}
canvas {
width: 600px;
height: 600px;
position: absolute;
top: 10%;
left: 30%;
border: 1px solid #000;
}
js
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
var img = document.querySelector('img')
// 加一个遮罩层
ctx.fillStyle = '#ccc'
ctx.fillRect(0, 0, cas.width, cas.height)
setImgUrl()
// 开关
var isClear = false
cas.addEventListener('mousedown', function () {
isClear = true
})
cas.addEventListener('mousemove', function (e) {
if (!isClear) {
return
}
var x = e.offsetX
var y = e.offsetY
ctx.clearRect(x, y, 30, 30)
})
cas.addEventListener('mouseup', function () {
isClear = false
})
function setImgUrl() {
var arr = ['./images/1.jpg', './images/2.jpg', './images/3.jpg', './images/4.jpg']
// 0-3
var random = Math.round(Math.random() * 3)
img.src = arr[random]
}
更多demo,请查看 github.com/Michael-lzg…
虽然Vue 3还没有正式发布,但是热爱新技术的我早已按捺不住自己的内心,开始尝试在小项目中使用它了。
根据这篇《今日凌晨Vue3 beta版震撼发布,竟然公开支持脚手架项目!》我搭建了一个Vue 3的脚手架项目,用这种方式搭建的脚手架项目不仅仅只有vue是新版的,就连vue-router、vuex都是的。
给大家截一下package.json的图:
可以看到vue-router和vuex都已经开启4.0时代啦!
不过其实我并没有去了解过vue-router 4.0的新用法什么的,因为我觉得它不像vue 3.0都已经进行到beta的版本不会有特别大的变动。
而vue-router 4.0还是alpha的阶段,所以我认为现在去学习它有些为时尚早。但却就是它!差点酿成了一场惨剧。
旧版vue + vue-router的使用方式
假如你在路由里面定义了一个动态参数通常都会这么写:
{
path: '/:id'
}
然后用编程式导航的时候通常会这样去写:
this.$router.push('/123')
在组件中是这样获取这个参数的:
this.$route.params.id
我以为的新版vue + vue-router的使用方式
由于vue 3.0的Composition API中没有this了,所以我想到了通过获取组件实例的方式来获取$route:
import { defineComponent, getCurrentInstance } from 'vue'
export default defineComponent((props, context) => {
const { ctx } = getCurrentInstance()
console.log(ctx.$route)
})
没想到打印出来的居然是undefined!
这是咋回事呢?
于是我又打印了一遍ctx(ctx是当前组件上下文):
没有$的那些字段是我在组件中自己定义的变量,带$的这些就是vue内置的了,找了半天发现没有$route了,只剩下了一个$router,估计vue-router 4.0把当前路由信息都转移到$router里面去了。
带着猜想,我点开了$router:
currentRoute! 看名字的话感觉应该就是它了!于是乎我:
import { defineComponent, getCurrentInstance } from 'vue'
export default defineComponent((props, context) => {
const { ctx } = getCurrentInstance()
console.log(ctx.$router.currentRoute.value.params.id)
})
果然获取到了!好开心!
实际的新版vue + vue-router用法
在接下来的过程中我用ctx.$router代替了原来的this.$router、用ctx.$router.currentRoute.value代替了原先的this.$route。
尽管在接下来的进度中并没有出现任何的bug,程序一直都是按照我所设想的那样去运行的。
但在项目打包后却出现了意想不到的bug:在跳转路由的时候报了一个在undefined上面没有push的错误。
奇了怪了,在开发阶段程序都没有任何的报错怎么一打包就不行了呢?根据我多年的开发经验,我很快就定位到了是vue-router的错误。
难道这样写是错的吗?可是我打印了ctx,它里面明明有一个$router、$router里面明明就有currentRoute、currentRoute里面明明就有一个value、value里面明明就有params、params里面我一点开明明就看到了传过来的参数啊:
估计可能是vue-router的bug,果然alpha阶段的产物不靠谱,我开始后悔使用新版的vue脚手架项目了。
vue-router里的hooks
不过这时我突然灵光一现,vue 3不是受到了react hooks的启发才产生了Composition API的吗?
那么估计vue-router肯定也会受到react-router的启发了!
还好我学过react,果然技多不压身啊!估计里面肯定是有一个useXxx,就像这样:
import { useXxx } from 'vue-router'
那么应该是use什么呢?按理来说应该会尽量的和以前的API保持一定的联系,我猜应该是useRoute和useRouter吧!
为了验证我的想法,我打开了node_modules找到了vue-router的源码:
果不其然,在第2454和第2455行我发现它导出了useRoute和useRouter,那么就是它了:
import { defineComponent } from 'vue'
import { useRoute, useRouter } from 'vue-router'
export default defineComponent(_ => {
const route = useRoute()
const router = useRouter()
console.log(route.params.id)
router.push('/xxx/xxx')
})
使用这种方式不但可以成功跳转路由,也同样可以获取到路由传过来的参数,这次再打包试了一下,果然就没有之前的那个报错了。
结语
估计以后的vue全家桶要开启全民hooks的时代了,在翻看源码的同时我发现他们把一些示例都写在了vue-router/playground文件夹下了,在里面我发现了一些有趣的用法。
如果有时间的话我会仔细研究一下然后出一篇更加深入的文章给大家,当然如果已经有小伙伴等不及我出新文章的话可以直接进入vue-router-next的github地址:
https://github.com/vuejs/vue-router-next
它的示例都放在了playground这个文件夹下,期待你们研究明白后出一篇更加深入的文章!
最近碰到个需要自动生成表格的任务,作为前端的我,就想在 node 和浏览器中生成强大的表格,所以特此研究了很多关于表格的 npm 库
支持读写 Excel 的 node.js 模块
node-xlsx: 基于 Node.js 解析 excel 文件数据及生成 excel 文件,仅支持 xlsx 格式文件
js-xlsx: 目前 Github 上 star 数量最多的处理 Excel 的库,支持解析多种格式表格 XLSX / XLSM / XLSB / XLS / CSV,解析采用纯 js 实现,写入需要依赖 nodejs 或者 FileSaver.js 实现生成写入 Excel,可以生成子表 Excel,功能强大,但上手难度稍大。不提供基础设置 Excel 表格 api 例单元格宽度,文档有些乱,不适合快速上手;普通版本不支持定义字体、颜色、背景色等,有这个功能需要的可以使用 pro 版,是要联系客服收费的,害我照着 API 设置调试了好多次都失败。好在样式设置问题有一些教程,通过研究本人已解决,可设置宽度颜色等等,见根目录本人修改的 xlsx.js
xlsx-style 基于 xlsx 封装的样式库,可以在 xlsx 的基础上设置样式。样式不全,宽度都设置不了,好多年前作者就不维护了.宽度设置问题本人已解决了,见修改的 xlsx-style.js 文件
exceljs 在使用此库之前,本人已花费了很大的精力,用以上库做好了表格,但是发现不能设置页眉页脚,添加图片,打印选项设置等等,直到发现了这个库,文档齐全,功能强大,并且还免费.但是star较少,差一点就错过了。本教程主要针对这个库
代码库地址
https://github.com/lingxiaoyi/excel
安装
npm install
npm install -g nodemon
调试使用,替代 node 命令,实现保存文件,node 自动重新启动执行,必须全局安装才能运行
使用
nodemon app.js
js-xlsx 具体 api 使用方法请参考 main.js demo 使用,app.js 中修改为 require('./src/main.js');
exceljs 具体 api 使用方法请参考 main-exceljs.js demo 使用,app.js 中修改为 require('./src/main-exceljs.js');
因为每次生成完表格,每次都需要打开表格查看样式,在 windows 电脑中,打开表格之后就锁定不能生成新文件了,本来想着能导出一个 html 文件对应表格的样式
node 调试
vscode 中打开调试右侧设置编辑,将下方代码复制进去,点 nodemon 启动就可以进行 debug 调试了
{
"type": "node",
"request": "launch",
"name": "nodemon",
"runtimeExecutable": "nodemon",
"program": "${workspaceFolder}/app.js",
"restart": true,
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"skipFiles": ["<node_internals>/**"]
},
webpack 目录的作用
每次生成完新表格,都需要重新打开表格查看样式,在 windows 电脑中,打开表格之后就锁定了,再次生成新表格就会报错,文件已锁定,不能写入,对于想偷懒的我,能不能实现像 webpack 热更新功能那种,修改样式 js 页面自动更新呢?
wps 自带另存 html 文件功能,但是没有提供生成的 api ,网上也搜索不到对应的转换功能,
本来以为自己要实现一套表格转 html 的功能。通过不断尝试,偶然间发现手机浏览器可以直接打开预览 xlsx 文件,内心狂喜啊
使用方法
进入 webpack 目录安装依赖包,安装好之后执行
npm run dev
启动成功之后,会自动打开带有 ip 地址的预览地址,此时在电脑浏览器会自动下载 xlsx 文件,忽略不管,用手机直接打开此地址,就能看到 xlsx 表格的内容了,并且每次新修改内容和样式,都会自动刷新页面显示新表格.
小技巧
谷歌浏览器插件:
生成二维码的插件生成二维码方便手机扫描
划词翻译 用来翻译一些看不懂的英文文档
browser 目录
浏览器中实现生成 xlsx 表格方法
进入 browser 目录安装依赖包,安装好之后执行
npm run dev
启动成功之后,拖动根目录 src 下的李四表格到页面上的输入框里,成功生成表格之后会生成一个下载链接地址,右键在新标签页打开链接,即会生成一个新的表格文件出来,完整 api 使用和 demo 文件请参考 index.js
vue 和 react 用法可以参考此例子,如果有必要也可以此版本库的例子
一些概念
在使用这个库之前,先介绍库中的一些概念。
workbook 对象,指的是整份 Excel 文档。我们在使用 js-xlsx 读取 Excel 文档之后就会获得 workbook 对象。
worksheet 对象,指的是 Excel 文档中的表。我们知道一份 Excel 文档中可以包含很多张表,而每张表对应的就是 worksheet 对象。
cell 对象,指的就是 worksheet 中的单元格,一个单元格就是一个 cell 对象。
xlsx 使用注意事项
constXLSX = require('xlsx');
let html = XLSX.utils.sheet_to_html(workbook.Sheets.Sheet1)
生成 html 的用法,并且不会有任何样式
exceljs 使用注意
读取文件问题
因为 exceljs 读取文件不支持 sync 同步读取,给的实例也是 await 例子.导致我读取完遇到一个问题,就是老是生成不成功,最后发现必须要把所有逻辑全部放入函数中,像下方这样
(async function (params) {
let res = await workbook.xlsx.readFile(`${__dirname}/赵六.xlsx`);
//执行所有数据处理逻辑
//执行写的逻辑
workbook.xlsx.writeFile(path.resolve(__dirname, '../webpack/test222.xlsx'));
});
所有逻辑全部要写入这个函数中,这样本来是可以的,但是出错调试几率较大,并且读取到的数据庞大还需要额外处理,所以我读取数据逻辑就用的 node-xlsx,十分简单方便,如果你用的 exceljs 读取文件数据出现问题,大概率是异步同步逻辑搞错了,多加注意即可
宽度设置
列宽不知道是以什么为单位,反正不是像素(已测量),例子中是以厘米为单位再乘以 4.7 的结果设置的,4.7 是不断测试的结果.
快捷查看列宽的方法,打开 wps 表格,长按列与列字母间的竖线,就能看到列宽,取厘米的单位即可.见下图
前景色
前景色设置必须右键单元格选择设置单元格格式,然后选择图案样式选择颜色,就可以前景色填充
worksheet.getCell('A2').fill = { type: 'pattern', pattern:'darkTrellis', fgColor:{argb:'FFFFFF00'}, bgColor:{argb:'FF0000FF'} };
背景色
worksheet.getCell('A2').fill = { type: "pattern", pattern: "solid", fgColor: { argb: next.bgColor }, }
排版不一致的问题
解决 Mac 下编辑 Microsoft Office Word 文档与 Windows 排版不一致的问题,,不同的系统用 wps 打开相同的表格,打印预览的时候,表格宽度显示不一样
问题详细说明地址
我的解决办法就是 mac 下显示正常,按 mac 下的宽度来设置就可以了
参考资料
exceljs
node-xlsx
js-xlsx
函数节流与函数防抖是我们解决频繁触发DOM事件的两种常用解决方案,但是经常傻傻分不清楚。。。这不,在项目中又用遇到了,在此处记录一下
函数防抖 debounce
原理:将若干函数调用合成为一次,并在给定时间过去之后,或者连续事件完全触发完成之后,调用一次(仅仅只会调用一次!!!!!!!!!!)。
举个栗子:滚动scroll事件,不停滑动滚轮会连续触发多次滚动事件,从而调用绑定的回调函数,我们希望当我们停止滚动的时,才触发一次回调,这时可以使用函数防抖。
原理性代码及测试:
// 给盒子较大的height,容易看到效果
<style>
* {
padding: 0;
margin: 0;
}
.box {
width: 800px;
height: 1200px;
}
</style>
<body>
<div class="container">
<div class="box" style="background: tomato"></div>
<div class="box" style="background: skyblue"></div>
<div class="box" style="background: red"></div>
<div class="box" style="background: yellow"></div>
</div>
<script>
window.onload = function() {
const decounce = function(fn, delay) {
let timer = null
return function() {
const context = this
let args = arguments
clearTimeout(timer) // 每次调用debounce函数都会将前一次的timer清空,确保只执行一次
timer = setTimeout(() => {
fn.apply(context, args)
}, delay)
}
}
let num = 0
function scrollTap() {
num++
console.log(看看num吧 ${num}
)
}
// 此处的触发时间间隔设置的很小
document.addEventListener('scroll', decounce(scrollTap, 500))
// document.addEventListener('scroll', scrollTap)
}
</script>
</body>
此处的触发时间间隔设置的很小,如果匀速不间断的滚动,不断触发scroll事件,如果不用debounce处理,可以发现num改变了很多次,用了debounce函数防抖,num在一次上时间的滚动中只改变了一次。
调用debouce使scrollTap防抖之后的结果:
直接调用scrollTap的结果:
补充:浏览器在处理setTimeout和setInterval时,有最小时间间隔。
setTimeout的最短时间间隔是4毫秒;
setInterval的最短间隔时间是10毫秒,也就是说,小于10毫秒的时间间隔会被调整到10毫秒。
事实上,未优化时,scroll事件频繁触发的时间间隔也是这个最小时间间隔。
也就是说,当我们在debounce函数中的间隔事件设置不恰当(小于这个最小时间间隔),会使debounce无效。
函数节流 throttle
原理:当达到了一定的时间间隔就会执行一次;可以理解为是缩减执行频率
举个栗子:还是以scroll滚动事件来说吧,滚动事件是及其消耗浏览器性能的,不停触发。以我在项目中碰到的问题,移动端通过scroll实现分页,不断滚动,我们不希望不断发送请求,只有当达到某个条件,比如,距离手机窗口底部150px才发送一个请求,接下来就是展示新页面的请求,不停滚动,如此反复;这个时候就得用到函数节流。
原理性代码及实现
// 函数节流 throttle
// 方法一:定时器实现
const throttle = function(fn,delay) {
let timer = null
return function() {
const context = this
let args = arguments
if(!timer) {
timer = setTimeout(() => {
fn.apply(context,args)
clearTimeout(timer)
},delay)
}
}
}
// 方法二:时间戳
const throttle2 = function(fn, delay) {
let preTime = Date.now()
return function() {
const context = this
let args = arguments
let doTime = Date.now()
if (doTime - preTime >= delay) {
fn.apply(context, args)
preTime = Date.now()
}
}
}
需要注意的是定时器方法实现throttle方法和debounce方法的不同:
在debounce中:在执行setTimeout函数之前总会将timer用setTimeout清除,取消延迟代码块,确保只执行一次
在throttle中:只要timer存在就会执行setTimeout,在setTimeout内部每次清空这个timer,但是延迟代码块已经执行啦,确保一定频率执行一次
我们依旧可以在html页面中进行测试scroll事件,html和css代码同debounce,此处不赘述,运行结果是(可以说是一场漫长的滚轮滚动了):
最后再来瞅瞅项目中封装好的debounce和throttle函数,可以说是很优秀了,考虑的特别全面,希望自己以后封装的函数也能考虑的这么全面吧,加油!
/*
空闲控制 返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
@param {function} func 传入函数,最后一个参数是额外增加的this对象,.apply(this, args) 这种方式,this无法传递进函数
@param {number} wait 表示时间窗口的间隔
@param {boolean} immediate 设置为ture时,调用触发于开始边界而不是结束边界
@return {function} 返回客户调用函数
/
const debounce = function(func, wait, immediate) {
let timeout, args, context, timestamp, result;
const later = function() {
// 据上一次触发时间间隔
let last = Number(new Date()) - timestamp;
// 上次被包装函数被调用时间间隔last小于设定时间间隔wait
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
// 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
if (!immediate) {
result = func.call(context, ...args, context);
if (!timeout) {
context = args = null;
}
}
}
};
return function(..._args) {
context = this;
args = _args;
timestamp = Number(new Date());
const callNow = immediate && !timeout;
// 如果延时不存在,重新设定延时
if (!timeout) {
timeout = setTimeout(later, wait);
}
if (callNow) {
result = func.call(context, ...args, context);
context = args = null;
}
return result;
};
};
/*
频率控制 返回函数连续调用时,func 执行频率限定为 次 / wait
@param {function} func 传入函数
@param {number} wait 表示时间窗口的间隔
@param {object} options 如果想忽略开始边界上的调用,传入{leading: false}。
如果想忽略结尾边界上的调用,传入{trailing: false}
@return {function} 返回客户调用函数
*/
const throttle = function(func, wait, options) {
let context, args, result;
let timeout = null;
// 上次执行时间点
let previous = 0;
if (!options) options = {};
// 延迟执行函数
let later = function() {
// 若设定了开始边界不执行选项,上次执行时间始终为0
previous = options.leading === false ? 0 : Number(new Date());
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function(..._args) {
let now = Number(new Date());
// 首次执行时,如果设定了开始边界不执行选项,将上次执行时间设定为当前时间。
if (!previous && options.leading === false) previous = now;
// 延迟执行时间间隔
let remaining = wait - (now - previous);
context = this;
args = _args;
// 延迟时间间隔remaining小于等于0,表示上次执行至此所间隔时间已经超过一个时间窗口
// remaining大于时间窗口wait,表示客户端系统时间被调整过
if (remaining <= 0 || remaining > wait) {
clearTimeout(timeout);
timeout = null;
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
//如果延迟执行不存在,且没有设定结尾边界不执行选项
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
};
本文旨在通过一个简单的例子,练习vuex的几个常用方法,使初学者以最快的速度跑起来一个vue + vuex的示例。
学习vuex需要你知道vue的一些基础知识和用法。相信点开本文的同学都具备这个基础。
另外对vuex已经比较熟悉的大佬可以忽略本文。
基于vue-cli脚手架生成一个vue项目
常用npm命令:
npm i vue-vli -g vue --version vue init webpack 项目名
进入项目目录,使用npm run dev先试着跑一下。
一般不会出现问题,试跑成功后,就可以写我们的vuex程序了。
使用vuex首先得安装vuex,命令:
npm i vuex --save
介绍一下我们的超简单Demo,一个父组件,一个子组件,父组件有一个数据,子组件有一个数据,想要将这两个数据都放置到vuex的state中,然后父组件可以修改自己的和子组件的数据。子组件可以修改父组件和自己的数据。
先放效果图,初始化效果如下:
如果想通过父组件触发子组件的数据,就点“改变子组件文本”按钮,点击后效果如下:
如果想通过子组件修改父组件的数据,就在子组件点击“修改父组件文本”按钮,点击后效果如下:
首先是Parent.vue组件
<template> <div class="parent"> <h3>这里是父组件</h3> <button type="button" @click="clickHandler">修改自己文本</button> <button type="button" @click="clickHandler2">修改子组件文本</button> <div>Test: {{msg}}</div> <child></child> </div> </template> <script> import store from '../vuex' import Child from './Child.vue' export default { computed: {
msg(){ return store.state.testMsg;
}
}, methods:{
clickHandler(){
store.commit('changeTestMsg', '父组件修改自己后的文本')
},
clickHandler2(){
store.commit('changeChildText', '父组件修改子组件后的文本')
}
}, components:{ 'child': Child
},
store,
} </script> <style scoped> .parent{ background-color: #00BBFF; height: 400px;
} </style>
下面是Child.vue子组件
<template> <div class="child"> <h3>这里是子组件</h3> <div>childText: {{msg}}</div> <button type="button" @click="clickHandler">修改父组件文本</button> <button type="button" @click="clickHandler2">修改自己文本</button> </div> </template> <script> import store from '../vuex' export default { name: "Child", computed:{
msg(){ return store.state.childText;
}
}, methods: {
clickHandler(){
store.commit("changeTestMsg", "子组件修改父组件后的文本");
},
clickHandler2(){
store.commit("changeChildText", "子组件修改自己后的文本");
}
},
store
} </script> <style scoped> .child{ background-color: palegreen; border:1px solid black; height:200px; margin:10px;
} </style>
最后是vuex的配置文件
import Vue from 'vue' import Vuex from 'vuex';
Vue.use(Vuex) const state = { testMsg: '原始文本', childText:"子组件原始文本" } const mutations = {
changeTestMsg(state, str){
state.testMsg = str;
},
changeChildText(state, str){
state.childText = str;
}
} const store = new Vuex.Store({ state: state, mutations: mutations
}) export default store;
通过该vuex示例,了解vuex的常用配置及方法调用。希望对不怎么熟悉vuex的同学快速上手vuex项目有点帮助。
因为没太多东西,我自己也是刚接触,本例就不往GitHub扔了,如果尝试了本例,但是没有跑起来的同学,可以一起交流下。
无论是 pc 端还是移动端,无可避免都会涉及到列表查询有关的操作,但对于这两种不同的设备,其列表查询的最佳处理方式也是完全不同。
对于 pc 端列表查询来说,前端通常是给与服务端当前需要获取的数据量(如 pageCount,limit 等参数)以及所需要获取数据的位置(如 pageSize,offset 等参数)作为查询条件。然后服务端然后返回数据总数,以及当前数据,前端再结合这些数据显示页面总数等信息。这里我称为相对位置取数。
对于移动端而言,没有pc 端那么大的空间展示以及操作,所以基本上都会采用下拉取数这种方案。
那么我们在处理移动端列表查询时候使用这种相对位置取数会有什么问题呢?
通过相对位置取数会具有性能问题,因为一旦使用 offset 信息来获取数据,随着页数的增加,响应速度也会变的越来越慢。因为在数据库层面,我们每次所获取的数据都是“从头开始第几条”,每次我们都需要从第一条开始计算,计算后舍弃前面的数据,只取最后多条数据返回前端。
当然了,对于相对位置取数来说,数据库优化是必然的,这里我就不多做赘述了。对于前端开发来说,优秀的的查询条件设计可以在一定方面解决此问题。
事实上,对于一个实际运行的项目而言,数据更新才是常态,如果数据更新的频率很高或者你在当前页停留的时间过久的话,会导致当前获取的数据出现一定的偏差。
例如:当你在获取最开始的 20 条数据后,正准备获取紧接着的后 20 条数据时,在这段时间内 ,发生了数据增加,此时移动端列表就可能会出现重复数据。虽然这个问题在 pc 端也存在,但是 pc 端只会展示当前页的信息,这样就避免了该问题所带来的负面影响。
我们在上面的问题中说明了,移动端下拉加载中使用相对位置查询取数是有问题的。
那么,如果当前不能迅速结合前后端进行修改 api 的情况下,当服务端传递过来的数据与用户想要得的数据不一致,我们必须在前端进行处理,至少处理数据重复问题所带来的负面影响。
因为当前分页请求时无状态的。在分页取到数据之后前端可以对取得的数据进行过滤,过滤掉当前页面已经存在的 key(例如 id 等能够确定的唯一键)。
通过这种处理方式,我们至少可以保证当前用户看到的数据不会出现重复。同时当列表数据可以编辑修改的时候,也不会出现因为 key 值相同而导致数据错乱。
如果不使用相对位置获取数据,前端可以利用当前列表中的最后一条数据作为请求源参数。前端事先记录最后一条数据的信息。例如当前的排序条件为创建时间,那么记录最后一条数据的创建时间为主查询条件(如果列表对应的数据不属于个人,可能创建时间不能唯一决定当前数据位置,同时还需要添加 ID 等信息作为次要查询条件)。
当我们使用绝对位置获取数据时候,虽然我们无法提供类似于从第 1 页直接跳转 100 页的查询请求,但对于下拉加载这种类型的请求,我们不必担心性能以及数据重复显示的问题。
对于相对位置取数来说,前端可以根据返回数据的总数来判断。但当使用绝对位置取数时,即使获取数据总数,也无法判断当前查询是否存在后续数据。
从服务器端实现的角度来说,当用户想要得到 20 条数据时候,服务端如果仅仅只向数据库请求 20 条数据,是无法得知是否有后续数据的。服务端可以尝试获取当前请求的数据条数 + 1, 如向数据库请求 21 条数据,如果成功获得 21 条数据,则说明至少存在着 1 条后续数据,这时候,我们就可以返回 20 条数据以及具有后续数据的信息。但如果我们请求 21 条数据却仅仅只能获取 20 条数据(及以下),则说明没有后续数据。
如可以通过 “hasMore” 字段来表示是否能够继续下拉加载的信息。
{ data: [], hasMore: true }
事实上,前面我们已经解决了移动端处理列表查询的问题。但是我们做的还不够好,前端还需要结合排序条件来处理并提供请求参数,这个操作对于前端来说也是一种负担。那么我们就聊一下 HATEOAS 。
HATEOAS (Hypermedia As The Engine Of Application State, 超媒体即应用状态引起) 这个概念最早出现在 Roy Fielding 的论文中。REST 设计级别如下所示:
HATEOAS 会在 API 返回的数据中添加下一步要执行的行为,要获取的数据等 URI 的链接信息。客户端只要获取这些信息以及行为链接,就可以根据这些信息进行接下来的操作。
对于当前的请求来说,服务端可以直接返回下一页的信息,如
{ data: [], hasMore: true, nextPageParams: {}
}
服务端如此传递数据,前端就不需要对其进行多余的请求处理,如果当前没有修改之前的查询以及排序条件,则只需要直接返回 “nextPageParams” 作为下一页的查询条件即可。
这样做的好处不但符合 REST LEVEL 3,同时也减轻了前端的心智模型。前端无需配置下一页请求参数。只需要在最开始查询的时候提供查询条件即可。
当然,如果前端已经实现了所有排序添加以及查询条件由服务端提供,前端仅仅提供组件,那么该方案更能体现优势。 前端是不需要知道当前业务究竟需要什么查询条件,自然也不需要根据查询条件来组织下一页的条件。同时,该方案的输入和输出都由后端提供,当涉及到业务替换( 查询条件,排序条件修改)时候,前端无需任何修改便可以直接替换和使用。
一旦涉及到移动端请求,不可避免的会有网络问题,当用户在火车或者偏远地区时候,一旦下拉就会涉及取数,但是当前数据没有返回之前,用户多次下拉可能会有多次取数请求,虽然前端可以结合 key 使得渲染不出错,但是还是会在缓慢的网络下请求多次,无疑雪上加霜。这时候我们需要增加条件变量 loading。
伪代码如下所示:
// 查询 function search(cond) {
loading = true api.then(res => {
loading = false }).catch(err => {
loading = false })
} // 获取下一页数据 function queryNextPage() { if (!nextPageParams) return if (!loading) return search(nextPageParams)
}
随着项目体积的增加,参与到项目中的同学越来越多,每个人都有自己的打 git log 的习惯:
add: 添加...
[add]: 添加...
Add 添加...
为了形成统一的规范,达成共识,从而降低协作开发成本,需要对 git commit 记录进行规范。
规范 git commit 记录,需要做两件事情:
问:既然已经交互式生成了规范记录,为什么需要在 hooks 进行检查?
交互式生成 commit 记录,需要用户调用自定义的 npm scripts,例如npm run commit
。但还是可以直接调用原生 git 命令 git commit
来提交记录。而检查是在正式提交前进行的,因此不符合要求的记录不会生效,需要重新 commit。
前期调研结果,关于 commit 提示有两种做法:
方法 1 的优缺点:
优点 1: 直接安装对应的 adapter 即可
优点 2: 无开发成本
缺点 1: 无法定制,不一定满足团队需要
方法 2 的优缺点:
优点 1: 可定制,满足开发需求
优点 2: 单独成库,发布 tnpm,作为技术建设
缺点 1: 需要单独一个仓库(但开发成本不高)
在实际工作中,发现方法 1 中的常用规范,足够覆盖团队日常开发场景。所以,选择了方法 1.
step1: 安装 npm 包
npm i --save-dev commitizen cz-conventional-changelog @commitlint/cli @commitlint/config-conventional husky
添加 package.json 的配置:
"scripts": { "commit": "git-cz" }, "husky": { "hooks": { "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" }
}, "config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog" }
}
在项目根目录下创建commitlint.config.js
:
module.exports = { extends: ["@commitlint/config-conventional"]
};
使用方法:不再使用git commit -m ...
,而是调用npm run commit
。
<img src="https://tva1.sinaimg.cn/large/006tNbRwly1gbjcfr3xb5j30cw00tjrd.jpg" style="width: 100% !important;"/>
微信小程序的wxss
、阿里旗下淘宝、支付宝小程序的acss
等等语法很类似原生css,但是在web开发里用惯了动态css语言,再写回原生css很不习惯,尤其是父子样式的嵌套写法非常繁琐。
因此,我希望能有一个自动化构建方案,能够简单地将scss转换成小程序的样式语言。
以前写微信小程序的依赖库时用过,使用gulp编译,将源码和编译后的代码分别放到src和dist两个目录。gulp会处理src下面的所有文件,将其中的scss转换成css,并将其他所有文件原封不动挪到dist下相应位置。
这里就不详细说了,代码参考Wux。
非常简单直接,使用Webstorm
/IDEA
的File Watchers
功能实时转换。
确保命令行输入sass -v
能出现版本号,安装过程略。
File Watchers
到插件市场上搜索并安装(已安装则跳过)
现在安装完插件打开项目会自动弹出scss转css的向导,方便了很多。但还需要做一些修改,配置如下:
首先要将生成文件的后缀名改掉,比如这里我的淘宝小程序就得是acss
。
其次,将Arguments改为:
$FileName$:$FileNameWithoutExtension$.acss --no-cache --sourcemap=none --default-encoding utf-8 --style expanded
如果不加--no-cache
,scss文件同目录下会出现一个.sass-cache
目录。
如果不加--sourcemap=none
, scss文件同目录下会出现一个.map
文件。
如果不加--default-encoding utf-8
, scss文件如果有中文注释转换就会报错。
style
可不加,这里用的是无缩进和压缩的风格,反正小程序打包发布时还会压,这里保持可读性。
现在这个scss转换是单独作用于项目的,如果新建一个小程序项目,就需要重新添加(不建议设置成global,容易误伤)。
注意到File Watchers
列表的右侧操作栏下方有导入导出按钮,可以将现在配好的设置导出保存,将来新建项目时只要导入一下就行了。
之后还有一个问题,如果我手动将编译后的css(即wxss
或者acss
,下略)文件删除,scss文件不改动的话,就不会重新编译出css文件。
或者万一监听失效或者不够及时,css还有可能是旧的。
所以还需要一个命令,用来将整个目录下的scss文件统一转换,确保没有遗漏和保持代码。
不过我看了半天sass
和sass-convert
的文档,没有找到一个可用的写法,能让命令行遍历指定目录下的所有scss文件,将其转换成css放到源文件所在目录,并且将后缀名改为wxss
或者acss
。
所以遍历这个行为只能交给nodejs来实现,代码如下:
创建编译脚本build/scss-convert.js
:
var path = require("path") var fs = require("fs") const { exec } = require('child_process') const basePath = path.resolve(__dirname, '../') function mapDir(dir, callback, finish) {
fs.readdir(dir, function(err, files) { if (err) { console.error(err) return }
files.forEach((filename, index) => { let pathname = path.join(dir, filename)
fs.stat(pathname, (err, stats) => { // 读取文件信息 if (err) { console.log('获取文件stats失败') return } if (stats.isDirectory()) {
mapDir(pathname, callback, finish)
} else if (stats.isFile()) { if (!['.scss'].includes(path.extname(pathname))) { return }
callback(pathname)
}
}) if (index === files.length - 1) {
finish && finish()
}
})
})
}
mapDir(
basePath, function (file) { const newFileWithoutExt = path.basename(file, '.scss') if (newFileWithoutExt.startsWith('_')) { return // 按照scss规则,下划线开头的文件不会生成css } // exec可以让nodejs执行外部命令 exec(`sass --no-cache --sourcemap=none --default-encoding utf-8 --style expanded ${file}:${newFileWithoutExt}.acss`, { cwd: path.dirname(file) // 不写这个会导致生成的文件出现在根目录 }, (err, stdout, stderr) => { if (err) { console.log(err) return } console.log(`stdout: ${stdout}`)
})
}, function() { // console.log('xxx文件目录遍历完了') }
)
在package.json
里添加一条script:
"scripts": { "scss": "node build/scss-convert",
},
性能优化(网络方向)
web应用无非是两台主机之间互相传输数据包的一个过程; 如何减少传输过程的耗时就是网络方向优化的重点, 优化出发点从第一篇文章中说起
DNS解析过程的优化
当浏览器从第三方服务跨域请求资源的时候,在浏览器发起请求之前,这个第三方的跨域域名需要被解析为一个IP地址,这个过程就是DNS解析;
DNS缓存可以用来减少这个过程的耗时,DNS解析可能会增加请求的延迟,对于那些需要请求许多第三方的资源的网站而言,DNS解析的耗时延迟可能会大大降低网页加载性能。
dns-prefetch
当站点引用跨域域上的资源时,都应在<head>元素中放置dns-prefetch提示,但是要记住一些注意事项。首先,dns-prefetch仅对跨域域上的DNS查找有效,因此请避免将其用于您当前访问的站点
<link rel="dns-prefetch" href="https://fonts.googleapis.com/">
preconnect
由于dns-prefetch仅执行DNS查找,但preconnect会建立与服务器的连接。如果站点是通过HTTPS服务的,则此过程包括DNS解析,建立TCP连接以及执行TLS握手。将两者结合起来可提供机会,进一步减少跨源请求的感知延迟
<!-- 注意顺序, precontent和dns-prefetch的兼容性 -->
<link rel="preconnect" href="https://fonts.googleapis.com/" crossorigin>
<link rel="dns-prefetch" href="https://fonts.googleapis.com/">
TCP传输阶段优化
这个前端方面好像能做的有限, 我们都知道 http协议 是基于 tcp的;
升级http协议版本可以考虑下, 比如把 http/1.0 -> http/1.1 -> http/2;
这个需要我们在应用服务器上配置(nginx, Apache等), 不做概述了, 另外还需要客户端和服务器都支持哦, 目前还没开发出稳定版本,好多只支持https,不过也不远了...
http2 的优势
# 1.多路复用: 同一个tcp连接传输多个资源
这样可以突破统一域名下只允许有限个tcp同时连接,
这样http1.1所做的减少请求数优化就没有太大必要了
如多张小图合成一张大图(雪碧图),合并js和css文件
# 2.报文头压缩和二进制编码: 减少传输体积
http1 中第一次请求有完整的http报文头部,第二次请求的也是;
http2 中第一次请求有完整的http报文头部,第二次请求只会携带 path 字段;
这样就大大减少了发送的量。这个的实现要求客户端和服务同时维护一个报文头表。
# 3.Server Push
http2可以让服务先把其它很可能客户端会请求的资源(比如图片)先push发给你,
不用等到请求的时候再发送,这样可以提高页面整体的加载速度
但目前支持性不太好...emm...
总的来说, 在 c 端业务下不会太普及, 毕竟需要软件支持才行...
http 请求响应阶段优化
为了让数据包传输的更快, 我们可以从两个方面入手: 请求的数据包大小(服务器), 请求数据包的频率(客户端)
减少请求文件的大小
请求文件对应的是我们项目完成后,打包所指的静态资源文件(会被部署到服务器), 文件越小, 传输的数据包也会相对较小, 讲道理也会更快到达客户端
how to reduce a package size?
目前我们都会使用打包工具了(比如webpack, rollup, glup 等), 如何使用工具来减小包的体积呢? 这边建议您去官网文档呢...当然这里列举一下常用的手段(webpack 的), 但是注意要插件版本更新哦
JS文件压缩
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
plugins: [
new UglifyJsPlugin({
// 允许并发
parallel: true,
// 开启缓存
cache: true,
compress: {
// 删除所有的console语句
drop_console: true,
// 把使用多次的静态值自动定义为变量
reduce_vars: true,
},
output: {
// 不保留注释
comment: false,
// 使输出的代码尽可能紧凑
beautify: false
}
})
]
}
CSS 文件压缩
// optimize-css-assets-webpack-plugin
plugins: [
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano'),
}),
];
html 文件压缩
// html-webpack-plugin
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, 'src/index.html'),
filename: 'index.html',
chunks: ['index'],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false,
},
}),
];
source map 文件关闭
tree shaking
1.代码不会被执行,不可到达,比如 if(false){// 这里边的代码}
2.代码执行的结果不会被用到
3.代码只会影响死变量(只写不读)
4.方法不能有副作用
// 原理相关: 以后在研究
利用 ES6 模块的特点:
只能作为模块顶层的语句出现
import 的模块名只能是字符串常量
import binding 是 immutable 的
代码擦除: uglify 阶段删除无用代码
scope hoisting(作用域提升)
分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
module.exports = {
resolve: {
// 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件
mainFields: ['jsnext:main', 'browser', 'main']
},
plugins: [
// 开启 Scope Hoisting
new ModuleConcatenationPlugin(),
],
};
项目中使用按需加载,懒加载(路由,组件级)
const router = new VueRouter({
routes: [
{ path: '/foo', component: () => import(/* webpackChunkName: "foo" */ './Foo.vue') }
{ path: '/bar', component: () => import(/* webpackChunkName: "bar" */ './Bar.vue') }
]
})
开启 gizp 压缩
有时候启用也会消耗服务器性能, 看情况使用吧
暂时先提这么些吧...后续想到了再加
减少请求频率
因为同一域名下 tcp 连接数的限制导致过多的请求会排队阻塞, 所以我们需要尽量控制请求的数量和频率
常见措施
将静态资源的内联到HTML中
这样这些资源无需从服务器获取, 但可能影响到渲染进程...
<!-- 1.小图片内联 base64 (url-loader) -->
<!-- 2.css内联 -->
<!-- 3.js内联 -->
<script>
${require('raw-loader!babel-loader!./node_modules/lib-flexible/flexible.js')}
</script>
利用各级缓存(下一篇存储方面介绍)
通常都是在服务端做相关配置, 但你要知道
我们可以利用http缓存(浏览器端)来减少和拦截二次请求, 当然一般都是在服务端设置的;
服务器端也可以设置缓存(redis等), 减少数据查询的时间同样可以缩短整个请求时间
利用本地存储
我们可以将常用不变的信息存在本地(cookie,storage API 等);
判断存在就不去请求相关的接口, 或者定期去请求也是可以的
花钱买 CDN 加速
CDN 又叫内容分发网络,通过把资源部署到世界各地,用户在访问时按照就近原则从离用户最近的服务器获取资源,从而加速资源的获取速度。 CDN 其实是通过优化物理链路层传输过程中的网速有限、丢包等问题来提升网速的...
购买 cdn 服务器;
然后把网页的静态资源上传到 CDN 服务上去,
在请求这些静态资源的时候需要通过 CDN 服务提供的 URL 地址去访问;
# 注意, cdn 缓存导致的新版本发布后不生效的问题
所以打包的时候常在文件后面加上 hash 值
然后在 HTML 文件中的资源引入地址也需要换成 CDN 服务提供的地址
/alicdn/xx12dsa311.js
# 利用不同域名的 cdn 去存放资源, (tcp连接限制)
webpack 构建时添加 cdn
// 静态资源的导入 URL 需要变成指向 CDN 服务的绝对路径的 URL 而不是相对于 HTML 文件的 URL。
// 静态资源的文件名称需要带上有文件内容算出来的 Hash 值,以防止被缓存。
// 不同类型的资源放到不同域名的 CDN 服务上去,以防止资源的并行加载被阻塞。
module.exports = {
// 省略 entry 配置...
output: {
// 给输出的 JavaScript 文件名称加上 Hash 值
filename: '[name]_[chunkhash:8].js',
path: path.resolve(__dirname, './dist'),
// 指定存放 JavaScript 文件的 CDN 目录 URL
publicPath: '//js.cdn.com/id/',
},
module: {
rules: [
{
// 增加对 CSS 文件的支持
test: /\.css$/,
// 提取出 Chunk 中的 CSS 代码到单独的文件中
use: ExtractTextPlugin.extract({
// 压缩 CSS 代码
use: ['css-loader?minimize'],
// 指定存放 CSS 中导入的资源(例如图片)的 CDN 目录 URL
publicPath: '//img.cdn.com/id/'
}),
},
{
// 增加对 PNG 文件的支持
test: /\.png$/,
// 给输出的 PNG 文件名称加上 Hash 值
use: ['file-loader?name=[name]_[hash:8].[ext]'],
},
// 省略其它 Loader 配置...
]
},
plugins: [
// 使用 WebPlugin 自动生成 HTML
new WebPlugin({
// HTML 模版文件所在的文件路径
template: './template.html',
// 输出的 HTML 的文件名称
filename: 'index.html',
// 指定存放 CSS 文件的 CDN 目录 URL
stylePublicPath: '//css.cdn.com/id/',
}),
new ExtractTextPlugin({
// 给输出的 CSS 文件名称加上 Hash 值
filename: `[name]_[contenthash:8].css`,
}),
// 省略代码压缩插件配置...
],
};
/*
以上代码中最核心的部分是通过 publicPath 参数设置存放静态资源的 CDN 目录 URL,
为了让不同类型的资源输出到不同的 CDN,需要分别在:
output.publicPath 中设置 JavaScript 的地址。
css-loader.publicPath 中设置被 CSS 导入的资源的的地址。
WebPlugin.stylePublicPath 中设置 CSS 文件的地址。
设置好 publicPath 后,WebPlugin 在生成 HTML 文件和 css-loader 转换 CSS 代码时,会考虑到配置中的 publicPath,用对应的线上地址替换原来的相对地址。
*/
参考
DNS MDN]
webpack 文档
深入浅出 Webpack
Scope Hoisting
IOS下的webview页面,内嵌iframe元素,将其样式指定为宽高100%:
.iframe { width: 100%; height: 100%;
}
在安卓下运行均无问题,但是在IOS下会出现异常。
具体表现为iframe页面内的子元素一旦超出原先的边界,只要能影响到html元素的宽高,就会自动撑开iframe,即使html元素设置了overflow:hidden
也没用。
比如一个body元素下的弹层需要从下往上滑动进场,这个弹层的位置就会导致html高度的变化,因此页面底部的tabbar就会在弹层运动期间先消失再出现。
解决方法就是使用具体的宽高数值锁定iframe元素:
function onLoadIFrame (index) { // 修复IOS下轮播图初始化瞬间会让iframe宽度自行扩大问题 if (this.ENV.isIOS) { const iframe = this.$el.querySelector('#iframe' + index)
iframe.style.width = iframe.clientWidth + 'px' iframe.style.height = iframe.clientHeight + 'px' }
}
蓝蓝设计的小编 http://www.lanlanwork.com