vue ssr 实践

  • 技术栈
  • 初始化项目并安装相关依赖
  • 编写webpack相关配置
  • 编写客户端,服务端通用代码
  • 组件异步获取数据
  • 编写客户端入口代码
  • 编写服务端入口代码
  • 后台代码
  • 总结

技术栈

后台使用的是express,前端使用的是vue+webpack。

初始化项目并安装相关依赖

首先新建一个文件夹, npm init -y 初始化项目,然后安装相关依赖包,这里简单列一下需要安装的依赖包以及相关版本

  "dependencies": {"express": "^4.17.1","vue": "^2.6.11","vue-router": "^3.4.3","vue-server-renderer": "^2.6.11","vuex": "^3.5.1"},"devDependencies": {"@babel/core": "^7.11.1","@babel/preset-env": "^7.11.0","babel-loader": "^8.1.0","css-loader": "^4.2.1","html-webpack-plugin": "^4.3.0","mini-css-extract-plugin": "^0.10.0","nodemon": "^2.0.4","style-loader": "^1.2.1","vue-loader": "^15.9.3","vue-style-loader": "^4.1.2","vue-template-compiler": "^2.6.11","webpack": "^4.44.1","webpack-cli": "^3.3.12","webpack-dev-server": "^3.11.0","webpack-merge": "^5.1.1"}

编写webpack相关配置

  • base相关配置

    • 这里只需要处理后缀为cssjsvue的文件,其他的东西,如图片,字体图标这里没有使用到,所以这里不做配置。
    • 这里主要使用到了2个插件vue-loader/lib/pluginmini-css-extract-plugin
    • vue-loader/lib/plugin主要是用来处理.vue相关文件。
    • mini-css-extract-plugin主要是webpack4用来提取css的,但是vue ssr官网上的文档比较老了,官网上写的还是使用extract-text-webpack-plugin,webpack4是不支持的,而且官网上还是用到了vue-style-loader,这里我们也用不上,因为vue-style-loader无法配合extract-text-webpack-plugin把css提取出来。还有一点就是如果你是用到了vue-style-loader,但是css-loader的版本是4.x的,就需要把css-loader的参数esModule设置为false,否则会报错,这也是我在编写配置的时候发现的一个问题。
      webpack.base.js
const path = require("path");
const VuePluginLoader = require("vue-loader/lib/plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const resolve = (dir) => {return path.resolve(__dirname, dir);
};
module.exports = {output: {filename: "[name].bundle.js",path: resolve("../dist"),},resolve: {extensions: [".js", ".vue"],},module: {rules: [{test: /\.vue$/,loader: "vue-loader",options: {extractCSS: true,},},{test: /\.js$/,use: {loader: "babel-loader",options: {presets: ["@babel/preset-env"],},},exclude: /node_modules/,},{test: /\.css$/,use: [MiniCssExtractPlugin.loader,"css-loader"],},],},plugins: [new MiniCssExtractPlugin({filename: "style.css",chunkFilename: "[id].css",}),new VuePluginLoader(),],
};
  • client相关配置

    • 客户端的配置主要使用到了vue-server-renderer/server-plugin这个插件,用来记录客户端打包出来的相关资源,并生成一个vue-ssr-client-manifest.json文件,后期在编写后端的时候会使用到这个文件,用来自动引入相关jscss相关文件到html中,减去手动引入的烦恼。
    • html-webpack-plugin生成html文件,这个主要是在开发的时候使用到,就是不走ssr渲染的时候使用到

webpack.client.js

const { merge } = require("webpack-merge");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ClientRenderPlugin = require("vue-server-renderer/client-plugin");
const path = require("path");
const baseConfig = require("./webpack.base");
const resolve = (dir) => {return path.resolve(__dirname, dir);
};
const config = {entry: { client: resolve("../src/client-entry.js") },plugins: [new ClientRenderPlugin(),new HtmlWebpackPlugin({filename: "index.html",template: resolve("../public/index.html"),}),],
};
module.exports = merge(baseConfig, config);
  • server相关配置

    • 服务端的配置主要使用到了vue-server-renderer/server-plugin这个插件,根据客户端的用法差不多,但是是用来记录服务端打包出来的相关资源,并生成一个ue-ssr-server-bundle.json文件。
    • html-webpack-plugin生成html文件,这个在服务端是不需要的,我这里是为了方便,目的是为了把所有文件都放在dist文件夹中。读取文件的时候就在dist中读取即可,不用读取js的时候就在dist中,读取html在public中这么麻烦。
    • 因为打包出来的文件是给node使用的,所以我们需要指定target打包的目标为node,还有输出的模块规范为commonjs2
      webpack.server.js
