2.1 开发环境准备
2.1.1 Node.js环境配置
# 检查当前Node.js版本
node --version
npm --version
# 推荐版本
# Node.js: >= 16.14.0
# npm: >= 8.0.0
# 使用nvm管理Node.js版本(推荐)
# 安装nvm (Windows)
choco install nvm
# 安装nvm (macOS/Linux)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
# 重启终端后安装Node.js
nvm install 18.17.0
nvm use 18.17.0
nvm alias default 18.17.0
2.1.2 包管理器选择
# 方案一:使用npm(默认)
npm config set registry https://registry.npmmirror.com
npm install -g npm@latest
# 方案二:使用yarn(推荐)
npm install -g yarn
yarn config set registry https://registry.npmmirror.com
# 方案三:使用pnpm(性能最佳)
npm install -g pnpm
pnpm config set registry https://registry.npmmirror.com
# 验证安装
yarn --version
pnpm --version
2.1.3 开发工具配置
// .vscode/settings.json
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"vetur.validation.template": false,
"vetur.validation.script": false,
"vetur.validation.style": false,
"typescript.preferences.importModuleSpecifier": "relative",
"vue.codeActions.enabled": false
}
// .vscode/extensions.json
{
"recommendations": [
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"formulahendry.auto-rename-tag",
"christian-kohler.path-intellisense"
]
}
2.2 项目初始化方式
2.2.1 使用Vue CLI创建项目
# 安装Vue CLI
npm install -g @vue/cli
# 创建项目
vue create vue-ssr-demo
# 选择配置
? Please pick a preset: Manually select features
? Check the features needed for your project:
◉ Choose Vue version
◉ Babel
◉ TypeScript
◉ Router
◉ Vuex
◉ CSS Pre-processors
◉ Linter / Formatter
◉ Unit Testing
? Choose a version of Vue.js: 3.x
? Use class-style component syntax? No
? Use Babel alongside TypeScript? Yes
? Use history mode for router? Yes
? Pick a CSS pre-processor: Sass/SCSS (with dart-sass)
? Pick a linter / formatter config: ESLint + Prettier
? Pick additional lint features: Lint on save
? Pick a unit testing solution: Jest
? Where do you prefer placing config? In dedicated config files
2.2.2 使用Vite创建项目
# 使用Vite创建Vue项目
npm create vue@latest vue-ssr-vite
# 进入项目目录
cd vue-ssr-vite
# 安装依赖
npm install
# 启动开发服务器
npm run dev
2.2.3 手动创建项目结构
# 创建项目目录
mkdir vue-ssr-manual
cd vue-ssr-manual
# 初始化package.json
npm init -y
# 创建基本目录结构
mkdir src
mkdir src/components
mkdir src/views
mkdir src/router
mkdir src/store
mkdir build
mkdir public
mkdir server
2.3 项目结构设计
2.3.1 标准项目结构
vue-ssr-project/
├── build/ # 构建配置
│ ├── webpack.base.js # 基础webpack配置
│ ├── webpack.client.js # 客户端webpack配置
│ ├── webpack.server.js # 服务端webpack配置
│ └── setup-dev-server.js # 开发服务器配置
├── public/ # 静态资源
│ ├── index.html # HTML模板
│ └── favicon.ico # 网站图标
├── server/ # 服务端代码
│ ├── index.js # 服务器入口
│ └── render.js # 渲染逻辑
├── src/ # 源代码
│ ├── components/ # 组件
│ ├── views/ # 页面组件
│ ├── router/ # 路由配置
│ ├── store/ # 状态管理
│ ├── utils/ # 工具函数
│ ├── app.js # 应用入口
│ ├── entry-client.js # 客户端入口
│ ├── entry-server.js # 服务端入口
│ └── main.js # 主入口文件
├── dist/ # 构建输出
│ ├── client/ # 客户端构建文件
│ └── server/ # 服务端构建文件
├── .env # 环境变量
├── .gitignore # Git忽略文件
├── package.json # 项目配置
└── README.md # 项目说明
2.3.2 核心文件创建
package.json配置
{
"name": "vue-ssr-demo",
"version": "1.0.0",
"description": "Vue SSR Demo Project",
"scripts": {
"dev": "node server/index.js",
"build": "npm run build:client && npm run build:server",
"build:client": "webpack --config build/webpack.client.js",
"build:server": "webpack --config build/webpack.server.js",
"start": "cross-env NODE_ENV=production node server/index.js",
"lint": "eslint --ext .js,.vue src/",
"lint:fix": "eslint --ext .js,.vue src/ --fix"
},
"dependencies": {
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"vuex": "^4.1.0",
"express": "^4.18.2",
"vue-server-renderer": "^3.3.4",
"serialize-javascript": "^6.0.1"
},
"devDependencies": {
"@babel/core": "^7.22.9",
"@babel/preset-env": "^7.22.9",
"babel-loader": "^9.1.3",
"css-loader": "^6.8.1",
"vue-loader": "^17.2.2",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"webpack-dev-middleware": "^6.1.1",
"webpack-hot-middleware": "^2.25.4",
"webpack-merge": "^5.9.0",
"webpack-node-externals": "^3.0.0",
"cross-env": "^7.0.3",
"eslint": "^8.45.0",
"eslint-plugin-vue": "^9.15.1",
"prettier": "^3.0.0"
}
}
应用入口文件
// src/app.js
import { createSSRApp } from 'vue'
import { createRouter } from './router'
import { createStore } from './store'
import App from './App.vue'
// 导出工厂函数,避免状态污染
export function createApp() {
const app = createSSRApp(App)
const router = createRouter()
const store = createStore()
app.use(router)
app.use(store)
return { app, router, store }
}
客户端入口
// src/entry-client.js
import { createApp } from './app'
const { app, router, store } = createApp()
// 等待路由准备就绪
router.isReady().then(() => {
// 恢复服务端状态
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
// 挂载应用
app.mount('#app')
})
服务端入口
// src/entry-server.js
import { createApp } from './app'
export default async function(context) {
const { app, router, store } = createApp()
// 设置路由
await router.push(context.url)
await router.isReady()
// 预取数据
const matchedComponents = router.currentRoute.value.matched
.flatMap(record => Object.values(record.components || {}))
await Promise.all(
matchedComponents.map(component => {
if (component.asyncData) {
return component.asyncData({
store,
route: router.currentRoute.value
})
}
})
)
// 返回应用实例和状态
context.state = store.state
return app
}
2.4 Webpack配置
2.4.1 基础配置
// build/webpack.base.js
const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')
module.exports = {
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
output: {
path: path.resolve(__dirname, '../dist'),
publicPath: '/dist/',
filename: '[name].[contenthash].js'
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'@': path.resolve(__dirname, '../src')
}
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
use: ['vue-style-loader', 'css-loader']
},
{
test: /\.(png|jpg|gif|svg)$/,
type: 'asset/resource',
generator: {
filename: 'images/[name].[contenthash][ext]'
}
}
]
},
plugins: [
new VueLoaderPlugin()
]
}
2.4.2 客户端配置
// build/webpack.client.js
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
module.exports = merge(baseConfig, {
entry: './src/entry-client.js',
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial'
}
}
}
},
plugins: [
new VueSSRClientPlugin()
]
})
2.4.3 服务端配置
// build/webpack.server.js
const { merge } = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
module.exports = merge(baseConfig, {
entry: './src/entry-server.js',
target: 'node',
devtool: 'source-map',
output: {
libraryTarget: 'commonjs2'
},
externals: nodeExternals({
allowlist: /\.(css|vue)$/
}),
plugins: [
new VueSSRServerPlugin()
]
})
2.5 开发服务器配置
2.5.1 Express服务器
// server/index.js
const express = require('express')
const { createBundleRenderer } = require('vue-server-renderer')
const setupDevServer = require('../build/setup-dev-server')
const app = express()
const isProd = process.env.NODE_ENV === 'production'
let renderer
let readyPromise
if (isProd) {
// 生产环境
const template = require('fs').readFileSync(
require('path').resolve(__dirname, '../public/index.html'),
'utf-8'
)
const serverBundle = require('../dist/vue-ssr-server-bundle.json')
const clientManifest = require('../dist/vue-ssr-client-manifest.json')
renderer = createBundleRenderer(serverBundle, {
template,
clientManifest,
runInNewContext: false
})
} else {
// 开发环境
readyPromise = setupDevServer(app, (bundle, options) => {
renderer = createBundleRenderer(bundle, options)
})
}
// 静态资源服务
app.use('/dist', express.static('./dist'))
app.use('/public', express.static('./public'))
// 渲染函数
function render(req, res) {
const context = {
url: req.url,
title: 'Vue SSR Demo'
}
renderer.renderToString(context, (err, html) => {
if (err) {
console.error(err)
if (err.code === 404) {
res.status(404).end('Page not found')
} else {
res.status(500).end('Internal Server Error')
}
return
}
res.end(html)
})
}
// 路由处理
app.get('*', isProd ? render : (req, res) => {
readyPromise.then(() => render(req, res))
})
const port = process.env.PORT || 3000
app.listen(port, () => {
console.log(`Server started at http://localhost:${port}`)
})
2.5.2 开发服务器设置
// build/setup-dev-server.js
const fs = require('fs')
const path = require('path')
const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpackHotMiddleware = require('webpack-hot-middleware')
const clientConfig = require('./webpack.client')
const serverConfig = require('./webpack.server')
module.exports = function setupDevServer(app, callback) {
let bundle
let template
let clientManifest
let ready
const readyPromise = new Promise(resolve => {
ready = resolve
})
const update = () => {
if (bundle && clientManifest) {
ready()
callback(bundle, {
template,
clientManifest,
runInNewContext: false
})
}
}
// 读取模板
template = fs.readFileSync(
path.resolve(__dirname, '../public/index.html'),
'utf-8'
)
// 客户端webpack配置
clientConfig.entry = ['webpack-hot-middleware/client', clientConfig.entry]
clientConfig.plugins.push(
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
)
const clientCompiler = webpack(clientConfig)
const devMiddleware = webpackDevMiddleware(clientCompiler, {
publicPath: clientConfig.output.publicPath,
stats: 'minimal'
})
app.use(devMiddleware)
app.use(webpackHotMiddleware(clientCompiler, { heartbeat: 5000 }))
// 监听客户端manifest
clientCompiler.hooks.done.tap('dev-server', stats => {
const fs = devMiddleware.context.outputFileSystem
const readFile = file => fs.readFileSync(
path.join(clientConfig.output.path, file),
'utf-8'
)
clientManifest = JSON.parse(readFile('vue-ssr-client-manifest.json'))
update()
})
// 服务端webpack配置
const serverCompiler = webpack(serverConfig)
serverCompiler.watch({}, (err, stats) => {
if (err) throw err
if (stats.hasErrors()) {
console.error(stats.toString())
return
}
bundle = JSON.parse(
fs.readFileSync(
path.join(serverConfig.output.path, 'vue-ssr-server-bundle.json'),
'utf-8'
)
)
update()
})
return readyPromise
}
2.6 HTML模板配置
2.6.1 基础模板
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title }}</title>
{{{ renderResourceHints() }}}
{{{ renderStyles() }}}
</head>
<body>
<!--vue-ssr-outlet-->
{{{ renderState() }}}
{{{ renderScripts() }}}
</body>
</html>
2.6.2 模板变量说明
// 模板变量解释
const templateVariables = {
'{{ title }}': '页面标题,从context传入',
'{{{ renderResourceHints() }}}': '资源预加载提示',
'{{{ renderStyles() }}}': '内联CSS样式',
'<!--vue-ssr-outlet-->': 'Vue应用挂载点',
'{{{ renderState() }}}': '初始状态注入',
'{{{ renderScripts() }}}': '客户端JavaScript'
}
2.7 环境变量配置
2.7.1 环境变量文件
# .env
NODE_ENV=development
PORT=3000
API_BASE_URL=http://localhost:8080/api
REDIS_URL=redis://localhost:6379
SESSION_SECRET=your-session-secret
# .env.production
NODE_ENV=production
PORT=80
API_BASE_URL=https://api.example.com
REDIS_URL=redis://redis-server:6379
SESSION_SECRET=production-secret
2.7.2 环境变量使用
// src/config/index.js
const config = {
development: {
apiBaseUrl: process.env.API_BASE_URL || 'http://localhost:8080/api',
port: process.env.PORT || 3000,
redis: {
url: process.env.REDIS_URL || 'redis://localhost:6379'
}
},
production: {
apiBaseUrl: process.env.API_BASE_URL,
port: process.env.PORT || 80,
redis: {
url: process.env.REDIS_URL
}
}
}
export default config[process.env.NODE_ENV || 'development']
2.8 代码规范配置
2.8.1 ESLint配置
// .eslintrc.js
module.exports = {
root: true,
env: {
node: true,
browser: true,
es2021: true
},
extends: [
'eslint:recommended',
'@vue/eslint-config-prettier',
'plugin:vue/vue3-recommended'
],
parserOptions: {
ecmaVersion: 2021,
sourceType: 'module'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'vue/multi-word-component-names': 'off',
'vue/no-v-html': 'off'
}
}
2.8.2 Prettier配置
// .prettierrc.js
module.exports = {
semi: false,
singleQuote: true,
trailingComma: 'es5',
tabWidth: 2,
useTabs: false,
printWidth: 80,
bracketSpacing: true,
arrowParens: 'avoid',
endOfLine: 'lf'
}
2.8.3 Git配置
# .gitignore
node_modules/
dist/
.env.local
.env.*.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.DS_Store
.vscode/
.idea/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
2.9 项目启动与测试
2.9.1 安装依赖
# 进入项目目录
cd vue-ssr-demo
# 安装依赖
npm install
# 或
yarn install
# 或
pnpm install
2.9.2 启动开发服务器
# 启动开发服务器
npm run dev
# 访问应用
# 浏览器打开 http://localhost:3000
2.9.3 构建生产版本
# 构建生产版本
npm run build
# 启动生产服务器
npm start
2.9.4 验证SSR功能
# 使用curl验证服务端渲染
curl http://localhost:3000
# 检查返回的HTML是否包含完整内容
# 而不是空的<div id="app"></div>
2.10 常见问题解决
2.10.1 依赖安装问题
# 清除缓存
npm cache clean --force
yarn cache clean
pnpm store prune
# 删除node_modules重新安装
rm -rf node_modules
npm install
# 使用淘宝镜像
npm config set registry https://registry.npmmirror.com
2.10.2 端口占用问题
# 查看端口占用
netstat -ano | findstr :3000 # Windows
lsof -i :3000 # macOS/Linux
# 杀死进程
taskkill /PID <PID> /F # Windows
kill -9 <PID> # macOS/Linux
# 或者修改端口
set PORT=3001 && npm run dev # Windows
PORT=3001 npm run dev # macOS/Linux
2.10.3 热重载不工作
// 检查webpack配置
// 确保HMR插件正确配置
const webpack = require('webpack')
module.exports = {
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}
// 检查客户端入口是否包含HMR代码
if (module.hot) {
module.hot.accept()
}
2.11 本章小结
2.11.1 核心要点
- 环境准备:Node.js >= 16、包管理器选择、开发工具配置
- 项目结构:清晰的目录组织、分离客户端和服务端代码
- 构建配置:Webpack基础、客户端、服务端三套配置
- 开发服务器:Express服务器、热重载、开发中间件
- 代码规范:ESLint、Prettier、Git配置
2.11.2 最佳实践
- 使用工厂函数避免状态污染
- 合理配置Webpack优化构建性能
- 设置完善的开发工具链
- 建立统一的代码规范
2.11.3 下章预告
下一章我们将学习Vue SSR的核心概念和基本实现: - 同构应用原理 - 客户端激活过程 - 状态管理 - 路由配置
练习作业:
- 按照本章步骤搭建一个完整的Vue SSR项目
- 修改HTML模板,添加自定义meta标签
- 配置不同的环境变量,测试开发和生产环境
- 尝试修改Webpack配置,添加CSS预处理器支持
下一章: Vue SSR核心概念与实现