拥抱 Vite2.0 系列(服务端渲染)

1,128 阅读7分钟

欢迎关注我的公众号《人生代码》

拥抱 Vite2.0 系列(一)

Vite提供了内置的服务器端渲染(SSR)支持。Vite playground包含了Vue 3和React的SSR设置示例,可以作为本指南的参考:

Source Structure

一个典型的SSR应用程序的源文件结构如下:

- index.htmlsrc/
  - main.js          # exports env-agnostic (universal) app code
  - entry-client.js  # mounts the app to a DOM element
  - entry-server.js  # renders the app using the framework's SSR API

index.html需要引用entry-client.js,并包含一个占位符,将服务器呈现的标记注入其中:

<div id="app"><!--ssr-outlet--></div>
<script type="module" src="/src/entry-client.js"></script>

您可以使用任何您喜欢的占位符,而不是<——ssr-outlet——>,只要可以精确替换。

Conditional Logic

如果您需要基于SSR和client执行条件逻辑,您可以使用

if (import.meta.env.SSR) {
  // ... server only logic
}

这是在构建过程中被静态替换的,因此它将允许对未使用的分支进行 tree-shaking。

Setting Up the Dev Server

在构建SSR应用程序时,您可能希望完全控制主服务器,并将Vite与生产环境解耦。建议在中间件模式下使用Vite。下面是express的一个例子:

server.js

const fs = require('fs')
const path = require('path')
const express = require('express')
const { createServer: createViteServer } = require('vite')

async function createServer() {
  const app = express()

  // Create vite server in middleware mode. This disables Vite's own HTML
  // serving logic and let the parent server take control.
  const vite = await createViteServer({
    server: { middlewareModetrue }
  })
  // use vite's connect instance as middleware
  app.use(vite.middlewares)

  app.use('*'async (req, res) => {
    // serve index.html - we will tackle this next
  })

  app.listen(3000)
}

createServer()

这里vite是ViteDevServer的一个实例。轻快地。中间件是一个连接实例,它可以在任何连接兼容的Node.js框架中用作中间件。

下一步是实现*处理程序来服务服务器渲染的HTML:

app.use('*'async (req, res) => {
  const url = req.originalUrl

  try {
    // 1. Read index.html
    let template = fs.readFileSync(
      path.resolve(__dirname, 'index.html'),
      'utf-8'
    )

    // 2. Apply vite HTML transforms. This injects the vite HMR client, and
    //    also applies HTML transforms from Vite plugins, e.g. global preambles
    //    from @vitejs/plugin-react-refresh
    template = await vite.transformIndexHtml(url, template)

    // 3. Load the server entry. vite.ssrLoadModule automatically transforms
    //    your ESM source code to be usable in Node.js! There is no bundling
    //    required, and provides efficient invalidation similar to HMR.
    const { render } = await vite.ssrLoadModule('/src/entry-server.js')

    // 4. render the app HTML. This assumes entry-server.js's exported `render`
    //    function calls appropriate framework SSR APIs,
    //    e.g. ReacDOMServer.renderToString()
    const appHtml = await render(url)

    // 5. Inject the app-rendered HTML into the template.
    const html = template.replace(`<!--ssr-outlet-->`, appHtml)

    // 6. Send the rendered HTML back.
    res.status(200).set({ 'Content-Type''text/html' }).end(html)
  } catch (e) {
    // If an error is caught, let vite fix the stracktrace so it maps back to
    // your actual source code.
    vite.ssrFixStacktrace(e)
    console.error(e)
    res.status(500).end(e.message)
  }
})
  "scripts": {
-   "dev": "vite"
+   "dev": "node server"
  }

Building for Production

为了将SSR项目交付生产,我们需要:

正常生成客户端构建;

生成一个SSR构建,它可以直接通过require()加载,这样我们就不必通过Vite的ssrLoadModule;

package.json 看起来像这样:

