moment.js
问题1:切换国家地区
切换国际环境,需要引入对应的国家文件。它默认自带英文环境。
javascript
import moment from 'moment'
import 'moment/locale/zh-cn.js'
moment.locale('zh-cn')
音视频播放
问题1:拉流(音视频的容器格式)
android支持: flv
ios支持:m3u8
问题2:视频播放问题
1、无交互的视频自动播放:需要静音。
2、有交互的但是延时播放的策略:
背景:
为了视频达到秒开又不浪费流量,开发会在一个图层里面同时渲染三个直播间(每个直播间带有一个视频播放器),分别为上、中、下直播间,中间的直播间是正常播放的,上和下直播间是暂停的。当用户切换直播间的时候,下一个直播间播放视频,如果是无声音则是能够自动播放的,但是有声音的播放在ios上则不行。通过测试可以知道通过交互,在移动端ios上video.play()方法需要在交互事件的0.9S内触发才行,android则可以更长。
解决方案:
通过调研与测试,发现:当通过交互让一个有声音的视频播放后再暂停,后续就可以通过异步的方法成功触发video.play()。参考资料
3、组件重新激活 或者 熄灭屏幕重新唤起 视频都会自动暂停,需要调用video.play()方法让视频继续播放。
sdk接入遇到的问题
火山引擎
问题1:拉流失败没有重新拉取
视频拉流过程中,会出现卡住不再播放问题,需要根据sdk提供的错误码,监听当错误出现时,重新调用sdk方法强制去拉流解决这个问题。
webview问题
H5在webview打开后遇到的一系列问题。
问题1:权限
h5在webview上能请求的权限就两个,相机与麦克风。mdn介绍
js
// js请求 权限的代码, 权限只能在localhost 或者 域名上才生效,
// ip地址访问会报错
navigator.mediaDevices
.getUserMedia({ audio: true, video: true })
.then(function(stream) {
/* 使用这个 stream stream */
})
.catch(function(err) {
console.error(err)
})
浏览器上的权限获取流程:h5 -> 浏览器 -> 系统 -> 用户
webview上的权限获取流程:h5 -> webview -> native(app) -> 系统 -> 用户
需要注意的地方则是
:h5在webview上获取权限是需要native的研发同学支持的,native的研发同学通过监听拦截到h5的权限请求,再进行赋予操作
,否则h5是无法调用权限相关的方法的。
还有一个地方:h5在webview android上是不能一次性请求相机与麦克风权限的
,现阶段在webview_flutter android上一次性获取,它那边只能监听拦截到麦克风权限。故此 js 需要分两次去获取,一次视频、一次音频。
问题2:组件input.type='file'
html
<input type="file" accept="*">
这个控件在浏览器上点击的时候会自动获取照相机和文件读取权限,不需要用户授权,但是在webview上是需要向系统获取照相机权限的,否则只能进行文件读取操作,故此解决方案则是,H5判断环境,如果是webview打开的H5,在触发这个控件前,需要触发照相机权限。
js
navigator.mediaDevices
.getUserMedia({ audio: false, video: true })
.then(function(stream) {
/* 使用这个 stream stream */
})
.catch(function(err) {
console.error(err)
})
问题3: google登录问题
webview上不能使用google登录,只能通过桥接方法,让app进行google登录。
优雅的桥接方式
传统的桥接方式:
css
1、h5通过调native注册的方法把数据传递给native
2、native通过调h5在window上注册的方法把数据传给h5
新的桥接方式:
java
native只注册一个controler,
h5通过postMessage的方式告诉native事件名称、传递的数据与回调函数(需要执行的JS代码),
native通过回调函数把数据传给h5。
将h5请求native端的数据以类似发送请求的形式获取
。
大概思路:
- native与h5约定以postMessage方式桥接,方式名为flutter_methods
- 约定传递的JSON数据格式,并要求native端如果有数据需要回传则通过callback回传。
json
{
"event_name": "事件名称",
"data": "事件所传递的数据",
"callback": "为native需要执行的js代码,通过它将native数据传递给h5"
}
- 将桥接方式进行封装并命名为flutter_methods,并返回一个promise,pormise包含了桥接后的数据。
js
export const flutter_methods = async ({
event_name,
data,
}, isNeedDataFromNative) => {
return await new Promise((resolve, reject) => {
if (window?.flutter_methods) {
window?.flutter_methods.postMessage(JSON.stringify({
event_name,
data,
callback: isNeedDataFromNative ? registerCallback(resolve) : ''
}))
if (!isNeedDataFromNative) {
resolve('')
}
} else {
reject(new Error('get data from native error!'))
}
})
}
- 声明一个函数方法registerCallback(接受一个reslove函数),该函数方法会在window.knCallbacks[callbackId]上声明一个函数方法,该函数方法会接收native传递过来的数据,再通过执行reslove函数将数据传递给封装的桥接方法flutter_methods。
js
function makeRandomId(func) {
return `${func.name || 'anonymous'}_${Date.now()}`;
}
export const registerCallback = (resolve, keepAlive) => {
if (!callback) {
return null;
}
const callbackId = makeRandomId(callback);
window.knCallbacks[callbackId] = (dataFromNative) => {
let result;
if (typeof dataFromNative === 'object') {
result = dataFromNative;
} else {
try {
result = JSON.parse(dataFromNative);
} catch (e) {
result = dataFromNative;
}
}
callback(result);
if (!keepAlive) {
delete window.knCallbacks[callbackId];
}
};
return `knCallbacks.${callbackId}`;
}
总代码
js
// 封装
function makeRandomId(func) {
return `${func.name || 'anonymous'}_${Date.now()}`;
}
export const registerCallback = (resolve, keepAlive) => {
if (!callback) {
return null;
}
const callbackId = makeRandomId(callback);
window.knCallbacks[callbackId] = (dataFromNative) => {
let result;
if (typeof dataFromNative === 'object') {
result = dataFromNative;
} else {
try {
result = JSON.parse(dataFromNative);
} catch (e) {
result = dataFromNative;
}
}
resolve(result);
if (!keepAlive) {
delete window.knCallbacks[callbackId];
}
};
return `knCallbacks.${callbackId}`;
}
export const flutter_methods = async ({
event_name,
data,
}, isNeedDataFromNative) => {
return await new Promise((resolve, reject) => {
if (window?.flutter_methods) {
window?.flutter_methods.postMessage(JSON.stringify({
event_name,
data,
callback: isNeedDataFromNative ? registerCallback(resolve) : ''
}))
if (!isNeedDataFromNative) {
resolve('')
}
} else {
reject(new Error('get data from native error!'))
}
})
}
// 业务执行
flutter_methods({
event_name: 'googleLogin',
}, true).then((data: FlutterGoogleMsg) => {
console.log(data)
if (data.success !== '1') {
return
}
handleLoginByGoogle(data.idToken)
}).catch((err) => {
console.error(err)
}).finally(() => {
dispatch(setIsShowLoginLoading(false))
})
白屏问题
背景:webview打开H5的时候,会有一段时间的白屏。
解决方案:
- 各种页面与组件的懒加载、预加载
- 图片cdn缓存
- 骨架屏(loading图)
其中最明显的优化效果就是骨架屏了,在index.html上写入一个loading的div元素(注意不要写在id=root的div上),然后在框架的根组件上监听生命周期,待监听到根组件渲染完成后,隐藏这个loading元素。
google上架问题
挂羊头,卖狗肉。狗肉则是H5。
实现的逻辑是:先上一版正常的APP(卖的是羊头),后上一版非正常的APP(卖狗肉)。后一版APP打开的时候会弹出一个webview,这个webview的url是服务返回的。
app发请求的时候会将广告渠道的标识
通过两次BASE64加密后传输。如果是非广告进来(google审核人员)所下载的APP,则服务会根据标识返回隐私页的HTML内容。如果是广告渠道(目标用户)所下载的APP,服务则会返回302状态码,并将实际H5内容放在header里面,让浏览器重定向,从而实现规避google审查。
注意核心点:
- app的测试不能有google服务。
- app不能用if else 等逻辑判断。
- app的桥接使用flutter的插件库实现。
- app的google登录使用flutter的插件库实现。
- h5所使用到的权限,APP都需要有对应的功能。
- app用flutter开发是因为google的审核对flutter没有那么智能,不会因为调用栈问题直接锁死,导致代码也上不了。
内存泄漏问题
根据浏览器的垃圾回收原理:当作用域消失后,其作用域所使用的内存将会被回收。
起因是在flutter里面,webview所打开的h5在观看视频的过程中,超过了十几分钟就崩溃了。一开始是想用传统的方法用google浏览器提供的 google浏览器-更多工具-开发者工具-内存
进行记录的,但是它录制不了很长的时间,超过五分钟就不行,如果用五分钟去录制又判断不出究竟有没有内存泄漏。后面换了一种方法 google浏览器-更多工具-任务管理器
看了十几分钟,发现在直播间里面,它的内存占用空间与JS使用的内存会一直上升,从而确定自己的H5代码出现内存泄漏了。(注意:用这个看的时候最好不要打开google的开发者工具,它会导致任务管理器提供的信息很杂
)
通过blob进行图片上传
通过裁剪工具裁剪完成后获取到的blob是一个地址,则时候如果要获取blob所对应的file,需要走本地接口
js
// 举例:blob:http://192.168.28.38:8000/202f8d0a-af47-41de-adce-36e2b878b059
export const getFileFromBlob = (
file,
type = 'image/jpeg',
quality = 0.5,
) => {
const fileName = moment().unix() + '.jpeg'
return new Promise((resolve, reject) => {
fetch(file)
.then(response => response.blob())
.then(blobData => {
// 创建一个 File 对象
const file = new File([blobData], fileName, { type });
// 现在,'file' 就是表示文件的 File 对象
resolve(file);
})
.catch(error => reject(error));
})
}
// 这时候获取到file 上传文件的话,需要上传二进制数据,这时候file转成二进制数据再上传接口
const fileReader = new FileReader()
fileReader.readAsArrayBuffer(currentFile)
fileReader.onloadend = async function () {
const binaryData = fileReader.result
setIsLoading(true)
try {
let { data } = await Apis.baseUploadImage(binaryData)
dispatch(
updateUserInfo({
avatar: data?.url,
}),
)
setData(val => ({
...val,
avatar: data?.url,
}))
} catch (error) {
console.log('error', error)
}
setIsLoading(false)
}
android设备上进行swiper切换问题
背景:swiper中的swiper-slide滑动切换的时候,如果滑动的区域刚好有滚动元素,则会出现滑动异常问题,如下:
解决方法: 给滚动元素绑定touchuStart、touchMove、touchEnd事件,touchuStart时禁止swiper滚动,并记录滑动的x位置,touchMove时获取此时的x与记录的x之间的差值,如大于一定数值,则调用方法滚动swiper-slide。