1 Star 1 Fork 0

ifer / vue_ssr

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README

01. 渲染一个 Vue 实例

npm init -y
npm i vue vue-server-renderer

server.js

const Vue = require('vue')
const renderer = require('vue-server-renderer').createRenderer()
const app = new Vue({
    template: `
    <div>
        <h1>{{msg}}</h1>
    </div>
    `,
    data() {
        return {
            msg: '牛批',
        }
    },
})

renderer.renderToString(app, (err, html) => {
    if (err) throw err
    console.log(html)
})

02. 结合到 Web 服务中

npm i express

server.js

const Vue = require('vue')
const express = require('express')
const renderer = require('vue-server-renderer').createRenderer()

const server = express()

server.get('/', (req, res) => {
    const app = new Vue({
        template: `
        <div>
            <h1>{{msg}}</h1>
        </div>
        `,
        data() {
            return {
                msg: '牛批',
            }
        },
    })

    renderer.renderToString(app, (err, html) => {
        if (err) {
            return res.status(500).end('Internal Server Error.')
        }
        res.setHeader('Content-Type', 'text/html; charset=utf8')
        res.end(`
        <!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">
            <title>Document</title>
        </head>

        <body>
            ${html}
        </body>

        </html>
        `)
    })
})

server.listen(3000, () =>
    console.log('Server running at http://localhost:3000')
)

03. 使用 HTML 模板

server.js

const fs = require('fs')
const Vue = require('vue')
const express = require('express')
const renderer = require('vue-server-renderer').createRenderer({
    template: fs.readFileSync('./index.template.html', 'utf-8'),
})

const server = express()

server.get('/', (req, res) => {
    const app = new Vue({
        template: `
        <div>
            <h1>{{msg}}</h1>
        </div>
        `,
        data() {
            return {
                msg: '牛批',
            }
        },
    })

    renderer.renderToString(app, (err, html) => {
        if (err) {
            return res.status(500).end('Internal Server Error.')
        }
        res.setHeader('Content-Type', 'text/html; charset=utf8')
        res.end(html)
    })
})

server.listen(3000, () =>
    console.log('Server running at http://localhost:3000')
)

index.template.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">
    <title>Document</title>
</head>

<body>
    <!--vue-ssr-outlet-->
</body>

</html>

04. 在模板中使用外部数据

server.js

const fs = require('fs')
const Vue = require('vue')
const express = require('express')
const renderer = require('vue-server-renderer').createRenderer({
    template: fs.readFileSync('./index.template.html', 'utf-8'),
})

const server = express()

server.get('/', (req, res) => {
    const app = new Vue({
        template: `
        <div>
            <h1>{{msg}}</h1>
        </div>
        `,
        data() {
            return {
                msg: '牛批',
            }
        },
    })

    renderer.renderToString(
        app,
        {
            title: 'SSR',
            meta: '<meta name="description" content="SSR 学习">',
        },
        (err, html) => {
            if (err) {
                return res.status(500).end('Internal Server Error.')
            }
            res.setHeader('Content-Type', 'text/html; charset=utf8')
            res.end(html)
        }
    )
})

server.listen(3000, () =>
    console.log('Server running at http://localhost:3000')
)
<!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">
    {{{meta}}}
    <title>{{title}}</title>
</head>

<body>
    <!--vue-ssr-outlet-->
</body>

</html>

05. 【构建配置】问题展示

server.js

const fs = require('fs')
const Vue = require('vue')
const express = require('express')
const renderer = require('vue-server-renderer').createRenderer({
    template: fs.readFileSync('./index.template.html', 'utf-8'),
})

const server = express()

server.get('/', (req, res) => {
    const app = new Vue({
        template: `
        <div>
            <h1>{{msg}}</h1>
            <div>
                <input v-model="msg"/>
            </div>
            <div>
                <button @click="handleClick">测试</button>
            </div>
        </div>
        `,
        data() {
            return {
                msg: '牛批',
            }
        },
        methods: {
            handleClick() {
                console.log('两横一竖')
            },
        },
    })

    renderer.renderToString(
        app,
        {
            title: 'SSR',
            meta: '<meta name="description" content="SSR 学习">',
        },
        (err, html) => {
            if (err) {
                return res.status(500).end('Internal Server Error.')
            }
            res.setHeader('Content-Type', 'text/html; charset=utf8')
            res.end(html)
        }
    )
})

