SoFunction
Updated on 2025-04-04

Detailed explanation of Vue SSR (Vue2 + Koa2 + Webpack4) configuration guide

As Vue officially said, SSR configuration is suitable for developers who are already familiar with Vue, webpack and development. Please move first Understand the basics of manually performing SSR configuration.

It is quite complicated to build an application of server-side rendering from scratch. If you have SSR requirements and are not very familiar with Webpack and Koa, please use it directly

Examples of the content described in this article are in Vue SSR Koa2 scaffolding:/yi-ge/Vue-SSR-Koa2-Scaffold

Let's take the latest version of this article: Vue 2, Webpack 4, Koa 2 as an example.

Special Note

This article describes the configuration of the API and WEB in the same project, and the API, SSR Server, and Static all use the same Koa example. The purpose is to explain the configuration method. All errors are displayed in one terminal for easy debugging.

Initialize the project

git init
yarn init
touch .gitignore

In the .gitignore file, place common directories in it.

.DS_Store
node_modules

# The following two directories of the compiled file/dist/web
/dist/api

# Log files
*
*
*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

Based on experience, pre-add dependencies that you will definitely use:

echo "yarn add cross-env # Cross-platform environment variable setting tool koa
 koa-body # Optional, recommended koa-compress # Compress data compressible # /jshttp/compressible
 axios # This project serves as an API request tool es6-promise 
 vue
 vue-router # vue routing Note that SSR is required vuex # Optional, but recommended to use. This article uses this to optimize Vuex in SSR vue-template-compiler
 vue-server-renderer # Essential lru-cache # Cache data with the above plug-in vuex-router-sync" | sed 's/#[[:space:]].*//g' | tr '\n' ' ' | sed 's/[ ][ ]*/ /g' | bash

echo "yarn add -D webpack
 webpack-cli
 webpack-dev-middleware # Essential webpack-hot-middleware # Essential webpack-merge # Merge multiple Webpack configuration files configurations webpack-node-externals # Do not package the modules in node_modules friendly-errors-webpack-plugin # Show friendly error prompt plugin case-sensitive-paths-webpack-plugin # Ignore path case plugin copy-webpack-plugin # Webpack plugin for copying files mini-css-extract-plugin # CSS compression plug-in chalk # console coloring @babel/core # Not explained babel-loader
 @babel/plugin-syntax-dynamic-import # Support dynamic import @babel/plugin-syntax-jsx # Compatible with JSX writing method babel-plugin-syntax-jsx # No repetition, necessary 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

Nowadays, npm module naming is becoming more and more semantic, and basically it is known to know the meaning. I have not added Eslint and CSS preprocessing modules such as Stylus and Less. They are not the focus of this article's research. Moreover, since you are reading this article, these configurations are no longer a problem.

Follow the example of electronicon to separate main and renderer, and create API and web directories in src. Follow vue-cli , create a public directory in the root directory to store static resource files in the root directory.

|-- public # Static resources|-- src
 |-- api # Backend code |-- web # Front-end code

For example, the front-end server proxy API performs back-end rendering. Our configuration can choose to perform a one-layer proxy, or we can configure to reduce this layer of proxy to directly return the rendering result. Generally speaking, SSR's server-side rendering only renders the first screen, so the API server is best on the same intranet as the front-end server.

Configured scripts:

"scripts": {
 "serve": "cross-env NODE_ENV=development node config/",
 "start": "cross-env NODE_ENV=production node config/"
}

  • yarn serve: Start development and debugging
  • yarn start: Run the compiled program
  • config/ exports some common configurations:
 = {
 app: {
 port: 3000, // The listening port devHost: 'localhost', // The address opened in the development environment is 0.0.0.0, but not all devices support accessing this address, and 127.0.0.1 or localhost is used instead. open: true // Whether to open the browser }
}

Configure SSR

We use Koa as the server framework for debugging and actually running, config/:

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 = .NODE_ENV === 'production'

const app = new Koa()

(koaCompress({ // Compress data filter: type => !(/event\-stream/(type)) && compressible(type) // eslint-disable-line
}))

(koaStatic(isProd ? (__dirname, '../dist/web') : (__dirname, '../public'), {
 maxAge: 30 * 24 * 60 * 60 * 1000
})) // Configure static resource directory and expiration time
// vue ssr processing, processing API in SSRSSR(app).then(server => {
 (, '0.0.0.0', () => {
 (`> server is staring...`)
 })
})

We have configured the corresponding static resource directory for the above file based on whether it is a development environment. It should be noted that we agree that the compiled API file is located in dist/api and the front-end file is located in dist/web.

Refer to koa-static to implement the processing of static resources, config/koa/:

'use strict'

/**
 * From koa-static
 */

const { resolve } = require('path')
const assert = require('assert')
const send = require('koa-send')

