专栏首页跟铭哥学音视频技术TRTC学习之旅(二)-- 使用vue+ts集成TRTC实现多人会议室
原创

TRTC学习之旅(二)-- 使用vue+ts集成TRTC实现多人会议室

根据上回学习了官方TRTC demo之后,已经了解了一个基础的多人会议室创建的流程,接下来我需要将自己学到的转换为自己能够运用的。

项目介绍

这个项目是使用的框架是vue2 + typescript,项目搭建工具使用的是vue-cli。vue-cli搭建项目比webpack方便快捷,还是比较推荐大家使用的。

TRTC是的web sdk是集成了npm的,也有ts版本,在项目中可以使用下面的命令进行安装部署

npm install trtc-js-sdk --save-dev

实现的功能有开闭音视频、监听音量、会议室登录登出等基础功能。

先给大家看看我实现的效果图。

登录页
会议室

功能是demo基本上差不多,项目ui使用的是element-ui,也是现在用的比较多的UI框架了,想必大家并不陌生。

项目基础目录

view目录是我新建的目录,主要是用来存放一些视图展示的vue文件。

会议室主体代码

1.Client类是一个基础类,主要用于处理一些客户端初始化的方法(不过由于本人设计思想还不到位,这个类其实包含的逻辑就是RTCClient所需要的处理的逻辑);

2.RTCClient和ShareClient这两个类分别是处理音视频通话客户端和屏幕共享客户端的逻辑;

3.Generate类是用来加密生成用户签名的类,处理加密逻辑(在开发过程中,加密逻辑是放在服务器端,一定要注意!我这里为了方便,就放在本地处理了);

4.video-stream-item主要是用来处理单个音视频流的显示和样式逻辑,流的事件监听和播放也是放在这个组件里边;

5.video-stream-list主要功能是管理批量流的组件,控制多个视频的排列逻辑等;

6.trtc-room会议室客户端页面代码。

项目代码分析

Client类

import TRTC from 'trtc-js-sdk';
import { TRTCMode, AppConfig } from './../enum/mode';
import Generate from './Generate'

/**
 * trtc音视频客户端base类
 */
export default class Client {
    userId: string;  //用户id
    userSig: string;  //用户签名
    roomId: string;  //会议室id
    isPublished: boolean;  //是否已经发布过流
    isJoined: boolean;  //是否已经加入过房间
    localStream: any;  //本地流
    client: any;  //客户端
    cameraId: string; //摄像头id
    microphoneId: string;  //麦克风id
    
    /**
     * 用本我思考的是在实例化客户端的时候就把流的视图dom绑定好,但是后面发现这样并不灵活
     * 我把流的视图dom绑定放在了video-stream-item里面
     */
    // DOMContainer: HTMLDivElement | string;
    
    remoteStreams: Map<String, any>;  //远端流的map

    constructor(options: {
        userId: string, 
        roomId: string, 
        cameraId: string, 
        microphoneId: string, 
        // DOMContainer: string
    }) {
        this.userId = options.userId;
        this.roomId = options.roomId;

        this.cameraId = options.cameraId || '';
        this.microphoneId = options.microphoneId || '';
        // this.DOMContainer = options.DOMContainer;

        const generate = new Generate(this.userId);
        this.userSig = generate.getUserSig();

        // console.warn(this.userSig)

        this.isPublished = false;
        this.isJoined = false;
        this.localStream = null;
        this.remoteStreams = new Map();

        this.createClient();
    }

    createClient() {
        /**
         * 这里先暂时不实现切换视频设备和麦克风的逻辑,
         * 因为切换逻辑一般是在初始化之后再做
         */
        let params = {
            mode: <any>TRTCMode.VIDEOCALL,
            sdkAppId: AppConfig.SDKAPPID,
            userId: this.userId,
            userSig: this.userSig,
            // cameraId: this.cameraId,
            // microphoneId: this.microphoneId,
        }
        // console.warn(params)
        this.client = TRTC.createClient(params)
    }

