# ssr-demo **Repository Path**: dc_teach/ssr-demo ## Basic Information - **Project Name**: ssr-demo - **Description**: vue-cli3 + ssr - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2019-05-21 - **Last Updated**: 2022-10-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # vue-cli3+koa2+ssr 服务器渲染(转载) #### 运行 1. 安装依赖 npm i 2. npm run build:win 打包 3. 启动服务 node server.js #### 文章目录 1. ssr 介绍 2. vue-cli3 创建项目 3. 一个简单的 ssr 4. 把项目改造成 ssr 5. 源码地址 6. 其他相关地址 > #### ssr 介绍 1. ###### 什么是 SSR SSR(服务端渲染)顾名思义就是将页面在服务端渲染完成后在客户端直接展示。 2. ###### 客户端渲染与服务端渲染的区别 1. 传统的 SPA 模式(即客户端渲染的模式) - Vue.js 构建的应用程序,默认情况下是有一个 html 模板页,然后通过 webpack 打包生成一堆 js、css 等等资源文件。然后塞到 index.html 中 - 用户输入 url 访问页面 -> 先得到一个 html 模板页 -> 然后通过异步请求服务端数据 -> 得到服务端的数据 -> 渲染成局部页面 -> 用户 2. SSR 模式(即服务端渲染模式) 用户输入 url 访问页面 -> 服务端接收到请求 -> 将对应请求的数据渲染完一个网页 -> 返回给用户 3. ###### 为什么要使用 SSR 呢? ssr 的好处官网已经给出,最有意思的两个优点如下: - 更有好的 SEO。由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。 - 更快的内容到达时间(time-to-content) 4. SSR 原理 这是 vue.js 官方的 SSR 原理介绍图,从这张图片,我们可以知道:我们需要通过 Webpack 打包生成两份 bundle 文件: - Client Bundle,给浏览器用。和纯 Vue 前端项目 Bundle 类似 - Server Bundle,供服务端 SSR 使用,一个 json 文件 不管你项目先前是什么样子,是否是使用 vue-cli 生成的。都会有这个构建改造过程。在构建改造这里会用到 vue-server-renderer 库,这里要注意的是 vue-server-renderer 版本要与 Vue 版本一样。 ![img](http://www.manongjc.com/images/51jb/1536803661cf1536T8036o61.png) > #### 开始构建基于 vue-cli3 的 SSR 应用程序 了解 ssr 原理后,来开始 step by step 搭建一个自己的 SSR 应用程序 ###### 1. 安装 vue-cli3 ​ 全局安装 vue-cli 脚手架 ``` npm install @vue/cli -g ``` ​ 创建一个 vue 项目 ``` vue create ssr-example ``` ​ 会进入到一个交互 bash 界面,按自己需要选择 ![img](http://www.manongjc.com/images/51jb/1536803662153b680Kd3m662.png) ​ 一步一步回车,根据自己需要选择 ​ 运行项目 ``` npm run serve ``` ![img](http://www.manongjc.com/images/51jb/153680366215BUg36803v662.png) > #### 一个简单的 ssr 1. ###### 安装需要的包 1. 安装 vue-server-renderer 2. 安装 lodash.merge 3. 安装 webpack-node-externals 4. 安装 cross-env(需要全局安装) ``` npm install vue-server-renderer lodash.merge webpack-node-externals --save-dev ``` ``` npm i cross-env -g ``` 2. ###### 在服务器中集成 在项目根目录下新建一个 server.js 安装 koa2 ``` npm install koa koa-static --save ``` ​ 在 koa2 集成 ssr ``` // server.js // 第 1 步:创建一个 Vue 实例 const Vue = require("vue"); const Koa = require("koa"); const app = new Koa(); // 第 2 步:创建一个 renderer const renderer = require("vue-server-renderer").createRenderer(); // 第 3 步:添加一个中间件来处理所有请求 app.use(async (ctx, next) => { const vm = new Vue({ data: { title: "ssr example", url: ctx.url }, template: `
访问的 URL 是: {{ url }}
` }); // 将 Vue 实例渲染为 HTML renderer.renderToString(vm, (err, html) => { if(err){ res.status(500).end('Internal Server Error') return } ctx.body = html }); }); const port = 3000; app.listen(port, function() { console.log(`server started at localhost:${port}`); }); ``` ​ 运行 server.js ``` node server.js ``` ![img](http://www.manongjc.com/images/51jb/1536803662l153w68Y036u62.png) 看到这个说明一个简单的 ssr 构建成功了。 不过到目前为止,我们并没有将客户端的.vue 文件通过服务端进行渲染,那么如何将前端的.vue 文件与后端 node 进行结合呢? > #### 把项目改造成 ssr 1. 修改源码结构 - 在 src 目录下新建两个文件,一个 **entry-client.js** 仅运行于浏览器 一个 **entry-server.js** 仅运行于服务器 - 修改 main.js main.js 是我们应用程序的「通用 entry」。在纯客户端应用程序中,我们将在此文件中创建根 Vue 实例,并直接挂载到 DOM。但是,对于服务器端渲染(SSR),责任转移到纯客户端 entry 文件。app.js 简单地使用 export 导出一个 createApp 函数: ``` // main.js import Vue from 'vue' import App from './App.vue' import { createRouter } from "./router"; // 导出一个工厂函数,用于创建新的 // 应用程序、router 和 store 实例 export function createApp () { const router = createRouter(); const app = new Vue({ router, // 根实例简单的渲染应用程序组件。 render: h => h(App) }) return { app } } ``` ​ 修改 entry-client.js ​ 客户端 entry 只需创建应用程序,并且将其挂载到 DOM 中 ``` import { createApp } from './main' // 客户端特定引导逻辑…… const { app } = createApp() // 这里假定 App.vue 模板中根元素具有 `id="app"` app.$mount('#app') ``` ​ 修改 entry-server.js ​ 服务器 entry 使用 default export 导出函数,并在每次渲染中重复调用此函数。 ``` import { createApp } from "./main"; export default context => { // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise, // 以便服务器能够等待所有的内容在渲染前, // 就已经准备就绪。 return new Promise((resolve, reject) => { const { app, router, store } = 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); }); }; ``` ​ 修改 router.js ``` import Vue from 'vue' import Router from 'vue-router' import Home from './views/Home.vue' Vue.use(Router) export function createRouter(){ return new Router({ mode: 'history', //一定要是history模式 routes: [ { path: '/', name: 'home', component: Home }, { path: '/about', name: 'about', component: () => import(/* webpackChunkName: "about" */ './views/About.vue') } ] }) } ``` 2、修改 webpack 配置 在 vue-cli3 创建的 vue 项目,已经没有了之前的 webpack.base.conf.js、webpack.dev.conf.js、webpack.prod.conf.js。那么如何进行 webpack 的配置呢? 在[vue-cli](https://cli.vuejs.org/zh/guide/webpack.html)官网上也说明了如何使用。 调整 webpack 配置最简单的方式就是在 vue.config.js 中的 configureWebpack 选项提供一个对象,该对象将会被 [webpack-merge ](https://github.com/survivejs/webpack-merge)合并入最终的 webpack 配置。 在项目根目录下,新建一个 vue.config.js ``` // vue.config.js const VueSSRServerPlugin = require("vue-server-renderer/server-plugin"); const VueSSRClientPlugin = require("vue-server-renderer/client-plugin"); const nodeExternals = require("webpack-node-externals"); const merge = require("lodash.merge"); const TARGET_NODE = process.env.WEBPACK_TARGET === "node"; const target = TARGET_NODE ? "server" : "client"; module.exports = { configureWebpack: () => ({ // 将 entry 指向应用程序的 server / client 文件 entry: `./src/entry-${target}.js`, // 对 bundle renderer 提供 source map 支持 devtool: 'source-map', target: TARGET_NODE ? "node" : "web", node: TARGET_NODE ? undefined : false, output: { libraryTarget: TARGET_NODE ? "commonjs2" : undefined }, // https://webpack.js.org/configuration/externals/#function // https://github.com/liady/webpack-node-externals // 外置化应用程序依赖模块。可以使服务器构建速度更快, // 并生成较小的 bundle 文件。 externals: nodeExternals({ // 不要外置化 webpack 需要处理的依赖模块。 // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件, // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单 whitelist: [/.css$/] }), optimization: { splitChunks: { chunks: "async", minSize: 30000, minChunks: 2, maxAsyncRequests: 5, maxInitialRequests: 3 } }, plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()] }), chainWebpack: config => { config.module .rule("vue") .use("vue-loader") .tap(options => { merge(options, { optimizeSSR: false }); }); } }; ``` 修改 package,新增三个脚本来生成 bundle.json ``` "build:client": "vue-cli-service build --no-clean", "build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build --no-clean", "build:win": "rimraf dist && npm run build:server && npm run build:client", ``` ![img](http://www.manongjc.com/images/51jb/15368036621536QY80636X62.png) 执行命令 ``` npm run build:win ``` 在 dist 目录下会生成两个 json 文件 ![img](http://www.manongjc.com/images/51jb/1536803663153680Q366J1u3.png) 3. ###### 改造 server.js 代码 ``` const fs = require("fs"); const Koa = require("koa"); const path = require("path"); const koaStatic = require('koa-static') const app = new Koa(); const resolve = file => path.resolve(__dirname, file); // 开放dist目录 app.use(koaStatic(resolve('./dist'))) // 第 2 步:获得一个createBundleRenderer const { createBundleRenderer } = require("vue-server-renderer"); const bundle = require("./dist/vue-ssr-server-bundle.json"); const clientManifest = require("./dist/vue-ssr-client-manifest.json"); const renderer = createBundleRenderer(bundle, { runInNewContext: false, template: fs.readFileSync(resolve("./src/index.temp.html"), "utf-8"), clientManifest: clientManifest }); function renderToString(context) { return new Promise((resolve, reject) => { renderer.renderToString(context, (err, html) => { err ? reject(err) : resolve(html); }); }); } // 第 3 步:添加一个中间件来处理所有请求 app.use(async (ctx, next) => { const context = { title: "ssr test", url: ctx.url }; // 将 context 数据渲染为 HTML const html = await renderToString(context); ctx.body = html; }); const port = 3000; app.listen(port, function() { console.log(`server started at localhost:${port}`); }); ``` 4. ###### 运行 server.js 访问 localhost:3000,可以看到页面的数据都是又服务端渲染完成后返回的。到这一步已经基本算完成了 SSR 的构建了。 如果有问题的话,可以把 dist 目录下的 index.html 文件删了。避免直接访问到了该 html 文件。 ![img](http://www.manongjc.com/images/51jb/153680366315N368q0N366T3.png) 5. ###### 集成 vuex 修改 store.js ``` import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export function createStore() { return new Vuex.Store({ state: { }, mutations: { }, actions: { } }); } ``` 修改 main.js ``` import Vue from "vue"; import App from "./App.vue"; import { createRouter } from "@/router"; import { createStore } from "@/store"; export function createApp() { const router = createRouter(); const store = createStore() // + const app = new Vue({ router, store, // + render: h => h(App) }); return { app, router }; } ``` 再次运行脚本构建 **5、案例代码** 附上案例源码 https://github.com/lentoo/vue-cli-ssr-example欢迎star,作者的源码里有三处bug 1. cross-env 需要全局安装,不然跑不动 2. package.json 需要修改的地方, 截图为作者的代码,然而跑不通,不知是不是少了什么,修改后的代码是截图上面的代码 3. entry-client.js 的第一行代码 './app' 应该是'./main' 修改后的源码地址为 https://gitee.com/dc_teach/ssr-demo **6、总结** 到目前为止,仅仅是完成了 SSR 的基础部分,以下是进阶文章. [[为我们的 SSR 程序添加热更新功能](https://www.cnblogs.com/lentoo/p/9790852.html)]