您好,登录后才能下订单哦!
在现代Web开发中,单页应用(SPA)已经成为主流。然而,SPA在首次加载时通常需要下载大量的JavaScript文件,这会导致首屏加载时间较长,影响用户体验。为了解决这个问题,服务端渲染(SSR)应运而生。Vue.js 流行的前端框架,提供了强大的SSR支持。本文将详细介绍Vue服务端渲染的原理、实现步骤、优化技巧以及常见问题的解决方案。
服务端渲染(Server-Side Rendering,简称SSR)是指在服务器端将Vue组件渲染成HTML字符串,然后将这些HTML字符串发送到客户端。客户端接收到HTML后,可以直接显示页面内容,而不需要等待JavaScript文件的下载和执行。
与传统的客户端渲染(CSR)相比,SSR具有以下优势:
Vue.js 提供了官方的SSR支持,使得开发者可以轻松地将Vue应用转换为SSR应用。Vue SSR 的优势包括:
Vue SSR 的基本原理是将Vue组件在服务器端渲染成HTML字符串,然后将这些HTML字符串发送到客户端。客户端接收到HTML后,Vue会在客户端重新激活这些组件,使其具有交互能力。
具体来说,Vue SSR 的工作流程如下:
首先,我们需要安装Vue SSR所需的依赖包。可以使用npm或yarn进行安装:
npm install vue vue-server-renderer express
接下来,我们需要创建一个Vue实例。这个实例将在服务器端和客户端共享。
// app.js
import Vue from 'vue';
import App from './App.vue';
export function createApp() {
const app = new Vue({
render: h => h(App)
});
return { app };
}
然后,我们需要创建一个Express服务器来处理客户端的请求,并将Vue组件渲染成HTML字符串。
// server.js
const express = require('express');
const { createApp } = require('./app');
const renderer = require('vue-server-renderer').createRenderer();
const server = express();
server.get('*', (req, res) => {
const { app } = createApp();
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error');
return;
}
res.end(`
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>${html}</body>
</html>
`);
});
});
server.listen(8080);
为了在服务器端和客户端共享Vue组件,我们需要配置Webpack。具体来说,我们需要为服务器端和客户端分别创建两个Webpack配置文件。
// webpack.server.config.js
const path = require('path');
const nodeExternals = require('webpack-node-externals');
module.exports = {
target: 'node',
externals: [nodeExternals()],
entry: './app.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'server.bundle.js',
libraryTarget: 'commonjs2'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
}
]
},
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
}
};
// webpack.client.config.js
const path = require('path');
module.exports = {
entry: './entry-client.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'client.bundle.js'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
}
]
},
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
}
};
在SSR中,路由的处理方式与CSR有所不同。我们需要在服务器端根据请求的URL匹配相应的Vue组件。
// router.js
import Vue from 'vue';
import Router from 'vue-router';
import Home from './components/Home.vue';
import About from './components/About.vue';
Vue.use(Router);
export function createRouter() {
return new Router({
mode: 'history',
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
});
}
// app.js
import Vue from 'vue';
import App from './App.vue';
import { createRouter } from './router';
export function createApp() {
const router = createRouter();
const app = new Vue({
router,
render: h => h(App)
});
return { app, router };
}
// server.js
const express = require('express');
const { createApp } = require('./app');
const renderer = require('vue-server-renderer').createRenderer();
const server = express();
server.get('*', (req, res) => {
const { app, router } = createApp();
router.push(req.url);
router.onReady(() => {
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error');
return;
}
res.end(`
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>${html}</body>
</html>
`);
});
}, err => {
if (err) {
res.status(500).end('Internal Server Error');
return;
}
});
});
server.listen(8080);
在SSR中,我们通常需要在服务器端预取数据,以便在渲染组件时使用这些数据。Vue SSR 提供了asyncData
方法来实现数据预取。
// Home.vue
<template>
<div>
<h1>{{ title }}</h1>
<p>{{ content }}</p>
</div>
</template>
<script>
export default {
asyncData({ store, route }) {
return store.dispatch('fetchData', route.params.id);
},
computed: {
title() {
return this.$store.state.title;
},
content() {
return this.$store.state.content;
}
}
};
</script>
// store.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export function createStore() {
return new Vuex.Store({
state: {
title: '',
content: ''
},
actions: {
fetchData({ commit }, id) {
return new Promise((resolve) => {
setTimeout(() => {
commit('setData', {
title: 'Hello World',
content: 'This is the content.'
});
resolve();
}, 1000);
});
}
},
mutations: {
setData(state, data) {
state.title = data.title;
state.content = data.content;
}
}
});
}
// app.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, store };
}
// server.js
const express = require('express');
const { createApp } = require('./app');
const renderer = require('vue-server-renderer').createRenderer();
const server = express();
server.get('*', (req, res) => {
const { app, router, store } = createApp();
router.push(req.url);
router.onReady(() => {
const matchedComponents = router.getMatchedComponents();
if (!matchedComponents.length) {
res.status(404).end('Not Found');
return;
}
Promise.all(matchedComponents.map(component => {
if (component.asyncData) {
return component.asyncData({ store, route: router.currentRoute });
}
})).then(() => {
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error');
return;
}
res.end(`
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>${html}</body>
</html>
`);
});
}).catch(err => {
res.status(500).end('Internal Server Error');
});
}, err => {
if (err) {
res.status(500).end('Internal Server Error');
return;
}
});
});
server.listen(8080);
在客户端,我们需要重新激活Vue组件,使其具有交互能力。这个过程称为“客户端激活”。
// entry-client.js
import { createApp } from './app';
const { app, router, store } = createApp();
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__);
}
router.onReady(() => {
app.$mount('#app');
});
// server.js
const express = require('express');
const { createApp } = require('./app');
const renderer = require('vue-server-renderer').createRenderer();
const server = express();
server.get('*', (req, res) => {
const { app, router, store } = createApp();
router.push(req.url);
router.onReady(() => {
const matchedComponents = router.getMatchedComponents();
if (!matchedComponents.length) {
res.status(404).end('Not Found');
return;
}
Promise.all(matchedComponents.map(component => {
if (component.asyncData) {
return component.asyncData({ store, route: router.currentRoute });
}
})).then(() => {
const context = { state: store.state };
renderer.renderToString(app, context, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error');
return;
}
res.end(`
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>
<div id="app">${html}</div>
<script>window.__INITIAL_STATE__ = ${JSON.stringify(context.state)}</script>
<script src="/dist/client.bundle.js"></script>
</body>
</html>
`);
});
}).catch(err => {
res.status(500).end('Internal Server Error');
});
}, err => {
if (err) {
res.status(500).end('Internal Server Error');
return;
}
});
});
server.listen(8080);
为了提高SSR的性能,我们可以使用缓存机制。Vue SSR 提供了createBundleRenderer
方法,可以将渲染结果缓存起来。
// server.js
const express = require('express');
const { createApp } = require('./app');
const renderer = require('vue-server-renderer').createBundleRenderer(require('./dist/vue-ssr-server-bundle.json'), {
cache: require('lru-cache')({
max: 1000,
maxAge: 1000 * 60 * 15
})
});
const server = express();
server.get('*', (req, res) => {
const context = { url: req.url };
renderer.renderToString(context, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error');
return;
}
res.end(`
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>${html}</body>
</html>
`);
});
});
server.listen(8080);
为了减少客户端加载的JavaScript文件大小,我们可以使用代码分割技术。Vue SSR 支持使用import()
动态导入组件。
// router.js
import Vue from 'vue';
import Router from 'vue-router';
Vue.use(Router);
export function createRouter() {
return new Router({
mode: 'history',
routes: [
{ path: '/', component: () => import('./components/Home.vue') },
{ path: '/about', component: () => import('./components/About.vue') }
]
});
}
为了提高SSR的性能,我们可以使用流式渲染。Vue SSR 提供了renderToStream
方法,可以将HTML以流的形式发送到客户端。
// server.js
const express = require('express');
const { createApp } = require('./app');
const renderer = require('vue-server-renderer').createRenderer();
const server = express();
server.get('*', (req, res) => {
const { app, router } = createApp();
router.push(req.url);
router.onReady(() => {
const stream = renderer.renderToStream(app);
res.write(`
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>
`);
stream.on('data', chunk => {
res.write(chunk);
});
stream.on('end', () => {
res.end(`
</body>
</html>
`);
});
stream.on('error', err => {
res.status(500).end('Internal Server Error');
});
}, err => {
if (err) {
res.status(500).end('Internal Server Error');
return;
}
});
});
server.listen(8080);
在SSR中,由于每次请求都会创建一个新的Vue实例,如果不及时清理这些实例,可能会导致内存泄漏。为了解决这个问题,我们可以使用destroy
方法手动销毁Vue实例。
”`javascript // server.js const express = require(‘express’); const { createApp } = require(‘./app’); const renderer = require(‘vue-server-renderer’).createRenderer();
const server = express();
server.get(‘*’, (req, res) => { const { app, router } = createApp();
router.push(req.url);
router.onReady(() => { renderer.renderToString(app, (err, html) => { if (err) { res.status(500).end(‘Internal Server Error’); return; }
res.end(`
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>${html}</body>
</html>
`);
app.$destroy();
});
}, err => { if (err) { res.status(500).end(‘Internal Server Error’); return; } }); });
server
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。