    /**
     * 绑定监听事件
     * @param error 客户端报错
     * @param clientBanned 客户端被踢出房间
     * @param peerJoin 客户端加入房间
     * @param peerLeave 客户端离开房间
     * @param streamAdded 远程流加入房间,返回boolean,代表要不要订阅
     * @param streamSubscribed 订阅远程流
     * @param streamRemoved 移除远程流
     * @param streamUpdated 远程流更新
     */
    handleEvents({ 
        error = _ => {},
        clientBanned = _ => {},
        peerJoin = _ => {},
        peerLeave = _ => {},
        streamAdded = _ => { return true },
        streamSubscribed = _ => {},
        streamRemoved = _ => {},
        streamUpdated = _ => {},
        unmuteVideo = _ => {},
        muteVideo = _ => {},
        unmuteAudio = _ => {},
        muteAudio = _ => {},
    }) {
        this.client.on('error', (err: any) => {
            console.error(err);
            error(err);
            location.reload();
        })

        this.client.on('client-banned', (err: any) => {
            alert('您被提出了房间');
            clientBanned(err);
            location.reload();
        })

        this.client.on('peer-join', (e: any) => {
            peerJoin(e);
            console.log(`用户${e.userId}加入了房间`);
        })

        this.client.on('peer-leave', (e: any) => {
            peerLeave(e);
            console.log(`用户${e.userId}离开了房间`);
        })

        this.client.on('stream-added', (e: any) => {
            if (!streamAdded(e)) {
                return;
            }
            const remoteStream = e.stream;
            const id = remoteStream.getId();
            const userId = remoteStream.getUserId();

            console.log(`用户${userId}的${remoteStream.getType()}远端流(ID:${id})接入`);
            
            //订阅过的远端流不再订阅
            if (this.remoteStreams.has(userId)) return;

            this.client.subscribe(remoteStream);
        })

        this.client.on('stream-subscribed', (e: any) => {
            const remoteStream = e.stream;
            const id = remoteStream.getId();
            const userId = remoteStream.getUserId();

            console.log(`用户${userId}的${remoteStream.getType()}远端流(ID:${id})订阅成功`);
            
            
            this.remoteStreams.set(userId, remoteStream);
            
            streamSubscribed(e);

            /**
             * 订阅成功之后,不再直接进行play和监听状态操作
             */
            // remoteStream.on('player-state-change', e => {
            //     console.log(`用户${userId}的${e.type}状态改变为${e.state},原因是${e.reason}`)
            // });
            // remoteStream.play(streamSubscribed(e) || id, { objectFit: 'contain' });
        })

        this.client.on('stream-removed', (e: any) => {
            const remoteStream = e.stream;
            const userId = remoteStream.getUserId();
            const id = remoteStream.getId();

            if (this.remoteStreams.has(userId)) this.remoteStreams.delete(userId);

            /**
             * 移除操作我们还是可以直接进行的
             */
            remoteStream.stop();
            streamRemoved(e);
            console.log(`stream-removed ID: ${id}  type: ${remoteStream.getType()}`);
        })

        this.client.on('stream-updated', (e: any) => {
            const remoteStream = e.stream;
            const id = remoteStream.getId();
            const userId = remoteStream.getUserId();
            
            console.log('远端流状态更新')
            // this.remoteStreams.set(userId, remoteStream);

            streamUpdated(e);
        })
        
        //有远端流禁用了音频
        this.client.on('mute-audio', (e: any) => {
            console.log('mute-audio', e)
            muteAudio(e)
        })
        
        //有远端流开启了音频
        this.client.on('unmute-audio', (e: any) => {
            console.log('unmute-audio', e)
            unmuteAudio(e)
        })
        
        //有远端流禁用了视频
        this.client.on('mute-video', (e: any) => {
            console.log('mute-video', e)
            muteVideo(e)
        })
        
        //有远端流开启了视频
        this.client.on('unmute-video', (e: any) => {
            console.log('unmute-video', e)
            unmuteVideo(e)
        })
    }

    /**
     * 初始化房间
     */
    async initRoom() {
        if (this.isJoined) {
            console.log('客户端已经加入过房间');
            return;
        }

        try {
            await this.client.join({
                roomId: this.roomId
            });
            console.log('成功加入聊天室');
            this.isJoined = true;
            
            await this.initStream(); 
        } catch(err) {
            console.log('加入房间失败', err)
        }
    }

    getLocalStream() {
        return this.localStream;
    }
    
    /**
     * 初始化流
     */
    async initStream() {
        this.localStream = TRTC.createStream({
            audio: true,
            video: true,
            // cameraId: this.cameraId,
            // microphoneId: this.microphoneId,
            mirror: true
        })

        try {
            await this.localStream.initialize();
            console.log('本地流初始化成功');

            this.localStream.on('player-state-change', e => {
                console.log(`本地流${e.type},状态改变=>${e.state},原因=>${e.reason}`);
            })

            await this.publishStream();
            
            // this.localStream.play(this.DOMContainer);

        } catch (err) {
            console.error('初始化本地流错误', err);
        }
    }

