Analysis of a Vue Project Structure
Introduction
When developing Vue projects, we typically use the vue init webpack my_project command to create a project.
The generated project can be quite complex, with many files that can be difficult to grasp.
To day, I've decided to thoroughly understand all the files generated by vue init webpack so I can move forward.
Directory Structure
The directory structure created by vue init webpack is as follows:

We mainly focus on the build and config directories.
The build directory contains files related to compilation, and the config directory also contains configuration files related to compilation. Files like webpack.base.conf.js are also configuraton files, so the directory structure feels a bit awkward.
But awkward or not, since this is the official directory structure, we have to adapt to it. Understanding the purpose of each file makes the directory structure less of a concern.
Module Dependency Diagram

The dependencies between modules in the build and config directory are quite complex. I'll analyze each module one by one.
build/build.js
This file is not complex. It mainly reads build/webpack.prod.conf.js and compiles with webpack.
'use strict'
require('./check-versions')()
process.env.NODE_ENV = 'production'
const ora = require('ora') // ora module implements loading effects in Node.js command line
const rm = require('rimraf') // module for deleting folders
const path = require('path') // module for file paths
const chalk = require('chalk') // module for modifying string styles in console (font style, color, background)
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf') // Since it's for production, import production config directly
// Start loading spinner
const spinner = ora('building for production...')
spinner.start()
// Delete output directory
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
// Start webpack build
webpack(webpackConfig, (err, stats) => {
// Stop spinner
spinner.stop()
if (err) throw err
// Output build information
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
chunks: false,
chunkModules: false
}) + '\n\n')
if (stats.hasErrors()) {
console.log(chalk.red(' Build failed with errors.\n'))
process.exit(1)
}
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
})
build/webpack.prod.conf.js
This is the configuration file for the production environment. It builds upon build/webpack.base.conf.js by adding styleLoaders and various optimization plugins.
'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const env = require('../config/prod.env')
const webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true,
usePostCSS: true
})
},
devtool: config.build.productionSourceMap ? config.build.devtool : false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
// Replaces specified variables with constants at compile time.
new webpack.DefinePlugin({
'process.env': env
}),
// Uglifies and optimizes JS files.
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false
}
},
sourceMap: config.build.productionSourceMap,
parallel: true
}),
// Extracts CSS into separate files.
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'),
allChunks: true,
}),
// Compresses extracted CSS files.
new OptimizeCSSPlugin({
cssProcessorOptions: config.build.productionSourceMap
? { safe: true, map: { inline: false } }
: { safe: true }
}),
// Generates HTML entry file and dynamically adds hashed external resources.
new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
},
chunksSortMode: 'dependency'
}),
// Keeps module ids stable when vendor modules don't change.
new webpack.HashedModuleIdsPlugin(),
// Enables scope hoisting for smaller and faster code.
new webpack.optimize.ModuleConcatenationPlugin(),
// Extracts third-party libraries and common modules to avoid large bundle sizes.
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks(module) {
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: 3
}),
// Copies files or directories to the build directory (e.g., static folder).
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
// Add compression plugin if gzip is enabled
if (config.build.productionGzip) {
const CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
threshold: 10240,
minRatio: 0.8
})
)
}
// Add bundle analyzer plugin if report is enabled
if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig
build/webpack.dev.conf.js
'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')
const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)
const devWebpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.dev.cssSourceMap,
usePostCSS: true
// Compared to webpack.prod.conf.js, extract is omitted here.
})
},
devtool: config.dev.devtool,
/**
* Dev server configuration
*/
devServer: {
clientLogLevel: 'warning',
historyApiFallback: {
rewrites: [
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
],
},
hot: true,
contentBase: false, // since we use CopyWebpackPlugin.
compress: true,
host: HOST || config.dev.host,
port: PORT || config.dev.port,
open: config.dev.autoOpenBrowser,
overlay: config.dev.errorOverlay
? { warnings: false, errors: true }
: false,
publicPath: config.dev.assetsPublicPath,
proxy: config.dev.proxyTable,
quiet: true, // necessary for FriendlyErrorsPlugin
watchOptions: {
poll: config.dev.poll,
}
},
/**
* Plugins
*
* Compared to webpack.prod.conf.js, the following are omitted:
* * UglifyJsPlugin: JS compression not needed in development.
* * ExtractTextPlugin: CSS extraction not needed in development.
* * OptimizeCSSPlugin: CSS compression not needed in development.
* * HashedModuleIdsPlugin: Hashing not needed in development.
* * ModuleConcatenationPlugin: Scope hoisting not needed in development.
* The following are added:
* * HotModuleReplacementPlugin: Hot reloading.
* * NamedModulesPlugin: Shows relative paths for modules when HMR is enabled.
* * NoEmitOnErrorsPlugin: Skips output phase when compilation errors occur.
*/
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.dev.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
module.exports = new Promise((resolve, reject) => {
portfinder.basePort = process.env.PORT || config.dev.port
portfinder.getPort((err, port) => {
if (err) {
reject(err)
} else {
// Publish the new port, necessary for e2e tests.
process.env.PORT = port
// Add port to devServer config
devWebpackConfig.devServer.port = port
// Add FriendlyErrorsPlugin for better error display
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
compilationSuccessInfo: {
messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
},
onErrors: config.dev.notifyOnErrors
? utils.createNotifierCallback()
: undefined
}))
// Return config
resolve(devWebpackConfig)
}
})
})
build/webpack.base.conf.js
This is the common configuration file for both development and production environments. Besides specifying entry and output, it also adds various loaders for Vue, JS, images, audio, and font files.
'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')
function resolve(dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
context: path.resolve(__dirname, '../'),
// Entry point
entry: {
app: './src/main.js'
},
// Output
output: {
path: config.build.assetsRoot, // Path to output, i.e., directory containing static and index.html
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
// Custom module resolution
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
// Loaders for transforming files
module: {
rules: [
// Vue files
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
// JS files
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
},
// Image files
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
// Audio files
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
// Font files
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
},
// Node configuration
node: {
setImmediate: false,
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
}
}
build/utils.js
Provides four methods:
assetsPath(_path): Returns the path for the given asset name.cssLoaders(options): Returns a list of loaders for various style files.styleLoaders(options): Returns a list of rules for various style files.createNotifierCallback(): Returns a function for handling scaffold errors.
'use strict'
const path = require('path')
const config = require('../config')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const packageConfig = require('../package.json')
/**
* Returns a clean relative root path for an asset
* @param {*} _path
* @returns
*/
exports.assetsPath = function (_path) {
const assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
/**
* Returns loader list for various style file extensions
* @param {*} options
* @returns
*/
exports.cssLoaders = function (options) {
options = options || {}
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: options.sourceMap
}
}
const postcssLoader = {
loader: 'postcss-loader',
options: {
sourceMap: options.sourceMap
}
}
function generateLoaders(loader, loaderOptions) {
const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
/**
* Generates rule list for various style files
* @param {*} options
* @returns
*/
exports.styleLoaders = function (options) {
const output = []
const loaders = exports.cssLoaders(options)
for (const extension in loaders) {
const loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}
/**
* Returns a function for handling scaffold errors
* @returns
*/
exports.createNotifierCallback = () => {
const notifier = require('node-notifier')
return (severity, errors) => {
if (severity !== 'error') return
const error = errors[0]
const filename = error.file && error.file.split('!').pop()
notifier.notify({
title: packageConfig.name,
message: severity + ': ' + error.name,
subtitle: filename || '',
icon: path.join(__dirname, 'logo.png')
})
}
}
build/check-version.js
'use strict'
const chalk = require('chalk')
const semver = require('semver') // Module for version comparison
const packageConfig = require('../package.json')
const shell = require('shelljs') // Module for executing Unix shell commands
function exec(cmd) {
return require('child_process').execSync(cmd).toString().trim()
}
// Version requirement list
const versionRequirements = [
{
name: 'node',
currentVersion: semver.clean(process.version),
versionRequirement: packageConfig.engines.node
}
]
// Add npm version requirement if installed
if (shell.which('npm')) {
versionRequirements.push({
name: 'npm',
currentVersion: exec('npm --version'),
versionRequirement: packageConfig.engines.npm
})
}
module.exports = function () {
const warnings = []
for (let i = 0; i < versionRequirements.length; i++) {
const mod = versionRequirements[i]
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' +
chalk.red(mod.currentVersion) + ' should be ' +
chalk.green(mod.versionRequirement)
)
}
}
if (warnings.length) {
console.log('')
console.log(chalk.yellow('To use this template, you must update following to modules:'))
console.log()
for (let i = 0; i < warnings.length; i++) {
const warning = warnings[i]
console.log(' ' + warning)
}
console.log()
process.exit(1)
}
}
build/vue-loader.conf.js
'use strict'
const utils = require('./utils')
const config = require('../config')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction
? config.build.productionSourceMap
: config.dev.cssSourceMap
module.exports = {
loaders: utils.cssLoaders({
sourceMap: sourceMapEnabled,
extract: isProduction
}),
cssSourceMap: sourceMapEnabled,
cacheBusting: config.dev.cacheBusting,
transformToRequire: {
video: ['src', 'poster'],
source: 'src',
img: 'src',
image: 'xlink:href'
}
}
config/index.js
'use strict'
const path = require('path')
module.exports = {
dev: {
/**
* Paths
*/
assetsSubDirectory: 'static',
assetsPublicPath: '/',
/**
* Proxy (for cross-origin requests)
*/
proxyTable: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
ws: true,
pathRewrite: {
'^/api': '/english_reading'
}
}
},
/**
* Server
*/
host: 'localhost',
port: 8080,
autoOpenBrowser: false,
errorOverlay: true,
notifyOnErrors: true,
poll: false,
/**
* Source Maps
*/
devtool: 'cheap-module-eval-source-map',
cacheBusting: true,
cssSourceMap: true
},
build: {
/**
* Paths
*/
index: path.resolve(__dirname, '../../english_reading/src/main/webapp/static/html/index.html'),
assetsRoot: path.resolve(__dirname, '../../english_reading/src/main/webapp'),
assetsSubDirectory: 'static',
assetsPublicPath: '/english_reading',
/**
* Source Maps
*/
productionSourceMap: true,
devtool: '#source-map',
/**
* Gzip
*/
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
bundleAnalyzerReport: process.env.npm_config_report
}
}
config/prod.env.js
This file is very simple; it exports an object with a NODE_ENV property set to '"production"'.
It is used by build/webpack.prod.conf.js to set process.env.
'use strict'
module.exports = {
NODE_ENV: '"production"'
}
config/dev.env.js
Compared to config/prod.env.js, it has a NODE_ENV property and merges with config/prod.env.js.
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})
References
vue-loader.conf.jsin the build folder