const { merge } = require("webpack-merge");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ServerRenderPlugin = require("vue-server-renderer/server-plugin");
const path = require("path");
const baseConfig = require("./webpack.base");
const resolve = (dir) => {return path.resolve(__dirname, dir);
};
const config = {entry: { server: resolve("../src/server-entry.js") },target: "node",output: {libraryTarget: "commonjs2",},plugins: [new ServerRenderPlugin(),new HtmlWebpackPlugin({filename: "index.ssr.html",template: resolve("../public/index.ssr.html"),inject: false,}),],
};
module.exports = merge(baseConfig, config);

编写客户端,服务端通用代码

  • vue-router路由方面需要使用工厂函数并返回一个路由对象实例,这是因为在服务端渲染的时候,每来一次请求都需要一个新的路由实例,否则所有请求都会使用同一个路由实例。同时还要注意的是不能使用路由懒加载的方式,因为这里我把css单独抽离出来,使用懒加载css会被抽离成一个chunk,在服务端渲染的时候这个chunk并不会被自动引入到html中,而是在加载完异步js的时候通过documentcss的链接挂在到html上,由于是在服务端上,document这个对象是不存在,会导致报错。当然,如果你选择不抽离css的话,路由懒加载是完全可以使用的。
import Vue from "vue";
import VueRouter from "vue-router";
import Foo from "./components/Foo.vue";
import Bar from "./components/Bar.vue";
Vue.use(VueRouter);
export default () => {const router = new VueRouter({mode: "history",routes: [{path: "/",component: Foo,},{path: "/bar",component: Bar,},],});return router;
};
  • vuex也是需要使用工厂函数并返回一个vuex对象实例,原因跟上面的vue-router一样。在服务端获取数据的时候,服务端会把数据挂在到window.__INITIAL_STATE__上,在客户端的时候需要将数据进行同步,可以使用replaceState这个方法进行数据同步
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);export default () => {const store = new Vuex.Store({state: {name: "张三"},mutations: {changeName(state, name) {state.name = name;},},actions: {changeName({ commit }) {return new Promise((resolve, reject) => {setTimeout(() => {commit("changeName", "李四");resolve();}, 1000);});}});// 如果浏览器执行的时候,需要将服务器设置的最新状态替换掉客户端的if (typeof window !== "undefined" && window.__INITIAL_STATE__) {store.replaceState(window.__INITIAL_STATE__);}return store;
};
  • 创建vue实例也是需要通过工厂函数进行创建。
import Vue from "vue";
import createRouter from "./router.js";
import App from "./App.vue";
import createStore from "./store";
export default () => {const router = createRouter();const store = createStore();const app = new Vue({router,store,render: (h) => h(App),});return { app, router, store };
};

组件异步获取数据

在组件需要异步获取数据的时候,需要在组件内部实现一个asyncData函数中,该函数接受一个store对象参数和一个router参数对象,并且需要返回一个Promise对象,方便在服务端或者客户端获取数据。这里需要特别注意的是,需要异步获取数据的组件只能进行局部注册,不能全局注册,否则在服务端或者客户端不能调用组件的asyncData,导致不能获取数据,这是因为在匹配路由的时候只能匹配到路由组件,路由组件内部使用到的组件不能匹配出来,只能通过路由组件的components属性获取,然后在通过递归的方式调用每个组件内部的asyncData函数。

<template><div>Bar{{$store.state.name}}<Son /><Son1 /></div>
</template><script>
import Son from "./son";
import Son1 from "./son1";
export default {components: {Son,Son1,},asyncData(store) {return store.dispatch("changeName");},
};
</script>

编写客户端入口代码

客户端入口代码主要做的是获取数据和渲染视图。获取数据采用的方式在路由导航之前解析数据。采用这种方式的优点就是不用判断在服务器是否已经获取过数据,就是可以防止二次获取数据。但是缺点就是会有卡顿的现象,建议提供一个数据加载指示器。当路由组件需要重用的时候,这种在路由导航之前解析数据就不太好使了。官网还提供了另外一种获取数据的方式,就是通过全局 mixin ,在beforeMount中调用asyncData函数,但是唯一的缺点就是需要判断是否已经在服务器中获取过数据,以免二次获取数据,造成性能上的浪费。

import createApp from "./main";
const { app, router, store } = createApp();
const getData = (components, store) => {const arr = [];for (let i = 0; i < components.length; i++) {const component = components[i];if (component.asyncData) {arr.push(component.asyncData(store));}const sonComp = component.components || {};if (Object.keys(sonComp).length !== 0) {const sonArr = Object.keys(sonComp).map((son) => sonComp[son]);arr.push(...getData(sonArr, store));}}return arr;
};
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();}const dataList = getData(activated, store);// 这里如果有加载指示器 (loading indicator),就触发Promise.all(dataList).then(() => {// 停止加载指示器(loading indicator)next();}).catch(next);});app.$mount("#app");
});