server.listen(3000, () =>
    console.log('Server running at http://localhost:3000')
)

06. 【构建配置】源码结构

src/app.js

import Vue from 'vue'
import App from './App.vue'

export function createApp() {
    const app = new Vue({
        render: (h) => h(App),
    })
    return { app }
}

src/App.vue

<template>
    <div id="app">
        <h1>{{ msg }}</h1>
        <div>
            <input v-model="msg" />
        </div>
        <div>
            <button @click="handleClick">测试</button>
        </div>
    </div>
</template>

<script>
export default {
    name: 'App',
    data() {
        return {
            msg: '牛批',
        }
    },
    methods: {
        handleClick() {
            console.log('两横一竖')
        },
    },
}
</script>

src/entry-client.js

import { createApp } from './app'

const { app } = createApp()

app.$mount('#app')

src/entry-server.js

import { createApp } from './app'

export default (context) => {
    const { app } = createApp()
    // 服务端的路由处理、数据预取...
    return app
}

07. 【构建配置】安装依赖

npm i cross-env # 可以通过 npm scripts 设置跨平台的环境变量,以区分打包环境
npm i -D webpack webpack-cli webpack-merge webpack-node-externals @babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader css-loader url-loader file-loader rimraf vue-loader vue-template-compiler friendly-errors-webpack-plugin

08. 【构建配置】Webpack

build/webpack.base.config.js

/**
 * 公共配置
 */
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const path = require('path')
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
const resolve = (file) => path.resolve(__dirname, file)
const isProd = process.env.NODE_ENV === 'production'
module.exports = {
    mode: isProd ? 'production' : 'development',
    output: {
        path: resolve('../dist/'),
        publicPath: '/dist/', // 打包结果的路径前缀
        filename: '[name].[chunkhash].js',
    },
    resolve: {
        alias: {
            // 路径别名,@ 指向 src
            '@': resolve('../src/'),
        },
        // 当省略扩展名的时候,按照从前往后的顺序依次解析
        extensions: ['.js', '.vue', '.json'],
    },
    devtool: isProd ? 'source-map' : 'cheap-module-eval-source-map',
    module: {
        rules: [
            // 处理图片资源
            {
                test: /\.(png|jpg|gif)$/i,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 8192,
                        },
                    },
                ],
            },
            // 处理字体资源
            {
                test: /\.(woff|woff2|eot|ttf|otf)$/,
                use: ['file-loader'],
            },
            // 处理 .vue 资源
            {
                test: /\.vue$/,
                loader: 'vue-loader',
            },
            // 处理 CSS 资源
            // 它会应用到普通的 `.css` 文件
            // 以及 `.vue` 文件中的 `<style>` 块
            {
                test: /\.css$/,
                use: ['vue-style-loader', 'css-loader'],
            },
            // CSS 预处理器,参考:https://vue-loader.vuejs.org/zh/guide/preprocessors.html
            // 例如处理 Less 资源
            // {
            //     test: /\.less$/,
            //     use: ['vue-style-loader', 'css-loader', 'less-loader'],
            // },
        ],
    },
    plugins: [new VueLoaderPlugin(), new FriendlyErrorsWebpackPlugin()],
}

build/webpack.client.config.js

/**
 * 客户端打包配置
 */
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base.config.js')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
module.exports = merge(baseConfig, {
    entry: {
        app: './src/entry-client.js', // 注意这里的相对路径相对的是命令行的执行目录
    },
    module: {
        rules: [
            // ES6 转 ES5
            {
                test: /\.m?js$/,
                exclude: /(node_modules|bower_components)/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env'],
                        cacheDirectory: true,
                        plugins: ['@babel/plugin-transform-runtime'],
                    },
                },
            },
        ],
    },
    // 重要信息:这将 webpack 运行时分离到一个引导 chunk 中,以便可以在之后正确注入异步 chunk。
    optimization: {
        splitChunks: {
            name: 'manifest',
            minChunks: Infinity,
        },
    },
    plugins: [
        // 此插件在输出目录中生成 `vue-ssr-client-manifest.json`。
        new VueSSRClientPlugin(),
    ],
})

build/webpack.server.config.js

/**
 * 服务端打包配置
 */
