webpack4-06-開發、生產環境、動態CDN配置

Qin在掘金發表於2019-05-12

環境分析

開發環境

  • 在開發環境中,我們需要具有強大的、具有實時重新載入(live reloading)、熱模組替換(hot module replacement)能力的 source map(方便開發者除錯程式碼) 和 localhost server(本地伺服器)。

大致如下:

  1. webpack-dev-server實時過載、熱替換
  2. 不壓縮程式碼
  3. css樣式不提取至單獨的檔案中
  4. 使用sourceMap配置,將原始碼對映會原始檔案(方便除錯)
  5. 不壓縮html

生產環境

  • 在生產環境中,我們的目標則轉向於關注更小的 bundle,更輕量的 source map,以及更優化的資源,以改善載入時間。。

大致如下:

  1. 不需要實時過載、熱替換
  2. 壓縮js、css
  3. css樣式提取至單獨的檔案中
  4. sourceMap.
  5. 程式碼分離(optimization)
  6. 壓縮html
  7. 資源快取(NamedChunksPlugin、HashedModuleIdsPlugin)
  8. 清除dist目錄檔案

解決通用程式碼

在兩個環境下,需要把通用的配置合併,所以需要用到 webpack-merge 合併工具,解決程式碼重複的問題。

安裝webpack-merge

npm i webpack-merge -D
複製程式碼

最新目錄、檔案

  lesson-05
    |- build
        |- webpack.base.conf.js     // + 通用配置
        |- webpack.dev.conf.js      // + 開發環境配置
        |- webpack.prod.conf.js     // + 生產環境
    |- node-modules
    |- pubilc
    |- package.json
    |- package-lock.json
    |- src
        |- print.js                 // + 用於測試快取
    |- favicon.ico                  // + 網頁icon
複製程式碼

安裝生產環境相關包

npm i cross-env copy-webpack-plugin mini-css-extract-plugin optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin -D
複製程式碼
  • cross-env:在命令列中配置環境變數(檢視package.json)
  • copy-webpack-plugin:拷貝資源
  • mini-css-extract-plugin:單獨提取至css檔案
  • optimize-css-assets-webpack-plugin:壓縮css檔案
  • uglifyjs-webpack-plugin:壓縮js檔案

通用配置

webpack.base.conf.js

const path = require('path')
const webpack = require('webpack')
const { VueLoaderPlugin } = require('vue-loader')

const resolve = (dir) =>  path.resolve(__dirname, dir)
const jsonToStr = (json) => JSON.stringify(json)
const isProd = process.env.NODE_ENV === 'production'
module.exports = {
  // 入口配置
  entry: {
    app: ['@babel/polyfill', resolve('../src/main.js')]
  },

  // 打包輸出配置
  output: {
    path: resolve('../dist'),
    filename: 'bundle.js'     // filename是相對於path路徑生成
  },

  // 引入資源省略字尾、資源別名配置
  resolve: {
    extensions: ['.js', '.json', '.vue'],
    alias: {
      '@': resolve('../src')
    }
  },

  // 定義模組規則
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: 'babel-loader',
        // 指定目錄去載入babel-loader,提升執行、打包速度
        include: [resolve('../src'), resolve('../node_modules/webpack-dev-server/client')],
        // 排除目錄,提升執行、打包速度
        exclude: file => (
          /node_modules/.test(file) &&
          !/\.vue\.js/.test(file)
        )
      },
      {
        test: /\.(png|svg|jpg|gif)$/,
        loader: 'file-loader',
        options: {
          // 指定生成的目錄
          name: 'static/images/[name].[hash:7].[ext]',
        },
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      }
    ]
  },
  
  // 外掛選項
  plugins: [
    // 定義環境變數
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': isProd ? jsonToStr('production') : jsonToStr('development')
    }),
    new VueLoaderPlugin()
  ]
} 
複製程式碼

開發環境配置

webpack.dev.conf.js

const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const baseWebpack = require('./webpack.base.conf')

