【开源心路历程】尤大同款,百行代码搞定实时聊天

1,308 阅读7分钟

前言

我是阿江,我又来了,首先说一下真不是标题党,而是前段时间去看vue2源码的时候,发现在源码目录里面有一个叫examples的文件夹,尤大用vue写的小demo,咱怎么着也得进去学习一下。
然后就发现了本文诞生的灵感demo: 基于firebase实现的共享邮箱的demo,当下笔者就坐不住了。 这如果用来实现一个简易的实时聊天功能,多是一件美逝啊!所以就有了下文。
这里是笔者实现并部署的一个简单的聊天室
话不多说欸哈哈哈哈,鸡汤来咯! image.png

开始

firebase的作用

首先在实现之前,先需要了解一下 firebase 是干嘛用的。firebase是由谷歌支持开发,一款云数据库服务。而firebase和常规云数据库不同的是,firebase 可以共享实时数据库。

接下来读者没有google账号的可以先去注册一个 google账号

当注册后,可以直接打开firebase官网

创建firebase项目

image.png

这里我们直接继续下一步。

image.png

这个我们只是一个demo 所以就不配置Google Analytics,有需要的笔者可以配置一下,大体就是统计分析流量等。

image.png

接下来就是创建一个 Realtime Database实时数据库。

image.png

这里我们选择一个最近的新加坡服务器

image.png

由于方便测试我们这里以测试模式开始

image.png

创建好的默认数据

这里需要注意的是firebase存储的并不是类似数据库的表型结构数据,而是json格式数据,以便保证传输实时性得到保证!而我们的聊天数据也就可以创建一个数组对象来存储。

创建脚手架项目

到这一步我就可以正式开始项目搭建了,这里笔者直接使用自己的脚手架下载项目模板搭建。感兴趣可以了解一下上一篇文章:【开源心路历程】创建一个属于你自己的脚手架

当然也可以使用vue-cli脚手架生成 ,不过多赘述。

create-cli

image.png

当前项目的目录层级如下:

|- page //项目页面
|-  |— index //index 页面文件
|-  |—   |— static //资源文件 
|-  |—   |—   |- css
|-  |—   |—   |- img
|-  |—   |—   |- js 
|- index.html //页面 html

下载依赖

 npm i firebase --save 

之后在js文件夹下创建一个firebase.config.js文件,用于配置firebase初始化配置对象等。

firebase.config.js 配置文件

回到firebase官网中

image.png

点击创建Web应用

image.png image.png

这里分别代表引入初始化函数生成firebase配置对象

将这段代码复制到firebase.config.js文件中

这里需要注意的是:引入firebase的方式,笔者没有按照firebase上面默认生成的配置,而是选择引入全部firebase即为import firebase from 'firebase';这种方式。

引入firebase代码

这里引入我们使用V9的版本

import {initializeApp} from  'firebase/app';
import {getDatabase,ref,onValue,push} from 'firebase/database'

const firebaseConfig = {
    apiKey: "AIzaSyCL59rWYn4qI40MOa55PFud-6dn-KtkiYc",
    authDomain: "chatdemo-aa281.firebaseapp.com",
    databaseURL: "https://chatdemo-aa281-default-rtdb.asia-southeast1.firebasedatabase.app",
    projectId: "chatdemo-aa281",
    storageBucket: "chatdemo-aa281.appspot.com",
    messagingSenderId: "771306835282",
    appId: "1:771306835282:web:de5dda7d19acc99a3bfe8b"
};
参数说明
getDatabase获取数据库引用
ref("xxx/xxx")返回对查询位置的引用
onValue侦听特定位置的数据更改
push如果您提供了一个值 push () ,则该值将写入生成的位置。如果没有传递值,则不会向数据库写入任何内容,并且子内容仍然为空(但可以在其他地方使用 Reference)。

初始化firebase

initializeApp(firebaseConfig);

创建websocket连接实时数据库

const database=getDatabase();
const  chatsRef = ref(database,'chatList'); //返回聊天信息位置的引用

编写提交函数

/***
 * 提交函数,用于判断符合规则的信息提交
 * @param nickName 昵称
 * @param chatContent 聊天信息
 * @param chatDate 发送日期
 */
const chatSubmit = (nickName,chatContent,chatDate) => {
    if (nickName.length == 0) {
        alert("昵称不能为空!");
        return ;
    }else if(chatContent.trim() == ""){
        alert("内容不能为空!");
        return ;
    }else if(chatContent.length > 200 ){
        alert("内容超过最大字数!");
        return ;
    } else{
        pushChatData(nickName,chatContent,chatDate)
    }
}
/***
 * 添加聊天信息到firebase库里
 * @param nickName 昵称
 * @param chatContent 聊天信息
 * @param chatDate 发送日期
 */
