├── .eslintrc.cjs ├── .gitignore ├── .prettierrc.js ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── auto-imports.d.ts ├── components.d.ts ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public └── favicon.ico ├── server.js ├── src ├── @types │ ├── shim-vue-jsx.d.ts │ ├── shim-vue.d.ts │ ├── source.d.ts │ └── vue-extend.d.ts ├── api │ └── market.ts ├── app.vue ├── assets │ ├── css │ │ └── index.css │ └── img │ │ └── logo.png ├── entry-client.ts ├── entry-server.js ├── main.ts ├── router │ └── index.ts ├── store │ ├── market.ts │ └── user.ts ├── styles │ └── market │ │ └── index.scss ├── utils │ └── index.ts └── views │ ├── index.tsx │ ├── market.tsx │ └── user.vue ├── tsconfig.json └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'vue-eslint-parser', 3 | parserOptions: { 4 | parser: '@typescript-eslint/parser', 5 | ecmaVersion: 2020, 6 | sourceType: 'module', 7 | ecmaFeatures: { 8 | tsx: true, 9 | jsx: true 10 | } 11 | }, 12 | extends: ['plugin:vue/vue3-recommended', 'plugin:@typescript-eslint/recommended'], 13 | rules: { 14 | camelcase: 0, 15 | semi: 'off', 16 | indent: 'off', 17 | quotes: 'off', 18 | eqeqeq: ['error', 'always'], 19 | 'no-param-reassign': 'off', 20 | 'max-len': 'off', 21 | 'vue/max-attributes-per-line': 0, 22 | 'no-angle-bracket-type-assertion': 0, 23 | 'no-var-requires': 0, 24 | 'no-return-await': 0, 25 | '@typescript-eslint/no-var-requires': 0, 26 | '@typescript-eslint/no-angle-bracket-type-assertion': 0, 27 | '@typescript-eslint/explicit-module-boundary-types': 0, 28 | 'import/no-webpack-loader-syntax': 0, 29 | // 设置默认eslint规则 30 | 'one-var': 0, 31 | 'arrow-parens': 0, 32 | 'generator-star-spacing': 0, 33 | 'no-debugger': 0, 34 | 'no-console': 0, 35 | 'no-extra-semi': 2, 36 | 'space-before-function-paren': 0, 37 | 'no-useless-escape': 0, 38 | 'no-tabs': 0, 39 | 'no-mixed-spaces-and-tabs': 0, 40 | 'new-cap': 0, 41 | 'no-new': 0, 42 | 'prefer-const': 0, 43 | 'vue/no-v-html': 0, 44 | 'lines-between-class-members': 0, 45 | 'no-unused-expressions': 0, 46 | 'no-unused-vars': 0, 47 | 'object-curly-spacing': ['error', 'always'], 48 | 'vue/singleline-html-element-content-newline': 0, 49 | 'no-trailing-spaces': ['error', {}], 50 | 'spaced-comment': ['error'], 51 | 'no-multi-spaces': ['error'], 52 | 'object-shorthand': ['error', 'always'], 53 | 'no-multiple-empty-lines': [ 54 | 'error', 55 | { 56 | max: 1, 57 | maxEOF: 1, 58 | maxBOF: 0 59 | } 60 | ], 61 | 'func-call-spacing': 'off', 62 | 'brace-style': 'off', 63 | 'comma-dangle': ['error', 'never'], 64 | 'comma-spacing': 'off', 65 | '@typescript-eslint/quotes': ['error', 'single', { allowTemplateLiterals: true }], 66 | '@typescript-eslint/comma-spacing': ['error', { before: false, after: true }], 67 | '@typescript-eslint/comma-dangle': ['error', 'never'], 68 | '@typescript-eslint/brace-style': ['error', '1tbs'], 69 | '@typescript-eslint/func-call-spacing': ['error', 'never'], 70 | '@typescript-eslint/array-type': 'off', 71 | '@typescript-eslint/no-unused-vars': [ 72 | 'error', 73 | { 74 | argsIgnorePattern: '^h$' 75 | } 76 | ], 77 | 'eslint@typescript-eslint/ban-ts-comment': 0, 78 | '@typescript-eslint/no-explicit-any': 0, 79 | '@typescript-eslint/no-this-alias': 0, 80 | '@typescript-eslint/no-inferrable-types': 0, 81 | '@typescript-eslint/semi': ['error'], 82 | '@typescript-eslint/indent': ['error', 2], 83 | '@typescript-eslint/member-delimiter-style': [ 84 | 'error', 85 | { 86 | multiline: { 87 | delimiter: 'semi', 88 | requireLast: true 89 | }, 90 | singleline: { 91 | delimiter: 'semi', 92 | requireLast: false 93 | }, 94 | multilineDetection: 'brackets' 95 | } 96 | ], 97 | '@typescript-eslint/explicit-member-accessibility': 0, 98 | '@typescript-eslint/explicit-function-return-type': 0, 99 | '@typescript-eslint/no-empty-function': 0, 100 | '@typescript-eslint/member-ordering': [ 101 | 2, 102 | { 103 | default: [ 104 | 'constructor', 105 | 'private-field', 106 | 'protected-field', 107 | 'public-field', 108 | 'field', 109 | 'private-method', 110 | 'protected-method', 111 | 'public-method', 112 | 'method' 113 | ] 114 | } 115 | ], 116 | 'vue/valid-template-root': 'off', 117 | 'vue/multi-word-component-names': 'off', 118 | 'vue/html-self-closing': [ 119 | 'error', 120 | { 121 | html: { 122 | void: 'always', 123 | normal: 'never', 124 | component: 'always' 125 | }, 126 | svg: 'always', 127 | math: 'always' 128 | } 129 | ] 130 | } 131 | }; 132 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | stats.html -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'none', 3 | printWidth: 130, 4 | bracketSpacing: true, 5 | arrowParens: 'always', 6 | tabWidth: 2, 7 | semi: true, 8 | singleQuote: true, 9 | jsxBracketSameLine: true 10 | }; 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "svn.ignoreMissingSvnWarning": true, 3 | "cSpell.words": [ 4 | "pinia" 5 | ] 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2020 vok123 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue3-ts-vite-ssr-starter 2 | 3 | - Lightning-fast cold server start 4 | - Instant hot module replacement (HMR) and dev SSR 5 | - True on-demand compilation 6 | - Use `vue3 vite4.0 typescript eslint SSR pinia unocss vue-router element-plus scss` 7 | 8 | # Getting Started 9 | - dev 10 | ```bash 11 | npm i 12 | npm run dev 13 | ``` 14 | 15 | - preview 16 | ```bash 17 | npm i 18 | npm run preview 19 | ``` 20 | 21 | 22 | - build 23 | ```bash 24 | npm i 25 | npm run build 26 | ``` 27 | 28 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-auto-import 5 | export {} 6 | declare global { 7 | const EffectScope: typeof import('vue')['EffectScope'] 8 | const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] 9 | const computed: typeof import('vue')['computed'] 10 | const createApp: typeof import('vue')['createApp'] 11 | const createPinia: typeof import('pinia')['createPinia'] 12 | const customRef: typeof import('vue')['customRef'] 13 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 14 | const defineComponent: typeof import('vue')['defineComponent'] 15 | const defineStore: typeof import('pinia')['defineStore'] 16 | const effectScope: typeof import('vue')['effectScope'] 17 | const getActivePinia: typeof import('pinia')['getActivePinia'] 18 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 19 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 20 | const h: typeof import('vue')['h'] 21 | const inject: typeof import('vue')['inject'] 22 | const isProxy: typeof import('vue')['isProxy'] 23 | const isReactive: typeof import('vue')['isReactive'] 24 | const isReadonly: typeof import('vue')['isReadonly'] 25 | const isRef: typeof import('vue')['isRef'] 26 | const mapActions: typeof import('pinia')['mapActions'] 27 | const mapGetters: typeof import('pinia')['mapGetters'] 28 | const mapState: typeof import('pinia')['mapState'] 29 | const mapStores: typeof import('pinia')['mapStores'] 30 | const mapWritableState: typeof import('pinia')['mapWritableState'] 31 | const markRaw: typeof import('vue')['markRaw'] 32 | const nextTick: typeof import('vue')['nextTick'] 33 | const onActivated: typeof import('vue')['onActivated'] 34 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 35 | const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] 36 | const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] 37 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 38 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 39 | const onDeactivated: typeof import('vue')['onDeactivated'] 40 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 41 | const onMounted: typeof import('vue')['onMounted'] 42 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 43 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 44 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 45 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 46 | const onUnmounted: typeof import('vue')['onUnmounted'] 47 | const onUpdated: typeof import('vue')['onUpdated'] 48 | const provide: typeof import('vue')['provide'] 49 | const reactive: typeof import('vue')['reactive'] 50 | const readonly: typeof import('vue')['readonly'] 51 | const ref: typeof import('vue')['ref'] 52 | const resolveComponent: typeof import('vue')['resolveComponent'] 53 | const setActivePinia: typeof import('pinia')['setActivePinia'] 54 | const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] 55 | const shallowReactive: typeof import('vue')['shallowReactive'] 56 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 57 | const shallowRef: typeof import('vue')['shallowRef'] 58 | const storeToRefs: typeof import('pinia')['storeToRefs'] 59 | const toRaw: typeof import('vue')['toRaw'] 60 | const toRef: typeof import('vue')['toRef'] 61 | const toRefs: typeof import('vue')['toRefs'] 62 | const triggerRef: typeof import('vue')['triggerRef'] 63 | const unref: typeof import('vue')['unref'] 64 | const useAttrs: typeof import('vue')['useAttrs'] 65 | const useCssModule: typeof import('vue')['useCssModule'] 66 | const useCssVars: typeof import('vue')['useCssVars'] 67 | const useLink: typeof import('vue-router')['useLink'] 68 | const useRoute: typeof import('vue-router')['useRoute'] 69 | const useRouter: typeof import('vue-router')['useRouter'] 70 | const useSlots: typeof import('vue')['useSlots'] 71 | const watch: typeof import('vue')['watch'] 72 | const watchEffect: typeof import('vue')['watchEffect'] 73 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 74 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 75 | } 76 | // for type re-export 77 | declare global { 78 | // @ts-ignore 79 | export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue' 80 | } 81 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | import '@vue/runtime-core' 7 | 8 | export {} 9 | 10 | declare module '@vue/runtime-core' { 11 | export interface GlobalComponents { 12 | ElButton: typeof import('element-plus/lib')['ElButton'] 13 | ElCard: typeof import('element-plus/lib')['ElCard'] 14 | ElForm: typeof import('element-plus/lib')['ElForm'] 15 | ElFormItem: typeof import('element-plus/lib')['ElFormItem'] 16 | ElInput: typeof import('element-plus/lib')['ElInput'] 17 | RouterLink: typeof import('vue-router')['RouterLink'] 18 | RouterView: typeof import('vue-router')['RouterView'] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vue3 App 7 | 8 | 9 | 10 | 11 |
12 | 13 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-ts-vite-ssr-starter", 3 | "version": "3.0.0", 4 | "description": "Use vue3 vite typescript eslint SSR vuex vue-router element-plus scss", 5 | "author": "vok123", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "node server", 9 | "build": "npm run build:client && npm run build:server", 10 | "build:client": "vite build --ssrManifest --outDir dist/client", 11 | "build:server": "vite build --ssr src/entry-server.js --outDir dist/server", 12 | "serve": "cross-env NODE_ENV=production node server", 13 | "preview": "npm run build && npm run serve" 14 | }, 15 | "dependencies": { 16 | "axios": "^0.21.4", 17 | "element-plus": "^2.3.4", 18 | "pinia": "2.0.22", 19 | "vue": "^3.2.47", 20 | "vue-router": "^4.1.6" 21 | }, 22 | "devDependencies": { 23 | "@rollup/plugin-alias": "^4.0.3", 24 | "@rollup/plugin-replace": "^5.0.2", 25 | "@types/node": "^14.18.36", 26 | "@typescript-eslint/eslint-plugin": "^5.59.1", 27 | "@typescript-eslint/parser": "^5.59.1", 28 | "@unocss/preset-mini": "^0.50.1", 29 | "@vitejs/plugin-vue": "^4.2.0", 30 | "@vitejs/plugin-vue-jsx": "^3.0.1", 31 | "compression": "^1.7.4", 32 | "cross-env": "^7.0.3", 33 | "eslint": "^8.35.0", 34 | "eslint-plugin-vue": "^8.5.0", 35 | "express": "^4.18.2", 36 | "postcss": "^8.4.21", 37 | "rollup-plugin-visualizer": "^5.9.0", 38 | "sass": "1.49.9", 39 | "serve-static": "^1.15.0", 40 | "typescript": "^5.0.4", 41 | "unocss": "^0.50.1", 42 | "unplugin-auto-import": "^0.15.0", 43 | "unplugin-vue-components": "^0.24.0", 44 | "vite": "^4.3.3", 45 | "vite-plugin-eslint": "^1.8.1", 46 | "vue-eslint-parser": "^9.1.0" 47 | }, 48 | "repository": { 49 | "type": "git", 50 | "url": "https://github.com/vok123/vue3-ts-vite-ssr-starter.git" 51 | }, 52 | "license": "MIT" 53 | } 54 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vok123/vue3-ts-vite-ssr-starter/49f2f4eb91cafd9e6eb5857f7cdf8ac9f5a7ac84/public/favicon.ico -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import fs from 'node:fs'; 3 | import path from 'node:path'; 4 | import express from 'express'; 5 | import axios from 'axios'; 6 | import { fileURLToPath } from 'node:url'; 7 | import adapter from 'axios/lib/adapters/http.js'; 8 | 9 | axios.defaults.adapter = adapter; 10 | const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD; 11 | const isProduction = process.env.NODE_ENV === 'production'; 12 | export async function createServer(root = process.cwd(), isProd = isProduction) { 13 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 14 | const resolve = (p) => path.resolve(__dirname, p); 15 | const indexProd = isProd ? fs.readFileSync(resolve('dist/client/index.html'), 'utf-8') : ''; 16 | const manifest = isProd ? JSON.parse(fs.readFileSync(resolve('dist/client/ssr-manifest.json'), 'utf-8')) : {}; 17 | // @ts-ignore 18 | 19 | const app = express(); 20 | 21 | let vite; 22 | if (!isProd) { 23 | vite = await ( 24 | await import('vite') 25 | ).createServer({ 26 | root, 27 | logLevel: isTest ? 'error' : 'info', 28 | server: { 29 | middlewareMode: true, 30 | watch: { 31 | usePolling: true, 32 | interval: 100 33 | } 34 | }, 35 | appType: 'custom' 36 | }); 37 | // use vite's connect instance as middleware 38 | app.use(vite.middlewares); 39 | } else { 40 | app.use((await import('compression')).default()); 41 | app.use( 42 | (await import('serve-static')).default(resolve('dist/client'), { 43 | index: false 44 | }) 45 | ); 46 | } 47 | 48 | app.use('/justTest/getFruitList', async (req, res) => { 49 | const names = ['Orange', 'Apricot', 'Apple', 'Plum', 'Pear', 'Pome', 'Banana', 'Cherry', 'Grapes', 'Peach']; 50 | const list = names.map((name, id) => { 51 | return { 52 | id: ++id, 53 | name, 54 | price: Math.ceil(Math.random() * 100) 55 | }; 56 | }); 57 | const data = { 58 | data: list, 59 | code: 0, 60 | msg: '' 61 | }; 62 | res.end(JSON.stringify(data)); 63 | }); 64 | 65 | app.use('*', async (req, res) => { 66 | try { 67 | const url = req.originalUrl; 68 | 69 | let template, render; 70 | if (!isProd) { 71 | // always read fresh template in dev 72 | template = fs.readFileSync(resolve('index.html'), 'utf-8'); 73 | template = await vite.transformIndexHtml(url, template); 74 | render = (await vite.ssrLoadModule('/src/entry-server')).render; 75 | } else { 76 | template = indexProd; 77 | render = (await import('./dist/server/entry-server.js')).render; 78 | } 79 | 80 | const [appHtml, state, links, teleports] = await render(url, manifest); 81 | 82 | const html = template 83 | .replace(``, links) 84 | .replace(`''`, state) 85 | .replace(``, appHtml) 86 | .replace(/(\n|\r\n)\s*/, teleports); 87 | 88 | res.status(200).set({ 'Content-Type': 'text/html' }).end(html); 89 | } catch (e) { 90 | vite && vite.ssrFixStacktrace(e); 91 | console.log(e.stack); 92 | res.status(500).end(e.stack); 93 | } 94 | }); 95 | 96 | return { app, vite }; 97 | } 98 | 99 | if (!isTest) { 100 | createServer().then(({ app }) => 101 | app.listen(80, () => { 102 | console.log('http://localhost:80'); 103 | }) 104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /src/@types/shim-vue-jsx.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-interface */ 2 | 3 | export interface VueSlots { 4 | [name: string]: (payload?: T) => JSX.Element | null; 5 | } 6 | 7 | export declare type VueModel = 8 | | string 9 | | number 10 | | boolean 11 | | bigint 12 | | null 13 | | undefined 14 | | symbol 15 | | Record 16 | | [unknown, string]; 17 | 18 | interface JsxComponentCustomProps { 19 | vShow?: boolean; 20 | vSlots?: VueSlots; 21 | vModel?: VueModel; 22 | vModels?: VueModel[]; 23 | vHtml?: string | JSX.Element | null; 24 | } 25 | 26 | declare module 'vue' { 27 | interface HTMLAttributes extends JsxComponentCustomProps {} 28 | 29 | interface ComponentCustomProps extends HTMLAttributes {} 30 | } 31 | -------------------------------------------------------------------------------- /src/@types/shim-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { DefineComponent } from 'vue'; 3 | 4 | const component: DefineComponent, Record, any>; 5 | export default component; 6 | } 7 | -------------------------------------------------------------------------------- /src/@types/source.d.ts: -------------------------------------------------------------------------------- 1 | declare const React: string; 2 | declare module '*.json'; 3 | declare module '*.png'; 4 | declare module '*.jpg'; 5 | 6 | declare namespace NodeJS { 7 | // eslint-disable-next-line 8 | interface ProcessEnv { 9 | NODE_ENV: 'development' | 'production'; 10 | } 11 | // eslint-disable-next-line 12 | interface Process { 13 | env: ProcessEnv; 14 | } 15 | } 16 | 17 | interface Window { 18 | __INITIAL_STATE__: any; 19 | } 20 | -------------------------------------------------------------------------------- /src/@types/vue-extend.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable spaced-comment */ 2 | import { RouteRecordRaw } from 'vue-router'; 3 | import { Pinia } from 'pinia'; 4 | 5 | export interface IAsyncDataContext { 6 | route: RouteRecordRaw; 7 | store: Pinia; 8 | } 9 | declare module '@vue/runtime-core' { 10 | interface ComponentCustomOptions { 11 | asyncData?(context: IAsyncDataContext): Promise; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/api/market.ts: -------------------------------------------------------------------------------- 1 | import Axios from 'axios'; 2 | 3 | interface IResponse { 4 | code: number; 5 | data: T; 6 | msg: string; 7 | } 8 | 9 | export interface IFruitItem { 10 | id: number; 11 | name: string; 12 | price: number; 13 | } 14 | 15 | export const getFruitList = async () => { 16 | const { data } = await Axios.get>('/justTest/getFruitList'); 17 | if (data.code === 0) { 18 | return data.data; 19 | } 20 | return []; 21 | }; -------------------------------------------------------------------------------- /src/app.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 34 | 48 | -------------------------------------------------------------------------------- /src/assets/css/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | #app { 7 | font-family: Avenir, Helvetica, Arial, sans-serif; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | text-align: center; 11 | color: #2c3e50; 12 | margin-top: 60px; 13 | } -------------------------------------------------------------------------------- /src/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vok123/vue3-ts-vite-ssr-starter/49f2f4eb91cafd9e6eb5857f7cdf8ac9f5a7ac84/src/assets/img/logo.png -------------------------------------------------------------------------------- /src/entry-client.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from './main'; 2 | import 'uno.css'; 3 | import '@/assets/css/index.css'; 4 | import 'element-plus/theme-chalk/base.css'; 5 | const { app, router, store } = createApp(); 6 | 7 | if (window.__INITIAL_STATE__) { 8 | store.state.value = JSON.parse(JSON.stringify(window.__INITIAL_STATE__)); 9 | } 10 | 11 | router.isReady().then(() => { 12 | app.mount('#app'); 13 | }); 14 | -------------------------------------------------------------------------------- /src/entry-server.js: -------------------------------------------------------------------------------- 1 | import 'uno.css'; 2 | import { renderToString } from 'vue/server-renderer'; 3 | import { createApp } from './main'; 4 | 5 | function renderPreloadLinks(modules, manifest) { 6 | let links = ''; 7 | const seen = new Set(); 8 | modules.forEach((id) => { 9 | const files = manifest[id]; 10 | if (files) { 11 | files.forEach((file) => { 12 | if (!seen.has(file)) { 13 | seen.add(file); 14 | links += renderPreloadLink(file); 15 | } 16 | }); 17 | } 18 | }); 19 | return links; 20 | } 21 | 22 | function renderPreloadLink(file) { 23 | if (file.endsWith('.js')) { 24 | return ``; 25 | } else if (file.endsWith('.css')) { 26 | return ``; 27 | } else { 28 | return ''; 29 | } 30 | } 31 | 32 | function renderTeleports(teleports) { 33 | if (!teleports) return ''; 34 | return Object.entries(teleports).reduce((all, [key, value]) => { 35 | if (key.startsWith('#el-popper-container-')) { 36 | return `${all}
${value}
`; 37 | } 38 | return all; 39 | }, teleports.body || ''); 40 | } 41 | 42 | export async function render(url, manifest) { 43 | const { app, router, store } = createApp(); 44 | try { 45 | await router.push(url); 46 | await router.isReady(); 47 | const ctx = {}; 48 | const html = await renderToString(app, ctx); 49 | const preloadLinks = renderPreloadLinks(ctx.modules, manifest); 50 | const teleports = renderTeleports(ctx.teleports); 51 | const state = JSON.stringify(store.state.value); 52 | return [html, state, preloadLinks, teleports]; 53 | } catch (error) { 54 | console.log(error); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia'; 2 | import { createSSRApp } from 'vue'; 3 | import App from './app.vue'; 4 | import createRouter from '@/router'; 5 | import { ID_INJECTION_KEY } from 'element-plus'; 6 | 7 | export function createApp() { 8 | const app = createSSRApp(App); 9 | const store = createPinia(); 10 | const router = createRouter(); 11 | app.use(store).use(router); 12 | app.provide(ID_INJECTION_KEY, { 13 | prefix: 1024, 14 | current: 0 15 | }); 16 | 17 | return { app, router, store }; 18 | } 19 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory, createMemoryHistory } from 'vue-router'; 2 | 3 | export default function () { 4 | const routerHistory = import.meta.env.SSR === false ? createWebHistory() : createMemoryHistory(); 5 | 6 | return createRouter({ 7 | history: routerHistory, 8 | routes: [ 9 | { 10 | path: '/', 11 | name: 'index', 12 | component: () => import('@/views') 13 | }, 14 | { 15 | path: '/user', 16 | name: 'user', 17 | component: () => import('@/views/user.vue') 18 | }, 19 | { 20 | path: '/market', 21 | name: 'market', 22 | component: () => import('@/views/market') 23 | } 24 | ] 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/store/market.ts: -------------------------------------------------------------------------------- 1 | import { getFruitList, IFruitItem } from '@/api/market'; 2 | import { defineStore } from 'pinia'; 3 | 4 | export interface IMarketState { 5 | fruitList: IFruitItem[]; 6 | } 7 | 8 | export const useMarket = defineStore('market', { 9 | state(): IMarketState { 10 | return { 11 | fruitList: [] 12 | }; 13 | }, 14 | actions: { 15 | async getList() { 16 | try { 17 | const data = await getFruitList(); 18 | this.fruitList = data; 19 | } catch (error) { 20 | console.log(error); 21 | } 22 | } 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /src/store/user.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | 3 | interface IUser { 4 | name: string; 5 | userId: string; 6 | token: string; 7 | } 8 | export interface IUserState { 9 | userInfo: IUser; 10 | } 11 | 12 | export const useUser = defineStore('user', { 13 | state(): IUserState { 14 | return { 15 | userInfo: { 16 | name: '', 17 | userId: '', 18 | token: '' 19 | } 20 | }; 21 | }, 22 | actions: { 23 | updateUser(info: IUser) { 24 | this.userInfo = info; 25 | }, 26 | updateToken(token: string) { 27 | this.userInfo.token = token; 28 | } 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /src/styles/market/index.scss: -------------------------------------------------------------------------------- 1 | h2 { 2 | padding-top: 10px; 3 | } 4 | .table { 5 | width: 70%; 6 | margin: 30px auto; 7 | td { 8 | text-align: center; 9 | padding: 8px 15px; 10 | border-bottom: 1px solid rgba(0, 0, 0, 0.03); 11 | } 12 | } -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const isPromise = (obj: any) => 2 | !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'; 3 | -------------------------------------------------------------------------------- /src/views/index.tsx: -------------------------------------------------------------------------------- 1 | import { ElSwitch } from 'element-plus'; 2 | import 'element-plus/theme-chalk/el-switch.css'; 3 | 4 | export default defineComponent({ 5 | name: 'Index', 6 | setup() { 7 | const isActive = ref(false); 8 | 9 | return () => ( 10 |
11 | 12 | switch 13 | 14 |
15 | ); 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /src/views/market.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/market/index.scss'; 2 | import { useMarket } from '@/store/market'; 3 | 4 | export default defineComponent({ 5 | name: 'Markets', 6 | async serverPrefetch() {}, 7 | async setup() { 8 | const marketStore = useMarket(); 9 | onServerPrefetch(async () => { 10 | await marketStore.getList(); 11 | }); 12 | 13 | onMounted(() => { 14 | marketStore.getList(); 15 | }); 16 | 17 | return () => ( 18 |
19 |

FruitList

20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {marketStore.fruitList.map((item) => { 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | })} 39 | 40 |
IDNamePrice
{item.id}{item.name}{'$' + item.price}
41 |
42 | ); 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /src/views/user.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 78 | 79 | 89 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "sourceMap": true, 6 | "strict": true, 7 | "noUnusedLocals": true, 8 | "jsx": "preserve", 9 | "noImplicitReturns": true, 10 | "moduleResolution": "node", 11 | "esModuleInterop": true, 12 | "baseUrl": ".", 13 | "types": ["axios", "node", "vite/client"], 14 | "typeRoots": ["."], 15 | "paths": { 16 | "@/*": ["src/*"] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vite'; 3 | import eslintPlugin from 'vite-plugin-eslint'; 4 | import vuePlugin from '@vitejs/plugin-vue'; 5 | import Components from 'unplugin-vue-components/vite'; 6 | import vueJsxPlugin from '@vitejs/plugin-vue-jsx'; 7 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'; 8 | import unocss from 'unocss/vite'; 9 | import presetMini from '@unocss/preset-mini'; 10 | import AutoImport from 'unplugin-auto-import/vite'; 11 | 12 | export default defineConfig({ 13 | plugins: [ 14 | vuePlugin(), 15 | vueJsxPlugin(), 16 | eslintPlugin({ 17 | cache: false, 18 | include: ['src/**/*.vue', 'src/**/*.ts', 'src/**/*.tsx'] 19 | }), 20 | unocss({ 21 | presets: [presetMini()] 22 | }), 23 | Components({ 24 | resolvers: [ElementPlusResolver({ ssr: true })], 25 | directoryAsNamespace: true 26 | }), 27 | AutoImport({ 28 | imports: ['vue', 'vue-router', 'pinia'], 29 | resolvers: [ElementPlusResolver({ ssr: true })] 30 | }) 31 | ], 32 | server: { 33 | port: 80 34 | }, 35 | resolve: { 36 | alias: { 37 | '@': path.resolve(__dirname, 'src') 38 | } 39 | } 40 | }); 41 | --------------------------------------------------------------------------------