const resolve = (dir) =>  path.resolve(__dirname, dir)
module.exports = merge(baseWebpack, {
  mode: 'development',
  devtool: 'cheap-source-map',  // 開啟cheap-source-map模式除錯
  
  // 開啟web伺服器、熱更新
  devServer: {
    open: true,
    hot: true,
    port: 3002,
    publicPath: '/',
    contentBase: resolve("../dist")   // 設定dist目錄為伺服器預覽的內容
  },
  
  // 定義模組規則
  module: {
    rules: [
      {
        test: /\.(css|scss|sass)$/,
        use: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1
            }
          },
          {
            loader: 'sass-loader',
            options: {
              implementation: require('dart-sass')
            }
          },
          {
            loader: 'postcss-loader'
          }
        ]
      }
    ]
  },

  // 外掛選項
  plugins: [
    // html模板、以及相關配置
    new HtmlWebpackPlugin({
      title: 'Lesson-06',
      template: resolve('../public/index.html')
    }),
    // 熱替換外掛
    new webpack.HotModuleReplacementPlugin(),
    // 在熱載入時直接返回更新檔名,而不是檔案的id。
    new webpack.NamedModulesPlugin()
  ]
})
複製程式碼

生產環境配置

webpack.prod.conf.js

const path = require('path')
const merge = require('webpack-merge')
const webpack = require('webpack')
const webpackConfig = require('./webpack.base.conf')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

const resolve = (dir) =>  path.resolve(__dirname, dir)

module.exports = merge(webpackConfig, {
  mode: 'production',
  devtool: false,
  // 執行、打包輸出配置
  output: {
    path: resolve('../dist'),
    filename: 'static/js/[name].[chunkhash:8].js'
  },

  // 壓縮js、css資源、分包
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        libs: {
          name: 'chunk-libs', // 打包後生成的js檔名稱
          test: /[\\/]node_modules[\\/]/,
          priority: 10, 
          chunks: 'initial' // 只打包初始時依賴的第三方
        },
        // elementUI選項暫時未使用到(參考elementUI中的配置)
        elementUI: {
          name: 'chunk-elementUI', // 單獨將 elementUI 拆包
          priority: 21, // 權重要大於 libs 和 app 不然會被打包進 libs 或者 app
          test: /[\\/]node_modules[\\/]element-ui[\\/]/
        },
        // commons選項暫時未使用到(參考elementUI中的配置)
        commons: {
          name: 'chunk-commons',
          test: resolve('../src/components'), // 可自定義擴充你的規則
          minChunks: 3, // 最小公用次數
          priority: 5,
          reuseExistingChunk: true
        }
      }
    },
    runtimeChunk: 'single',
    minimizer: [ // 壓縮js、壓縮css配置
      new UglifyJsPlugin({
        sourceMap: false,
        cache: true,
        parallel: true
      }),
      new OptimizeCSSAssetsPlugin()
    ]
  },

  // 定義模組規則
  module: {
    rules: [
      {
        test: /\.(scss|sass)$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader
          },
          {
            loader: 'css-loader',
            options: {
              importLoaders: 2,
              sourceMap: false
            }
          },
          {
            loader: 'sass-loader',
            options: {
              implementation: require('dart-sass'),
              sourceMap: false
            }
          },
          {
            loader: 'postcss-loader',
            options: {
              sourceMap: false
            }
          }
        ]
      },
    ]
  },

  // 外掛選項
  plugins: [
    // 清除上次構建的檔案,清除目錄是基於output出口目錄
    new CleanWebpackPlugin(),
    // 建立html入口,無需手動引入js、css資源
    new HtmlWebpackPlugin({
      template: resolve('../public/index.html'),
      title: 'Lesson-06',
      favicon: resolve('../favicon.ico'),
      // 壓縮html
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
        // more options:
        // https://github.com/kangax/html-minifier#options-quick-reference
      }
    }),

    // 提取至單獨css檔案
    new MiniCssExtractPlugin({
      filename: 'static/css/[name].[contenthash:8].css',
      chunkFilename: 'static/css/[name].[contenthash:8].css'
    }),

    // 拷貝資源
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../public'),
        to: path.resolve(__dirname, '../dist')
      }
    ]),
    
    // 當chunk沒有名字時,保持chunk.id穩定(快取chunk)
    new webpack.NamedChunksPlugin(chunk => {
      if (chunk.name) {
        return chunk.name
      }
      const modules = Array.from(chunk.modulesIterable)
      if (modules.length > 1) {
        const hash = require('hash-sum')
        const joinedHash = hash(modules.map(m => m.id).join('_'))
        let len = 4
        const seen = new Set()
        while (seen.has(joinedHash.substr(0, len))) len++
        seen.add(joinedHash.substr(0, len))
        return `chunk-${joinedHash.substr(0, len)}`
      } else {
        return modules[0].id
      }
    }),

    // 當vender模組沒有變化時,保持module.id穩定(快取vender)
    new webpack.HashedModuleIdsPlugin()
  ]
})
複製程式碼

