nieyuge 3 年之前
父节点
当前提交
5609823c7d
共有 9 个文件被更改,包括 235 次插入75 次删除
  1. 3 2
      index.html
  2. 13 4
      package.json
  3. 96 0
      server.js
  4. 7 15
      src/App.vue
  5. 0 52
      src/components/HelloWorld.vue
  6. 8 0
      src/entry-client.js
  7. 69 0
      src/entry-server.js
  8. 11 2
      src/main.ts
  9. 28 0
      src/router.ts

+ 3 - 2
index.html

@@ -5,9 +5,10 @@
     <link rel="icon" href="/favicon.ico" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>Vite App</title>
+    <!--preload-links-->
   </head>
   <body>
-    <div id="app"></div>
-    <script type="module" src="/src/main.ts"></script>
+    <div id="app"><!--app-html--></div>
+    <script type="module" src="/src/entry-client.js"></script>
   </body>
 </html>

+ 13 - 4
package.json

@@ -3,17 +3,26 @@
   "private": true,
   "version": "0.0.0",
   "scripts": {
-    "dev": "vite",
-    "build": "vue-tsc --noEmit && vite build",
+    "dev": "node server",
+    "serve": "cross-env NODE_ENV=production node server",
+    "build": "npm run build:client && npm run build:server",
+    "build:client": "vite build --ssrManifest --outDir dist/client",
+    "build:server": "vite build --ssr src/entry-server.js --outDir dist/server",
     "preview": "vite preview"
   },
   "dependencies": {
-    "vue": "^3.2.25"
+    "vue": "^3.2.25",
+    "vue-router": "^4.0.15"
   },
   "devDependencies": {
     "@vitejs/plugin-vue": "^2.3.3",
+    "compression": "^1.7.4",
+    "cross-env": "^7.0.3",
+    "express": "^4.18.1",
+    "less": "^4.1.2",
+    "less-loader": "^10.2.0",
     "typescript": "^4.5.4",
     "vite": "^2.9.9",
     "vue-tsc": "^0.34.7"
   }
-}
+}

+ 96 - 0
server.js

@@ -0,0 +1,96 @@
+// @ts-check
+const fs = require('fs')
+const path = require('path')
+const express = require('express')
+
+const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD
+
+async function createServer(
+  root = process.cwd(),
+  isProd = process.env.NODE_ENV === 'production'
+) {
+  const resolve = (p) => path.resolve(__dirname, p)
+
+  const indexProd = isProd
+    ? fs.readFileSync(resolve('dist/client/index.html'), 'utf-8')
+    : ''
+
+  const manifest = isProd
+    ? // @ts-ignore
+      require('./dist/client/ssr-manifest.json')
+    : {}
+
+  const app = express()
+
+  /**
+   * @type {import('vite').ViteDevServer}
+   */
+  let vite
+  if (!isProd) {
+    vite = await require('vite').createServer({
+      root,
+      logLevel: isTest ? 'error' : 'info',
+      server: {
+        middlewareMode: 'ssr',
+        watch: {
+          // During tests we edit the files too fast and sometimes chokidar
+          // misses change events, so enforce polling for consistency
+          usePolling: true,
+          interval: 100
+        }
+      }
+    })
+    // use vite's connect instance as middleware
+    app.use(vite.middlewares)
+  } else {
+    app.use(require('compression')())
+    app.use(
+      require('serve-static')(resolve('dist/client'), {
+        index: false
+      })
+    )
+  }
+
+  app.use('*', async (req, res) => {
+    try {
+      const url = req.originalUrl
+
+      let template, render
+      if (!isProd) {
+        // always read fresh template in dev
+        template = fs.readFileSync(resolve('index.html'), 'utf-8')
+        template = await vite.transformIndexHtml(url, template)
+        render = (await vite.ssrLoadModule('/src/entry-server.js')).render
+      } else {
+        template = indexProd
+        // @ts-ignore
+        render = require('./dist/server/entry-server.js').render
+      }
+
+      const [appHtml, preloadLinks] = await render(url, manifest)
+
+      const html = template
+        .replace(`<!--preload-links-->`, preloadLinks)
+        .replace(`<!--app-html-->`, appHtml)
+
+      res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
+    } catch (e) {
+      vite && vite.ssrFixStacktrace(e)
+      console.log(e.stack)
+      res.status(500).end(e.stack)
+    }
+  })
+
+  return { app, vite }
+}
+
+if (!isTest) {
+  createServer().then(({ app }) =>
+    app.listen(5173, () => {
+      console.log('http://localhost:5173')
+    })
+  )
+}
+
+// for test use
+exports.createServer = createServer

+ 7 - 15
src/App.vue

@@ -1,21 +1,13 @@
-<script setup lang="ts">
-// This starter template is using Vue 3 <script setup> SFCs
-// Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
-import HelloWorld from './components/HelloWorld.vue'
-</script>
 
 <template>
-  <img alt="Vue logo" src="./assets/logo.png" />
-  <HelloWorld msg="Hello Vue 3 + TypeScript + Vite" />
+  <div class="a">111</div>
 </template>
 
-<style>
-#app {
-  font-family: Avenir, Helvetica, Arial, sans-serif;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-  text-align: center;
-  color: #2c3e50;
-  margin-top: 60px;
+<script setup lang="ts">
+</script>
+
+<style lang="less">
+.a {
+    color: red;
 }
 </style>

+ 0 - 52
src/components/HelloWorld.vue