    async publishStream() {
        if (!this.isJoined) {
            console.warn('本地客户端还没有加入任何聊天室');
            return
        }

        if (this.isPublished) {
            console.warn('本地客户端已经发布过音视频流了');
            return
        }
        try {
            await this.client.publish(this.localStream);
            console.log('本地流发布成功');
            this.isPublished = true;
        } catch (err) {
            this.isPublished = false;
            console.error('发布本地流出现错误', err);
        }
    }

    async unpublishStream() {
        if (!this.isJoined) {
            console.warn('尚未加入房间,无法实行取消流发布');
            return;
        }

        if (!this.isPublished) {
            console.warn('尚未发布任何本地流,无法取消发布');
            return;
        }

        await this.client.unpublish(this.localStream);
        this.isPublished = false;
    }
    
    async leave() {
        if (!this.isJoined) {
            console.warn('未加入房间,无法执行离开操作')
            return;
        }

        await this.unpublishStream();

        await this.client.leave();

        this.localStream.stop();
        this.localStream.close();
        this.localStream = null;
        this.isJoined = false;
    }

    /**
     * 恢复播放所有流
     * 该方法主要是解决视频无法自动播放的问题
     */
    // resumeStreams() {
    //     this.localStream.resume();
    //     for (let [userId, stream] of this.remoteStreams) {
    //     stream.resume();
    //     }
    // }

    /**
     * 禁止本地音频
     */
    muteLocalAudio() {
        return this.localStream.muteAudio();
    }

    /**
     * 打开本地音频
     */
    unmuteLocalAudio() {
        return this.localStream.unmuteAudio();
    }

    /**
     * 禁止本地视频
     */
    muteLocalVideo() {
        return this.localStream.muteVideo();
    }

    /**
     * 打开本地视频
     */
    unmuteLocalVideo() {
        return this.localStream.unmuteVideo();
    }
}

RTCClient类

import Client from './Client';

export default class RTCClient extends Client {
    constructor(options: any) {
        super(options);
    }
}

好尴尬呀,因为设计缺陷,我把逻辑都放在Client类了,还在思考能怎么拆分,有大佬有想法的,可以在评论告知我一些思路。因为我是感觉那些逻辑无论是哪个客户端都需要进行。

ShareClient类

import Client from './Client';
import TRTC from 'trtc-js-sdk';
import { TRTCMode, AppConfig } from './../enum/mode';

export class ShareClient extends Client {
    constructor(options: any) {
        super(options);
    }

    createClient() {
        this.client = TRTC.createClient({
            mode: TRTCMode.VIDEOCALL,
            sdkAppId: AppConfig.SDKAPPID,
            userId: this.userId,
            userSig: this.userSig
        })

        //于普通的客户端不同,共享屏幕的客户端不接收远端流,所以要重写创建客户端方法
        this.client.setDefaultMuteRemoteStreams(true);
    }

    /**
     * 共享屏幕的流也需要重新初始化流的方法
     */
    async initStream() {
        this.localStream = TRTC.createStream({
            audio: false,
            screen: true,
            video: true,
        })

        try {
            this.localStream.initialize();
            console.log('本地流初始化成功');

            this.localStream.on('player-state-change', e => {
                console.log(`本地流${e.type},状态改变=>${e.state},原因=>${e.reason}`);
            })

            await this.publishStream();
            
            //同理这里也不进行play操作
            // this.localStream.play(this.DOMContainer);

        } catch (err) {
            console.error('初始化本地流错误', err);
        }
    }
}

trtc-room/index.vue

<template>
    <el-container class="trtc-room">
        <el-header class="trtc-room_header" height="44px">
            会议室{{roomId}}
            <el-button 
                class="float-right close-btn" 
                type="primary" 
                size="small"
                @click="closeMeeting">退出会议室
            </el-button>
        </el-header>
        <el-container>
            <el-aside class="trtc-room_aside" width="240px">
                <video-stream-item
                    ref="videoStreamItem"
                    v-if="hasInit"
                    :user-id="userId" 
                    :show-user-id="false"
                    :stream='rtcClient.localStream'>
                </video-stream-item>
                <div class="header-info">
                    <p>{{userId}}</p>
                </div>
                <div class="device-control">
                    <div class="icon-container" @click="changeAudio">
                        <icon :icon="isMuteAudio ? 'audio' : 'audio-off'" size="large"></icon>
                    </div>
                    <div class="icon-container" @click="changeVideo">
                        <icon :icon="isMuteVideo ? 'video' : 'video-off'" size="large"></icon>
                    </div>
                </div>
            </el-aside>
            <el-main>
                <video-stream-list :streams="remoteStreams" ref="videoStreamList"></video-stream-list>
            </el-main>
        </el-container>
    </el-container>
