├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── README.md ├── build ├── dev-server.js ├── vue-loader.config.js ├── webpack.base.config.js ├── webpack.client.config.js └── webpack.server.config.js ├── package-lock.json ├── package.json ├── server.js ├── src ├── App.vue ├── app.js ├── assets │ └── logo.png ├── client-entry.js ├── components │ └── Todo.vue ├── index.template.html ├── router │ └── index.js ├── server-entry.js ├── views │ ├── About.vue │ ├── Counter.vue │ ├── Home.vue │ └── Topics.vue └── vuex │ ├── actions.js │ ├── getters.js │ └── store.js └── test └── unit ├── .eslintrc ├── index.js ├── karma.conf.js └── specs └── About.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["es2015", { "modules": false }], "stage-2"], 3 | "ignore": ["node_modules/*"] 4 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 8 | extends: 'standard', 9 | // required to lint *.vue files 10 | plugins: [ 11 | 'html' 12 | ], 13 | // add your custom rules here 14 | 'rules': { 15 | // allow paren-less arrow functions 16 | 'arrow-parens': 0, 17 | // allow async-await 18 | 'generator-star-spacing': 0, 19 | // allow debugger during development 20 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | .vscode/ 6 | yarn.lock 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "7" 5 | script: npm run unit --single-run -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-ssr-starter-kit 2 | 3 | > A Vue.js project with vue 2.0, vue-router and vuex starter kit for server side rendering. 4 | 5 | [![Build Status](https://travis-ci.org/doabit/vue-ssr-starter-kit.svg?branch=master)](https://travis-ci.org/doabit/vue-ssr-starter-kit) 6 | 7 | ## Node Version Requirement 8 | 9 | ```bash 10 | node 6.* 11 | node 7.* 12 | ``` 13 | 14 | ## Build Setup 15 | 16 | ``` bash 17 | npm install 18 | npm run build 19 | npm start 20 | ``` 21 | 22 | ## Development Setup 23 | 24 | ```bash 25 | npm install 26 | npm run dev 27 | ``` 28 | 29 | ## Reference resources 30 | 31 | [vue-hackernews-2.0](https://github.com/vuejs/vue-hackernews-2.0) 32 | 33 | ## License 34 | 35 | [MIT](http://opensource.org/licenses/MIT) 36 | -------------------------------------------------------------------------------- /build/dev-server.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const MFS = require('memory-fs') 4 | const clientConfig = require('./webpack.client.config') 5 | const serverConfig = require('./webpack.server.config') 6 | 7 | module.exports = function setupDevServer (app, cb) { 8 | let bundle 9 | let template 10 | 11 | // modify client config to work with hot middleware 12 | clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app] 13 | clientConfig.output.filename = '[name].js' 14 | clientConfig.plugins.push( 15 | new webpack.HotModuleReplacementPlugin(), 16 | new webpack.NoEmitOnErrorsPlugin() 17 | ) 18 | 19 | // dev middleware 20 | const clientCompiler = webpack(clientConfig) 21 | const devMiddleware = require('webpack-dev-middleware')(clientCompiler, { 22 | publicPath: clientConfig.output.publicPath, 23 | stats: { 24 | colors: true, 25 | chunks: false 26 | } 27 | }) 28 | app.use(devMiddleware) 29 | clientCompiler.plugin('done', () => { 30 | const fs = devMiddleware.fileSystem 31 | const filePath = path.join(clientConfig.output.path, 'index.html') 32 | if (fs.existsSync(filePath)) { 33 | template = fs.readFileSync(filePath, 'utf-8') 34 | if (bundle) { 35 | cb(bundle, template) 36 | } 37 | } 38 | }) 39 | 40 | // hot middleware 41 | app.use(require('webpack-hot-middleware')(clientCompiler)) 42 | 43 | // watch and update server renderer 44 | const serverCompiler = webpack(serverConfig) 45 | const mfs = new MFS() 46 | serverCompiler.outputFileSystem = mfs 47 | serverCompiler.watch({}, (err, stats) => { 48 | if (err) throw err 49 | stats = stats.toJson() 50 | stats.errors.forEach(err => console.error(err)) 51 | stats.warnings.forEach(err => console.warn(err)) 52 | 53 | // read bundle generated by vue-ssr-webpack-plugin 54 | const bundlePath = path.join(serverConfig.output.path, 'vue-ssr-bundle.json') 55 | bundle = JSON.parse(mfs.readFileSync(bundlePath, 'utf-8')) 56 | if (template) { 57 | cb(bundle, template) 58 | } 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /build/vue-loader.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extractCSS: process.env.NODE_ENV === 'production' 3 | } 4 | -------------------------------------------------------------------------------- /build/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | // const projectRoot = path.resolve(__dirname, '../') 3 | const vueConfig = require('./vue-loader.config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const isProduction = process.env.NODE_ENV === 'production' 6 | 7 | module.exports = { 8 | devtool: '#source-map', 9 | entry: { 10 | app: './src/client-entry.js', 11 | vendor: ['vue', 'vue-router', 'vuex', 'vuex-router-sync', 'axios'] 12 | }, 13 | resolve: { 14 | modules: [path.resolve(__dirname, 'src'), 'node_modules'], 15 | extensions: ['.js', '.vue'], 16 | alias: { 17 | 'src': path.resolve(__dirname, '../src'), 18 | 'assets': path.resolve(__dirname, '../src/assets'), 19 | 'components': path.resolve(__dirname, '../src/components'), 20 | 'views': path.resolve(__dirname, '../src/views') 21 | } 22 | }, 23 | 24 | output: { 25 | path: path.resolve(__dirname, '../dist'), 26 | publicPath: '/dist/', 27 | filename: 'client-bundle.[chunkhash].js' 28 | }, 29 | 30 | module: { 31 | rules: [ 32 | { 33 | enforce: 'pre', 34 | test: /\.js$/, 35 | loader: 'eslint-loader', 36 | exclude: /node_modules/ 37 | }, 38 | { 39 | enforce: 'pre', 40 | test: /\.vue$/, 41 | loader: 'eslint-loader', 42 | exclude: /node_modules/ 43 | }, 44 | { 45 | test: /\.vue$/, 46 | loader: 'vue-loader', 47 | options: vueConfig 48 | }, 49 | { 50 | test: /\.js$/, 51 | loader: 'babel-loader', 52 | exclude: /node_modules/ 53 | }, 54 | { 55 | test: /\.(png|jpg|gif|svg)$/, 56 | loader: 'file-loader', 57 | options: { 58 | name: '[name].[ext]?[hash]' 59 | } 60 | } 61 | ] 62 | }, 63 | plugins: isProduction 64 | // 确保添加了此插件! 65 | ? [new ExtractTextPlugin({ filename: 'common.[chunkhash].css' })] 66 | : [] 67 | } 68 | -------------------------------------------------------------------------------- /build/webpack.client.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const merge = require('webpack-merge') 3 | const base = require('./webpack.base.config') 4 | const vueConfig = require('./vue-loader.config') 5 | const HTMLPlugin = require('html-webpack-plugin') 6 | const SWPrecachePlugin = require('sw-precache-webpack-plugin') 7 | 8 | const config = merge(base, { 9 | resolve: { 10 | alias: { 11 | 'create-api': './create-api-client.js' 12 | } 13 | }, 14 | plugins: [ 15 | // strip dev-only code in Vue source 16 | new webpack.DefinePlugin({ 17 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), 18 | 'process.env.VUE_ENV': '"client"' 19 | }), 20 | // extract vendor chunks for better caching 21 | new webpack.optimize.CommonsChunkPlugin({ 22 | name: 'vendor' 23 | }), 24 | // generate output HTML 25 | new HTMLPlugin({ 26 | template: 'src/index.template.html' 27 | }) 28 | ] 29 | }) 30 | 31 | if (process.env.NODE_ENV === 'production') { 32 | config.plugins.push( 33 | // minify JS 34 | new webpack.optimize.UglifyJsPlugin({ 35 | compress: { 36 | warnings: false 37 | } 38 | }), 39 | // auto generate service worker 40 | new SWPrecachePlugin({ 41 | cacheId: 'vue-hn', 42 | filename: 'service-worker.js', 43 | dontCacheBustUrlsMatching: /./, 44 | staticFileGlobsIgnorePatterns: [/index\.html$/, /\.map$/] 45 | }) 46 | ) 47 | } 48 | 49 | module.exports = config 50 | -------------------------------------------------------------------------------- /build/webpack.server.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const merge = require('webpack-merge') 3 | const base = require('./webpack.base.config') 4 | const VueSSRPlugin = require('vue-ssr-webpack-plugin') 5 | 6 | module.exports = merge(base, { 7 | target: 'node', 8 | entry: './src/server-entry.js', 9 | output: { 10 | filename: 'server-bundle.js', 11 | libraryTarget: 'commonjs2' 12 | }, 13 | resolve: { 14 | alias: { 15 | 'create-api': './create-api-server.js' 16 | } 17 | }, 18 | externals: Object.keys(require('../package.json').dependencies), 19 | plugins: [ 20 | new webpack.DefinePlugin({ 21 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), 22 | 'process.env.VUE_ENV': '"server"' 23 | }), 24 | new VueSSRPlugin() 25 | ] 26 | }) 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-ssr-starter-kit", 3 | "version": "0.0.0", 4 | "description": "A Vue.js project wuth vue 2.0, vue-router and vuex for server side rendering.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/doabit/vue-ssr-starter-kit.git" 8 | }, 9 | "keywords": [ 10 | "vue", 11 | "vuex", 12 | "vue-router", 13 | "webpack", 14 | "starter", 15 | "server-side", 16 | "boilerplate" 17 | ], 18 | "author": "doabit ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/doabit/vue-ssr-starter-kit/issues" 22 | }, 23 | "homepage": "https://github.com/doabit/vue-ssr-starter-kit#readme", 24 | "scripts": { 25 | "start": "cross-env NODE_ENV=production node server", 26 | "dev": "node server", 27 | "build:client": "cross-env NODE_ENV=production webpack --config ./build/webpack.client.config.js --progress --hide-modules", 28 | "build:server": "cross-env NODE_ENV=production webpack --config ./build/webpack.server.config.js --progress --hide-modules", 29 | "build": "rimraf dist && npm run build:client && npm run build:server", 30 | "unit": "karma start ./test/unit/karma.conf.js --single-run" 31 | }, 32 | "dependencies": { 33 | "axios": "^0.21.2", 34 | "cross-env": "^3.1.4", 35 | "express": "^4.18.2", 36 | "lru-cache": "^4.1.3", 37 | "serialize-javascript": "^3.1.0", 38 | "serve-favicon": "^2.5.0", 39 | "vue": "^2.5.17", 40 | "vue-router": "^2.8.1", 41 | "vue-server-renderer": "^2.5.17", 42 | "vuex": "^2.5.0", 43 | "vuex-router-sync": "^4.3.2" 44 | }, 45 | "devDependencies": { 46 | "babel-core": "^6.26.3", 47 | "babel-eslint": "^7.1.0", 48 | "babel-loader": "^9.1.0", 49 | "babel-preset-es2015": "^6.13.2", 50 | "babel-preset-stage-2": "^6.13.0", 51 | "chai": "^3.5.0", 52 | "css-loader": "^1.0.0", 53 | "debug": "^2.6.9", 54 | "eslint": "^4.18.2", 55 | "eslint-config-standard": "^6.0.0-beta.2", 56 | "eslint-friendly-formatter": "^2.0.6", 57 | "eslint-loader": "^1.5.0", 58 | "eslint-plugin-html": "^3.0.2", 59 | "eslint-plugin-promise": "^3.8.0", 60 | "eslint-plugin-standard": "^2.0.0", 61 | "extract-text-webpack-plugin": "2.1.2", 62 | "file-loader": "^6.2.0", 63 | "html-webpack-plugin": "^5.5.0", 64 | "karma": "^6.3.16", 65 | "karma-mocha": "^1.3.0", 66 | "karma-phantomjs-launcher": "^1.0.4", 67 | "karma-sinon-chai": "^1.3.4", 68 | "karma-sourcemap-loader": "^0.3.7", 69 | "karma-spec-reporter": "0.0.26", 70 | "karma-webpack": "^3.0.5", 71 | "lodash": "^4.17.21", 72 | "mocha": "^10.1.0", 73 | "phantomjs-prebuilt": "^2.1.16", 74 | "rimraf": "^2.6.2", 75 | "sinon": "^1.17.5", 76 | "sinon-chai": "^2.14.0", 77 | "stylus": "^0.54.5", 78 | "stylus-loader": "^7.1.0", 79 | "sw-precache-webpack-plugin": "^0.9.1", 80 | "vue-loader": "^12.0.0", 81 | "vue-ssr-webpack-plugin": "^1.0.2", 82 | "vue-template-compiler": "^2.5.17", 83 | "webpack": "^5.75.0", 84 | "webpack-dev-middleware": "^1.12.2", 85 | "webpack-hot-middleware": "^2.24.0", 86 | "webpack-merge": "^4.1.4" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const express = require('express') 4 | const favicon = require('serve-favicon') 5 | const resolve = file => path.resolve(__dirname, file) 6 | 7 | const isProd = process.env.NODE_ENV === 'production' 8 | 9 | const app = express() 10 | 11 | let renderer 12 | if (isProd) { 13 | // In production: create server renderer using server bundle and index HTML 14 | // template from real fs. 15 | // The server bundle is generated by vue-ssr-webpack-plugin. 16 | const bundle = require('./dist/vue-ssr-bundle.json') 17 | // src/index.template.html is processed by html-webpack-plugin to inject 18 | // build assets and output as dist/index.html. 19 | const template = fs.readFileSync(resolve('./dist/index.html'), 'utf-8') 20 | renderer = createRenderer(bundle, template) 21 | } else { 22 | // In development: setup the dev server with watch and hot-reload, 23 | // and create a new renderer on bundle / index template update. 24 | require('./build/dev-server')(app, (bundle, template) => { 25 | renderer = createRenderer(bundle, template) 26 | }) 27 | } 28 | 29 | function createRenderer (bundle, template) { 30 | // https://github.com/vuejs/vue/blob/dev/packages/vue-server-renderer/README.md#why-use-bundlerenderer 31 | return require('vue-server-renderer').createBundleRenderer(bundle, { 32 | template, 33 | cache: require('lru-cache')({ 34 | max: 1000, 35 | maxAge: 1000 * 60 * 15 36 | }) 37 | }) 38 | } 39 | 40 | const serve = (path, cache) => express.static(resolve(path), { 41 | maxAge: cache && isProd ? 60 * 60 * 24 * 30 : 0 42 | }) 43 | 44 | 45 | app.use('/dist', serve('./dist', true)) 46 | app.use(favicon(path.resolve(__dirname, 'src/assets/logo.png'))) 47 | app.use('/service-worker.js', serve('./dist/service-worker.js')) 48 | 49 | app.get('*', (req, res) => { 50 | if (!renderer) { 51 | return res.end('waiting for compilation... refresh in a moment.') 52 | } 53 | 54 | const s = Date.now() 55 | 56 | res.setHeader("Content-Type", "text/html") 57 | 58 | const errorHandler = err => { 59 | if (err && err.code === 404) { 60 | res.status(404).end('404 | Page Not Found') 61 | } else { 62 | // Render Error Page or Redirect 63 | res.status(500).end('500 | Internal Server Error') 64 | console.error(`error during render : ${req.url}`) 65 | console.error(err) 66 | } 67 | } 68 | 69 | renderer.renderToStream({ url: req.url }) 70 | .on('error', errorHandler) 71 | .on('end', () => console.log(`whole request: ${Date.now() - s}ms`)) 72 | .pipe(res) 73 | }) 74 | 75 | const port = process.env.PORT || 3000 76 | app.listen(port, () => { 77 | console.log(`server started at http://localhost:${port}`) 78 | }) 79 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import createStore from './vuex/store' 4 | import createRouter from './router' 5 | import { sync } from 'vuex-router-sync' 6 | 7 | export function createApp () { 8 | const store = createStore() 9 | const router = createRouter() 10 | // 同步路由状态(route state)到 store 11 | sync(store, router) 12 | // 创建应用程序实例,将 router 和 store 注入 13 | const app = new Vue({ 14 | router, 15 | store, 16 | render: h => h(App) 17 | }) 18 | // 暴露 app, router 和 store。 19 | return { app, router, store } 20 | } 21 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doabit/vue-ssr-starter-kit/be13c643bb238819540e8d139dde6882df99d4c9/src/assets/logo.png -------------------------------------------------------------------------------- /src/client-entry.js: -------------------------------------------------------------------------------- 1 | import { createApp } from './app' 2 | 3 | const { app, router, store } = createApp() 4 | 5 | if (window.__INITIAL_STATE__) { 6 | store.replaceState(window.__INITIAL_STATE__) 7 | } 8 | 9 | router.onReady(() => { 10 | // Add router hook for handling asyncData. 11 | // Doing it after initial route is resolved so that we don't double-fetch 12 | // the data that we already have. Using router.beforeResolve() so that all 13 | // async components are resolved. 14 | router.beforeResolve((to, from, next) => { 15 | const matched = router.getMatchedComponents(to) 16 | const prevMatched = router.getMatchedComponents(from) 17 | 18 | let diffed = false 19 | const activated = matched.filter((c, i) => { 20 | return diffed || (diffed = (prevMatched[i] !== c)) 21 | }) 22 | if (!activated.length) { 23 | return next() 24 | } 25 | // start loading indicator 26 | Promise.all(activated.map(c => { 27 | if (c.asyncData) { 28 | return c.asyncData({ store, route: to }) 29 | } 30 | })).then(() => { 31 | // stop loading indicator 32 | next() 33 | }).catch(next) 34 | }) 35 | app.$mount('#app') 36 | }) 37 | 38 | // service worker 39 | if (window.location.protocol === 'https:' && navigator.serviceWorker) { 40 | navigator.serviceWorker.register('/service-worker.js') 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Todo.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 22 | -------------------------------------------------------------------------------- /src/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vue-ssr-starter-kit 6 | 7 | 8 | 9 | <% for (var chunk of webpack.chunks) { 10 | for (var file of chunk.files) { 11 | if (file.match(/\.(js|css)$/)) { %> 12 | <% }}} %> 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Home from '../views/Home.vue' 3 | import About from '../views/About.vue' 4 | import Counter from '../views/Counter.vue' 5 | import Topics from '../views/Topics.vue' 6 | import VueRouter from 'vue-router' 7 | 8 | Vue.use(VueRouter) 9 | 10 | export default function () { 11 | return new VueRouter({ 12 | mode: 'history', 13 | base: __dirname, 14 | routes: [ 15 | { path: '/', component: Home }, 16 | { path: '/Topics', component: Topics }, 17 | { path: '/Counter', component: Counter }, 18 | { path: '/About', component: About } 19 | ] 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/server-entry.js: -------------------------------------------------------------------------------- 1 | import { createApp } from './app' 2 | 3 | export default context => { 4 | return new Promise((resolve, reject) => { 5 | const { app, router, store } = createApp() 6 | router.push(context.url) 7 | router.onReady(() => { 8 | const matchedComponents = router.getMatchedComponents() 9 | if (!matchedComponents.length) { 10 | return reject({ code: 404 }) 11 | } 12 | // Call fetchData hooks on components matched by the route. 13 | // A preFetch hook dispatches a store action and returns a Promise, 14 | // which is resolved when the action is complete and store state has been 15 | // updated. 16 | Promise.all(matchedComponents.map(Component => { 17 | if (Component.asyncData) { 18 | return Component.asyncData({ 19 | store, 20 | route: router.currentRoute 21 | }) 22 | } 23 | })).then(() => { 24 | // After all preFetch hooks are resolved, our store is now 25 | // filled with the state needed to render the app. 26 | // Expose the state on the render context, and let the request handler 27 | // inline the state in the HTML response. This allows the client-side 28 | // store to pick-up the server-side state without having to duplicate 29 | // the initial data fetching on the client. 30 | context.state = store.state 31 | resolve(app) 32 | }).catch(reject) 33 | }, reject) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/Counter.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 37 | 38 | 49 | -------------------------------------------------------------------------------- /src/views/Topics.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | -------------------------------------------------------------------------------- /src/vuex/actions.js: -------------------------------------------------------------------------------- 1 | import request from 'axios' 2 | 3 | request.defaults.baseURL = 'http://jsonplaceholder.typicode.com/' 4 | 5 | export const getTopics = ({ commit, state }) => { 6 | return request.get('posts').then((response) => { 7 | if (response.statusText === 'OK') { 8 | commit('TOPICS_LIST', response.data) 9 | } 10 | }).catch((error) => { 11 | console.log(error) 12 | }) 13 | } 14 | 15 | export const getIndexData = ({ commit, state }) => { 16 | const requestUsers = request.get('users') 17 | const requestTodos = request.get('todos') 18 | return request.all([requestUsers, requestTodos]).then(request.spread((...responses) => { 19 | const requestUsers = responses[0] 20 | const responseTodos = responses[1] 21 | if (requestUsers.statusText === 'OK') { 22 | commit('INDEX_USERS_LIST', requestUsers.data) 23 | } 24 | if (responseTodos.statusText === 'OK') { 25 | commit('INDEX_TODOS_LIST', responseTodos.data) 26 | } 27 | // use/access the results 28 | })).catch(errors => { 29 | // react on errors. 30 | console.log(errors) 31 | }) 32 | } 33 | 34 | export const increment = ({ commit }) => commit('INCREMENT') 35 | export const decrement = ({ commit }) => commit('DECREMENT') 36 | -------------------------------------------------------------------------------- /src/vuex/getters.js: -------------------------------------------------------------------------------- 1 | export const getTopics = state => state.topics 2 | export const getCount = state => state.count 3 | 4 | export const getIndexData = state => state.indexData 5 | -------------------------------------------------------------------------------- /src/vuex/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import * as actions from './actions' 4 | import * as getters from './getters' 5 | 6 | Vue.use(Vuex) 7 | 8 | const state = { 9 | indexData: { todos: [], users: [] }, 10 | topics: [], 11 | count: 0 12 | } 13 | 14 | const mutations = { 15 | TOPICS_LIST: (state, topics) => { 16 | state.topics = topics 17 | }, 18 | 19 | INDEX_TODOS_LIST: (state, data) => { 20 | Vue.set(state.indexData, 'todos', data) 21 | }, 22 | 23 | INDEX_USERS_LIST: (state, data) => { 24 | Vue.set(state.indexData, 'users', data) 25 | }, 26 | 27 | INCREMENT: (state) => { 28 | state.count++ 29 | }, 30 | 31 | DECREMENT: (state) => { 32 | state.count-- 33 | } 34 | } 35 | 36 | export default function () { 37 | return new Vuex.Store({ 38 | state, 39 | actions, 40 | mutations, 41 | getters 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | // require all test files (files that ends with .spec.js) 2 | var testsContext = require.context('./specs', true, /\.spec$/) 3 | testsContext.keys().forEach(testsContext) 4 | -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('../../build/webpack.base.config') 2 | const webpack = require('webpack') 3 | const webpackConfig = Object.assign({}, baseConfig, { 4 | devtool: '#inline-source-map', 5 | plugins: [ 6 | new webpack.DefinePlugin({ 7 | 'process.env.NODE_ENV': '"test"' 8 | }) 9 | ] 10 | }) 11 | 12 | // no need for app entry during tests 13 | delete webpackConfig.entry 14 | 15 | module.exports = function (config) { 16 | config.set({ 17 | browsers: ['PhantomJS'], 18 | frameworks: ['mocha', 'sinon-chai'], 19 | reporters: ['spec'], 20 | files: ['./index.js'], 21 | preprocessors: { 22 | './index.js': ['webpack', 'sourcemap'] 23 | }, 24 | webpack: webpackConfig, 25 | webpackMiddleware: { 26 | noInfo: true 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /test/unit/specs/About.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import About from 'views/About.vue' 3 | 4 | describe('About.vue', () => { 5 | it('should render correct about contents', () => { 6 | const vm = new Vue(About).$mount() 7 | expect(vm.$el.textContent).to.contain('About') 8 | }) 9 | }) 10 | --------------------------------------------------------------------------------