Em comparação com o SPA tradicional, a renderização do lado do servidor (SSR) é mais amigável para SEO, é conveniente para os rastreadores do mecanismo de pesquisa rastrear e você pode visualizar diretamente a página totalmente renderizada. Além disso, o SSR pode renderizar o conteúdo da página em um tempo mais curto tempo, deixe os usuários terem uma melhor experiência do usuário.
prefácio
Este artigo apresentará a renderização do lado do servidor dos três módulos a seguir:
- O que é renderização de cliente?
- O que é renderização do lado do servidor?
- Como implementar a renderização do lado do servidor? Espero que seja útil para você depois de lê-lo!
renderização do cliente
1. Conceito
Renderização do lado do cliente (CSR), ou seja, o modo tradicional de aplicativo de página única (SPA). O aplicativo criado pelo Vue é uma página de modelo HTML por padrão. Há apenas um contêiner raiz cujo id é o aplicativo e, em seguida, empacotado com webpack para gerar css, js, etc. Os arquivos de recursos são carregados e analisados pelo navegador para renderizar HTML.
Clique com o botão direito do mouse para visualizar o código-fonte de uma página da Web padrão do projeto Vue. Pode-se ver que o código-fonte não possui conteúdo relevante realmente renderizado na página. Há apenas um elemento raiz cujo id é o aplicativo, portanto, o conteúdo exibido na página da Web é renderizado dinamicamente por meio de JavaScript. Essa maneira de renderizar páginas da Web por meio do JavaScript do lado do navegador é chamada de renderização do lado do cliente .
2. Vantagens e desvantagens
2.1. Vantagens:
Separação das extremidades dianteiras e traseiras; melhor experiência;
2.2 Desvantagens:
A renderização da primeira tela é lenta; o SEO não é amigável;
renderização do lado do servidor
Server Side Render (Server Side Render) é renderizar um componente Vue no lado do servidor como uma string HTML e enviá-la para o navegador e, finalmente, o processo de "ativar" essas tags estáticas em um aplicativo interativo é chamado de renderização do lado do servidor.
Resumindo, quando o navegador solicita a URL da página, o servidor monta o texto HTML que precisamos e o devolve ao navegador. Depois que o texto HTML é analisado pelo navegador, ele pode construir diretamente a árvore DOM desejada e exibi-la no a página. O processo de montagem do HTML no lado do servidor é chamado de renderização do lado do servidor.
concluir
1. Pequeno teste
Vamos simplesmente implementar a renderização do lado do servidor primeiro e gerar de forma assíncrona o código HTML de que precisamos por meio do renderizador de servidor que vem com o Vue3.
// nodejs服务器 express koa
const express = require('express');
const {
createSSRApp } = require('vue');
const {
renderToString } = require('@vue/server-renderer');
// 创建express实例
let app = express();
// 通过渲染器渲染page可以得到html内容
const page = createSSRApp({
data: () => {
return {
title: 'ssr',
count: 0,
}
},
template: `<div><h1>{
{title}}</h1>hello world!</div>`,
});
app.get('/', async function (req, res) {
try {
// 异步生成html
const html = await renderToString(page);
res.send(html);
} catch (error) {
res.status(500).send('系统错误');
}
});
app.listen(9001, () => {
console.log('9001 success');
});
Em seguida, inicie-o por meio do comando node ou inicie-o por meio do nodemon (você pode consultar o Baidu sobre o uso do nodemon).
node .\server\index.js
// or
nodemon .\server\index.js
início do nó
Em seguida, abra http://localhost:9001/ para ver:
Depois de clicar com o botão direito do mouse para visualizar o código-fonte da página da Web:
Pode ser visto no código-fonte da página da Web que, quando o navegador obtém diretamente o código HTML do servidor, ele pode exibir **hello world!** na página sem executar o código JavaScript. Este é todo o processo de implementação de SSR.
Você pode usar o vue-cli para criar um novo projeto vue, exibir olá mundo na página e comparar as diferenças visualizando o código-fonte da página da web!
2. Projetos isomórficos
O SSR foi demonstrado através de um caso simples, então como aplicá-lo em nosso projeto? Um conceito precisa ser introduzido aqui: isomorfismo. O chamado isomorfismo significa que um pedaço de código pode ser executado tanto no servidor quanto no cliente, e o efeito da execução é o mesmo, é completar a montagem desse HTML e exibir a página corretamente. Em outras palavras, um trecho de código pode ser renderizado tanto no lado do cliente quanto no lado do servidor.
2.1. Arquivos de configuração do servidor e do cliente
Crie um novo diretório webpack no diretório raiz. Esta pasta armazena principalmente arquivos de configuração de empacotamento.
Crie um novo arquivo de configuração do servidor: server.config.js
const base = require('./base.config.js');
const path = require('path');
// webpack插件
const {
default: merge } = require('webpack-merge');
const nodeExternals = require("webpack-node-externals");
const {
WebpackManifestPlugin } = require('webpack-manifest-plugin');
module.exports = merge(base, {
mode: "production",
// 将 entry 指向应用程序的 server 文件
entry: {
'server': path.resolve(__dirname, '../entry/server.entry.js')
},
// 对 bundle renderer 提供 source map 支持
devtool: 'source-map',
// 外置化应用程序依赖模块。可以使服务器构建速度更快,并生成较小的 bundle 文件。
externals: nodeExternals({
allowlist: [/\.css$/],
}),
output: {
path: path.resolve(__dirname, './../dist/server'),
filename: '[name].server.bundle.js',
library: {
type: 'commonjs2' // 构建目标加载模式 commonjs
}
},
// 这允许 webpack 以 Node 适用方式处理动态导入
// 并且还会在编译 Vue 组件时告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
target: 'node',
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: [["@babel/plugin-transform-runtime", {
"corejs": 3
}]]
},
},
exclude: /node_modules/
}
]
},
// 这是将服务器的整个输出构建为单个 JSON 文件的插件。
// 服务端默认文件名为 `ssr-manifest.json`
plugins: [
new WebpackManifestPlugin({
fileName: 'ssr-manifest.json' }),
],
})
Crie um novo arquivo de configuração do cliente: client.config.js
const base = require('./base.config.js');
const path = require('path');
// webpack插件
const {
default: merge } = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyPlugin = require("copy-webpack-plugin");
module.exports = merge(base, {
mode: "production",
// 对 bundle renderer 提供 source map 支持
devtool: 'source-map',
// 将 entry 指向应用程序的 client 文件
entry: {
'client': path.resolve(__dirname, '../entry/client.entry.js')
},
output: {
path: path.resolve(__dirname, './../dist/client'),
clean: true,
filename: '[name].client.bundle.js',
},
plugins: [
// 通过 html-webpack-plugin 生成client index.html
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.resolve('public/index.html')
}),
// 图标
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, "../public/favicon.ico"), to: path.resolve(__dirname, './../dist/client') },
],
}),
]
})
Finalmente, crie um novo arquivo de configuração base: base.config.js
const path = require('path');
const {
VueLoaderPlugin } = require('vue-loader');
module.exports = {
// 输出
output: {
path: path.resolve(__dirname, './../dist'),
filename: '[name].bundle.js',
},
// loader
module: {
rules: [
{
test: /\.vue$/, use: 'vue-loader' },
{
test: /\.css$/, use: [
'vue-style-loader',
'css-loader'
]
},
{
test: /\.s[ac]ss$/i,
use: [
"vue-style-loader",
"css-loader",
"sass-loader",
],
},
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
},
},
exclude: /node_modules/
}
],
},
plugins: [
new VueLoaderPlugin(),
]
}
Neste ponto, o arquivo de configuração é concluído e, finalmente, o comando de empacotamento (package.json) é postado
{
"scripts": {
"build:client": "webpack --config ./webpack/client.config.js",
"build:server": "webpack --config ./webpack/server.config.js",
"build": "npm run build:client && npm run build:server"
},
}
2.2. Arquivo de entrada
Crie um diretório de entrada no diretório raiz, esta pasta armazena principalmente arquivos de entrada.
Crie um novo arquivo de entrada geral: app.js
// 通用入口
// 创建VUE实例
import {
createSSRApp } from 'vue';
import App from './../src/App.vue';
export default function () {
return createSSRApp(App);
}
Crie um novo arquivo de entrada do servidor: server.entry.js
import createApp from './app';
// 服务器端路由与客户端使用不同的历史记录
import {
createMemoryHistory } from 'vue-router'
import createRouter from './router.js'
import createStore from './store';
import {
renderToString } from '@vue/server-renderer'
export default context => {
return new Promise(async (resolve, reject) => {
const app = createApp();
const router = createRouter(createMemoryHistory())
const store = createStore();
app.use(router);
app.use(store);
// 设置服务器端 router 的位置
await router.push(context.url);
// isReady 等到 router 将可能的异步组件和钩子函数解析完
await router.isReady();
// 匹配路由是否存在
const matchedComponents = router.currentRoute.value.matched.flatMap(record => Object.values(record.components))
// 不存在路由,返回 404
if (!matchedComponents.length) {
return reject({
code: 404 });
}
// 对所有匹配的路由组件调用 `asyncData()`
Promise.all(matchedComponents.map(component => {
if (component.asyncData) {
return component.asyncData({
store,
route: router.currentRoute.value
});
}
})).then(async (res) => {
let html = await renderToString(app);
html += `<script>window.__INITIAL_STATE__ = ${
replaceHtmlTag(JSON.stringify(store.state))}</script>`
resolve(html);
}).catch(() => {
reject(html)
})
})
}
/**
* 替换标签
* @param {*} html
* @returns
*/
function replaceHtmlTag(html) {
return html.replace(/<script(.*?)>/gi, '<script$1>').replace(/<\/script>/g, '</script>')
}
Novo arquivo de entrada do cliente: client.entry.js
// 挂载、激活app
import createApp from './app';
import {
createWebHistory } from 'vue-router'
import createRouter from './router.js'
import createStore from './store';
const router = createRouter(createWebHistory())
const app = createApp();
app.use(router);
const store = createStore();
// 判断window.__INITIAL_STATE__是否存在,存在的替换store的值
if (window.__INITIAL_STATE__) {
// 激活状态数据
store.replaceState(window.__INITIAL_STATE__);
}
app.use(store)
// 在客户端和服务端我们都需要等待路由器先解析异步路由组件以合理地调用组件内的钩子。因此使用 router.isReady 方法
router.isReady().then(() => {
app.mount('#app')
})
Após a conclusão dos arquivos de entrada do servidor e do cliente, o roteador e o armazenamento precisam ser compartilhados.
novo roteador.js
import {
createRouter } from 'vue-router'
const routes = [
{
path: '/', component: () => import('../src/views/index.vue') },
{
path: '/about', component: () => import('../src/views/about.vue') },
]
// 导出路由仓库
export default function (history) {
// 工厂
return createRouter({
history,
routes
})
}
Dois componentes vue também são necessários, ou seja, index.vue e about.vue
<script>
// 声明额外的选项
export default {
// 对外暴露方法,执行store
asyncData: ({
store, route }) => {
// 触发 action 后,会返回 Promise
return store.dispatch('asyncSetData', route.query?.id || route.params?.id);
},
}
</script>
<script setup>
import {
computed } from 'vue';
import {
useStore } from 'vuex';
import {
generateRandomInteger} from '../utils'
const clickMe = () => {
store.dispatch('asyncSetData', generateRandomInteger(1,4));
}
const store = useStore();
// 得到后赋值
const storeData = computed(() => store.state.data);
</script>
<template>
<div>
<div>index</div>
<button @click="clickMe">点击</button>
<div style="margin-top:20px">store
<div>id: {
{ storeData?.id }}</div>
<div>title: {
{ storeData?.title }}</div>
</div>
</div>
</template>
<style lang='scss' scoped>
</style>
<script setup>
import {
ref } from 'vue'
</script>
<template>
<div>about</div>
</template>
<style lang='scss' scoped>
</style>
Para a conveniência do teste, modificamos App.vue para router-view
<template>
<div id="nav">
<router-link to="/?id=1">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view />
</template>
<style lang="scss">
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
a {
font-weight: bold;
color: #2c3e50;
&.router-link-exact-active {
color: #42b983;
}
}
}
</style>
Nova loja.js
// 实例化store
import {
createStore as _createStore } from 'vuex';
// 引入数据
import {
getData } from './data';
// 对外导出一个仓库
export default function createStore() {
return _createStore({
state: {
// 状态数据
data: {
}
},
mutations: {
// 同步数据
SET_DATA(state, item) {
state.data = item;
}
},
actions: {
// 异步数据
asyncSetData({
commit }, id) {
getData(id).then(item => {
commit('SET_DATA', item);
})
},
},
modules: {
}
});
}
Para facilitar o teste, criamos um novo arquivo data.js
export function getData(id) {
const bookList = [
{
id: 1, title: '西游记' },
{
id: 2, title: '红楼梦' },
{
id: 3, title: '水浒传' },
{
id: 4, title: '三国演义' },
];
const item = bookList.find(i => i.id == id);
return Promise.resolve(item);
}
Neste ponto, podemos construir o cliente e o servidor. O comando é npm run build . A compilação aqui será construída automaticamente duas vezes, ou seja, build:client e build:server. Quando o terminal for exibido com sucesso, significa que a compilação foi bem-sucedida.
2.3、server.js
A última é modificar o arquivo server/index.js e criar um novo index2.js aqui para diferenciá-lo
const express = require('express')
const {
renderToString } = require('@vue/server-renderer')
const app = express();
const path = require('path');
// 构建结果清单
const manifest = require('./../dist/server/ssr-manifest.json')
const appPath = path.join(__dirname, './../dist/server', manifest['server.js'])
const createApp = require(appPath).default;
const fs = require('fs');
// 搭建静态资源目录
// 这里index必须为false,有兴趣的话可以试试前后会有什么区别
app.use('/', express.static(path.join(__dirname, '../dist/client'), {
index: false }));
// 获取模板
const indexTemplate = fs.readFileSync(path.join(__dirname, './../dist/client/index.html'), 'utf-8');
// 匹配所有的路径,搭建服务
app.get('*', async (req, res) => {
try {
const appContent = await createApp(req);
const html = indexTemplate
.toString()
.replace('<div id="app">', `<div id="app">${
appContent}`)
res.setHeader('Content-Type', 'text/html');
res.send(html);
} catch (error) {
console.log('error', error)
res.status(500).send('服务器错误');
}
})
app.listen(9002, () => {
console.log('success 9002')
});
Crie um novo arquivo de modelo: 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">
<title>vue ssr</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
Em seguida, execute nodemon .\server\index2.js e visite http://localhost:9002/?id=1
Exibir o código-fonte da página da web
Resumir
Quando o navegador acessa o projeto de renderização do lado do servidor, o servidor passa a URL para o renderizador de aplicativo VUE pré-selecionado. Depois que o renderizador corresponde ao componente de roteamento correspondente, ele executa o método asyncData que pré-definimos no componente para obter dados. E passe os dados obtidos para o contexto do renderizador, use o modelo para montar o HTML e envie o HTML e o estado do estado para o navegador. Depois que o navegador carregar o aplicativo Vue construído, ele sincronizará os dados do estado para a frente -end store, e de acordo com a ativação de dados, o texto HTML retornado pelo navegador e analisado em elementos DOM pelo navegador completa a sincronização de status de dados, roteamento e componentes, tornando o tempo de tela branca menor, com uma melhor experiência de carregamento , e ao mesmo tempo mais Bom para SEO.