正如Vue官方所说,SSR配置适合已经熟悉 Vue, webpack 和 Node.js 开发的开发者阅读。请先移步ssr.vuejs.org 了解手工进行SSR配置的基本内容。
从头搭建一个服务端渲染的应用是相当复杂的。如果您有SSR需求,对Webpack及Koa不是很熟悉,请直接使用NUXT.js。
本文所述内容示例在 Vue SSR Koa2 脚手架 : https://github.com/yi-ge/Vue-SSR-Koa2-Scaffold
我们以撰写本文时的最新版:Vue 2,Webpack 4,Koa 2为例。
特别说明
此文描述的是API与WEB同在一个项目的情况下进行的配置,且API、SSR Server、Static均使用了同一个Koa示例,目的是阐述配置方法,所有的报错显示在一个终端,方便调试。
初始化项目
git init yarn init touch .gitignore
在 .gitignore 文件,将常见的目录放于其中。
.DS_Store node_modules # 编译后的文件以下两个目录 /dist/web /dist/api # Log files npm-debug.log* yarn-debug.log* yarn-error.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw*
根据经验来预先添加肯定会用到的依赖项:
echo \"yarn add cross-env # 跨平台的环境变量设置工具 koa koa-body # 可选,推荐 koa-compress # 压缩数据 compressible # https://github.com/jshttp/compressible axios # 此项目作为API请求工具 es6-promise vue vue-router # vue 路由 注意,SSR必选 vuex # 可选,但推荐使用,本文基于此做Vuex在SSR的优化 vue-template-compiler vue-server-renderer # 关键 lru-cache # 配合上面一个插件缓存数据 vuex-router-sync\" | sed \'s/#[[:space:]].*//g\' | tr \'\\n\' \' \' | sed \'s/[ ][ ]*/ /g\' | bash echo \"yarn add -D webpack webpack-cli webpack-dev-middleware # 关键 webpack-hot-middleware # 关键 webpack-merge # 合并多个Webpack配置文件的配置 webpack-node-externals # 不打包node_modules里面的模块 friendly-errors-webpack-plugin # 显示友好的错误提示插件 case-sensitive-paths-webpack-plugin # 无视路径大小写插件 copy-webpack-plugin # 用于拷贝文件的Webpack插件 mini-css-extract-plugin # CSS压缩插件 chalk # console着色 @babel/core # 不解释 babel-loader @babel/plugin-syntax-dynamic-import # 支持动态import @babel/plugin-syntax-jsx # 兼容JSX写法 babel-plugin-syntax-jsx # 不重复,必须的 babel-plugin-transform-vue-jsx babel-helper-vue-jsx-merge-props @babel/polyfill @babel/preset-env file-loader json-loader url-loader css-loader vue-loader vue-style-loader vue-html-loader\" | sed \'s/#[[:space:]].*//g\' | tr \'\\n\' \' \' | sed \'s/[ ][ ]*/ /g\' | bash
现在的npm模块命名越来越语义化,基本上都是见名知意。关于Eslint以及Stylus、Less等CSS预处理模块我没有添加,其不是本文研究的重点,况且既然您在阅读本文,这些配置相信早已不在话下了。
效仿 electorn 分离main及renderer,在 src 中创建 api 及 web 目录。效仿 vue-cli ,在根目录下创建 public 目录用于存放根目录下的静态资源文件。
|-- public # 静态资源 |-- src |-- api # 后端代码 |-- web # 前端代码
譬如 NUXT.js ,前端服务器代理API进行后端渲染,我们的配置可以选择进行一层代理,也可以配置减少这层代理,直接返回渲染结果。通常来说,SSR的服务器端渲染只渲染首屏,因此API服务器最好和前端服务器在同一个内网。
配置 package.json 的 s :
\" s\": {
\"serve\": \"cross-env NODE_ENV=development node config/server.js\",
\"start\": \"cross-env NODE_ENV=production node config/server.js\"
}
- yarn serve : 启动开发调试
- yarn start : 运行编译后的程序
- config/app.js 导出一些常见配置:
module.exports = {
app: {
port: 3000, // 监听的端口
devHost: \'localhost\', // 开发环境下打开的地址,监听了0.0.0.0,但是不是所有设备都支持访问这个地址,用127.0.0.1或localhost代替
open: true // 是否打开浏览器
}
}
配置SSR
我们以Koa作为调试和实际运行的服务器框架, config/server.js :
const path = require(\'path\')
const Koa = req uire(\'koa\')
const koaCompress = require(\'koa-compress\')
const compressible = require(\'compressible\')
const koaStatic = require(\'./koa/static\')
const SSR = require(\'./ssr\')
const conf = require(\'./app\')
const isProd = process.env.NODE_ENV === \'production\'
const app = new Koa()
app.use(koaCompress({ // 压缩数据
filter: type => !(/event\\-stream/i.test(type)) && compressible(type) // eslint-disable-line
}))
app.use(koaStatic(isProd ? path.resolve(__dirname, \'../dist/web\') : path.resolve(__dirname, \'../public\'), {
maxAge: 30 * 24 * 60 * 60 * 1000
})) // 配置静态资源目录及过期时间
// vue ssr处理,在SSR中处理API
SSR(app).then(server => {
server.listen(conf.app.port, \'0.0.0.0\', () => {
console.log(`> server is staring...`)
})
})
上述文件我们根据是否是开发环境,配置了对应的静态资源目录。需要说明的是,我们约定编译后的API文件位于 dist/api ,前端文件位于 dist/web 。
参考 koa-static 实现静态资源的处理, config/koa/static.js :
\'use strict\'
/**
* From koa-static
*/
const { resolve } = require(\'path\')
const assert = require(\'assert\')
const send = require(\'koa-send\')
/**
* Expose `serve()`.
*/
module.exports = serve
/**
* Serve static files from `root`.
*
* @param {String} root
* @param { } [opts]
* @return {Function}
* @api public
*/
function serve (root, opts) {
opts = .assign({}, opts)
assert(root, \'root directory is required to serve files\')
// options
opts.root = resolve(root)
if (opts.index !== false) opts.index = opts.index || \'index.html\'
if (!opts.defer) {
return async function serve (ctx, next) {
let done = false
if (ctx.method === \'HEAD\' || ctx.method === \'GET\') {
if (ctx.path === \'/\' || ctx.path === \'/index.html\') { // exclude index.html file
await next()
return
}
try {
done = await send(ctx, ctx.path, opts)
} catch (err) {
if (err.status !== 404) {
throw err
}
}
}
if (!done) {
await next()
}
}
}
return async function serve (ctx, next) {
await next()
if (ctx.method !== \'HEAD\' && ctx.method !== \'GET\') return
// response is already handled
if (ctx.body != null || ctx.status !== 404) return // eslint-disable-line
try {
await send(ctx, ctx.path, opts)
} catch (err) {
if (err.status !== 404) {
throw err
}
}
}
}
我们可以看到, koa-static 仅仅是对 koa-send 进行了简单封装( yarn add koa-send )。接下来就是重头戏SSR相关的配置了, config/ssr.js :
const fs = require(\'fs\')
const path = require(\'path\')
const chalk = require(\'chalk\')
const LRU = require(\'lru-cache\')
const {
createBundleRenderer
} = require(\'vue-server-renderer\')
const isProd = process.env.NODE_ENV === \'production\'
const setUpDevServer = require(\'./setup-dev-server\')
const HtmlMinifier = require(\'html-minifier\').minify
const pathResolve = file => path.resolve(__dirname, file)
module.exports = app => {
return new Promise((resolve, reject) => {
const createRenderer = (bundle, options) => {
return createBundleRenderer(bundle, .assign(options, {
cache: LRU({
max: 1000,
maxAge: 1000 * 60 * 15
}),
dir: pathResolve(\'../dist/web\'),
runInNewContext: false
}))
}
let renderer = null
if (isProd) {
// prod mode
const template = HtmlMinifier(fs.readFileSync(pathResolve(\'../public/index.html\'), \'utf-8\'), {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: false
})
const bundle = require(pathResolve(\'../dist/web/vue-ssr-server-bundle.json\'))
const clientManifest = require(pathResolve(\'../dist/web/vue-ssr-client-manifest.json\'))
renderer = createRenderer(bundle, {
template,
clientManifest
})
} else {
// dev mode
setUpDevServer(app, (bundle, options, apiMain, apiOutDir) => {
try {
const API = eval(apiMain).default // eslint-disable-line
const server = API(app)
renderer = createRenderer(bundle, options)
resolve(server)
} catch (e) {
console.log(chalk.red(\'\\nServer error\'), e)
}
})
}
app.use(async (ctx, next) => {
if (!renderer) {
ctx.type = \'html\'
ctx.body = \'waiting for compilation... refresh in a moment.\'
next()
return
}
let status = 200
let html = null
const context = {
url: ctx.url,
: \'OK\'
}
if (/^\\/api/.test(ctx.url)) { // 如果请求以/api开头,则进入api部分进行处理。
next()
return
}
try {
status = 200
html = await renderer.renderToString(context)
} catch (e) {
if (e.message === \'404\') {
status = 404
html = \'404 | Not Found\'
} else {
status = 500
console.log(chalk.red(\'\\nError: \'), e.message)
html = \'500 | Internal Server Error\'
}
}
ctx.type = \'html\'
ctx.status = status || ctx.status
ctx.body = html
next()
})
if (isProd) {
const API = require(\'../dist/api/api\').default
const server = API(app)
resolve(server)
}
})
}
这里新加入了 html-minifier 模块来压缩生产环境的 index.html 文件( yarn add html-minifier )。其余配置和官方给出的差不多,不再赘述。只不过Promise返回的是 require(\'http\').createServer(app.callback()) (详见源码)。这样做的目的是为了共用一个koa2实例。此外,这里拦截了 /api 开头的请求,将请求交由API Server进行处理(因在同一个Koa2实例,这里直接next()了)。在 public 目录下必须存在 index.html 文件:
<!DOCTYPE html>
<html lang=\"zh-cn\">
<head>
< >{{ }}</ >
...
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
开发环境中,处理数据的核心在 config/setup-dev-server.js 文件:
const fs = require(\'fs\')
const path = require(\'path\')
const chalk = require(\'chalk\')
const MFS = require(\'memory-fs\')
const webpack = require(\'webpack\')
const chokidar = require(\'chokidar\')
const apiConfig = require(\'./webpack.api.config\')
const serverConfig = require(\'./webpack.server.config\')
const webConfig = require(\'./webpack.web.config\')
const webpackDevMiddleware = require(\'./koa/dev\')
const webpackHotMiddleware = require(\'./koa/hot\')
const readline = require(\'readline\')
const conf = require(\'./app\')
const {
hasProjectYarn,
openBrowser
} = require(\'./lib\')
const readFile = (fs, file) => {
try {
return fs.readFileSync(path.join(webConfig.output.path, file), \'utf-8\')
} catch (e) {}
}
module.exports = (app, cb) => {
let apiMain, bundle, template, clientManifest, serverTime, webTime, apiTime
const apiOutDir = apiConfig.output.path
let isFrist = true
const clearConsole = () => {
if (process.stdout.isTTY) {
// Fill screen with blank lines. Then move to 0 (beginning of visible part) and clear it
const blank = \'\\n\'.repeat(process.stdout.rows)
console.log(blank)
readline.cursorTo(process.stdout, 0, 0)
readline.clearScreenDown(process.stdout)
}
}
const update = () => {
if (apiMain && bundle && template && clientManifest) {
if (isFrist) {
const url = \'http://\' + conf.app.devHost + \':\' + conf.app.port
console.log(chalk.bgGreen.black(\' DONE \') + \' \' + chalk.green(`Compiled successfully in ${serverTime + webTime + apiTime}ms`))
console.log()
console.log(` App running at: ${chalk.cyan(url)}`)
console.log()
const buildCommand = hasProjectYarn(process.cwd()) ? `yarn build` : `npm run build`
console.log(` Note that the development build is not optimized.`)
console.log(` To create a production build, run ${chalk.cyan(buildCommand)}.`)
console.log()
if (conf.app.open) openBrowser(url)
isFrist = false
}
cb(bundle, {
template,
clientManifest
}, apiMain, apiOutDir)
}
}
// server for api
apiConfig.entry.app = [\'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=2000&reload=true\', apiConfig.entry.app]
apiConfig.plugins.push(
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmit sPlugin()
)
const apiCompiler = webpack(apiConfig)
const apiMfs = new MFS()
apiCompiler.outputFileSystem = apiMfs
apiCompiler.watch({}, (err, stats) => {
if (err) throw err
stats = stats.toJson()
if (stats.errors.length) return
console.log(\'api-dev...\')
apiMfs.readdir(path.join(__dirname, \'../dist/api\'), function (err, files) {
if (err) {
return console.error(err)
}
files.forEach(function (file) {
console.info(file)
})
})
apiMain = apiMfs.readFileSync(path.join(apiConfig.output.path, \'api.js\'), \'utf-8\')
update()
})
apiCompiler.plugin(\'done\', stats => {
stats = stats.toJson()
stats.errors.forEach(err => console.error(err))
stats.warnings.forEach(err => console.warn(err))
if (stats.errors.length) return
apiTime = stats.time
// console.log(\'web-dev\')
// update()
})
// web server for ssr
const serverCompiler = webpack(serverConfig)
const mfs = new MFS()
serverCompiler.outputFileSystem = mfs
serverCompiler.watch({}, (err, stats) => {
if (err) throw err
stats = stats.toJson()
if (stats.errors.length) return
// console.log(\'server-dev...\')
bundle = JSON.parse(readFile(mfs, \'vue-ssr-server-bundle.json\'))
update()
})
serverCompiler.plugin(\'done\', stats => {
stats = stats.toJson()
stats.errors.forEach(err => console.error(err))
stats.warnings.forEach(err => console.warn(err))
if (stats.errors.length) return
serverTime = stats.time
})
// web
webConfig.entry.app = [\'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=2000&reload=true\', webConfig.entry.app]
webConfig.output.filename = \'[name].js\'
webConfig.plugins.push(
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmit sPlugin()
)
const clientCompiler = webpack(webConfig)
const devMiddleware = webpackDevMiddleware(clientCompiler, {
// publicPath: webConfig.output.publicPath,
stats: { // or \'errors-only\'
colors: true
},
reporter: (middlewareOptions, options) => {
const { log, state, stats } = options
if (state) {
const displayStats = (middlewareOptions.stats !== false)
if (displayStats) {
if (stats.hasErrors()) {
log.error(stats.toString(middlewareOptions.stats))
} else if (stats.hasWarnings()) {
log.warn(stats.toString(middlewareOptions.stats))
} else {
log.info(stats.toString(middlewareOptions.stats))
}
}
let message = \'Compiled successfully.\'
if (stats.hasErrors()) {
message = \'Failed to compile.\'
} else if (stats.hasWarnings()) {
message = \'Compiled with warnings.\'
}
log.info(message)
clearConsole()
update()
} else {
log.info(\'Compiling...\')
}
},
noInfo: true,
serverSideRender: false
})
app.use(devMiddleware)
const templatePath = path.resolve(__dirname, \'../public/index.html\')
// read template from disk and watch
template = fs.readFileSync(templatePath, \'utf-8\')
chokidar.watch(templatePath).on(\'change\', () => {
template = fs.readFileSync(templatePath, \'utf-8\')
console.log(\'index.html template updated.\')
update()
})
clientCompiler.plugin(\'done\', stats => {
stats = stats.toJson()
stats.errors.forEach(err => console.error(err))
stats.warnings.forEach(err => console.warn(err))
if (stats.errors.length) return
clientManifest = JSON.parse(readFile(
devMiddleware.fileSystem,
\'vue-ssr-client-manifest.json\'
))
webTime = stats.time
})
app.use(webpackHotMiddleware(clientCompiler))
}
由于篇幅限制, koa 及 lib 目录下的文件参考示例代码。其中 lib 下的文件均来自 vue-cli ,主要用于判断用户是否使用 yarn 以及在浏览器中打开URL。 这时,为了适应上述功能的需要,需添加以下模块(可选):
yarn add memory-fs chokidar readline yarn add -D opn execa
通过阅读 config/setup-dev-server.js 文件内容,您将发现此处进行了三个webpack配置的处理。
Server for API // 用于处理`/api`开头下的API接口,提供非首屏API接入的能力 Web server for SSR // 用于服务器端对API的代理请求,实现SSR WEB // 进行常规静态资源的处理
Webpack 配置
|-- config |-- webpack.api.config.js // Server for API |-- webpack. .config.js // 基础Webpack配置 |-- webpack.server.config.js // Web server for SSR |-- webpack.web.config.js // 常规静态资源
由于Webpack的配置较常规Vue项目以及Node.js项目并没有太大区别,不再一一赘述,具体配置请翻阅源码。
值得注意的是,我们为API和WEB指定了别名:
alias: {
\'@\': path.join(__dirname, \'../src/web\'),
\'~\': path.join(__dirname, \'../src/api\'),
\'vue$\': \'vue/dist/vue.esm.js\'
},
此外, webpack. .config.js 中设定编译时拷贝 public 目录下的文件到 dist/web 目录时并不包含 index.html 文件。
编译脚本:
\" s\": {
...
\"build\": \"rimraf dist && npm run build:web && npm run build:server && npm run build:api\",
\"build:web\": \"cross-env NODE_ENV=production webpack --config config/webpack.web.config.js --progress --hide-modules\",
\"build:server\": \"cross-env NODE_ENV=production webpack --config config/webpack.server.config.js --progress --hide-modules\",
\"build:api\": \"cross-env NODE_ENV=production webpack --config config/webpack.api.config.js --progress --hide-modules\"
},
执行 yarn build 进行编译。编译后的文件存于 /dist 目录下。正式环境请尽量分离API及SSR Server。
测试
执行 yarn serve (开发)或 yarn start (编译后)命令,访问 http://localhost:3000 。
通过查看源文件可以看到,首屏渲染结果是这样的:
~ curl -s http://localhost:3000/ | grep Hello <div id=\"app\" data-server-rendered=\"true\"><div>Hello World SSR</div></div>
至此,Vue SSR配置完成。希望对大家的学习有所帮助,也希望大家多多支持脚本之家。
继续阅读与本文标签相同的文章
机器人可以在0.38秒内解决魔方问题
三星全新“笛子”音响,游戏爱好者的福音
-
短视频SDK的编译设计与实现系列(一)——CMake基础入门
2026-05-19栏目: 教程
-
ASP.NET Core on K8S深入学习(8)数据管理
2026-05-19栏目: 教程
-
AI翻译新思路,OBTranslate打破非洲城乡居民之间的沟通差距
2026-05-19栏目: 教程
-
物联网平台实用技巧:调用API获取设备状态
2026-05-19栏目: 教程
-
路漫漫其修远兮,吾将上下而求索
2026-05-19栏目: 教程
