├── .gitignore ├── Procfile ├── README.md ├── package-lock.json ├── package.json ├── prettier.config.js ├── src ├── client │ ├── app.ts │ ├── components │ │ └── App.vue │ ├── declarations │ │ └── vim-shim.d.ts │ └── index.html └── server │ ├── index.ts │ └── reactivity.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # created by gitignorer 2 | # profile used: node 3 | *.sw* 4 | .DS_Store 5 | .vscode 6 | dist 7 | .cache 8 | node_modules 9 | npm-debug.log* 10 | .env.local 11 | .env.*.local 12 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node dist/server/index.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue Composition Chat 2 | 3 | POC of how backend can also leverage the power of Vue's reactivity. 4 | 5 | ## What to look? 6 | 7 | The most interesting bit is probably in the file: `src/server/index.ts`. 8 | 9 | ```ts 10 | import { ref, watch } from '@vue/reactivity'; 11 | import WebSocket from 'ws'; 12 | const wss = new WebSocket.Server({ server }); 13 | 14 | type ClientId = string; 15 | const websockets = ref([]); 16 | const messages = ref([]); 17 | 18 | watch([messages, () => websockets.value.length], ([messages]) => { 19 | for (const ws of websockets.value) { 20 | ws.send(JSON.stringify(messages)); 21 | } 22 | }); 23 | 24 | wss.on('connection', ws => { 25 | websockets.value.push(ws); 26 | 27 | const id: ClientId = nanoid(); 28 | 29 | ws.on('message', message => { 30 | if (typeof message !== 'string') return; 31 | messages.value.push(`${id} said: ${message}`); 32 | }); 33 | 34 | ws.on('close', () => { 35 | websockets.value = websockets.value.filter(socket => ws !== socket); 36 | }); 37 | }); 38 | ``` 39 | 40 | Notice as messages come in from the websocket, we simply push the message to the `messages` ref. And the watcher would take care of broadcasting messages for us. The watcher will also fire when new user connects to the server. 41 | 42 | ## Author 43 | 44 | Jason Yu 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "vue-chat", 4 | "version": "0.0.1", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "server:dev": "parcel serve --out-dir dist/server --target node src/server/index.ts", 9 | "server:build": "parcel build --out-dir dist/server --target node src/server/index.ts", 10 | "client:dev": "parcel serve --out-dir dist/client src/client/index.html", 11 | "client:build": "parcel build --out-dir dist/client src/client/index.html", 12 | "build": "npm run server:build && npm run client:build" 13 | }, 14 | "keywords": [], 15 | "author": "Jason Yu ", 16 | "license": "ISC", 17 | "dependencies": { 18 | "@vue/composition-api": "^0.3.2", 19 | "koa": "^2.8.2", 20 | "koa-static": "^5.0.0", 21 | "nanoid": "^2.1.2", 22 | "normalize.css": "^8.0.1", 23 | "parcel-bundler": "^1.12.4", 24 | "vue": "^2.6.10", 25 | "vue-hot-reload-api": "^2.3.4", 26 | "ws": "^7.1.2" 27 | }, 28 | "devDependencies": { 29 | "@types/koa": "^2.0.50", 30 | "@types/koa-static": "^4.0.1", 31 | "@types/nanoid": "^2.1.0", 32 | "@types/ws": "^6.0.3", 33 | "@vue/component-compiler-utils": "^3.0.0", 34 | "prettier": "^1.18.2", 35 | "typescript": "^3.7.0-beta", 36 | "vue-template-compiler": "^2.6.10" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | semi: true, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | } 7 | -------------------------------------------------------------------------------- /src/client/app.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueCompositionAPI from '@vue/composition-api'; 3 | 4 | import App from './components/App.vue'; 5 | 6 | Vue.use(VueCompositionAPI); 7 | 8 | new Vue({ 9 | el: '#app', 10 | render: h => h(App), 11 | }); 12 | -------------------------------------------------------------------------------- /src/client/components/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 64 | 65 | 93 | 94 | 112 | -------------------------------------------------------------------------------- /src/client/declarations/vim-shim.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue'; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Vue Chat 4 | 5 | 6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | import { ref, watch } from './reactivity'; 2 | import WebSocket from 'ws'; 3 | import nanoid from 'nanoid'; 4 | import Koa from 'koa'; 5 | import serve from 'koa-static'; 6 | import http from 'http'; 7 | import { resolve } from 'path'; 8 | 9 | const app = new Koa(); 10 | app.use(serve(resolve(__dirname, '../client'))); 11 | const server = http.createServer(app.callback()); 12 | const wss = new WebSocket.Server({ server }); 13 | 14 | type ClientId = string; 15 | const websockets = ref([]); 16 | const messages = ref([]); 17 | 18 | watch([messages, () => websockets.value.length], ([messages]) => { 19 | for (const ws of websockets.value) { 20 | ws.send(JSON.stringify(messages)); 21 | } 22 | }); 23 | 24 | wss.on('connection', ws => { 25 | websockets.value.push(ws); 26 | 27 | const id: ClientId = nanoid(); 28 | 29 | ws.on('message', message => { 30 | if (typeof message !== 'string') return; 31 | messages.value.push(`${id} said: ${message}`); 32 | }); 33 | 34 | ws.on('close', () => { 35 | websockets.value = websockets.value.filter(socket => ws !== socket); 36 | }); 37 | }); 38 | 39 | const PORT = parseInt(process.env.PORT || '8080'); 40 | server.listen(PORT); 41 | -------------------------------------------------------------------------------- /src/server/reactivity.ts: -------------------------------------------------------------------------------- 1 | import VueCompositionAPI from '@vue/composition-api'; 2 | import Vue from 'vue'; 3 | 4 | Vue.use(VueCompositionAPI); 5 | 6 | export * from '@vue/composition-api'; 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "moduleResolution": "node", 5 | "resolveJsonModule": true, 6 | "esModuleInterop": true, 7 | "paths": { 8 | "/*": ["src/*"] 9 | }, 10 | 11 | "target": "esnext", 12 | "module": "esnext", 13 | 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true 18 | }, 19 | "include": ["src/**/*.ts", "src/**/*.vue"] 20 | } 21 | --------------------------------------------------------------------------------