{
  "scripts": {
    "dev": "node server",
    "build:client": "vite build --outDir dist/client",
    "build:server": "vite build --outDir dist/server --ssr src/entry-server.js "
  }
}

注意——ssr标志,它表明这是一个ssr构建。它还应该指定SSR入口。

然后,在server.js中,我们需要通过检查process.env.NODE_ENV来添加一些特定于生产的逻辑:

而不是读取根索引。html,使用dist/client/index.html作为模板,因为它包含了到客户端构建的正确资产链接。

使用require('./dist/server/entry-server.js')代替await vite.ssrLoadModule('/src/entry-server.js')(此文件是SSR构建的结果)。

将vite开发服务器的创建和所有使用都移到dev-only条件分支后面,然后添加静态文件服务中间件来服务dist/client中的文件。

请参阅Vue和React演示以了解如何进行设置。

Generating Preload Directives

vite构建支持——ssrManifest标志,它将生成ssr-manifest.json在编译输出目录:

"build:client": "vite build --outDir dist/client","build:client": "vite build --outDir dist/client --ssrManifest",

上面的脚本现在将生成dist/client/ssr-manifest.json用于客户端构建(是的,从客户端构建生成SSR清单,因为我们希望将模块id映射到客户端文件)。清单包含模块id到它们关联的块和资产文件的映射。

为了利用清单,框架需要提供一种方法来收集在服务器呈现调用期间使用的组件的模块id。

@vitejs/plugin-vue支持这一开箱即用,并自动注册使用的组件模块id到相关的Vue SSR上下文:

// src/entry-server.js
const ctx = {}
const html = await vueServerRenderer.renderToString(app, ctx)
// ctx.modules is now a Set of module IDs that were used during the render

在server.js的生产分支中,我们需要读取清单并将其传递给src/entry-server.js导出的渲染函数。这将为我们提供足够的信息来渲染异步路由使用的文件的预加载指令!完整的示例请参见demo源代码。

Pre-Rendering / SSG

如果预先知道某些路由所需的路由和数据,我们可以使用与生产SSR相同的逻辑将这些路由预先渲染到静态HTML中。这也可以被认为是静态站点生成(SSG)的一种形式。请看演示预渲染脚本的工作示例。

SSR Externals

许多依赖都附带ESM和CommonJS文件。当运行SSR时,提供CommonJS构建的依赖关系可以从Vite的SSR转换/模块系统“外部化”,从而加速开发和构建。例如,不是将React的预绑定ESM版本拉入,然后将其转换回Node。与js兼容,简单地要求('react')代替会更有效。它还大大提高了SSR包构建的速度。

Vite基于以下启发式执行自动化的SSR外部化:

如果一个依赖项的解析ESM入口点和它的默认节点入口点不同,那么它的默认节点入口可能是一个可以外部化的CommonJS构建。例如,vue将被自动外部化,因为它同时提供ESM和CommonJS构建。

否则,Vite将检查包的入口点是否包含有效的ESM语法——如果不是,包可能是CommonJS,并将被外部化。例如,react-dom将被自动外部化,因为它只指定了CommonJS格式的单个条目。

如果这种试探导致错误,您可以使用SSR手动调整SSR外部。外部和苏维埃社会主义共和国。noExternal配置选项。

将来,这种启发式可能会改进,以检测项目是否启用了type:“module”,这样Vite也可以通过在SSR期间通过dynamic import()导入它们,将发布节点兼容ESM构建的依赖关系外部化。

SSR-specific Plugin Logic

一些框架,如Vue或苗条,会根据客户机和SSR将组件编译成不同的格式。为了支持条件转换,Vite向以下插件钩子传递了额外的ssr参数:

resolveId

负载

变换

例子:

export function mySSRPlugin() {
  return {
    name'my-ssr',
    transform(code, id, ssr) {
      if (ssr) {
        // perform ssr-specific transform...
      }
    }
  }
}