Vue.js 3 s-s-r - 客户端水合期间缺少模板或渲染功能

Posted

技术标签:

【中文标题】Vue.js 3 s-s-r - 客户端水合期间缺少模板或渲染功能【英文标题】:Vue.js 3 s-s-r - missing template or render function during client hydration 【发布时间】:2021-02-18 04:07:20 【问题描述】:

我正在尝试创建 Vue.js 3 s-s-r 应用程序(包括 ts、@vue/cli、babel)。我使用 nodejs + express 作为后端。 s-s-r 工作正常(我正在从服务器正确呈现 html),但在客户端水合期间发生错误。似乎我的客户端构建不包含组件的模板,因为我在浏览器中遇到了这些错误

Vue warn]: Component is missing template or render function. 
  at <App> 
  at <App>

Vue warn]: Hydration node mismatch:
- Client vnode: Symbol(Comment) 
- Server rendered DOM: <div class=​"hello-world">​hello​</div>​  
  at <App> 
  at <App>

runtime-core.cjs.js:2942 Hydration completed but contains mismatches.

我发现当我用渲染函数替换模板时错误消失了,但它再次出现在子组件中(为了简单起见,HelloWorld没有子组件)。 这是my github repo,我在那里重现了我的问题。

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
  </head>
  <body>
    <div id="app">s-s-r_APP_CONTENT</div>
  </body>
</html>

package.json


    "name": "s-s-r3",
    "version": "0.1.0",
    "private": true,
    "scripts": 
        "serve": "vue-cli-service serve",
        "build": "rimraf dist && yarn build:server && yarn build:client",
        "build:server": "cross-env s-s-r=true vue-cli-service build --dest dist/server --mode development",
        "build:client": "vue-cli-service build --dest dist/client --mode development",
        "lint": "vue-cli-service lint"
    ,
    "dependencies": 
        "core-js": "^3.6.5",
        "vue": "^3.0.0",
        "vue-router": "^4.0.0-0",
        "@vue/server-renderer": "^3.0.0",
        "class-transformer": "^0.3.1",
        "express": "^4.17.1",
        "reflect-metadata": "^0.1.13",
        "webpack-manifest-plugin": "^2.2.0",
        "webpack-node-externals": "^2.5.2"
    ,
    "devDependencies": 
        "@typescript-eslint/eslint-plugin": "^2.33.0",
        "@typescript-eslint/parser": "^2.33.0",
        "@vue/cli-plugin-babel": "~4.5.0",
        "@vue/cli-plugin-eslint": "~4.5.0",
        "@vue/cli-plugin-router": "~4.5.0",
        "@vue/cli-plugin-typescript": "~4.5.0",
        "@vue/cli-service": "~4.5.0",
        "@vue/compiler-sfc": "^3.0.0",
        "@vue/eslint-config-prettier": "^6.0.0",
        "@vue/eslint-config-typescript": "^5.0.2",
        "eslint": "^6.7.2",
        "eslint-plugin-prettier": "^3.1.3",
        "eslint-plugin-vue": "^7.0.0-0",
        "node-sass": "^4.12.0",
        "prettier": "^1.19.1",
        "sass-loader": "^8.0.2",
        "typescript": "~3.9.3",
        "cross-env": "^7.0.2",
        "husky": "^4.2.5",
        "lint-staged": "^10.2.11"
    ,
    "eslintConfig": 
        "root": true,
        "env": 
            "node": true
        ,
        "extends": [
            "plugin:vue/vue3-essential",
            "eslint:recommended",
            "@vue/typescript/recommended",
            "@vue/prettier",
            "@vue/prettier/@typescript-eslint"
        ],
        "parserOptions": 
            "ecmaVersion": 2020
        ,
        "rules": 
    ,
    "prettier": 
        "semi": false,
        "singleQuote": true,
        "tabWidth": 4,
        "arrowParens": "avoid",
        "printWidth": 100
    ,
    "browserslist": [
        "> 1%",
        "last 2 versions",
        "not dead"
    ]

vue.config.js

const ManifestPlugin = require('webpack-manifest-plugin')
const nodeExternals = require('webpack-node-externals')
const webpack = require('webpack')

