Open xccjk opened 2 years ago
移动端不兼容 移动端不兼容 移动端不兼容
老版本Google浏览器可以a标签直接下载,新版本会打开图片,只有图片同源才会直接下载 老版本Google浏览器可以a标签直接下载,新版本会打开图片,只有图片同源才会直接下载 老版本Google浏览器可以a标签直接下载,新版本会打开图片,只有图片同源才会直接下载
现在新版的浏览器,点击a标签进行下载图片时,往往不会按照预期实现图片的下载功能,而是会在新的窗口打开图片,代码如下:
<a href="https://static001.geekbang.org/resource/image/fa/af/face2257c62b291620a1750b4cdaf4af.jpg?x-oss-process=image/resize,m_fill,h_400,w_818" download>下载</a>
要想实现点击按钮下载图片,常见的做法是后端配置请求头来实现资源的下载,但是现在一般都前后端分离了,并且很多图片在oss上,不能直接修改通用的请求配置
当请求的图片是同源的时,可以直接通过a标签实现下载
// 网址
https://www.xxx.com
// 图片地址
https://www.xxx.com/image/1.png
当图片是cdn地址时,配置图片的请求头为当前域名也可以
// 极客时间地址
https://time.geekbang.org/
// 极客时间图片地址
https://static001.geekbang.org/resource/image/ab/9f/ab17ecf8fdb768db1362ec72c2f8ce9f.jpg
// 图片请求头配置
referer: https://time.geekbang.org/
当图片不再跨域时,可以使用的方法就比较多了:
通过blob流来实现下载:
const imgDownload = (src, name) => {
const canvas = document.createElement('canvas');
const img = document.createElement('img');
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
const context = canvas.getContext('2d');
context.drawImage(img, 0, 0, img.width, img.height);
canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height);
canvas.toBlob(
(blob) => {
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = name || 'download'; // resource name
link.click();
},
'0.95',
);
};
img.setAttribute('crossOrigin', 'Anonymous');
img.src = src;
}
imgDownload(url, 1.png);
通过FileSaver.js实现下载
import { saveAs } from 'file-saver';
saveAs(url, 1.png)
import JSZip from 'jszip';
import { saveAs } from 'file-saver';
const imgBlob = (src) => {
const canvas = document.createElement('canvas');
const img = document.createElement('img');
img.setAttribute('crossOrigin', 'Anonymous');
img.src = src;
return new Promise((resolve) => {
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
const context = canvas.getContext('2d');
context.drawImage(img, 0, 0, img.width, img.height);
canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height);
canvas.toBlob(
(blob) => {
resolve(blob);
},
'0.95',
);
};
});
};
const downloadImgs = async (urls, name) => {
const zip = new JSZip();
const imgs = zip.folder('images');
const blobs = urls.map(async (url) => {
const blob = await imgBlob(url);
return blob;
});
blobs.forEach((blob, index) => {
imgs.file(`${index}.png`, blob);
});
zip.generateAsync({ type: 'blob' }).then((content) => {
saveAs(content, `${name || 'example'.zip}`);
});
};
调整像素比参数devicePixelRatio,devicePixelRatio值越大清晰度越高
const chartDom = document.getElementById('main');
const myChart = echarts.init(chartDom, null, { devicePixelRatio: 15 });
调整生成图片类型为SVG,这样在缩放时就不会失真了
const chartDom = document.getElementById('main');
const myChart = echarts.init(chartDom, null, { renderer: 'svg' });
const arr = [1, [2, 3], [4, 5, [6]], [7, 8, [9, [10]]]];
let i = 0;
function fn5(array, index = Infinity) {
let result1 = [];
if (index === Infinity) {
array.forEach((item) => {
if (Array.isArray(item)) {
result1 = [...result1, ...item]
} else {
result1.push(item)
}
})
if (!array.some(item => Array.isArray(item))) return array;
return fn5(result1, Infinity);
} else {
index = Number(index)
if (!index) return array;
if (i <= index && !array.some(item => Array.isArray(item))) return array;
array.forEach((item) => {
if (Array.isArray(item)) {
result1 = [...result1, ...item]
} else {
result1.push(item)
}
})
i++;
if (i === index) return result1;
return fn5(result1, index);
}
}
console.log(fn5(arr, true))
// 获取指定类型所有节点
const nodes = document.querySelectorAll('audio[preload]');
// NodeList转数组
const arr = [].slice.apply(nodes);
// 移除节点
arr.forEach(node => node?.remove());
<div id="app"></div>
document.getElementById('app').innerHTML = '<h1>1024</h1>'
<div
dangerouslySetInnerHTML={{
__html: '<h1>1024</h1>'
/>
dome: codesandbox
export const hasUsableSWF = () => {
let swf;
if (typeof window?.ActiveXObject !== 'undefined') {
swf = new ActiveXObject('ShockwaveFlash.ShockwaveFlash');
} else {
swf = navigator.plugins['Shockwave Flash'];
}
return !!swf;
};
例:2000秒转为时间格式为33:20
function formateTime(time) {
const h = parseInt(time / 3600, 10);
const minute = parseInt((time / 60) % 60, 10);
const second = Math.ceil(time % 60);
const hours = h < 10 ? '0' + h : h;
const formatSecond = second > 59 ? 59 : second;
return `${hours > 0 ? `${hours}:` : ''}${minute < 10 ? '0' + minute : minute}:${
formatSecond < 10 ? '0' + formatSecond : formatSecond
}`;
}
formateTime(2000) // '33:20'
export const isWeixin = () => {
return /MicroMessenger|WeXin|WeChat/g.test(navigator.userAgent);
};
export const clearCookie = () => {
const keys = document.cookie.match(/[^ =;]+(?=\=)/g);
if (keys) {
for (let i = keys.length; i--; ) document.cookie = keys[i] + '=0;expires=' + new Date(0).toUTCString();
}
};
export const preload = (arr) => {
for (let i = 0; i < arr.length; i++) {
const img = new Image();
img.src = arr[i];
}
};
useEffect(() => {
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
}, []);
通过添加meta标签来控制页面缩放
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
// bad
function filter(type) {
if (type === 1 || type === 2 || type === 3 || type === 4 || ...) {
console.log('条件成立了...')
}
}
// good
const types = [1, 2, 3, 4, ...]
function filter(type) {
if (types.includes(type)) {
console.log('条件成立了...')
}
}
const data = [
{ name: 'a', age: 20 },
{ name: 'b', age: 28 },
{ name: 'c', age: 18 }
]
// bad
function filter(n) {
let isAll = true
data.forEach(({ age }) => {
if (age > n) {
isAll = false
}
})
return isAll
}
// bad
function filter(n) {
const o = data.find(x => x.age > n)
return o ? true : false
}
// good
function filter(n) {
return data.every(x => x.age > n)
}
const data = [
{ name: 'a', age: 20 },
{ name: 'b', age: 28 },
{ name: 'c', age: 18 }
]
// bad
let isAge = false
data.forEach(({ age }) => {
if (age > 25) {
isAge = true
}
})
// good
const isAge = data.some(({ age }) => age > 25)
const data = [
{ name: 'a', age: 20 },
{ name: 'b', age: 28 },
{ name: 'c', age: 18 }
]
// bad
const { name, age } = data.filter(({ age }) => age > 25)[0] || {}
// good
const { name, age } = data.find(({ age }) => age > 25) || {}
// bad
function f(m) {
return m || 1
}
// good
function f(m = 1) {
return m
}
const obj = { a: { b: { c: 1 } } }
// bad
const { a = {} } = obj
const { b = {} } = a
const { c } = b
// good
// 需要配置babel
const m = a?.b?.c
// bad
<div>
{
(this.props.data || []).map(li => <span>{li}</span>)
}
</div>
// good
<div>
{
this.props?.data?.map(li => <span>{li}</span>)
}
</div>
// bad
function pick(type) {
if (type === 1) {
return [0, 1]
} else if (type === 2) {
return [0, 1, 2]
} else if (type === 3) {
return [0, 3]
} else {
return []
}
}
// bad
function pick(type) {
switch (type) {
case 1:
return [0, 1]
case 2:
return [0, 1, 2]
case 3:
return [0, 3]
default:
return []
}
}
// good
// 枚举法
const fn = {
1: [0, 1],
2: [0, 1, 2],
3: [0, 3]
}
const filter = fn[type] ?? []
// good
// map数据结构
const fn = new Map()
.set('1', [0, 1])
.set('2', [0, 1, 2])
.set('3', [0, 3])
const filter = fn[type] ?? []
// bad
return (
<>
{
isCheck ? (
<div>check all</div>
) : null
}
</div>
)
// good
if (!isCheck) return null
return <div>check all</div>
// bad
function sum(num) {
return +num + 1
}
// bad
function sum(num) {
return Number(num) + 1
}
sum('1.1') // 2.1
sum('1.1abc') // NaN
// good
function sum(num) {
return ~~num + 1
}
sum('1.1') // 2.1
sum('1.1abc') // 1
// bad
function async fetchData() {
const res = await getData({})
const { success, response = [] } = res || {}
if (success) {
setData(response)
}
}
// good
function async fetchData() {
try {
const res = await getData({}) ?? []
const { success, response = [] } = res
if (success) {
setData(response)
}
} catch (err) {
console.log('error', const res = await getData({}))
}
}
在设置页面禁止复制文本是,设置了-webkit-user-select: none导致iOS手机上输入框类失效
// index.html
* {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
// 排除input与textarea
[contenteditable="true"], input, textarea {
-webkit-user-select: auto!important;
-khtml-user-select: auto!important;
-moz-user-select: auto!important;
-ms-user-select: auto!important;
-o-user-select: auto!important;
user-select: auto!important;
}
iPhoneX底部样式兼容,包括页面主体内容的在安全区域及fix定位下bottom: 0的兼容。参考:iPhone X兼容
h5中拨号,采用window.open('tel:15000000000'),在iOS中不能使用。原因:iOS中不兼容window.open方法,通过window.location.href = 'tel:15000000000'来实现拨号操作
const isIos = function() {
const isIphone = navigator.userAgent.includes('iPhone')
const isIpad = navigator.userAgent.includes('iPad')
return isIphone || isIpad
}
if (isIos) { window.location.href = 'tel:15000000000' } else { window.open('tel:15000000000') }
4. 键盘遮挡输入框,onBlur方法监听处理
<input id='phone' type='tel' maxLength='11' placeholder='请输入' onChange={({ target: { value } }) => handlePhone(value)} onBlur={() => inputOnBlur()} />
const check = () => { setTimeout(() => { const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop || 0 window.scrollTo(0, Math.max(scrollHeight - 1, 0)) }, 100) }
const inputOnBlur = () => { document.getElementById('phone').setAttribute('onblur', check())
setTimeout(() => { window.scrollTo(0, document.body.scrollTop + 1) document.body.scrollTop >= 1 && window.scrollTo(0, document.body.scrollTop - 1) }, 100) }
5. 在react中使用类keep-alive组件在pc/h5中的使用 - react-live-route
- 场景:长列表中滚动到很多页后查看了某条数据,进入列表详情,返回到列表页是,会回到顶部
- 原因:react中进行路由跳转时,state等数据会丢失,也不回记录滚动等信息
- 解决方式:
- 采用redux等数据流工具,在列表页面跳转的时候在componentWillUnmount生命周期记录state中的数据与滚动位置信息,在componentDidMount生命周期对数据进行恢复、
- 在路由跳转的时候隐藏列表页,在回到列表页的时候再重新渲染出来
- 遇到的问题
- Switch渲染的是匹配到的第一个路由,而LiveRoute是为了让组件强制渲染不匹配的路由
- Switch与LiveRoute应包裹在同一个div中,不然会报错A <Router> may have only one child element
- 采用position: absolute进行定位的元素会有影响
- routes中的路由应该与keepRouter中的路由不重复,重复的情况下会在同一个路由下渲染页面两次
- 参考链接
- https://github.com/facebook/react/issues/12039
- https://github.com/fi3ework/react-live-route
- https://codesandbox.io/s/yj9j33pw4j?file=/src/index.js:399-409
- https://zhuanlan.zhihu.com/p/59637392
npm install react-live-route --save
// routes.js
export const routes = [
{
path: '/',
exact: true,
component: () =>
)
} ] // 需要处理的路由列表 export const keepRouter = [ { path: '/list', component: DirectListPage }, { path: '/demand/list', component: DemandListPage } ]
// APP.js import NotLiveRoute from 'react-live-route' import { routes, keepRouter } from './pages/routes'
const LiveRoute = withRouter(NotLiveRoute)
6. 微信H5重复授权
- 问题描述
- android手机上,主要在低版本的android机上,客户打开微信H5,页面出现多次弹出授权窗口,需要点击多次确认才会消失
- 问题原因
- 多次重定向导致出现多次授权窗口
- hash模式路由导致的参数丢失
- 授权链接中参数不完整
// 最终的重定向方法
// 去除重定向地址中的#,同时添加参数connect_redirect
redirectWXAuth = () => {
const { goToPage } = this.state
const url = (goToPage + '').replace('#', '')
const redirectUrl = encodeURIComponent(
${process.env.REDIRECT_HOST}/login?goto_page=${encodeURIComponent(url)}&bindCode=1
)
const wechatAuthUrl = https://open.weixin.qq.com/connect/oauth2/authorize?appid=${process.env.WXAPPID}&redirect_uri=${redirectUrl}&response_type=code&scope=snsapi_userinfo&state=STATE&connect_redirect=1#wechat_redirect
window.location.replace(wechatAuthUrl)
}
const qqShare = ({ url = window.location.href, desc, title, summary, pics }) => {
const urlPath = `https://connect.qq.com/widget/shareqq/index.html?url=${encodeURI(
url
)}&desc=${desc}&title=${title}&summary=${summary}&pics=${pics}`;
window.open(urlPath);
};
const wbShare = ({ url = window.location.href, desc, title, summary, pics }) => {
const urlPath = `http://service.weibo.com/share/share.php?url=${encodeURI(
url
)}&desc=${desc}&title=${title}&summary=${summary}&pics=${pics}`;
window.open(urlPath);
};
video播放相关
video播放结束事件ended监听不生效
当video设置了循环播放属性loop时,监听ended事件不会生效
上面的代码在视频播放结束后,控制台不会打印播放结束
要想在视频设置了循环播放,同时获取是否播放结束,可以结合timeupdate与loadedmetadata来实现