動態CDN資源配置

在參考別人的配置過程中,發現一個可以在webpack中自定義配置CDN的方式,主要是利用 html-webpack-plugin 外掛的能力,可以新增自定義屬性,將CDN資源連結,配置至此自定義屬性中。通過在 index.html 模板中遍歷屬性來自動生成CDN資源引入。

如下:

webpack.dev.conf.js、webpack.prod.conf.js

  ...省略
  // 外掛選項
  plugins: [
    // html模板、以及相關配置
    new HtmlWebpackPlugin({
      title: 'Lesson-06',
      template: resolve('../public/index.html'),
      // cdn(自定義屬性)載入的資源,不需要手動新增至index.html中,
      // 順序按陣列索引載入
      cdn: {
        css:['https://cdn.bootcss.com/element-ui/2.8.2/theme-chalk/index.css'],
        js: [
          'https://cdn.bootcss.com/vue/2.6.10/vue.min.js',
          'https://cdn.bootcss.com/element-ui/2.8.2/index.js'
        ]
      }
    })
  ]
  ...省略
複製程式碼

public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title><%= htmlWebpackPlugin.options.title %></title>
	<!-- import cdn css -->
	<% if(htmlWebpackPlugin.options.cdn) {%>
		<% for(var css of htmlWebpackPlugin.options.cdn.css) { %>
			<link rel="stylesheet" href="<%=css%>">
		<% } %>
	<% } %>
</head>
<body>
	<div id="box"></div>
	<!-- import cdn js -->
	<% if(htmlWebpackPlugin.options.cdn) {%>
		<% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
			<script src="<%=js%>"></script>
		<% } %>
	<% } %>
</body>
</html>
複製程式碼

配置開發、生產環境命令

在package.json的scripts選項中新增,dev、build命令,分別是開發環境、生產環境的命令,

package.json

{
  "name": "lesson-06",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {
    "dev": "npx webpack-dev-server --config ./build/webpack.dev.conf.js",
    "build": "cross-env NODE_ENV=production npx webpack --config ./build/webpack.prod.conf.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.4.4",
    "@babel/polyfill": "^7.4.4",
    "@babel/preset-env": "^7.4.4",
    "autoprefixer": "^9.5.1",
    "babel-loader": "^8.0.5",
    "clean-webpack-plugin": "^2.0.1",
    "copy-webpack-plugin": "^5.0.3",
    "cross-env": "^5.2.0",
    "css-loader": "^2.1.1",
    "dart-sass": "^1.19.0",
    "file-loader": "^3.0.1",
    "html-webpack-plugin": "^3.2.0",
    "mini-css-extract-plugin": "^0.6.0",
    "optimize-css-assets-webpack-plugin": "^5.0.1",
    "postcss-loader": "^3.0.0",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.1",
    "uglifyjs-webpack-plugin": "^2.1.2",
    "vue-loader": "^15.7.0",
    "vue-template-compiler": "^2.6.10",
    "webpack": "^4.30.0",
    "webpack-cli": "^3.3.0",
    "webpack-dev-server": "^3.3.1",
    "webpack-merge": "^4.2.1"
  },
  "dependencies": {
    "vue": "^2.6.10"
  }
}

複製程式碼

完成

配置完成後,可嘗試執行:

  • 開發環境
npm run dev
複製程式碼
  • 生產環境
npm run build
複製程式碼

如果沒有什麼問題,會分別開啟本地伺服器、生成生產環境的目錄、檔案(根目錄的dist)。否則可以自行前往github進行clone專案檢視原始碼,內有註釋。

專案地址

原始碼地址點選這GitHub

相關文章