node.js - Vue.js 3 SSR - قالب یا تابع رندر در طول هیدراسیون کلاینت گم شده است


من در تلاش هستم یک برنامه Vue.js 3 SSR ایجاد کنم (شامل ts، @vue/cli، babel). من از nodejs + express به عنوان بکاند استفاده میکنم. SSR به درستی کار میکند (من 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.

من متوجه شدم که خطا وقتی قالب را با تابع render جایگزین میکنم ناپدید میشود، اما دوباره در کامپوننت فرزند رخ میدهد (برای سادگی، HelloWorld کامپوننت فرزند ندارد).
اینجا ریپوی گیتهاب من است که در آن مشکل خود را بازتولید کردهام.

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">SSR_APP_CONTENT</div>
  </body>
</html>
package.json
{
    "name": "ssr3",
    "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 SSR=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 isSSR = process.env.SSR
        webpackConfig
            .entry('app')
            .clear()
            .add(isSSR ? './src/entry-server.ts' : './src/entry-client.ts')

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

        if (!isSSR) {
            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 { createSSRApp, h } from 'vue'
import App from '@/App.vue'

export const createApp = () => {
    const rootComponent = {
        render: () => h(App),
        components: { App }
    }
    const app = createSSRApp(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('SSR_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

پاسخ شماره ۱

سرانجام متوجه شدم که cache-loader باعث مشکل من شده بود.
با اجرای yarn build:server && yarn build:client، بیلد کلاینت از کامپوننتهای کش شده بیلد سرور استفاده میکرد و سپس هیچ تابع render وجود نداشت، زیرا بیلد SSR فقط تابع ssrRender را تولید میکند.
من این مشکل را با غیرفعال کردن cache-loader در تابع chainWebpack در vue.config.js حل کردهام.

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