/**
 * Expose `serve()`.
 */

 = serve

/**
 * Serve static files from `root`.
 *
 * @param {String} root
 * @param {Object} [opts]
 * @return {Function}
 * @api public
 */

function serve (root, opts) {
 opts = ({}, opts)

 assert(root, 'root directory is required to serve files')

 // options
  = resolve(root)
 if ( !== false)  =  || ''

 if (!) {
 return async function serve (ctx, next) {
  let done = false

  if ( === 'HEAD' ||  === 'GET') {
  if ( === '/' ||  === '/') { // exclude  file
   await next()
   return
  }
  try {
   done = await send(ctx, , opts)
  } catch (err) {
   if ( !== 404) {
   throw err
   }
  }
  }

  if (!done) {
  await next()
  }
 }
 }

 return async function serve (ctx, next) {
 await next()

 if ( !== 'HEAD' &&  !== 'GET') return
 // response is already handled
 if ( != null ||  !== 404) return // eslint-disable-line

 try {
  await send(ctx, , opts)
 } catch (err) {
  if ( !== 404) {
  throw err
  }
 }
 }
}

We can see that koa-static just simply encapsulates koa-send ( yarn add koa-send ). Next is the SSR-related configuration, config/:

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 = .NODE_ENV === 'production'
const setUpDevServer = require('./setup-dev-server')
const HtmlMinifier = require('html-minifier').minify

const pathResolve = file => (__dirname, file)

 = app => {
 return new Promise((resolve, reject) => {
 const createRenderer = (bundle, options) => {
  return createBundleRenderer(bundle, (options, {
  cache: LRU({
   max: 1000,
   maxAge: 1000 * 60 * 15
  }),
  basedir: pathResolve('../dist/web'),
  runInNewContext: false
  }))
 }

 let renderer = null
 if (isProd) {
  // prod mode
  const template = HtmlMinifier((pathResolve('../public/'), 'utf-8'), {
  collapseWhitespace: true,
  removeAttributeQuotes: true,
  removeComments: false
  })
  const bundle = require(pathResolve('../dist/web/'))
  const clientManifest = require(pathResolve('../dist/web/'))
  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) {
   (('\nServer error'), e)
  }
  })
 }

 (async (ctx, next) => {
  if (!renderer) {
   = 'html'
   = 'waiting for compilation... refresh in a moment.'
  next()
  return
  }

  let status = 200
  let html = null
  const context = {
  url: ,
  title: 'OK'
  }

  if (/^\/api/.test()) { // If the request starts with /api, enter the api part for processing.  next()
  return
  }

  try {
  status = 200
  html = await (context)
  } catch (e) {
  if ( === '404') {
   status = 404
   html = '404 | Not Found'
  } else {
   status = 500
   (('\nError: '), )
   html = '500 | Internal Server Error'
  }
  }
   = 'html'
   = status || 
   = html
  next()
 })

 if (isProd) {
  const API = require('../dist/api/api').default
  const server = API(app)
  resolve(server)
 }
 })
}

Here, a new html-minifier module has been added to compress the production environment files (yarn add html-minifier). The other configurations are similar to those given by the official, so I won’t go into details. However, Promise returns require('http').createServer(()) (see the source code for details). The purpose of this is to share a koa2 instance. In addition, requests starting with /api are intercepted here and the request is handed over to the API Server for processing (because it is next() directly here in the same Koa2 instance). Files must exist in the public directory:

<!DOCTYPE html>
<html lang="zh-cn">
<head>
 <title>{{ title }}</title>
 ...
</head>
<body>
 <!--vue-ssr-outlet-->
</body>
</html>

