Quickly build a "server-side rendering" website vue ssr

Get the source code: https://github.com/tian2nian/vue-ssr

1. What is "server-side rendering"?

1. Traditional ssr:

When the client browser initiates an address request, the server directly returns the complete HTML content to the browser for rendering.

2. ssr view:

Vue components that were originally output by Vue.js (the framework for building client-side applications) in the browser are rendered as HTML strings by the server side (), send them directly to the browser, and finally "activate" these static tags as Fully interactive application on the client side.

2. What is the need for "server-side rendering"?

1. Compared with traditional SPA (Single-Page Application), the advantages of server-side rendering (SSR) are mainly:

  • Better SEO (Search engine crawler crawlers can directly view fully rendered pages. Currently Google and Bing index synchronous JavaScript applications just fine):

    If your application initially displays the loading daisy diagram, and then fetches the content via Ajax, the crawler does not wait for the asynchronous completion before crawling the page content. That said, if SEO is critical to your site and your pages are fetching content asynchronously, you may need server-side rendering (SSR) to solve this problem.

  • Faster content arrival time (time-to-content, no need to wait for all js to be downloaded and executed before displaying the complete data, so the user will see the fully rendered page faster):

    A slow network or device can often improve the user experience, and for applications where "time-to-content is directly related to conversion", server-side rendering (SSR) is critical, can help you achieve the best initial load performance.

2. Server-side rendering (SSR) requires attention:

  • development conditions. Browser-specific code that can only be used in certain lifecycle hooks; some external libraries may require special handling to run in server-rendered applications.

  • More requirements involving build setup and deployment. Unlike fully static single-page applications (SPAs) that can be deployed on any static file server, server-rendered applications require a Node.js server runtime environment.

  • More server side load. Rendering a complete application in Node.js will obviously be more CPU-intensive than a server that just serves static files (CPU-intensive - CPU-intensive), so if you expect to use it in a high traffic environment, please Prepare for a corresponding server load and adopt a caching strategy wisely.

3. "Pre-rendering" VS "Server-side rendering"

If your project has only a few marketing pages that require SEO, then you probably only need pre-rendering . Simply generate static HTML files for specific routes at build time. The advantages of prerendering are: simpler setup and the ability to treat your front end as a completely static site without the need to use a web server to dynamically compile HTML on the fly.

Fourth, quickly build vue ssr

1. A simple and easy-to-understand demo

Prepare:

  • Node.js version 6+ is recommended.
  • vue-server-renderer and  vue must match the version.
  • vue-server-renderer Depends on some Node.js native modules, so can only be used in Node.js.
npm install vue
npm install vue vue-server-renderer --save
npm install express --save

Start: server.js

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

server.get('*', (req, res) => {
// 第 1 步:创建一个 Vue 实例
    const app = new Vue({
        data: {
            hello: 'hello,vue ssr'
        },
        template: `<div>{{ hello }}</div>`
    })
// 第 3 步:将 Vue 实例渲染为 HTML 字符串
    renderer.renderToString(app, (err, html) => {
        if (err) {
            res.status(500).end('Internal Server Error')
            return
        }
//第 4 步:将拼接好的完整HTML发送给客户端让浏览器直接渲染
        res.end(`
      <!DOCTYPE html>
      <html lang="en">
        <head><title>Hello</title></head>
        <body>${html}</body>
      </html>
    `)
    })
})
//监听端口
server.listen(8080)

run:

node server.js

Result: You can see that the HTML returned by the server to the browser has data-server-rendered="true" indicating that this content is server-side rendering

 

2. Complete demo combined with webpack

Combined with the official website example, the instructions that need attention in the operation are all commented, and the precautions that do not appear in the code will be written separately. Only the code that differs from the SPA project is posted here.

Project structure:

Development environment run configuration example: build/setup-dev-server.js

const fs = require('fs')
const path = require('path')
const MFS = require('memory-fs')
const webpack = require('webpack')
/*chokidar 是封装 Node.js 监控文件系统文件变化功能的库。解决nodeJs原生监控文件系统的问题:
* 1.事件处理有大量问题
* 2.不提供递归监控文件树功能
* 3.导致 CPU 占用高
*/
const chokidar = require('chokidar')
const clientConfig = require('./webpack.client.config')
const serverConfig = require('./webpack.server.config')

const readFile = (fs, file) => {
    try {
        return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8')
    } catch (e) {}
}