</template>

<script>
    import RTCClient from './js/client/class/RTCClient'
    import VideoStreamItem from '@/components/trtc-room/components/video-stream-item'
    import VideoStreamList from '@/components/trtc-room/components/video-stream-list'
    import Icon from '@/components/icon'
    import { Component, Vue, Prop }  from 'vue-property-decorator';
    @Component({
        components: {
            RTCClient,
            VideoStreamItem,
            Icon,
            VideoStreamList
        }
    })
    export default class TrtcRoom extends Vue {
        rtcClient = null; //客户端
        hasInit = false; //是否已经完成初始化
        remoteStreams = [];  //远端流
        isMuteVideo = false;  //是否禁用视频
        isMuteAudio = false;  //是否禁用音频

        @Prop() roomId;
        @Prop() userId;

        created() {
            console.log('trtc-room created')
        }
        async mounted() {
            const options = {
                userId: this.userId,
                roomId: this.roomId,
            };

            this.rtcClient = new RTCClient(options);

            await this.init();
            this.hasInit = true;
        }

        /**
         * 切换音频状态
         */
        changeAudio() {
            const actionFunc = () => {
                return this.isMuteAudio 
                    ? this.rtcClient.unmuteLocalAudio()
                    : this.rtcClient.muteLocalAudio();
            }
                
            actionFunc()
                ? this.isMuteAudio = !this.isMuteAudio
                : this.$message({
                    type: 'warning',
                    message: '没有视频设备'
                })
            
            //调用组件修改状态
            this.$refs.videoStreamItem.changeAudio(this.isMuteAudio);
        }

        /**
         * 切换视频状态
         */
        changeVideo() {
            const actionFunc = () => {
                return this.isMuteVideo 
                    ? this.rtcClient.unmuteLocalVideo()
                    : this.rtcClient.muteLocalVideo();
            }
                
            actionFunc()
                ? this.isMuteVideo = !this.isMuteVideo
                : this.$message({
                    type: 'warning',
                    message: '没有视频设备'
                })
            
            //调用组件修改状态
            this.$refs.videoStreamItem.changeVideo(this.isMuteVideo);
        }

        /**
         * 初始化
         */
        async init() {

            //监听事件绑定
            const events = {
                error: this.error,
                clientBanned: this.clientBanned,
                peerJoin: this.peerJoin,
                peerLeave: this.peerLeave,
                streamAdded: this.streamAdded,
                streamSubscribed: this.streamSubscribed,
                streamRemoved: this.streamRemoved,
                streamUpdated: this.streamUpdated,
                unmuteAudio: this.unmuteAudio,
                muteAudio: this.muteAudio,
                unmuteVideo: this.unmuteVideo,
                muteVideo: this.muteVideo,
            }
            this.rtcClient.handleEvents(events);
            try {
                await this.rtcClient.initRoom();
            } catch (error) {
                console.error(error)
            }
        }

        /**
         * 退出会议室
         */
        async closeMeeting() {
            try {
                await this.rtcClient.leave();
                this.$router.push({
                    name: 'Login'
                })
            } catch (error) {
                console.error(error)
            }
        }

        /**
         * 更新远端流
         */
        updateRemoteStreams() {
            let streams = Array.from(this.rtcClient.remoteStreams.values());
            this.remoteStreams = streams;
        }

        error(e) {
            console.log(e)
        }
        clientBanned(e) {
            console.log(e)
        }
        peerJoin(e) {
            console.log(e)
        }
        peerLeave(e) {
            console.log(e)
        }
        streamAdded(e) { 
            console.log(e);
            return true;
        }
        streamSubscribed(e) { 
            console.log('订阅到新远端流了', e.userId)
            this.updateRemoteStreams();
        }
        streamRemoved(e) {
            console.log('有一位远端流离开了房间', e)
            this.updateRemoteStreams();
        }
        streamUpdated(e) {
            console.log(e)
        }
        muteAudio(e) {
            console.log('远端禁用音频', e)
            //调用组件修改状态
            this.$refs.videoStreamList.changeAudio(e.userId, true);
        }
        unmuteAudio(e) {
            console.log(e)
            //调用组件修改状态
            this.$refs.videoStreamList.changeAudio(e.userId, false);
        }
        muteVideo(e) {
            //调用组件修改状态
            this.$refs.videoStreamList.changeVideo(e.userId, true);
            console.log('远端禁用视频', e)
        }
        unmuteVideo(e) {
            //调用组件修改状态
            this.$refs.videoStreamList.changeVideo(e.userId, false);
            console.log(e)
        }
    }