const pushChatData =(nickName,chatContent, chatDate) =>{
    push(chatsRef,{
        chatContent,
        chatDate,
        nickName,
    })
}

导出

export {chatsRef , chatSubmit ,onValue }

chatsRef 代表聊天ref连接,在外面通过onValue获取聊天信息列表数据时需要用到。

index.js 入口文件

由于笔者使用的脚手架中有index.js文件所以不用创建,读者只需在同级创建一个index.js文件,引入代码

import {chatsRef,chatSubmit,onValue} from './firebase.config' // 引入firebase 的配置

引入Vue

下载依赖

npm install vue --save 

引入Vue

这里需要注意是,我们不能默认import Vue from 'vue'引入,这样引入vue是默认不包含编译器的版本,无法在页面上正常编译标签!所以我们引入完整版本的import Vue from 'vue/dist/vue' 具体解释可以参照Vue文档:对不同构建版本的解释

index.js中引入Vue

import Vue from 'vue/dist/vue'

创建Vue实例

el:"#index", //定义实例id
data:{ 
        state: {
            chatsList: [],//聊天信息数据
            prpoverVisible: false, // 气泡控制
            spinning: true, //加载控制
        },
        userState: {
            nickName: "", //昵称
            chatContent: "",//信息内容
            nickCheckStatus: 0,//0 为未填写, 1 非法  2 合格 3 校验成功
        }
},

加入逻辑函数,这里主要包含,

整体概要逻辑

image.png

初始化如果用户之前已经输入过昵称就会记录在localStorage中,并且会取出来验证昵称的合理性(这里的合理性,并没有做名称去重验证,只是简单验证昵称长度和特殊字符等)。如果合理则开始聊天。

并且保证信息发生改变时,整体显示信息的列表框要始终在最底部位置。以及日期转换和前后空格合并等。

methods:{
  /***
   * 初始化
   * 获取本地存储nickname 如果没有就需要强行设置
   */
   init(){
        let nickName=localStorage.getItem('nickName');
        if (nickName == null) return ;
      if (this.checkNickName(nickName)) {
            this.userState.nickName = nickName;
            this.userState.nickCheckStatus = 3;
        }
   },
    /***
     * 验证昵称是否通过
     * @param nickName 昵称
     * @returns {string|*|void|undefined} 验证通过返回名字,没通过返回空字符串,用于直接判断。
     */
   checkNickName(nickName){
            let checkRule=new RegExp(/^[\u4e00-\u9fa5_a-zA-Z0-9-]{2,8}$/);
            let checkValue=nickName.replace(/\s/g, "");
            if (!checkValue.length) {
                this.userState.nickCheckStatus=0;
                return '';
            }else if (checkRule.test(checkValue)) {
                this.userState.nickCheckStatus=2;
                return checkValue;
            }else{
                this.userState.nickCheckStatus=1;
                //这里非法,是为了防止,有人直接通过浏览器 application 修改昵称用
                //做的非法清空
                window.localStorage.removeItem('nickName');
                return '';
            }
   },
   /**
     * 添加昵称
     */
    addNickName(){
        if (this.checkNickName(this.userState.nickName)) {
            localStorage.setItem('nickName',this.userState.nickName);
            this.init();
        }
    },
    /***
     * 清空输入框和去掉空格
     */
    clearChatContent(){
        this.userState.chatContent="";
        this.userState.chatContent=this.userState.chatContent.replace(/\s/g, "");
    },
    /**
     * 转换年月日
     * @param date
     * @param fmt
     * @returns {*|void|string}
     */
    dateShow(date,fmt){
        let o = {
            "M+" : date.getMonth()+1,                 //月份
            "d+" : date.getDate(),                    //日
            "h+" : date.getHours(),                   //小时
            "m+" : date.getMinutes(),                 //分
            "s+" : date.getSeconds(),                 //秒
            "q+" : Math.floor((date.getMonth()+3)/3), //季度
        };
        if(/(y+)/.test(fmt)) {
            fmt=fmt.replace(RegExp.$1, (date.getFullYear()+"").substr(4 - RegExp.$1.length));
        }
        for(var k in o) {
            if(new RegExp("("+ k +")").test(fmt)){
                fmt = fmt.replace(RegExp.$1, (RegExp.$1.length==1) ? (o[k]) : (("00"+ o[k]).substr((""+ o[k]).length)));
            }
        }
        return fmt;
    },
    /**
     * 添加聊天
     * @param e
     */
    addChats(e){
        event.cancelBubble=true; //禁止换行
        event.preventDefault();
        event.stopPropagation();
        this.userState.chatContent = this.userState.chatContent.trim();
        chatSubmit(this.userState.nickName,this.userState.chatContent,this.dateShow(new Date(),"yyyy/MM/dd hh:mm:ss"));
        this.clearChatContent();
    },
    /**
     * 变成底部
     * @returns {Promise<void>}
     */
    async scrollToEnd(){
        await this.$nextTick();
        this.$refs.chatMedia.scrollTop = this.$refs.chatMedia.scrollHeight;
    }
},

