├── .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 | 
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 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
2 |
3 |
{{ msg }}
4 |
5 | For a guide and recipes on how to configure / customize this project,
6 | check out the
7 | vue-cli documentation.
8 |
9 |
Installed CLI Plugins
10 |
14 |
Essential Links
15 |
22 |
Ecosystem
23 |
30 |
31 |
32 |
33 |
41 |
42 |
43 |
59 |
--------------------------------------------------------------------------------
/src/components/vue-chat.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
我的编号是:{{my_id}}
4 |
5 |
6 |
7 |
12 |
13 |
14 |
15 |
16 |
22 | -
23 |
24 |

25 |
26 | {{item}}
27 |
28 |
29 |
30 |
31 |
32 |
36 |
37 |
38 |
39 |
40 | -
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
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 | }
--------------------------------------------------------------------------------