module.exports = 
    configureWebpack: 
        resolve:  mainFields: ['main', 'module'] 
    ,

    chainWebpack: webpackConfig => 
        const iss-s-r = process.env.s-s-r
        webpackConfig
            .entry('app')
            .clear()
            .add(iss-s-r ? './src/entry-server.ts' : './src/entry-client.ts')

        webpackConfig.plugin('manifest').use(new ManifestPlugin( fileName: 'manifest.json' ))

        if (!iss-s-r) 
            return
        

        webpackConfig.target('node')
        webpackConfig.output.libraryTarget('commonjs2')

        webpackConfig.externals(nodeExternals( allowlist: /\.(css|vue)$/ ))

        webpackConfig.optimization.splitChunks(false).minimize(false)

        webpackConfig.plugins.delete('hmr')
        webpackConfig.plugins.delete('preload')
        webpackConfig.plugins.delete('prefetch')
        webpackConfig.plugins.delete('progress')
        webpackConfig.plugin('limit').use(
            new webpack.optimize.LimitChunkCountPlugin(
                maxChunks: 1
            )
        )
    

App.vue

<template>
    <HelloWorld />
</template>

<script lang="ts">
    import  defineComponent  from 'vue'
    import HelloWorld from '@/components/HelloWorld.vue'

    export default defineComponent(
        components:  HelloWorld 
    )
</script>

HelloWorld.vue

<template>
    <div class="hello-world">hello</div>
</template>

<script lang="ts">
    import  defineComponent  from 'vue'
    export default defineComponent()
</script>

app.ts

import  creates-s-rApp, h  from 'vue'
import App from '@/App.vue'

export const createApp = () => 
    const rootComponent = 
        render: () => h(App),
        components:  App 
    
    const app = creates-s-rApp(rootComponent)
    return  app 

entry-server.ts

import  createApp  from '@/app'

export default async () => 
    const  app  = createApp()
    return app

entry-client.ts

import  createApp  from '@/app.ts'

const  app  = createApp()
app.mount('#app', true)

server.js

const path = require('path')
const fs = require('fs')
const express = require('express')
const  renderToString  = require('@vue/server-renderer')
const serverManifest = require('./dist/server/manifest.json')

const server = express()

const appPath = path.join(__dirname, '/dist/server', serverManifest['app.js'])
const createApp = require(appPath).default

server.use('/img', express.static(path.join(__dirname, '/dist/client', 'img')))
server.use('/js', express.static(path.join(__dirname, '/dist/client', 'js')))
server.use('/css', express.static(path.join(__dirname, '/dist/client', 'css')))

server.get(['/*'], async (req, res) => 
    const app = await createApp()
    const appContent = await renderToString(app)

    fs.readFile(path.join(__dirname, '/dist/client/index.html'), (err, html) => 
        if (err) 
            throw err
        

        html = html.toString().replace('s-s-r_APP_CONTENT', `$appContent`)
        res.setHeader('Content-Type', 'text/html')
        res.send(html)
    )
)

server.listen(80)

我的设置

- macOS Catalina 10.15.5
- Nodejs v12.19.0
- Google Chrome v86.0.4240.111

【问题讨论】:

【参考方案1】:

我终于发现是cache-loader 导致了我的问题。 通过运行yarn build:server &amp;&amp; yarn build:client,客户端构建使用来自服务器构建的缓存组件,然后没有render 函数,因为s-s-r 构建只产生s-s-rRender 函数。 我已经通过在vue.config.js 中禁用chainWebpack 函数的缓存加载器来修复它。

chainWebpack: webpackConfig => 
    ...
    webpackConfig.module.rule('vue').uses.delete('cache-loader')
    webpackConfig.module.rule('js').uses.delete('cache-loader')
    webpackConfig.module.rule('ts').uses.delete('cache-loader')
    ...

参考:https://forum.vuejs.org/t/disable-cache-loader-in-webpack-4-vue-cli-3/57561

【讨论】:

以上是关于Vue.js 3 s-s-r - 客户端水合期间缺少模板或渲染功能的主要内容,如果未能解决你的问题,请参考以下文章

提取 CSS 以用于 s-s-r (Vue.js)

vue.js - 未在实例上定义,但在渲染期间引用

如何在同构或 s-s-r 应用程序中使用 MongoDB Stitch Auth?

从组件库导入的组件样式在 s-s-r 期间未应用

Fluxible 中的“脱水”和“再水合”代表啥?

将 peerjs 与 nuxtjs (vue.js) 集成时出错