Repository: vuejs/vue-hackernews-2.0 Branch: master Commit: 98399b55c6f1 Files: 34 Total size: 48.3 KB Directory structure: gitextract_ha5ywnnr/ ├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── build/ │ ├── setup-dev-server.js │ ├── webpack.base.config.js │ ├── webpack.client.config.js │ └── webpack.server.config.js ├── manifest.json ├── package.json ├── server.js └── src/ ├── App.vue ├── api/ │ ├── create-api-client.js │ ├── create-api-server.js │ └── index.js ├── app.js ├── components/ │ ├── Comment.vue │ ├── Item.vue │ ├── ProgressBar.vue │ └── Spinner.vue ├── entry-client.js ├── entry-server.js ├── index.template.html ├── router/ │ └── index.js ├── store/ │ ├── actions.js │ ├── getters.js │ ├── index.js │ └── mutations.js ├── util/ │ ├── filters.js │ └── title.js └── views/ ├── CreateListView.js ├── ItemList.vue ├── ItemView.vue └── UserView.vue ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ ["env", { "modules": false }] ], "plugins": [ "syntax-dynamic-import" ] } ================================================ FILE: .gitignore ================================================ .DS_Store node_modules/ dist/ npm-debug.log yarn-error.log .idea *.iml ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2013-present, Yuxi (Evan) You Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # vue-hackernews-2.0 HackerNews clone built with Vue 2.0 + vue-router + vuex, with server-side rendering.
## Features > Note: in practice, it is unnecessary to code-split for an app of this size (where each async chunk is only a few kilobytes), nor is it optimal to extract an extra CSS file (which is only 1kb) -- they are used simply because this is a demo app showcasing all the supported features. - Server Side Rendering - Vue + vue-router + vuex working together - Server-side data pre-fetching - Client-side state & DOM hydration - Automatically inlines CSS used by rendered components only - Preload / prefetch resource hints - Route-level code splitting - Progressive Web App - App manifest - Service worker - 100/100 Lighthouse score - Single-file Vue Components - Hot-reload in development - CSS extraction for production - Animation - Effects when switching route views - Real-time list updates with FLIP Animation ## A Note on Performance This is a demo primarily aimed at explaining how to build a server-side rendered Vue app, as a companion to our SSR documentation. There are a few things we probably won't do in production if we were optimizing for performance, for example: - This demo uses the Firebase-based HN API to showcase real-time updates, but the Firebase API also comes with a larger bundle, more JavaScript to parse on the client, and doesn't offer an efficient way to batch-fetch pages of items, so it impacts performance quite a bit on a cold start or cache miss. - In practice, it is unnecessary to code-split for an app of this size (where each async chunk is only a few kilobytes so the extra request isn't really worth it), nor is it optimal to extract an extra CSS file (which is only 1kb). It is therefore not recommended to use this app as a reference for Vue SSR performance - instead, do your own benchmarking, and make sure to measure and optimize based on your actual app constraints. ## Architecture Overview
**A detailed Vue SSR guide can be found [here](https://ssr.vuejs.org).**
## Build Setup
**Requires Node.js 7+**
``` bash
# install dependencies
npm install # or yarn
# serve in dev mode, with hot reload at localhost:8080
npm run dev
# build for production
npm run build
# serve in production mode
npm start
```
## License
MIT
================================================
FILE: build/setup-dev-server.js
================================================
const fs = require('fs')
const path = require('path')
const MFS = require('memory-fs')
const webpack = require('webpack')
const chokidar = require('chokidar')
const clientConfig = require('./webpack.client.config')
const serverConfig = require('./webpack.server.config')
const readFile = (fs, file) => {
try {
return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8')
} catch (e) {}
}
module.exports = function setupDevServer (app, templatePath, cb) {
let bundle
let template
let clientManifest
let ready
const readyPromise = new Promise(r => { ready = r })
const update = () => {
if (bundle && clientManifest) {
ready()
cb(bundle, {
template,
clientManifest
})
}
}
// 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()
})
// modify client config to work with hot middleware
clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app]
clientConfig.output.filename = '[name].js'
clientConfig.plugins.push(
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
)
// dev middleware
const clientCompiler = webpack(clientConfig)
const devMiddleware = require('webpack-dev-middleware')(clientCompiler, {
publicPath: clientConfig.output.publicPath,
noInfo: true
})
app.use(devMiddleware)
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'
))
update()
})
// hot middleware
app.use(require('webpack-hot-middleware')(clientCompiler, { heartbeat: 5000 }))
// watch and update server renderer
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
// read bundle generated by vue-ssr-webpack-plugin
bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))
update()
})
return readyPromise
}
================================================
FILE: build/webpack.base.config.js
================================================
const path = require('path')
const webpack = require('webpack')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')
const isProd = process.env.NODE_ENV === 'production'
module.exports = {
devtool: isProd
? false
: '#cheap-module-source-map',
output: {
path: path.resolve(__dirname, '../dist'),
publicPath: '/dist/',
filename: '[name].[chunkhash].js'
},
resolve: {
alias: {
'public': path.resolve(__dirname, '../public')
}
},
module: {
noParse: /es6-promise\.js$/, // avoid webpack shimming process
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false
}
}
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: 'url-loader',
options: {
limit: 10000,
name: '[name].[ext]?[hash]'
}
},
{
test: /\.styl(us)?$/,
use: isProd
? ExtractTextPlugin.extract({
use: [
{
loader: 'css-loader',
options: { minimize: true }
},
'stylus-loader'
],
fallback: 'vue-style-loader'
})
: ['vue-style-loader', 'css-loader', 'stylus-loader']
},
]
},
performance: {
hints: false
},
plugins: isProd
? [
new VueLoaderPlugin(),
new webpack.optimize.UglifyJsPlugin({
compress: { warnings: false }
}),
new webpack.optimize.ModuleConcatenationPlugin(),
new ExtractTextPlugin({
filename: 'common.[chunkhash].css'
})
]
: [
new VueLoaderPlugin(),
new FriendlyErrorsPlugin()
]
}
================================================
FILE: build/webpack.client.config.js
================================================
const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const SWPrecachePlugin = require('sw-precache-webpack-plugin')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const config = merge(base, {
entry: {
app: './src/entry-client.js'
},
resolve: {
alias: {
'create-api': './create-api-client.js'
}
},
plugins: [
// strip dev-only code in Vue source
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.VUE_ENV': '"client"'
}),
// extract vendor chunks for better caching
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module) {
// a module is extracted into the vendor chunk if...
return (
// it's inside node_modules
/node_modules/.test(module.context) &&
// and not a CSS file (due to extract-text-webpack-plugin limitation)
!/\.css$/.test(module.request)
)
}
}),
// extract webpack runtime & manifest to avoid vendor chunk hash changing
// on every build.
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest'
}),
new VueSSRClientPlugin()
]
})
if (process.env.NODE_ENV === 'production') {
config.plugins.push(
// auto generate service worker
new SWPrecachePlugin({
cacheId: 'vue-hn',
filename: 'service-worker.js',
minify: true,
dontCacheBustUrlsMatching: /./,
staticFileGlobsIgnorePatterns: [/\.map$/, /\.json$/],
runtimeCaching: [
{
urlPattern: '/',
handler: 'networkFirst'
},
{
urlPattern: /\/(top|new|show|ask|jobs)/,
handler: 'networkFirst'
},
{
urlPattern: '/item/:id',
handler: 'networkFirst'
},
{
urlPattern: '/user/:id',
handler: 'networkFirst'
}
]
})
)
}
module.exports = config
================================================
FILE: build/webpack.server.config.js
================================================
const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const nodeExternals = require('webpack-node-externals')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
module.exports = merge(base, {
target: 'node',
devtool: '#source-map',
entry: './src/entry-server.js',
output: {
filename: 'server-bundle.js',
libraryTarget: 'commonjs2'
},
resolve: {
alias: {
'create-api': './create-api-server.js'
}
},
// https://webpack.js.org/configuration/externals/#externals
// https://github.com/liady/webpack-node-externals
externals: nodeExternals({
// do not externalize CSS files in case we need to import it from a dep
whitelist: /\.css$/
}),
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.VUE_ENV': '"server"'
}),
new VueSSRServerPlugin()
]
})
================================================
FILE: manifest.json
================================================
{
"name": "Vue Hackernews 2.0",
"short_name": "Vue HN",
"icons": [{
"src": "/public/logo-120.png",
"sizes": "120x120",
"type": "image/png"
}, {
"src": "/public/logo-144.png",
"sizes": "144x144",
"type": "image/png"
}, {
"src": "/public/logo-152.png",
"sizes": "152x152",
"type": "image/png"
}, {
"src": "/public/logo-192.png",
"sizes": "192x192",
"type": "image/png"
}, {
"src": "/public/logo-256.png",
"sizes": "256x256",
"type": "image/png"
}, {
"src": "/public/logo-384.png",
"sizes": "384x384",
"type": "image/png"
}, {
"src": "/public/logo-512.png",
"sizes": "512x512",
"type": "image/png"
}],
"start_url": "/",
"background_color": "#f2f3f5",
"display": "standalone",
"theme_color": "#f60"
}
================================================
FILE: package.json
================================================
{
"name": "vue-hackernews-2.0",
"description": "A Vue.js project",
"author": "Evan You
{{ item.kids ? item.descendants + ' comments' : 'No comments yet.' }}