├── .eslintrc ├── .gitignore ├── .lintstagedrc ├── LICENSE ├── README.md ├── index.html ├── package.json ├── public └── favicon.ico ├── server-dev.js ├── server-prod.js ├── src ├── App.vue ├── components │ ├── AppFooter.vue │ ├── AppLink.vue │ ├── AppNavigation.vue │ ├── AppPagination.vue │ ├── ArticleDetail.vue │ ├── ArticleDetailComment.vue │ ├── ArticleDetailComments.vue │ ├── ArticleDetailCommentsForm.vue │ ├── ArticleDetailMeta.vue │ ├── ArticlesList.vue │ ├── ArticlesListArticlePreview.vue │ ├── ArticlesListNavigation.vue │ └── PopularTags.vue ├── composable │ ├── useArticles.ts │ ├── useFavoriteArticle.ts │ ├── useFollowProfile.ts │ ├── useProfile.ts │ └── useTags.ts ├── config.ts ├── entry-client.ts ├── entry-server.ts ├── pages │ ├── Article.vue │ ├── EditArticle.vue │ ├── Home.vue │ ├── Login.vue │ ├── Profile.vue │ ├── Register.vue │ └── Settings.vue ├── plugins │ ├── global-components.ts │ ├── marked.ts │ └── set-authorization-token.ts ├── router.ts ├── services │ ├── article │ │ ├── deleteArticle.ts │ │ ├── favoriteArticle.ts │ │ ├── getArticle.ts │ │ ├── getArticles.ts │ │ └── postArticle.ts │ ├── auth │ │ ├── postLogin.ts │ │ └── postRegister.ts │ ├── comment │ │ ├── getComments.ts │ │ └── postComment.ts │ ├── index.ts │ ├── profile │ │ ├── followProfile.ts │ │ ├── getProfile.ts │ │ └── putProfile.ts │ └── tag │ │ └── getTags.ts ├── shimes-vue.d.ts ├── store │ ├── init.ts │ └── user.ts ├── types │ ├── app-routes.d.ts │ ├── article.d.ts │ ├── comment.d.ts │ ├── error.ts │ ├── global.d.ts │ ├── response.d.ts │ └── user.d.ts └── utils │ ├── cookie.ts │ ├── create-async-process.ts │ ├── either.ts │ ├── filters.ts │ ├── map-checkable-response.ts │ ├── params-to-query.ts │ ├── request.ts │ └── storage.ts ├── tsconfig.json ├── vite.config.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "vue-eslint-parser", 4 | "parserOptions": { 5 | "parser": "@typescript-eslint/parser", 6 | "project": "./tsconfig.json", 7 | "sourceType": "module", 8 | "extraFileExtensions": [".vue", ".d.ts"] 9 | }, 10 | "extends": [ 11 | "standard-with-typescript", 12 | "plugin:@typescript-eslint/eslint-recommended", 13 | "plugin:@typescript-eslint/recommended", 14 | "plugin:vue/vue3-recommended" 15 | ], 16 | "rules": { 17 | "no-undef": "off", 18 | "no-unused-vars": "off", 19 | "comma-dangle": ["warn", "always-multiline"], 20 | "@typescript-eslint/promise-function-async": "off", 21 | "@typescript-eslint/strict-boolean-expressions": "off", 22 | "@typescript-eslint/no-unused-vars": "off" 23 | }, 24 | "overrides": [ 25 | { 26 | "files": [ "src/**/*.spec.ts" ], 27 | "rules": { 28 | "@typescript-eslint/no-non-null-assertion": "off", 29 | "@typescript-eslint/no-unnecessary-type-assertion": "off", 30 | "@typescript-eslint/no-explicit-any": "off" 31 | } 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | *.local 5 | .idea 6 | *.iml 7 | cypress/videos 8 | cypress/screenshots 9 | coverage 10 | .vscode 11 | yarn-error.log 12 | .husky 13 | .clinic -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "src/**/*.{ts,vue}": "eslint --fix" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Dongsen 4 | Copyright (c) 2021 levchak0910 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 Realword app with Server Side Sendering (SSR) 2 | 3 | This is an experiment with a try to clone the [original repo](https://github.com/mutoe/vue3-realworld-example-app) and add SSR using latest [Vite2](https://github.com/vitejs/vite) features 4 | 5 | 6 | 7 | # What works? 8 | 9 | - [x] [Vite](https://github.com/vitejs/vite) 10 | - [x] [Composition API](https://composition-api.vuejs.org/) 11 | - [x] [Suspense](https://v3.vuejs.org/guide/component-dynamic-async.html#using-with-suspense) 12 | - [x] [TypeScript](https://www.typescriptlang.org/) 13 | - [x] [ESLint](https://eslint.vuejs.org/) 14 | - [x] [Vue router](https://next.router.vuejs.org/) 15 | - [x] [Harlem](https://github.com/andrewcourtice/harlem) 16 | - [x] Vetur Tools: [VTI](https://vuejs.github.io/vetur/guide/vti.html) and [Interpolation](https://vuejs.github.io/vetur/guide/interpolation.html) 17 | - [x] [SSR](https://v3.vuejs.org/guide/ssr/introduction.html) with [Vite2 HMR](https://vitejs.dev/guide/ssr.html) 18 | 19 | ## Getting started 20 | 21 | ```shell script 22 | git clone https://github.com/levchak0910/vue3-ssr-realworld-example-app.git 23 | cd vue3-ssr-realworld-example-app 24 | yarn install 25 | yarn build 26 | yarn serve 27 | ``` 28 | 29 | ### For development 30 | ```shell script 31 | yarn dev 32 | ``` 33 | 34 | ### Test performance 35 | ```shell script 36 | yarn perf 37 | ``` 38 | 39 | ## Acknowledges 40 | 41 | - [@mutoe](https://github.com/mutoe) and [contributors](https://github.com/mutoe/vue3-realworld-example-app#contributors) - for original repo 42 | - [@andrewcourtice](https://github.com/andrewcourtice) - for [state manager](https://github.com/andrewcourtice/harlem) with [ssr support](https://github.com/andrewcourtice/harlem/blob/main/plugins/ssr) 43 | - [@tbgse](https://github.com/tbgse) - for [example](https://github.com/tbgse/vue3-vite-ssr-example) how to use vite for creating ssr bundles 44 | - [@yyx990803](https://github.com/yyx990803) - for another [example](https://github.com/vitejs/vite/tree/main/packages/playground/ssr-vue) how to use vite2 for creating ssr apps 45 | 46 | ## Vue related implementations of the Realworld app 47 | [gothinkster/vue-realworld-example-app](https://github.com/gothinkster/vue-realworld-example-app) - vue2, js 48 | [AlexBrohshtut/vue-ts-realworld-app](https://github.com/AlexBrohshtut/vue-ts-realworld-app) - vue2, ts, class-component 49 | [devJang/nuxt-realworld](https://github.com/devJang/nuxt-realworld) - nuxt, ts, composition api 50 | [mutoe/vue3-realworld-example-app](https://github.com/mutoe/vue3-realworld-example-app) - vue3, vite, ts, composition api 51 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Conduit 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-ssr-realworld-example-app", 3 | "version": "1.1.0", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "build": "yarn build:client && yarn build:server", 8 | "build:client": "vite build --ssrManifest --outDir dist/client", 9 | "build:server": "vite build --ssr src/entry-server --outDir dist/server", 10 | "dev": "node server-dev.js", 11 | "serve": "node server-prod.js", 12 | "check-ts-errors": "yarn tsc --noEmit && yarn vti diagnostics", 13 | "lint": "eslint \"src/**/*.{js,ts,vue}\"", 14 | "test": "yarn check-ts-errors && yarn lint", 15 | "perf": "yarn build && clinic doctor --on-port=\"sleep 5 && autocannon http://localhost:8080 -a 500 -c 50 && sleep 5 && autocannon http://localhost:8080 -a 10 -c 2 && sleep 5\" -- node server-prod.js", 16 | "prepare": "husky install" 17 | }, 18 | "dependencies": { 19 | "@harlem/core": "^1.3.1", 20 | "@harlem/plugin-ssr": "^1.3.1", 21 | "@vue/server-renderer": "3.2.27", 22 | "clinic": "^11.0.0", 23 | "cross-fetch": "^3.1.4", 24 | "insane": "^2.6.2", 25 | "js-cookie": "^2.2.1", 26 | "koa": "^2.13.1", 27 | "koa-connect": "^2.1.0", 28 | "koa-send": "^5.0.1", 29 | "marked": "^2.0.3", 30 | "vue": "3.2.27", 31 | "vue-router": "4.0.6" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.13.16", 35 | "@types/js-cookie": "^2.2.6", 36 | "@types/koa-send": "^4.1.2", 37 | "@types/marked": "^2.0.2", 38 | "@typescript-eslint/eslint-plugin": "^4.22.0", 39 | "@typescript-eslint/parser": "^4.22.0", 40 | "@vitejs/plugin-vue": "^1.2.2", 41 | "@vue/compiler-sfc": "3.2.27", 42 | "eslint": "^7.25.0", 43 | "eslint-config-standard-with-typescript": "^20.0.0", 44 | "eslint-plugin-import": "^2.22.1", 45 | "eslint-plugin-node": "^11.1.0", 46 | "eslint-plugin-promise": "^5.1.0", 47 | "eslint-plugin-vue": "^7.9.0", 48 | "husky": "^6.0.0", 49 | "lint-staged": "^10.5.4", 50 | "rollup-plugin-analyzer": "^4.0.0", 51 | "typescript": "^4.2.4", 52 | "vite": "^2.2.3", 53 | "vti": "^0.1.1" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levchak0910/vue3-ssr-realworld-example-app/49bf4737b5921d247fe0cacf94771d77aaa936d9/public/favicon.ico -------------------------------------------------------------------------------- /server-dev.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | const Koa = require('koa') 7 | const koaConnect = require('koa-connect') 8 | 9 | const vite = require('vite') 10 | 11 | const root = process.cwd() 12 | const resolve = (p) => path.resolve(__dirname, p) 13 | 14 | ;(async () => { 15 | const app = new Koa() 16 | 17 | const viteServer = await vite.createServer({ 18 | root, 19 | logLevel: 'error', 20 | server: { 21 | middlewareMode: true, 22 | }, 23 | }) 24 | app.use(koaConnect(viteServer.middlewares)) 25 | 26 | app.use(async ctx => { 27 | try { 28 | let template = fs.readFileSync(resolve('index.html'), 'utf-8') 29 | template = await viteServer.transformIndexHtml(ctx.path, template) 30 | const { render } = await viteServer.ssrLoadModule('/src/entry-server.ts') 31 | 32 | const [appHtml] = await render(ctx, {}) 33 | 34 | const html = template.replace('', appHtml) 35 | 36 | ctx.type = 'text/html' 37 | ctx.body = html 38 | } catch (e) { 39 | viteServer && viteServer.ssrFixStacktrace(e) 40 | console.log(e.stack) 41 | ctx.throw(500, e.stack) 42 | } 43 | }) 44 | 45 | app.listen(3000, () => console.log('http://localhost:3000')) 46 | })() 47 | -------------------------------------------------------------------------------- /server-prod.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | const Koa = require('koa') 4 | const sendFile = require('koa-send') 5 | 6 | const path = require('path') 7 | const fs = require('fs') 8 | 9 | const resolve = (p) => path.resolve(__dirname, p) 10 | 11 | const clientRoot = resolve('dist/client') 12 | const template = fs.readFileSync(resolve('dist/client/index.html'), 'utf-8') 13 | const render = require('./dist/server/entry-server.js').render 14 | const manifest = require('./dist/client/ssr-manifest.json') 15 | 16 | ;(async () => { 17 | const app = new Koa() 18 | 19 | app.use(async (ctx) => { 20 | // send static file 21 | if (ctx.path.startsWith('/assets')) { 22 | await sendFile(ctx, ctx.path, { root: clientRoot }) 23 | return 24 | } 25 | 26 | const [appHtml, preloadLinks] = await render(ctx, manifest) 27 | 28 | const html = template 29 | .replace('', preloadLinks) 30 | .replace('', appHtml) 31 | 32 | ctx.type = 'text/html' 33 | ctx.body = html 34 | }) 35 | 36 | app.listen(8080, () => console.log('started server on http://localhost:8080')) 37 | })() 38 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | -------------------------------------------------------------------------------- /src/components/AppFooter.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 32 | -------------------------------------------------------------------------------- /src/components/AppLink.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 42 | -------------------------------------------------------------------------------- /src/components/AppNavigation.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 100 | -------------------------------------------------------------------------------- /src/components/AppPagination.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 45 | -------------------------------------------------------------------------------- /src/components/ArticleDetail.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 77 | -------------------------------------------------------------------------------- /src/components/ArticleDetailComment.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 67 | -------------------------------------------------------------------------------- /src/components/ArticleDetailComments.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 63 | -------------------------------------------------------------------------------- /src/components/ArticleDetailCommentsForm.vue: -------------------------------------------------------------------------------- 1 |