const { merge } = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.config.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
module.exports = merge(baseConfig, {
    // 将 entry 指向应用程序的 server entry 文件
    entry: './src/entry-server.js',
    // 这允许 webpack 以 Node 适用方式处理模块加载
    // 并且还会在编译 Vue 组件时,
    // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
    target: 'node',
    output: {
        filename: 'server-bundle.js',
        // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)
        libraryTarget: 'commonjs2',
    },
    // 不打包 node_modules 第三方包,而是保留 require 方式直接加载
    externals: [
        nodeExternals({
            // 白名单中的资源依然正常打包
            allowlist: [/\.css$/],
        }),
    ],
    plugins: [
        // 这是将服务器的整个输出构建为单个 JSON 文件的插件。
        // 默认文件名为 `vue-ssr-server-bundle.json`
        new VueSSRServerPlugin(),
    ],
})

09. 【构建配置】配置命令

package.json

{
    "scripts": {
        "build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js",
        "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js",
        "build": "rimraf dist && npm run build:client && npm run build:server"
    },
}

测试

npm run build:client
npm run build:server
npm run build

10. 【构建配置】启动程序

server.js

const fs = require('fs')
const Vue = require('vue')
const express = require('express')

const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const template = fs.readFileSync('./index.template.html', 'utf-8')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')

// 注意:换成了 createBundleRenderer
const renderer = require('vue-server-renderer').createBundleRenderer(
    serverBundle,
    {
        template,
        clientManifest,
    }
)

const server = express()

// 注意:托管静态资源,应该和打包出口当中的 publicPath 保持一致
server.use('/dist', express.static('./dist'))

server.get('/', (req, res) => {
    // !删除了 app 实例,因为会自动去 entry-server.js 里面找到实例
    renderer.renderToString(
        {
            title: 'SSR',
            meta: '<meta name="description" content="SSR 学习">',
        },
        (err, html) => {
            if (err) {
                return res.status(500).end('Internal Server Error.')
            }
            res.setHeader('Content-Type', 'text/html; charset=utf8')
            res.end(html)
        }
    )
})

server.listen(3000, () =>
    console.log('Server running at http://localhost:3000')
)

11. 【构建配置】解析渲染流程

12. 【构建配置】【开发模式】基本思路

期望自动打包,热更新!

package.json

{
    "scripts": {
        "start": "cross-env NODE_ENV=production && node server.js",
        "dev": "node server.js"
    },
}

server.js

const fs = require('fs')
const Vue = require('vue')
const express = require('express')

const isProd = process.env.NODE_ENV === 'production'

let renderer
if (isProd) {
    const serverBundle = require('./dist/vue-ssr-server-bundle.json')
    const template = fs.readFileSync('./index.template.html', 'utf-8')
    const clientManifest = require('./dist/vue-ssr-client-manifest.json')

    renderer = require('vue-server-renderer').createBundleRenderer(
        serverBundle,
        {
            template,
            clientManifest,
        }
    )
} else {
    // 开发 -> 监听打包构建 -> 重新生成 Renderer 渲染器
}

const server = express()

server.use('/dist', express.static('./dist'))

const render = (req, res) => {
    // !删除了 app 实例,因为会自动去 entry-server.js 里面找到实例
    renderer.renderToString(
        {
            title: 'SSR',
            meta: '<meta name="description" content="SSR 学习">',
        },
        (err, html) => {
            if (err) {
                return res.status(500).end('Internal Server Error.')
            }
            res.setHeader('Content-Type', 'text/html; charset=utf8')
            res.end(html)
        }
    )
}

server.get(
    '/',
    isProd
        ? render
        : (req, res) => {
              // 等待有了 Renderer 渲染器以后,调用 render 进行渲染
              render()
          }
)

server.listen(3000, () =>
    console.log('Server running at http://localhost:3000')
)

13. 【构建配置】【开发模式】提取模块

提取处理模块

const fs = require('fs')
const Vue = require('vue')
const express = require('express')
const server = express()
const { createBundleRenderer } = require('vue-server-renderer')
const setupDevServer = require('./build/setup-dev-server')

const isProd = process.env.NODE_ENV === 'production'