created初始化 建聊天信息的更新的监听和初始化函数

created(){
    onValue(chatsRef,(snapshot)=>{
        this.state.chatsList = snapshot.val();
        if(this.state.spinning) this.state.spinning=false;
        this.scrollToEnd();
    })
    this.init();
}

index.html 页面

分别创建两个区域 :聊天信息展示区域输入区域

<div id="index">
    <div class="norem-index">
        <div class="chat">
            <h3 class="chat-title"> LiveChat 聊天室</h3>
            <!--  聊天信息展示区域 -->
            <!--  输入区域 -->
        </div>
    </div>
</div>

聊天信息展示区域

<div class="chat-media" ref="chatMedia">
    <div class="media"  v-for="item in state.chatsList">
        <div class="media-body">
            <p class="media-heading">
                {{ dateShow(new Date(item.chatDate),"yyyy/MM/dd hh:mm:ss") }}
                <span class="media-name">{{item.nickName}}</span></p>
            <p class="media-content">
                {{item.chatContent}}
            </p>
        </div>
    </div>
</div>

输入区域

<div class="enter">
    <!--   这里是负责初次进来填写的正常来说不填昵称不让输入的-->
    <div class="init-enter"  v-if="userState.nickCheckStatus !== 3">
        <input class="nick-input" v-model="userState.nickName" @change="checkNickName(userState.nickName)" placeholder="初次见面,请设定您的昵称~"/>
        <button  :disabled="userState.nickCheckStatus !== 2" @click="addNickName">开始聊天!</button>
    </div>
    <div class="enter-main" v-if="userState.nickCheckStatus === 3">
        <textarea
                class="textarea-enter"
                v-model="userState.chatContent"
                placeholder="你有什么想说的吗? Enter发送"
                :rows="5"
                @keydown.enter.stop="addChats"
        />
    </div>
</div>

这里只是做演示,样式不在这里贴了,下面笔者会贴上github地址用于查看源码。

这里的流程就是:用户需要进来的时候,先输入昵称,验证昵称是否合理,然后再进行聊天。自此我们其实就已经可以查看页面了,已经实现了基本的聊天功能。

image.png

拓展

上面只是事先了简单的聊天,但是聊天怎么能只有文字呢,下面会加入一些emoji做为表情库,来为我们的聊天增加易玩度。

firebase添加emojiList

首先在firebaseRealtime Database加入专门存放emoji表情的字段:emojiList

image.png

这里需要注意的是,一定要存储成数组,而不是字符串,否则会导致emoji乱码

firebase.config.js 加入 emoji

获取emoji的引入位置

const emojiRef = ref(database,'emojiList'); //返回对查询位置的引用

导出

加入emoji位置的导出 emojiRef

export {chatsRef ,emojiRef , chatSubmit ,onValue }

index.js 加入 emoji 逻辑

Vue 实例的 data对象 中 state 加入

state: {
    emojiList: [],//emoji表情集
    prpoverVisible: false, // 气泡控制
},

methods函数中加入 点击对应的emoji添加到输入框中,并关闭表情框。

/**
 * emoji表情点击添加
 * @param item
 */
emojiAddChat(item){
    this.userState.chatContent+=item;
    this.state.prpoverVisible=false;
},

created生命周期加入侦听emoji的数据更改。以保证我们只需要更新数据库中的emojiList用户表情框也会实时变化

onValue(emojiRef,(snapshot)=>{
    this.state.emojiList = snapshot.val();
})

index.html 加入 emoji 展示区域

找到我们之前添加的输入区域

<div class="enter-main"> 

添加如下代码
主要就是遍历获取到的emojiList,以及在输入框上层加入emoji选择icon

<div class="menu">
    <div>
        <div class="menu-tip" v-show="state.prpoverVisible">
             <span class="menu-emoji" @click="emojiAddChat(item)"  v-for="item in state.emojiList">
                {{item}}
             </span>
        </div>
        <img class="emoji"  @click="()=>{state.prpoverVisible=!state.prpoverVisible}" src="https://www.emojiall.com/favicon.ico"/>
    </div>
</div>

至此emoji也已加入成功。

image.png

结语

这样我们就实现了一个简单的实时聊天,可以根据上述api的去实现更多有意思的功能。如登陆在线人数在线列表讨论高级特工等

最后这里留下笔者的 成果地址,其中包含V8版本demoV9版本demo

希望小伙伴们能点点star支持一下笔者。之后会给大家带来更好的文章!
一起努力一起进步,我是阿江!