├── .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 |
2 |
3 |
4 |

5 |
Vue3.0 Typescript Eslint SSR Starter
6 |
{{ time }}
7 |
8 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
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 | ID |
24 | Name |
25 | Price |
26 |
27 |
28 |
29 |
30 | {marketStore.fruitList.map((item) => {
31 | return (
32 |
33 | {item.id} |
34 | {item.name} |
35 | {'$' + item.price} |
36 |
37 | );
38 | })}
39 |
40 |
41 |
42 | );
43 | }
44 | });
45 |
--------------------------------------------------------------------------------
/src/views/user.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
User Page
4 |
5 |
6 | User logged in
7 | UserName: {{ userInfo.name }}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Submit
19 |
20 |
21 |
22 |
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 |
--------------------------------------------------------------------------------