server.use('/dist', express.static('./dist'))
let renderer
let onReady
if (isProd) {
    const serverBundle = require('./dist/vue-ssr-server-bundle.json')
    const template = fs.readFileSync('./index.template.html', 'utf-8')
    const clientManifest = require('./dist/vue-ssr-client-manifest.json')

    renderer = createBundleRenderer(serverBundle, {
        template,
        clientManifest,
    })
} else {
    // 开发 -> 监听打包构建 -> 重新生成 Renderer 渲染器
    onReady = setupDevServer(
        server,
        (serverBundle, template, clientManifest) => {
            renderer = createBundleRenderer(serverBundle, {
                template,
                clientManifest,
            })
        }
    )
}

const render = (req, res) => {
    // !删除了 app 实例,因为会自动去 entry-server.js 里面找到实例
    renderer.renderToString(
        {
            title: 'SSR',
            meta: '<meta name="description" content="SSR 学习">',
        },
        (err, html) => {
            if (err) {
                return res.status(500).end('Internal Server Error.')
            }
            res.setHeader('Content-Type', 'text/html; charset=utf8')
            res.end(html)
        }
    )
}

server.get(
    '/',
    isProd
        ? render
        : async (req, res) => {
              // 等待有了 Renderer 渲染器以后,调用 render 进行渲染
              await onReady()
              render()
          }
)

server.listen(3000, () =>
    console.log('Server running at http://localhost:3000')
)

build/setup-dev-server.js

module.exports = function(server, callback) {
    const onReady = new Promise()
    // 监视构建 -> 更新 Renderer
    return onReady
}

14. 【构建配置】【开发模式】update

build/setup-dev-server.js

module.exports = function(server, callback) {
    let ready
    const onReady = new Promise((r) => (ready = r))
    // 监视构建 -> 更新 Renderer
    let template
    let serverBundle
    let clientManifest

    const update = () => {
        if (template && serverBundle && clientManifest) {
            ready()
            callback(serverBundle, template, clientManifest)
        }
    }

    // 监视构建 template -> 调用 update -> 更新 Renderer 渲染器
    // 监视构建 serverBundle -> 调用 update -> 更新 Renderer 渲染器
    // 监视构建 clientManifest -> 调用 update -> 更新 Renderer 渲染器
    return onReady
}

15. 【构建配置】【开发模式】处理模板

npm i chokidar
const fs = require('fs')
const path = require('path')
const chokidar = require('chokidar')

module.exports = function(server, callback) {
    let ready
    const onReady = new Promise((r) => (ready = r))
    // 监视构建 -> 更新 Renderer
    let template
    let serverBundle
    let clientManifest

    const update = () => {
        if (template && serverBundle && clientManifest) {
            ready()
            callback(serverBundle, template, clientManifest)
        }
    }

    // 监视构建 template -> 调用 update -> 更新 Renderer 渲染器
    const templatePath = path.resolve(__dirname, '../index.template.html')
    template = fs.readFileSync(templatePath, 'utf8')
    update()
    chokidar.watch(templatePath).on('change', () => {
        template = fs.readFileSync(templatePath, 'utf8')
        update()
    })
    // 监视构建 serverBundle -> 调用 update -> 更新 Renderer 渲染器
    // 监视构建 clientManifest -> 调用 update -> 更新 Renderer 渲染器
    return onReady
}

16. 【构建配置】【开发模式】服务端监听

const fs = require('fs')
const path = require('path')
const chokidar = require('chokidar')
const webpack = require('webpack')
const resolve = (file) => path.resolve(__dirname, file)

module.exports = function(server, callback) {
    let ready
    const onReady = new Promise((r) => (ready = r))
    // 监视构建 -> 更新 Renderer
    let template
    let serverBundle
    let clientManifest

    const update = () => {
        if (template && serverBundle && clientManifest) {
            ready()
            callback(serverBundle, template, clientManifest)
        }
    }

    // 监视构建 template -> 调用 update -> 更新 Renderer 渲染器
    const templatePath = path.resolve(__dirname, '../index.template.html')
    template = fs.readFileSync(templatePath, 'utf8')
    update()
    chokidar.watch(templatePath).on('change', () => {
        template = fs.readFileSync(templatePath, 'utf8')
        update()
    })
    // 监视构建 serverBundle -> 调用 update -> 更新 Renderer 渲染器
    const serverConfig = require('./webpack.server.config')
    const serverCompiler = webpack(serverConfig)
    serverCompiler.watch({}, (err, stats) => {
        if (err) throw err // Webpack 的错误
        if (stats.hasErrors()) return // 打包的结果中自己源代码是否有错
        serverBundle = JSON.parse(
            fs.readFileSync(
                resolve('../dist/vue-ssr-server-bundle.json'),
                'utf-8'
            )
        )
        update()
    })
    // 监视构建 clientManifest -> 调用 update -> 更新 Renderer 渲染器
    return onReady
}

