1. 程式人生 > >vue-cli 中的 webpack 配置詳解

vue-cli 中的 webpack 配置詳解

class api 靜態文件 分析 錯誤提示 後端 nodejs start 三方庫

本篇文章主要介紹了 vue-cli 2.8.2 中的 webpack 配置詳解, 做個學習筆記

版本

vue-cli 2.8.1 (終端通過 vue -V 可查看)
vue 2.2.2
webpack 2.2.1

目錄結構

├── README.md
├── build
│  ├── build.js
│  ├── check-versions.js
│  ├── dev-client.js
│  ├── dev-server.js
│  ├── utils.js
│  ├── vue-loader.conf.js
│  ├── webpack.base.conf.js
│  ├── webpack.dev.conf.js
│  └── webpack.prod.conf.js
├── config
│  ├── dev.env.js
│  ├── index.js
│  └── prod.env.js
├── index.html
├── package.json
├── src
│  ├── App.vue
│  ├── assets
│  │  └── logo.png
│  ├── components
│  │  └── Hello.vue
│  └── main.js
└── static

webpack 配置

主要對 build 目錄下的 webpack 配置做詳細分析

  • webpack.base.conf.js
    入口文件

    entry: {
     app: '.src/main.js'
    }
  • 輸出文件 output
    config 的配置在 config/index.js 文件中

    output: {
     path: config.build.assetsRoot, //導出目錄的絕對路徑
     filename: '[name].js', //導出文件的文件名
     publicPath: process.env.NODE_ENV === 'production'? config.build.assetsPublicPath : config.dev.assetsPublicPath //生產模式或開發模式下html、js等文件內部引用的公共路徑
    }
  • 文件解析 resolve
    主要設置模塊如何被解析

    resolve: {
     extensions: ['.js', '.vue', '.json'], //自動解析確定的拓展名,使導入模塊時不帶拓展名
     alias: {  // 創建import或require的別名
      'vue$': 'vue/dist/vue.esm.js', 
      '@': resolve('src')
     }
    }
  • 模塊解析 module
    如何處理項目不同類型的模塊

    module: {
     rules: [
      {
       test: /\.vue$/, // vue文件後綴
       loader: 'vue-loader', //使用vue-loader處理
       options: vueLoaderConfig //options是對vue-loader做的額外選項配置
      },
      {
       test: /\.js$/, // js文件後綴
       loader: 'babel-loader', //使用babel-loader處理
       include: [resolve('src'), resolve('test')] //必須處理包含src和test文件夾
      },
      {
       test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, //圖片後綴
       loader: 'url-loader', //使用url-loader處理
       query: { // query是對loader做額外的選項配置
        limit: 10000, //圖片小於10000字節時以base64的方式引用
        name: utils.assetsPath('img/[name].[hash:7].[ext]') //文件名為name.7位hash值.拓展名
       }
      },
      {
       test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, //字體文件
       loader: 'url-loader', //使用url-loader處理
       query: {
        limit: 10000, //字體文件小於1000字節的時候處理方式
        name: utils.assetsPath('fonts/[name].[hash:7].[ext]') //文件名為name.7位hash值.拓展名
       }
      }
     ]
    }
    

    註: 關於 query 僅由於兼容性原因而存在。請使用 options 代替。

  • webpack.dev.conf.js
    開發環境下的 webpack 配置,通過 merge 方法合並 webpack.base.conf.js 基礎配置

    var merge = require('webpack-merge')
    var baseWebpackConfig = require('./webpack.base.conf')
    module.exports = merge(baseWebpackConfig, {})
  • 模塊配置
    js module: { //通過傳入一些配置來獲取rules配置,此處傳入了sourceMap: false,表示不生成sourceMap rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) }
  • 在 util.styleLoaders 中的配置如下

    exports.styleLoaders = function (options) {
     var output = [] //定義返回的數組,數組中保存的是針對各類型的樣式文件的處理方式
     var loaders = exports.cssLoaders(options) // 調用cssLoaders方法返回各類型的樣式對象(css: loader)
     for (var extension in loaders) { //循環遍歷loaders
      var loader = loaders[extension] //根據遍歷獲得的key(extension)來得到value(loader)
      output.push({   //
       test: new RegExp('\\.' + extension + '$'), // 處理的文件類型
       use: loader //用loader來處理,loader來自loaders[extension]
      })
     }
     return output
    }
  • 上面的代碼中調用了 exports.cssLoaders(options), 用來返回針對各類型的樣式文件的處理方式, 具體實現如下

    exports.cssLoaders = function (options) {
     options = options || {}
    
     var cssLoader = { 
      loader: 'css-loader',
      options: { //options是loader的選項配置 
       minimize: process.env.NODE_ENV === 'production', //生成環境下壓縮文件
       sourceMap: options.sourceMap //根據參數是否生成sourceMap文件
      }
     }
     function generateLoaders (loader, loaderOptions) { //生成loader
      var loaders = [cssLoader] // 默認是css-loader
      if (loader) { // 如果參數loader存在
       loaders.push({
        loader: loader + '-loader',
        options: Object.assign({}, loaderOptions, { //將loaderOptions和sourceMap組成一個對象
         sourceMap: options.sourceMap
        })
       })
      }
      if (options.extract) { // 如果傳入的options存在extract且為true
       return ExtractTextPlugin.extract({ //ExtractTextPlugin分離js中引入的css文件
        use: loaders, //處理的loader
        fallback: 'vue-style-loader' //沒有被提取分離時使用的loader
       })
      } else {
       return ['vue-style-loader'].concat(loaders)
      }
     }
     return { //返回css類型對應的loader組成的對象 generateLoaders()來生成loader
      css: generateLoaders(),
      postcss: generateLoaders(),
      less: generateLoaders('less'),
      sass: generateLoaders('sass', { indentedSyntax: true }),
      scss: generateLoaders('sass'),
      stylus: generateLoaders('stylus'),
      styl: generateLoaders('stylus')
     }
    }
    
  • 插件配置

    plugins: [
    new webpack.DefinePlugin({ // 編譯時配置的全局變量
    'process.env': config.dev.env //當前環境為開發環境
    }),
    new webpack.HotModuleReplacementPlugin(), //熱更新插件
    new webpack.NoEmitOnErrorPlugin(), //不觸發錯誤,即編譯後運行的包正常運行
    new HtmlWebpackPlugin({ //自動生成html文件,比如編譯後文件的引入
    filename: 'index.html', //生成的文件名
    template: 'index.html', //模板
    inject: true
    }),
    new FriendlyErrorsPlugin() //友好的錯誤提示
    ]
  • webpack.prod.conf.js
    生產環境下的 webpack 配置,通過 merge 方法合並 webpack.base.conf.js 基礎配置
    module 的處理, 主要是針對 css 的處理
    同樣的此處調用了 utils.styleLoaders

    module: {
     rules: utils.styleLoaders({
      sourceMap: config.build.productionSourceMap,
      extract: true
     }) 
    }
  • 輸出文件 output

    output: {
     //導出文件目錄
     path: config.build.assetsRoot, 
     //導出的文件名
     filename: utils.assetsPath('js/[name].[chunkhash].js'), 
     //非入口文件的文件名,而又需要被打包出來的文件命名配置,如按需加載的模塊
     chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
    }
  • 插件 plugins

    var path = require('path')
    var utils = require('./utils')
    var webpack = require('webpack')
    var config = require('../config')
    var merge = require('webpack-merge')
    var baseWebpackConfig = require('./webpack.base.conf')
    var CopyWebpackPlugin = require('copy-webpack-plugin')
    var HtmlWebpackPlugin = require('html-webpack-plugin')
    var ExtractTextPlugin = require('extract-text-webpack-plugin')
    var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
    var env = config.build.env
    plugins: [
     new webpack.DefinePlugin({
      'process.env': env //配置全局環境為生產環境
     }),
     new webpack.optimize.UglifyJsPlugin({ //js文件壓縮插件
      compress: { //壓縮配置
       warnings: false // 不顯示警告
      },
      sourceMap: true //生成sourceMap文件
     }),
     new ExtractTextPlugin({ //將js中引入的css分離的插件
      filename: utils.assetsPath('css/[name].[contenthash].css') //分離出的css文件名
     }),
     //壓縮提取出的css,並解決ExtractTextPlugin分離出的js重復問題(多個文件引入同一css文件)
     new OptimizeCSSPlugin(), 
     //生成html的插件,引入css文件和js文件
     new HtmlWebpackPlugin({
      filename: config.build.index, //生成的html的文件名
      template: 'index.html', //依據的模板
      inject: true, //註入的js文件將會被放在body標簽中,當值為'head'時,將被放在head標簽中
      minify: { //壓縮配置
       removeComments: true, //刪除html中的註釋代碼
       collapseWhitespace: true, //刪除html中的空白符
       removeAttributeQuotes: true //刪除html元素中屬性的引號
      },
      chunksSortMode: 'dependency' //按dependency的順序引入
     }),
     //分離公共js到vendor中
     new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor', //文件名
      minChunks: functions(module, count) { // 聲明公共的模塊來自node_modules文件夾
       return (module.resource && /\.js$/.test(module.resource) && module,resource.indexOf(path.join(__dirname, '../node_modules')) === 0)
      }
     }),
     //上面雖然已經分離了第三方庫,每次修改編譯都會改變vendor的hash值,導致瀏覽器緩存失效。原因是vendor包含了webpack在打包過程中會產生一些運行時代碼,運行時代碼中實際上保存了打包後的文件名。當修改業務代碼時,業務代碼的js文件的hash值必然會改變。一旦改變必然會導致vendor變化。vendor變化會導致其hash值變化。
     //下面主要是將運行時代碼提取到單獨的manifest文件中,防止其影響vendor.js
     new webpack.optimize.CommonsChunkPlugin({
      name: 'mainifest',
      chunks: ['vendor']
     }),
     // 復制靜態資源,將static文件內的內容復制到指定文件夾
     new CopyWebpackPlugin([{
      from: path.resolve(__dirname, '../static'),
      to: config.build.assetsSubDirectory,
      ignore: ['.*'] //忽視.*文件
     }])
    ]
  • 額外配置

    if (config.build.productionGzip) { //配置文件開啟了gzip壓縮
    
     //引入壓縮文件的組件,該插件會對生成的文件進行壓縮,生成一個.gz文件
     var CompressionWebpackPlugin = require('compression-webpack-plugin') 
    
     webpackConfig.plugins.push(
      new CompressionWebpackPlugin({
       asset: '[path].gz[query]', //目標文件名
       algorithm: 'gzip', //使用gzip壓縮
       test: new RegExp( //滿足正則表達式的文件會被壓縮
        '\\.(' +
        config.build.productionGzipExtensions.join('|') +
        ')$'
       ),
       threshold: 10240, //資源文件大於10240B=10kB時會被壓縮
       minRatio: 0.8 //最小壓縮比達到0.8時才會被壓縮
      })
     )
    }
  • npm run dev
    有了上面的配置之後,下面看看運行命令npm run dev發生了什麽
    在 package.json 文件中定義了 dev 運行的腳本

    "scripts": {
      "dev": "node build/dev-server.js",
      "build": "node build/build.js"
    }

    當運行 npm run dev 命令時,實際上會運行 dev-server.js 文件
    該文件以 express 作為後端框架

    // nodejs環境配置
    var config = require('../config')
    if (!process.env.NODE_ENV) {
     process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
    }
    var opn = require('opn') //強制打開瀏覽器
    var path = require('path')
    var express = require('express')
    var webpack = require('webpack')
    var proxyMiddleware = require('http-proxy-middleware') //使用代理的中間件
    var webpackConfig = require('./webpack.dev.conf') //webpack的配置
    
    var port = process.env.PORT || config.dev.port //端口號
    var autoOpenBrowser = !!config.dev.autoOpenBrowser //是否自動打開瀏覽器
    var proxyTable = config.dev.proxyTable //http的代理url
    
    var app = express() //啟動express
    var compiler = webpack(webpackConfig) //webpack編譯
    
    //webpack-dev-middleware的作用
    //1.將編譯後的生成的靜態文件放在內存中,所以在npm run dev後磁盤上不會生成文件
    //2.當文件改變時,會自動編譯。
    //3.當在編譯過程中請求某個資源時,webpack-dev-server不會讓這個請求失敗,而是會一直阻塞它,直到webpack編譯完畢
    var devMiddleware = require('webpack-dev-middleware')(compiler, {
     publicPath: webpackConfig.output.publicPath,
     quiet: true
    })
    
    //webpack-hot-middleware的作用就是實現瀏覽器的無刷新更新
    var hotMiddleware = require('webpack-hot-middleware')(compiler, {
     log: () => {}
    })
    //聲明hotMiddleware無刷新更新的時機:html-webpack-plugin 的template更改之後
    compiler.plugin('compilation', function (compilation) {
     compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
      hotMiddleware.publish({ action: 'reload' })
      cb()
     })
    })
    
    //將代理請求的配置應用到express服務上
    Object.keys(proxyTable).forEach(function (context) {
     var options = proxyTable[context]
     if (typeof options === 'string') {
      options = { target: options }
     }
     app.use(proxyMiddleware(options.filter || context, options))
    })
    
    //使用connect-history-api-fallback匹配資源
    //如果不匹配就可以重定向到指定地址
    app.use(require('connect-history-api-fallback')())
    
    // 應用devMiddleware中間件
    app.use(devMiddleware)
    // 應用hotMiddleware中間件
    app.use(hotMiddleware)
    
    // 配置express靜態資源目錄
    var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
    app.use(staticPath, express.static('./static'))
    
    var uri = 'http://localhost:' + port
    
    //編譯成功後打印uri
    devMiddleware.waitUntilValid(function () {
     console.log('> Listening at ' + uri + '\n')
    })
    //啟動express服務
    module.exports = app.listen(port, function (err) {
     if (err) {
      console.log(err)
      return
     }
     // 滿足條件則自動打開瀏覽器
     if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
      opn(uri)
     }
    })
  • npm run build
    由於 package.json 中的配置,運行此命令後會執行 build.js 文件

    process.env.NODE_ENV = 'production' //設置當前環境為production
    var ora = require('ora') //終端顯示的轉輪loading
    var rm = require('rimraf') //node環境下rm -rf的命令庫
    var path = require('path') //文件路徑處理庫
    var chalk = require('chalk') //終端顯示帶顏色的文字
    var webpack = require('webpack') 
    var config = require('../config') 
    var webpackConfig = require('./webpack.prod.conf') //生產環境下的webpack配置
    
    // 在終端顯示ora庫的loading效果
    var spinner = ora('building for production...')
    spinner.start()
    
    // 刪除已編譯文件
    rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
     if (err) throw err
     //在刪除完成的回調函數中開始編譯
     webpack(webpackConfig, function (err, stats) {
      spinner.stop() //停止loading
      if (err) throw err
    
      // 在編譯完成的回調函數中,在終端輸出編譯的文件
      process.stdout.write(stats.toString({
       colors: true,
       modules: false,
       children: false,
       chunks: false,
       chunkModules: false
      }) + '\n\n')
     })
    })
    

vue-cli 中的 webpack 配置詳解