【Vue项目】仿哔哩哔哩网页
目前市面上,有一些以短视频为主的手机应用越来越多。如哔哩哔哩、抖音、小红书、快手、youtube,这些手机短视频软件应用,个人认为,之所以能拔地而起,主要是由于以下几点:1、门槛低、视频功能强大、平台社交功能强大、贴近生活。毫不夸张地说,假设一个人自制力较弱,并且拥有大把的闲暇时光,给他推荐一款这样类型的软件,一天之内,他就会沉浸在刷视频的快感之中。话不多说,我设计的这款应用主要参照哔哩哔哩这款手
一、 项目开发的背景
目前市面上,有一些以短视频为主的手机应用越来越多。如哔哩哔哩、抖音、小红书、快手、youtube,这些手机短视频软件应用,个人认为,之所以能拔地而起,主要是由于以下几点:1、门槛低、视频功能强大、平台社交功能强大、贴近生活。毫不夸张地说,假设一个人自制力较弱,并且拥有大把的闲暇时光,给他推荐一款这样类型的软件,一天之内,他就会沉浸在刷视频的快感之中。话不多说,我设计的这款应用主要参照哔哩哔哩这款手机应用,同时搭建本都服务器来提供数据响应。这款网站将部署至阿里云服务器,其他用户可以通过公网来访问该网站。3
二、涉及的技术
该项目前端使用Vue框架开发,后端采用NodeJs搭建服务器。使用了WebSocket技术和PWA技术,
三、服务器的搭建说明
采用NodeJS搭建后台服务器,项目serve_chat和serve_main实现聊天功能,项目serve_main是实现页面基础操作,包括用户登录、视频播放、网页数据的提供等。
A 前端项目
采用Vue框架搭建项目vue_project_bilibili。
B 项目运行
后端服务器要在项目所在文件夹下运行命令
node app.js
前端服务器也要在项目所在文件夹下运行命令
npm run serve
C 界面展示
1 登录界面
1.1入口
用户未登录时,部分功能将无法使用。在主页侧滑栏点击头像,可以登录
功能:用户在此界面可以在地区空白处,点击鼠标选择中国大陆或者美国,输入11位手机号点击发送验证码按钮,就会收到后端服务器发来的验证码,若验证码不正确,则登陆失败。若初次登录,则会注册用户信息。
用户信息完善
用户输入自己完整的信息为之后用户可以进入直播间,点击提交之后会存储到后端数据库中
用户信息包括用户名、密码、牌子、勋章等级、用户等级、性别、所在城市、头像。
1.2 组件选取
1.3 登录界面的功能
输入密码时,上方图片会改变
点击地区输入框给与提示,此处是用ElmentUI中的组件,具体内容见ElementUI的官网。
输入验证码后即可登录,输入验证码其实是使用get的方式去访问本地服务器的内容,看看是否有该手机号,并验证手机号所对应的验证码是否匹配。若匹配则返回登陆成功的信息。同时验证码有时间限制,超过十秒,将会失效。
2 番剧界面
2.1 入口
从底部导航栏进入该界面
2.2 组件的选取
2.3 番剧界面的功能
轮播图会自动滚动,以及返回顶部功能
2.4 番剧详情
番剧评论长按实现举报功能
自定义指令实现
import Vue from "vue";
Vue.directive('longpress',{
bind:function (el,binding,vNode) {
// el: 指令所绑定的元素,可以用来直接操作DOM。
// binding: 一个对象,包含指令的很多信息。
// vnode: Vue编译生成的虚拟节点。
let pressTimer = null;
let start = (e) => {
if (e.type === 'click') {
return;
}
if (pressTimer === null) {
pressTimer = setTimeout(() => {
handler();
}, 1000)
}
};
let cancel = (e) => {
if ( pressTimer !== null ) {
clearTimeout(pressTimer);
pressTimer = null;
}
};
const handler = (e) => {
binding.value(e)
};
el.addEventListener("mousedown", start);
el.addEventListener("click", cancel);
}
});
<div class="commentContent" v-longpress="OpenBottomSheet">{{cmt.content.message}}</div>
<more-message :open="open" @closeBottomSheet = "closeBottomSheet"></more-message>
3 视频界面
3.1 入口
加载主界面的时候,可以点击底部的导航栏,进入该界面
3.2 组件选取
3.3 视频界面功能
返回底部按钮、五个标签可以点击进入不同界面、轮播图随时间变化、下拉刷新
此处的下拉刷新插件使用了vant的list,可以实现一个下拉刷新的操作,下面是插件需要配置的方法,其中是一个每个一秒就执行一次的操作,包括当用户下拉刷新,那么refreshing会赋值为true,由此会使if判断为真。当然onfresh()方法是将新的数据加入其中。
onLoad() {
setTimeout(() => {
if (this.refreshing) {
this.$message("已刷新");
this.refreshing = false;
}
this.loading = false;
}, 1000);
},
onRefresh() {
this.finished = false;
this.loading = true;
this.getMoreData();
this.numberToShow+=4;
this.onLoad();
},
下面是从接口请求新的数据,每一次请求4条
getMoreData() {
getHomeMultidata_life().then(res => {
let temp = res.rank.list.slice(this.numberToShow, this.numberToShow+4);
for(let i=0;i<4;i++){
this.CardShowList.unshift(temp[i]);
}
})
},
此处对比bilibili,其实bilibili在刷新之后,广告界面被推到后面的界面了,我这里重新刷新界面是再一次请求新的数据,重新请求四个数据,同时要想保留之前的数据,并加到前部,就要将numbertoshow加四,并且新的数据要展示在前部,也就是unshift()。
3.4 直播界面
点击上面的五个标题的第二个标题“直播”
进入直播间
用户点击某一个直播,可以进入某一个直播间,该直播间可以进行聊天,同时借用了vant框架中的部分组件使界面更加完整。
组件选取
使用了vant框架的组件NoticeBar 通知栏和 Tab
直播界面功能
该界面使用了websocket技术可以在每次刷新界面的时候,即代表一名用户加入直播间,在次直播间的用户可以发表看法,并且咋刚进入直播间的用户会显示“某用户进入直播间”,以及退出直播间的时候,会显示“某用户退出直播间”。
用户可以在进入直播间后发送弹幕,但是在此之前要用户输入个人信息,输入完全后就可以发送弹幕,发出评论会显示用户的基本信息,和之前输入的信息是一致的,同时在用户进入直播间之前,显示“连接服务器成功”和用户XXX进入直播间。当有其他用户进入直播间时,还会显示其他用户进入直播间的信息,以及其他用户发出消息,本人也能看到。
4 会员购界面
4.1 入口
同样也是从主界面的底部菜单栏可以进入
4.2 组件选取
4.3 会员购界面的功能
从本地服务端获取数据;点击按钮全部时间可以显示日期
getMutildata_Tickets(){
getMutildata_Tickets().then(res=>{
this.showList = res.data.result.slice(0,10);
console.log(JSON.stringify(res));
})
}
购买商品
点击商品可以选择购买商品,这里使用了vant组件库的sku组件
5 发现界面
D 配置路由
加载最初界面时,重定向进入home页面,也就是主界面。此处使用了懒加载.配置模式是history
//懒加载
const Home = () => import('../views/home/Home');
const Watching = () => import('../views/watching/Watching');
const Profile = () => import('../views/profile/Profile');
const FanJu = () => import('../views/fanJu/FanJu');
const Words = () => import('../views/words/Words');
const Login = () => import('../views/login/Login');
1 配置路径
const routes = [
{
path:'/info',
name:'info',
component: InfoForm
},
{
path: '',
redirect: '/watching',
meta: {
title: '首页'
},
},
{
path:'/watching/onlineShow',
name:'onlineShow',
component: OnlineShow
},
{
path:'/drama/watching',
redirect:'/watching'
},
{
path:'/drama/fanJu',
redirect:'/fanJu'
},
{
path:'/drama/profile',
redirect:'/profile'
},
{
path:'/drama/friend',
redirect:'/friend'
},
{
path:'/drama/Find',
redirect:'/Find'
},
{
path: '/friend',
name:'friend',
component: Friend
},
{
path: '/watching',
component: Watching
},
{
path: '/fanJu',
component: FanJu,
},
{
path: '/profile',
component: Profile
},
{
path: '/Find',
component: Words
},
{
path: '/drama/:season_id',
name: 'dramaPlay',
component: dramaPlay
},
{
path:'/guanzhu',
name:'guanZhu',
component:GuanZhu
},
{
path:'/login',
name:'login',
component:Login
}
];
E 配置网络相关的内容
利用axios来获取数据(包括自己搭建的服务器和bilibiliAPI)
1 server
为了使整个代码结构看起来更好管理,所以我在这里对这里进行封装,并抛出接口,对于不同的url拼接上原先的baseUrl就能可以更好管理不同数据获取。这样我访问不同类型的数据时,若我需要改动数据,就可以直接更改这里的接口里面的url。
1.1 服务器涉及的前端界面及其提供的功能
保存用户输入的数据
后端对应的配置代码如下
saveUserData = (req, res) => {
res.header("Access-Control-Allow-Origin","*");
let {userName, password, paiZi, paiZiLevel, userLevel, sex, city, tel} = req.query;
var sqlArr = [userName, password, paiZi, paiZiLevel, userLevel, sex, city, tel];
var sql = "INSERT INTO users (userName, password, paiZi, paiZiLevel, userLevel, sex, city, tel) VALUES (?,?,?,?,?,?,?,?)";
var callBack = (err, data) => {
if(err){
console.log('连接出错了');
}else{
res.send({
'message':'success'
})
}
};
dbConfig.sqlConnect(sql,sqlArr,callBack);
};
主要操作是数据库的存储数据
获取用户的数据(根据电话号码)
getUserInfo = (req, res) => {
res.header("Access-Control-Allow-Origin","*");
let {tel} = req.query;
var sql = 'Select userName,paiZi,paiZiLevel,userLevel from users where tel = ?';
//跟上参数,表示用户想要访问什么数据
var sqlArr = [tel];
var callBack = (err, data) => {
if(err) {
console.log('连接出错了');
res.send({
'message' : 'error'
})
}else {
res.send({
'user' : data
})
}
};
dbConfig.sqlConnect(sql,sqlArr,callBack);
};
为前端提供展示的数据
涉及大多数界面,其中这些require来的数据是json数据
var data_1 = require('../static/json/data_1');
var data_2 = require('../static/json/data_2');
var data_fanju = require('../static/json/fanJu');
var data_fanju_2 = require('../static/json/fanju_2');
var comments = require('../static/json/comments');
sendMultiInfo = (req, res)=>{
res.header("Access-Control-Allow-Origin","*");
res.send(data_fanju);
};
sendMultiInfo_FanJu = (req, res)=>{
res.header("Access-Control-Allow-Origin","*");
res.send(data_fanju_2);
};
sendMultiInfo_Comments = (req, res)=>{
res.header("Access-Control-Allow-Origin","*");
res.send(comments);
};
sendMultiInfo_Video = (req, res)=>{
res.header("Access-Control-Allow-Origin","*");
res.send(data_2);
};
sendMultiInfo_TimeRank = (req, res)=>{
res.header("Access-Control-Allow-Origin","*");
res.send(data_1);
};
module.exports = {
sendMultiInfo,
sendMultiInfo_FanJu,
sendMultiInfo_Comments,
sendMultiInfo_Video,
sendMultiInfo_TimeRank
};
var dataTickets = require('../static/json/bili');
sendTicketsInfo = (req, res)=>{
res.header("Access-Control-Allow-Origin","*");
res.send(dataTickets);
};
module.exports = {
sendTicketsInfo
};
验证码登录与校验
此处验证码会随机产生一个四位数,同时在十秒之后,验证码失效,用户可以再次发送验证码
sendCode = (req, res) => {
setTimeout(function(){
validatePhoneCode = [];
},10000);
// 此处返回的是string类型的数据
let phone = req.query.phone;
//判断之前是否已经发送过验证码
if(sendCodePhone(phone)){
res.header("Access-Control-Allow-Origin","*");
res.send({
'code': 400,
'msg': '已经发送验证码,稍后再发'
})
}
else{
//随机一个验证码
let password = rand(1000,9999);
// 注意此处返回的是number类型的数据
//每发送一个,记录在validatePhoneCode
validatePhoneCode.push({
'phone': phone,
'password': password
});
res.header("Access-Control-Allow-Origin","*");
res.send({
'code': 200,
'msg': '发送成功',
'phone': phone,
'password': password
});
console.log(password);
}
};
//验证码登录
codePhoneLogin = (req, res) => {
let phone = req.query.phone;
let password = req.query.password;
// 改手机号是否已经发送过验证码
if(sendCodePhone(phone)){
//验证码和手机号是否匹配
let status = findCodeAndPhone(phone, password);
if(status==='login'){
res.header("Access-Control-Allow-Origin","*");
res.send({
'status': status,
'code': 200,
'msg': '登陆成功',
'phone':phone,
'password': password
})
}else if(status==='error'){
res.header("Access-Control-Allow-Origin","*");
res.send({
'status': status,
'code': 200,
'msg': '登陆失败',
'phone': phone,
'password':password
})
}
}else{
//未找到验证码
res.header("Access-Control-Allow-Origin","*");
res.send({
'code': 400,
'msg': '未发送验证码'
})
}
};
1.2 配置app.js
主要是采用express搭建一个服务器,监听3000端口,实现不同端口返回不同的API数据
引入需要的工具
var createError = require('http-errors');
var express = require('express');
//引入body-parser
const bodyParser = require('body-parser');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes');
var usersRouter = require('./routes/users');
var ticketsRouter = require('./routes/tickets');
var multiDataRouter = require('./routes/multiData');
配置访问的端口
var http = require('http');
var server = http.createServer(app);
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
//静态资源
app.use(express.static(path.join(__dirname, 'public')));
//允许post请求
app.use(bodyParser.urlencoded({extended: true}));
//接口在此
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/tickets',ticketsRouter);
app.use('/multiData',multiDataRouter);
2 serverForChart
配置app.js
服务端监听5000端口,用socketio来实现多人在线聊天,为下面界面提供服务
// 创建了http服务器
const http = require('http')
var fs = require('fs')
const app = http.createServer();
// 记录所有已经登录过的用户
const users = [];
app.on('request', (req, res) => {
fs.readFile(__dirname + '/index.html', function(err, data) {
if (err) {
res.writeHead(500)
return res.end('Error loading index.html')
}
res.writeHead(200)
res.end(data)
})
});
app.listen(5000, () => {
console.log('服务器启动成功')
});
// socket.emit: 告诉当前用户
// io.emit : 广播事件
const io = require('socket.io')(app);
io.on('connection', socket => {
console.log('新用户连接');
socket.on('comeIn', data => {
let user = users.find(item => item.name === data.name);
if (user) {
socket.emit('comeSuccess', { name:data.name })
}else{
users.push(data);
console.log(users);
socket.emit('comeSuccess', data);
io.emit('addUser', data);
io.emit('userList', users);
socket.name = data.name
}
});
socket.on('myDiscount',() => {
console.log("有用户离开直播间");
let idx = users.findIndex(item => item.name === socket.name);
users.splice(idx, 1);
console.log(socket.name);
console.log(users);
io.emit('delUser', {
name: socket.name,
});
io.emit('userList', users);
});
socket.on('disconnect', () => {
console.log("有用户离开浏览器");
let idx = users.findIndex(item => item.name === socket.name);
users.splice(idx, 1);
console.log(socket.name);
console.log(users);
io.emit('delUser', {
name: socket.name,
});
io.emit('userList', users);
});
socket.on('sendMessage', data => {
console.log(data);
io.emit('receiveMessage', data);
});
});
3 FRP
配置过程见下
F 服务端的跨域问题
res.header("Access-Control-Allow-Origin","*");
但是仅限于get方法获取数据,对于post方法不奏效
G vuex设置公共变量
设置一些区域需要令牌才可以访问
const store = new Vuex.Store({
state: {
//对未登录的用户进行限制一部分功能
token:'unLogin',
shift:'watching',
logo: require('../assets/img/userlogo.jpg'),
tel: undefined,
userName: undefined,
paiZi:undefined,
paiZiLevel: undefined,
userLevel: undefined,
},
});
export default {
name: "MainTabBar",
computed:{
handleShift(){
console.log(this.$store.state.shift);
return this.$store.state.shift;
}
},
methods:{
handleChange(val) {
let userToken = this.$store.state.token;
if(val==='friend'&&userToken==='unLogin'){
this.$confirm('请登录并注册会员才能访问VIP专区', '$穷人止步$', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$message({
type: 'success',
message: '请登录并注册会员'
});
this.$router.push({
name:'login'
});
}).catch(() => {
this.$message({
type: 'info',
message: '可惜可惜~~~'
});
this.$store.state.shift = 'watching'
});
}else{
this.$store.state.shift = val;
this.$router.push(val);
}
}
}
}
通过改变token来确定是否是会员
H、PWA&FRP内网穿透
1 两种技术的简介及使用它们的原因
1.1 使用FRP的原因
FRP其中一个作用是在没有公网IP的情况下让内网的网站让外网访问,内网穿透!!!内网穿透工具有很多,其中 Frp (Fast Reverse Proxy) 是比较流行的一款。FRP 是一个免费开源的用于内网穿透的反向代理应用,它支持 TCP、UDP 协议, 也为 http 和 https 协议提供了额外的支持。你可以粗略理解它是一个中转站,帮你实现 公网 ←→ FRP(服务器) ←→ 家庭内网
的连接,让内网里的设备也可以被公网访问到。
一些参考
参考网页:https://xuebin.me/posts/fc2d1561.html
Vue和脚手架集成了PWA,正合我意,PWA可以离线访问一些历史!!!
https://www.bilibili.com/video/BV13E411e7kh?p=15
1.2 使用PWA的原因
当然还有更多!
1.3 跟项目的关系
显然我开发的是一款仿bilibili的视频软件。用户在观看视频、文章、直播的时候,自然是想要最新的数据。这就需要不断访问网络,刷新界面后,访问网络,再刷新界面,再访问网络。好的,那么离线状态下,用户还能访问上一次的旧数据吗?,假如看到的全是空白的界面,势必让用户体验不好。
所以,PWA一个主要的功能就是可以解决这个问题!!!
2 开始配置(PWA)
pwa几个核心功能
-
Web App Manifest
-
Service Worker
-
Cache API 缓存
-
Push&Notification 推送与通知
-
Background Sync 后台同步
-
响应式设计
先配置manifest.json文件并在index.html文件中加以引用
在index.html引用该json文件
配置名字,简写名字,小图标的图片的存放位置、大小、类型,主题颜色,运行chrome浏览器我们按下F12可以看到之前配置的效果,
可以点击Add to homescreen这个链接,图标将会安装至桌面
直接进入主页,下面是在浏览器上运行,对比另一张图是安装至桌面上的应用打开
如何离线缓存,使用pwa技术,先得注册service worker,按以下步骤进行
- 配置registerServiceWorker.js
window.addEventListener('load',() => {
if('serviceWorker' in navigator){
navigator.serviceWorker.register('./sw.js').then(res=>{
console.log(res);
}).catch(err=>{
console.log(err);
})
}
});
//告诉用户联网情况
window.addEventListener("offline", ()=>{
new Notification('提示', {
body: '快联网!我等不急了!'
})
});
window.addEventListener('online', () => {
new Notification('提示', {
body: '有网络还不刷新?'
})
});
-
配置./sw.js文件
说明:这里配置了两种方式,一个是缓存优先,那么,每次获取的数据是先拿之前缓存下的数据进行显示。还有一个是网络有限,也就是有网络的情况下,先获取网络get到的数据,将其显示,若是没有网络,则显示缓存下的数据。
const CACHE_NAME = "Version_0.1.0";
const urls = [
'/icon.ico',
'/index.html',
'/icon.png',
'/manifest.json',
];
self.addEventListener('install',async event =>{
const cache = await caches.open(CACHE_NAME);
await cache.addAll(urls);
await self.skipWaiting()
});
self.addEventListener('activate', async event => {
const keys = await caches.keys();
keys.forEach(key => {
if (key !== CACHE_NAME) {
caches.delete(key)
}
});
await self.clients.claim()
});
self.addEventListener('fetch', async event => {
const req = event.request;
const url = new URL(req.url);
if(url.origin!==self.origin){
console.log('这是一个新的版本!')
return
}
//此处
// if(req.url.include('/api')){
event.respondWith(networkFirst(req))
// }else{
// event.respondWith(cacheFirst(req))
// }
});
async function cacheFirst(req){
const cache = await caches.open(CACHE_NAME)
const cached = await cache.match(req)
if(cached) {
return cached
}else{
const fresh = await fetch(req)
return fresh
}
}
async function networkFirst(req) {
const cache = await caches.open(CACHE_NAME)
try{
const fresh = await fetch(req);
await cache.put(req, fresh.clone());
return fresh;
} catch (e) {
const cache = await caches.open(CACHE_NAME)
const cached = await cache.match(req)
return cached
}
}
针对我这个项目的使用情况,基本上是网络请求优先。但是也有些静态的资源需要提前缓存下来,在下一次读取数据的时候就不需要再去请求网络服务。更奇特的功能是PWA能实现在离线情况下也能显示之前缓存下来的数据。
一些能使用户体验优化的通知
当网络断开和连接的时候是需要给用户一些提示Notification
<script>
if (Notification.permission === 'default') {
Notification.requestPermission().then(r => {
console.log('相关功能需要通知权限')
})
}
</script>
window.addEventListener("offline", ()=>{
new Notification('提示', {
body: '快联网!我等不急了!'
})
});
window.addEventListener('online', () => {
new Notification('提示', {
body: '有网络还不刷新?'
})
});
3 server服务器与数据库Mysql连接
用express搭建服务器(Node+Express+Mysql搭建API接口平台)
3.1 下载express
3.2 下载宝塔软件
监听端口
安装在当前文件下mysql
服务端配置dbconfig.js文件
为了获取数据,需要配置index.js
var express = require('express');
var router = express.Router();
var dbconfig = require('../util/dbconfig');
/* GET home page. */
router.get('/', function(req, res, next) {
// res.render('index', { title: 'Express' });
var sql = "select * from cate"
var sqlArr = [];
var callBack = (err,data)=>{
if(err){
console.log('连接出错');
} else{
res.send({
'list': data
})
}
};
dbconfig.sqlConnect(sql, sqlArr, callBack);
});
module.exports = router;
搭建一个服务器,使用工具Navicat for Mysql,连接它
前面测试之后,事实证明,这是可以建立服务的。那么接下来转到我项目所在的根目录下进行如上面的操作。配置基础环境
express server
4 FRP内网穿透技术
这里的服务器搭建之后只能在局域网内访问,那么想要外网也能访问,就要使用内网穿透技术。当然我了解到其实也可以使用外网的一些服务器,也可以实现外网访问内网的网页。
我是用vue开发的项目,那么就要将本地的项目定为FRPC,而购买了阿里云的服务器,那么将其定为FRPS,同时我会设置token,也就是密码。同时,FRP需要从github上下载对应的代码,对本地的项目做映射,当外网访问公网IP加对应的端口时,其实就是对应映射至我本地的项目,其中任何一环都不能断开连接,所以要保证本地的服务是一直开着的。最后,FRP是跨平台的,有linux、win版本的。
4.1 购买云服务器(阿里云服务器-学生优惠)
需要一台云服务器,本人在阿里云上购买了一个云服务器
显然我已经获得了一个公网IP地址
4.2 创建密钥对和绑定密钥对
4.3 重启实例
4.4 测试一下是否连接成功
4.5 FinalShell连接管理器
4.6 秘钥登录之后,就进入后台
4.7 下载frp
https://github.com/fatedier/frp/releases
4.8 将文件放到root目录下
4.9 修改配置文件
在这里插入图片描述
4.10 运行脚本+之前的配置文件
配置客户端
4.11 配置service
查看是否有该进程在运行
4.12 配置客户端
显然,在上面的步骤中,我已经将服务端建立起来,那么接下来我要建立的是客户端frpc,由于我这是vuecli建立起来的一个本地的项目,电脑是win10家庭版,所以首先下载对应版本的frpc包
把它解压至cmd运行环境下面
进入之后,配置frpc.ini文件,配置一些端口和token的验证
4.13 整个frp工作原理
4.14 开始吧!
4.15 访问网址http://47.98.245.159:8082/
I WebSocket socket.io技术实现一个直播间聊天室
1 用websocket的原因
搭建一个聊天的服务器,这是必须的,比如XX学生给YY老师发送一条消息,那么不是直接就发送给郭鸣老师,而是发送给服务器发送一个请求。
**websocket实现了浏览器与服务器全双工通信,允许服务器主动发送消息给客户端。最重要点,websocket是一种持久的协议,而假设用http是非持久协议。**websocket性能也比ajax轮循性能要高。
综上所述,websocket是一种网络协议,允许客户端和服务端双向发送信息,即全双工的进行网络通讯。
2 开始配置websocket
2.1 安装依赖包
yarn add node.js-websocket
2.2 基本配置过程
浏览器做服务端,向本人搭建的一个服务端发送数据,以及请求数据,客户端的核心代码如下:
serveChart_webSocket(){
var input = document.querySelector('input');
var sendMessage = document.querySelector('#send');
var div = document.querySelector('#chartContent');
var socket = new WebSocket('ws://localhost:4000');
// open:当websocket服务连接成功时候触发
socket.addEventListener('open',function () {
div.innerHTML = '连接服务成功了'
});
// 主动的给websocket服务发送消息
sendMessage.addEventListener('click',function () {
if(input.value !== ''){
socket.send(input.value);
}else{
console.log('不能发送空消息')
}
input.value = ''
});
socket.addEventListener('message',function (e) {
console.log(e.data);
var dv = document.createElement('div');
dv.innerText = e.data;
dv.className = 'newDivClass';
div.appendChild(dv);
});
socket.addEventListener('close',function () {
div.innerHTML = '服务断开'
})
},
2.3 服务端的配置
服务端处理的逻辑主要是用户链接该服务器后,向该服务器下的所有用户都发送一条通知,而单个用户发送消息,也就是向服务器发送消息,服务器再注册事件来接收消息,最终将消息再一次发送给所有用户。最后是用户在断开连接,服务器注册一个close事件,便会接收用户断开消息,于是会想所有用户发送“某用户离开的”的消息。客户端也就是每个用户会去注册一个接收消息的监听器,接收来自服务器的任何消息。从而实现多人在同一个直播间的聊天功能。
服务端配置的核心代码,如下
const ws = require('nodejs-websocket');
let count = 0;
const server = ws.createServer(conn=>{
count++;
console.log('新的连接');
//告诉所有用户有人加入聊天室
conn.userName = '用户'+ count;
broadcast(conn.userName + '进入了直播间');
conn.on('text',data => {
//当接收某个消息的时候,告诉所有用户,发送的消息内容是什么
broadcast(data);
});
//链接关闭的时候,触发
conn.on('close',data=>{
console.log('关闭连接');
count--;
broadcast(conn.userName + '离开了直播间')
});
//链接错误的时候,触发
conn.on('error',data=>{
console.log('出现错误');
})
});
// 能实现给所有用户发送消息的函数
var broadcast = (msg) => {
server.connections.forEach(item => {
item.send(msg)
})
};
server.listen(4000,()=>{
console.log('链接成功');
});
3 使用socket.io
该框架也是做做服务端和客户端之间的通讯。基本的操作是浏览器注册事件、服务端发出数据;反之。服务端注册时间,浏览器发出数据。如命令为io.emit和io.on.相比较于websocket,socket.io会更方便些。
配置一些依赖包
yarn add socket.io express
npm install --save socket.io
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js"></script>
首先
// socket.emit: 告诉当前用户
// io.emit : 广播事件,告诉所有用户
服务端配置app.js
// 创建了http服务器
const http = require('http')
var fs = require('fs')
const app = http.createServer();
// 记录所有已经登录过的用户
const users = [];
app.on('request', (req, res) => {
fs.readFile(__dirname + '/index.html', function(err, data) {
if (err) {
res.writeHead(500)
return res.end('Error loading index.html')
}
res.writeHead(200)
res.end(data)
})
});
app.listen(5000, () => {
console.log('服务器启动成功')
});
// socket.emit: 告诉当前用户
// io.emit : 广播事件
const io = require('socket.io')(app);
io.on('connection', socket => {
console.log('新用户连接');
socket.on('comeIn', data => {
let user = users.find(item => item.name === data.name);
if (user) {
socket.emit('loginError', { msg: '登录失败' })
}else{
users.push(data);
console.log(users);
socket.emit('comeSuccess', data);
io.emit('addUser', data);
io.emit('userList', users);
socket.name = data.name
}
});
socket.on('disconnect', () => {
console.log("有用户离开");
let idx = users.findIndex(item => item.name === socket.name);
users.splice(idx, 1);
console.log(socket.name);
console.log(users);
io.emit('delUser', {
name: socket.name,
});
io.emit('userList', users);
});
socket.on('sendMessage', data => {
console.log(data);
io.emit('receiveMessage', data);
});
});
利用之前存储在数据库的数据,可以凭借电话号码来查询用户的其他信息。
客户端配置对应的接收事件
serveChart_socketIo(){
this.socket = io('http://localhost:5000');
var div = document.querySelector('#chartContent');
this.socket.emit('comeIn',{
// name: this.randomString(6)
name: this.$store.state.userName
});
this.socket.on('comeSuccess', data =>{
let dv = document.createElement('div');
dv.innerText = '连接服务器成功......';
dv.className = 'newDivClass';
div.appendChild(dv);
this.$store.state.userName = data.name;
});
this.socket.on('loginError', function() {
alert('用户名存在')
});
this.socket.on('addUser', data => {
let dv = document.createElement('div');
dv.innerText = '用户'+data.name+'进入了直播间';
dv.className = 'newDivClass';
div.appendChild(dv);
});
this.socket.on('userList', data =>{
console.log("当前用户列表"+data);
});
this.socket.on('delUser', data => {
let dv = document.createElement('div');
dv.innerText = '有用户退出了直播间';
dv.className = 'newDivClass';
div.appendChild(dv);
console.log("退出了--"+data);
});
this.socket.on('receiveMessage', data => {
console.log("我收到消息"+data);
this.commentsList.push(data);
})
},
sendMessage(){
var input = document.querySelector('input');
if(input.value!==''){
this.socket.emit('sendMessage', {
paiZi: this.$store.state.paiZi,
paiZiLevel: this.$store.state.paiZiLevel,
userLevel:'UL ' + this.$store.state.userLevel,
name: this.$store.state.userName,
say: ":"+input.value
});
input.value = ''
}else{
console.log('不能发送空消息');
this.$message('不能发送空消息');
}
},
更新本地的用户信息
getUserInfo(tel){
getUserInfo(tel).then(res => {
this.userData = res.user[0];
console.log("key3"+this.userData.userName);
this.$store.state.userName = this.userData.userName;
this.$store.state.paiZi = this.userData.paiZi;
this.$store.state.paiZiLevel = this.userData.paiZiLevel;
this.$store.state.userLevel = this.userData.userLevel;
alert(this.$store.state.userName);
alert(this.$store.state.paiZi);
alert(this.$store.state.paiZiLevel);
alert(this.$store.state.userLevel);
})
},
J Koa实现服务端J
略
K、作品地址
1 GitHub 网址
2 git操作的记录
L 解决的困难
1 地址传参找不着
问题:当我从一个页面跳转到另一个页面时,想要传递第一个参数,但是是到另一个页面竟然显示undefined,这个错误是不能直接调用this.$route.query.id,而是要将调用数据之前,现将其写在一个方法里面才行,同时写在钩子里面(mounted)
解决办法:点击进入
解释:复用相同的页面也就是相同的route, 然后传入的参数不同, vue并不会重新发起请求, 我尝试加了 watch 监控参数变化, 重新调用获取数据方法, 但是实际的结果是 传入query执行了一次获取数据, 又执行了一次默认参数获取数据, 最终结果就是用默认页面的数据把我想要的数据覆盖掉了.
getQueryId(){
console.log(this.$route.query.id);
}
mounted() {
this.getQueryId();
}
this.$router.push({
name:'login',
query:{
id:'/friend'
}
});
2 只有自己用户接收到数据
[socket.io] 在设计点击发送信息后,其他用户接收到数据,但是只有自己接收到数据,最终我发现原来是我将接收数据的函数放在了只有点击才会触发的位置,也许越做越糊涂了吧…
3 执行的顺序
在存入数据和从数据库取数据,需要promise对象来控制先后的顺序。否则会造成数据还没存储,就把数据读取出来了。
最终采用了箭头函数,延迟执行
this.getUserInfo(this.$store.state.tel);
window.setTimeout(()=>{
if(this.$store.state.userName===undefined){
this.$confirm('请完善信息', '(#`O′)', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$message({
type: 'success',
message: '让我更了解你吧~~~'
});
this.$router.push({
name:'info',
// query:{
// id:'/watching/onlineShow'
// }
});
}).catch(() => {
this.$message({
type: 'info',
message: '为什么不让我了解你?'
});
this.$router.go(-3);
});
}else{
this.serveChart_socketIo();
// this.serveChart_webSocket();
}
},1000);
M 数据库优化搜索
trigger
N 函数式编程FRP
vue函数式组件
函数式编程—改造代码
server
const R = require('ramda');
//随机一个登陆的验证码
const rand = (min, max) => {
return Math.floor(Math.random() * (max - min)) + min
};
//验证手机是否已经发送验证码
var validatePhoneCode = [];
const handleLife = () => R.map(item => item.lifeTime--, validatePhoneCode);
const validateTel = x => x.lifeTime !== 0;
const handleLifeOut = () => {
validatePhoneCode = R.filter(validateTel,validatePhoneCode);
};
const handleValidatePhoneCode = () =>{
R.forEach(item => console.log("电话"+item.phone+"验证码"+item.password+"验证码有效时间为"+item.lifeTime), validatePhoneCode);
};
const update = R.pipe(
handleLife,
handleLifeOut,
handleValidatePhoneCode
);
setInterval(function(){
update();
},1000);
let sendCodePhone = (phone) =>{
var result = false;
validatePhoneCode.map(item => {
result = (phone === item.phone);
});
return result;
};
let findCodeAndPhone = (phone, password) => {
let ans = 'error';
validatePhoneCode.map(item=>{
if((phone==item.phone)&&(password==item.password)){
ans = 'login';
}
});
return ans;
// for(let item of validatePhoneCode){
// // console.log(typeof(item.phone));
// // console.log(typeof(phone));
// // console.log(item.phone===phone);
// // console.log(typeof(item.password));
// // console.log(typeof(password));
// // console.log(item.password===password);
// // console.log((phone==item.phone)&&(password==item.password));
// // console.log((phone===item.phone)&&(password===item.password));
// //不要使用===,否则会出现类型不同而数值相同,却返回false的情况
// if((phone==item.phone)&&(password==item.password)){
// return 'login';
// }
// }
};
//模拟验证码发送接口
sendCode = (req, res) => {
// 此处返回的是string类型的数据
let phone = req.query.phone;
//判断之前是否已经发送过验证码
if(sendCodePhone(phone)){
res.header("Access-Control-Allow-Origin","*");
res.send({
'code': 400,
'msg': '已经发送验证码,稍后再发'
})
} else{
//随机一个验证码
let password = rand(1000,9999);
// 注意此处返回的是number类型的数据
//每发送一个,记录在validatePhoneCode
validatePhoneCode.push({
'phone': phone,
'password': password,
'lifeTime':10
});
res.header("Access-Control-Allow-Origin","*");
res.send({
'code': 200,
'msg': '发送成功',
'phone': phone,
'password': password
});
console.log(password);
}
};
//验证码登录
codePhoneLogin = (req, res) => {
let phone = req.query.phone;
let password = req.query.password;
// 该手机号是否已经发送过验证码
if(sendCodePhone(phone)){
//验证码和手机号是否匹配
let status = findCodeAndPhone(phone, password);
if(status==='login'){
res.header("Access-Control-Allow-Origin","*");
res.send({
'status': status,
'code': 200,
'msg': '登陆成功',
'phone':phone,
'password': password
})
}else if(status==='error'){
res.header("Access-Control-Allow-Origin","*");
res.send({
'status': status,
'code': 200,
'msg': '登陆失败',
'phone': phone,
'password':password
})
}
}else{
//未找到验证码
res.header("Access-Control-Allow-Origin","*");
res.send({
'code': 400,
'msg': '未发送验证码'
})
}
};
module.exports = {
sendCode,
codePhoneLogin,
};
O sension storage
用户打开浏览器之后,当用户输入电话号码,存储用户电话号码,当用户退出当前账号时,并且有打开登录界面,那么,就查询session storage,获取之前登陆的电话号码,自动填入框内。
1、当用户输入电话号码之后,记录
var storage = window.sessionStorage;
storage.setItem('tel', this.$store.state.tel);
2、退出登录之后,用户重新登录
var storage = window.sessionStorage;
alert(storage.getItem("tel"));
this.tel = storage.getItem("tel");
P 界面颜色优化及样式优化
所有界面
Q 服务端模块化开发
见代码
R 为了更好的用户体验–骨架屏
setTimeout(()=>{
this.loading = false
},3000);
<van-skeleton title avatar :row="2" :loading="loading">
<img :src="cmt.member.avatar" alt="" class="avatar">
<span class="name">{{cmt.member.uname}}</span>
<span class="ico" >
<i class="el-icon-star-off" aria-hidden="true" @click="star(index)"></i> {{cmt.like}}
</span>
<div class="commentContent" v-longPress="OpenBottomSheet">{{cmt.content.message}}</div>
</van-skeleton>
S 代码风格标准
npm install standard --global
standard --fix
T 项目分工表
姓名 | 学号 | 班级 | 任务 | 权重 |
---|---|---|---|---|
王洋俊 | 31701392 | 软件工程1703 | 整个项目的制作及答辩报告的撰写 | 1.0 |
U 项目自我评估表
函数式编程 | xhr提取api | FRP | session Storage | 响应式设计 | 其他 |
---|---|---|---|---|---|
有 | axios | 有 | 有 | 有 | 有 |
我没有使用AJAX,而是使用了axios,相比ajax,这个来获取API要简单、便捷。下面是一部分代码:
import axios from 'axios'
export function request(config) {
// 1.创建axios的实例
const instance = axios.create({
baseURL: 'http://localhost:3000/',
timeout: 10000,
});
// 2.axios的拦截器
// 2.1.请求拦截的作用
instance.interceptors.request.use(config => {
return config
}, err => {
// console.log(err);
});
// 2.2.响应拦截
instance.interceptors.response.use(res => {
return res.data
}, err => {
console.log(err);
});
// 3.发送真正的网络请求
return instance(config)
}
V 自我评价
满分,我认为我这个作品非常的有水平,同时我也投入了大量精力,也收获了许多新的知识,特别是设计一个作品应具备什么样的思维、设计方法,这个是很重要的。
W 参考资料
更多推荐
所有评论(0)