module.exports = function setupDevServer (app, templatePath, cb) {
    let bundle
    let template
    let clientManifest

    let ready
    const readyPromise = new Promise(r => { ready = r })
    const update = () => {
        if (bundle && clientManifest) {
            ready()
            cb(bundle, {
                template,
                clientManifest
            })
        }
    }

    // read template from disk and watch
    template = fs.readFileSync(templatePath, 'utf-8')
    chokidar.watch(templatePath).on('change', () => {
        template = fs.readFileSync(templatePath, 'utf-8')
        console.log('index.html template updated.')
        update()
    })

    // modify client config to work with hot middleware
    clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app]
    clientConfig.output.filename = '[name].js'
    clientConfig.plugins.push(
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoEmitOnErrorsPlugin()
    )

    // dev middleware
    const clientCompiler = webpack(clientConfig)
    const devMiddleware = require('webpack-dev-middleware')(clientCompiler, {
        publicPath: clientConfig.output.publicPath,
        noInfo: true
    })
    app.use(devMiddleware)
    clientCompiler.plugin('done', stats => {
        stats = stats.toJson()
        stats.errors.forEach(err => console.error(err))
        stats.warnings.forEach(err => console.warn(err))
        if (stats.errors.length) return
        clientManifest = JSON.parse(readFile(
            devMiddleware.fileSystem,
            'vue-ssr-client-manifest.json'
        ))
        update()
    })

    // hot middleware
    app.use(require('webpack-hot-middleware')(clientCompiler, { heartbeat: 5000 }))

    // watch and update server renderer
    const serverCompiler = webpack(serverConfig)
    const mfs = new MFS()
    serverCompiler.outputFileSystem = mfs
    serverCompiler.watch({}, (err, stats) => {
        if (err) throw err
        stats = stats.toJson()
        if (stats.errors.length) return

        // read bundle generated by vue-ssr-webpack-plugin
        bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))
        update()
    })

    return readyPromise
}

Example of client packaging configuration in production environment: build/webpack.client.config.js:

const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
//用于使用service workers缓存您的外部项目依赖项。它将使用sw-precache生成一个服务工作者文件,并将其添加到您的构建目录中。
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')

const config = merge(base, {
    entry: {
        app: './src/entry-client.js'
    },
    optimization: {
        splitChunks: {
            cacheGroups: {
                commons: {
                    name: 'vendor',
                    minChunks: 1
                }
            }
        }
    },
    plugins: [
        new webpack.DefinePlugin({
            'process.env.VUE_ENV': '"client"'
        }),
        // 此插件在输出目录中
        // 生成 `vue-ssr-client-manifest.json`。
        new VueSSRClientPlugin()
    ]
})

module.exports = config

Example of server-side packaging configuration in production environment: build/webpack.server.config.js

const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
//用于使用service workers缓存您的外部项目依赖项。它将使用sw-precache生成一个服务工作者文件,并将其添加到您的构建目录中。
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')

const config = merge(base, {
    entry: {
        app: './src/entry-client.js'
    },
    optimization: {
        splitChunks: {
            cacheGroups: {
                commons: {
                    name: 'vendor',
                    minChunks: 1
                }
            }
        }
    },
    plugins: [
        new webpack.DefinePlugin({
            'process.env.VUE_ENV': '"client"'
        }),
        // 此插件在输出目录中
        // 生成 `vue-ssr-client-manifest.json`。
        new VueSSRClientPlugin()
    ]
})

module.exports = config

Example of state management module: src/store/modules/test.js

export default {
    namespaced: true,
    // 重要信息:state 必须是一个函数,
    // 因此可以创建多个实例化该模块
    state: () => ({
        count: 1
    }),
    actions: {
        inc: ({ commit }) => commit('inc')
    },
    mutations: {
        inc: state => state.count++
    }
}

State management usage example: src/views/Home.vue

<template>
    <section>
        这里是:views/Home.vue
        状态管理数据{{fooCount}}
        <hello-world></hello-world>
    </section>
</template>

<script>
    import HelloWorld from '../components/HelloWorld.vue'
    // 在这里导入模块,而不是在 `store/index.js` 中
    import fooStoreModule from '../store/modules/test'

    export default {
        asyncData ({ store }) {
            store.registerModule('foo', fooStoreModule);
            return store.dispatch('foo/inc')
        },

        // 重要信息:当多次访问路由时,
        // 避免在客户端重复注册模块。
        destroyed () {
            this.$store.unregisterModule('foo')
        },

        computed: {
            fooCount () {
                return this.$store.state.foo.count
            }
        },
        components: {
            HelloWorld
        }
    }
</script>

Common entry: src/app.js:

Note: The creation of router, store, and vue instances should be encapsulated into constructors, so that each time the server returns a brand new instance object

/*app.js通用入口。
 *核心作用是创建Vue实例。类似SPA的main.js。
*/
import Vue from 'vue'
//导入跟页面
import App from './App.vue'
// 导入路由生成器
import {createRouter} from "./router";
// 导入状态管理生成器
import {createStore} from "./store";
import {sync} from 'vuex-router-sync'

