首页 » 神马SEO » vue做seo和ssr_带你五步学会Vue SSR

vue做seo和ssr_带你五步学会Vue SSR

访客 2024-11-01 0

扫一扫用手机浏览

文章目录 [+]

作者:liuxuan 前端名狮

转发链接:https://mp.weixin.***.com/s/6K6GUHcLwLG4mzfaYtVMBQ

vue做seo和ssr_带你五步学会Vue SSR vue做seo和ssr_带你五步学会Vue SSR 神马SEO

序言

SSR大家肯定都不陌生,通过做事端渲染,可以优化SEO抓取,提升首页加载速率等,我在学习SSR的时候,看过很多文章,有些对我有很大的启示浸染,有些就只是照搬官网文档。
通过几天的学习,我对SSR有了一些理解,也从头开始完全的配置出了SSR的开拓环境,以是想通过这篇文章,总结一些履历,同时希望能够对学习SSR的朋友起到一点帮助。

vue做seo和ssr_带你五步学会Vue SSR vue做seo和ssr_带你五步学会Vue SSR 神马SEO
(图片来自网络侵删)

我会通过五个步骤,一步步带你完成SSR的配置:

纯浏览器渲染做事端渲染,不包含Ajax初始化数据做事端渲染,包含Ajax初始化数据做事端渲染,利用serverBundle和clientManifest进行优化一个完全的基于Vue + VueRouter + Vuex的SSR工程

如果你现在对付我上面说的还不太理解,没有关系,随着我一步步向下走,终极你也可以独立配置一个SSR开拓项目,所有源码我会放到github上,大家可以作为参考。

地址:https://github.com/leocoder351/vue-ssr-demo

正文1. 纯浏览器渲染

这个配置相信大家都会,便是基于weback + vue的一个常规开拓配置,这里我会放一些关键代码,完全代码可以去github查看。

目录构造

- node_modules- components - Bar.vue - Foo.vue- App.vue- app.js- index.html- webpack.config.js- package.json- yarn.lock- postcss.config.js- .babelrc- .gitignoreapp.js

import Vue from 'vue';import App from './App.vue';let app = new Vue({ el: '#app', render: h => h(App)});App.vue

<template> <div> <Foo></Foo> <Bar></Bar> </div></template><script>import Foo from './components/Foo.vue';import Bar from './components/Bar.vue';export default { components: { Foo, Bar }}</script>index.html

<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>纯浏览器渲染</title></head><body> <div id="app"></div></body></html>components/Foo.vue

<template> <div class="foo"> <h1>Foo Component</h1> </div></template><style>.foo { background: yellowgreen;}</style>components/Bar.vue

<template> <div class="bar"> <h1>Bar Component</h1> </div></template><style>.bar { background: bisque;}</style>webpack.config.js

