5 | {{item.name === name ? '我' : item.name}}:{{item.content}} 6 |
7 |├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── LICENSE
├── README.md
├── config
├── build.js
├── dev-client.js
├── dev-server.js
├── mock.routes.js
├── webpack.base.conf.js
├── webpack.dev.conf.js
├── webpack.prod.conf.js
└── ws-server.js
├── mock
└── data.js
├── package.json
└── src
├── App.vue
├── api
├── index.js
└── util.js
├── components
└── chat.vue
├── index.html
├── main.js
├── router
└── index.js
├── service
├── cache.js
└── chatService.js
├── store
├── actions.js
├── index.js
├── mutations.js
└── wsPlugin.js
└── view
├── RX.vue
└── Vuex.vue
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 4
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
9 |
10 | [*.{less,json}]
11 | indent_size = 2
12 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | public/*.js
2 | config/*.js
3 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: 'babel-eslint',
4 | parserOptions: {
5 | ecmaVersion: 6,
6 | sourceType: 'module'
7 | },
8 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
9 | extends: 'standard',
10 | // required to lint *.vue files
11 | plugins: [
12 | 'html'
13 | ],
14 | env: {
15 | 'browser': true,
16 | 'node': true
17 | },
18 | globals: {
19 | '_': true
20 | },
21 | // add your custom rules here
22 | rules: {
23 | // allow paren-less arrow functions
24 | 'arrow-parens': 0,
25 | // allow async-await
26 | 'generator-star-spacing': 0,
27 | //
28 | 'space-before-function-paren': [0, "always"],
29 | "keyword-spacing": 0,
30 | // 4 space indent
31 | 'indent': [2, 4, { "SwitchCase": 1 }],
32 | // disable no-new
33 | 'no-new': 0,
34 | // allow debugger during development
35 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # build dir
7 | public
8 |
9 | # e2e test reports
10 | test/e2e/reports
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 |
17 | # Directory for instrumented libs generated by jscoverage/JSCover
18 | lib-cov
19 |
20 | # Coverage directory used by tools like istanbul
21 | coverage
22 |
23 | # nyc test coverage
24 | .nyc_output
25 |
26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
27 | .grunt
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules
37 | jspm_packages
38 |
39 | # Optional npm cache directory
40 | .npm
41 |
42 | # Optional REPL history
43 | .node_repl_history
44 |
--------------------------------------------------------------------------------
/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 | # vue-rx-chat
2 |
3 | 分别基于 Vuex 和 Vue-rx 的方式实现简单聊天室Demo,对比两者在 Websocket 实时推送后的数据和视图同步差异。
4 |
5 | ## start
6 |
7 | ```bash
8 | # dev server, http://localhost:3000
9 | $ npm run dev
10 |
11 | # websocket server, ws://localhost:3300
12 | $ npm run ws
13 |
14 | # dev server && websocket server
15 | $ npm start
16 |
17 | # build
18 | $ npm run build
19 | ```
20 |
--------------------------------------------------------------------------------
/config/build.js:
--------------------------------------------------------------------------------
1 | require('shelljs/global')
2 | env.NODE_ENV = 'production'
3 |
4 | const ora = require('ora')
5 | const webpack = require('webpack')
6 | const conf = require('./webpack.prod.conf')
7 |
8 | const spinner = ora('building for production...')
9 | spinner.start()
10 |
11 | rm('-rf', 'public')
12 | mkdir('public')
13 |
14 | // cp('-R', 'src', conf.output.path)
15 |
16 | webpack(conf, (err, stats) => {
17 | spinner.stop()
18 | if (err) throw err
19 | process.stdout.write(stats.toString({
20 | colors: true,
21 | modules: false,
22 | children: false,
23 | chunks: false,
24 | chunkModules: false
25 | }) + '\n')
26 | })
27 |
--------------------------------------------------------------------------------
/config/dev-client.js:
--------------------------------------------------------------------------------
1 | require('eventsource-polyfill')
2 | const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
3 |
4 | hotClient.subscribe(event => {
5 | if (event.action === 'reload') {
6 | window.location.reload()
7 | }
8 | })
9 |
--------------------------------------------------------------------------------
/config/dev-server.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const webpack = require('webpack')
3 | const config = require('./webpack.dev.conf')
4 | const proxyMiddleware = require('http-proxy-middleware')
5 | const reload = require('reload')
6 | const http = require('http')
7 |
8 | const app = express()
9 | const compiler = webpack(config)
10 | const devMiddleware = require('webpack-dev-middleware')(compiler, {
11 | publicPath: config.output.publicPath,
12 | noInfo: true,
13 | stats: {
14 | colors: true,
15 | chunks: false
16 | }
17 | })
18 | const hotMiddleware = require('webpack-hot-middleware')(compiler)
19 |
20 | // force page reload when html-webpack-plugin template changes
21 | compiler.plugin('compilation', compilation => {
22 | compilation.plugin('html-webpack-plugin-after-emit', (data, cb) => {
23 | hotMiddleware.publish({ action: 'reload' })
24 | cb()
25 | })
26 | })
27 |
28 | // handle fallback for HTML5 history API
29 | app.use(require('connect-history-api-fallback')())
30 |
31 | app.use(devMiddleware)
32 | app.use(hotMiddleware)
33 |
34 | // serve pure static assets
35 | app.use('/src', express.static('./src'))
36 |
37 | // mock router
38 | require('./mock.routes')(app)
39 |
40 | const server = http.createServer(app)
41 | reload(server, app)
42 |
43 | const prot = 3000
44 |
45 | module.exports = server.listen(prot, err => {
46 | if (err) {
47 | console.log(err)
48 | return
49 | }
50 | const uri = 'http://localhost:' + prot
51 | console.log(`Listening at ${uri}\n`)
52 | })
53 |
--------------------------------------------------------------------------------
/config/mock.routes.js:
--------------------------------------------------------------------------------
1 | const Mock = require('mockjs')
2 | const path = require('path')
3 | const fs = require('fs')
4 | const mockPath = path.resolve(__dirname, '../mock')
5 |
6 | module.exports = app => {
7 | fs.readdirSync(mockPath).forEach(file => {
8 | require(path.resolve(mockPath, file)).forEach(({url, method = 'get', data }) => {
9 | app[method](url, (req, res) => {
10 | const template = typeof data === 'function' ? data(req) : data
11 | res.send(Mock.mock(template))
12 | })
13 | })
14 | })
15 | }
16 |
--------------------------------------------------------------------------------
/config/webpack.base.conf.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 | const path = require('path')
3 | const projectRoot = path.resolve(__dirname, '../')
4 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
5 | const autoprefixer = require('autoprefixer')
6 | const LodashWebpackPlugin = require('lodash-webpack-plugin')
7 |
8 | module.exports = {
9 | entry: {
10 | app: './src/main.js'
11 | },
12 | output: {
13 | path: path.resolve(__dirname, '../public/'),
14 | // publicPath: './public/',
15 | filename: '[name].js'
16 | },
17 | resolve: {
18 | extensions: ['', '.js', '.vue'],
19 | fallback: [path.join(__dirname, '../node_modules')],
20 | alias: {
21 | src: path.resolve(__dirname, '../src'),
22 | components: 'src/components',
23 | view: 'src/view',
24 | less: 'src/less',
25 | assets: 'src/assets',
26 | fonts: 'assets/fonts',
27 | images: 'assets/images'
28 | }
29 | },
30 | resolveLoader: {
31 | fallback: [path.join(__dirname, '../node_modules')]
32 | },
33 | module: {
34 | preLoaders: [{
35 | test: /\.vue$/,
36 | loader: 'eslint',
37 | include: projectRoot,
38 | exclude: /node_modules/
39 | }, {
40 | test: /\.js$/,
41 | loader: 'eslint',
42 | include: projectRoot,
43 | exclude: /node_modules/
44 | }],
45 | loaders: [{
46 | test: /\.vue$/,
47 | loader: 'vue'
48 | }, {
49 | test: /\.js$/,
50 | loader: 'babel',
51 | include: projectRoot,
52 | exclude: /node_modules/
53 | }, {
54 | test: /\.json$/,
55 | loader: 'json'
56 | }, {
57 | test: /\.html$/,
58 | loader: 'vue-html'
59 | }, {
60 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
61 | loader: 'url',
62 | query: {
63 | limit: 10000,
64 | name: 'assets/images/[name].[hash:7].[ext]'
65 | }
66 | }, {
67 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
68 | loader: 'url',
69 | query: {
70 | limit: 10000,
71 | name: 'assets/fonts/[name].[hash:7].[ext]'
72 | }
73 | }]
74 | },
75 | vue: {
76 | loaders: {
77 | css: 'vue-style!css',
78 | less: 'vue-style!css!less'
79 | },
80 | postcss: [autoprefixer({
81 | browsers: ['last 3 versions']
82 | })]
83 | },
84 | eslint: {
85 | formatter: require('eslint-friendly-formatter')
86 | },
87 | babel: {
88 | presets: ['es2015', 'stage-2'],
89 | plugins: ['transform-runtime', 'lodash'],
90 | comments: false
91 | },
92 | postcss: [autoprefixer({
93 | browsers: ['last 3 versions']
94 | })],
95 | plugins: [
96 | new LodashWebpackPlugin()
97 | ]
98 | }
99 |
--------------------------------------------------------------------------------
/config/webpack.dev.conf.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 | const merge = require('webpack-merge')
3 | const baseConfig = require('./webpack.base.conf')
4 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
5 | const HtmlWebpackPlugin = require('html-webpack-plugin')
6 |
7 | // add hot-reload related code to entry chunks
8 | Object.keys(baseConfig.entry).forEach(name => {
9 | baseConfig.entry[name] = ['./config/dev-client'].concat(baseConfig.entry[name])
10 | })
11 |
12 | module.exports = merge(baseConfig, {
13 | module: {
14 | loaders: [{
15 | test: /\.(css|less)$/,
16 | loader: 'vue-style!css!less'
17 | }]
18 | },
19 | devtool: '#eval-source-map',
20 | output: {
21 | publicPath: '/'
22 | },
23 | plugins: [
24 | new webpack.DefinePlugin({
25 | 'process.env': {
26 | NODE_ENV: '"development"'
27 | }
28 | }),
29 | new webpack.optimize.OccurenceOrderPlugin(),
30 | new webpack.HotModuleReplacementPlugin(),
31 | new webpack.NoErrorsPlugin(),
32 | new HtmlWebpackPlugin({
33 | title: 'Vue-demo',
34 | filename: 'index.html',
35 | template: '!!ejs!src/index.html',
36 | NODE_ENV: 'dev',
37 | inject: true
38 | })
39 | ]
40 | })
41 |
--------------------------------------------------------------------------------
/config/webpack.prod.conf.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 | const merge = require('webpack-merge')
3 | const baseConfig = require('./webpack.base.conf')
4 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
5 | const HtmlWebpackPlugin = require('html-webpack-plugin')
6 |
7 | module.exports = merge(baseConfig, {
8 | module: {
9 | loaders: [{
10 | test: /\.(css|less)$/,
11 | loader: ExtractTextPlugin.extract('style-loader', 'css!postcss!less')
12 | }]
13 | },
14 | devtool: '#source-map',
15 | output: {
16 | filename: '[name].[chunkhash].js',
17 | chunkFilename: '[id].[chunkhash].js'
18 | },
19 | vue: {
20 | loaders: {
21 | css: ExtractTextPlugin.extract('vue-style-loader', 'css?sourceMap!postcss?sourceMap'),
22 | less: ExtractTextPlugin.extract('vue-style-loader', 'css?sourceMap!postcss?sourceMap!less?sourceMap')
23 | }
24 | },
25 | plugins: [
26 | new webpack.DefinePlugin({
27 | 'process.env': {
28 | NODE_ENV: '"production"'
29 | }
30 | }),
31 | new webpack.optimize.UglifyJsPlugin({
32 | compress: {
33 | warnings: false
34 | }
35 | }),
36 | new webpack.optimize.OccurenceOrderPlugin(),
37 | new ExtractTextPlugin('assets/[name].[contenthash].css'),
38 | new HtmlWebpackPlugin({
39 | title: 'Vue-demo',
40 | filename: 'index.html',
41 | template: 'src/index.html',
42 | inject: true,
43 | NODE_ENV: 'prod',
44 | minify: {
45 | removeComments: true,
46 | collapseWhitespace: true,
47 | removeAttributeQuotes: true,
48 | removeScriptTypeAttributes: true
49 | }
50 | })
51 | ]
52 | })
53 |
--------------------------------------------------------------------------------
/config/ws-server.js:
--------------------------------------------------------------------------------
1 | const http = require('http')
2 | const WebSocket = require('faye-websocket')
3 | const _remove = require('lodash/remove')
4 | const Mock = require('mockjs')
5 |
6 | const server = http.createServer((req, res) => {
7 | console.log('ws running!')
8 | })
9 |
10 | let clients = []
11 |
12 | server.on('upgrade', (request, socket, body) => {
13 | if (WebSocket.isWebSocket(request)) {
14 | const ws = new WebSocket(request, socket, body)
15 |
16 | clients.push(ws)
17 |
18 | ws.on('open', e => {
19 | console.log('[open]')
20 | })
21 |
22 | ws.on('message', e => {
23 | const msg = JSON.stringify(Object.assign({}, JSON.parse(e.data), {
24 | uid: Mock.mock('@id')
25 | }))
26 | clients.forEach(client => client.send(msg))
27 | })
28 |
29 | ws.on('close', e => {
30 | console.log('[close]', e.code, e.reason)
31 | _remove(clients, client => client === ws)
32 | })
33 | }
34 | })
35 |
36 | server.listen(3300)
37 |
--------------------------------------------------------------------------------
/mock/data.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | {
3 | url: '/messages',
4 | data: {
5 | 'list|1-10': [{
6 | uid: '@id',
7 | name: '@cname',
8 | content: '@cparagraph'
9 | }]
10 | }
11 | }
12 | ]
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-rx-chat",
3 | "version": "0.0.1",
4 | "author": {
5 | "name": "yusen",
6 | "email": "634206017@qq.com"
7 | },
8 | "scripts": {
9 | "dev": "start http://localhost:3000 & cross-env NODE_ENV=dev supervisor -w config -i src config/dev-server.js",
10 | "build": "node config/build.js",
11 | "ws": "node config/ws-server.js",
12 | "start": "npm run ws | npm run dev"
13 | },
14 | "dependencies": {
15 | "axios": "^0.15.3",
16 | "faye-websocket": "^0.11.1",
17 | "lodash": "^4.17.2",
18 | "rxjs": "^5.2.0",
19 | "vue": "^2.1.0",
20 | "vue-router": "^2.0.1",
21 | "vue-rx": "^2.3.1",
22 | "vuex": "^2.0.0"
23 | },
24 | "devDependencies": {
25 | "autoprefixer": "^6.5.2",
26 | "babel-core": "^6.7.7",
27 | "babel-eslint": "^7.1.0",
28 | "babel-loader": "^6.2.4",
29 | "babel-plugin-lodash": "^3.2.10",
30 | "babel-plugin-transform-runtime": "^6.7.5",
31 | "babel-preset-es2015": "^6.6.0",
32 | "babel-preset-stage-2": "^6.5.0",
33 | "babel-register": "^6.18.0",
34 | "babel-runtime": "^5.8.38",
35 | "connect-history-api-fallback": "^1.2.0",
36 | "cross-env": "^1.0.7",
37 | "css-loader": "^0.25.0",
38 | "ejs-loader": "^0.3.0",
39 | "eslint": "^3.9.1",
40 | "eslint-config-standard": "^6.2.1",
41 | "eslint-friendly-formatter": "^2.0.6",
42 | "eslint-loader": "^1.6.1",
43 | "eslint-plugin-html": "^1.6.0",
44 | "eslint-plugin-promise": "^3.3.1",
45 | "eslint-plugin-standard": "^2.0.1",
46 | "eventsource-polyfill": "^0.9.6",
47 | "express": "^4.14.0",
48 | "extract-text-webpack-plugin": "^1.0.1",
49 | "file-loader": "^0.9.0",
50 | "html-webpack-plugin": "^2.14.0",
51 | "http-proxy-middleware": "^0.17.2",
52 | "json-loader": "^0.5.4",
53 | "less": "^2.7.1",
54 | "less-loader": "^2.2.3",
55 | "lodash-webpack-plugin": "^0.10.6",
56 | "mockjs": "^1.0.1-beta3",
57 | "opn": "^4.0.2",
58 | "ora": "^0.3.0",
59 | "postcss-loader": "^1.1.0",
60 | "reload": "^1.1.0",
61 | "shelljs": "^0.7.4",
62 | "style-loader": "^0.13.1",
63 | "url-loader": "^0.5.7",
64 | "vue-hot-reload-api": "^1.3.2",
65 | "vue-html-loader": "^1.2.2",
66 | "vue-loader": "^10.0.0",
67 | "vue-style-loader": "^1.0.0",
68 | "vue-template-compiler": "^2.1.0",
69 | "webpack": "^1.13.2",
70 | "webpack-dev-middleware": "^1.8.3",
71 | "webpack-hot-middleware": "^2.12.2",
72 | "webpack-merge": "^0.14.1"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 | {{item.name === name ? '我' : item.name}}:{{item.content}}
6 |