//创建并导出 vue实例生成器
export function createApp() {
    // 生成路由器
    let router = createRouter();
    // 生成状态管理器
    let store = createStore();
    // 同步路由状态(route state)到 store
    sync(store, router);
    let app = new Vue({
        //将路由器挂载到vue实例
        router,
        //将状态管理器挂载到vue实例
        store,
        // 生成App渲染
        render: h => h(App)
    });
    //返回生成的实例们
    return {app, router, store}
}

Client rendering entry file: src/entry-client.js

/** entry-client.js客户端入口。
 * 仅运行于浏览器
 * 核心作用:挂载、激活app。将服务器刚刚返回给浏览器的完整HTML替换为spa
 */
// 导入App生成器
import {createApp} from "./app";
//创建实例们
const {app, router,store} = createApp();
//当使用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态,自动嵌入到最终的 HTML 中。而在客户端,在挂载到应用程序之前,store 就应该获取到状态
if (window.__INITIAL_STATE__) {
    store.replaceState(window.__INITIAL_STATE__);
}
// 路由就绪后
router.onReady(() => {
    // 添加路由钩子函数,用于处理 asyncData.
    // 在初始路由 resolve 后执行,
    // 以便我们不会二次预取(double-fetch)已有的数据。
    // 使用 `router.beforeResolve()`,以便确保所有异步组件都 resolve。
    router.beforeResolve((to, from, next) => {
        const matched = router.getMatchedComponents(to)
        const prevMatched = router.getMatchedComponents(from)

        // 我们只关心非预渲染的组件
        // 所以我们对比它们,找出两个匹配列表的差异组件
        let diffed = false
        const activated = matched.filter((c, i) => {
            return diffed || (diffed = (prevMatched[i] !== c))
        })

        if (!activated.length) {
            return next()
        }

        // 这里如果有加载指示器 (loading indicator),就触发

        Promise.all(activated.map(c => {
            if (c.asyncData) {
                return c.asyncData({store, route: to})
            }
        })).then(() => {

            // 停止加载指示器(loading indicator)

            next()
        }).catch(next)
    });

    // 将App实例挂载到#app对应的DOM节点。在没有 data-server-rendered 属性的元素上向 $mount 函数的 hydrating 参数位置传入 true,强制使用应用程序的激活模式:app.$mount('#app', true)
    app.$mount('#app');
});

Server-side rendering entry file: src/entry-server.js

/** entry-server.js服务端入口。
 * 仅运行于服务器。
 * 核心作用是:拿到App实例生成HTML返回给浏览器渲染首屏
 */
//导入App生成器
import {createApp} from "./app";
/*
context:“服务器”调用上下文。如:访问的url,根据url决定将来createApp里路由的具体操作
 */
export default context => {
    return new Promise((resolve, reject) => {
        //创建App实例,router实例
        const {app, router, store} = createApp();
        //进入首屏:约定node服务器会将浏览器请求的url放进上下文context中,使用router.push()将当前访问的url对应的vue组件路由到App实例当前页
        router.push(context.url);
        //路由准备就绪后
        router.onReady(() => {
            const matchedComponents = router.getMatchedComponents();
            // 匹配不到的路由,执行 reject 函数,并返回 404
            if (!matchedComponents.length) {
                return reject({code: 404})
            }
            // 对所有匹配的路由组件调用 `asyncData()`
            Promise.all(matchedComponents.map(Component => {
                if (Component.asyncData) {
                    return Component.asyncData({
                        store,
                        route: router.currentRoute
                    })
                }
            })).then(() => {
                // 在所有预取钩子(preFetch hook) resolve 后,
                // 我们的 store 现在已经填充入渲染应用程序所需的状态。
                // 当我们将状态附加到上下文,
                // 并且 `template` 选项用于 renderer 时,
                // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
                context.state = store.state;
                context.title = router.currentRoute.name;
                //将渲染出来的App返回
                resolve(app);
            }, reject)
        });
    });
}

Server-side rendering template: index.template.html

Note: data-server-rendered Special attribute to let client-side Vue know that this part of HTML is server-rendered by Vue and should be mounted in active mode. Note that here is not adding  id="app", but adding  data-server-rendered attributes: you need to add an ID or other selector that can select the root element of the application, otherwise the application will not activate properly.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <meta http-equiv="X-UA-Compatible" content="ie=edge"/>
    <title>vue ssr</title>
</head>
<body>
<div id="app">
    <!--vue-ssr-outlet-->
</div>
</body>

Project running entry file: server.js

