vue-srr 实现原理内附完整代码( vuex、vue-router、syncData )

vue-ssr demo 链接 https://github.com/zenghao1998/vue-ssr/tree/main

Vue SSR

什么是 SSR

服务端渲染:凡是是从服务器返回的html页面,均算作是服务端渲染,包括php,jsp,nodejs,

SSR 的优点

  • 更好的 SEO

  • 更快的内容到达时间

为什么使用 SSR

在传统 vue 单页面应用中,页面的渲染都是由 js 完成,在服务端返回的html文件中,body中只有一个div标签和一个script标签,页面其余的dom结构都将由bundle.js生成,然后挂载到<div id="app"></div>上。这让搜索引擎爬虫抓取工具无法爬取页面的内容,如果 SEO 对你的站点很重要,则你可能需要服务器端渲染(SSR)解决此问题。

SSR 基本使用

ssr 的本质就服务端返回渲染好的 html 文档。我们先在项目根目录启动一个服务器,然后返回一个html 文档。这里我们使用 express 作为服务端框架。

const server = require('express')()
server.get('*', (req, res) => {
    res.end(`<!DOCTYPE html>
  <html lang="en">
    <head><title>Vue SSR</title></head>
    <body>
      <div>This is a server render page</div>
    </body>
  </html>`)
})
server.listen(8088, () => {
    console.log('http://127.0.0.1:8088')
})

打开 http://127.0.0.1:8088 右键查看源代码 ,服务端返回的内容如下。
在这里插入图片描述

vue 使用 SSR

一个最简单官方示例

const Vue = require('vue')
const server = require('express')()
const renderer = require('vue-server-renderer').createRenderer()
server.get('*', (req, res) => {
  const app = new Vue({
    data: {
      url: req.url
    },
    template: `<div>访问的 URL 是:{{ url }}</div>`
  })
  renderer.renderToString(app, (err, html) => {
    if (err) {
      res.status(500).end('Internal Server Error')
      return
    }
    res.end(`
      <!DOCTYPE html>
      <html lang="en">
        <head><title>Hello</title></head>
        <body>${html}</body>
      </html>
    `)
  })
})
server.listen(8080)

vue 使用 ssr渲染最核心是:通过 vue-server-renderer 这个库把 vue 对象转换成字符串,返回给客户端。

此时一个最简单的 vue-ssr 已经实现了。

模块化使用 vue-ssr

我们搭建一个本地项目,首先我们需要一个入口文件,app.js。

import Vue from 'vue'
import App from './App.vue'
//这里需要返回一个函数,避免单例状态,我们需要知道 node 服务是一个长期运行的进程,当我们的代码进入该进程时,它将进行一次取值并留存在内存中。这意味着如果创建一个单例对象,它将在每个传入的请求之间共享。
export function createApp() {
    const app = new Vue({
        store,
        router,
        render: h => h(App)
    })
    return { app }
}

创建 client-entry.js 用于服务端渲染后客户端激活。

import { createApp } from './app.js';
const { app } = createApp();
app.$mount('#app');

它需要做的事情很简单,直接挂载到根标签即可。

创建 server-entry.js 用于服务端渲染

import { createApp } from './app.js';
export default function (context) {
    //这个方法服务端渲染会调用 renderer.renderToString() 时调用
    return new Promise((reslove, reject) => {
        const { app } = createApp();
        reslove(app)
    })
};

和客户端代码不同的是,这里需要返回一个工厂函数,保证用户每次访问服务端都是一个全新的 vue。

创建 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>
    <div id="app">
        <!--vue-ssr-outlet-->
    </div>
</body>
</html>

服务端会把 server-entry.js 里的 vue 对象通过 vue-server-renderer 解析成字符串放在这里 <!--vue-ssr-outlet-->

创建 build 文件夹,使用 webpack 打包客户端代码 vue 和 服务端 vue

创建 webpack.base.config.js ( 通用配置 )

