├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── LICENSE ├── README.md ├── build ├── build.js ├── check-versions.js ├── dev-client.js ├── dev-server.js ├── utils.js ├── vue-loader.conf.js ├── webpack.base.conf.js ├── webpack.dev.conf.js ├── webpack.prod.conf.js └── webpack.test.conf.js ├── config ├── dev.env.js ├── index.js ├── prod.env.js └── test.env.js ├── docs ├── README_zh_cn.md ├── group_chat.png ├── login.png ├── others.png └── private_chat.png ├── index.html ├── package.json ├── server ├── app.js ├── config │ ├── db.js │ └── secret.key ├── middlewares │ └── socketHandler.js ├── models │ ├── records.js │ └── users.js ├── public │ └── favicon.ico └── routes │ └── chat.js ├── src ├── App.vue ├── api │ └── api.js ├── assets │ └── favicon.ico ├── components │ ├── Avatar │ │ ├── Avatar.js │ │ ├── Avatar.less │ │ ├── Avatar.vue │ │ └── index.js │ ├── Chat │ │ ├── Chat.js │ │ ├── Chat.less │ │ ├── Chat.vue │ │ └── index.js │ ├── GroupChat │ │ ├── GroupChat.js │ │ ├── GroupChat.less │ │ ├── GroupChat.vue │ │ └── index.js │ ├── Login │ │ ├── Login.js │ │ ├── Login.less │ │ ├── Login.vue │ │ └── index.js │ ├── NotFound │ │ ├── NotFound.js │ │ ├── NotFound.less │ │ ├── NotFound.vue │ │ └── index.js │ └── PrivateChat │ │ ├── PrivateChat.js │ │ ├── PrivateChat.less │ │ ├── PrivateChat.vue │ │ └── index.js ├── less │ └── mixins.less ├── main.js ├── router │ └── index.js └── store │ ├── index.js │ ├── modules │ ├── people.js │ └── records.js │ ├── mutation-types.js │ └── store.js ├── static └── .gitkeep └── test └── unit ├── .eslintrc ├── index.js ├── karma.conf.js └── specs └── Hello.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "modules": false }], 4 | "stage-2" 5 | ], 6 | "plugins": ["transform-runtime"], 7 | "comments": false, 8 | "env": { 9 | "test": { 10 | "presets": ["env", "stage-2"], 11 | "plugins": [ "istanbul" ] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | }, 12 | extends: 'airbnb-base', 13 | // required to lint *.vue files 14 | plugins: [ 15 | 'html' 16 | ], 17 | // check if imports actually resolve 18 | 'settings': { 19 | 'import/resolver': { 20 | 'webpack': { 21 | 'config': 'build/webpack.base.conf.js' 22 | } 23 | } 24 | }, 25 | 'globals': { 26 | 'document': true 27 | }, 28 | // add your custom rules here 29 | 'rules': { 30 | // don't require .vue extension when importing 31 | 'import/extensions': ['off', 'always', { 32 | 'js': 'never', 33 | 'vue': 'never' 34 | }], 35 | 'import/no-unresolved': [0, {commonjs: true, amd: true}], 36 | // allow optionalDependencies 37 | 'import/no-extraneous-dependencies': ['error', { 38 | 'optionalDependencies': ['test/unit/index.js'] 39 | }], 40 | // allow debugger during development 41 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 42 | 'no-param-reassign': 0, 43 | 'prefer-const': 0, 44 | 'no-console':0, 45 | 'no-unused-expressions': [2, { allowTernary: true }] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | node_modules/ 4 | dist/ 5 | npm-debug.log 6 | yarn-error.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | test/unit/coverage 11 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserlist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2017 Lan Canrong (blue) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # socket.io immediate chat room 2 | 3 | [中文文档](./docs/README_zh_cn.md) 4 | 5 | ## glance 6 | 7 | - login page 8 | 9 | ![login page](./docs/login.png) 10 | 11 | - group chat page 12 | 13 | ![group chat page](./docs/group_chat.png) 14 | 15 | - others list 16 | 17 | ![others](./docs/others.png) 18 | 19 | - private chat page 20 | 21 | ![private chat](./docs/private_chat.png) 22 | 23 | ## install dependencies 24 | 25 | 1. node7.x 26 | 27 | 2. auto restart node server tool 28 | 29 | ``` 30 | npm i -g nodemon 31 | ``` 32 | 33 | 3. QA tool 34 | 35 | install eslint plugin for your code editor(like "eslint" in vscode) 36 | 37 | ## download 38 | 39 | ``` bash 40 | # clone 41 | git clone git@github.com:Chanran/vueSocketChatroom.git 42 | cd vueSocketChatroom 43 | 44 | # install dependencies 45 | npm install -d 46 | ``` 47 | 48 | ## start 49 | 50 | ``` 51 | npm run dev 52 | npm run server # open another terminal 53 | ``` 54 | 55 | visit [http://localhost:8080/](http://localhost:8080/) 56 | 57 | ## deploy 58 | 59 | ``` 60 | npm install -g pm2 # install just once 61 | npm i -d --production 62 | npm run build 63 | npm run deploy 64 | ``` 65 | 66 | ## tech docs 67 | 68 | - [api docs](https://www.showdoc.cc/1629169?page_id=14974136) 69 | - [vue2](https://vuejs.org/) 70 | - [vue-router2](https://router.vuejs.org/en/) 71 | - [vuex](https://vuex.vuejs.org/en/) 72 | - [vue-loader](https://vue-loader.vuejs.org/en/) 73 | - [vux](https://vux.li/#/) 74 | - [express4.x](https://expressjs.com/) 75 | - [mongodb](https://docs.mongodb.com/) 76 | 77 | ## License 78 | 79 | [MIT](./LICENSE) 80 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | var ora = require('ora') 6 | var rm = require('rimraf') 7 | var path = require('path') 8 | var chalk = require('chalk') 9 | var webpack = require('webpack') 10 | var config = require('../config') 11 | var webpackConfig = require('./webpack.prod.conf') 12 | 13 | var spinner = ora('building for production...') 14 | spinner.start() 15 | 16 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 17 | if (err) throw err 18 | webpack(webpackConfig, function (err, stats) { 19 | spinner.stop() 20 | if (err) throw err 21 | process.stdout.write(stats.toString({ 22 | colors: true, 23 | modules: false, 24 | children: false, 25 | chunks: false, 26 | chunkModules: false 27 | }) + '\n\n') 28 | 29 | console.log(chalk.cyan(' Build complete.\n')) 30 | console.log(chalk.yellow( 31 | ' Tip: built files are meant to be served over an HTTP server.\n' + 32 | ' Opening index.html over file:// won\'t work.\n' 33 | )) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var semver = require('semver') 3 | var packageConfig = require('../package.json') 4 | 5 | function exec (cmd) { 6 | return require('child_process').execSync(cmd).toString().trim() 7 | } 8 | 9 | var versionRequirements = [ 10 | { 11 | name: 'node', 12 | currentVersion: semver.clean(process.version), 13 | versionRequirement: packageConfig.engines.node 14 | }, 15 | { 16 | name: 'npm', 17 | currentVersion: exec('npm --version'), 18 | versionRequirement: packageConfig.engines.npm 19 | } 20 | ] 21 | 22 | module.exports = function () { 23 | var warnings = [] 24 | for (var i = 0; i < versionRequirements.length; i++) { 25 | var mod = versionRequirements[i] 26 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 27 | warnings.push(mod.name + ': ' + 28 | chalk.red(mod.currentVersion) + ' should be ' + 29 | chalk.green(mod.versionRequirement) 30 | ) 31 | } 32 | } 33 | 34 | if (warnings.length) { 35 | console.log('') 36 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 37 | console.log() 38 | for (var i = 0; i < warnings.length; i++) { 39 | var warning = warnings[i] 40 | console.log(' ' + warning) 41 | } 42 | console.log() 43 | process.exit(1) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('eventsource-polyfill') 3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 4 | 5 | hotClient.subscribe(function (event) { 6 | if (event.action === 'reload') { 7 | window.location.reload() 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /build/dev-server.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | var config = require('../config') 4 | if (!process.env.NODE_ENV) { 5 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 6 | } 7 | 8 | var opn = require('opn') 9 | var path = require('path') 10 | var express = require('express') 11 | var webpack = require('webpack') 12 | var proxyMiddleware = require('http-proxy-middleware') 13 | var webpackConfig = process.env.NODE_ENV === 'testing' 14 | ? require('./webpack.prod.conf') 15 | : require('./webpack.dev.conf') 16 | 17 | // default port where dev server listens for incoming traffic 18 | var port = process.env.PORT || config.dev.port 19 | // automatically open browser, if not set will be false 20 | var autoOpenBrowser = !!config.dev.autoOpenBrowser 21 | // Define HTTP proxies to your custom API backend 22 | // https://github.com/chimurai/http-proxy-middleware 23 | var proxyTable = config.dev.proxyTable 24 | 25 | var app = express() 26 | var compiler = webpack(webpackConfig) 27 | 28 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 29 | publicPath: webpackConfig.output.publicPath, 30 | quiet: true 31 | }) 32 | 33 | var hotMiddleware = require('webpack-hot-middleware')(compiler, { 34 | log: () => {} 35 | }) 36 | // force page reload when html-webpack-plugin template changes 37 | compiler.plugin('compilation', function (compilation) { 38 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 39 | hotMiddleware.publish({ action: 'reload' }) 40 | cb() 41 | }) 42 | }) 43 | 44 | // proxy api requests 45 | Object.keys(proxyTable).forEach(function (context) { 46 | var options = proxyTable[context] 47 | if (typeof options === 'string') { 48 | options = { target: options } 49 | } 50 | app.use(proxyMiddleware(options.filter || context, options)) 51 | }) 52 | 53 | // handle fallback for HTML5 history API 54 | app.use(require('connect-history-api-fallback')()) 55 | 56 | // serve webpack bundle output 57 | app.use(devMiddleware) 58 | 59 | // enable hot-reload and state-preserving 60 | // compilation error display 61 | app.use(hotMiddleware) 62 | 63 | // serve pure static assets 64 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 65 | app.use(staticPath, express.static('./static')) 66 | 67 | var uri = 'http://localhost:' + port 68 | 69 | var _resolve 70 | var readyPromise = new Promise(resolve => { 71 | _resolve = resolve 72 | }) 73 | 74 | console.log('> Starting dev server...') 75 | devMiddleware.waitUntilValid(() => { 76 | console.log('> Listening at ' + uri + '\n') 77 | // when env is testing, don't need open it 78 | 79 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 80 | opn(uri) 81 | } 82 | _resolve() 83 | }) 84 | 85 | var server = app.listen(port) 86 | 87 | module.exports = { 88 | ready: readyPromise, 89 | close: () => { 90 | server.close() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | exports.assetsPath = function (_path) { 6 | var assetsSubDirectory = process.env.NODE_ENV === 'production' 7 | ? config.build.assetsSubDirectory 8 | : config.dev.assetsSubDirectory 9 | return path.posix.join(assetsSubDirectory, _path) 10 | } 11 | 12 | exports.cssLoaders = function (options) { 13 | options = options || {} 14 | 15 | var cssLoader = { 16 | loader: 'css-loader', 17 | options: { 18 | minimize: process.env.NODE_ENV === 'production', 19 | sourceMap: options.sourceMap 20 | } 21 | } 22 | 23 | // generate loader string to be used with extract text plugin 24 | function generateLoaders (loader, loaderOptions) { 25 | var loaders = [cssLoader] 26 | if (loader) { 27 | loaders.push({ 28 | loader: loader + '-loader', 29 | options: Object.assign({}, loaderOptions, { 30 | sourceMap: options.sourceMap 31 | }) 32 | }) 33 | } 34 | 35 | // Extract CSS when that option is specified 36 | // (which is the case during production build) 37 | if (options.extract) { 38 | return ExtractTextPlugin.extract({ 39 | use: loaders, 40 | fallback: 'vue-style-loader' 41 | }) 42 | } else { 43 | return ['vue-style-loader'].concat(loaders) 44 | } 45 | } 46 | 47 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 48 | return { 49 | css: generateLoaders(), 50 | postcss: generateLoaders(), 51 | less: generateLoaders('less'), 52 | sass: generateLoaders('sass', { indentedSyntax: true }), 53 | scss: generateLoaders('sass'), 54 | stylus: generateLoaders('stylus'), 55 | styl: generateLoaders('stylus') 56 | } 57 | } 58 | 59 | // Generate loaders for standalone style files (outside of .vue) 60 | exports.styleLoaders = function (options) { 61 | var output = [] 62 | var loaders = exports.cssLoaders(options) 63 | for (var extension in loaders) { 64 | var loader = loaders[extension] 65 | output.push({ 66 | test: new RegExp('\\.' + extension + '$'), 67 | use: loader 68 | }) 69 | } 70 | return output 71 | } 72 | -------------------------------------------------------------------------------- /build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var config = require('../config') 3 | var isProduction = process.env.NODE_ENV === 'production' 4 | 5 | module.exports = { 6 | loaders: utils.cssLoaders({ 7 | sourceMap: isProduction 8 | ? config.build.productionSourceMap 9 | : config.dev.cssSourceMap, 10 | extract: isProduction 11 | }), 12 | postcss: [ 13 | require('autoprefixer')({ 14 | browsers: ['iOS >= 7', 'Android >= 4.1'] 15 | }) 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | 4 | var projectRoot = path.resolve(__dirname, '../') 5 | const vuxLoader = require('vux-loader') 6 | 7 | var config = require('../config') 8 | var vueLoaderConfig = require('./vue-loader.conf') 9 | 10 | function resolve (dir) { 11 | return path.join(__dirname, '..', dir) 12 | } 13 | 14 | let webpackConfig = { 15 | entry: { 16 | app: './src/main.js' 17 | }, 18 | output: { 19 | path: config.build.assetsRoot, 20 | filename: '[name].js', 21 | publicPath: process.env.NODE_ENV === 'production' 22 | ? config.build.assetsPublicPath 23 | : config.dev.assetsPublicPath 24 | }, 25 | resolve: { 26 | extensions: ['.js', '.vue', '.json'], 27 | alias: { 28 | 'vue$': 'vue/dist/vue.esm.js', 29 | '@': resolve('src') 30 | } 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.(js|vue)$/, 36 | loader: 'eslint-loader', 37 | enforce: 'pre', 38 | include: [resolve('src'), resolve('test')], 39 | options: { 40 | formatter: require('eslint-friendly-formatter') 41 | } 42 | }, 43 | { 44 | test: /\.vue$/, 45 | loader: 'vue-loader', 46 | options: vueLoaderConfig 47 | }, 48 | { 49 | test: /\.js$/, 50 | loader: 'babel-loader', 51 | include: [resolve('src'), resolve('test')] 52 | }, 53 | { 54 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 55 | loader: 'url-loader', 56 | options: { 57 | limit: 10000, 58 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 59 | } 60 | }, 61 | { 62 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 63 | loader: 'url-loader', 64 | options: { 65 | limit: 10000, 66 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 67 | } 68 | } 69 | ] 70 | } 71 | } 72 | 73 | 74 | module.exports = vuxLoader.merge(webpackConfig, { 75 | plugins: ['vux-ui', 'progress-bar', 'duplicate-style'] 76 | }) 77 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var webpack = require('webpack') 3 | var config = require('../config') 4 | var merge = require('webpack-merge') 5 | var baseWebpackConfig = require('./webpack.base.conf') 6 | var HtmlWebpackPlugin = require('html-webpack-plugin') 7 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 8 | 9 | // add hot-reload related code to entry chunks 10 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 11 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 12 | }) 13 | 14 | module.exports = merge(baseWebpackConfig, { 15 | module: { 16 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 17 | }, 18 | // cheap-module-eval-source-map is faster for development 19 | devtool: '#cheap-module-eval-source-map', 20 | plugins: [ 21 | new webpack.DefinePlugin({ 22 | 'process.env': config.dev.env 23 | }), 24 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 25 | new webpack.HotModuleReplacementPlugin(), 26 | new webpack.NoEmitOnErrorsPlugin(), 27 | // https://github.com/ampedandwired/html-webpack-plugin 28 | new HtmlWebpackPlugin({ 29 | filename: 'index.html', 30 | template: 'index.html', 31 | inject: true 32 | }), 33 | new FriendlyErrorsPlugin() 34 | ] 35 | }) 36 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var webpack = require('webpack') 4 | var config = require('../config') 5 | var merge = require('webpack-merge') 6 | var baseWebpackConfig = require('./webpack.base.conf') 7 | var CopyWebpackPlugin = require('copy-webpack-plugin') 8 | var HtmlWebpackPlugin = require('html-webpack-plugin') 9 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 10 | var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 11 | 12 | var env = process.env.NODE_ENV === 'testing' 13 | ? require('../config/test.env') 14 | : config.build.env 15 | 16 | var webpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ 19 | sourceMap: config.build.productionSourceMap, 20 | extract: true 21 | }) 22 | }, 23 | devtool: config.build.productionSourceMap ? '#source-map' : false, 24 | output: { 25 | path: config.build.assetsRoot, 26 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 27 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 28 | }, 29 | plugins: [ 30 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 31 | new webpack.DefinePlugin({ 32 | 'process.env': env 33 | }), 34 | new webpack.optimize.UglifyJsPlugin({ 35 | compress: { 36 | warnings: false 37 | }, 38 | sourceMap: true 39 | }), 40 | // extract css into its own file 41 | new ExtractTextPlugin({ 42 | filename: utils.assetsPath('css/[name].[contenthash].css') 43 | }), 44 | // Compress extracted CSS. We are using this plugin so that possible 45 | // duplicated CSS from different components can be deduped. 46 | new OptimizeCSSPlugin({ 47 | cssProcessorOptions: { 48 | safe: true 49 | } 50 | }), 51 | // generate dist index.html with correct asset hash for caching. 52 | // you can customize output by editing /index.html 53 | // see https://github.com/ampedandwired/html-webpack-plugin 54 | new HtmlWebpackPlugin({ 55 | filename: process.env.NODE_ENV === 'testing' 56 | ? 'index.html' 57 | : config.build.index, 58 | template: 'index.html', 59 | inject: true, 60 | minify: { 61 | removeComments: true, 62 | collapseWhitespace: true, 63 | removeAttributeQuotes: true 64 | // more options: 65 | // https://github.com/kangax/html-minifier#options-quick-reference 66 | }, 67 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 68 | chunksSortMode: 'dependency' 69 | }), 70 | // split vendor js into its own file 71 | new webpack.optimize.CommonsChunkPlugin({ 72 | name: 'vendor', 73 | minChunks: function (module, count) { 74 | // any required modules inside node_modules are extracted to vendor 75 | return ( 76 | module.resource && 77 | /\.js$/.test(module.resource) && 78 | module.resource.indexOf( 79 | path.join(__dirname, '../node_modules') 80 | ) === 0 81 | ) 82 | } 83 | }), 84 | // extract webpack runtime and module manifest to its own file in order to 85 | // prevent vendor hash from being updated whenever app bundle is updated 86 | new webpack.optimize.CommonsChunkPlugin({ 87 | name: 'manifest', 88 | chunks: ['vendor'] 89 | }), 90 | // copy custom static assets 91 | new CopyWebpackPlugin([ 92 | { 93 | from: path.resolve(__dirname, '../static'), 94 | to: config.build.assetsSubDirectory, 95 | ignore: ['.*'] 96 | } 97 | ]) 98 | ] 99 | }) 100 | 101 | if (config.build.productionGzip) { 102 | var CompressionWebpackPlugin = require('compression-webpack-plugin') 103 | 104 | webpackConfig.plugins.push( 105 | new CompressionWebpackPlugin({ 106 | asset: '[path].gz[query]', 107 | algorithm: 'gzip', 108 | test: new RegExp( 109 | '\\.(' + 110 | config.build.productionGzipExtensions.join('|') + 111 | ')$' 112 | ), 113 | threshold: 10240, 114 | minRatio: 0.8 115 | }) 116 | ) 117 | } 118 | 119 | if (config.build.bundleAnalyzerReport) { 120 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 121 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 122 | } 123 | 124 | module.exports = webpackConfig 125 | -------------------------------------------------------------------------------- /build/webpack.test.conf.js: -------------------------------------------------------------------------------- 1 | // This is the webpack config used for unit tests. 2 | 3 | var utils = require('./utils') 4 | var webpack = require('webpack') 5 | var merge = require('webpack-merge') 6 | var baseConfig = require('./webpack.base.conf') 7 | 8 | var webpackConfig = merge(baseConfig, { 9 | // use inline sourcemap for karma-sourcemap-loader 10 | module: { 11 | rules: utils.styleLoaders() 12 | }, 13 | devtool: '#inline-source-map', 14 | plugins: [ 15 | new webpack.DefinePlugin({ 16 | 'process.env': require('../config/test.env') 17 | }) 18 | ] 19 | }) 20 | 21 | // no need for app entry during tests 22 | delete webpackConfig.entry 23 | 24 | module.exports = webpackConfig 25 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../dist/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../dist'), 9 | assetsSubDirectory: 'static', 10 | assetsPublicPath: '/', 11 | productionSourceMap: true, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'], 18 | // Run the build command with an extra argument to 19 | // View the bundle analyzer report after build finishes: 20 | // `npm run build --report` 21 | // Set to `true` or `false` to always turn it on or off 22 | bundleAnalyzerReport: process.env.npm_config_report 23 | }, 24 | dev: { 25 | env: require('./dev.env'), 26 | port: 8080, 27 | autoOpenBrowser: false, 28 | assetsSubDirectory: 'static', 29 | assetsPublicPath: '/', 30 | proxyTable: { 31 | '/api/*': { 32 | target: 'http://localhost:3000', 33 | secure: false 34 | }, 35 | '/socket.io': { 36 | target: 'http://localhost:3000', 37 | ws: true, 38 | secure: false 39 | } 40 | }, 41 | // CSS Sourcemaps off by default because relative paths are "buggy" 42 | // with this option, according to the CSS-Loader README 43 | // (https://github.com/webpack/css-loader#sourcemaps) 44 | // In our experience, they generally work as expected, 45 | // just be aware of this issue when enabling this option. 46 | cssSourceMap: false 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /config/test.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var devEnv = require('./dev.env') 3 | 4 | module.exports = merge(devEnv, { 5 | NODE_ENV: '"testing"' 6 | }) 7 | -------------------------------------------------------------------------------- /docs/README_zh_cn.md: -------------------------------------------------------------------------------- 1 | # socket.io 即时聊天室 2 | 3 | [English docs](../README.md) 4 | 5 | ## 界面一瞥 6 | 7 | - 登录页 8 | 9 | ![login page](./login.png) 10 | 11 | - 群聊页 12 | 13 | ![group chat page](./group_chat.png) 14 | 15 | - 其他人列表 16 | ![others](./others.png) 17 | 18 | - 私聊页 19 | 20 | ![private chat](./private_chat.png) 21 | 22 | ## 安装依赖 23 | 24 | 1. node7.x 25 | 26 | 2. 自动重启node服务工具 27 | 28 | ``` 29 | npm i -g nodemon 30 | ``` 31 | 32 | 3. QA(代码质量保证) 工具 33 | 34 | 给你的编辑器装一个eslint插件(比如vscode的"eslint") 35 | 36 | ## 下载 37 | 38 | ``` bash 39 | # 克隆 40 | git clone git@github.com:Chanran/vueSocketChatroom.git 41 | cd vueSocketChatroom 42 | 43 | # 安装依赖 44 | npm install -d 45 | ``` 46 | 47 | ## 启动 48 | 49 | ``` 50 | npm run dev 51 | npm run server # 另开一个终端 52 | ``` 53 | 54 | 访问 [http://localhost:8080/](http://localhost:8080/) 55 | 56 | ## 部署 57 | 58 | ``` 59 | npm install -g pm2 # 只安装一次 60 | npm i -d --production 61 | npm run build 62 | npm run deploy 63 | ``` 64 | 65 | ## 技术文档 66 | 67 | - [api文档](https://www.showdoc.cc/1629169?page_id=14974136) 68 | - [vue2(前端MVVM框架)](https://cn.vuejs.org/) 69 | - [vue-router2(前端路由框架)](https://router.vuejs.org/zh-cn/) 70 | - [vue-loader(webpack的loader,vue-cli使用的)](https://lvyongbo.gitbooks.io/vue-loader/content/) 71 | - [vux(前端UI框架)](https://vux.li/#/) 72 | - [express4.x(node框架)](http://www.expressjs.com.cn/) 73 | - [mongodb(数据库)](http://mongodb.github.io/node-mongodb-native/2.2/installation-guide/installation-guide/) 74 | 75 | ## License 76 | 77 | [MIT](../LICENSE) 78 | -------------------------------------------------------------------------------- /docs/group_chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chanran/vueSocketChatroom/a37fce8d652b7059a20bf910d327d19d5d6cb0b7/docs/group_chat.png -------------------------------------------------------------------------------- /docs/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chanran/vueSocketChatroom/a37fce8d652b7059a20bf910d327d19d5d6cb0b7/docs/login.png -------------------------------------------------------------------------------- /docs/others.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chanran/vueSocketChatroom/a37fce8d652b7059a20bf910d327d19d5d6cb0b7/docs/others.png -------------------------------------------------------------------------------- /docs/private_chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chanran/vueSocketChatroom/a37fce8d652b7059a20bf910d327d19d5d6cb0b7/docs/private_chat.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | chat 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "im", 3 | "version": "1.0.0", 4 | "description": "im chatroom", 5 | "author": "blue ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "server": "nodemon server/app.js", 10 | "deploy": "pm2 start server/app.js", 11 | "build": "node build/build.js", 12 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", 13 | "test": "npm run unit", 14 | "lint": "eslint --ext .js,.vue src test/unit/specs" 15 | }, 16 | "dependencies": { 17 | "axios": "^0.16.1", 18 | "bluebird": "^3.5.0", 19 | "body-parser": "~1.13.2", 20 | "connect-mongo": "^1.3.2", 21 | "cookie-parser": "~1.3.5", 22 | "express": "~4.13.1", 23 | "express-session": "^1.15.2", 24 | "fastclick": "^1.0.6", 25 | "moment": "^2.18.1", 26 | "mongoose": "^4.10.0", 27 | "morgan": "~1.6.1", 28 | "serve-favicon": "~2.3.0", 29 | "socket.io": "^1.7.3", 30 | "urlencode": "^1.1.0", 31 | "vue": "^2.2.2", 32 | "vue-router": "^2.2.0", 33 | "vuex": "^2.3.1", 34 | "vux": "^2.2.1" 35 | }, 36 | "devDependencies": { 37 | "autoprefixer": "^6.7.2", 38 | "babel-core": "^6.22.1", 39 | "babel-eslint": "^7.1.1", 40 | "babel-loader": "^6.2.10", 41 | "babel-plugin-istanbul": "^3.1.2", 42 | "babel-plugin-transform-runtime": "^6.22.0", 43 | "babel-preset-env": "^1.2.1", 44 | "babel-preset-stage-2": "^6.22.0", 45 | "babel-register": "^6.22.0", 46 | "chai": "^3.5.0", 47 | "chalk": "^1.1.3", 48 | "compression-webpack-plugin": "^0.3.2", 49 | "connect-history-api-fallback": "^1.3.0", 50 | "copy-webpack-plugin": "^4.0.1", 51 | "cross-env": "^3.1.4", 52 | "css-loader": "^0.26.4", 53 | "eslint": "^3.14.1", 54 | "eslint-config-airbnb-base": "^11.0.1", 55 | "eslint-friendly-formatter": "^2.0.7", 56 | "eslint-import-resolver-webpack": "^0.8.1", 57 | "eslint-loader": "^1.6.1", 58 | "eslint-plugin-html": "^2.0.0", 59 | "eslint-plugin-import": "^2.2.0", 60 | "eventsource-polyfill": "^0.9.6", 61 | "extract-text-webpack-plugin": "^2.0.0", 62 | "file-loader": "^0.10.0", 63 | "friendly-errors-webpack-plugin": "^1.1.3", 64 | "function-bind": "^1.1.0", 65 | "html-webpack-plugin": "^2.28.0", 66 | "http-proxy-middleware": "^0.17.3", 67 | "inject-loader": "^2.0.1", 68 | "karma": "^1.4.1", 69 | "karma-coverage": "^1.1.1", 70 | "karma-mocha": "^1.3.0", 71 | "karma-phantomjs-launcher": "^1.0.2", 72 | "karma-sinon-chai": "^1.2.4", 73 | "karma-sourcemap-loader": "^0.3.7", 74 | "karma-spec-reporter": "0.0.26", 75 | "karma-webpack": "^2.0.2", 76 | "less": "^2.7.1", 77 | "less-loader": "^2.2.3", 78 | "lolex": "^1.5.2", 79 | "mocha": "^3.2.0", 80 | "opn": "^4.0.2", 81 | "optimize-css-assets-webpack-plugin": "^1.3.0", 82 | "ora": "^1.1.0", 83 | "phantomjs-prebuilt": "^2.1.14", 84 | "rimraf": "^2.6.0", 85 | "semver": "^5.3.0", 86 | "sinon": "^1.17.7", 87 | "sinon-chai": "^2.8.0", 88 | "url-loader": "^0.5.7", 89 | "vue-loader": "^11.1.4", 90 | "vue-style-loader": "^2.0.5", 91 | "vue-template-compiler": "^2.2.4", 92 | "vux-loader": "^1.0.56", 93 | "webpack": "^2.2.1", 94 | "webpack-bundle-analyzer": "^2.2.1", 95 | "webpack-dev-middleware": "^1.10.0", 96 | "webpack-hot-middleware": "^2.16.1", 97 | "webpack-merge": "^2.6.1", 98 | "yaml-loader": "^0.4.0" 99 | }, 100 | "engines": { 101 | "node": ">= 4.0.0", 102 | "npm": ">= 3.0.0" 103 | }, 104 | "browserslist": [ 105 | "iOS >= 7", 106 | "Android >= 4.1" 107 | ] 108 | } 109 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const logger = require('morgan'); 4 | const cookieParser = require('cookie-parser'); 5 | const bodyParser = require('body-parser'); 6 | const session = require('express-session'); 7 | const MongoStore = require('connect-mongo')(session); 8 | const fs = require('fs'); 9 | const favicon = require('serve-favicon'); 10 | 11 | const dbUrl = require('./config/db').url; 12 | 13 | const socketHandler = require('./middlewares/socketHandler'); 14 | const chat = require('./routes/chat'); 15 | 16 | const app = express(); 17 | const secret = fs.readFileSync(path.resolve(__dirname, 'config/secret.key'), 'utf8'); 18 | 19 | // 禁用x-powered-by头 20 | app.disable('x-powered-by'); 21 | 22 | // 启用session 23 | app.use(session({ 24 | secret, 25 | name: 'iouser', 26 | resave: false, 27 | saveUninitialized: false, 28 | cookie: { 29 | // signed: true, 30 | secure: false, 31 | expires: new Date(Date.now() + (1000 * 60 * 60 * 24)), // cookie保存一天时间 32 | httpOnly: true, 33 | }, 34 | store: new MongoStore({ 35 | url: dbUrl, 36 | }), 37 | })); 38 | // 解析客户端传来的cookie 39 | app.use(cookieParser(secret)); 40 | 41 | // uncomment after placing your favicon in /public 42 | app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 43 | app.use(logger('dev')); 44 | app.use(bodyParser.json()); 45 | app.use(bodyParser.urlencoded({ extended: false })); 46 | app.use('/static', express.static(path.join(__dirname, '../dist/static'))); 47 | 48 | // 允许跨域访问 49 | app.all('*', (req, res, next) => { 50 | res.header('Access-Control-Allow-Origin', '*'); 51 | res.header('Access-Control-Allow-Headers', 'Content-Type,Content-Length, Authorization, Accept,X-Requested-With'); 52 | res.header('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS'); 53 | res.header('X-Powered-By', ' 3.2.1'); 54 | if (req.method === 'OPTIONS') res.send(200);/* 让options请求快速返回*/ 55 | else next(); 56 | }); 57 | 58 | // 路由 59 | app.use('/api', chat); 60 | // 上线路由 61 | if (process.env.NODE_ENV !== 'development') { 62 | app.get('/', (req, res) => { 63 | res.sendFile(path.resolve(__dirname, '../dist/index.html')); 64 | }); 65 | } 66 | 67 | // catch 404 and forward to error handler 68 | app.use((req, res, next) => { 69 | const err = new Error('Not Found'); 70 | err.status = 404; 71 | next(err); 72 | }); 73 | 74 | // error handlers 75 | 76 | // development error handler 77 | // will print stacktrace 78 | if (app.get('env') === 'development') { 79 | app.use((err, req, res) => { 80 | res.status(err.status || 500); 81 | res.render('error', { 82 | message: err.message, 83 | error: err, 84 | }); 85 | }); 86 | } 87 | 88 | // production error handler 89 | // no stacktraces leaked to user 90 | app.use((err, req, res) => { 91 | res.status(err.status || 500); 92 | res.render('error', { 93 | message: err.message, 94 | error: {}, 95 | }); 96 | }); 97 | 98 | const server = socketHandler.createServer(app); 99 | 100 | server.listen(3000); 101 | 102 | console.log('listening on port', 3000); 103 | 104 | -------------------------------------------------------------------------------- /server/config/db.js: -------------------------------------------------------------------------------- 1 | /* 数据库配置文件 */ 2 | 3 | module.exports = { 4 | host: 'localhost', 5 | port: 27017, 6 | dbname: 'test', 7 | url: 'mongodb://localhost:27017/test', 8 | }; 9 | -------------------------------------------------------------------------------- /server/config/secret.key: -------------------------------------------------------------------------------- 1 | 5570301C878DE91801EAA5AE56C66E1E3F190088F1866689039C3F9693A363A3 2 | -------------------------------------------------------------------------------- /server/middlewares/socketHandler.js: -------------------------------------------------------------------------------- 1 | let io = require('socket.io'); 2 | const http = require('http'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const users = require('../models/users'); 6 | const cookieParser = require('cookie-parser'); 7 | const urlencode = require('urlencode'); 8 | const moment = require('moment'); 9 | 10 | const { addRecord } = require('../models/records'); 11 | 12 | const secret = fs.readFileSync(path.resolve(__dirname, '../config/secret.key'), 'utf8'); 13 | 14 | function getUnSignedCookie(cookieString, cookieName) { 15 | // console.log(cookieString); 16 | let matches = new RegExp(`${cookieName}=([^;]+)`, 'gmi').exec(cookieString); 17 | // console.log(matches); 18 | return matches ? matches[1] : null; 19 | } 20 | 21 | function getSessionId(socket) { 22 | let cookies = socket.request.headers.cookie; 23 | let unsignedCookie = urlencode.decode(getUnSignedCookie(cookies, 'iouser')); 24 | let sessionId = cookieParser.signedCookie(unsignedCookie, secret); 25 | return sessionId; 26 | } 27 | 28 | 29 | function messageHandler(socketio) { 30 | socketio.on('connection', (socket) => { 31 | console.log(socket.id, '已连接'); 32 | 33 | socket.on('login', (data) => { 34 | let sessionId = getSessionId(socket); 35 | console.log(sessionId); 36 | let time = data.time; 37 | if (sessionId) { 38 | // 设置登录的用户的socket 39 | users.setUserSocket(sessionId, socket); 40 | let username = users.getUsername(sessionId); 41 | // console.log(username); 42 | 43 | // 广播通知有用户进入聊天室 44 | socket.broadcast.emit('someOneLogin', { 45 | user: { 46 | username, 47 | sessionId, 48 | }, 49 | msg: `${username} 进入了房间`, 50 | time, 51 | }); 52 | } 53 | }); 54 | 55 | // 广播 56 | socket.on('broadcast', (data) => { 57 | let sessionId = getSessionId(socket); 58 | console.log(sessionId); 59 | let username = users.getUsername(sessionId); 60 | // console.log(username); 61 | let msg = data.msg; 62 | let time = data.time; 63 | if (username) { 64 | socket.broadcast.emit('broadcast', { 65 | user: { 66 | sessionId, 67 | username, 68 | }, 69 | msg, 70 | time, 71 | }); 72 | 73 | // 储存聊天记录 74 | addRecord(username, sessionId, msg, time); 75 | } 76 | }); 77 | 78 | // 私聊 79 | socket.on('private', (data) => { 80 | let sessionId = getSessionId(socket); 81 | let username = users.getUsername(sessionId); 82 | let time = data.time; 83 | console.log('private', data.msg); 84 | if (username) { 85 | let to = users.findUser(data.toSessionId); 86 | if (to) { 87 | to.socket.emit('private', { 88 | user: { 89 | sessionId, 90 | username, 91 | }, 92 | msg: data.msg, 93 | time, 94 | }); 95 | } 96 | } 97 | }); 98 | 99 | socket.on('disconnect', () => { 100 | let sessionId = getSessionId(socket); 101 | let username = users.getUsername(sessionId); 102 | console.log(username, '已退出聊天室'); 103 | let time = moment().format('YYYY/MM/DD HH:mm:ss'); 104 | socket.broadcast.emit('quit', { 105 | user: { 106 | sessionId, 107 | username, 108 | }, 109 | msg: `${username} 退出了聊天室`, 110 | time, 111 | }); 112 | }); 113 | }); 114 | } 115 | 116 | /** 117 | * 创建server 118 | * @param {obejct} app 119 | * @returns {object} server 120 | */ 121 | function createServer(app) { 122 | const server = http.createServer(app); 123 | io = io(server); 124 | messageHandler(io); 125 | return server; 126 | } 127 | 128 | module.exports = { 129 | createServer, 130 | }; 131 | -------------------------------------------------------------------------------- /server/models/records.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const bluebird = require('bluebird'); 3 | const dbUrl = require('../config/db').url; 4 | 5 | // 连接mongodb 6 | mongoose.Promise = bluebird; 7 | mongoose.connect(dbUrl); 8 | const db = mongoose.connection; 9 | 10 | db.on('error', (err) => { 11 | console.log(err); 12 | }); 13 | 14 | 15 | const record = new mongoose.Schema({ 16 | username: String, 17 | sessionId: String, 18 | msg: String, 19 | time: String, 20 | }); 21 | 22 | const RecordModel = mongoose.model('records', record); 23 | 24 | /** 25 | * 添加一条聊天记录 26 | * 27 | * @param {string} username 28 | * @param {string} sessionId 29 | * @param {string} msg 30 | * @param {string} time 31 | */ 32 | function addRecord(username, sessionId, msg, time) { 33 | // console.log(username); 34 | // console.log(sessionId); 35 | // console.log(msg); 36 | // console.log(time); 37 | if (!username || !msg || !sessionId || !time) { 38 | return false; 39 | } 40 | let oneRecord = new RecordModel({ 41 | username, 42 | sessionId, 43 | msg, 44 | time, 45 | }); 46 | 47 | oneRecord.save((err, docs) => { 48 | if (err) { 49 | console.log(err); 50 | } 51 | console.log(docs); 52 | }); 53 | 54 | return true; 55 | } 56 | 57 | function getAllRecords() { 58 | return RecordModel.find({}).exec(); 59 | } 60 | 61 | module.exports = { 62 | addRecord, 63 | getAllRecords, 64 | }; 65 | -------------------------------------------------------------------------------- /server/models/users.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 内部数据结构:用户列表 3 | * [{username, sessionId, socket} ...] 4 | * */ 5 | let users = []; 6 | 7 | function findInUsers(sessionId) { // 通过sessionId查找 8 | let index = -1; 9 | for (let j = 0, len = users.length; j < len; j += 1) { 10 | if (users[j].sessionId === sessionId) { 11 | index = j; 12 | } 13 | } 14 | return index; 15 | } 16 | function addUser(username, sessionId) { // 添加用户 17 | let index = findInUsers(sessionId); 18 | if (index === -1) { 19 | users.push({ 20 | username, 21 | sessionId, 22 | socket: null, 23 | }); 24 | } else if (users[index].username !== username) { 25 | users[index].username = username; 26 | } 27 | } 28 | function setUserSocket(sessionId, socket) { // 更新用户socket 29 | // console.log(sessionId); 30 | let index = findInUsers(sessionId); 31 | if (index !== -1) { 32 | users[index].socket = socket; 33 | } 34 | // console.log(users); 35 | } 36 | function findUser(sessionId) { // 查找 37 | let index = findInUsers(sessionId); 38 | return index > -1 ? users[index] : null; 39 | } 40 | function otherUsers(sessionId) { // 其他人 41 | let results = []; 42 | for (let j = 0, len = users.length; j < len; j += 1) { 43 | if (users[j].sessionId !== sessionId) { 44 | results.push({ 45 | sessionId: users[j].sessionId, 46 | username: users[j].username, 47 | }); 48 | } 49 | } 50 | return results; 51 | } 52 | 53 | function allUsers() { 54 | return users; 55 | } 56 | 57 | function getUsername(sessionId) { 58 | for (let i = 0; i < users.length; i += 1) { 59 | // console.log(users[i].sessionId); 60 | // console.log(sessionId); 61 | // console.log(users[i].sessionId === sessionId); 62 | if (users[i].sessionId === sessionId) { 63 | return users[i].username; 64 | } 65 | } 66 | return '404NotFound'; 67 | } 68 | 69 | module.exports = { 70 | findUser, 71 | otherUsers, 72 | allUsers, 73 | getUsername, 74 | addUser, 75 | setUserSocket, 76 | }; 77 | -------------------------------------------------------------------------------- /server/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chanran/vueSocketChatroom/a37fce8d652b7059a20bf910d327d19d5d6cb0b7/server/public/favicon.ico -------------------------------------------------------------------------------- /server/routes/chat.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const users = require('../models/users'); 3 | const records = require('../models/records'); 4 | 5 | const router = express.Router(); 6 | 7 | /* GET home page. */ 8 | router.get('/login', (req, res) => { 9 | let username = req.query.username || req.params.username; 10 | // let ip = req.connection.remoteAddress; 11 | // let port = req.connection.remotePort; 12 | let sessionId = req.session.id; 13 | console.log(sessionId); 14 | if (username) { 15 | req.session.username = username; 16 | users.addUser(username, sessionId); 17 | 18 | res.json({ 19 | msg: 'success', 20 | code: '200', 21 | }); 22 | } else { 23 | res.json({ 24 | msg: 'username is required', 25 | code: '201', 26 | }); 27 | } 28 | }); 29 | 30 | router.get('/logout', (req, res) => { 31 | if (req.session.username) { 32 | req.session.destroy((err) => { 33 | if (err) { 34 | console.log(err); 35 | } 36 | }); 37 | res.clearCookie('iouser', { path: '/' }); 38 | res.json({ 39 | code: '200', 40 | msg: 'success', 41 | }); 42 | } else { 43 | res.json({ 44 | code: '202', 45 | msg: 'log out error,you are not logged in', 46 | }); 47 | } 48 | }); 49 | 50 | router.get('/others', (req, res) => { 51 | let sessionId = req.session.id; 52 | let username = req.session.username; 53 | if (sessionId && username) { 54 | res.json({ 55 | msg: 'success', 56 | code: '200', 57 | data: users.otherUsers(sessionId), 58 | }); 59 | } else { 60 | res.json({ 61 | msg: 'sessionId and username are not null', 62 | code: '203', 63 | }); 64 | } 65 | }); 66 | 67 | router.get('/testlogin', (req, res) => { 68 | // console.log(req.cookies); 69 | // console.log(req.signedCookies); 70 | // console.log(req.session); 71 | let username = req.session.username; 72 | if (username) { 73 | res.json({ 74 | msg: 'logged in', 75 | code: '200', 76 | username, 77 | }); 78 | } else { 79 | res.json({ 80 | msg: 'not log in', 81 | code: '204', 82 | }); 83 | } 84 | }); 85 | 86 | router.get('/records', (req, res) => { 87 | records.getAllRecords() 88 | .then((docs) => { 89 | console.log(docs); 90 | res.json({ 91 | code: 200, 92 | msg: 'success', 93 | data: docs, 94 | }); 95 | }) 96 | .catch((err) => { 97 | console.log(err); 98 | res.json({ 99 | code: '205', 100 | msg: 'get records error', 101 | }); 102 | }); 103 | }); 104 | 105 | router.get('/user', (req, res) => { 106 | let sessionId = req.session.id; 107 | let username = req.session.username; 108 | if (username) { 109 | res.json({ 110 | code: '200', 111 | msg: '', 112 | data: { 113 | sessionId, 114 | username, 115 | }, 116 | }); 117 | } else { 118 | res.json({ 119 | code: '206', 120 | msg: 'not log in', 121 | }); 122 | } 123 | }); 124 | 125 | module.exports = router; 126 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 26 | -------------------------------------------------------------------------------- /src/api/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | /** 4 | * 检查登录状态 5 | * 6 | * @param {func} to 7 | * @param {func} from 8 | * @param {func} next 9 | * @param {string} [loginNextRoute=''] 已登录的跳转链接 10 | * @param {string} [logoutNextRoute=''] 未登录的跳转链接 11 | * @param {string} [ErrorNextRoute=''] 异步请求客户端错误跳转链接 12 | */ 13 | export function checkLogin(to, from, next, loginNextRoute = '', logoutNextRoute = '', ErrorNextRoute = '') { 14 | axios.get('/api/testlogin') 15 | .then(({ data }) => { 16 | // console.log(data); 17 | if (parseInt(data.code, 10) === 200) { 18 | loginNextRoute === '' ? next() : next(loginNextRoute); 19 | } else { 20 | logoutNextRoute === '' ? next() : next(logoutNextRoute); 21 | } 22 | }) 23 | .catch((err) => { 24 | console.log(err); 25 | ErrorNextRoute === '' ? next(ErrorNextRoute) : next('/login'); 26 | }); 27 | } 28 | 29 | /** 30 | * 登录 31 | * 32 | * @export function 33 | * @param {object} vueInstance vuejs的实例 34 | * @param {string} username 用户名 35 | */ 36 | export function login(vueInstance, username) { 37 | axios.get('api/login', { 38 | params: { 39 | username, 40 | }, 41 | }) 42 | .then(({ data }) => { 43 | // console.log(data); 44 | if (parseInt(data.code, 10) === 200) { 45 | vueInstance.$router.push('/chat'); 46 | } else { 47 | vueInstance.$vux.alert.show({ 48 | title: data.msg, 49 | }); 50 | } 51 | }) 52 | .catch((err) => { 53 | console.log(err); 54 | }); 55 | } 56 | 57 | /** 58 | * 退出登录 59 | * 60 | * @export function 61 | * @param {object} vueInstance vuejs的实例 62 | */ 63 | export function logout(vueInstance) { 64 | axios.get('/api/logout') 65 | .then(({ data }) => { 66 | // console.log(data); 67 | if (parseInt(data.code, 10) === 200) { 68 | vueInstance.$router.push('/login'); 69 | } else { 70 | vueInstance.$vux.alert.show({ 71 | title: data.msg, 72 | }); 73 | } 74 | }) 75 | .catch((err) => { 76 | console.log(err); 77 | console.log(vueInstance); 78 | vueInstance.$vux.alert.show({ 79 | title: err, 80 | }); 81 | }); 82 | } 83 | 84 | /** 85 | * 取得在线人的列表 86 | * 87 | * @export function 88 | */ 89 | export function getOthers(cb, errorCb) { 90 | axios.get('/api/others') 91 | .then(({ data }) => { 92 | if (parseInt(data.code, 10) === 200) { 93 | cb(data.data); 94 | } else { 95 | console.log(data.msg); 96 | } 97 | }) 98 | .catch((err) => { 99 | errorCb(err); 100 | }); 101 | } 102 | 103 | /** 104 | * 得到所有群聊聊天记录 105 | * 106 | * @export 107 | * @param {function} cb 108 | * @param {function} errorCb 109 | */ 110 | export function getRecords(cb, errorCb) { 111 | axios.get('/api/records') 112 | .then(({ data }) => { 113 | if (parseInt(data.code, 10) === 200) { 114 | cb(data.data); 115 | } else { 116 | console.log(data.msg); 117 | } 118 | }) 119 | .catch((err) => { 120 | errorCb(err); 121 | }); 122 | } 123 | 124 | export function getUser(cb, errorCb) { 125 | axios.get('/api/user') 126 | .then(({ data }) => { 127 | if (parseInt(data.code, 10) === 200) { 128 | cb(data.data); 129 | } else { 130 | console.log(data.msg); 131 | } 132 | }) 133 | .catch((err) => { 134 | errorCb(err); 135 | }); 136 | } 137 | -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chanran/vueSocketChatroom/a37fce8d652b7059a20bf910d327d19d5d6cb0b7/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/components/Avatar/Avatar.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'Avatar', 3 | props: ['name'], 4 | data() { 5 | return { 6 | shortName: '', 7 | }; 8 | }, 9 | mounted() { 10 | this.shortName = this.name.trim().substring(0, 1).toUpperCase(); 11 | // console.log(this.shortName); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/Avatar/Avatar.less: -------------------------------------------------------------------------------- 1 | .avatar{ 2 | display: inline-flex; 3 | align-items: center; 4 | justify-content: center; 5 | background-color: pink; 6 | height: 40px; 7 | width: 40px; 8 | border-radius: 50%; 9 | color: white; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Avatar/Avatar.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /src/components/Avatar/index.js: -------------------------------------------------------------------------------- 1 | import Avatar from './Avatar.vue'; 2 | 3 | export default Avatar; 4 | -------------------------------------------------------------------------------- /src/components/Chat/Chat.js: -------------------------------------------------------------------------------- 1 | import { 2 | Divider, 3 | Actionsheet, 4 | XHeader, 5 | TransferDom, 6 | Popup, 7 | Tab, 8 | TabItem, 9 | Tabbar, 10 | TabbarItem, 11 | XButton, 12 | XInput, 13 | Grid, 14 | GridItem, 15 | Group } from 'vux'; 16 | import { 17 | mapActions, 18 | mapGetters } from 'vuex'; 19 | import moment from 'moment'; 20 | 21 | import GroupChat from '../GroupChat'; 22 | import PrivateChat from '../PrivateChat'; 23 | import { logout } from '../../api/api'; 24 | // const socket = null; 25 | 26 | export default { 27 | name: 'Chat', 28 | directives: { 29 | TransferDom, 30 | }, 31 | components: { 32 | Divider, 33 | Actionsheet, 34 | XHeader, 35 | Popup, 36 | Tab, 37 | TabItem, 38 | Tabbar, 39 | TabbarItem, 40 | XButton, 41 | XInput, 42 | Grid, 43 | GridItem, 44 | Group, 45 | GroupChat, 46 | PrivateChat, 47 | }, 48 | data() { 49 | return { 50 | showMenus: false, 51 | message: '', 52 | }; 53 | }, 54 | mounted() { 55 | const socket = window.io('/'); 56 | const that = this; 57 | // 告诉socket server该用户登录的动作 58 | let time = moment().format('YYYY/MM/DD HH:mm:ss'); 59 | socket.emit('login', { 60 | time, 61 | }); 62 | // 监听socket server其他用户登录的消息 63 | socket.on('someOneLogin', ({ user, msg }) => { 64 | that.addPeople({ 65 | label: user.username, 66 | value: user.sessionId, 67 | msgs: [], 68 | }); 69 | that.addRecord({ 70 | username: '', 71 | sessionId: '', 72 | tip: true, 73 | msg, 74 | time, 75 | }); 76 | console.log(msg); 77 | }); 78 | // 监听socket server 其他用户退出的消息 79 | socket.on('quit', (data) => { 80 | that.addRecord({ 81 | username: '', 82 | sessionId: '', 83 | tip: true, 84 | msg: data.msg, 85 | time: data.time, 86 | }); 87 | }); 88 | // 监听socket server 的广播 89 | socket.on('broadcast', (data) => { 90 | if (data.user.sessionId !== that.user.sessionId) { 91 | that.addRecord({ 92 | username: data.user.username, 93 | sessionId: data.user.sessionId, 94 | msg: data.msg, 95 | time: data.time, 96 | }); 97 | } 98 | }); 99 | // 监听私聊信息 100 | socket.on('private', (data) => { 101 | console.log(data); 102 | for (let i = 0; i < this.people.length; i += 1) { 103 | if (this.people[i].value === data.user.sessionId) { 104 | this.addPrivateRecord( 105 | { 106 | privateGroupIndex: i, 107 | sessionId: data.user.sessionId, 108 | username: data.user.username, 109 | msg: data.msg, 110 | time: data.time, 111 | }); 112 | } 113 | } 114 | }); 115 | // 聊天室成员 116 | this.getOthers(); 117 | this.getRecords(); 118 | this.getUser(); 119 | }, 120 | computed: { 121 | ...mapGetters([ 122 | 'people', 123 | 'talkingTo', 124 | 'talkToPeople', 125 | 'records', 126 | 'user', 127 | ]), 128 | }, 129 | methods: { 130 | ...mapActions([ 131 | 'getOthers', 132 | 'setTalkingTo', 133 | 'addTalkToPeople', 134 | 'addPeople', 135 | 'addRecord', 136 | 'getRecords', 137 | 'getUser', 138 | 'addPrivateRecord', 139 | ]), 140 | sendMsg() { 141 | const socket = window.io('/'); 142 | if (this.message.trim() !== '') { 143 | // 非群聊 144 | if (this.talkingTo !== -1) { 145 | let sessionId = this.people[this.talkingTo].value; 146 | let time = moment().format('YYYY/MM/DD HH:mm:ss'); 147 | // 发送私聊消息 148 | socket.emit('private', { 149 | msg: this.message, 150 | toSessionId: sessionId, 151 | time, 152 | }); 153 | this.addPrivateRecord( 154 | { 155 | privateGroupIndex: this.talkingTo, 156 | username: this.user.username, 157 | sessionId: this.user.sessionId, 158 | msg: this.message, 159 | time, 160 | }); 161 | // 清除输入框 162 | this.message = ''; 163 | 164 | // 群聊 165 | } else { 166 | // 发送群聊消息 167 | let time = moment().format('YYYY/MM/DD HH:mm:ss'); 168 | socket.emit('broadcast', { 169 | msg: this.message, 170 | time, 171 | }); 172 | this.addRecord({ 173 | username: this.user.username, 174 | sessionId: this.user.sessionId, 175 | msg: this.message, 176 | time, 177 | }); 178 | // 清除输入框 179 | this.message = ''; 180 | } 181 | } else { 182 | this.$vux.alert.show({ 183 | title: '发送消息不能为空!', 184 | }); 185 | } 186 | }, 187 | talkToThis(index) { 188 | this.setTalkingTo(index); 189 | }, 190 | choosePerson(value) { 191 | for (let i = 0; i < this.people.length; i += 1) { 192 | if (this.people[i].value === value) { 193 | if (this.talkToPeople.includes(i)) { 194 | this.setTalkingTo(i); 195 | } else { 196 | this.addTalkToPeople(i); 197 | this.setTalkingTo(i); 198 | } 199 | break; 200 | } 201 | } 202 | }, 203 | logout() { 204 | const that = this; 205 | this.$vux.confirm.show({ 206 | title: '确定要退出聊天室吗?', 207 | onConfirm() { 208 | logout(that); 209 | }, 210 | }); 211 | }, 212 | }, 213 | }; 214 | -------------------------------------------------------------------------------- /src/components/Chat/Chat.less: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100%; 3 | .grid-item { 4 | padding: 0 !important; 5 | } 6 | .chat-container { 7 | padding: 10px 5px 5px 5px; 8 | min-height: 300px; 9 | } 10 | .bottom-input { 11 | position: fixed; 12 | bottom: 0; 13 | z-index: 2; 14 | width: 100%; 15 | .input { 16 | margin-left: 5px; 17 | padding-left: 5px; 18 | height: 40px; 19 | line-height: 2.8; 20 | font-size: 20px; 21 | outline: none; 22 | border: none; 23 | border-bottom: 1px solid #1AAD19; 24 | width: 75%; 25 | display: inline-block; 26 | } 27 | .button { 28 | width: 20%; 29 | display: inline-block; 30 | } 31 | } 32 | .replace-block { 33 | height: 50px; 34 | width: 100%; 35 | z-index: 3; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/Chat/Chat.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 62 | 63 | 64 | 69 | -------------------------------------------------------------------------------- /src/components/Chat/index.js: -------------------------------------------------------------------------------- 1 | import Chat from './Chat.vue'; 2 | 3 | export default Chat; 4 | -------------------------------------------------------------------------------- /src/components/GroupChat/GroupChat.js: -------------------------------------------------------------------------------- 1 | import { 2 | Divider } from 'vux'; 3 | import Avatar from '../Avatar'; 4 | 5 | export default { 6 | name: 'GroupChat', 7 | props: ['records', 'user'], 8 | components: { 9 | Avatar, 10 | Divider, 11 | }, 12 | data() { 13 | return { 14 | 15 | }; 16 | }, 17 | methods: { 18 | 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/GroupChat/GroupChat.less: -------------------------------------------------------------------------------- 1 | .record { 2 | margin: 0 10px 20px 10px; 3 | position: relative; 4 | .avatar { 5 | vertical-align: top; 6 | } 7 | .name { 8 | display: inline-block; 9 | position: absolute; 10 | top: 0; 11 | left: 45px; 12 | color: gray; 13 | font-size: 5px; 14 | } 15 | .msg { 16 | display: inline-block; 17 | position: relative; 18 | top: 30px; 19 | width: 300px; 20 | margin-bottom: 30px; 21 | } 22 | .time { 23 | display: inline-block; 24 | position: absolute; 25 | top: 0; 26 | right: 0; 27 | color: gray; 28 | font-size: 5px; 29 | } 30 | } 31 | .record-now { 32 | margin: 0 10px 20px 10px; 33 | position: relative; 34 | .avatar { 35 | float: right; 36 | vertical-align: top; 37 | } 38 | .name { 39 | display: inline-block; 40 | position: absolute; 41 | top: 0; 42 | right: 45px; 43 | color: gray; 44 | font-size: 5px; 45 | } 46 | .msg { 47 | display: inline-block; 48 | position: relative; 49 | top: 25px; 50 | width: 300px; 51 | margin-bottom: 30px; 52 | text-align: right; 53 | } 54 | .time { 55 | display: inline-block; 56 | position: absolute; 57 | top: 0; 58 | left: 0; 59 | color: gray; 60 | font-size: 5px; 61 | } 62 | } 63 | .tip-msg { 64 | color: gray; 65 | font-size: 5px; 66 | } 67 | -------------------------------------------------------------------------------- /src/components/GroupChat/GroupChat.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 28 | 29 | 32 | -------------------------------------------------------------------------------- /src/components/GroupChat/index.js: -------------------------------------------------------------------------------- 1 | import GroupChat from './GroupChat.vue'; 2 | 3 | export default GroupChat; 4 | -------------------------------------------------------------------------------- /src/components/Login/Login.js: -------------------------------------------------------------------------------- 1 | import { 2 | XInput, 3 | XButton, 4 | Cell, 5 | Group } from 'vux'; 6 | 7 | import { login } from '../../api/api'; 8 | 9 | export default { 10 | name: 'Login', 11 | components: { 12 | XInput, 13 | XButton, 14 | Cell, 15 | Group, 16 | }, 17 | data() { 18 | return { 19 | username: '', 20 | }; 21 | }, 22 | methods: { 23 | login() { 24 | let username = this.username; 25 | if (this.username.trim() !== '') { 26 | login(this, username); 27 | } else { 28 | this.$vux.alert.show({ 29 | title: '用户名不能为空', 30 | }); 31 | } 32 | }, 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /src/components/Login/Login.less: -------------------------------------------------------------------------------- 1 | @import '../../less/mixins.less'; 2 | 3 | .login-container { 4 | .flexCenter(); 5 | box-sizing: border-box; 6 | height: 100%; 7 | background-color: #999; 8 | } 9 | 10 | .main { 11 | width: 90%; 12 | padding: 20px 10px 40px; 13 | border-radius: 8px; 14 | background: #fff; 15 | } 16 | 17 | .header { 18 | color: #1AAD19; 19 | font-size: 30px; 20 | text-align: center; 21 | letter-spacing: 5px; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Login/Login.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | 21 | 22 | 27 | -------------------------------------------------------------------------------- /src/components/Login/index.js: -------------------------------------------------------------------------------- 1 | import Login from './Login.vue'; 2 | 3 | export default Login; 4 | -------------------------------------------------------------------------------- /src/components/NotFound/NotFound.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'NotFound', 3 | data() { 4 | return { 5 | }; 6 | }, 7 | methods: { 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/NotFound/NotFound.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chanran/vueSocketChatroom/a37fce8d652b7059a20bf910d327d19d5d6cb0b7/src/components/NotFound/NotFound.less -------------------------------------------------------------------------------- /src/components/NotFound/NotFound.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /src/components/NotFound/index.js: -------------------------------------------------------------------------------- 1 | import NotFound from './NotFound.vue'; 2 | 3 | export default NotFound; 4 | -------------------------------------------------------------------------------- /src/components/PrivateChat/PrivateChat.js: -------------------------------------------------------------------------------- 1 | import { 2 | Divider } from 'vux'; 3 | import Avatar from '../Avatar'; 4 | 5 | export default { 6 | name: 'PrivateChat', 7 | props: ['records', 'user'], 8 | components: { 9 | Avatar, 10 | Divider, 11 | }, 12 | data() { 13 | return { 14 | 15 | }; 16 | }, 17 | mounted() { 18 | console.log(this.records); 19 | console.log(this.user); 20 | }, 21 | methods: { 22 | 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/PrivateChat/PrivateChat.less: -------------------------------------------------------------------------------- 1 | .record { 2 | margin: 0 10px 20px 10px; 3 | position: relative; 4 | .avatar { 5 | vertical-align: top; 6 | } 7 | .name { 8 | display: inline-block; 9 | position: absolute; 10 | top: 0; 11 | left: 45px; 12 | color: gray; 13 | font-size: 5px; 14 | } 15 | .msg { 16 | display: inline-block; 17 | position: relative; 18 | top: 30px; 19 | width: 300px; 20 | margin-bottom: 30px; 21 | } 22 | .time { 23 | display: inline-block; 24 | position: absolute; 25 | top: 0; 26 | right: 0; 27 | color: gray; 28 | font-size: 5px; 29 | } 30 | } 31 | .record-now { 32 | margin: 0 10px 20px 10px; 33 | position: relative; 34 | .avatar { 35 | float: right; 36 | vertical-align: top; 37 | } 38 | .name { 39 | display: inline-block; 40 | position: absolute; 41 | top: 0; 42 | right: 45px; 43 | color: gray; 44 | font-size: 5px; 45 | } 46 | .msg { 47 | display: inline-block; 48 | position: relative; 49 | top: 25px; 50 | width: 300px; 51 | margin-bottom: 30px; 52 | text-align: right; 53 | } 54 | .time { 55 | display: inline-block; 56 | position: absolute; 57 | top: 0; 58 | left: 0; 59 | color: gray; 60 | font-size: 5px; 61 | } 62 | } 63 | .tip-msg { 64 | color: gray; 65 | font-size: 5px; 66 | } 67 | -------------------------------------------------------------------------------- /src/components/PrivateChat/PrivateChat.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 28 | 29 | 32 | -------------------------------------------------------------------------------- /src/components/PrivateChat/index.js: -------------------------------------------------------------------------------- 1 | import PrivateChat from './PrivateChat.vue'; 2 | 3 | export default PrivateChat; 4 | -------------------------------------------------------------------------------- /src/less/mixins.less: -------------------------------------------------------------------------------- 1 | /* mixins */ 2 | 3 | .flexCenter { 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | } 8 | 9 | .fullBg(@url) { 10 | background-image: url(@url); 11 | background-repeat: no-repeat; 12 | background-size: 100% 100%; 13 | } 14 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue'; 4 | import FastClick from 'fastclick'; 5 | import VueRouter from 'vue-router'; 6 | import { AlertPlugin, ConfirmPlugin } from 'vux'; 7 | import router from './router'; 8 | import store from './store'; 9 | import App from './App'; 10 | 11 | 12 | Vue.prototype.$socketIoClient = window.io; // 将socket client赋给Vue实例 13 | Vue.use(VueRouter); // 使用vue-router 14 | Vue.use(AlertPlugin); // 使用alert插件 15 | Vue.use(ConfirmPlugin); // 使用confirm插件 16 | 17 | FastClick.attach(document.body); 18 | 19 | Vue.config.productionTip = false; 20 | 21 | /* eslint-disable no-new */ 22 | new Vue({ 23 | router, 24 | store, 25 | render: h => h(App), 26 | }).$mount('#app-box'); 27 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import Chat from '@/components/Chat/'; 4 | import Login from '@/components/Login/'; 5 | import NotFound from '@/components/NotFound/'; 6 | 7 | import { checkLogin } from '../api/api'; 8 | 9 | Vue.use(Router); 10 | 11 | export default new Router({ 12 | routes: [ 13 | { 14 | path: '/', 15 | redirect: '/chat', 16 | }, 17 | { 18 | path: '/chat', 19 | name: 'Chat', 20 | component: Chat, 21 | beforeEnter: (to, from, next) => { 22 | checkLogin(to, from, next, '', '/login'); 23 | }, 24 | }, 25 | { 26 | path: '/login', 27 | name: 'Login', 28 | component: Login, 29 | beforeEnter: (to, from, next) => { 30 | checkLogin(to, from, next, '/chat', ''); 31 | }, 32 | }, 33 | { 34 | path: '*', 35 | name: '404', 36 | component: NotFound, 37 | }, 38 | ], 39 | }); 40 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import store from './store'; 2 | 3 | export default store; 4 | -------------------------------------------------------------------------------- /src/store/modules/people.js: -------------------------------------------------------------------------------- 1 | import * as api from '../../api/api'; 2 | import * as types from '../mutation-types'; 3 | 4 | const initialState = { 5 | people: [], // [{label:username,value:sessionId,msgs:[{sessionId,username,msg,time}]}] 6 | talkingTo: -1, 7 | talkToPeople: [], 8 | user: { 9 | username: '', 10 | sessionId: '', 11 | }, 12 | }; 13 | 14 | const getters = { 15 | people: state => state.people, 16 | talkingTo: state => state.talkingTo, 17 | talkToPeople: state => state.talkToPeople, 18 | user: state => state.user, 19 | }; 20 | 21 | const actions = { 22 | // 得到其他人列表 23 | getOthers({ commit }) { 24 | // 开始异步请求,展示loading动画 25 | commit(types.START_LOADING); 26 | api.getOthers( 27 | (data) => { 28 | commit(types.GET_OTHERS_SUCCESS, data); 29 | // 关闭loading 30 | commit(types.END_LOADING); 31 | }, 32 | (err) => { 33 | console.log(err); 34 | commit(types.GET_OTHERS_FAILURE); 35 | // 关闭loading 36 | commit(types.END_LOADING); 37 | }); 38 | }, 39 | // 有人进入了房间 40 | addPeople({ commit }, user) { 41 | commit(types.ADD_PEOPLE, user); 42 | }, 43 | // 设置talkingTo 44 | setTalkingTo({ commit }, value) { 45 | commit(types.SET_TALKING_TO, value); 46 | }, 47 | // 移出某个talkToPeople 48 | reduceTalkToPeople({ commit }, value) { 49 | commit(types.REDUCE_TALK_TO_PEOPLE, value); 50 | }, 51 | // 增加某个talkToPeople 52 | addTalkToPeople({ commit }, index) { 53 | commit(types.ADD_TALK_TO_PEOPLE, index); 54 | }, 55 | getUser({ commit }) { 56 | // 开始异步请求,展示loading动画 57 | commit(types.START_LOADING); 58 | api.getUser( 59 | (user) => { 60 | commit(types.GET_USERNAME_SUCCESS, user); 61 | // 关闭loading 62 | commit(types.END_LOADING); 63 | }, 64 | (err) => { 65 | console.log(err); 66 | commit(types.GET_USERNAME_FAILURE); 67 | // 关闭loading 68 | commit(types.END_LOADING); 69 | }); 70 | }, 71 | // 增加一条私聊聊天记录 72 | addPrivateRecord({ commit }, privateRecord) { 73 | console.log(privateRecord); 74 | commit(types.ADD_PRIVATE_RECORD, privateRecord); 75 | }, 76 | }; 77 | 78 | const mutations = { 79 | // 成功得到其他人列表 80 | [types.GET_OTHERS_SUCCESS](state, others) { 81 | if (others.length > 0) { 82 | state.people.splice(0); 83 | others.map((other) => { 84 | state.people.push({ 85 | label: other.username, 86 | value: other.sessionId, 87 | msgs: [], 88 | }); 89 | return true; 90 | }); 91 | } 92 | }, 93 | // 得到其他人列表失败 94 | [types.GET_OTHERS_FAILURE](state) { 95 | state.people = []; 96 | state.talkingTo = -1; 97 | state.talkToPeople = []; 98 | }, 99 | // 设置正在聊天的人 100 | [types.SET_TALKING_TO](state, value) { 101 | state.talkingTo = value; 102 | }, 103 | // 关闭某个聊天室 104 | [types.REDUCE_TALK_TO_PEOPLE](state, value) { 105 | let index = null; 106 | for (let i = 0; i < state.talkToPeople.length; i += 1) { 107 | if (state.talkToPeople[i] === value) { 108 | index = i; 109 | } 110 | } 111 | if (index !== null) { 112 | state.talkToPeople.splice(index, 1); 113 | } 114 | }, 115 | // 增加一个私聊聊天室 116 | [types.ADD_TALK_TO_PEOPLE](state, index) { 117 | state.talkToPeople.push(index); 118 | }, 119 | // 增加一个聊天室的用户 120 | [types.ADD_PEOPLE](state, user) { 121 | state.people.push(user); 122 | }, 123 | [types.GET_USERNAME_SUCCESS](state, user) { 124 | state.user = { ...user }; 125 | }, 126 | [types.GET_OTHERS_FAILURE](state) { 127 | state.user = { 128 | username: '', 129 | sessionId: '', 130 | }; 131 | }, 132 | [types.ADD_PRIVATE_RECORD](state, privateRecord) { 133 | let groupIndex = privateRecord.privateGroupIndex; 134 | let privateGroupRecord = { 135 | sessionId: privateRecord.sessionId, // sessionId 136 | username: privateRecord.username, // username 137 | msg: privateRecord.msg, 138 | time: privateRecord.time, 139 | }; 140 | console.log(state.people); 141 | console.log(groupIndex); 142 | state.people[groupIndex].msgs.push(privateGroupRecord); 143 | }, 144 | }; 145 | 146 | 147 | export default { 148 | state: initialState, 149 | getters, 150 | actions, 151 | mutations, 152 | }; 153 | -------------------------------------------------------------------------------- /src/store/modules/records.js: -------------------------------------------------------------------------------- 1 | import * as api from '../../api/api'; 2 | import * as types from '../mutation-types'; 3 | 4 | const initialState = { 5 | records: [], // [{sessionId,username,msg,time}...] 6 | // sessionId是发出来的人的sessionId 7 | }; 8 | 9 | const getters = { 10 | records: state => state.records, 11 | privateGroups: state => state.privateGroups, 12 | }; 13 | 14 | const actions = { 15 | // 得到群聊聊天记录 16 | getRecords({ commit }) { 17 | commit(types.START_LOADING); 18 | api.getRecords( 19 | (data) => { 20 | commit(types.GET_RECORDS_SUCCESS, data); 21 | // 关闭loading 22 | commit(types.END_LOADING); 23 | }, 24 | (err) => { 25 | console.log(err); 26 | commit(types.GET_RECORDS_FAILURE); 27 | // 关闭loading 28 | commit(types.END_LOADING); 29 | }); 30 | }, 31 | // 增加一条群聊聊天记录 32 | addRecord({ commit }, record) { 33 | // console.log('test'); 34 | commit(types.ADD_RECORD, record); 35 | }, 36 | }; 37 | 38 | const mutations = { 39 | [types.GET_RECORDS_SUCCESS](state, records) { 40 | if (records.length > 0) { 41 | state.records.splice(0); 42 | records.map((record) => { 43 | state.records.push(record); 44 | return true; 45 | }); 46 | } 47 | }, 48 | [types.GET_RECORDS_FAILURE](state) { 49 | state.records = []; 50 | }, 51 | [types.ADD_RECORD](state, record) { 52 | state.records.push(record); 53 | }, 54 | }; 55 | 56 | 57 | export default { 58 | state: initialState, 59 | getters, 60 | actions, 61 | mutations, 62 | }; 63 | -------------------------------------------------------------------------------- /src/store/mutation-types.js: -------------------------------------------------------------------------------- 1 | /* 异步请求 */ 2 | 3 | // 得到其他人列表 4 | export const GET_OTHERS_SUCCESS = 'GET_OTHERS_SUCCESS'; 5 | export const GET_OTHERS_FAILURE = 'GET_OTHERS_FAILURE'; 6 | 7 | // 得到聊天记录 8 | export const GET_RECORDS_SUCCESS = 'GET_RECORDS_SUCCESS'; 9 | export const GET_RECORDS_FAILURE = 'GET_RECORDS_FAILURE'; 10 | 11 | // 得到用户名 12 | export const GET_USERNAME_SUCCESS = 'GET_USERNAME_SUCCESS'; 13 | export const GET_USERNAME_FAILURE = 'GET_USERNAME_FAILURE'; 14 | 15 | // loading动画 16 | export const START_LOADING = 'START_LOADING'; 17 | export const END_LOADING = 'END_LOADING'; 18 | 19 | export const SET_TALKING_TO = 'SET_TALKING_TO'; 20 | export const REDUCE_TALK_TO_PEOPLE = 'REDUCE_TALK_TO_PEOPLE'; 21 | export const ADD_TALK_TO_PEOPLE = 'ADD_TALK_TO_PEOPLE'; 22 | export const ADD_PEOPLE = 'ADD_PEOPLE'; 23 | export const ADD_RECORD = 'ADD_RECORD'; 24 | 25 | // 增加私聊组 26 | export const ADD_PRIVATE_GROUP = 'ADD_PRIVATE_GROUP'; 27 | // 增加一条私聊聊天记录 28 | export const ADD_PRIVATE_RECORD = 'ADD_PRIVATE_RECORD'; 29 | 30 | -------------------------------------------------------------------------------- /src/store/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | import * as types from './mutation-types'; 5 | import people from './modules/people'; 6 | import records from './modules/records'; 7 | 8 | Vue.use(Vuex); 9 | const debug = process.env.NODE_ENV !== 'production'; 10 | 11 | /* 全局 */ 12 | 13 | // 全局可用的state 14 | const initialState = { 15 | isLoading: false, 16 | menu: false, 17 | }; 18 | 19 | // 全局可用的getters 20 | const getters = { 21 | loading: state => state.isLoading, 22 | }; 23 | 24 | // 全局可用的actions 25 | const actions = { 26 | // 开始loading 27 | startLoading({ commit }) { 28 | commit(types.START_LOADING); 29 | }, 30 | // 结束loading 31 | endLoading({ commit }) { 32 | commit(types.END_LOADING); 33 | }, 34 | }; 35 | 36 | // 全局可用的mutations 37 | const mutations = { 38 | // 开始loading 39 | [types.START_LOADING](state) { 40 | state.isLoading = true; 41 | }, 42 | // 结束loading 43 | [types.END_LOADING](state) { 44 | state.isLoading = false; 45 | }, 46 | }; 47 | 48 | 49 | export default new Vuex.Store({ 50 | state: initialState, 51 | getters, 52 | actions, 53 | mutations, 54 | modules: { 55 | people, 56 | records, 57 | }, 58 | strict: debug, 59 | }); 60 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chanran/vueSocketChatroom/a37fce8d652b7059a20bf910d327d19d5d6cb0b7/static/.gitkeep -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | Vue.config.productionTip = false; 3 | 4 | // Polyfill fn.bind() for PhantomJS 5 | /* eslint-disable no-extend-native */ 6 | Function.prototype.bind = require('function-bind'); 7 | 8 | // require all test files (files that ends with .spec.js) 9 | const testsContext = require.context('./specs', true, /\.spec$/); 10 | testsContext.keys().forEach(testsContext); 11 | 12 | // require all src files except main.js for coverage. 13 | // you can also change this to match only the subset of files that 14 | // you want coverage for. 15 | const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/); 16 | srcContext.keys().forEach(srcContext); 17 | -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // This is a karma config file. For more details see 2 | // http://karma-runner.github.io/0.13/config/configuration-file.html 3 | // we are also using it with karma-webpack 4 | // https://github.com/webpack/karma-webpack 5 | 6 | var webpackConfig = require('../../build/webpack.test.conf'); 7 | 8 | module.exports = function (config) { 9 | config.set({ 10 | // to run in additional browsers: 11 | // 1. install corresponding karma launcher 12 | // http://karma-runner.github.io/0.13/config/browsers.html 13 | // 2. add it to the `browsers` array below. 14 | browsers: ['PhantomJS'], 15 | frameworks: ['mocha', 'sinon-chai'], 16 | reporters: ['spec', 'coverage'], 17 | files: ['./index.js'], 18 | preprocessors: { 19 | './index.js': ['webpack', 'sourcemap'] 20 | }, 21 | webpack: webpackConfig, 22 | webpackMiddleware: { 23 | noInfo: true, 24 | }, 25 | coverageReporter: { 26 | dir: './coverage', 27 | reporters: [ 28 | { type: 'lcov', subdir: '.' }, 29 | { type: 'text-summary' }, 30 | ] 31 | }, 32 | }); 33 | }; -------------------------------------------------------------------------------- /test/unit/specs/Hello.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Hello from '@/components/Hello'; 3 | 4 | describe('Hello.vue', () => { 5 | it('should render correct contents', () => { 6 | const Constructor = Vue.extend(Hello); 7 | const vm = new Constructor().$mount(); 8 | expect(vm.$el.querySelector('.hello h1').textContent) 9 | .to.equal('Welcome to Your Vue.js App'); 10 | }); 11 | }); 12 | --------------------------------------------------------------------------------