├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── 1.gif ├── favicon.ico └── index.html ├── server.js └── src ├── App.vue ├── assets └── logo.png ├── components ├── HelloWorld.vue └── vue-chat.vue ├── main.js ├── store.js └── util ├── cursor.js └── socket.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended' 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 13 | }, 14 | parserOptions: { 15 | parser: 'babel-eslint' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-chat 2 | 3 | > 本人对vue和原生js理解不是特别透彻,所以该代码可能有写不合理的地方 4 | 5 | ## 安装所需环境 6 | ``` 7 | npm install 8 | ``` 9 | 10 | ### 启动node建议websocket服务器 11 | ``` 12 | node server.js 13 | ``` 14 | 15 | ### 启动项目 16 | ``` 17 | npm run serve 18 | ``` 19 | 20 | [我的Git](https://github.com/JeasonLaung/vue-chat.git). 21 | 22 | ## 展示gif 23 | ![Image text](https://github.com/JeasonLaung/vue-chat/blob/master/public/1.gif) 24 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-chat", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "clipboard": "^2.0.4", 12 | "node-sass": "^4.11.0", 13 | "nodejs-websocket": "^1.7.2", 14 | "sass-loader": "^7.1.0", 15 | "vue": "^2.5.21", 16 | "vuex": "^3.0.1" 17 | }, 18 | "devDependencies": { 19 | "@vue/cli-plugin-babel": "^3.2.0", 20 | "@vue/cli-plugin-eslint": "^3.2.0", 21 | "@vue/cli-service": "^3.2.0", 22 | "babel-eslint": "^10.0.1", 23 | "eslint": "^5.8.0", 24 | "eslint-plugin-vue": "^5.0.0", 25 | "vue-template-compiler": "^2.5.21" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeasonLaung/vue-chat/ff5fc348327032d683770d3b19d2ad1fb6818d0a/public/1.gif -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeasonLaung/vue-chat/ff5fc348327032d683770d3b19d2ad1fb6818d0a/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-chat 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // npm i nodejs-websocket 2 | let ws = require("nodejs-websocket") 3 | 4 | //str转json 5 | let json = str => { 6 | return (new Function('return '+str))() 7 | } 8 | //json转str 9 | let str = json =>{ 10 | return JSON.stringify(json) 11 | } 12 | 13 | // 实时聊天人数数组,通过随机数生成 14 | let conns = [], 15 | message_welcome = {}, 16 | message_member = {}, 17 | message_between = {}, 18 | heart_beat = 9999, //每8秒心跳一次,否则短线 19 | message_last = '' 20 | 21 | let server = ws.createServer(function (conn) { 22 | //计算心跳时间 23 | conn.heart_time = 0 24 | let timer = setInterval(()=>{ 25 | if (conn.heart_time > heart_beat) { 26 | clearInterval(timer); 27 | conn.close() 28 | } 29 | conn.heart_time++ 30 | },1000) 31 | //根据时间戳生成用户id uid 32 | let uid = str((new Date()).getTime()).slice(-6) 33 | //保存用户id在全局数组conns中方便我们处理聊天对象信息 34 | conns[uid] = conn 35 | message_welcome = {'my_id':uid,type:'welcome'} 36 | conn.sendText(str(message_welcome)) 37 | 38 | //如果有新的人员加入,广播数据给全部人 39 | message_member = {'members':Object.keys(conns),type:'member'} 40 | // if (message) {} 41 | for(var i in conns){ 42 | conns[i].sendText(str(message_member)) 43 | } 44 | 45 | //接受到发过来的信息 46 | conn.on("text", function (text) { 47 | //重置心跳 48 | conn.heart_time = 0 49 | //判断发给谁 50 | console.log(text) 51 | let data = json(text), 52 | to = data['to'], 53 | from = uid, 54 | msg = data['msg'] 55 | //存在发送的对象 56 | console.log(str(Object.keys(conns)),to) 57 | if (Object.keys(conns).indexOf(to) != -1) { 58 | message_between = {type:'chat','from':from,'to':to,'msg':msg} 59 | console.log(str(message_between)) 60 | //发给别人 61 | conns[from].sendText(str(message_between)) 62 | //发给自己 63 | conns[to].sendText(str(message_between)) 64 | } 65 | }) 66 | //断开连接的回调 67 | conn.on("close", function (code, reason) { 68 | //删除成员信息 69 | delete conns[uid] 70 | //广播 71 | message = {'members':Object.keys(conns),type:'member'} 72 | for(var i in conns){ 73 | conns[i].sendText(str(message_member)) 74 | } 75 | }) 76 | 77 | //处理错误事件信息 78 | conn.on('error', function (err) { 79 | //异常conn就直接删除 80 | conn.close() 81 | delete conns[uid] 82 | }) 83 | }).listen(8001);//8001端口 84 | 85 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 30 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeasonLaung/vue-chat/ff5fc348327032d683770d3b19d2ad1fb6818d0a/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 41 | 42 | 43 | 59 | -------------------------------------------------------------------------------- /src/components/vue-chat.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 254 | 255 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import store from './store' 4 | 5 | Vue.config.productionTip = false 6 | 7 | new Vue({ 8 | store, 9 | render: h => h(App) 10 | }).$mount('#app') 11 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | // str转json 6 | let json = str => { 7 | return (new Function('return '+str))() 8 | } 9 | //json转str 10 | let str = json =>{ 11 | return JSON.stringify(json) 12 | } 13 | export default new Vuex.Store({ 14 | state: { 15 | WS:false,//websocket对象 16 | WS_URL:'ws://127.0.0.1:8001', 17 | WS_RECONNECT:false,//自动重连 18 | // HEART_BEAT:5,//每50秒心跳一次 19 | HEART_MSG:'hello', 20 | }, 21 | mutations: { 22 | SOCKET_INIT(state,config={}){ 23 | if (!state.WS) { 24 | state.WS = new WebSocket(state.WS_URL) 25 | } 26 | for(var i in config){ 27 | state[i] = config[i] 28 | } 29 | }, 30 | SOCKET_MESSAGE(state,func){ 31 | return new Promise((resolve,reject)=>{ 32 | state.WS.onmessage=(data)=>{ 33 | // data = (new Function('return '+data))() 34 | resolve(func(json(data.data))) 35 | } 36 | //这个有问题,虽然很方便,但还是搁置 37 | // state.WS.addEventListener('message',data=>{resolve(func(data))}) 38 | }) 39 | }, 40 | SOCKET_SEND(msg){ 41 | state.WS.send(str(msg)) 42 | }, 43 | SOCKET_HEART(state){ 44 | //设置心跳 45 | if (state.HEART_BEAT > 0) { 46 | let timer = setInterval(()=>{ 47 | state.WS.send(state.HEART_MSG) 48 | //如果已经关闭就停止 49 | if([2,3].indexOf(state.WS.readyState) !== -1){ 50 | clearInterval(timer) 51 | } 52 | },parseInt(state.HEART_BEAT)*1000) 53 | } 54 | } 55 | }, 56 | getters:{ 57 | 58 | }, 59 | actions: { 60 | SOCKET_INIT({state,commit},func){ 61 | return new Promise((resolve,reject)=>{ 62 | commit('SOCKET_INIT') 63 | state.WS.onerror = function (msg) { 64 | reject(msg) 65 | } 66 | 67 | let timer = setInterval(()=>{ 68 | //不是正在连接状态 69 | if(state.WS.readyState!==0){ 70 | clearInterval(timer) 71 | //正在关闭,关闭 72 | if([2,3].indexOf(state.WS.readyState) !== -1){ 73 | state.WS = false 74 | } 75 | //连接 76 | if(state.WS.readyState===1){ 77 | resolve(state.WS) 78 | commit('SOCKET_HEART') 79 | commit('SOCKET_MESSAGE',func) 80 | } 81 | } 82 | },100) 83 | }) 84 | }, 85 | // SOCKET_MESSAGE({state,commit},func){ 86 | // commit('SOCKET_MESSAGE',func) 87 | // } 88 | } 89 | }) 90 | -------------------------------------------------------------------------------- /src/util/cursor.js: -------------------------------------------------------------------------------- 1 | //获取当前光标位置 2 | const getCursorPosition = function (element) { 3 | var caretOffset = 0; 4 | var doc = element.ownerDocument || element.document; 5 | var win = doc.defaultView || doc.parentWindow; 6 | var sel; 7 | if (typeof win.getSelection != "undefined") {//谷歌、火狐 8 | sel = win.getSelection(); 9 | if (sel.rangeCount > 0) {//选中的区域 10 | var range = win.getSelection().getRangeAt(0); 11 | var preCaretRange = range.cloneRange();//克隆一个选中区域 12 | preCaretRange.selectNodeContents(element);//设置选中区域的节点内容为当前节点 13 | preCaretRange.setEnd(range.endContainer, range.endOffset); //重置选中区域的结束位置 14 | caretOffset = preCaretRange.toString().length; 15 | } 16 | } else if ((sel = doc.selection) && sel.type != "Control") {//IE 17 | var textRange = sel.createRange(); 18 | var preCaretTextRange = doc.body.createTextRange(); 19 | preCaretTextRange.moveToElementText(element); 20 | preCaretTextRange.setEndPoint("EndToEnd", textRange); 21 | caretOffset = preCaretTextRange.text.length; 22 | } 23 | console.log(caretOffset) 24 | return caretOffset; 25 | } 26 | 27 | //输入框获取光标 28 | const getPosition = function (element) { 29 | let cursorPos = 0; 30 | if (document.selection) {//IE 31 | var selectRange = document.selection.createRange(); 32 | selectRange.moveStart('character', -element.value.length); 33 | cursorPos = selectRange.text.length; 34 | } else if (element.selectionStart || element.selectionStart == '0') { 35 | cursorPos = element.selectionStart; 36 | } 37 | return cursorPos; 38 | } 39 | 40 | //设置光标位置 41 | const setCursorPosition = function (element, pos) { 42 | var range, selection; 43 | if (document.createRange)//Firefox, Chrome, Opera, Safari, IE 9+ 44 | { 45 | range = document.createRange();//创建一个选中区域 46 | range.selectNodeContents(element);//选中节点的内容 47 | if(element.innerHTML.length > 0) { 48 | range.setStart(element.childNodes[0], pos); //设置光标起始为指定位置 49 | } 50 | range.collapse(true); //设置选中区域为一个点 51 | selection = window.getSelection();//获取当前选中区域 52 | selection.removeAllRanges();//移出所有的选中范围 53 | selection.addRange(range);//添加新建的范围 54 | } 55 | else if (document.selection)//IE 8 and lower 56 | { 57 | range = document.body.createTextRange();//Create a range (a range is a like the selection but invisible) 58 | range.moveToElementText(element);//Select the entire contents of the element with the range 59 | range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start 60 | range.select();//Select the range (make it the visible selection 61 | } 62 | } 63 | 64 | 65 | // 设置光标位置 66 | function setPosition(textDom, pos){ 67 | if(textDom.setSelectionRange) { 68 | // IE Support 69 | textDom.focus(); 70 | textDom.setSelectionRange(pos, pos); 71 | }else if (textDom.createTextRange) { 72 | // Firefox support 73 | var range = textDom.createTextRange(); 74 | range.collapse(true); 75 | range.moveEnd('character', pos); 76 | range.moveStart('character', pos); 77 | range.select(); 78 | } 79 | } 80 | 81 | function getSelectedContents(){ 82 | if (window.getSelection) { //chrome,firefox,opera 83 | var range=window.getSelection().getRangeAt(0); 84 | var container = document.createElement('div'); 85 | container.appendChild(range.cloneContents()); 86 | return container.innerHTML; 87 | //return window.getSelection(); //只复制文本 88 | } 89 | else if (document.getSelection) { //其他 90 | var range=window.getSelection().getRangeAt(0); 91 | var container = document.createElement('div'); 92 | container.appendChild(range.cloneContents()); 93 | return container.innerHTML; 94 | //return document.getSelection(); //只复制文本 95 | } // by www.jquerycn.cn 96 | else if (document.selection) { //IE特有的 97 | return document.selection.createRange().htmlText; 98 | //return document.selection.createRange().text; //只复制文本 99 | } 100 | } 101 | export default { 102 | getCursorPosition, 103 | getPosition, 104 | setCursorPosition, 105 | setPosition, 106 | getSelectedContents 107 | } 108 | 109 | // handleCopy(e){ 110 | // // console.log(window.getSelection().toString()) 111 | // e.clipboardData.setData('text',window.getSelection().toString()) 112 | // return false 113 | // // .focusNode.innerHTML 114 | // }, 115 | // getCursorPosition(ele){ 116 | // util.getCursorPosition(ele) 117 | // }, 118 | // tell(e){ 119 | // var that = this; 120 | // var startPosition = util.getCursorPosition(e.target); 121 | // var header = e.target.innerHTML.slice(0,startPosition); 122 | // var footer = e.target.innerHTML.slice(startPosition,-1); 123 | // for (var i = 0; i < e.clipboardData.items.length; i++) { 124 | // var item = e.clipboardData.items[i] 125 | // if (item.kind === "string") { 126 | // item.getAsString(function (str) { 127 | // // str 是获取到的字符串 128 | // var text = that.HTML2Text(str), 129 | // len = text.length 130 | // e.target.innerHTML = header + text + footer 131 | // util.setCursorPosition(e.target,(header + text).length) 132 | 133 | // }) 134 | // } else if (item.kind === "file") { 135 | // var pasteFile = item.getAsFile(); 136 | // // pasteFile就是获取到的文件 137 | // } 138 | // } 139 | // return false 140 | // }, -------------------------------------------------------------------------------- /src/util/socket.js: -------------------------------------------------------------------------------- 1 | export default class socket{ 2 | constructor(url,reconnect=false) { 3 | this.url = url 4 | this.ws = null 5 | this.config = { 6 | closeByHand:false, 7 | //自动重连 8 | reconnect, 9 | 10 | } 11 | } 12 | open(){ 13 | return new Promise((resolve,reject) => { 14 | // console.log(this.ws) 15 | if (this.ws === null) { 16 | this.ws = new WebSocket(this.url) 17 | // resolve(this.ws) 18 | // console.log(this.ws) 19 | this.ws.open = (e) => { 20 | resolve(this); 21 | } 22 | this.ws.onerror = (e) => { 23 | reject(e); 24 | } 25 | this.ws.onclose = (e) => { 26 | this.ws = null 27 | if (!this.closeConfig.closing) { 28 | console.log('reconnect') 29 | if (this.config.reconnect) { 30 | this.open() 31 | } 32 | } 33 | // 若手动close,恢复初始状态 34 | this.config.closeByHand = false; 35 | } 36 | } 37 | }) 38 | 39 | } 40 | 41 | 42 | close(){ 43 | this.config.closeByHand = true 44 | this.ws.close() 45 | this.ws = null 46 | } 47 | send(content){ 48 | this.ws.send(content) 49 | } 50 | } --------------------------------------------------------------------------------