15分钟学会vue项目改造成SSR(小白教程)

15分钟学会vue项目改造成SSR(小白教程)

2023年6月29日发(作者:)

15分钟学会vue项⽬改造成SSR(⼩⽩教程)15分钟学会vue项⽬改造成SSRPs:⽹上看了好多服务器渲染的例⼦,基本都是从0开始的,⽤Nuxt或者vue官⽹推荐的ssr⽅案(vue-server-renderer),但是我们在开发过程中基本上是已经有了现有的项⽬了,我们所要做的是对现有项⽬的SSR改造。那么这⾥,跟我⼀起对⼀个vue-cil2.0⽣成的项⽬进⾏SSR改造关于这篇⽂章的案例源代码我放在我的上⾯,有兴趣的同学,也可以去我的github查看我之前写的博客。⼀、改造技术的分析对⽐。⼀般来说,我们做seo有两种⽅式:1、预渲染我在性能优化的博客中说过,预渲染的问题,预渲染是⼀个⽅案,使⽤爬⾍技术。由于我们打包过后的都是⼀些js⽂件,使⽤⼀些技术(puppeteer)可以爬取到项⽬在chrome浏览器展⽰的页⾯,然后把它写⼊js,和打包⽂件⼀起。类似prerender-spa-plugin 。最⼤的特点就是,所有获取的数据都是静态的,⽐如说你的页⾯⾸页有新闻,是通过接⼝获取的,当你在2019-11-30打包之后,不管⽤户在2020年也是看到的2019-11-30的新闻,当然的爬⾍爬到的也是。如果你只需要改善少数页⾯(例如 /, /about, /contact 等)的 SEO,那么你可能需要预渲染2、服务端渲染服务端渲染是将完整的 html 输出到客户端,⼜被认为是‘同构'或‘通⽤',如果你的项⽬有⼤量的detail页⾯,相互特别频繁,建议选择服务端渲染。**服务端渲染除了SEO还有很多时候⽤作⾸屏优化,加快⾸屏速度,提⾼⽤户体验。**但是对服务器有要求,⽹络传输数据量⼤,占⽤部分服务器运算资源。由于三⼤框架的兴起,SPA项⽬到处都是,所以涌现了⼀批、这些服务器渲染的框架。但是这些框架构建出来的项⽬可能⽂件夹和我们现有的项⽬很⼤不⼀样,所以本⽂章主要是⽤vue-server-renderer来对现有项⽬进⾏改造,⽽不是去⽤框架。ps:(划重点)单页⾯项⽬的ssr改造的原理:vue项⽬是通过虚拟 DOM来挂载到html的,所以对spa项⽬,爬⾍才会只看到初始结构。虚拟 DOM,最终要通过⼀定的⽅法将其转换为真实 DOM。虚拟 DOM 也就是 JS 对象,整个服务端的渲染流程就是通过虚拟 DOM 的编译成完整的html来完成的。我们通过服务端渲染解析虚拟 DOM成html之后,你会发现页⾯的事件,都没法触发。那是因为服务端渲染vue-server-renderer插件并没有做这⽅⾯的处理,所以我们需要客户端再渲染⼀遍,简称同构。所以Vue服务端渲染其实是渲染了两遍。下⾯给出⼀个官⽅的图:⼆、改造前后⽬录⽂件对⽐黄线部分是改造后新增的⽂件,怎么样,是不是觉得差别不⼤,总体架构上只有6个⽂件的差别。(#.#) 我们来理⼀理这些新增的⽂件。 本地调试和热更新需要的配置⽂件 客户端打包配置⽂件,ssr打包是⽣成分为客户端和服务端的两部分打包⽂件 服务端打包配置⽂件,ssr打包是⽣成分为客户端和服务端的两部分打包⽂件 客户端⼊⼝⽂件。spa的⼊⼝是,ssr就分为两个⼊⼝(服务端和客户端) 服务端⼊⼝⽂件。spa的⼊⼝是,ssr就分为两个⼊⼝(服务端和客户端) 模板⽂件,因为服务端渲染是通过服务器把页⾯丢出来,所以我们需要⼀个模板,作为页⾯初始载体,然后往⾥⾯添加内容。 启动⽂件,服务端渲染我们需要启动⼀个node服务器,主要配置在这个⽂件⾥⾯。三、webpack添加客户端与服务端配置k客户端配置const webpack = require('webpack')const merge = require('webpack-merge')const baseConfig = require('./')const HtmlWebpackPlugin = require('html-webpack-plugin')const path = require('path')const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')s = merge(baseConfig, { entry: './src/', plugins: [ new sChunkPlugin({ name: "manifest", minChunks: Infinity }), // 此插件在输出⽬录中 // ⽣成 ``。 new VueSSRClientPlugin(), new HtmlWebpackPlugin({ template: e(__dirname, './../src/'), filename: '' }) ]})这⾥⾯和spa项⽬有两点不同,第⼀是⼊⼝变了,变为了。第⼆是VueSSRClientPlugin,这个是⽣成⼀个客户端⼊⼝⽂件。k服务端配置const merge = require('webpack-merge')const nodeExternals = require('webpack-node-externals')const baseConfig = require('./')const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')s = merge(baseConfig, { // 将 entry 指向应⽤程序的 server entry ⽂件 entry: './src/', // 这允许 webpack 以 Node 适⽤⽅式(Node-appropriate fashion)处理动态导⼊(dynamic import), // 并且还会在编译 Vue 组件时, // 告知 `vue-loader` 输送⾯向服务器代码(server-oriented code)。 target: 'node', // 对 bundle renderer 提供 source map ⽀持 devtool: 'source-map', // 此处告知 server bundle 使⽤ Node 风格导出模块(Node-style exports) output: { libraryTarget: 'commonjs2' }, externals: nodeExternals({ // 不要外置化 webpack 需要处理的依赖模块。 // 你可以在这⾥添加更多的⽂件类型。例如,未处理 *.vue 原始⽂件, // 你还应该将修改 `global`(例如 polyfill)的依赖模块列⼊⽩名单 whitelist: /.css$/ }), // 这是将服务器的整个输出 // 构建为单个 JSON ⽂件的插件。 // 默认⽂件名为 `` plugins: [ new VueSSRServerPlugin() ]})这段代码⼀⽬了然,第⼀是是告诉webpack这是要打包node能运⾏的东西,第⼆是打包⼀个服务端⼊⼝四、vue、router、store实例改造当编写纯客户端 (client-only) 代码时,我们习惯于每次在新的上下⽂中对代码进⾏取值。但是, 服务器是⼀个长期运⾏的进程。当我们的代码进⼊该进程时,它将进⾏⼀次取值并留存在内存中。这意味着如果创建⼀个单例对象,它将在每个传⼊的请求之间共享。nodejs是⼀个运⾏时,如果只是个单例的话,所有的请求都会共享这个单例,会造成状态污染。所以我们需要为每个请求创造⼀个vue,router,store实例。第⼀步修改// rt Vue from 'vue'import App from './'import { createRouter } from './router'import { createStore } from './store/'import { sync } from 'vuex-router-sync'export function createApp () { // 创建 router 实例 const router = createRouter() const store = createStore() // 同步路由状态(route state)到 store sync(store, router) const app = new Vue({ // 注⼊ router 到根 Vue 实例 router, store, render: h => h(App) }) // 返回 app 和 router return { app, router, store }}看到这个createApp没,没错,它就是我们熟悉的⼯⼚模式。同样的store和router⼀样改造// rt Vue from 'vue'import Router from 'vue-router'import HelloWorld from '@/components/HelloWorld'(Router)export let createRouter = () => { let route = new Router({ mode:'history', routes: [] }) return route}// // rt Vue from 'vue'import Vuex from 'vuex'(Vuex)export function createStore () { return new ({ state: { }, actions: { }, mutations: { } })}到这⾥,三个实例对象改造完成了。是不是很简单~五、数据预取和存储服务器渲染,可以理解为在被访问的时候,服务端做预渲染⽣成页⾯,上⾯说过,预渲染的缺点就是,实时数据的获取。所以如果应⽤程序依赖于⼀些异步数据,那么在开始渲染过程之前,需要先预取和解析好这些数据。另⼀个需要关注的问题是在客户端,在挂载 (mount) 到客户端应⽤程序之前,需要获取到与服务器端应⽤程序完全相同的数据 - 否则,客户端应⽤程序会因为使⽤与服务器端应⽤程序不同的状态,然后导致混合失败。这个地⽅上⾯提过,叫同构(服务端渲染⼀遍,客户端拿到数据再渲染⼀遍)。因为我们⽤的vue框架嘛,那当然数据存储选vuex咯。然后我们来理⼀下总体的流程:客户端访问⽹站 —> 服务器获取动态数据,⽣成页⾯,并把数据存⼊vuex中,然后返回html —> 客户端获取html(此时已经返回了完整的页⾯) —> 客户端获取到vuex的数据,并解析到vue⾥⾯,然后再⼀次找到根元素挂载vue,重复渲染页⾯。(同构阶段)流程清楚之后,那我们怎么设定,哪个地⽅的代码,被服务端执⾏,并获取数据存⼊vuex呢? 我们分为三步:1.⾃定义函数asyncData官⽅的例⼦是定义⼀个asyncData函数(这个名字不是唯⼀的哈,是⾃⼰定义的,可以随便取,不要理解为内置的函数哈),这个函数写在路由组件⾥⾯。假设有⼀个组件(官⽹的例⼦)2. 服务端⼊⼝配置到这⾥,asyncData函数,我们知道它是放在哪⾥了。接下来,我们有了这个函数,我们服务器肯定要去读到这个函数,然后去获取数据吧?我们把⽬光放到,之前我们提到过,这是服务端的⼊⼝页⾯。那我们是不是能够在这⾥⾯处理asyncData呢。下⾯还是官⽹的例⼦:// rt { createApp } from './app'export default context => { return new Promise((resolve, reject) => { const { app, router, store } = createApp() () y(() => { const matchedComponents = chedComponents() if (!) { return reject({ code: 404 }) } // 对所有匹配的路由组件调⽤ `asyncData()` ((Component => { if (ata) { return ata({ store, route: tRoute }) } })).then(() => { // 在所有预取钩⼦(preFetch hook) resolve 后, // 我们的 store 现在已经填充⼊渲染应⽤程序所需的状态。 // 当我们将状态附加到上下⽂, // 并且 `template` 选项⽤于 renderer 时, // 状态将⾃动序列化为 `window.__INITIAL_STATE__`,并注⼊ HTML。 = resolve(app) }).catch(reject) }, reject) })}简单的读下这段代码。⾸先为什么是返回Promise呢?因为可能是异步路由和组件,我们得保证,服务器渲染之前,已经完全准备就绪了。 然后注意**matchedComponents **它是通过传⼊的地址,获取到和路由匹配到的组件,然后如果存在asyncData,我们就去执⾏它,然后注⼊到context(渲染上下⽂,可以在客户端获取)⾥⾯。是不是简单?这⼀步我们就已经从服务器端取到动态数据了,同时丢到页⾯⾥⾯了。如果不是为了客户端数据同步,这⼀步我们已经搞完服务端渲染了~ = =3.客户端⼊⼝配置搞完服务器端的配置,该客户端了,毕竟数据要同步嘛。我们来看看客户端的⼊⼝⽂件代码:const { app, router, store } = createApp()if (window.__INITIAL_STATE__) { eState(window.__INITIAL_STATE__)}之前服务端⼊⼝说过,状态将⾃动序列化为

window.__INITIAL_STATE__,并注⼊ HTML。所以客户端我们获取到了,服务端已经搞好了数据了,我们拿过来直接替换现有的vuex就好了。看到这⾥,不是已经完成啦,完整的流程。但是到此为⽌了吗?还没呢,既然是服务端渲染,你总要启动服务器吧…Ps: 数据预期,我们刚才讲到的只是服务端预取,其实还有客户端预取。什么是客户端预取呢,简单的理解就是,我们可以在路由钩⼦⾥⾯,找有当前路由组件没有asyncData,有的话,就去请求,获取到数据后,填充完之后,再渲染页⾯。六、启动服务()配置服务端渲染,服务端,肯定要⼀个启动服务的⽂件哈,const express = require("express");const fs = require('fs');let path = require("path");const server = express()const { createBundleRenderer } = require('vue-server-renderer')let rendererconst resolve = file => e(__dirname, file)const templatePath = resolve('./src/')function createRenderer (bundle, options) { return createBundleRenderer(bundle, (options, { runInNewContext: false }))}const template = leSync(templatePath, 'utf-8')const bundle = require('./dist/')const clientManifest = require('./dist/')renderer = createRenderer(bundle, { template, clientManifest})(('./dist'))// 在服务器处理函数中……('*', (req, res) => { const context = { url: } // 这⾥⽆需传⼊⼀个应⽤程序,因为在执⾏ bundle 时已经⾃动创建过。 ToString(context, (err, html) => { // 处理异常…… (html) })})(3001, () => { ('服务已开启')})这就是服务端的启动代码了,只需处理获取⼏个打包过后的参数(template模板和clientManifest),传⼊createBundleRenderer函数。然后通过renderToString,展现给客户端。七、热更新与本地调试上⾯⼀步是启动服务,但是我们本地调试的时候,不可能每次build之后,再启动,然后再修改,再build吧?那也太⿇烦了。所以我们借助webpack搞⼀个热更新。这⾥在build⾥⾯添加⼀个⽂件//t fs = require('fs')const path = require('path')const MFS = require('memory-fs')const webpack = require('webpack')const chokidar = require('chokidar')const clientConfig = require('./')const serverConfig = require('./')const readFile = (fs, file) => { try { return leSync((, file), 'utf-8') } catch (e) {}}s = 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 = leSync(templatePath, 'utf-8') (templatePath).on('change', () => { template = leSync(templatePath, 'utf-8') (' template updated.') update() }) // modify client config to work with hot middleware = ['webpack-hot-middleware/client', ] me = '[name].js' ( new uleReplacementPlugin(), new OnErrorsPlugin() ) // dev middleware const clientCompiler = webpack(clientConfig) const devMiddleware = require('webpack-dev-middleware')(clientCompiler, { publicPath: Path, noInfo: true }) (devMiddleware) ('done', stats => { stats = () h(err => (err)) h(err => (err)) if () return clientManifest = (readFile( stem, '' )) update() }) // hot middleware (require('webpack-hot-middleware')(clientCompiler, { heartbeat: 5000 })) // watch and update server renderer const serverCompiler = webpack(serverConfig) const mfs = new MFS() FileSystem = mfs ({}, (err, stats) => { if (err) throw err stats = () if () return // read bundle generated by vue-ssr-webpack-plugin bundle = (readFile(mfs, '')) update() }) return readyPromise}这个代码基本上是从官⽅⽂档copy下来的,写的挺好的 哈哈。怎么理解这段代码呢,这个代码封装了⼀个promise,因为代码更新后重新打包需要时间,所以我们在renderToString之前,需要等待⼀段处理的时间。这个代码对3部分进⾏了监控,、vue业务代码、客户端配置代码。检测到有改动之后,就重新打包获取,然后返回。这⾥就是热更新部分代码,当然我们还要改动部分代码,毕竟要处理开发模式和⽣成模式的不同。//t express = require("express");const fs = require('fs');let path = require("path");const server = express()const { createBundleRenderer } = require('vue-server-renderer')const isProd = _ENV === 'production'let rendererlet readyPromiseconst resolve = file => e(__dirname, file)const templatePath = resolve('./src/')function createRenderer (bundle, options) { return createBundleRenderer(bundle, (options, { runInNewContext: false }))}if(isProd){ const template = leSync(templatePath, 'utf-8') const bundle = require('./dist/') const clientManifest = require('./dist/') renderer = createRenderer(bundle, { template, clientManifest })}else{ readyPromise = require('./build/')( server, templatePath, (bundle, options) => { renderer = createRenderer(bundle, options) } )}(('./dist'))// 在服务器处理函数中……('*', (req, res) => { const context = { url: } // 这⾥⽆需传⼊⼀个应⽤程序,因为在执⾏ bundle 时已经⾃动创建过。 // 现在我们的服务器与应⽤程序已经解耦! if(isProd){ ToString(context, (err, html) => { // 处理异常…… (html) }) }else{ (()=>{ ToString(context, (err, html) => { // 处理异常…… (html) }) }) }})(3001, () => { ('服务已开启')})从的代码改动,我们可以看到,server进⾏了是否为⽣产环境的判断,如果是测试环境,就取运⾏,获得返回的promise,然后再renderToString之前,把renderToString加⼊到promise链式调⽤⾥⾯,这样,热更新就完成了,每次调⽤路由的时候,都会去获取到最新的页⾯。到这⾥所有的ssr改造已经完成了,当然我们还能优化,下⾯给出⼏个点,⾃⼰思考哈:服务器缓存,既然是node服务器,我们当然可以做服务器缓存拉。流式渲染 (Streaming) ⽤ renderToStream 替代 renderToString;当 renderer 遍历虚拟 DOM 树 (virtual DOM tree) 时,会尽快发送数据。这意味着我们可以尽快获得"第⼀个 chunk",并开始更快地将其发送给客户端以上就是本⽂的全部内容,希望对⼤家的学习有所帮助,也希望⼤家多多⽀持。

发布者:admin,转转请注明出处:http://www.yc00.com/news/1687986368a64024.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信