17. 【构建配置】【开发模式】写入内存

npm i webpack-dev-middleware -D
const fs = require('fs')
const path = require('path')
const chokidar = require('chokidar')
const webpack = require('webpack')
const devMiddleware = require('webpack-dev-middleware')
const resolve = (file) => path.resolve(__dirname, file)

module.exports = function(server, callback) {
    let ready
    const onReady = new Promise((r) => (ready = r))
    // 监视构建 -> 更新 Renderer
    let template
    let serverBundle
    let clientManifest

    const update = () => {
        if (template && serverBundle && clientManifest) {
            ready()
            callback(serverBundle, template, clientManifest)
        }
    }

    // 监视构建 template -> 调用 update -> 更新 Renderer 渲染器
    const templatePath = path.resolve(__dirname, '../index.template.html')
    template = fs.readFileSync(templatePath, 'utf8')
    update()
    chokidar.watch(templatePath).on('change', () => {
        template = fs.readFileSync(templatePath, 'utf8')
        update()
    })
    // 监视构建 serverBundle -> 调用 update -> 更新 Renderer 渲染器
    const serverConfig = require('./webpack.server.config')
    const serverCompiler = webpack(serverConfig)
    const serverDevMiddleware = devMiddleware(serverCompiler) // 如何关闭日志输出?
    serverCompiler.hooks.done.tap('server', () => {
        // serverDevMiddleware.fileSystem.readFileSync // 旧版本写法
        serverBundle = JSON.parse(
            serverDevMiddleware.context.outputFileSystem.readFileSync(
                resolve('../dist/vue-ssr-server-bundle.json'),
                'utf-8'
            )
        )
        console.log(serverBundle)
        update()
    })
    /* serverCompiler.watch({}, (err, stats) => {
        if (err) throw err
        if (stats.hasErrors()) return
        serverBundle = JSON.parse(
            fs.readFileSync(
                resolve('../dist/vue-ssr-server-bundle.json'),
                'utf-8'
            )
        )
        update()
    }) */
    // 监视构建 clientManifest -> 调用 update -> 更新 Renderer 渲染器
    return onReady
}

18. 【构建配置】【开发模式】客户端构建

build/setup-dev-server.js

const fs = require('fs')
const path = require('path')
const chokidar = require('chokidar')
const webpack = require('webpack')
const devMiddleware = require('webpack-dev-middleware')
const resolve = (file) => path.resolve(__dirname, file)

module.exports = function(server, callback) {
    let ready
    const onReady = new Promise((r) => (ready = r))
    // 监视构建 -> 更新 Renderer
    let template
    let serverBundle
    let clientManifest

    const update = () => {
        if (template && serverBundle && clientManifest) {
            ready()
            callback(serverBundle, template, clientManifest)
        }
    }

    // 监视构建 template -> 调用 update -> 更新 Renderer 渲染器
    const templatePath = path.resolve(__dirname, '../index.template.html')
    template = fs.readFileSync(templatePath, 'utf8')
    update()
    chokidar.watch(templatePath).on('change', () => {
        template = fs.readFileSync(templatePath, 'utf8')
        update()
    })
    // 监视构建 serverBundle -> 调用 update -> 更新 Renderer 渲染器
    const serverConfig = require('./webpack.server.config')
    const serverCompiler = webpack(serverConfig)
    const serverDevMiddleware = devMiddleware(serverCompiler)
    // console.log(serverDevMiddleware.context.outputFileSystem.readFileSync)
    serverCompiler.hooks.done.tap('server', () => {
        serverBundle = JSON.parse(serverDevMiddleware.context.outputFileSystem.readFileSync(resolve('../dist/vue-ssr-server-bundle.json'), 'utf-8'))
        console.log(serverBundle)
        update()
    })
    // 监视构建 clientManifest -> 调用 update -> 更新 Renderer 渲染器
    const clientConfig = require('./webpack.client.config')
    const clientCompiler = webpack(clientConfig)
    const clientDevMiddleware = devMiddleware(clientCompiler, {
        publicPath: clientConfig.output.publicPath,
    })
    clientCompiler.hooks.done.tap('client', () => {
        clientManifest = JSON.parse(clientDevMiddleware.context.outputFileSystem.readFileSync(resolve('../dist/vue-ssr-client-manifest.json'), 'utf-8'))
        update()
    })
    // 将 clientDevMiddleware 挂载到 Express 服务中,提供对其内部内存中数据的访问,不然会 404
    server.use(clientDevMiddleware)
    return onReady
}