</script>

<style lang="less" scoped>
.trtc-room_header {
    line-height: 44px;
    box-shadow: 0 5px 5px #ddd;
    margin-bottom: 10px;
}

.trtc-room_aside {
    box-shadow: 5px 0px 5px #ddd;
}

.header-info {
    text-align: center;
    height: 48px;
    line-height: 48px;
    border-bottom: 1px solid #ddd;
}

.device-control {
    display: flex;
    .icon-container {
        flex: 1 1 50%;
        text-align: center;
        padding: 15px;
        cursor: pointer;
        transition: all .3s ease 0s;
        &:hover {
            background-color: rgba(221, 221, 221, .3);
        }
        &:first-child {
            border-right: 1px solid #ddd;
        }
    }
}
.close-btn {
    margin-top: 6px;
}
</style>
<style lang="less">

</style>

video-stream-item.vue

<template>
    <div class="video-play-plane">
        <div class="video-play-view" :id="videoId"></div>
        <div class="video-play-desc">
            <span class="user-name" v-if="showUserId">{{userId}}</span>
            <icon 
                :icon="isMuteAudio ? 'audio-off' : 'audio'"
                size="normal"
                class="audio-icon">
            </icon>
            <div class="audio-level" :style="levelStyle"></div>
        </div>
    </div>
</template>

<script>
    import { Vue, Prop, Component } from 'vue-property-decorator';
    import Icon from '@/components/icon'
    
    @Component({
        components: {
            Icon
        }
    })
    export default class VideoStreamItem extends Vue {
        @Prop({
            required: true,
        }) stream

        @Prop({
            required: true
        }) userId

        @Prop({
            type: Boolean,
            default: true
        }) showUserId

        videoId = 'trtcVideo'; //单个流的id
        isMuteVideo = false;  //是否禁用了视频
        isMuteAudio = false;  //是否禁用了音频
        audioLevel = 0;  //音量等级(0 - 1)
        interval = null;  //定时器(偶用来监听声音等级变化)

        /**
         * 计算当前音量样式
         */
        get levelStyle() {
            const baseHeight = 11;
            const baseWidth = 6;
            const baseTop = 21;
            
            let realHeight = 
                !this.isMuteAudio 
                ? baseHeight * this.audioLevel
                : 0;
            
            let realTop = baseTop - realHeight;

            return {
                height: realHeight + 'px',
                width: baseWidth + 'px',
                top: realTop + 'px'
            }
        }
        
        created() {
            this.videoId = this.videoId + this.stream.getId();
        }
        mounted() {

            //监听当前流的状态
            this.stream.on('player-state-changed', e => {
                console.log(`用户${this.userId}的${e.type}状态改变为${e.state},原因是${e.reason}`)
                
                //由于浏览器自动播放策略可能会导致视频不会自动播放,需要手动吊起
                if (event.state === 'PAUSED') {
                    this.stream.resume();
                }
            });

            //执行play,流的音视频在页面上展示
            this.stream.play(this.videoId);

            //设置定时器监听音量变化
            this.interval = setInterval(() => {
                this.audioLevel = this.stream.getAudioLevel();

                //对于等于0.1就相当于有在说话了
                if (this.audioLevel >= 0.1) {
                    console.log(`user ${this.stream.getUserId()} is speaking`);
                }
            }, 200);
        }
        destroyed() {
            //清楚定时器
            clearInterval(this.interval);   
        }

        changeVideo(sign) {
            this.isMuteVideo = sign;
        }
        changeAudio(sign) {
            this.isMuteAudio = sign;
        }
    }
</script>

<style lang="less" scoped>
.video-play-plane {
    position: relative;
    width: 240px;
    height: 180px;
}

.video-play-view {
    width: 240px;
    height: 180px;
    background-color: red;
}

