服务端渲染就是由服务端返回页面给客户端,老牌技术有
jsp
、aps
、php
等技术,而像vue
、react
等技术是属于客户端渲染。客户端渲染的优势就是更少的带宽,更快的响应速度(因为请求和响应的内容变少)。
问题就是
seo
不友好,Time-To-Content
更长(因为要等所有的资源加载完毕才会开始渲染页面)。
SSR(Server Side Render)
正是为了解决这些问题而出现的技术。本质上,SSR 是一种在服务端将组件渲染 HTML 字符串并发送到浏览器,最后在浏览器上将这些 HTML 片段“激活”为客户端上可交互的应用技术。
搭建vue
环境
本次项目为新搭建的环境,所以不使用之前的配置文件,这里默认是空的目录。
这边建议新建一个空目录,然后
npm init -y
生成一个package.json
就可以跟着我开始了。
- 根目录下新建
src
目录 src
目录下新建App.vue
文件,内容如下
<template> <div :class="['main', cls]"> <h3>{{ message }}</h3> <button @click="handleClick">Toggle</button> </div> </template> <script> import { ref, computed } from "@vue/reactivity"; export default { setup() { const isActivity = ref(false); const cls = computed(() => (isActivity.value ? "activate" : "deactivate")); const handleClick = () => { isActivity.value = !isActivity.value; }; return { isActivity, message: "Hello World", cls, handleClick, }; }, }; </script> <style> h3 { color: #42b983; } .main { position: fixed; top: 0; left: 0; bottom: 0; right: 0; padding: 20px 12px; transition: background 0.3s linear; } .activate { background: #000; } .deactivate { background: #fff; } </style>
需要为客户端、服务端环境分别准备项目
Entry
文件,所以需要创建客户端的entry-client.js
和服务端的entry-server.js
src
目录下新建entry-client.js
,内容如下
import { createSSRApp } from "vue"; import App from "./App.vue"; createSSRApp(App).mount("#app");
src
目录下新建entry-server.js
,内容如下
import { createSSRApp } from "vue"; import App from "./App.vue"; export default () => { return createSSRApp(App); };
搭建ssr
环境
ssr
需要有两份资源文件,一份client
客户端,一份server
服务端。
- 根目录下新建
webpack.base.js
文件,主要是复用配置
const path = require('path'); const { VueLoaderPlugin } = require("vue-loader"); module.exports = { output: { filename: '[name].js', path: path.resolve(__dirname, 'dist') }, module: { rules: [ { test: /.js$/, use: { loader: 'babel-loader', } }, { test: /.vue$/, use: { loader: 'vue-loader' } } ] }, resolve: { extensions: ['.ts', '.js'], }, plugins: [ new VueLoaderPlugin() ] }
- 根目录下新建
webpack.client.js
文件,主要是生成客户端资源的配置
const Merge = require("webpack-merge"); const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const {WebpackManifestPlugin} = require("webpack-manifest-plugin"); const base = require("./webpack.base"); // 继承自 `webpack.base.js` module.exports = Merge.merge(base, { mode: "development", entry: { // 入口指向 `entry-client.js` 文件 client: path.join(__dirname, "./src/entry-client.js"), }, output: { publicPath: "/", }, module: { rules: [{test: /.css$/, use: ["style-loader", "css-loader"]}], }, plugins: [ // 这里使用 webpack-manifest-plugin 记录产物分布情况 // 方面后续在 `server.js` 中使用 new WebpackManifestPlugin({fileName: "manifest-client.json"}), // 自动生成 HTML 文件内容 new HtmlWebpackPlugin({ templateContent: ` <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Webpack App</title> </head> <body> <div id="app" /> </body> </html> `, }), ], });
- 根目录下新建
webpack.server.js
文件,主要是生成服务端资源的配置
const Merge = require("webpack-merge"); const path = require("path"); const {WebpackManifestPlugin} = require("webpack-manifest-plugin"); const base = require("./webpack.base"); module.exports = Merge.merge(base, { mode: "production", entry: { server: path.join(__dirname, "src/entry-server.js"), }, target: "node", output: { // 打包后的结果会在 node 环境使用 // 因此此处将模块化语句转译为 commonjs 形式 libraryTarget: "commonjs2", }, module: { rules: [ { test: /.css$/, use: [ // 注意,这里用 `vue-style-loader` 而不是 `style-loader` // 因为 `vue-style-loader` 对 SSR 模式更友好 "vue-style-loader", { loader: "css-loader", options: { esModule: false, }, }, ], }, ], }, plugins: [ // 这里使用 webpack-manifest-plugin 记录产物分布情况 // 方面后续在 `server.js` 中使用 new WebpackManifestPlugin({fileName: "manifest-server.json"}), ], });
为了方便构建,所以我这里还是将生成资源文件的命令封装到了
package.json
文件中,如下
{ "scripts": { "build:client": "npx webpack --config webpack.client.js", "build:server": "npx webpack --config webpack.server.js" } }
完成上述操作后,执行npm run build:client
和npm run build:server
分别生成客服端和服务端的资源文件。
使用node
搭建发布环境
- 根目录下新建
server.js
,内容如下
const express = require("express"); const path = require("path"); const { renderToString } = require("@vue/server-renderer"); // 通过 manifest 文件,找到正确的产物路径 const clientManifest = require("./dist/manifest-client.json"); const serverManifest = require("./dist/manifest-server.json"); const serverBundle = path.join( __dirname, "./dist", serverManifest["server.js"] ); // 这里就对标到 `entry-server.js` 导出的工厂函数 const createApp = require(serverBundle).default; const server = express(); server.get("/", async (req, res) => { const app = createApp(); const html = await renderToString(app); const clientBundle = clientManifest["client.js"]; res.send(` <!DOCTYPE html> <html> <head> <title>Vue SSR Example</title> </head> <body> <!-- 注入组件运行结果 --> <div id="app">${html}</div> <!-- 注入客户端代码产物路径 --> <!-- 实现 Hydrate 效果 --> <script src="${clientBundle}"></script> </body> </html> `); }); server.use(express.static("./dist")); server.listen(3000, () => { console.log("服务启动成功:http://localhost:3000"); );
- 命令行:
node server.js
运行查看效果吧
由于这里涉及到的依赖比较多,所以这一节就没有讲安装依赖的代码了,所以这里直接附上
package.json
文件
{ "name": "vue-ssr", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "build:client": "npx webpack --config webpack.client.js", "build:server": "npx webpack --config webpack.server.js", "test": "echo "Error: no test specified" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@babel/core": "^7.18.9", "@vue/server-renderer": "^3.2.37", "babel-loader": "^8.2.5", "css-loader": "^6.7.1", "express": "^4.18.1", "html-webpack-plugin": "^5.5.0", "style-loader": "^3.3.1", "vue": "^3.2.37", "vue-loader": "^17.0.0", "vue-style-loader": "^4.1.3", "webpack": "^5.74.0", "webpack-cli": "^4.10.0", "webpack-manifest-plugin": "^5.0.0", "webpack-merge": "^5.8.0" } }
总结
本文介绍如何使用 Webpack 开发 Vue 应用,从最基础的 Vue SFC 文件编译;到复用更多基础编译工具;再到 SSR、SSG 等高阶用法;以及如何借助 Vue CLI 迅速搭建开发环境。
虽然 Vue 官方团队已经推出了一套更轻、更快的编译工具:Vite,但相关生态还不太成熟,短期内使用 Webpack 依然会是更保险可控的方案,因此依然值得投入时间精力深入学习 Webpack。
我这里把
vue-cli
的部分给省略了,我是学习webpack
的,所以只记录和webpakc
相关的,其他的当做拓展学习,这里提一下Nuxt.js
、Quasar
,感兴趣的可以自己去找资源学习,瑞思拜。示例代码:vue-ssr