server.js

server.get(
    '/',
    isProd
        ? render
        : async (req, res) => {
              // 等待有了 Renderer 渲染器以后,调用 render 进行渲染
              // onReady is not a function
              await onReady
              render(req, res)
          }
)

19. 【构建配置】【开发模式】热更新

npm i webpack-hot-middleware -D

build/setup-dev-server.js

const fs = require('fs')
const path = require('path')
const chokidar = require('chokidar')
const webpack = require('webpack')
const devMiddleware = require('webpack-dev-middleware')
const hotMiddleware = require('webpack-hot-middleware')
const resolve = (file) => path.resolve(__dirname, file)

module.exports = function(server, callback) {
    let ready
    const onReady = new Promise((r) => (ready = r))
    // 监视构建 -> 更新 Renderer
    let template
    let serverBundle
    let clientManifest

    const update = () => {
        if (template && serverBundle && clientManifest) {
            ready()
            callback(serverBundle, template, clientManifest)
        }
    }

    // 监视构建 template -> 调用 update -> 更新 Renderer 渲染器
    const templatePath = path.resolve(__dirname, '../index.template.html')
    template = fs.readFileSync(templatePath, 'utf8')
    update()
    chokidar.watch(templatePath).on('change', () => {
        template = fs.readFileSync(templatePath, 'utf8')
        update()
    })
    // 监视构建 serverBundle -> 调用 update -> 更新 Renderer 渲染器
    const serverConfig = require('./webpack.server.config')
    const serverCompiler = webpack(serverConfig)
    const serverDevMiddleware = devMiddleware(serverCompiler)
    // console.log(serverDevMiddleware.context.outputFileSystem.readFileSync)
    serverCompiler.hooks.done.tap('server', () => {
        serverBundle = JSON.parse(serverDevMiddleware.context.outputFileSystem.readFileSync(resolve('../dist/vue-ssr-server-bundle.json'), 'utf-8'))
        update()
    })
    // 监视构建 clientManifest -> 调用 update -> 更新 Renderer 渲染器
    const clientConfig = require('./webpack.client.config')
    clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin())
    clientConfig.entry.app = ['webpack-hot-middleware/client?quiet=true&reload=true', clientConfig.entry.app]
    clientConfig.output.filename = '[name].js' // 热更新模式下不设置 Hash
    const clientCompiler = webpack(clientConfig)
    const clientDevMiddleware = devMiddleware(clientCompiler, {
        publicPath: clientConfig.output.publicPath,
    })
    clientCompiler.hooks.done.tap('client', () => {
        clientManifest = JSON.parse(clientDevMiddleware.context.outputFileSystem.readFileSync(resolve('../dist/vue-ssr-client-manifest.json'), 'utf-8'))
        update()
    })
    server.use(
        hotMiddleware(clientCompiler, {
            log: false,
        })
    )
    // 将 clientDevMiddleware 挂载到 Express 服务中,提供对其内部内存中数据的访问
    server.use(clientDevMiddleware)
    return onReady
}

20. 【构建配置】编写通用代码注意事项

文档说明

21. 【路由处理】基本配置

src/router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/pages/Home'

Vue.use(VueRouter)

export const createRouter = () => {
    const router = new VueRouter({
        mode: 'history',
        routes: [
            {
                path: '/',
                name: 'home',
                component: Home,
            },
            {
                path: '/about',
                name: 'about',
                component: () => import('@/pages/About'),
            },
            {
                path: '*',
                name: 'error404',
                component: () => import('@/pages/404'),
            },
        ],
    })
    return router
}

22. 【路由处理】将路由注册到根实例

src/app.js

import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'

export function createApp() {
    const router = createRouter()
    const app = new Vue({
        router, // 将路由注册到跟市里
        render: (h) => h(App),
    })
    return { app, router }
}

23. 【路由处理】适配服务端入口

src/entry-server.js

import { createApp } from './app'

