├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── app └── websocket.php ├── index.html ├── package.json ├── src ├── App.vue ├── Auth.vue ├── assets │ ├── bg.jpg │ └── logo.png ├── components │ ├── List.vue │ ├── Message.vue │ ├── Notice.vue │ ├── Text.vue │ └── User.vue ├── main.js └── vuex │ ├── actions.js │ ├── getters.js │ └── store.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"], 3 | "plugins": ["transform-runtime"], 4 | "comments": false 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 翁航 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swoole-vue-webim 2 | 3 | 这是一个`Web`版的聊天应用,前端基于`Vue`来构建,用`Vuex`来进行状态管理,`webpack`构建;服务端通过`Swoole`来实现。基本功能有单聊、群聊、用户状态、消息状态以及通知信息。 4 | 5 | 6 | 7 | # 如何部署? 8 | 9 | ## 1.前期准备 10 | 11 | 需要安装`npm`和`Swoole`,其中还要安装`webpack`来作为构建工具,所以你需要拥有`Linux`系统。具体安装过程可以查看相关工具的文档。 12 | 13 | 14 | 15 | ## 2.构建项目 16 | 17 | 切换到项目目录,安装相关依赖: 18 | 19 | ``` 20 | npm install 21 | ``` 22 | 23 | 构建项目: 24 | 25 | ``` 26 | npm run build 27 | ``` 28 | 29 | 30 | 31 | ## 3.启动服务 32 | 33 | 其实很简单,启动自动化构建: 34 | 35 | ``` 36 | npm run dev 37 | ``` 38 | 39 | 切换到`app`目录,执行: 40 | 41 | ``` 42 | php websocket.php 43 | ``` 44 | 45 | ## 4.访问 46 | 47 | ``` 48 | http://localhost:8080 49 | ``` 50 | 51 | ![](http://i1.piimg.com/567571/697dbe904510959c.png) 52 | 53 | Job done! 54 | 55 | 56 | 57 | # License 58 | 59 | MIT 60 | -------------------------------------------------------------------------------- /app/websocket.php: -------------------------------------------------------------------------------- 1 | port = $port; 78 | $this->init(); 79 | } 80 | 81 | public function init(){ 82 | $this->table = new swoole_table(1024); 83 | $this->table->column('id', swoole_table::TYPE_INT, 4); //1,2,4,8 84 | $this->table->column('avatar', swoole_table::TYPE_STRING, 1024); 85 | $this->table->column('nickname', swoole_table::TYPE_STRING, 64); 86 | $this->table->create(); 87 | 88 | $this->server = $server = new swoole_websocket_server('0.0.0.0',$this->port); 89 | $server->set([ 90 | 'task_worker_num' => 4 91 | ]); 92 | $server->on('open', [ $this,'open' ]); 93 | $server->on('message', [$this, 'message']); 94 | $server->on('close', [$this, 'close']); 95 | $server->on('task', [$this, 'task']); 96 | $server->on('finish', [$this, 'finish']); 97 | 98 | $server->start(); 99 | } 100 | 101 | public function open(swoole_websocket_server $server, swoole_http_request $req){ 102 | 103 | $avatar = $this->avatars[array_rand($this->avatars)]; 104 | $nickname = $this->nicknames[array_rand($this->nicknames)]; 105 | 106 | $this->table->set($req->fd,[ 107 | 'id' => $req->fd, 108 | 'avatar' => $avatar, 109 | 'nickname' => $nickname 110 | ]); 111 | 112 | //init selfs data 113 | $userMsg = $this->buildMsg([ 114 | 'id' => $req->fd, 115 | 'avatar' => $avatar, 116 | 'nickname' => $nickname, 117 | 'count' => count($this->table) 118 | ],self::INIT_SELF_TYPE); 119 | $this->server->task([ 120 | 'to' => [$req->fd], 121 | 'except' => [], 122 | 'data' => $userMsg 123 | ]); 124 | 125 | //init others data 126 | $others = []; 127 | foreach ($this->table as $row) { 128 | $others[] = $row; 129 | } 130 | $otherMsg = $this->buildMsg($others,self::INIT_OTHER_TYPE); 131 | $this->server->task([ 132 | 'to' => [$req->fd], 133 | 'except' => [], 134 | 'data' => $otherMsg 135 | ]); 136 | 137 | 138 | 139 | //broadcast a user is online 140 | $msg = $this->buildMsg([ 141 | 'id' => $req->fd, 142 | 'avatar' => $avatar, 143 | 'nickname' => $nickname, 144 | 'count' => count($this->table) 145 | ],self::CONNECT_TYPE); 146 | $this->server->task([ 147 | 'to' => [], 148 | 'except' => [$req->fd], 149 | 'data' => $msg 150 | ]); 151 | } 152 | 153 | public function message(swoole_websocket_server $server, swoole_websocket_frame $frame){ 154 | $receive = json_decode($frame->data,true); 155 | $msg = $this->buildMsg($receive,self::MESSAGE_TYPE); 156 | 157 | $task = [ 158 | 'to' => [], 159 | 'except' => [$frame->fd], 160 | 'data' => $msg 161 | ]; 162 | 163 | if ($receive['to'] != 0) { 164 | $task['to'] = [$receive['to']]; 165 | } 166 | 167 | $server->task($task); 168 | } 169 | 170 | public function close(swoole_websocket_server $server, $fd){ 171 | $this->table->del($fd); 172 | $msg = $this->buildMsg([ 173 | 'id' => $fd, 174 | 'count' => count($this->table) 175 | ],self::DISCONNECT_TYPE); 176 | $this->server->task([ 177 | 'to' => [], 178 | 'except' => [$fd], 179 | 'data' => $msg 180 | ]); 181 | } 182 | 183 | public function task($server, $task_id, $from_id, $data){ 184 | $clients = $server->connections; 185 | if (count($data['to']) > 0) { 186 | $clients = $data['to']; 187 | } 188 | foreach ($clients as $fd) { 189 | if (!in_array($fd, $data['except'])) { 190 | $this->server->push($fd,$data['data']); 191 | } 192 | } 193 | } 194 | 195 | public function finish(){ 196 | 197 | } 198 | 199 | private function buildMsg($data,$type,$status = 200){ 200 | return json_encode([ 201 | 'status' => $status, 202 | 'type' => $type, 203 | 'data' => $data 204 | ]); 205 | } 206 | } 207 | 208 | new WebSocket(9501); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vue 6 | 7 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-chat", 3 | "description": "A Vue.js project", 4 | "author": "Summer", 5 | "private": true, 6 | "scripts": { 7 | "dev": "webpack-dev-server --inline --hot", 8 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules" 9 | }, 10 | "dependencies": { 11 | "babel-runtime": "^6.0.0", 12 | "vue": "^1.0.0", 13 | "vuex": "^1.0.0-rc.2" 14 | }, 15 | "devDependencies": { 16 | "babel-core": "^6.0.0", 17 | "babel-loader": "^6.0.0", 18 | "babel-plugin-transform-runtime": "^6.0.0", 19 | "babel-preset-es2015": "^6.0.0", 20 | "babel-preset-stage-2": "^6.0.0", 21 | "cross-env": "^1.0.6", 22 | "css-loader": "^0.23.0", 23 | "file-loader": "^0.8.4", 24 | "json-loader": "^0.5.4", 25 | "less": "^2.7.1", 26 | "less-loader": "^2.2.3", 27 | "url-loader": "^0.5.7", 28 | "vue-hot-reload-api": "^1.2.0", 29 | "vue-html-loader": "^1.0.0", 30 | "vue-loader": "^8.2.1", 31 | "vue-style-loader": "^1.0.0", 32 | "webpack": "^1.12.2", 33 | "webpack-dev-server": "^1.12.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 82 | 83 | -------------------------------------------------------------------------------- /src/Auth.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 40 | 41 | -------------------------------------------------------------------------------- /src/assets/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mogody/swoole-vue-webim/5f0971fc501a20853f41ff7035ecd2b136f9ffc8/src/assets/bg.jpg -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mogody/swoole-vue-webim/5f0971fc501a20853f41ff7035ecd2b136f9ffc8/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/List.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 32 | 33 | -------------------------------------------------------------------------------- /src/components/Message.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 30 | 31 | -------------------------------------------------------------------------------- /src/components/Notice.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/components/Text.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/components/User.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 30 | 31 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import Auth from './Auth.vue' 4 | 5 | Vue.config.debug = true; 6 | 7 | new Vue({ 8 | el: 'body', 9 | components: { App, Auth }, 10 | data : { 11 | currentView : 'App' 12 | } 13 | }); 14 | 15 | -------------------------------------------------------------------------------- /src/vuex/actions.js: -------------------------------------------------------------------------------- 1 | export default { 2 | //the first arg is the instance of Vuex.Store , another is custom 3 | 4 | searchUser: ({ dispatch }, filterUser) => { 5 | dispatch('FILTER_USER', filterUser); 6 | }, 7 | 8 | selectSession : ({ dispatch }, userId) => { 9 | dispatch('CHANGE_SESSION', userId); 10 | }, 11 | 12 | setUser : ({ dispatch }, user) => { 13 | if (user.id && user.avatar && user.nickname) { 14 | dispatch('SET_USER', user); 15 | } 16 | }, 17 | 18 | addUser : ({ dispatch }, user) => { 19 | if (user instanceof Array || user.id && user.avatar && user.nickname) { 20 | dispatch('ADD_USER', user); 21 | } 22 | }, 23 | 24 | removeUser : ({ dispatch }, userId) => { 25 | dispatch('REMOVE_USER', userId); 26 | }, 27 | 28 | setConn : ({ dispatch }, conn) => { 29 | dispatch('SET_CONN', conn); 30 | }, 31 | 32 | changeStatus : ({ dispatch }, status) => { 33 | dispatch('CHANGE_STATUS', status); 34 | }, 35 | 36 | addMessage : ({ dispatch }, message) => { 37 | if (message.is_self != 1) { 38 | let userId = message.to == 0 ? 0 : message.from; 39 | 40 | dispatch('SET_HAS_MESSAGE',userId,true); 41 | } 42 | 43 | 44 | dispatch('ADD_MESSAGE',message); 45 | }, 46 | 47 | setHasMessageStatus : ({ dispatch }, userId, status) => { 48 | dispatch('SET_HAS_MESSAGE', userId, status); 49 | }, 50 | 51 | setCount : ({ dispatch }, count) => { 52 | dispatch('SET_COUNT', count); 53 | }, 54 | 55 | showNotice : ({ dispatch }, msg, type) => { 56 | dispatch('SHOW_NOTICE', msg, type); 57 | } 58 | 59 | 60 | }; -------------------------------------------------------------------------------- /src/vuex/getters.js: -------------------------------------------------------------------------------- 1 | // export function getMsg(state){ 2 | // return state.msg; 3 | // } 4 | export default { 5 | filterUser : ({ filterUser }) => filterUser, 6 | 7 | currentUser : ({ currentUser }) => currentUser, //current user 8 | 9 | users : ({ users }) => users, //user list 10 | 11 | currentSession : ({ currentSession }) => currentSession, 12 | 13 | broadcast : ({ broadcast }) => broadcast, 14 | 15 | conn : ({ connection }) => { 16 | 17 | if (connection != null) { 18 | return connection; 19 | } 20 | }, 21 | 22 | online : ({ online }) => online, 23 | 24 | currentCount : ({ currentCount }) => currentCount, 25 | 26 | notice : ({ notice }) => notice 27 | } -------------------------------------------------------------------------------- /src/vuex/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | //create a pbject to save the state of app at start 7 | const state = { 8 | //the user of current 9 | currentUser : { 10 | id : 13, 11 | avatar : 'http://tva3.sinaimg.cn/crop.0.0.200.200.50/701cac0cjw8ez3nd2wa7rj205k05kt8v.jpg', 12 | nickname : 'jack' 13 | }, 14 | //all users 15 | users : [ 16 | { 17 | id : 0, 18 | nickname : '群聊', 19 | avatar : 'http://58pic.ooopic.com/58pic/12/25/04/02k58PICVwf.jpg', 20 | has_message : false 21 | } 22 | ], 23 | filterUser: '', 24 | 25 | currentSession : { 26 | id : 0, 27 | nickname : '群聊', 28 | avatar : 'http://b.hiphotos.baidu.com/exp/w=480/sign=d86a96f25766d0167e199f20a72ad498/b8014a90f603738d48c6191db61bb051f819ec05.jpg', 29 | chat : null 30 | }, 31 | 32 | currentCount : 0, 33 | 34 | online : false, 35 | 36 | broadcast : [], 37 | 38 | connection : null, 39 | 40 | notice : { 41 | show : false, 42 | type : '', 43 | msg : '' 44 | } 45 | } 46 | 47 | //create a object to save the function of mutation 48 | const mutations = { 49 | 50 | FILTER_USER: (state, nickname) => { 51 | state.filterUser = nickname; 52 | }, 53 | 54 | CHANGE_SESSION: (state, userId) => { 55 | for (var i = state.users.length - 1; i >= 0; i--) { 56 | if (state.users[i].id != userId) { 57 | continue; 58 | } 59 | state.currentSession = state.users[i]; 60 | break; 61 | } 62 | }, 63 | 64 | SET_USER: (state, user) => { 65 | state.currentUser = user; 66 | }, 67 | 68 | ADD_USER: (state, user) => { 69 | if (user instanceof Array) { 70 | 71 | for (var i = user.length - 1; i >= 0; i--) { 72 | if (user[i].id != state.currentUser.id) { 73 | user[i].has_message = false; 74 | state.users.push(user[i]); 75 | } 76 | 77 | } 78 | }else{ 79 | user.has_message = false; 80 | state.users.push(user); 81 | } 82 | 83 | }, 84 | 85 | REMOVE_USER: (state, userId) => { 86 | state.users.forEach((item,index) => { 87 | if (item.id == userId) { 88 | state.users.$remove(item); 89 | } 90 | }); 91 | }, 92 | 93 | SET_CONN: (state, conn) => { 94 | if (conn != null && state.connection == null) { 95 | state.connection = conn; 96 | } 97 | 98 | }, 99 | 100 | CHANGE_STATUS: (state, status) => { 101 | state.online = status; 102 | }, 103 | 104 | ADD_MESSAGE: (state, message) => { 105 | let msg = { 106 | user : { 107 | id : message.from, 108 | avatar : '', 109 | nickname : '' 110 | }, 111 | msg : message.msg, 112 | time : message.date 113 | }; 114 | if (message.from == state.currentUser.id) { 115 | msg.user = state.currentUser; 116 | }else{ 117 | for (var i = state.users.length - 1; i >= 0; i--) { 118 | if (state.users[i].id == message.from) { 119 | msg.user = state.users[i]; 120 | break; 121 | } 122 | } 123 | } 124 | 125 | if (message.to == 0) { 126 | if (state.broadcast[ 0 ] == undefined) { 127 | state.broadcast[ 0 ] = new Array; 128 | } 129 | 130 | state.broadcast[ 0 ].push(msg); 131 | 132 | state.broadcast.$set(0,state.broadcast[0]); 133 | }else{ 134 | if (message.is_self == 1) { 135 | message.from = message.to; 136 | } 137 | 138 | 139 | if (state.broadcast[ message.from ] == undefined) { 140 | state.broadcast[ message.from ] = new Array; 141 | } 142 | 143 | state.broadcast[ message.from ].push(msg); 144 | 145 | state.broadcast.$set(message.from,state.broadcast[ message.from ]); 146 | } 147 | 148 | }, 149 | 150 | SET_HAS_MESSAGE : (state, userId, status) => { 151 | 152 | for (var i = state.users.length - 1; i >= 0; i--) { 153 | if (status == false && state.users[i].id == userId || state.users[i].id == userId && state.currentSession.id != userId ) { 154 | state.users[i].has_message = status; 155 | } 156 | } 157 | }, 158 | 159 | SET_COUNT : (state, count) => { 160 | state.currentCount = count; 161 | }, 162 | 163 | SHOW_NOTICE : (state, msg, type) => { 164 | state.notice = { 165 | show : true, msg, type 166 | } 167 | 168 | setTimeout(function(){ 169 | state.notice.show = false; 170 | },1000); 171 | } 172 | } 173 | 174 | export default new Vuex.Store({ 175 | state,mutations 176 | }); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | 4 | module.exports = { 5 | entry: './src/main.js', 6 | output: { 7 | path: path.resolve(__dirname, './dist'), 8 | publicPath: '/dist/', 9 | filename: 'build.js' 10 | }, 11 | resolveLoader: { 12 | root: path.join(__dirname, 'node_modules'), 13 | }, 14 | module: { 15 | loaders: [ 16 | { 17 | test: /\.vue$/, 18 | loader: 'vue' 19 | }, 20 | { 21 | test: /\.js$/, 22 | loader: 'babel', 23 | exclude: /node_modules/ 24 | }, 25 | { 26 | test: /\.json$/, 27 | loader: 'json' 28 | }, 29 | { 30 | test: /\.html$/, 31 | loader: 'vue-html' 32 | }, 33 | { 34 | test: /\.(png|jpg|gif|svg)$/, 35 | loader: 'url', 36 | query: { 37 | limit: 10000, 38 | name: '[name].[ext]?[hash]' 39 | } 40 | } 41 | ] 42 | }, 43 | devServer: { 44 | historyApiFallback: true, 45 | noInfo: true 46 | }, 47 | devtool: '#eval-source-map' 48 | } 49 | 50 | if (process.env.NODE_ENV === 'production') { 51 | module.exports.devtool = '#source-map' 52 | // http://vue-loader.vuejs.org/en/workflow/production.html 53 | module.exports.plugins = (module.exports.plugins || []).concat([ 54 | new webpack.DefinePlugin({ 55 | 'process.env': { 56 | NODE_ENV: '"production"' 57 | } 58 | }), 59 | new webpack.optimize.UglifyJsPlugin({ 60 | compress: { 61 | warnings: false 62 | } 63 | }), 64 | new webpack.optimize.OccurenceOrderPlugin() 65 | ]) 66 | } 67 | --------------------------------------------------------------------------------