//nodeJs 服务器
const fs = require('fs');
const path = require('path');
const express = require('express');
//创建 express实例
const server = express();
//导入渲染器插件
const { createBundleRenderer } = require('vue-server-renderer');
const resolve = file => path.resolve(__dirname, file);
const templatePath = resolve('./src/index.template.html');
//获取 npm run 后面的命令
const isProd = process.env.NODE_ENV === 'production';
/**
 * 创建Renderer渲染器
 */
function createRenderer(bundle, options) {
    return createBundleRenderer(
        bundle,
        Object.assign(options, {
            runInNewContext: false
        })
    );
}
let renderer;
//生产环境
if (isProd) {
    const template = fs.readFileSync(templatePath, 'utf-8');
    const serverBundle = require('./dist/vue-ssr-server-bundle.json');
    const clientManifest = require('./dist/vue-ssr-client-manifest.json');
    renderer = createRenderer(serverBundle, {
        template,
        clientManifest
    });
} else {
    readyPromise = require('./build/setup-dev-server.js')(
        server,
        templatePath,
        (bundle, options) => {
            renderer = createRenderer(bundle, options);
        }
    );
}
//当浏览器请求 *(任意接口)时
server.get('*', async (req, res) => {
    try {
        const context = {
            url: req.url
        };
        //将url对应的vue组件渲染为HTML
        const html = await renderer.renderToString(context);
        //将HTML返回给浏览器
        res.send(html);
    } catch (e) {
        console.log(e);
        res.status(500).send('服务器内部错误');
    }
});
//监听浏览器8080端口
server.listen(8080, () => {
    console.log('监听8000,服务器启动成功')
});

package.json:

{
  "name": "webpackstudy",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "nodemon server",
    "build": "npm run build:client && npm run build:server",
    "build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js --progress --hide-modules",
    "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js --progress --hide-modules",
    "mock": "webpack-dev-server --progress --color"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "axios": "^0.19.0",
    "body-parser": "^1.19.0",
    "cheerio": "^1.0.0-rc.3",
    "cookie-parser": "^1.4.4",
    "cookie-session": "^1.3.3",
    "cors": "^2.8.5",
    "express": "^4.17.1",
    "jsonwebtoken": "^8.5.1",
    "mongoose": "^5.7.7",
    "multer": "^1.4.2",
    "nodemailer": "^6.3.1",
    "redis": "^2.8.0",
    "request": "^2.88.0",
    "util": "^0.12.1",
    "vue-router": "^3.1.2",
    "vuex": "^3.1.1",
    "ws": "^7.2.0"
  },
  "devDependencies": {
    "@babel/core": "^7.5.5",
    "@babel/preset-env": "^7.5.5",
    "@vue/cli-plugin-typescript": "^4.0.5",
    "autoprefixer": "^9.6.1",
    "babel-loader": "^8.0.6",
    "clean-webpack-plugin": "^3.0.0",
    "compression": "^1.7.4",
    "cross-env": "^6.0.3",
    "css-loader": "^3.2.0",
    "extract-text-webpack-plugin": "^3.0.2",
    "file-loader": "^4.2.0",
    "friendly-errors-webpack-plugin": "^1.7.0",
    "fs": "0.0.1-security",
    "html-webpack-plugin": "^3.2.0",
    "html-withimg-loader": "^0.1.16",
    "install": "^0.13.0",
    "jsonc": "^2.0.0",
    "less": "^3.10.2",
    "less-loader": "^5.0.0",
    "lru-cache": "^5.1.1",
    "memory-fs": "^0.5.0",
    "mini-css-extract-plugin": "^0.8.0",
    "mocker-api": "^1.8.1",
    "npm": "^6.13.3",
    "optimize-css-assets-webpack-plugin": "^5.0.3",
    "postcss-loader": "^3.0.0",
    "route-cache": "^0.4.4",
    "serve-favicon": "^2.5.0",
    "style-loader": "^1.0.0",
    "sw-precache-webpack-plugin": "^0.11.5",
    "terser-webpack-plugin": "^1.4.1",
    "uglifyjs-webpack-plugin": "^2.2.0",
    "url-loader": "^2.1.0",
    "vue": "^2.6.10",
    "vue-loader": "^15.7.1",
    "vue-server-renderer": "^2.6.10",
    "vue-style-loader": "^4.1.2",
    "vue-template-compiler": "^2.6.10",
    "vuex-router-sync": "^5.0.0",
    "webpack": "^4.39.2",
    "webpack-cli": "^3.3.7",
    "webpack-dev-server": "^3.8.0",
    "webpack-hot-middleware": "^2.25.0",
    "webpack-merge": "^4.2.2",
    "webpack-node-externals": "^1.7.2"
  }
}

Only simple runnable code is provided here. For details, please refer to the official website

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324079733&siteId=291194637