/* export default (context) => {
    // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
    // 以便服务器能够等待所有的内容在渲染前,
    // 就已经准备就绪。
    return new Promise((resolve, reject) => {
        const { app, router } = createApp()
        // 设置服务器端 router 的位置
        router.push(context.url)

        // 等到 router 将可能的异步组件和钩子函数解析完
        router.onReady(() => {
            const matchedComponents = router.getMatchedComponents()
            // 匹配不到的路由,执行 reject 函数,并返回 404
            if (!matchedComponents.length) {
                return reject({ code: 404 })
            }
            // Promise 应该 resolve 应用程序实例,以便它可以渲染
            resolve(app)
        }, reject)
    })
} */
export default async (context) => {
    const { app, router } = createApp()
    router.push(context.url)
    await new Promise(router.onReady.bind(router))
    return app
}

24. 【路由处理】服务端 server 适配

server.js

const fs = require('fs')
const express = require('express')
const server = express()
const { createBundleRenderer } = require('vue-server-renderer')
const setupDevServer = require('./build/setup-dev-server')

const isProd = process.env.NODE_ENV === 'production'

server.use('/dist', express.static('./dist'))
let renderer
let onReady
if (isProd) {
    const serverBundle = require('./dist/vue-ssr-server-bundle.json')
    const template = fs.readFileSync('./index.template.html', 'utf-8')
    const clientManifest = require('./dist/vue-ssr-client-manifest.json')

    renderer = createBundleRenderer(serverBundle, {
        template,
        clientManifest,
    })
} else {
    // 开发 -> 监听打包构建 -> 重新生成 Renderer 渲染器
    onReady = setupDevServer(server, (serverBundle, template, clientManifest) => {
        renderer = createBundleRenderer(serverBundle, {
            template,
            clientManifest,
        })
    })
}

const render = async (req, res) => {
    try {
        // !删除了 app 实例,因为会自动去 entry-server.js 里面找到实例
        const html = await renderer.renderToString({
            title: 'SSR',
            meta: '<meta name="description" content="SSR 学习">',
            url: req.url,
        })
        res.setHeader('Content-Type', 'text/html; charset=utf8')
        res.end(html)
    } catch (err) {
        res.status(500).end('Internal Server Error.')
    }
}

server.get(
    '*',
    isProd
        ? render
        : async (req, res) => {
              // 等待有了 Renderer 渲染器以后,调用 render 进行渲染
              // onReady is not a function
              await onReady
              render(req, res)
          }
)

server.listen(3000, () => console.log('Server running at http://localhost:3000'))

25. 【路由处理】客户端入口适配

entry-client.js

import { createApp } from './app'

const { app, router } = createApp()

router.onReady(() => {
    app.$mount('#app')
})

26. 【路由处理】处理完成

link 标签的 rel 属性值 preload 和 prefetch 的区别是什么

App.vue

<template>
    <div id="app">
        <ul>
            <li>
                <router-link to="/">Home</router-link>
            </li>
            <li>
                <router-link to="/about">About</router-link>
            </li>
        </ul>
        <router-view></router-view>
    </div>
</template>

<script>
export default {
    name: 'App',
    data() {
        return {
            msg: '牛批',
        }
    },
    methods: {
        handleClick() {
            console.log('两横一竖')
        },
    },
}
</script>

27. 【路由处理】处理页面 Head 内容

让不同的页面拥有自己的 head

npm i vue-meta

src/app.js

import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import VueMeta from 'vue-meta'

Vue.use(VueMeta)
Vue.mixin({
    metaInfo: {
        titleTemplate: '%s - SSR 学习',
    },
})

export function createApp() {
    const router = createRouter()
    const app = new Vue({
        router, // 将路由注册到跟市里
        render: (h) => h(App),
    })
    return { app, router }
}

src/entry-server.js

import { createApp } from './app'

export default async (context) => {
    const { app, router } = createApp()
    const meta = app.$meta()
    router.push(context.url)
    context.meta = meta
    await new Promise(router.onReady.bind(router))
    return app
}

index.template.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">
    {{{meta.inject().meta.text()}}}
    {{{meta.inject().title.text()}}}
</head>

<body>
    <!--vue-ssr-outlet-->
</body>

</html>

src/pages/Home.vue

<template>
    <div>Home</div>
</template>

<script>
export default {
    name: 'Home',
    metaInfo: {
        title: '首页',
    },
}
</script>

