"vue": "^2.6.11", "vue-router": "^3.3.4", "vue-server-renderer": "^2.6.11", "vuex": "^3.4.0"
服务端渲染:将vue实例渲染为HTML字符串直接返回,在前端激活为交互程序,老得;SSH只能返回HTML字符串,并没有法激活。html
接下来 采用同构(同vue)的方式来进行开发,实现服务端渲染。前端
传统web开发,网⻚内容直接在服务端渲染完成,一次性传输到浏览器。从数据库直接拿到html代码;缺点是:服务器响应时间长;带宽消耗,负荷比较大。这种就是所见即所得。vue
单⻚应用优秀的用户体验,使其逐渐成为主流,⻚面内容由JS渲染出来,这种方式称为客户端渲染。 给前端返回的是html的结构,可是没有内容。内容由前端的库,vue or react渲染html;再发送ajax请求,请求数据获取data中的数据来渲染。缺点:不利于SEO,不利于搜索引擎。若是数据没有返回,首屏加载速度会慢node
SSR解决方案,后端渲染出完整的首屏的dom结构返回,仍是用vue or react模板去开发的。前端拿到的内容包括首屏(html结构)及完整spa结构(路由....),在前端作路由的跳转;应用激活后依然按照spa方式运行,这种⻚面渲染方式被称为服务端渲染 (server side render)
若是是一个spa 一个请求 一次响应,打开请求以后还须要ajax请求数据,速度就没有那么快。
npm i express -S
基础http服务 代码演示:
// nodejs 代码 const express = require('express') // 这里是获取express的实例, // 能够从源码中看到: //源码路径:/node_modules/@types/express/index.d.ts // declare function e(): core.Express; 最后导出了 export = e; 全部执行这个函数就能够获得实例 const server = express() // 须要作路由处理,不然打开http://localhost:3000/ 端口会报错。 // 编写路由作不一样的url处理 // req 请求 // res 响应 server.get('/', (req,res)=>{ res.send('3000') //浏览器会认为返回的是 html }) // 监听端口 server.listen(3000, ()=>{ console.log('执行了') })
进入到当前目录下,执行文件,如node 1-express-start.js
;
显示执行了
就说明 代码没有问题
直接访问端口也是同样的,localhost:300
node 文件进行修改,每次都须要node运行一下,能够安装nodemon;就能够实时更新 npm install -g nodemon
; 在启动node 的时候能够用 nodemon 1-express-start.js
react
使用服务器将vue实例成HTMLHTML字符串并返回webpack
npm i vue-server-renderer -S
或者 同时安装vue npm i vue vue-server-renderer -S
;确保版本相同git
首先建立文件
分为三个步骤
1. 建立vue实例 2. 获取渲染器实例 3. 用渲染器来渲染vue实例
// 建立vue实例 const Vue = require ('vue') const app = new Vue({ template:'<div>Hello</div>' }) // 获取渲染器实例 const {createRenderer} = require('vue-server-renderer') // 获取到工厂函数 const renderer =createRenderer() // 就能够获得一个渲染器 // 用渲染器来渲染vue实例 // 返回的是promise,须要.then renderer.renderToString(app) .then((html)=>{ console.log(html) }) .catch((err)=>{ console.log(err); })
结果展现:
![]()
data-server-rendered 服务端渲染的github
在刚刚的express的get中,将上面vue编写的代码进行返回便可
// nodejs 代码 const express = require('express') const server = express() // 建立vue实例 const Vue = require ('vue') // 获取渲染器实例 const {createRenderer} = require('vue-server-renderer') // 获取到工厂函数 // 用渲染器来渲染vue实例 const renderer =createRenderer() // 就能够获得一个渲染器 server.get('/', (req,res)=>{ // 每次用户刷新 都渲染出一个全新的vue出来 const app = new Vue({ template:'<div>Hello~~~~哇哦~~~</div>' }) // 返回的是promise,须要.then renderer.renderToString(app) .then((html)=>{ // 直接把结果返回给浏览器 res.send(html) }) .catch(()=>{ // 错误时 返回状态吗500 res.status(500) res.send('Internal Server Error, 500') }) }) // 监听端口 server.listen(3000, ()=>{ console.log('执行了') })
修改一下,使用数据展现
const app = new Vue({ template:'<div>{{context}}</div>', data(){ return { context:'vvvvvue' } }
如何实现交互呢?
若是直接在服务端写,是否能实现呢?
template:'<div @click="onClick">{{context}}</div>', data(){ return { context:'vue-ssr' } }, methods: { onClick(){ console.log('能够点击吗') } }
页面:
![]()
没有绑定成功,缘由是:
已经转化成字符串再发送到前端,是不可能的。因此须要 激活过程
npm i vue-router -s
在没有SSR的状况下,是返回一个单例的Router实例,web
在服务端渲染的状况下,为了不Router污染的问题,每次请求都返回一个全新的Router。
//做为一个工厂函数,每次用户请求返回一个新的router实例 export default function createRouter(){ return new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes }) }如何让服务端渲染的路由,拿到前端来用?
须要先理解构建流程:
仍是须要用webpack进行打包;
服务端的入口有两个:Server entry
、Client enrty
——————————会生成两个包——————————
生成文件: Server Bundle
「服务器 bundle」用于服务端首屏(不是首页,请求的是什么页什么就是首屏)渲染、Client Bundle
「客户端bundle」用于客户端激活(生成的js代码附加到html,新建一个vue实例,好比上面测试的点击事件的实现)由于服务端传过来的是字符串。前端须要激活。
代码结构
src ├── router ├────── index.js # 路由 ├── store ├────── index.js # 全局状态 ├── main.js # 建立vue实例 ├── entry-client.js # 客户端入口,静态内容“激活” └── entry-server.js # 服务端入口,首屏内容渲染
src/main.js
import Vue from 'vue' import App from './App.vue' import createRouter from './router' Vue.config.productionTip = false // 每一个请求获取一个单独的vue实例 // `调用者是entry-server(首屏渲染) 会传递参数是上下文对象` export function createApp(context){ const router = createRouter() const app =new Vue({ router, context, // 利用context能够拿到一些参数 render: h => h(App) }).$mount('#app') // 导出app实例以及router实例 return {app, router} }
src/entry-server.js
`// 首屏渲染` `// 在服务端执行的代码` import {createApp} from './main' // 建立vue的实例 `// 调用者是renderer` export default context =>{ // 为了让renderer 能够等待处理最后的结果,return的应该是一个promiss return new Promise((resolve, reject)=>{ // 建立vue实例和路由实例 const {app, router} =createApp(context) // 须要渲染首屏 就要拿到当前的url 渲染器会拿到当前的url // 跳转首屏。 // url的来源。是从请求中能够拿到。传递给renderer router.push(context.url) // 考虑到当前页面会存在ajax请求等异步任务处理。要等异步任务处理完在跳转页面 // 监听路由器的ready,确异步任务都完成 router.onReady(()=>{ //该方法把一个回调排队,在路由完成初始导航时调用,这意味着它能够解析全部的异步进入钩子和路由初始化相关联的异步组件。 //这能够有效确保服务端渲染时服务端和客户端输出的一致。 resolve(app) }, reject) // 做为onReady事件的失败函数处理 }) }
src/entry-client.js
`// 客户端激活 就是用户端的交互 好比click等 // 在浏览器执行的代码` import {createApp}~~~~ from './main' // 建立vue实例 const {app, router} =createApp() //等待router就绪 router.onReady(()=>{ //挂载激活 app.$mount('#app') })
entry-server 和 entry-client都用到了 main.js中的createApp 渲染实例必需要获得vue的实例;
npm install webpack-node-externals lodash.merge \-D
// 两个插件分别负责打包客户端和服务端 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 = { css: { extract: false }, outputDir: './dist/'+target, // 输出路径 target看上面判断 configureWebpack: () => ({ // 输入路径 // 将 entry 指向应用程序的 server / client 文件 entry: `./src/entry-${target}.js`, // 入口 // 对 bundle renderer 提供 source map 支持 devtool: 'source-map', // target设置为node使webpack以Node适用的方式处理动态导入, // 而且还会在编译Vue组件时告知`vue-loader`输出面向服务器代码。 target: TARGET_NODE ? "node" : "web", // 是否模拟node全局变量 node: TARGET_NODE ? undefined : false, output: { // 此处使用Node风格导出模块 libraryTarget: TARGET_NODE ? "commonjs2" : undefined }, // https://webpack.js.org/configuration/externals/#function // https://github.com/liady/webpack-node-externals // 外置化应用程序依赖模块。可使服务器构建速度更快,并生成较小的打包文件。 externals: TARGET_NODE ? nodeExternals({ // 不要外置化webpack须要处理的依赖模块。 // 能够在这里添加更多的文件类型。例如,未处理 *.vue 原始文件, // 还应该将修改`global`(例如polyfill)的依赖模块列入白名单 whitelist: [/\.css$/] }) : undefined, optimization: { splitChunks: undefined }, // 这是将服务器的整个输出构建为单个 JSON 文件的插件。 // 服务端默认文件名为 `vue-ssr-server-bundle.json` // 客户端默认文件名为 `vue-ssr-client-manifest.json`。 plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()] }), chainWebpack: config => { // cli4项目添加 if (TARGET_NODE) { config.optimization.delete('splitChunks') } config.module .rule("vue") .use("vue-loader") .tap(options => { merge(options, { optimizeSSR: false }); }); } };
npm i cross-env \-D
"scripts": { "build": "npm run build:server & npm run build:client", "build:client": "vue-cli-service build", "build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build" },
执行 npm run build 进行打包
public/index.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"> </head> <body> <!--vue-ssr-outlet--> </body> </html>
server/4-ssr.js
const express = require('express') const app = express() // 静态资源服务 // 把这个路径打开(../dist/client),让用户能够下载文件 const path = require('path') const resolve = dir => path.resolve(__dirname, dir) // 相对路径不可靠(../dist/client),须要用绝对路径 app.use(express.static(resolve('../dist/client'),{index: false}))//指定根目录,将根目录开发给用户看 //{index: false} 设置的目的是,由于在client 里面有index.html; 因此就不会走下面的代码,就会直接返回client里面的index.html // 渲染器: bundleRenderer, 它能够获取前面生成的两个json文件 const { createBundleRenderer } = require('vue-server-renderer') //指向绝对路径 const bundle = resolve('../dist/server/vue-ssr-server-bundle.json') //获得渲染器能够直接渲染 vue实例 const renderer = createBundleRenderer(bundle, { // 选项 runInNewContext: false, // https://ssr.vuejs.org/zh/api/#runinnewcontext 文档地址 // 宿主文件 template: require('fs').readFileSync(resolve("../public/index.html"), "utf-8"), // 宿主文件 utf-8的方式转化成字符串 clientManifest: require(resolve("../dist/client/vue-ssr-client-manifest.json")) // 客户端清单 优化内容 }) app.get('*',async(req,res)=>{ const context = { url: req.url } try{ // 渲染获取html // 建立vue实例 建立首屏 渲染出来 如今是个静态的不能交互 const html = await renderer.renderToString(context) res.send(html)//发送到 前端交互 entry-client 一挂在就能够渲染出来 }catch(error){ res.status(500).send('Internal Server Error') } }) app.listen(3001)
同构成功!!!!
每一次的项目修改,都须要从新 npm run build 和 启动node文件
`npm install -S vuex` 若是是用vue-cli建立的项目,能够用 vue add vuex 安装,结果同样
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) // `须要独立出来` export default function createStore(){ return new Vuex.Store({ state: { count:100 }, mutations: { add(state){ state.count+=1 } }, actions: { }, modules: { } }) }
import Vue from 'vue' import App from './App.vue' import createRouter from './router' `import createStore from './store'` Vue.config.productionTip = false // 每一个请求获取一个单独的vue实例 // 调用者是entry-server(首屏渲染) 会传递参数是上下文对象 export function createApp(context){ const router = createRouter() const store = createStore() const app =new Vue({ router, // 利用context能够拿到一些参数 context, store, `// 挂载` render: h => h(App) }).$mount('#app') // 导出app实例以及router实例 return {app, router, store} }
测试vuex的引入
<p @click="$store.commit('add')">{{$store.state.count}}</p>
服务器端渲染的是应用程序的"快照",若是应用依赖于一些异步数据,那么在开始渲染以前,须要先预取和解析好这些数据。
在服务端把数据准备好,带着数据的把页面渲染完成。
mutations: { // 加初始化数据 init(state,count){ state.count =count } },
actions: { `// 一个异步请求数据 触发init 模仿一个接口` getCount({commit}){ return new Promise((reslove)=>{ setTimeout(() => { commit('init', Math.random()*100) // 生成随机数做为初始值 reslove() }, 1000); }) } },
接下来在须要调用数据的路由对应页面来作数据预取逻辑
Hello.vueexport default { name: 'Hello', asyncData({store, router}) { console.log(router, 'asyncDtata-router') return store.dispatch('getCount') } }
asyncData
它使得你可以在渲染组件以前异步获取数据。 asyncData方法会在组件(限于页面组件)每次加载以前被调用。它能够在服务端或路由更新以前被调用。
// 首屏渲染 //在服务端执行的代码 import {createApp} from './main' //建立vue的实例 // 调用者是renderer export default context =>{ // 为了让renderer 能够等待处理最后的结果,return的应该是一个promiss return new Promise((resolve, reject)=>{ // 建立vue实例和路由实例 `const {app, router,store} =createApp(context)` // 须要渲染首屏 就要拿到当前的url 渲染器会拿到当前的url // 跳转首屏。 // url的来源。是从请求中能够拿到。传递给renderer router.push(context.url) // 考虑到当前页面会存在ajax请求等异步任务处理。要等异步任务处理完在跳转页面 // 监听路由器的ready,确异步任务都完成 router.onReady(()=>{ //该方法把一个回调排队,在路由完成初始导航时调用,这意味着它能够解析全部的异步进入钩子和路由初始化相关联的异步组件。 //这能够有效确保服务端渲染时服务端和客户端输出的一致。 `// 首先处理异步的数据,以后在存放到渲染中 // 因此须要匹配组建中是否存在asyncData选项 const matchedComponents =router.getMatchedComponents() // 获取url匹配 到全部匹配的组建数组` //用户是瞎输入的地址,可能会致使matchedComponents获取错误 404 if(!matchedComponents.length){ return reject({code:404}) } `//须要遍历一下数组 看组建是否有匹配asyncData` Promise.all( matchedComponents.map(comp =>{ // 看组建是否有匹配到asyncData if(comp.asyncData){ // 传 store 为了找到dispatch 对应的 actions // 传 router 是为了若是url后面带参数 &wd=vue return comp.asyncData({store,route:router.currentRoute})// 异步调用 因此返回的是一个promise,每次都return,就会返回一个promise数组 } })).then(()=>{ // 数据放在store 前端不知道这一步,因此须要通知前端 //接下来作一个约定 // 全部的预取数据resolve以后 // store已进填充了当前数据状态 //数据须要同步到前端 // 序列化操做,转化成字符串 前端使用window.__INITIAL_STATE__获取 // 赋值给 context.state; 是一个约定, context.state = store.state resolve(app) }).catch(reject) // 捕获异常 }, reject) // 做为onReady事件的失败函数处理 }) }
在entry-client.js
恢复store
const {app, router, store} =createApp() if(window.__INITIAL_STATE__){ console.log(window.__INITIAL_STATE__, 'window.__INITIAL_STATE__'); // 恢复state store.replaceState(window.__INITIAL_STATE__) }
服务端渲染的时候直接生成的,反序列化以后直接生成一个字符串插入这里,在前端一执行就变成对象了。
若是,路由切换到http://localhost:3001/about
,在About刷新
再切回首页,就不会走服务器的数据,而是本地的state数据。
About刷新结果:
切回来会变成这样:
问题
只是解决首屏加载数据的问题,没有解决在客户端路由切换的问题。
若是在客户端的组建里面也发现asyncData这个配置项,也须要执行
思路
加入全局混入 mixin
在main.js 中 混合式mixin
// 添加全局混入 mixin // 混入到 beforeMount钩子中 // 服务端中不会被触发beforeMount,由于对服务端来讲直接渲染页面,不存在Dom挂在,因此不会触发这个钩子 // 因此只会在客户端执行 beforMount Vue.mixin({ beforeMount() { // 这个钩子执行的时候vue实例已经存在了,在前端已经挂载过了。因此能够从this中去获取sotre const {asyncData} =this.$options if(asyncData){ //存在就调用 asyncData( { store: this.$store, // this 指的是vue实例 route: this.$route } )//这里须要传参数 } }, })
OK~关于 初识vue 的ssr服务端渲染就到这里了。
可能会存在错别字,可是不影响知识的传递,哈哈哈哈哈哈!
有问题随时留言,咱们一块儿探讨和进步,谢谢
一块儿加油把 Yes ok!
原生:vue ssr https://ssr.vuejs.org/zh/
框架:nuxt.js https://nuxtjs.org/
github: https://github.com/speak44/ss...