├── .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 | 
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
82 |
83 |
--------------------------------------------------------------------------------
/src/Auth.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |

6 |
请用微信扫描二维码
7 |
8 |
9 |
扫描成功
10 |
11 |
12 |
13 |
14 |
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 |
2 |
11 |
12 |
13 |
32 |
33 |
--------------------------------------------------------------------------------
/src/components/Message.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ currentSession.nickname }}
5 |
6 |
7 |
8 | -
9 |
{{ msg.time }}
10 |
11 |
![]()
12 |
{{ msg.msg }}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
30 |
31 |
--------------------------------------------------------------------------------
/src/components/Notice.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/components/Text.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/src/components/User.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
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 |
--------------------------------------------------------------------------------