@@ -1,52 +0,0 @@
-<script setup lang="ts">
-import { ref } from 'vue'
-
-defineProps<{ msg: string }>()
-
-const count = ref(0)
-</script>
-
-<template>
-  <h1>{{ msg }}</h1>
-
-  <p>
-    Recommended IDE setup:
-    <a href="https://code.visualstudio.com/" target="_blank">VS Code</a>
-    +
-    <a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
-  </p>
-
-  <p>See <code>README.md</code> for more information.</p>
-
-  <p>
-    <a href="https://vitejs.dev/guide/features.html" target="_blank">
-      Vite Docs
-    </a>
-    |
-    <a href="https://v3.vuejs.org/" target="_blank">Vue 3 Docs</a>
-  </p>
-
-  <button type="button" @click="count++">count is: {{ count }}</button>
-  <p>
-    Edit
-    <code>components/HelloWorld.vue</code> to test hot module replacement.
-  </p>
-</template>
-
-<style scoped>
-a {
-  color: #42b983;
-}
-
-label {
-  margin: 0 0.5em;
-  font-weight: bold;
-}
-
-code {
-  background-color: #eee;
-  padding: 2px 4px;
-  border-radius: 4px;
-  color: #304455;
-}
-</style>

+ 8 - 0
src/entry-client.js

@@ -0,0 +1,8 @@
+import { createApp } from './main'
+
+const { app, router } = createApp()
+
+// wait until router is ready before mounting to ensure hydration match
+router.isReady().then(() => {
+  app.mount('#app')
+})

+ 69 - 0
src/entry-server.js

@@ -0,0 +1,69 @@
+import { createApp } from './main'
+import { renderToString } from 'vue/server-renderer'
+import path, { basename } from 'path'
+
+export async function render(url, manifest) {
+    const { app, router } = createApp()
+
+    // set the router to the desired URL before rendering
+    router.push(url)
+    await router.isReady()
+
+    // passing SSR context object which will be available via useSSRContext()
+    // @vitejs/plugin-vue injects code into a component's setup() that registers
+    // itself on ctx.modules. After the render, ctx.modules would contain all the
+    // components that have been instantiated during this render call.
+    const ctx = {}
+    const html = await renderToString(app, ctx)
+
+    // the SSR manifest generated by Vite contains module -> chunk/asset mapping
+    // which we can then use to determine what files need to be preloaded for this
+    // request.
+    const preloadLinks = renderPreloadLinks(ctx.modules, manifest)
+    return [html, preloadLinks]
+}
+
+function renderPreloadLinks(modules, manifest) {
+    let links = ''
+    const seen = new Set()
+    modules.forEach((id) => {
+        const files = manifest[id]
+        if (files) {
+            files.forEach((file) => {
+                if (!seen.has(file)) {
+                    seen.add(file)
+                    const filename = basename(file)
+                    if (manifest[filename]) {
+                        for (const depFile of manifest[filename]) {
+                            links += renderPreloadLink(depFile)
+                            seen.add(depFile)
+                        }
+                    }
+                    links += renderPreloadLink(file)
+                }
+            })
+        }
+    })
+    return links
+}
+
+function renderPreloadLink(file) {
+    if (file.endsWith('.js')) {
+        return `<link rel="modulepreload" crossorigin href="${file}">`
+    } else if (file.endsWith('.css')) {
+        return `<link rel="stylesheet" href="${file}">`
+    } else if (file.endsWith('.woff')) {
+        return ` <link rel="preload" href="${file}" as="font" type="font/woff" crossorigin>`
+    } else if (file.endsWith('.woff2')) {
+        return ` <link rel="preload" href="${file}" as="font" type="font/woff2" crossorigin>`
+    } else if (file.endsWith('.gif')) {
+        return ` <link rel="preload" href="${file}" as="image" type="image/gif">`
+    } else if (file.endsWith('.jpg') || file.endsWith('.jpeg')) {
+        return ` <link rel="preload" href="${file}" as="image" type="image/jpeg">`
+    } else if (file.endsWith('.png')) {
+        return ` <link rel="preload" href="${file}" as="image" type="image/png">`
+    } else {
+        // TODO
+        return ''
+    }
+}

+ 11 - 2
src/main.ts

@@ -1,4 +1,13 @@
-import { createApp } from 'vue'
 import App from './App.vue'
+import { createSSRApp } from 'vue'
+import { createRouter } from './router'
 
-createApp(App).mount('#app')
+// SSR requires a fresh app instance per request, therefore we export a function
+// that creates a fresh app instance. If using Vuex, we'd also be creating a
+// fresh store here.
+export function createApp() {
+  const app = createSSRApp(App)
+  const router = createRouter()
+  app.use(router)
+  return { app, router }
+}

+ 28 - 0
src/router.ts

@@ -0,0 +1,28 @@
+import {
+    createMemoryHistory,
+    createRouter as _createRouter,
+    createWebHistory
+  } from 'vue-router'
+  
+  // Auto generates routes from vue files under ./pages
+  // https://vitejs.dev/guide/features.html#glob-import
+  const pages = import.meta.glob('./pages/*.vue')
+  
+  const routes = Object.keys(pages).map((path) => {
+    // @ts-ignore
+    const name = path.match(/\.\/pages(.*)\.vue$/)[1].toLowerCase()
+    return {
+      path: name === '/home' ? '/' : name,
+      component: pages[path] // () => import('./pages/*.vue')
+    }
+  })
+  
+  export function createRouter() {
+    return _createRouter({
+      // use appropriate history implementation for server/client
+      // import.meta.env.SSR is injected by Vite.
+      history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(),
+      routes
+    })
+  }
+