编写服务端入口代码

服务端入口代码跟客户端的入口代码其实是差不多的,唯一不同的是服务端会接受一个context上下文对象,然后根据请求路径匹配出路由组件。同时还要将vuex状态挂载到上下文中

import createApp from "./main";
const getData = (components, store) => {const arr = [];for (let i = 0; i < components.length; i++) {const component = components[i];if (component.asyncData) {arr.push(component.asyncData(store));}const sonComp = component.components || {};if (Object.keys(sonComp).length !== 0) {const sonArr = Object.keys(sonComp).map((son) => sonComp[son]);arr.push(...getData(sonArr, store));}}return arr;
};
export default (context) => {return new Promise((resolve, reject) => {const { app, router, store } = createApp();router.push(context.url);// 涉及到异步组件router.onReady(() => {// 获取路径匹配到的组件const matches = router.getMatchedComponents();if (matches.length === 0) {reject({ code: 404 });return;}const dataList = getData(matches, store);// 获取异步数据Promise.all(dataList).then(() => {// 将vuex状态挂载到上下文中,会将状态挂载到window上context.state = store.state;resolve(app);});}, reject);});
};

后台代码

后台主要是使用vue-server-renderer这个插件进行渲染html,这里需要注意的是index.ssr.html这个html需要有 <!--vue-ssr-outlet--> 这个占位符

const experss = require("express");
const VueServerRender = require("vue-server-renderer");
const fs = require("fs");
const app = experss();
const router = experss.Router();
const ServerBundle = require("./dist/vue-ssr-server-bundle.json");
const clientManifest = require("./dist/vue-ssr-client-manifest.json");
const template = fs.readFileSync("./dist/index.ssr.html", "utf-8");
const render = VueServerRender.createBundleRenderer(ServerBundle, {template,clientManifest,
});
app.use(experss.static("./dist"));
app.use(async (req, res) => {try {const str = await new Promise((resolve, reject) => {render.renderToString({ url: req.url }, (err, data) => {if (err) {reject(err);return;}resolve(data);});});res.end(str);} catch (error) {res.end("404");}
});
app.listen(3000, function() {console.log("localhost:3000");
});

总结

其实ssr主要的难点还是在于怎么去获取数据的问题,怎么防止二次获取数据。而且获取到的数据都是存储在vuex中的,怎么把组件获取到的数据存储到组件本身内部这个暂时我还没想到怎么实现。最后这篇文章的代码已经放在了github上面,有需要的可以拉代码去参考一下,https://github.com/c10342/vue-ssr

vue ssr 实践相关推荐

  1. Vue SSR 性能优化实践

    齐云雷,微医云服务团队前端工程师,本文是作者在<第二届缤纷前端技术沙龙>分享主题的文字版. 估计大部分读者对标题中的性能优化更感兴趣,可惜我分享的重点其实更多在于实践.实践有深有浅,下面介 ...

  2. Vue + vue-router + express SSR实践

    Vue SSR SSR概念: https://ssr.vuejs.org/zh/ server side render服务端渲染 服务端渲染解释: 将一个Vue组件在服务器渲染为HTML字符串并发送到 ...

  3. 理解vue ssr原理,自己搭建简单的ssr框架

    理解vue ssr原理,自己搭建简单的ssr框架 前言 大多数Vue项目要支持SSR应该是为了SEO考虑,毕竟对于WEB应用来说,搜索引擎是一个很大的流量入口.Vue SSR现在已经比较成熟了,但是如 ...

  4. 手把手带你从零打造Vue SSR,清晰易懂!

    Vue SSR,服务端渲染,优点大家都很清楚,能大大提升首屏渲染速度,优化用户体验,还有利于SEO. 但说实话,Vue SSR并不好上手.官网给的例子大而全,太复杂.而网上很多从0到1打造Vue SS ...

  5. PM2 自动化部署项目 之 (Vue SSR)

    背景 常规部署项目比较传统的方式通过上传工具直接上传文件替换服务器文件, 也可以通过Xftp 方式来更新/发布指定站点.随着项目复杂度的增强,开发技术等手段增多.一些部署方式显得有点力不从心,且操作过 ...

  6. Vue SSR 服务端渲染原理(简易版本)

    前言 在了解Vue SSR之前,我们要搞明白两个东西先:SSR 和 浏览器的渲染, 涉及到的技术: Vue vue-server-renderer Nodejs Express 1. 什么是SSR S ...

  7. 了解 Vue SSR 这一篇足以

    文章目录 1 - 什么是服务器端渲染? 1.1 新建server文件夹 1.2 生成一个node项目 1.3 安装express 1.4 服务端渲染小案例 1.5 运行查看效果 1.6 打开浏览器 1 ...

  8. [vue] SSR解决了什么问题?有做过SSR吗?你是怎么做的?

    [vue] SSR解决了什么问题?有做过SSR吗?你是怎么做的? SSR server side render服务端渲染,解决spa应用缺点的首屏加载速度慢.不利于SEO问题 个人简介 我是歌谣,欢迎 ...

  9. [vue] 实际工作中,你总结的vue最佳实践有哪些?

    [vue] 实际工作中,你总结的vue最佳实践有哪些? .babelrc 是目前 babel-polyfill 的最佳实践 { "presets": [ [ "@babe ...

最新文章

  1. 第七天总结:字符编码
  2. div模拟textarea自适应高度
  3. hive 常见面试题
  4. java中有几种内部类,Java中的四种内部类
  5. 小米暑期实习在线笔试2015-04-25
  6. SpringMVC自定义注入controller变量
  7. 二叉树的概念及其遍历方法 - python实现
  8. 还在用全部token训练ViT?清华UCLA提出token的动态稀疏化采样,降低inference时的计算量...
  9. DataTable对象的操作问题
  10. 跨过野蛮生长的直播电商下一步该走向何方?
  11. [SSM]报错500:org.springframework.dao.DataIntegrityViolationException
  12. 开发支付宝接口时的错误报告:openssl_sign(): supplied key param cannot be coerced into a private key【解决方法】
  13. zabbix-8:zabbix-api 获取hostid
  14. 使用RedRocket方便的查看证券数据
  15. 一道经典的面试题:一只公鸡5块钱,一只母鸡3块钱,3只小鸡一块钱,一个农夫用100块钱买100只鸡(编写java程序)...
  16. 软件测试——前言介绍
  17. javajavascript:void(‘h2‘)WEB(jsp基本语法表单提交)
  18. SAP的统驭科目 - 什么是SAP的统驭科目
  19. (c++)两道关于日期相减的题目
  20. 【论文阅读】Geography-Aware Sequential Location Recommendation

热门文章

  1. iaas-3.0.6版本安装云平台基础服务
  2. 清华差生10年奋斗经历
  3. 什么是Java内存模型?为什么会引发线程安全问题?
  4. python实现pca降维_python实现PCA(主成分分析)降维
  5. 完整java配置以及简单java源代码使用
  6. 【数据结构】单链表的增删查改(C语言实现)
  7. 计算机组成原理试题(五)(附参考答案)
  8. 微信小程序提示更新版本
  9. ASP.NET Core教程
  10. 二十一世纪大学英语读写教程(第二册)学习笔记(原文)——3 - The Tale of a Cultural Translator(一个文化译员的传奇)