笔记来源:拉勾教育 大前端高薪训练营
阅读建议:内容较多,建议通过左侧导航栏进行阅读
官方文档的解释:
Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。
服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行。
在对你的应用程序使用服务器端渲染 (SSR) 之前,你应该问的第一个问题是,是否真的需要它。
技术层面:
业务层面:
基于 Vue SSR 官方文档提供的解决方案
官方方案具有更直接的控制应用程序的结构,更深入底层,更加灵活,同时在使用官方方案的过程中, 也会对Vue SSR有更加深入的了解。
该方式需要你熟悉 Vue.js 本身,并且具有 Node.js 和 webpack 的相当不错的应用经验。
Nuxt.js 开发框架
NUXT提供了平滑的开箱即用的体验,它建立在同等的Vue技术栈之上,但抽象出很多模板,并提供了 一些额外的功能,例如静态站点生成。通过 Nuxt.js 可以快速的使用 Vue SSR 构建同构应用。
接下来我们以 Vue SSR 的官方文档为参考,来学习一下它的基本用法。
目标:
- 了解如何使用 VueSSR 将一个 Vue 实例渲染为 HTML 字符串
首先,我们来学习一下服务端渲染中最基础的工作:模板渲染。 说白了就是如何在服务端使用 Vue 的方式解析替换字符串。
在它的官方文档中其实已经给出了示例代码,下面我们来把这个案例的实现过程以及其中含义演示一 下。
准备工作
# 创建文件夹
mkdir vue-ssr
# 进入创建的文件夹内
cd vue-ssr
# 初始化包管理文件 package.json
npm init -y # -y 表示快速初始,不经过一些问题的回答,直接走默认回答
# 安装 vue 及 vue-server-renderer 依赖包
npm i vue vue-serve-renderer
创建 server.js 文件
,将 Vue 实例渲染成 HTML 字符串
// 第 1 步:创建一个 Vue 实例
const Vue = require('vue')
const app = new Vue({
template: `
{
{ message }}
`,
data: {
message: '拉勾教育'
}
})
// 第 2 步:创建一个 renderer 渲染器
const renderer = require('vue-server-renderer').createRenderer()
/**
* @param {Vue} app
* @param {Function}
*/
// 第 3 步:将 Vue 实例渲染为 HTML
renderer.renderToString(app, (err, html) => {
if (err) throw err
// html 就是渲染出来的结果字符串
console.log(html);
// 拉勾教育
})
// 在 2.5.0+,如果没有传入回调函数,则会返回 Promise:
renderer
.renderToString(app)
.then((html) => {
console.log(html);
})
.catch((err) => {
console.error(err);
});
使用 node 运行 server.js
node server.js
执行效果,如图所示:
在 Node.js 服务器中使用时相当简单直接,例如 Express
。
首先,安装 Express
到项目中:
npm i express
然后,使用 Express 创建一个基本的 Web 服务:
const express = require("express");
const app = express();
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.listen(3000, () => console.log("app listening at http://localhost:port"));
启动 Web 服务,nodemon
启动服务,可以实时监听,热更新:
nodemon server.js
在 Web 服务中渲染 Vue 实例:
const Vue = require('vue')
const express = require('express')
// 创建一个渲染器
const renderer = require('vue-server-renderer').createRenderer()
// 创建一个 experss 的 server 实例
const server = express()
// 添加路由
server.get('/', (req, res) => {
const app = new Vue({
template: `
{
{ message }}
`,
data: {
message: '拉勾教育'
}
})
renderer.renderToString(app, (err, html) => {
if (err) {
return res.status(500).end('Internal Server Error')
}
// html 就是渲染出来的结果字符串
res.end(html)
})
})
server.listen(3000, () => {
console.log('server running at port 3000');
})
浏览器访问结果,如图所示:
请求响应数据,如图所示:
解决上面出现的乱码问题,添加 html响应头
,或 使用 meta
设置编码格式
......
server.get('/', (req, res) => {
......
renderer.renderToString(app, (err, html) => {
if (err) {
return res.status(500).end('Internal Server Error')
}
// html 就是渲染出来的结果字符串
// 添加响应头,解决编码问题
res.setHeader('Content-Type', 'text/html; charset=utf8')
res.end(`
Document
${
html}
`)
})
})
......
建议:
将上述的两种解决编码的方案,都保留
使用一个页面模板
创建一个页面模板 index.template.html
,优化上述代码
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
body>
html>
注意:
- 上面的
注释语句,是固定语法。会作为 Vue实例 转换的 html 字符串存放的位置,类似于占位。
vue-ssr-outlet
前后不可以有空格。
在 server.js
中,创建 renderer
渲染器时,添加一个参数,指定模板
const Vue = require('vue')
const express = require('express')
const fs = require('fs') // 读取文件
// 创建一个渲染器
const renderer = require('vue-server-renderer').createRenderer({
// 设置模板
template: fs.readFileSync('./index.template.html', 'utf-8')
})
// 创建一个 experss 的 server 实例
const server = express()
// 添加路由
server.get('/', (req, res) => {
......
renderer.renderToString(app, (err, html) => {
......
// 结合了模板的完整内容
res.end(html)
})
})
server.listen(3000, () => {
console.log('server running at port 3000');
})
访问浏览器,查看响应数据,如图所示:
在模板中使用外部数据
server.js
,在使用 renderer
渲染器进行渲染时,传入第二个参数(可选的),配置传入到模板中的数据
renderer.renderToString(app, {
// 配置传到模板中的数据
title: '拉勾教育',
meta: ``
}, (err, html) => {
......
})
index.template.html
,使用 vue 中的模板引擎的语法,进行渲染
{
{
{ meta }}}
<title>{
{ title }}title>
{ { { }}}
表示原文输出
我们需要使用 webpack
来打包我们的 Vue 应用程序。事实上,我们可能需要在服务器上使用 webpack
打包 Vue 应用程序,因为:
webpack
和 vue-loader
构建,并且许多 webpack
特定功能 不能直接在 Node.js
中运行(例如通过 file-loader
导入文件,通过 css-loader
导入 CSS)。Node.js
最新版本能够完全支持 ES2015
特性,我们还是需要转译客户端代码以适应老版浏览器。这也会涉及到构建步骤。所以基本看法是,对于客户端应用程序和服务器应用程序,我们都要使用 webpack
打包 - 服务器需要 「服务器 bundle」然后用于服务器端渲染(SSR),而「客户端 bundle」会发送给浏览器,用于混合静态标记。
现在我们正在使用 webpack
来处理服务器和客户端的应用程序,大部分源码可以使用通用方式编写,可以使用 webpack
支持的所有功能。同时,在编写通用代码时,有一些事项要牢记在心。
一个基本项目可能像是这样:
src
├── components
│ ├── Foo.vue
│ ├── Bar.vue
│ └── Baz.vue
├── App.vue
├── app.js # 通用 entry(universal entry)
├── entry-client.js # 仅运行于浏览器
└── entry-server.js # 仅运行于服务器
src/App.vue
<template>
<div id="app">
<h1>{
{ message }}h1>
<h2>客户端动态交互h2>
<div>
<input v-model="message" />
div>
<div>
<button @click="onClick">点击测试button>
div>
div>
template>
<script>
export default {
name: "App",
data () {
return {
message: "拉勾教育",
}
},
methods: {
onClick() {
console.log("Hello World");
},
},
};
script>
app.js
是我们应用程序的「通用 entry」。在纯客户端应用程序中,我们将在此文件中创建根 Vue 实例,并直接挂载到 DOM。但是,对于服务器端渲染(SSR),责任转移到纯客户端 entry 文件。app.js
简单地使用 export 导出一个 createApp
函数:
src/app.js
import Vue from 'vue'
import App from './App.vue'
// 导出一个工厂函数,用于创建新的
// 应用程序、router 和 store 实例
export function createApp() {
const app = new Vue({
// 根实例简单的渲染应用程序组件。
render: h => h(App)
})
// 后期还有导出 router 、store实例
return {
app }
}
避免状态单例
当编写纯客户端 (client-only) 代码时,我们习惯于每次在新的上下文中对代码进行取值。但是,Node.js 服务器是一个长期运行的进程。当我们的代码进入该进程时,它将进行一次取值并留存在内存中。这意味着如果创建一个单例对象,它将在每个传入的请求之间共享。
如基本示例所示,我们为每个请求创建一个新的根 Vue 实例。这与每个用户在自己的浏览器中使用新应用程序的实例类似。如果我们在多个请求之间使用一个共享的实例,很容易导致交叉请求状态污染 (cross-request state pollution)。
因此,我们不应该直接创建一个应用程序实例,而是应该暴露一个可以重复执行的工厂函数,为每个请求创建新的应用程序、router 和 store实例。
客户端 entry
只需创建应用程序,并且将其挂载到 DOM 中:
src/entry-client.js
/**
* 客户端入口
*/
import {
createApp } from './app'
// 客户端特定引导逻辑……
const {
app } = createApp()
// 这里假定 App.vue 模板中根元素具有 `id="app"`
app.$mount('#app')
服务器 entry 使用 default export 导出函数,并在每次渲染中重复调用此函数。此时,除了创建和返回 应用程序实例之外,它不会做太多事情 - 但是稍后我们将在此执行服务器端路由匹配 (server-side route matching) 和数据预取逻辑 (data pre-fetching logic)。
src/entry-server.js
/**
* 服务端启动入口
*/
import {
createApp } from './app'
export default context => {
const {
app } = createApp()
// 服务端路由处理、数据预取...
return app
}
1,安装生产依赖
npm i vue vue-server-renderer express cross-env
说明
包 说明 vue Vue.js 核心库 vue-server-renderer Vue 服务端渲染工具 express 基于 Node 的 Web 服务框架 cross-env 通过 npm scripts 设置跨平台环境变量,区分不同的打包环境
2,安装开发依赖
npm i -D webpack webpack-cli webpack-merge webpack-node-externals @babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader css-loader url-loader file-loader rimraf vue-loader vue-template-compiler friendly-errors-webpack-plugin
说明
包 说明 webpack webpack 核心包 webpack-cli webpack 的命令行工具 webpack-merge webpack 配置信息合并工具 webpack-node-externals 排除 webpack 中的 Node 模块 rimraf 基于 Node 封装的一个跨平台 rm -rf
工具friendly-errors- webpack-plugin 友好的 webpack
错误提示@babel/core
@babel/plugin-transform-runtime
@babel/preset-env
babel-loader
Babel 相关工具vue-loader
vue-template-compiler处理 .vue 资源 file-loader 处理字体资源 css-loader 处理 CSS 资源 url-loader 处理图片资源
初始化 webpack
打包配置文件
build
├── webpack.base.config.js # 公共配置
├── webpack.client.config.js # 客户端打包配置文件
└── webpack.server.config.js # 服务端打包配置文件
build/webpack.base.config.js
/**
* 公共配置
*/
// 处理 .vue 资源的插件
const VueLoaderPlugin = require('vue-loader/lib/plugin')
// 处理路径
const path = require('path')
// 提供 webpack 打包过程中,友好的日志输出
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
// 将路径进行组合,从而得到绝对安全的路径
const resolve = file => path.resolve(__dirname, file)
const isProd = process.env.NODE_ENV === 'production'
module.exports = {
// 打包模式
mode: isProd ? 'production' : 'development',
// 打包结果输出
output: {
path: resolve('../dist/'),
publicPath: '/dist/', // 打包结果的请求路径
filename: '[name].[chunkhash].js'
},
resolve: {
alias: {
// 路径别名,@ 指向 src
'@': resolve('../src/')
},
// 可以省略的扩展名
// 当省略扩展名的时候,按照从前往后的顺序依次解析
extensions: ['.js', '.vue', '.json']
},
// 定位到原始代码的位置,方便调试
devtool: isProd ? 'source-map' : 'cheap-module-eval-source-map',
module: {
rules: [
// 处理图片资源
{
test: /\.(png|jpg|gif)$/i, use: [
{
loader: 'url-loader', options: {
limit: 8192,
},
}
]
},
// 处理字体资源
{
test: /\.(woff|woff2|eot|ttf|otf)$/, use: [
'file-loader',
],
},
// 处理 .vue 资源
{
test: /\.vue$/,
loader: 'vue-loader'
},
// 处理 CSS 资源
// 它会应用到普通的 `.css` 文件
// 以及 `.vue` 文件中的 `