前端
web有哪些新的api?

透过那些web较新的api,我们来看一下当前的web能做什么?

showDirectoryPicker读取目录

印象中,一直以来web对file文件的操作都是割裂的,或者说权限不足的

顶多也只input:type:file实现读,a标签download实现写

但现在已经有 开源作品 对其直接的应用:分别对目录读,文件读,文件写:

mind导图站

具体来说,js有了新api:【showDirectoryPicker,showOpenFilePicker】

// 方式一:问要【文件夹】权限
const askDictAuth = async () => {
    // 问要权限
    // 获取文件
    if (!window.showDirectoryPicker) {
        return alert('浏览器不支持')
    }
    const d = await window.showDirectoryPicker()
    dirsName.value = d.name
    // 同意过一次就不问了
    for await (const [key, value] of d.entries()) {
        dirs.value.push({ d: value, name: key, kind: value.kind, next: [] })
    }
}


// 方式二:问要【文件】权限 
const askFileAuth = async () => {
    // 获取文件
    const [fileHandle] = await window.showOpenFilePicker({
        multiple: false,
        // excludeAcceptAllOption: true,
        types: [
            {
                description: '',
                accept: {
                    'application/json': [type.value]
                }
            }
        ],
    })
    // 获取文件信息
    toFile(fileHandle)
}
const toFile = async (d) => {
    const f = await d.getFile()
    const fileread = new FileReader()
    fileread.onload = () => {
        content.value = fileread.result
    }
    fileread.readAsText(f)
    file.value = d
}

showSaveFilePicker写入文件

定好文件名称和内容,可以直接新建

const toNewFile = async () => {
    const _fileHandle = await window.showSaveFilePicker({
        suggestedName: 'newfile.txt',
        types: [
            {
                description: 'Text file',
                accept: {
                    'text/plain': ['.txt'],
                },
            },
        ],
    })
    if (!_fileHandle) {
        return
    }
    const writable = await _fileHandle.createWritable()
    await writable.write('test')
    await writable.close()
    toFile(_fileHandle)
}

demo-showDirectoryPicker

小结: 主要用到这三个api 也畅想一下是否未来可以做更多的本地工具比如游戏配置编排等等

照相机和麦克风打开

getUserMedia是新的api,导航->媒体设备->用户媒体:取音频,视频

可以询问用户是否允许访问摄像头和麦克风,通过后拿到steam流

navigator.mediaDevices.getUserMedia({ audio: true, video: true })
    .then(function (stream) {
        // 处理stream
        videoSrc.value = stream
    })
    .catch(function (err) {
        // 处理error
    });

不过这个和http的文件流的流有本质区别,一个是静态的,一个是动态不断变化的

流可以直接播放,也可以用webRTC传播,用MediaRecorder记录存储,编辑等等。

<video :srcObject="videoSrc" width="320" height="240" controls autoplay></video>

还可以分别拿到视频或者音频的音轨,用于暂停

const close = () => {
    videoSrc.value.getTracks().forEach(track => track.stop())

    // mdum.close() // 不好使
    // videoSrc.value = '' // 不好使
}

demo:mediaDevices-getUserMedia

录屏

录屏的流程和音视频很像,可以理解成流的输入不再取麦克风而是取自屏幕设备

const stream = await navigator.mediaDevices.getDisplayMedia({
    video: true,
    audio: true
});
videoSrcObject.value = stream

getDisplayMedia开启 拿到流的视频轨道后,可以监听ended,点击页面的【停止共享】就会触发

const [video] = stream.getVideoTracks();
video.addEventListener("ended", () => {
    console.log('video stop')
    // recoder.stop(); // 停止记录
});

这里我还开启了MediaRecorder来接收流也即屏幕画面,再监听【dataavailable】,停止后就能直接接收到录屏画面的blob文件

const recoder = new MediaRecorder(stream);
recoder.start();
recoder.addEventListener("dataavailable", (evt) => {
    src.value = URL.createObjectURL(evt.data);
});

结束后还可以回看录制的视频,等待视频播放完也能直接再video标签下载

你甚至可以和上一个demo一起双开,会发现连摄像头的返显都会录上

demo:mediaDevices-getDisplayMedia

交叉观察器实现img懒加载

IntersectionObserver用于观察一个dom元素的出现和消失

实现懒加载只需要关注出现,即:intersectionRatio>0:

const $imgs = document.querySelectorAll('img')
const imgSet = ($el) => {
    if ($el.getAttribute('data-src')) {
        const img = new Image()
        const src = img.src = $el.getAttribute('data-src')
        img.onload = () => {
            $el.setAttribute('src', src)
        }
        img.onerror = () => {
            $el.setAttribute('title', '加载失败')
        }
        $el.removeAttribute('data-src')
    }
}
const obs = new IntersectionObserver((entrys) => {
    entrys.forEach((item) => {
        if (item.intersectionRatio > 0) {
            imgSet(item.target)
        }
    })
}


其他细节:容器min-height:100px给空间或者默认图
demo:IntersectionObserver

animation-timeline实现滚动条动画

首先用@supports保证能解析,我理解为js的if语句

animation-timeline是新的css属性,值可以是root即根html,这里是内dom

view-timeline-name是特殊的css属性,值是任意css变量

最重要的是设置好随滚动条滚动的范围range:animation-range

这里巧妙的把sticky结合,每块区域精准的只在所占范围内执行了动画,所以就有了挨个黏住缩小的动画

.item {
    height: 400px;
    margin-bottom: 10px;
    border: 1px solid #ccc;
    padding: 10px;
    background-color: #ccc;

    &:nth-child(1) {
        --index: 1;
    }

    &:nth-child(2) {
        --index: 2;
    }

    &:nth-child(3) {
        --index: 3;
    }

    &:nth-child(4) {
        --index: 4;
    }

    position: sticky;
    top: calc(var(--index) * 20px);
}

@supports(animation-timeline: view()) {
    .list {
        view-timeline-name: --timeline-1;
    }

    .item {
        transform-origin: 50% 0;
        /* 动画时间轴 */
        --rangestart: calc((var(--index) - 1) * 20%);
        --rangeEnd: calc((var(--index)) * 20%);
        animation: stackingSmall linear forwards;
        animation-timeline: --timeline-1;

        animation-range: exit-crossing var(--rangestart) exit-crossing var(--rangeEnd);
        /* exit-crossing 50% 表示subject在超出交叉的【自身的50%】后开始 */
        /* exit 50% 表示subject在【视觉上占滚动容器的50%】后开始 */
    }

    @keyframes stackingSmall {
        to {
            transform: scale(calc(var(--index) * 0.05 + 0.7));
            background-color: antiquewhite;
            border-radius: 10px;

        }
    }
}

demo-css-timeline

startViewTransition实现单图移位预览

startViewTransition是个提前截图的机制,即先对比回调方法执行动画后的位置,再根据不同改变(默认就是渐变)

实现知乎打开图片动画的关键就是分别获取先后位置:

先:getBoundingClientRect

后:居中位置

直接在startViewTransition回调设置后者位置即可,整体开发的感知就是前后两帧

<img @click="toLarge($event)" src="https://gjx.zone/bg-logo500.webp" width="200"  alt="">
<div v-if="src" class="fixed z-10 top-0 left-0 right-0 bottom-0  overflow-auto" @click="toSmall">
    <img ref="img" :src="src" alt="" class=" z-10 duration-500 transition-all">
    <div class=" fixed top-0 left-0 right-0 bottom-0  bg-gray-500 bg-opacity-50 "></div>
</div>
let originStyle
const toLarge = (e) => {
    src.value = e.target.src
    nextTick(() => {
        const style = img.value.style
        Object.assign(style, originStyle = originStyleGen(e.target.getBoundingClientRect())) // 第一帧
        document.startViewTransition(() => {
            const { naturalWidth, naturalHeight } = e.target
            const { clientWidth, clientHeight } = document.documentElement
            Object.assign(style, originStyleGen({
                x: clientWidth / 2 - naturalWidth / 2,
                y: clientHeight / 2 - naturalHeight / 2,
                width: naturalWidth,
                height: naturalHeight
            })) // 第二帧
        })
    })
}
const originStyleGen = (orginRect)=>({
    position: 'absolute',
    left: `${orginRect.x}px`,
    top: `${orginRect.y}px`,
    width: `${orginRect.width}px`,
    height: `${orginRect.height}px`
})
const toSmall = (e) => {
    const style = img.value.style
    Object.assign(style, originStyle) // 还原
    setTimeout(()=>{
        src.value = '' // 动画完成后干掉dom
        originStyle = {}
    }, 500)
}

当然这里只是点击取消,没有做到滚动后的缩小还原,感兴趣的可以试试

demo-startViewTransition

animate更流畅

用js来实现动画可以有三种方式:requestAnimationFrame,animate,setTimeout

    const times = 200
    
    // 方式一:requestAnimationFrame+200次
    i = 0
    const loop = () => {
        // 可以尽可能的16ms一刷
        requestAnimationFrame(() => {
            data.left = i
            if (i < times) {
                i++
                loop()
            }
        })
    }
    loop()

    // 方式二: animate+200次16.7
    
    div2.value.animate([{
        left: 0
    }, {
        left: times + 'px'
    }], {
        duration: 16.7 * times,
        fill: 'forwards'
    })

    // 方式三: setTimeout+200次16.7
    j = 0
    const loop2 = () => {
        setTimeout(() => {
            div3.value.style.left = j + 'px'
            if (j < times) {
                j++
                loop2()
            }
        }, 16.7)
    }
    loop2()

demo-requestAnimationFrame

demo在正常情况下三种方式都很流畅,差异不大

但animate可以切换chrome的标签后不停,rfa和setTimeout就不行,切换tab标签页回来才会接着执行

这也侧面说明了css3是开启的不同gpu线程

日期:2024-06-04 16:00:36 | 阅读:66 | 评论:1