Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Analysis of a Vue Project Structure

Tech May 18 3

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:

Directory Structure

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

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

  1. vue-loader.conf.js in the build folder

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.