In the development environment, the core of processing data is in the config/ file:

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('./')
const serverConfig = require('./')
const webConfig = require('./')
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 ((, file), 'utf-8')
 } catch (e) {}
}

 = (app, cb) => {
 let apiMain, bundle, template, clientManifest, serverTime, webTime, apiTime
 const apiOutDir = 
 let isFrist = true

 const clearConsole = () => {
 if () {
  // Fill screen with blank lines. Then move to 0 (beginning of visible part) and clear it
  const blank = '\n'.repeat()
  (blank)
  (, 0, 0)
  ()
 }
 }

 const update = () => {
 if (apiMain && bundle && template && clientManifest) {
  if (isFrist) {
  const url = 'http://' +  + ':' + 
  ((' DONE ') + ' ' + (`Compiled successfully in ${serverTime + webTime + apiTime}ms`))
  ()
  (` App running at: ${(url)}`)
  ()
  const buildCommand = hasProjectYarn(()) ? `yarn build` : `npm run build`
  (` Note that the development build is not optimized.`)
  (` To create a production build, run ${(buildCommand)}.`)
  ()
  if () openBrowser(url)
  isFrist = false
  }
  cb(bundle, {
  template,
  clientManifest
  }, apiMain, apiOutDir)
 }
 }

 // server for api
  = ['webpack-hot-middleware/client?path=/__webpack_hmr&timeout=2000&reload=true', ]
 (
 new (),
 new ()
 )
 const apiCompiler = webpack(apiConfig)
 const apiMfs = new MFS()
  = apiMfs
 ({}, (err, stats) => {
 if (err) throw err
 stats = ()
 if () return
 ('api-dev...')
 ((__dirname, '../dist/api'), function (err, files) {
  if (err) {
  return (err)
  }
  (function (file) {
  (file)
  })
 })
 apiMain = ((, ''), 'utf-8')
 update()
 })
 ('done', stats => {
 stats = ()
 (err => (err))
 (err => (err))
 if () return

 apiTime = 
 // ('web-dev')
 // update()
 })

 // web server for ssr
 const serverCompiler = webpack(serverConfig)
 const mfs = new MFS()
  = mfs
 ({}, (err, stats) => {
 if (err) throw err
 stats = ()
 if () return
 // ('server-dev...')
 bundle = (readFile(mfs, ''))
 update()
 })
 ('done', stats => {
 stats = ()
 (err => (err))
 (err => (err))
 if () return

 serverTime = 
 })

 // web
  = ['webpack-hot-middleware/client?path=/__webpack_hmr&timeout=2000&reload=true', ]
  = '[name].js'
 (
 new (),
 new ()
 )
 const clientCompiler = webpack(webConfig)
 const devMiddleware = webpackDevMiddleware(clientCompiler, {
 // publicPath: ,
 stats: { // or 'errors-only'
  colors: true
 },
 reporter: (middlewareOptions, options) => {
  const { log, state, stats } = options

  if (state) {
  const displayStats = ( !== false)

  if (displayStats) {
   if (()) {
   (())
   } else if (()) {
   (())
   } else {
   (())
   }
  }

  let message = 'Compiled successfully.'

  if (()) {
   message = 'Failed to compile.'
  } else if (()) {
   message = 'Compiled with warnings.'
  }
  (message)

  clearConsole()

  update()
  } else {
  ('Compiling...')
  }
 },
 noInfo: true,
 serverSideRender: false
 })
 (devMiddleware)

 const templatePath = (__dirname, '../public/')

 // read template from disk and watch
 template = (templatePath, 'utf-8')
 (templatePath).on('change', () => {
 template = (templatePath, 'utf-8')
 (' template updated.')
 update()
 })

 ('done', stats => {
 stats = ()
 (err => (err))
 (err => (err))
 if () return

 clientManifest = (readFile(
  ,
  ''
 ))

 webTime = 
 })
 (webpackHotMiddleware(clientCompiler))
}

Due to space limitations, the files in the koa and lib directories refer to the sample code. The files under lib are all from vue-cli, which are mainly used to determine whether the user uses yarn and open URLs in the browser. At this time, in order to meet the needs of the above functions, the following modules (optional):

yarn add memory-fs chokidar readline

yarn add -D opn execa

By reading the config/ file contents, you will find that there are three webpack configuration processing here.

Server for API // Used to handle the API interface at the beginning of `/api`, providing the ability to access non-first-screen APIs
Web server for SSR // Used for proxy requests to the API on the server side, implementing SSR
WEB // Processing of regular static resources

Webpack configuration

|-- config
 |--  // Server for API
 |--  // Basic Webpack configuration |--  // Web server for SSR
 |--  // Regular static resources

Since the configuration of Webpack is not much different from that of conventional Vue projects and projects, I will not elaborate on them one by one. Please refer to the source code for the specific configuration.

It is worth noting that we have assigned alias for the API and WEB:

alias: {
 '@': (__dirname, '../src/web'),
 '~': (__dirname, '../src/api'),
 'vue$': 'vue/dist/'
},

In addition, the file is not included when copying files in the public directory during compilation to the dist/web directory.

Compile the script:

"scripts": {
 ...
 "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/ --progress --hide-modules",
 "build:server": "cross-env NODE_ENV=production webpack --config config/ --progress --hide-modules",
 "build:api": "cross-env NODE_ENV=production webpack --config config/ --progress --hide-modules"
},

Execute yarn build to compile. The compiled files are stored in the /dist directory. For formal environment, please try to separate API and SSR Server.

test

Execute the yarn serve (development) or yarn start (after compilation) commands to access http://localhost:3000.

By viewing the source file, you can see that the rendering result of the first screen is as follows:

 ~ curl -s http://localhost:3000/ | grep Hello
 <div  data-server-rendered="true"><div>Hello World SSR</div></div>

At this point, the Vue SSR configuration is completed. I hope it will be helpful to everyone's learning and I hope everyone will support me more.