.video-play-desc {
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
    height: 37px;
    padding: 5px;
    box-sizing: border-box;
    background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .8));
    .user-name {
        color: #fff;
    }
}
.audio-icon {
    float: right;
    color: #e8ff26;
    position: absolute;
    right: 0;
    bottom: 5px;
    z-index: 1;
}
.audio-level {
    background-color: #52c352;
    position: absolute;
    right: 9.5px;
    // top: 210px;
    z-index: 0;
}
</style>

video-stream-list.vue

<template>
    <el-card class="stream-list-container">
        <header class="header-title" slot="header">会议成员</header>
        <video-stream-item 
            v-for="stream in streams" 
            :key="stream.getId()"
            :ref="stream.getUserId()"
            :user-id="stream.getUserId()"
            :stream="stream">
        </video-stream-item>
        <div class="no-data" v-if="!streams.length">暂无其他参会人员</div>
    </el-card>
</template>

<script>
    import { Vue, Component, Prop } from 'vue-property-decorator'
    import VideoStreamItem from './video-stream-item'

    @Component({
        components: {
            VideoStreamItem
        }
    })
    export default class VideoStreamList extends Vue {
        @Prop({
            required: true,
            type: Array,
            default() {
                return []
            }
        }) streams;
        
        changeVideo(userId, sign) {
            const ref = this.$refs[userId][0];
            if (ref) ref.changeVideo(sign);
        }
        
        changeAudio(userId, sign) {
            const ref = this.$refs[userId][0];
            if (ref) ref.changeAudio(sign);
        }
    }
</script>

总结

从上手程度上来看,用trtc搭建一个简单的多人聊天室速度基本上比较快,难度不高,基本上能看懂官网的文档就可以直接上手使用了。但是里面有几点要注意一下:

1.在包裹了音视频流播放标签的父标签中设置text-align: center,会导致视频偏移;

视频发生了偏移

2.在client监听的音视频切换(mute-audio、mute-video等四个),都是远端的流的切换事件,目前我还没找到怎么监听本地流;

3.stream上绑定player-state-changed,似乎无法监听到关闭和打开音视频流,不知道是不是我写的有问题,大家可以一起试试。

下一篇文章我会基于这次的项目更新直播模式,实现多人小课堂的模式,大概功能如下:

1.屏幕共享;

3.模拟房管功能;

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • TRTC学习之旅(三)-- 使用vue+ts集成互动直播

    上次我们已经用vue+ts实现了多人会议室的搭建,这次我们继续在上次项目的基础上,实现互动直播功能。

    黑眼圈云豆
  • TRTC学习之旅(一)--多人聊天室web篇(官方demo)

    大家好,我是刚入坑TRTC的小菜鸡,黑圆圈云豆。因为我的主要技术方向是web,所以我就从基于web开发的TRTC demo进行学习和知识分享。

    黑眼圈云豆
  • IM即时通信探索(三)-- 实现一个简单的直播聊天室

    今天我们用IM来简单的实现一个直播聊天室场景。不过在这之前呢,我们还需要先来熟悉一下IM直播功能的一些特性。本文以web端进行代码讲解,与其它端可能会有些差异,...

    黑眼圈云豆
  • 使用electron制作满屏心特效

    这样就会让窗口全屏 但是有一个问题 就是这样做界面不会正确响应 我们可以使用进程通信去解决

    李昊天
  • RocketMQ 源码分析 —— Message 存储

    本文接《RocketMQ 源码分析 —— Message 发送与接收》。主要解析 CommitLog 存储消息部分。

    芋道源码
  • vue生命周期钩子函数

    1. created与mounted都常见于ajax请求,前者如果请求响应时间过长,容易白屏

    用户1149564
  • Mybatis深入源码分析之Mapper与接口绑定原理源码分析

    紧接上篇文章:Mybatis深入源码分析之SqlSessionFactoryBuilder源码分析,这里再来分析下,Mapper与接口绑定原理。

    须臾之余
  • 修炼内功之JavaScript设计模式(三)

    工作时间久了,自然对软件系统产生自己的思考,还会面临职业生涯的一个挑战。要不要成为一个技术负责人?

    童欧巴
  • 移动端 局部dom实现滚动

    https://github.com/surmon-china/vue-awesome-swiper/issues/423

    念念不忘
  • 学习 Phaser.js HTML5游戏开发-DAY3

    3. 构建基本的子弹对象,fire 方法用来初始化子弹实例,update方法用来绘制子弹轨迹

    tonglei0429

扫码关注云+社区

领取腾讯云代金券