├── .babelrc ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── config ├── setup-dev-server.js ├── webpack.base.config.js ├── webpack.client.config.js └── webpack.server.config.js ├── index.html ├── package.json ├── server.js └── src ├── App.vue ├── api └── index.js ├── app.js ├── assets ├── images │ └── background-blur.jpg └── style.scss ├── client-entry.js ├── modules ├── about │ └── index.vue └── home │ ├── components │ └── users-list │ │ └── index.vue │ └── index.vue ├── router └── index.js ├── server-entry.js └── store ├── actions.js ├── getters.js ├── index.js └── mutations.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "modules": "commonjs", 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"], 7 | "node": "current" 8 | } 9 | }] 10 | ] 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Oleg Pisklov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue SSR Simple Setup 2 | 3 | > [Medium article](https://medium.com/namecheap-engineering/production-ready-vue-ssr-in-5-simple-steps-39d171904150) 4 | 5 | This repo is an example of SSR setup for Vue.js application using: 6 | * _Webpack 4_ for building client and server bundles; 7 | * _Node.js Express_ server; 8 | * _webpack-dev-middleware_ and _webpack-hot-middleware_ for comfortable dev environment; 9 | * _Babel_ for transpiling modern js syntax; 10 | * _Vuex_ for a state management; 11 | * _vue-meta_ for metadata management. 12 | 13 | Feel free to use it as a boilerplate for your projects. 14 | 15 | ## Project setup 16 | ``` 17 | npm install 18 | ``` 19 | 20 | ### Run Express dev server with HMR support 21 | ``` 22 | npm run dev 23 | ``` 24 | 25 | ### Build client bundle for production 26 | ``` 27 | npm run build:client 28 | ``` 29 | 30 | ### Build SSR bundle for production 31 | ``` 32 | npm run build:server 33 | ``` 34 | -------------------------------------------------------------------------------- /config/setup-dev-server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Setup webpack-dev-middleware and webpack-hot-middleware. 3 | * Rebuild SSR bundle on src files change. 4 | * 5 | * @param {Object} app Express application 6 | * @param {Function} onServerBundleReady Callback 7 | */ 8 | const setupDevServer = (app, onServerBundleReady) => { 9 | const webpack = require('webpack'); 10 | const MFS = require('memory-fs'); 11 | const path = require('path'); 12 | const clientConfig = require('./webpack.client.config'); 13 | const serverConfig = require('./webpack.server.config'); 14 | 15 | // additional client entry for hot reload 16 | clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app]; 17 | 18 | const clientCompiler = webpack(clientConfig); 19 | 20 | // setup dev middleware 21 | app.use(require('webpack-dev-middleware')(clientCompiler, { 22 | publicPath: clientConfig.output.publicPath, 23 | serverSideRender: true, 24 | logLevel: 'silent' 25 | })); 26 | 27 | // setup hot middleware 28 | app.use(require('webpack-hot-middleware')(clientCompiler)); 29 | 30 | // watch src files and rebuild SSR bundle 31 | global.console.log('Building SSR bundle...'); 32 | const serverCompiler = webpack(serverConfig); 33 | const mfs = new MFS(); 34 | 35 | serverCompiler.outputFileSystem = mfs; 36 | serverCompiler.watch({}, (error, stats) => { 37 | if (error) throw error; 38 | 39 | global.console.log( 40 | `${stats.toString({ 41 | colors: true, 42 | modules: false, 43 | children: false, 44 | chunks: false, 45 | chunkModules: false, 46 | })}\n\n` 47 | ); 48 | 49 | if (stats.hasErrors()) { 50 | console.error(stats.compilation.errors); 51 | throw new Error(stats.compilation.errors); 52 | } 53 | 54 | // read bundle generated by vue-ssr-webpack-plugin 55 | const bundle = JSON.parse( 56 | mfs.readFileSync(path.join(clientConfig.output.path, 'vue-ssr-server-bundle.json'), 'utf-8') 57 | ); 58 | onServerBundleReady(bundle); 59 | }); 60 | }; 61 | 62 | module.exports = setupDevServer; 63 | -------------------------------------------------------------------------------- /config/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const VueLoaderPlugin = require('vue-loader/lib/plugin'); 3 | const srcPath = path.resolve(process.cwd(), 'src'); 4 | const isProduction = process.env.NODE_ENV === 'production'; 5 | 6 | module.exports = { 7 | mode: process.env.NODE_ENV, 8 | devtool: isProduction ? 'source-map' : 'eval-source-map', 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.vue$/, 13 | loader: 'vue-loader', 14 | include: [ srcPath ], 15 | }, 16 | { 17 | test: /\.js$/, 18 | loader: 'babel-loader', 19 | include: [ srcPath ], 20 | exclude: /node_modules/, 21 | }, 22 | { 23 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 24 | use: [ 25 | { 26 | loader: 'url-loader', 27 | options: { 28 | limit: 10000, 29 | name: '[path][name].[hash:7].[ext]', 30 | context: srcPath 31 | } 32 | } 33 | ] 34 | }, 35 | { 36 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 37 | use: [ 38 | { 39 | loader: 'url-loader', 40 | options: { 41 | limit: 10000, 42 | name: '[name].[hash:7].[ext]' 43 | } 44 | } 45 | ] 46 | }, 47 | ] 48 | }, 49 | plugins: [ 50 | new VueLoaderPlugin() 51 | ] 52 | }; -------------------------------------------------------------------------------- /config/webpack.client.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const merge = require('webpack-merge'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | const autoprefixer = require('autoprefixer'); 6 | 7 | const base = require('./webpack.base.config'); 8 | const isProduction = process.env.NODE_ENV === 'production'; 9 | const srcPath = path.resolve(process.cwd(), 'src'); 10 | 11 | module.exports = merge(base, { 12 | entry: { 13 | app: path.join(srcPath, 'client-entry.js') 14 | }, 15 | output: { 16 | path: path.resolve(process.cwd(), 'dist'), 17 | publicPath: '/public', 18 | filename: isProduction ? '[name].[hash].js' : '[name].js', 19 | sourceMapFilename: isProduction ? '[name].[hash].js.map' : '[name].js.map', 20 | }, 21 | resolve: { 22 | extensions: ['.js', '.vue'], 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.css$/, 28 | use: [ 29 | MiniCssExtractPlugin.loader, 30 | { loader: 'css-loader', options: { sourceMap: !isProduction } }, 31 | ] 32 | }, 33 | { 34 | test: /\.scss$/, 35 | use: [ 36 | MiniCssExtractPlugin.loader, 37 | 'css-loader', 38 | { 39 | loader: 'postcss-loader', 40 | options: { 41 | plugins: () => [autoprefixer] 42 | } 43 | }, 44 | 'sass-loader', 45 | ], 46 | }, 47 | ] 48 | }, 49 | 50 | plugins: (isProduction ? 51 | [ 52 | new MiniCssExtractPlugin({ 53 | filename: '[name].[contenthash].css', 54 | }), 55 | ] : [ 56 | new MiniCssExtractPlugin({ 57 | filename: '[name].css', 58 | hmr: true, 59 | }), 60 | new webpack.HotModuleReplacementPlugin(), 61 | ] 62 | ) 63 | }); -------------------------------------------------------------------------------- /config/webpack.server.config.js: -------------------------------------------------------------------------------- 1 | const nodeExternals = require('webpack-node-externals'); 2 | const VueSSRServerPlugin = require('vue-server-renderer/server-plugin'); 3 | const path = require('path'); 4 | const merge = require('webpack-merge'); 5 | 6 | const base = require('./webpack.base.config'); 7 | const srcPath = path.resolve(process.cwd(), 'src'); 8 | 9 | module.exports = merge(base, { 10 | entry: path.join(srcPath, 'server-entry.js'), 11 | target: 'node', 12 | // This tells the server bundle to use Node-style exports 13 | output: { 14 | libraryTarget: 'commonjs2' 15 | }, 16 | 17 | // https://webpack.js.org/configuration/externals/#function 18 | // https://github.com/liady/webpack-node-externals 19 | // Externalize app dependencies. This makes the server build much faster 20 | // and generates a smaller bundle file. 21 | externals: nodeExternals({ 22 | // do not externalize dependencies that need to be processed by webpack. 23 | // you can add more file types here e.g. raw *.vue files 24 | // you should also whitelist deps that modifies `global` (e.g. polyfills) 25 | whitelist: /\.css$/ 26 | }), 27 | 28 | // This is the plugin that turns the entire output of the server build 29 | // into a single JSON file. The default file name will be 30 | // `vue-ssr-server-bundle.json` 31 | plugins: [ 32 | new VueSSRServerPlugin(), 33 | ] 34 | }); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | {{{ meta.inject().title.text() }}} 5 | {{{ meta.inject().meta.text() }}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-ssr-simple-steps", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "dev": "cross-env NODE_ENV=development nodemon ./server.js", 6 | "build:client": "cross-env NODE_ENV=production webpack --config ./config/webpack.client.config.js", 7 | "build:server": "cross-env NODE_ENV=production webpack --config ./config/webpack.server.config.js" 8 | }, 9 | "dependencies": { 10 | "axios": "^0.21.1", 11 | "vue": "^2.6.10", 12 | "vue-meta": "^2.2.2", 13 | "vue-router": "^3.1.2", 14 | "vue-server-renderer": "^2.6.10", 15 | "vuex": "^3.1.1" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.5.5", 19 | "@babel/plugin-proposal-class-properties": "^7.5.5", 20 | "@babel/plugin-proposal-decorators": "^7.4.4", 21 | "@babel/plugin-proposal-json-strings": "^7.2.0", 22 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 23 | "@babel/plugin-syntax-import-meta": "^7.2.0", 24 | "@babel/preset-env": "^7.5.5", 25 | "autoprefixer": "^9.6.1", 26 | "babel-loader": "^8.0.6", 27 | "compression-webpack-plugin": "^3.0.0", 28 | "cross-env": "^5.2.0", 29 | "css-loader": "^3.2.0", 30 | "file-loader": "^4.2.0", 31 | "friendly-errors-webpack-plugin": "^1.7.0", 32 | "html-webpack-plugin": "^3.2.0", 33 | "memory-fs": "^0.5.0", 34 | "mini-css-extract-plugin": "^0.8.0", 35 | "node-sass": "^4.12.0", 36 | "nodemon": "^1.19.1", 37 | "optimize-css-assets-webpack-plugin": "^5.0.3", 38 | "postcss-loader": "^3.0.0", 39 | "resolve-url-loader": "^3.1.0", 40 | "sass-loader": "^7.2.0", 41 | "uglifyjs-webpack-plugin": "^2.2.0", 42 | "url-loader": "^2.1.0", 43 | "vue-loader": "^15.7.1", 44 | "vue-style-loader": "^4.1.2", 45 | "vue-template-compiler": "^2.6.10", 46 | "webpack": "^4.39.1", 47 | "webpack-bundle-analyzer": "^3.4.1", 48 | "webpack-cli": "^3.3.6", 49 | "webpack-dev-middleware": "^3.7.0", 50 | "webpack-hot-middleware": "^2.25.0", 51 | "webpack-merge": "^4.2.2", 52 | "webpack-node-externals": "^1.7.2" 53 | }, 54 | "nodemonConfig": { 55 | "watch": [ 56 | "server.js", 57 | "index.html", 58 | "config/setup-dev-server.js" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const vueServerRenderer = require('vue-server-renderer'); 5 | const setupDevServer = require('./config/setup-dev-server'); 6 | 7 | const port = 3000; 8 | const app = express(); 9 | 10 | const createRenderer = (bundle) => 11 | vueServerRenderer.createBundleRenderer(bundle, { 12 | runInNewContext: false, 13 | template: fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8') 14 | }); 15 | let renderer; 16 | 17 | // you may want to serve static files with nginx or CDN in production 18 | app.use('/public', express.static(path.resolve(__dirname, './dist'))); 19 | 20 | if (process.env.NODE_ENV === 'development') { 21 | setupDevServer(app, (serverBundle) => { 22 | renderer = createRenderer(serverBundle); 23 | }); 24 | } else { 25 | renderer = createRenderer(require('./dist/vue-ssr-server-bundle.json')); 26 | } 27 | 28 | app.get(/^\/(about)?\/?$/, async (req, res) => { 29 | const context = { 30 | url: req.params['0'] || '/', 31 | state: { 32 | title: 'Vue SSR Simple Setup', 33 | users: [] 34 | } 35 | }; 36 | let html; 37 | 38 | try { 39 | html = await renderer.renderToString(context); 40 | } catch (error) { 41 | if (error.code === 404) { 42 | return res.status(404).send('404 | Page Not Found'); 43 | } 44 | return res.status(500).send('500 | Internal Server Error'); 45 | } 46 | 47 | res.end(html); 48 | }); 49 | 50 | // the endpoint for 'serverPrefetch' demonstration 51 | app.get('/users', (req, res) => { 52 | res.json([{ 53 | name: 'Albert', 54 | lastname: 'Einstein' 55 | }, { 56 | name: 'Isaac', 57 | lastname: 'Newton' 58 | }, { 59 | name: 'Marie', 60 | lastname: 'Curie' 61 | }] 62 | ); 63 | }); 64 | 65 | app.listen(port, () => console.log(`Listening on: ${port}`)); -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 2 |