const VueLoaderPlugin = require('vue-loader/lib/plugin')
const path = require('path');
module.exports = {
    resolve: {
        extensions: ['.js', '.vue', '.json'],// 自动补全后缀
        alias: {
            '@': path.resolve(__dirname, '../src'),// 路径别名
        }
    },
    mode: 'development',
    output: {//打包的出口
        path: path.resolve(__dirname, '../dist'),
        filename: '[name].js'
    },
    module: {
        rules: [
            {// 配置 vue-loader 才能正常解析 .vue 文件
                test: /\.vue$/,
                loader: 'vue-loader'
            },
            {// vue-style-loader 支持服务端渲染啊 style 样式
                test: /\.css$/,
                use: ['vue-style-loader', 'css-loader']
            },
            {// 配置 babel es6 转 es5
                test: /\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/, // 排除第三方的包
                options: {
                    presets: [
                        [
                            '@babel/preset-env',
                            {
                                modules: false, // 不转换成 commonjs 模块
                                useBuiltIns: 'usage', // 按需 polyfill
                                corejs: 3,
                            }
                        ]
                    ]
                }
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin(), // 这个和 vue-loader 一起的用于解析 .vue 文件
    ]
};

创建 webpack.client.config.js ( 打包客户端代码 )

const { merge } = require('webpack-merge');
const base = require('./webpack.base.config.js');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = merge(base, {
    entry: {
        client: path.join(__dirname, '../src/client-entry.js'), // 指定客户端代码入口
    },
    plugins: [
        new HtmlWebpackPlugin({ //使用 index.template.html 模板
            template: path.join(__dirname, '../src/index.template.html'),
            filename: 'index.ssr.html',
            minify: false,
        })
    ]
})

创建 webpack.server.config.js( 打包服务端代码给 node 使用 )

const { merge } = require('webpack-merge');
const base = require('./webpack.base.config.js');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = merge(base, {
    target: 'node', // 指定是node环境
    entry: { // 指定客户端代码入口
        server: path.join(__dirname, '../src/server-entry.js')
    },
    output: {
        libraryTarget: 'commonjs2' // 必须按照 commonjs规范打包才能被服务器调用。
    },
})

创建 server.js 服务端渲染

当然,要返回的html字符串可以是由vue模板生成的,这就需要用到vue-server-renderer,它会基于Vue实例生成html字符串,是Vue SSR的核心。server.js中使用

const fs = require('fs');
const path = require('path');
const express = require('express');
const server = express();
//设置静态目录
server.use(express.static('dist'));
//获取到打包后的服务端vue代码
const bundle = fs.readFileSync(path.resolve(__dirname, './dist/server.js'), 'utf-8');
//拿着vue代码和模板生成字符串
const renderer = require('vue-server-renderer').createBundleRenderer(bundle, {
    template: fs.readFileSync(path.resolve(__dirname, './dist/index.ssr.html'), 'utf-8'),
});
server.get('*', (req, res) => {
    if (req.url !== "/favicon.ico") {
    	// 调用 server-entry.js 触发工厂函数,服务端渲染数据
        renderer.renderToString().then((html) => {
            res.end(html)
        })
    }
});
server.listen(8011, () => {
    console.log('http://127.0.0.1:8011');
});

此时一个简单的模块化SSR已经好了,但是目前还不支持vue-router状态管理 vuex ,现在对上面的代码改造一下。

在 src 下创建 router 和 store。

// router.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import baz from '@/components/baz'
import foo from '@/components/foo'
Vue.use(VueRouter)
// 返回一个函数,避免单例状态
export default function createRouter() {
    return new VueRouter({
        mode: "history",
        routes: [
            {
                path: "/",
                name: 'baz',
                component: baz
            },
            {
                path: "/foo",
                name: 'foo',
                component: foo
            }
        ]
    })
}
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
// 返回一个函数,避免单例状态
export default function createStore() {
    return new Vuex.Store({
        state: {
            article: '数据'
        },
        actions: {
            GET_ARTICLE({ commit }) {
                return new Promise((r) => {
                    setTimeout(() => {
                        commit('SET_ARTICLE', 'vuex数据')
                        r()
                    }, 1000);
                })
            }
        },
        mutations: {
            SET_ARTICLE(state, data) {
                state.article = data
            }
        }
    })
}

router 和 vuex 需要给服务端使用,返回一个函数,避免单例状态。

对 app.js 改造

import Vue from 'vue'
import App from './App.vue'
import createStore from './store/index.js';
import createRouter from './router/index.js';
//每次访问都创建一个新的vue 主要用于服务端
export function createApp() {
	//创建 store
    const store = createStore();
    //创建 router
    const router = createRouter();
    const app = new Vue({
        store,
        router,
        render: h => h(App)
    })
    return { app, store, router }
}

对 server-entry 改造

import { createApp } from './app.js';
// 服务端渲染会调用此方法
export default function (context) {
    return new Promise((reslove, reject) => {
        const { app, store, router } = createApp();
        //context.url => renderer.renderToString({url: req.url})
        //服务端跳转页面
        router.push(context.url);
        //服务端跳转页面完成
        router.onReady(() => {
            //获取页面级组件
            const matchedComponents = router.getMatchedComponents();
			//没有匹配成功返回 404
            if (!matchedComponents.length) {
                return reject({ code: 404 })
            }
            //对所有匹配的路由组件调用 `asyncData()`
            //拿到页面组件上的asyncData调用
            Promise.all(matchedComponents.map(Component => {
                if (Component.asyncData) {
                    return Component.asyncData({
                        store,
                    })
                }
            })).then(() => {
                // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
                context.state = store.state
                reslove(app)
            })
        })
    })
};

对 client-entry 改造

import { createApp } from './app.js';
const { app, store } = createApp();
//客户端激活时替换state状态
if (window.__INITIAL_STATE__) {
    store.replaceState(window.__INITIAL_STATE__);
};
app.$mount('#app');

对 server.js 改造

const fs = require('fs');
const path = require('path');
const express = require('express');
const server = express();
server.use(express.static('dist'));
//获取到服务端vue代码
const bundle = fs.readFileSync(path.resolve(__dirname, './dist/server.js'), 'utf-8');
//拿着js代码和模板生成字符串
const renderer = require('vue-server-renderer').createBundleRenderer(bundle, {
    template: fs.readFileSync(path.resolve(__dirname, './dist/index.ssr.html'), 'utf-8'), // 服务端渲染数据
});
server.get('*', (req, res) => {
    if (req.url !== "/favicon.ico") {
        renderer.renderToString({// 这个 url 会传给 server-entry.js 里的 context
            url: req.url
        }).then((html) => {
            res.end(html)
        }).catch((error) => {
            if (error.code == 404) {
                res.writeHead(404, {
                    "content-type": "text/html;charset=utf8"
                })
                res.end('找不到页面')
            }
        })
    }
});
server.listen(8011, () => {
    console.log('http://127.0.0.1:8011');
});

vue-ssr 本质上就是通过 webpack 打包 client-entry.js 和 server-entry.js 代码,首次进入页面通过 vue-server-renderer 把 server-entry.js 的 vue 生成字符串返回给客户端渲染,后续通过 client-entry.js 进行客户端激活。

所谓客户端激活,指的是 Vue 在浏览器端接管由服务端发送的静态 HTML,使其变为由 Vue 管理的动态 DOM 的过程。
在这里插入图片描述

完整代码 https://github.com/zenghao1998/vue-ssr/tree/main

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值