28. 【数据预取和状态管理】思路分析

src/pages/Posts.vue

<template>
    <div>
        <h1>Post List</h1>
        <ul>
            <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
        </ul>
    </div>
</template>

<script>
import axios from 'axios'
export default {
    name: 'Posts',
    components: {},
    data() {
        return {
            posts: [],
        }
    },
    // !服务端渲染
    // 只支持 beforeCreate 和 created
    // 不会等待 beforeCreate 和 created 中的异步操作
    // 不支持响应式数据
    // 所以这种做法在服务端渲染中是不会工作的
    async created() {
        console.log('Posts Created Start')
        const { data } = await axios({
            method: 'GET',
            url: 'https://cnodejs.org/api/v1/topics',
        })
        this.posts = data.data
        console.log('Posts Created End')
    },
}
</script>

29. 【数据预取和状态管理】数据预取

src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'

Vue.use(Vuex)

export const createStore = () => {
    return new Vuex.Store({
        state: () => ({
            posts: [],
        }),
        mutations: {
            setPosts(state, data) {
                state.posts = data
            },
        },
        actions: {
            // 服务端渲染期间务必让 action 返回 Promise
            async getPosts({ commit }) {
                const { data } = await axios.get('https://cnodejs.org/api/v1/topics')
                commit('setPosts', data.data)
            },
        },
    })
}

src/app.js

import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import VueMeta from 'vue-meta'
import { createStore } from './store'

Vue.use(VueMeta)
Vue.mixin({
    metaInfo: {
        titleTemplate: '%s - SSR 学习',
    },
})

export function createApp() {
    const router = createRouter()
    const store = createStore()
    const app = new Vue({
        router, // 将路由注册到跟市里
        store,
        render: (h) => h(App),
    })
    return { app, router, store }
}

src/pages/Posts.vue

<template>
    <div>
        <h1>Post List</h1>
        <ul>
            <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
        </ul>
    </div>
</template>

<script>
import axios from 'axios'
import { mapState, mapActions } from 'vuex'
export default {
    name: 'Posts',
    data() {
        return {
            // posts: [],
        }
    },
    computed: {
        ...mapState(['posts']),
    },
    // !服务端渲染
    // 只支持 beforeCreate 和 created
    // 不会等待 beforeCreate 和 created 中的异步操作
    // 不支持响应式数据
    // 所以这种做法在服务端渲染中是不会工作的
    /* async created() {
        console.log('Posts Created Start')
        const { data } = await axios({
            method: 'GET',
            url: 'https://cnodejs.org/api/v1/topics',
        })
        this.posts = data.data
        console.log('Posts Created End')
    }, */
    methods: {
        ...mapActions(['getPosts']),
    },
    // Vue SSR 为服务端渲染提供的一个生命周期函数
    serverPrefetch() {
        // 发起 action,返回 Promise
        return this.getPosts()
    },
}
</script>

问题:客户端没有此数据,和服务端数据同步失败

30. 【数据预取和状态管理】将预取数据同步到客户端

src/entry-server.js

import { createApp } from './app'

export default async (context) => {
    const { app, router, store } = createApp()
    const meta = app.$meta()
    router.push(context.url)
    context.meta = meta
    await new Promise(router.onReady.bind(router))
    context.rendered = () => {
        // 服务端渲染完毕后会调用
        // Renderer 会把 context.state 数据对象内敛到页面模板中
        // 最终发送给客户端的页面中会包含一段脚本:window.__INITIAL_STATE__ = context.state
        // 客户端需要把页面中的 window.__INITIAL_STATE__ 拿出来填充到客户端 store 容器中
        context.state = store.state
    }
    return app
}

src/entry-client.js

import { createApp } from './app'

const { app, router, store } = createApp()

if (window.__INITIAL_STATE__) {
    store.replaceState(window.__INITIAL_STATE__)
}

router.onReady(() => {
    app.$mount('#app')
})

客户端用自己的数据和服务端进行比对,一比对发现一致,直接接管服务端数据进行使用,并混合进去和动态交互相关的功能,例如点击事件,双向数据绑定等等

空文件

简介

暂无描述 展开 收起
JavaScript
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
1
https://gitee.com/ifercarly/vue_ssr.git
git@gitee.com:ifercarly/vue_ssr.git
ifercarly
vue_ssr
vue_ssr
master

搜索帮助