const path = require('path');const VueLoaderPlugin = require('vue-loader/lib/plugin');const HtmlWebpackPlugin = require('html-webpack-plugin');const ExtractTextPlugin = require('extract-text-webpack-plugin');module.exports = { mode: 'development', entry: './app.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js' }, module: { rules: [ { test: /\.js$/, use: 'babel-loader' }, { test: /\.css$/, use: ['vue-style-loader', 'css-loader', 'postcss-loader'] // 如果须要单独抽出CSS文件,用下面这个配置 // use: ExtractTextPlugin.extract({ // fallback: 'vue-style-loader', // use: [ // 'css-loader', // 'postcss-loader' // ] // }) }, { test: /\.(jpg|jpeg|png|gif|svg)$/, use: { loader: 'url-loader', options: { limit: 10000 // 10Kb } } }, { test: /\.vue$/, use: 'vue-loader' } ] }, plugins: [ new VueLoaderPlugin(), new HtmlWebpackPlugin({ template: './index.html' }), // 如果须要单独抽出CSS文件,用下面这个配置 // new ExtractTextPlugin("styles.css") ]};postcss.config.js

module.exports = { plugins: [ require('autoprefixer') ]};.babelrc

{ "presets": [ "@babel/preset-env" ], "plugins": [ // 让其支持动态路由的写法 const Foo = () => import('../components/Foo.vue') "dynamic-import-webpack" ]}package.json

{ "name": "01", "version": "1.0.0", "main": "index.js", "license": "MIT", "scripts": { "start": "yarn run dev", "dev": "webpack-dev-server", "build": "webpack" }, "dependencies": { "vue": "^2.5.17" }, "devDependencies": { "@babel/core": "^7.1.2", "@babel/preset-env": "^7.1.0", "babel-plugin-dynamic-import-webpack": "^1.1.0", "autoprefixer": "^9.1.5", "babel-loader": "^8.0.4", "css-loader": "^1.0.0", "extract-text-webpack-plugin": "^4.0.0-beta.0", "file-loader": "^2.0.0", "html-webpack-plugin": "^3.2.0", "postcss": "^7.0.5", "postcss-loader": "^3.0.0", "url-loader": "^1.1.1", "vue-loader": "^15.4.2", "vue-style-loader": "^4.1.2", "vue-template-compiler": "^2.5.17", "webpack": "^4.20.2", "webpack-cli": "^3.1.2", "webpack-dev-server": "^3.1.9" }}命令启动开拓环境

yarnstart构建生产环境

yarnrun build

终极效果截图:

完全代码查看:https://github.com/leocoder351/vue-ssr-demo/tree/master/01

2. 做事端渲染,不包含Ajax初始化数据

做事端渲染SSR,类似于同构,终极要让一份代码既可以在做事端运行,也可以在客户端运行。
如果说在SSR的过程中涌现问题,还可以回滚到纯浏览器渲染,担保用户正常看到页面。

那么,顺着这个思路,肯定就会有两个webpack的入口文件,一个用于浏览器端渲染weboack.client.config.js,一个用于做事端渲染webpack.server.config.js,将它们的公有部分抽出来作为webpack.base.cofig.js,后续通过webpack-merge进行合并。
同时,也要有一个server来供应http做事,我这里用的是koa。

我们来看一下新的目录构造:

- node_modules- config // 新增 - webpack.base.config.js - webpack.client.config.js - webpack.server.config.js- src - components - Bar.vue - Foo.vue - App.vue - app.js - entry-client.js // 新增 - entry-server.js // 新增 - index.html - index.ssr.html // 新增- package.json- yarn.lock- postcss.config.js- .babelrc- .gitignore

在纯客户端运用程序(client-only app)中,每个用户会在他们各自的浏览器中利用新的运用程序实例。
对付做事器端渲染,我们也希望如此:每个要求该当都是全新的、独立的运用程序实例,以便不会有交叉要求造成的状态污染(cross-request state pollution)。

以是,我们要对app.js做修正,将其包装为一个工厂函数,每次调用都会天生一个全新的根组件。

app.js

import Vue from 'vue';import App from './App.vue';export function createApp() { const app = new Vue({ render: h => h(App) }); return { app };}

在浏览器端,我们直接新建一个根组件,然后将其挂载就可以了。

entry-client.js

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

在做事器端,我们就要返回一个函数,该函数的浸染是吸收一个context参数,同时每次都返回一个新的根组件。
这个context在这里我们还不会用到,后续的步骤会用到它。

entry-server.js

import { createApp } from './app.js';export default context => { const { app } = createApp(); return app;}

然后再来看一下index.ssr.html

index.ssr.html

<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>做事端渲染</title></head><body> <!--vue-ssr-outlet--> <script type="text/javascript" src="<%= htmlWebpackPlugin.options.files.js %>"></script></body></html>

<!--vue-ssr-outlet-->的浸染是作为一个占位符,后续通过vue-server-renderer插件,将做事器解析出的组件html字符串插入到这里。

<script type="text/javascript" src="<%= htmlWebpackPlugin.options.files.js %>"></script>是为了将webpack通过webpack.client.config.js打包出的文件放到这里(这里是为了大略演示,后续会有别的办法来做这个事情)。

由于做事端吐出来的便是一个html字符串,后续的Vue干系的相应式、事宜相应等等,都须要浏览器端来接管,以是就须要将为浏览器端渲染打包的文件在这里引入。

用官方的词来说,叫客户端激活(client-side hydration)。

所谓客户端激活,指的是 Vue 在浏览器端接管由做事端发送的静态 HTML,使其变为由 Vue 管理的动态 DOM 的过程。

在 entry-client.js 中,我们用下面这行挂载(mount)运用程序:

// 这里假定 App.vue template 根元素的 `id="app"`app.$mount('#app')

由于做事器已经渲染好了 HTML,我们显然无需将其丢弃再重新创建所有的 DOM 元素。
相反,我们须要"激活"这些静态的 HTML,然后使他们成为动态的(能够相应后续的数据变革)。

如果你检讨做事器渲染的输出结果,你会把稳到运用程序的根元素上添加了一个分外的属性:

<div id="app"data-server-rendered="true">

Vue在浏览器端就依赖这个属性将做事器吐出来的html进行激活,我们一会自己构建一下就可以看到了。

接下来我们看一下webpack干系的配置:

webpack.base.config.js

const path = require('path');const VueLoaderPlugin = require('vue-loader/lib/plugin');module.exports = { mode: 'development', resolve: { extensions: ['.js', '.vue'] }, output: { path: path.resolve(__dirname, '../dist'), filename: '[name].bundle.js' }, module: { rules: [ { test: /\.vue$/, use: 'vue-loader' }, { test: /\.js$/, use: 'babel-loader' }, { test: /\.css$/, use: ['vue-style-loader', 'css-loader', 'postcss-loader'] }, { test: /\.(jpg|jpeg|png|gif|svg)$/, use: { loader: 'url-loader', options: { limit: 10000 // 10Kb } } } ] }, plugins: [ new VueLoaderPlugin() ]};

webpack.client.config.js

const path = require('path');const merge = require('webpack-merge');const HtmlWebpackPlugin = require('html-webpack-plugin');const base = require('./webpack.base.config');module.exports = merge(base, { entry: { client: path.resolve(__dirname, '../src/entry-client.js') }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, '../src/index.html'), filename: 'index.html' }) ]});

把稳,这里的入口文件变成了entry-client.js,将其打包出的client.bundle.js插入到index.html中。

webpack.server.config.js

const path = require('path');const merge = require('webpack-merge');const HtmlWebpackPlugin = require('html-webpack-plugin');const base = require('./webpack.base.config');module.exports = merge(base, { target: 'node', entry: { server: path.resolve(__dirname, '../src/entry-server.js') }, output: { libraryTarget: 'commonjs2' }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, '../src/index.ssr.html'), filename: 'index.ssr.html', files: { js: 'client.bundle.js' }, excludeChunks: ['server'] }) ]});

这里有几个点须要把稳一下:

入口文件是 entry-server.js由于是打经办事器端依赖的代码,以是target要设为node,同时,output的libraryTarget要设为commonjs2

这里关于HtmlWebpackPlugin配置的意思是,不要在index.ssr.html中引入打包出的server.bundle.js,要引为浏览器打包的client.bundle.js,缘故原由前面说过了,是为了让Vue可以将做事器吐出来的html进行激活,从而接管后续相应。

那么打包出的server.bundle.js在哪用呢?接着往下看就知道了~~

package.json

{ "name": "01", "version": "1.0.0", "main": "index.js", "license": "MIT", "scripts": { "start": "yarn run dev", "dev": "webpack-dev-server", "build:client": "webpack --config config/webpack.client.config.js", "build:server": "webpack --config config/webpack.server.config.js" }, "dependencies": { "koa": "^2.5.3", "koa-router": "^7.4.0", "koa-static": "^5.0.0", "vue": "^2.5.17", "vue-server-renderer": "^2.5.17" }, "devDependencies": { "@babel/core": "^7.1.2", "@babel/preset-env": "^7.1.0", "autoprefixer": "^9.1.5", "babel-loader": "^8.0.4", "css-loader": "^1.0.0", "extract-text-webpack-plugin": "^4.0.0-beta.0", "file-loader": "^2.0.0", "html-webpack-plugin": "^3.2.0", "postcss": "^7.0.5", "postcss-loader": "^3.0.0", "style-loader": "^0.23.0", "url-loader": "^1.1.1", "vue-loader": "^15.4.2", "vue-style-loader": "^4.1.2", "vue-template-compiler": "^2.5.17", "webpack": "^4.20.2", "webpack-cli": "^3.1.2", "webpack-dev-server": "^3.1.9", "webpack-merge": "^4.1.4" }}

接下来我们看server端关于http做事的代码:

server/server.js

const Koa = require('koa');const Router = require('koa-router');const serve = require('koa-static');const path = require('path');const fs = require('fs');const backendApp = new Koa();const frontendApp = new Koa();const backendRouter = new Router();const frontendRouter = new Router();const bundle = fs.readFileSync(path.resolve(__dirname, '../dist/server.js'), 'utf-8');const renderer = require('vue-server-renderer').createBundleRenderer(bundle, { template: fs.readFileSync(path.resolve(__dirname, '../dist/index.ssr.html'), 'utf-8')});// 后端ServerbackendRouter.get('/index', (ctx, next) => { // 这里用 renderToString 的 promise 返回的 html 有问题,没有样式 renderer.renderToString((err, html) => { if (err) { console.error(err); ctx.status = 500; ctx.body = '做事器内部缺点'; } else { console.log(html); ctx.status = 200; ctx.body = html; } });});backendApp.use(serve(path.resolve(__dirname, '../dist')));backendApp .use(backendRouter.routes()) .use(backendRouter.allowedMethods());backendApp.listen(3000, () => { console.log('做事器端渲染地址:http://localhost:3000');});// 前端ServerfrontendRouter.get('/index', (ctx, next) => { let html = fs.readFileSync(path.resolve(__dirname, '../dist/index.html'), 'utf-8'); ctx.type = 'html'; ctx.status = 200; ctx.body = html;});frontendApp.use(serve(path.resolve(__dirname, '../dist')));frontendApp .use(frontendRouter.routes()) .use(frontendRouter.allowedMethods());frontendApp.listen(3001, () => { console.log('浏览器端渲染地址:http://localhost:3001');});

这里对两个端口进行监听,3000端口是做事端渲染,3001端口是直接输出index.html,然后会在浏览器端走Vue的那一套,紧张是为了和做事端渲染做比拟利用。

这里的关键代码是如何在做事端去输出html`字符串。

const bundle = fs.readFileSync(path.resolve(__dirname, '../dist/server.bundle.js'), 'utf-8');const renderer = require('vue-server-renderer').createBundleRenderer(bundle, { template: fs.readFileSync(path.resolve(__dirname, '../dist/index.ssr.html'), 'utf-8')});

可以看到,server.bundle.js在这里被利用了,由于它的入口是一个函数,吸收context作为参数(非必传),输出一个根组件app。

这里我们用到了vue-server-renderer插件,它有两个方法可以做渲染,一个是createRenderer,另一个是createBundleRenderer。

const { createRenderer } = require('vue-server-renderer')const renderer = createRenderer({ / 选项 / })

const { createBundleRenderer } = require('vue-server-renderer')const renderer = createBundleRenderer(serverBundle, { / 选项 / })

createRenderer无法吸收为做事端打包出的server.bundle.js文件,以是这里只能用createBundleRenderer。

serverBundle 参数可以是以下之一:

绝对路径,指向一个已经构建好的 bundle 文件(.js 或 .json)。
必须以 / 开头才会被识别为文件路径。
由 webpack + vue-server-renderer/server-plugin 天生的 bundle 工具。
JavaScript 代码字符串(不推举)。

这里我们引入的是.js文件,后续会先容如何利用.json文件以及有什么好处。

renderer.renderToString((err, html) => { if (err) { console.error(err); ctx.status = 500; ctx.body = '做事器内部缺点'; } else { console.log(html); ctx.status = 200; ctx.body = html; }});

利用createRenderer和createBundleRenderer返回的renderer函数包含两个方法renderToString和renderToStream,我们这里用的是renderToString成功后直接返回一个完全的字符串,renderToStream返回的是一个Node流。

renderToString支持Promise,但是我在利用Prmoise形式的时候样式会渲染不出来,暂时还不知道缘故原由,如果大家知道的话可以给我留言哦。

配置基本就完成了,来看一下如何运行。

yarn run build:client // 打包浏览器端须要bundleyarn run build:server // 打包SSR须要bundleyarn start // 实在便是 node server/server.js,供应http做事

终极效果展示:

访问http://localhost:3000/index

我们看到了前面提过的data-server-rendered="true"属性,同时会加载client.bundle.js文件,为了让Vue在浏览器端做后续接管。

访问http://localhost:3001/index还和第一步实现的效果一样,纯浏览器渲染,这里就不放截图了。

完全代码查看:https://github.com/leocoder351/vue-ssr-demo/tree/master/02

3. 做事端渲染,包含Ajax初始化数据

如果SSR须要初始化一些异步数据,那么流程就会变得繁芜一些。

我们先提出几个问题:

做事端拿异步数据的步骤在哪做?如何确定哪些组件须要获取异步数据?获取到异步数据之后要如何塞回到组件内?

带着问题我们向下走,希望看完这篇文章的时候上面的问题你都找到了答案。

做事器端渲染和浏览器端渲染组件经由的生命周期是有差异的,在做事器端,只会经历beforeCreate和created两个生命周期。
由于SSR做事器直接吐出html字符串就好了,不会渲染DOM构造,以是不存在beforeMount和mounted的,也不会对其进行更新,以是也就不存在beforeUpdate和updated等。

我们先来想一下,在纯浏览器渲染的Vue项目中,我们是怎么获取异步数据并渲染到组件中的?一样平常是在created或者mounted生命周期里发起异步要求,然后在成功回调里实行this.data = xxx,Vue监听到数据发生改变,走后面的Dom Diff,打patch,做DOM更新。

那么做事端渲染可不可以也这么做呢?答案是弗成的。

在mounted里肯定弗成,由于SSR都没有mounted生命周期,以是在这里肯定弗成。
在beforeCreate里发起异步要求是否可以呢,也是弗成的。
由于要求是异步的,可能还没有等接口返回,做事端就已经把html字符串拼接出来了。

以是,参考一下官方文档,我们可以得到以下思路:

在渲染前,要预先获取所有须要的异步数据,然后存到Vuex的store中。
在后端渲染时,通过Vuex将获取到的数据注入到相应组件中。
把store中的数据设置到window.__INITIAL_STATE__属性中。
在浏览器环境中,通过Vuex将window.__INITIAL_STATE__里面的数据注入到相应组件中。

正常情形下,通过这几个步骤,做事端吐出来的html字符串相应组件的数据都是最新的,以是第4步并不会引起DOM更新,但如果出了某些问题,吐出来的html字符串没有相应数据,Vue也可以在浏览器端通过`Vuex注入数据,进行DOM更新。

更新后的目录构造:

- node_modules- config - webpack.base.config.js - webpack.client.config.js - webpack.server.config.js- src - components - Bar.vue - Foo.vue - store // 新增 store.js - App.vue - app.js - entry-client.js - entry-server.js - index.html - index.ssr.html- package.json- yarn.lock- postcss.config.js- .babelrc- .gitignore

先来看一下store.js:

store/store.js

import Vue from 'vue';import Vuex from 'vuex';Vue.use(Vuex);const fetchBar = function() { return new Promise((resolve, reject) => { setTimeout(() => { resolve('bar 组件返回 ajax 数据'); }, 1000); });};function createStore() { const store = new Vuex.Store({ state: { bar: '' }, mutations: { 'SET_BAR'(state, data) { state.bar = data; } }, actions: { fetchBar({ commit }) { return fetchBar().then((data) => { commit('SET_BAR', data); }).catch((err) => { console.error(err); }) } } }); if (typeof window !== 'undefined' && window.__INITIAL_STATE__) { console.log('window.__INITIAL_STATE__', window.__INITIAL_STATE__); store.replaceState(window.__INITIAL_STATE__); } return store;}export default createStore;typeof window

如果不太理解Vuex,可以去Vuex官网先看一些基本观点。

这里fetchBar可以算作是一个异步要求,这里用setTimeout仿照。
在成功回调中commit相应的mutation进行状态修正。

这里有一段关键代码:

if (typeof window !== 'undefined' && window.__INITIAL_STATE__) { console.log('window.__INITIAL_STATE__', window.__INITIAL_STATE__); store.replaceState(window.__INITIAL_STATE__);}

由于store.js同样也会被打包到做事器运行的server.bundle.js中,以是运行环境不一定是浏览器,这里须要对window做判断,防止报错,同时如果有window.__INITIAL_STATE__属性,解释做事器已经把所有初始化须要的异步数据都获取完成了,要对store中的状态做一个更换,担保统一。

components/Bar.vue

<template> <div class="bar"> <h1 @click="onHandleClick">Bar Component</h1> <h2>异步Ajax数据:</h2> <span>{{ msg }}</span> </div></template><script> const fetchInitialData = ({ store }) => { store.dispatch('fetchBar'); }; export default { asyncData: fetchInitialData, methods: { onHandleClick() { alert('bar'); } }, mounted() { // 由于做事端渲染只有 beforeCreate 和 created 两个生命周期,不会走这里 // 以是把调用 Ajax 初始化数据也写在这里,是为了供单独浏览器渲染利用 let store = this.$store; fetchInitialData({ store }); }, computed: { msg() { return this.$store.state.bar; } } }</script><style>.bar { background: bisque;}</style>

这里在Bar组件的默认导出工具中增加了一个方法asyncData,在该方法中会dispatch相应的action,进行异步数据获取。

须要把稳的是,我在mounted中也写了获取数据的代码,这是为什么呢? 由于想要做到同构,代码单独在浏览器端运行,也该当是没有问题的,又由于做事器没有mounted生命周期,以是我写在这里就可以办理单独在浏览器环境利用也可以发起同样的异步要求去初始化数据。

components/Foo.vue

<template> <div class="foo"> <h1 @click="onHandleClick">Foo Component</h1> </div></template><script>export default { methods: { onHandleClick() { alert('foo'); } },}</script><style>.foo { background: yellowgreen;}</style>

这里我对两个组件都添加了一个点击事宜,为的是证明在做事器吐出首页html后,后续的步骤都会被浏览器真个Vue接管,可以正常实行后面的操作。

app.js

import Vue from 'vue';import createStore from './store/store.js';import App from './App.vue';export function createApp() { const store = createStore(); const app = new Vue({ store, render: h => h(App) }); return { app, store, App };}

在建立根组件的时候,要把Vuex的store传进去,同时要返回,后续会用到。

末了来看一下entry-server.js,关键步骤在这里:

entry-server.js

import { createApp } from './app.js';export default context => { return new Promise((resolve, reject) => { const { app, store, App } = createApp(); let components = App.components; let asyncDataPromiseFns = []; Object.values(components).forEach(component => { if (component.asyncData) { asyncDataPromiseFns.push(component.asyncData({ store })); } }); Promise.all(asyncDataPromiseFns).then((result) => { // 当利用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态,自动嵌入到终极的 HTML 中 context.state = store.state; console.log(222); console.log(store.state); console.log(context.state); console.log(context); resolve(app); }, reject); });}

我们通过导出的App拿到了所有它下面的components,然后遍历,找出哪些component有asyncData方法,有的话调用并传入store,该方法会返回一个Promise,我们利用Promise.all等所有的异步方法都成功返回,才resolve(app)。

context.state = store.state浸染是,当利用createBundleRenderer时,如果设置了template选项,那么会把context.state的值作为window.__INITIAL_STATE__自动插入到模板html中。

这里须要大家多思考一下,弄清楚全体做事端渲染的逻辑。

如何运行:

yarn run build:clientyarn run build:serveryarn start

终极效果截图:

做事端渲染:打开http://localhost:3000/index

可以看到window.__INITIAL_STATE__被自动插入了。

我们来比拟一下SSR到底对加载性能有什么影响吧。

做事端渲染时performance截图:

纯浏览器端渲染时performance截图:

同样都是在fast 3G网络模式下,纯浏览器端渲染首屏加载花费韶光2.9s,由于client.js加载就花费了2.27s,由于没有client.js就没有Vue,也就没有后面的东西了。

做事端渲染首屏韶光花费0.8s,虽然client.js加载扔花费2.27s,但是首屏已经不须要它了,它是为了让Vue在浏览器端进行后续接管。

从这我们可以真正的看到,做事端渲染对付提升首屏的相应速率是很有浸染的。

当然有的同学可能会问,在做事端渲染获取初始ajax数据时,我们还延时了1s,在这个韶光用户也是看不到页面的。
没错,接口的韶光我们无法避免,就算是纯浏览器渲染,首页该调接口还是得调,如果接口相应慢,那么纯浏览器渲染看到完全页面的韶光会更慢。

完全代码查看:https://github.com/leocoder351/vue-ssr-demo/tree/master/03

4. 利用serverBundle和clientManifest进行优化

前面我们创建做事端renderer的方法是:

const bundle = fs.readFileSync(path.resolve(__dirname, '../dist/server.js'), 'utf-8');const renderer = require('vue-server-renderer').createBundleRenderer(bundle, { template: fs.readFileSync(path.resolve(__dirname, '../dist/index.ssr.html'), 'utf-8')});

serverBundle我们用的是打包出的server.bundle.js文件。
这样做的话,在每次编辑过运用程序源代码之后,都必须停滞并重启做事。
这在开拓过程中会影响开拓效率。
此外,Node.js 本身不支持 source map。

vue-server-renderer 供应一个名为 createBundleRenderer 的 API,用于处理此问题,通过利用 webpack 的自定义插件,server bundle 将天生为可通报到 bundle renderer 的分外 JSON 文件。
所创建的 bundle renderer,用法和普通 renderer 相同,但是 bundle renderer 供应以下优点:

内置的 source map 支持(在 webpack 配置中利用 devtool: 'source-map')在开拓环境乃至支配过程中热重载(通过读取更新后的 bundle,然后重新创建 renderer 实例)关键 CSS(critical CSS) 注入(在利用 .vue 文件时):自动内联在渲染过程中用到的组件所需的CSS。
更多细节请查看 CSS 章节。
利用 clientManifest 进行资源注入:自动推断出最佳的预加载(preload)和预取(prefetch)指令,以及初始渲染所需的代码分割 chunk。

preload和prefetch有不理解的话可以自行查一下它们的浸染哈。

那么我们来修正webpack配置:

webpack.client.config.js

const path = require('path');const merge = require('webpack-merge');const HtmlWebpackPlugin = require('html-webpack-plugin');const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');const base = require('./webpack.base.config');module.exports = merge(base, { entry: { client: path.resolve(__dirname, '../src/entry-client.js') }, plugins: [ new VueSSRClientPlugin(), // 新增 new HtmlWebpackPlugin({ template: path.resolve(__dirname, '../src/index.html'), filename: 'index.html' }) ]});

webpack.server.config.js

const path = require('path');const merge = require('webpack-merge');const nodeExternals = require('webpack-node-externals');const HtmlWebpackPlugin = require('html-webpack-plugin');const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');const base = require('./webpack.base.config');module.exports = merge(base, { target: 'node', // 对 bundle renderer 供应 source map 支持 devtool: '#source-map', entry: { server: path.resolve(__dirname, '../src/entry-server.js') }, externals: [nodeExternals()], // 新增 output: { libraryTarget: 'commonjs2' }, plugins: [ new VueSSRServerPlugin(), // 这个要放到第一个写,否则 CopyWebpackPlugin 不起浸染,缘故原由还没查清楚 new HtmlWebpackPlugin({ template: path.resolve(__dirname, '../src/index.ssr.html'), filename: 'index.ssr.html', files: { js: 'client.bundle.js' }, excludeChunks: ['server'] }) ]});

由于是做事端引用模块,以是不须要打包node_modules中的依赖,直接在代码中require引用就好,以是配置externals: [nodeExternals()]。

两个配置文件会分别天生vue-ssr-client-manifest.json和vue-ssr-server-bundle.json。
作为createBundleRenderer的参数。

来看server.js

server.js

const serverBundle = require(path.resolve(__dirname, '../dist/vue-ssr-server-bundle.json'));const clientManifest = require(path.resolve(__dirname, '../dist/vue-ssr-client-manifest.json'));const template = fs.readFileSync(path.resolve(__dirname, '../dist/index.ssr.html'), 'utf-8');const renderer = createBundleRenderer(serverBundle, { runInNewContext: false, template: template, clientManifest: clientManifest});

效果和第三步便是一样的啦,就不截图了,完全代码查看github。

5. 配置一个完全的基于Vue + VueRouter + Vuex的SSR

这里和第四步不一样的是引入了vue-router,更靠近于实际开拓项目。

在src下新增router目录。

router/index.js

import Vue from 'vue';import Router from 'vue-router';import Bar from '../components/Bar.vue';Vue.use(Router);function createRouter() { const routes = [ { path: '/bar', component: Bar }, { path: '/foo', component: () => import('../components/Foo.vue') // 异步路由 } ]; const router = new Router({ mode: 'history', routes }); return router;}export default createRouter;

这里我们把Foo组件作为一个异步组件引入,做成按需加载。

在app.js中引入router,并导出:

app.js

import Vue from 'vue';import createStore from './store/store.js';import createRouter from './router';import App from './App.vue';export function createApp() { const store = createStore(); const router = createRouter(); const app = new Vue({ router, store, render: h => h(App) }); return { app, store, router, App };}

修正App.vue引入路由组件:

App.vue

<template> <div id="app"> <router-link to="/bar">Goto Bar</router-link> <router-link to="/foo">Goto Foo</router-link> <router-view></router-view> </div></template><script>export default { beforeCreate() { console.log('App.vue beforeCreate'); }, created() { console.log('App.vue created'); }, beforeMount() { console.log('App.vue beforeMount'); }, mounted() { console.log('App.vue mounted'); }}</script>

最主要的修正在entry-server.js中,

entry-server.js

import { createApp } from './app.js';export default context => { return new Promise((resolve, reject) => { const { app, store, router, App } = createApp(); router.push(context.url); router.onReady(() => { const matchedComponents = router.getMatchedComponents(); console.log(context.url) console.log(matchedComponents) if (!matchedComponents.length) { return reject({ code: 404 }); } Promise.all(matchedComponents.map(component => { if (component.asyncData) { return component.asyncData({ store }); } })).then(() => { // 当利用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态,自动嵌入到终极的 HTML 中 context.state = store.state; // 返回根组件 resolve(app); }); }, reject); });}

这里前面提到的context就起了大浸染,它将用户访问的url地址传进来,供vue-router利用。
由于有异步组件,以是在router.onReady的成功回调中,去找该url路由所匹配到的组件,获取异步数据那一套还和前面的一样。

于是,我们就完成了一个基本完全的基于Vue + VueRouter + VuexSSR配置,完成代码查看github。

终极效果演示:

访问http://localhost:3000/bar:

完全代码查看github

后续

上面我们通过五个步骤,完成了从纯浏览器渲染到完全做事端渲染的同构,代码既可以运行在浏览器端,也可以运行在做事器端。
那么,回过分来我们再看一下是否有优化的空间,又或者有哪些扩展的思考。

1. 优化我们目前是利用renderToString方法,完备天生html后,才会向客户端返回,如果利用renderToStream,运用bigpipe技能可以向浏览器持续不断的返回一个流,那么文件的加载浏览器可以尽早的显示一些东西出来。

conststream = renderer.renderToStream(context)

返回的值是 Node.js stream:

let html = ''stream.on('data', data => { html += data.toString()})stream.on('end', () => { console.log(html) // 渲染完成})stream.on('error', err => { // handle error...})

在流式渲染模式下,当 renderer 遍历虚拟 DOM 树(virtual DOM tree)时,会尽快发送数据。
这意味着我们可以尽快得到"第一个 chunk",并开始更快地将其发送给客户端。

然而,当第一个数据 chunk 被发出时,子组件乃至可能不被实例化,它们的生命周期钩子也不会被调用。
这意味着,如果子组件须要在其生命周期钩子函数中,将数据附加到渲染高下文(render context),当流(stream)启动时,这些数据将不可用。
这是由于,大量高下文信息(context information)(如头信息(head information)或内联关键 CSS(inline critical CSS))须要在运用程序标记(markup)之前涌现,我们基本上必须等待流(stream)完成后,才能开始利用这些高下文数据。

因此,如果你依赖由组件生命周期钩子函数添补的高下文数据,则不建议利用流式传输模式。

webpack优化

webpack优化又是一个大的话题了,这里不展开谈论,感兴趣的同学可以自行查找一些资料,后续我也可能会专门写一篇文章来讲webpack优化。

2. 思考是否必须利用vuex?

答案是不用。
Vuex只是为了帮助你实现一套数据存储、更新、获取的机制,如果你不用Vuex,那么你就必须自己想一套方案可以将异步获取到的数据存起来,并且在适当的机遇将它注入到组件内,有一些文章提出了一些方案,我会放到参考文章里,大家可以阅读一下。

是否利用SSR就一定好?

这个也是不一定的,任何技能都有利用场景。
SSR可以帮助你提升首页加载速率,优化搜索引擎SEO,但同时由于它须要在node中渲染整套Vue的模板,会占用做事器负载,同时只会实行beforeCreate和created两个生命周期,对付一些外部扩展库须要做一定处理才可以在SSR中运行等等。

结语

本文通过五个步骤,从纯浏览器端渲染开始,到配置一个完全的基于Vue + vue-router + Vuex的SSR环境,先容了很多新的观点,大概你看完一遍不太理解,那么结合着源码,去自己手敲几遍,然后再来看几遍文章,相信你一定可以节制SSR。

推举Vue学习资料文章:

《记一次Vue3.0技能干货分享会》

《Vue 3.x 如何有惊无险地快速入门「进阶篇」》

《「干货」微信支付前后端流程整理(Vue+Node)》

《带你理解 vue-next(Vue 3.0)之 出神入化「实践」》

《「干货」Vue+高德舆图实现页面点击绘制多边形及多边形切割拆分》

《「干货」Vue+Element前端导入导出Excel》

《「实践」Deno bytes 模块全解析》

《细品pdf.js实践办理含水印、电子签章问题「Vue篇」》

《基于vue + element的后台管理系统办理方案》

《Vue仿蘑菇街商城项目(vue+koa+mongodb)》

《基于 electron-vue 开拓的音乐播放器「实践」》

《「实践」Vue项目中标配编辑器插件Vue-Quill-Editor》

《基于 Vue 技能栈的微前端方案实践》

《行列步队助你成为高薪 Node.js 工程师》

《Node.js 中的 stream 模块详解》

《「干货」Deno TCP Echo Server 是怎么运行的?》

《「干货」了不起的 Deno 实战教程》

《「干货」普通易懂的Deno 入门教程》

《Deno 正式发布,彻底弄明白和 node 的差异》

《「实践」基于Apify+node+react/vue搭建一个有点意思的爬虫平台》

《「实践」深入比拟 Vue 3.0 Composition API 和 React Hooks》

《前端网红框架的插件机制全梳理(axios、koa、redux、vuex)》

《深入Vue 必学高阶组件 HOC「进阶篇」》

《深入学习Vue的data、computed、watch来实现最精简相应式系统》

《10个实例小练习,快速入门闇练 Vue3 核心新特性(一)》

《10个实例小练习,快速入门闇练 Vue3 核心新特性(二)》

《教你支配搭建一个Vue-cli4+Webpack移动端框架「实践」》

《2020前端就业Vue框架篇「实践」》

《详解Vue3中 router 带来了哪些变革?》

《Vue项目支配及性能优化辅导篇「实践」》

《Vue高性能渲染大数据Tree组件「实践」》

《尤大大细品VuePress搭建技能网站与个人博客「实践」》

《10个Vue开拓技巧「实践」》

《是什么导致尤大大选择放弃Webpack?【vite 事理解析】》

《带你理解 vue-next(Vue 3.0)之 小试牛刀【实践】》

《带你理解 vue-next(Vue 3.0)之 初入茅庐【实践】》

《实践Vue 3.0做JSX(TSX)风格的组件开拓》

《一篇文章教你并列比较React.js和Vue.js的语法【实践】》

《手拉手带你开启Vue3天下的鬼斧神工【实践】》

《深入浅出通过vue-cli3构建一个SSR运用程序【实践】》

《若何为你的 Vue.js 单页运用提速》

《聊聊昨晚尤雨溪现场针对Vue3.0 Beta版本新特性知识点汇总》

《【新】Vue 3.0 Beta 版本发布,你还学的动么?》

《Vue真是太好了 壹万多字的Vue知识点 超详细!》

《Vue + Koa从零打造一个H5页面可视化编辑器——Quark-h5》

《深入浅出Vue3 随着尤雨溪学 TypeScript 之 Ref 【实践】》

《手把手教你深入浅出vue-cli3升级vue-cli4的方法》

《Vue 3.0 Beta 和React 开拓者分别杠上了》

《手把手教你用vue drag chart 实现一个可以拖动 / 缩放的图表组件》

《Vue3 尝鲜》

《总结Vue组件的通信》

《Vue 开源项目 TOP45》

《2020 年,Vue 受欢迎程度是否会超过 React?》

《尤雨溪:Vue 3.0的设计原则》

《利用vue实现HTML页面天生图片》

《实现全栈收银系统(Node+Vue)(上)》

《实现全栈收银系统(Node+Vue)(下)》

《vue引入原生高德舆图》

《Vue合理配置WebSocket并实现群聊》

《多年vue项目实战履历汇总》

《vue之将echart封装为组件》

《基于 Vue 的两层吸顶踩坑总结》

《Vue插件总结【前端开拓必备】》

《Vue 开拓必须知道的 36 个技巧【近1W字】》

《构建大型 Vue.js 项目的10条建议》

《深入理解vue中的slot与slot-scope》

《手把手教你Vue解析pdf(base64)转图片【实践】》

《利用vue+node搭建前端非常监控系统》

《推举 8 个俊秀的 vue.js 进度条组件》

《基于Vue实现拖拽升级(九宫格拖拽)》

《手摸手,带你用vue撸后台 系列二(登录权限篇)》

《手摸手,带你用vue撸后台 系列三(实战篇)》

《前端框架用vue还是react?清晰比拟两者差异》

《Vue组件间通信几种办法,你用哪种?【实践】》

《浅析 React / Vue 跨端渲染事理与实现》

《10个Vue开拓技巧助力成为更好的工程师》

《手把手教你Vue之父子组件间通信实践讲解【props、$ref 、$emit】》

《1W字长文+多图,带你理解vue的双向数据绑定源码实现》

《深入浅出Vue3 的相应式和以前的差异到底在哪里?【实践】》

《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》

《基于Vue/VueRouter/Vuex/Axios登录路由和接口级拦截事理与实现》

《手把手教你D3.js 实现数据可视化极速上手到Vue运用》

《吃透 Vue 项目开拓实践|16个方面深入前端工程化开拓技巧【上】》

《吃透 Vue 项目开拓实践|16个方面深入前端工程化开拓技巧【中】》

《吃透 Vue 项目开拓实践|16个方面深入前端工程化开拓技巧【下】》

《Vue3.0权限管理实现流程【实践】》

《后台管理系统,前端Vue根据角色动态设置菜单栏和路由》

标签:

相关文章

海常规网站,打造海洋信息交流新平台

随着海洋经济的快速发展,海洋信息的获取与交流显得尤为重要。在我国,海常规网站应运而生,成为海洋信息交流的新平台。本文将从海常规网站...

神马SEO 2025-01-04 阅读0 评论0

海建筑公司,引领行业发展的先锋力量

随着我国经济的快速发展,基础设施建设成为国家战略的重要组成部分。在这个过程中,海建筑公司凭借其卓越的品质、精湛的技艺和良好的口碑,...

神马SEO 2025-01-04 阅读0 评论0

海建设公司,引领行业创新,铸就品质工程

随着我国经济的快速发展,基础设施建设已经成为国民经济的重要支柱。在这个充满机遇与挑战的时代,海建设公司凭借其雄厚的实力、创新的精神...

神马SEO 2025-01-04 阅读0 评论0