├── static └── .gitkeep ├── .eslintignore ├── config ├── prod.env.js ├── dev.env.js └── index.js ├── src ├── assets │ └── logo.png ├── entry-client.js ├── App.vue ├── components │ ├── Test.vue │ └── HelloWorld.vue ├── router │ └── index.js ├── main.js └── entry-server.js ├── .editorconfig ├── index.html ├── .gitignore ├── .postcssrc.js ├── .babelrc ├── README.md ├── .eslintrc.js ├── server.js └── package.json /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tangdaohai/vue-ssr-demo/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{title}} 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Editor directories and files 9 | .idea 10 | .vscode 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/entry-client.js: -------------------------------------------------------------------------------- 1 | import { createApp } from './main' 2 | const { app, router } = createApp() 3 | // 因为可能存在异步组件,所以等待router将所有异步组件加载完毕,服务器端配置也需要此操作 4 | router.onReady(() => { 5 | console.log('router ready') 6 | app.$mount('#app') 7 | }) 8 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"], 15 | "plugins": ["istanbul"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-ssr-demo 2 | 3 | > support ssr with node 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | npm install 10 | 11 | # 打包生成环境与服务器端渲染资源 12 | npm run build 13 | 14 | # 启动服务 15 | npm run start-prod 16 | #或者 17 | node server.js 18 | ``` 19 | 20 | 链接到本库的博客文章地址: 21 | 22 | [让vue-cli初始化后的项目集成支持SSR](http://blog.myweb.kim/vue/%E8%AE%A9vue-cli%E5%88%9D%E5%A7%8B%E5%8C%96%E5%90%8E%E7%9A%84%E9%A1%B9%E7%9B%AE%E9%9B%86%E6%88%90%E6%94%AF%E6%8C%81SSR/?utm-source=github) 23 | -------------------------------------------------------------------------------- /src/components/Test.vue: -------------------------------------------------------------------------------- 1 | 12 | 22 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import HelloWorld from '@/components/HelloWorld' 4 | 5 | Vue.use(Router) 6 | 7 | export function createRouter () { 8 | return new Router({ 9 | mode: 'history', 10 | routes: [ 11 | { 12 | path: '/', 13 | name: 'Hello', 14 | component: HelloWorld 15 | }, { 16 | path: '/test', 17 | name: 'Test', 18 | component: () => import('@/components/Test') 19 | } 20 | ] 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import { createRouter } from './router' 6 | 7 | Vue.config.productionTip = false 8 | 9 | /* eslint-disable */ 10 | export function createApp () { 11 | // 创建 router 实例 12 | const router = new createRouter() 13 | const app = new Vue({ 14 | // 注入 router 到根 Vue 实例 15 | router, 16 | render: h => h(App) 17 | }) 18 | // 返回 app 和 router 19 | return { app, router } 20 | } 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | }, 12 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 13 | extends: 'standard', 14 | // required to lint *.vue files 15 | plugins: [ 16 | 'html' 17 | ], 18 | // add your custom rules here 19 | 'rules': { 20 | // allow paren-less arrow functions 21 | 'arrow-parens': 0, 22 | // allow async-await 23 | 'generator-star-spacing': 0, 24 | // allow debugger during development 25 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/entry-server.js: -------------------------------------------------------------------------------- 1 | import { createApp } from './main' 2 | export default context => { 3 | // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise, 4 | // 以便服务器能够等待所有的内容在渲染前, 5 | // 就已经准备就绪。 6 | return new Promise((resolve, reject) => { 7 | const { app, router } = createApp() 8 | // 设置服务器端 router 的位置 9 | router.push(context.url) 10 | // 等到 router 将可能的异步组件和钩子函数解析完 11 | router.onReady(() => { 12 | const matchedComponents = router.getMatchedComponents() 13 | // 匹配不到的路由,执行 reject 函数,并返回 404 14 | if (!matchedComponents.length) { 15 | // eslint-disable-next-line 16 | return reject({ code: 404 }) 17 | } 18 | // Promise 应该 resolve 应用程序实例,以便它可以渲染 19 | resolve(app) 20 | }, reject) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa') 2 | const app = new Koa() 3 | const fs = require('fs') 4 | const path = require('path') 5 | const { createBundleRenderer } = require('vue-server-renderer') 6 | 7 | const resolve = file => path.resolve(__dirname, file) 8 | 9 | // 生成服务端渲染函数 10 | const renderer = createBundleRenderer(require('./dist/vue-ssr-server-bundle.json'), { 11 | // 推荐 12 | runInNewContext: false, 13 | // 模板html文件 14 | template: fs.readFileSync(resolve('./index.html'), 'utf-8'), 15 | // client manifest 16 | clientManifest: require('./dist/vue-ssr-client-manifest.json') 17 | }) 18 | 19 | function renderToString (context) { 20 | return new Promise((resolve, reject) => { 21 | renderer.renderToString(context, (err, html) => err ? reject(err) : resolve(html)) 22 | }) 23 | } 24 | app.use(require('koa-static')(resolve('./dist'))) 25 | // response 26 | app.use(async (ctx, next) => { 27 | try { 28 | const context = { 29 | title: '服务端渲染测试', // default title 30 | url: ctx.url 31 | } 32 | // 将服务器端渲染好的html返回给客户端 33 | ctx.body = await renderToString(context) 34 | 35 | // 设置请求头 36 | ctx.set('Content-Type', 'text/html') 37 | ctx.set('Server', 'Koa2 server side render') 38 | } catch (e) { 39 | // 如果没找到,放过请求,继续运行后面的中间件 40 | next() 41 | } 42 | }) 43 | 44 | app.listen(3001) 45 | .on('listening', () => console.log('服务已启动')) 46 | .on('error', err => console.log(err)) 47 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 34 | 35 | 36 | 55 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | // Template version: 1.1.3 4 | // see http://vuejs-templates.github.io/webpack for documentation. 5 | 6 | const path = require('path') 7 | 8 | module.exports = { 9 | build: { 10 | env: require('./prod.env'), 11 | index: path.resolve(__dirname, '../dist/index.html'), 12 | assetsRoot: path.resolve(__dirname, '../dist'), 13 | assetsSubDirectory: 'static', 14 | assetsPublicPath: '/', 15 | productionSourceMap: true, 16 | // Gzip off by default as many popular static hosts such as 17 | // Surge or Netlify already gzip all static assets for you. 18 | // Before setting to `true`, make sure to: 19 | // npm install --save-dev compression-webpack-plugin 20 | productionGzip: false, 21 | productionGzipExtensions: ['js', 'css'], 22 | // Run the build command with an extra argument to 23 | // View the bundle analyzer report after build finishes: 24 | // `npm run build --report` 25 | // Set to `true` or `false` to always turn it on or off 26 | bundleAnalyzerReport: process.env.npm_config_report 27 | }, 28 | dev: { 29 | env: require('./dev.env'), 30 | port: process.env.PORT || 8080, 31 | autoOpenBrowser: true, 32 | assetsSubDirectory: 'static', 33 | assetsPublicPath: '/', 34 | proxyTable: {}, 35 | // CSS Sourcemaps off by default because relative paths are "buggy" 36 | // with this option, according to the CSS-Loader README 37 | // (https://github.com/webpack/css-loader#sourcemaps) 38 | // In our experience, they generally work as expected, 39 | // just be aware of this issue when enabling this option. 40 | cssSourceMap: false 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-ssr-demo", 3 | "version": "1.0.0", 4 | "description": "support ssr with node", 5 | "author": "Jerry Tang ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "start": "npm run dev", 10 | "build:client": "node build/build.js", 11 | "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.conf.js --progress --hide-modules", 12 | "build": "rimraf dist && npm run build:client && npm run build:server", 13 | "lint": "eslint --ext .js,.vue src", 14 | "start-prod": "node server.js" 15 | }, 16 | "dependencies": { 17 | "koa": "^2.3.0", 18 | "vue": "^2.5.2", 19 | "vue-router": "^3.0.1" 20 | }, 21 | "devDependencies": { 22 | "autoprefixer": "^7.1.2", 23 | "babel-core": "^6.22.1", 24 | "babel-eslint": "^7.1.1", 25 | "babel-loader": "^7.1.1", 26 | "babel-plugin-transform-runtime": "^6.22.0", 27 | "babel-preset-env": "^1.3.2", 28 | "babel-preset-stage-2": "^6.22.0", 29 | "babel-register": "^6.22.0", 30 | "chalk": "^2.0.1", 31 | "connect-history-api-fallback": "^1.3.0", 32 | "copy-webpack-plugin": "^4.0.1", 33 | "cross-env": "^5.1.1", 34 | "css-loader": "^0.28.0", 35 | "eslint": "^3.19.0", 36 | "eslint-config-standard": "^10.2.1", 37 | "eslint-friendly-formatter": "^3.0.0", 38 | "eslint-loader": "^1.7.1", 39 | "eslint-plugin-html": "^3.0.0", 40 | "eslint-plugin-import": "^2.7.0", 41 | "eslint-plugin-node": "^5.2.0", 42 | "eslint-plugin-promise": "^3.4.0", 43 | "eslint-plugin-standard": "^3.0.1", 44 | "eventsource-polyfill": "^0.9.6", 45 | "express": "^4.14.1", 46 | "extract-text-webpack-plugin": "^3.0.0", 47 | "file-loader": "^1.1.4", 48 | "friendly-errors-webpack-plugin": "^1.6.1", 49 | "html-webpack-plugin": "^2.30.1", 50 | "http-proxy-middleware": "^0.17.3", 51 | "koa-static": "^4.0.1", 52 | "opn": "^5.1.0", 53 | "optimize-css-assets-webpack-plugin": "^3.2.0", 54 | "ora": "^1.2.0", 55 | "portfinder": "^1.0.13", 56 | "rimraf": "^2.6.0", 57 | "semver": "^5.3.0", 58 | "shelljs": "^0.7.6", 59 | "url-loader": "^0.5.8", 60 | "vue-loader": "^13.3.0", 61 | "vue-server-renderer": "^2.5.2", 62 | "vue-style-loader": "^3.0.1", 63 | "vue-template-compiler": "^2.5.2", 64 | "webpack": "^3.6.0", 65 | "webpack-bundle-analyzer": "^2.9.0", 66 | "webpack-dev-middleware": "^1.12.0", 67 | "webpack-hot-middleware": "^2.18.2", 68 | "webpack-merge": "^4.1.0", 69 | "webpack-node-externals": "^1.6.0" 70 | }, 71 | "engines": { 72 | "node": ">= 4.0.0", 73 | "npm": ">= 3.0.0" 74 | }, 75 | "browserslist": [ 76 | "> 1%", 77 | "last 2 versions", 78 | "not ie <= 8" 79 | ] 80 | } 81 | --------------------------------------------------------------------------------