一份比較詳細的 webpack 4.x 手工配置基礎開發環境 附原始碼

褲襠三重奏發表於2018-07-22

webpack

重新書寫了部落格內容,希望可以更好的呈現該有的知識點。
bundle.js 指的是 webpack 打包後的檔案。

小劇場

專案經理:我們要開始一個新的專案,褲襠你來負責專案構建吧。
我:好的沒問題,經理請稍等。

npm install vue-cli -g
vue init webpack -y new-project-name
複製程式碼

我:好了,我們開始吧。
專案經理:接下來呢?

黑人問號臉

我:接下來沒了,可以開發了。
專案經理:褲襠啊,速度快是好事,但是我看你每次都是那麼幾步,能不能來點不一樣的,你看那些面試官,面試手寫一個 webpack 4.x 的配置,你知道怎麼寫麼?
我:。。。。。。
專案經理:(拂袖而去,遠遠地聽到空中傳來一句話)年輕人,切勿急躁,穩中求勝啊。
我:專案急的時候你不是這麼說的。
專案經理:褲襠你說啥?
我:經理你說得對。

前言

在我們在面對一個新的專案的時候,網上的大量優秀的模板可以使我們少走很多彎路,可以把主要的精力放在業務上,等到後期專案龐大了,業務複雜了的時候再去做一些優化,這其中包括專案打包速度優化,專案打包體積優化(也可以看做是首屏載入優化),等等,但是,身為一個愛折騰的程式猿,面對這些模板,是的,我很好奇!

~~我很好騎~~

當然,文章開始之前,附上該專案的地址,github@jsjzh,所有的程式碼我都加上了註釋,希望大家看完之後可以有所收穫,最好能賞個 star 啦!=3=

git clone https://github.com/jsjzh/my-webpack-template.git
cd my-webpack-template
npm install
npm start
複製程式碼

webpack 4.x 的那些新玩意兒

在最新的 官方文件 中,有兩個新的配置項,modeoptimization,我們就從這兩個入手,看看 webpack .4x 有什麼新東西。

下面會介紹因為你配置了不同的 mode 之後,你的程式碼會受到的不同對待。

mode 是個啥

這個配置項是區分 webpack 4.x 和其他版本最方便的手段,webpack 4.x 給我們提供了兩個模式用作開發和生產的模式,這兩個模式下 webpack 默默給我們開啟了不少優化手段,當然,這些優化手段我們也是可以配置的,就是在 optimization 這個配置項中,我們也可以自己增加 optimization 選項對專案進行更細的優化。

production | development | none
複製程式碼

下面我會對 mode 的兩個值 productiondevelopment 進行比較詳細的說明,看看 webpack 到底偷偷給我們開啟了什麼優化。

proddev 相同的優化

mode: production || development 時,webpack 都會開啟的優化。

{
  "mode": "production" || "development",
  "optimization": {
    // 如果 子模組 和 父模組 都載入了同一個 A模組 的時候,開啟這個選項將會告訴 webpack 跳過在 子模組 中對 A模組 的檢索,這可以加快打包速度。
    "removeAvailableModules": true,
    // webpack 將會不會去打包一個空的模組。
    "removeEmptyChunks": true,
    // 告訴 webpack 合併一些包含了相同模組的模組。
    "mergeDuplicateChunks": true,
    // 會在 process.env.NODE_ENV 中傳入當前的 mode 環境。
    "nodeEnv": "production" || "development"
  }
}
複製程式碼

proddev 不同的優化

mode: production 時,webpack 開啟的優化。

{
  "mode": "production",
  "optimization": {
    // 告訴 webpack 確定和標記塊,這些塊是其他塊的子集,當更大的塊已經被載入時,不需要載入這些子集。
    "flagIncludedChunks": true,
    // 告訴 webpack 找出一個模組的順序,這可以使打包出來的入口 bundle.js 最小化。
    "occurrenceOrder": true,
    // 確定每個模組下匯出被使用的。
    "usedExports": true,
    // 告訴 webpack 查詢可以安全地連線到單個模組的模組圖的片段。取決於優化。
    "concatenateModules": true,
    // 使用 UglifyjsWebpackPlugin 進行程式碼壓縮。
    "minimize": true
  },
  // 效能相關配置
  "performance": {
    "hints": "error",
    // ...
  }
}
複製程式碼

mode: development 時,webpack 開啟的優化。

{
  "mode": "development",
  // 生成 source map 的格式選擇,這個選項可以直接影響構建速度。
  "devtool": "eval",
  // 快取模組,避免在未更改時重建它們。
  "cache": true,
  "module": {
    // 快取已解決的依賴項,避免重新解析它們。
    "unsafeCache": true
  },
  "output": {
    // 在 bundle.js 中引入專案所包含模組的註釋資訊。
    "pathinfo": true
  },
  "optimization": {
    // 在可能的情況下確定每個模組的匯出。
    "providedExports": true,
    // 找到 chunk 中共享的模組,取出來生成單獨的 chunk。
    // 該配置用於程式碼分割打包,取代了曾經的 CommonsChunkPlugin 外掛。
    "splitChunks": true,
    // 為 webpack 執行時程式碼建立單獨的 chunk。
    "runtimeChunk": true,
    // 編譯錯誤時不寫入到輸出。
    // 取代了曾經的 NoEmitOnErrorsPlugin 外掛。
    "noEmitOnErrors": true,
    // 給模組更有意義更方便除錯的名稱。
    // 取代了曾經的 NamedModulesPlugin 外掛。
    "namedModules": true,
    // 給 chunk 更有意義更方便除錯的名稱。
    "namedChunks": true,
  }
}
複製程式碼

webpack 4.x 基礎版開發環境詳細配置

基礎版擁有 npm start 之後 開啟一個新的網頁,並且更改 js 會自動更新的功能,不包含對 ES6 語法的轉義以及 css 打包,image 圖片轉為 dataURL 的功能。

先來一套組合拳,建立一個專案資料夾,並初始化專案。

md my-webpack-template
cd my-webpack-template
npm init -y
複製程式碼

接著可以參考我的目錄結構(列出主要的檔案,只針對 dev 環境)。

+---my-webpack-template
|       index.html
|       package.json
+---build
|       utils.js
|       build-server.js
|       webpack.base.conf.js
|       webpack.dev.conf.js
+---config
|       index.js
|       dev.env.js
+---src
|       index.js
複製程式碼

安裝所需依賴。

webpackwebpack-cli 曾經是在一起的,在 4.x 版本中進行了拆分,所以如果不好好同時安裝他們倆是不行的哦。

不推薦全域性安裝 webpack,這會導致命令列執行 webpack 的時候鎖定版本。

npm install webpack webpack-cli -D
複製程式碼

webpack-dev-server 是一個專門用於開發環境使用的整合了眾多功能的環境,基於 express,擁有即時編譯程式碼(webapck-dev-middleware),熱更新(webpack-hot-middleware),自動開啟瀏覽器(opn),對 HTML5history 做特殊處理(connect-history-api-fallback)等等功能。

對於開發環境,即時編譯的程式碼不會儲存在硬碟中而是在記憶體中,這是由 webapck-dev-middleware 完成的功能。

npm install webpack-dev-server -D
複製程式碼

用於合併 webpack 配置的,一般我們會把 webpackbase 配置和 dev 配置 和 prod 配置分開寫,用這個工具就可以很方便的合併 basedev 的配置。

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

一個用於處理打包這個程式的外掛,可以清除打包時候殘留的控制檯資訊,並且可以在控制檯列印出打包成功之後的文字提示,當然,對於打包錯誤之後的回撥也是有的。

npm install friendly-errors-webpack-plugin -D
複製程式碼

這個相對來說各位看官應該用的很多了吧,用於生成一個 html 檔案,並且可以在底部注入通過 webpack 打包好的 bundle.js 檔案。

npm install html-webpack-plugin -D
複製程式碼

一個尋找可用埠的工具,當你配置的埠被佔用時,這個工具會自動尋找一個可用的埠。

npm install portfinder -D
複製程式碼

配置 package.json 中的執行指令碼

接著,安裝完了依賴我們需要配置 npm 執行時候的指令碼了。

當你在命令列直接輸入 webpack 報錯的,並且確信自己已經安裝了 webpack 的時候,試試直接配置 package.json 中的 scripts,說不定你安裝的是專案中的 webpack,而 package.json 中執行的指令碼將優先該專案的環境。

"scripts": {
  "dev": "webpack-dev-server --inline --progress --config build/build-server.js",
  "start": "npm run dev"
}
複製程式碼

build/webpack.base.conf.js 配置詳解

先來配置 webpack 基礎的配置,這裡的配置 proddev 相同。

// 將一些配置寫在 config/index.js 中,方便直接獲取
var config = require("../config");

// 獲取專案的初始目錄,包裝了個小函式
function resolve(file) {
  return path.resolve(__dirname, "../", file)
}

module.exports = {
  // webpack 處理打包檔案的時候的初始目錄
  context: resolve("./"),
  // 入口檔案,webapck 4.x 預設的就是 src/index.js
  entry: {
    app: "./src/index.js"
  },
  // 輸出檔案的目錄
  output: {
    path: config.build.assetsRoot,
    filename: "[name].js",
    publicPath: process.env.NODE_ENV === "production" ?
      config.build.assetsPublicPath : config.dev.assetsPublicPath
  }
}
複製程式碼

build/webpack.dev.conf.js 配置詳解

// 將一些配置寫在 config/index.js 中,方便直接獲取
var config = require("../config");
var devConfig = config.dev;
// 一些工具函式
var utils = require("./utils");
// nodeJs 內建的函式,專門用來解析路徑
var path = require("path");
// 大名鼎鼎的 webpack
var webpack = require("webpack");
var merge = require("webpack-merge");
var HtmlWebpackPlugin = require("html-webpack-plugin");
var webpackBaseConfig = require("./webpack.base.conf");

module.exports = merge(webpackBaseConfig,{
  // 配置開發環境 mode
  mode: "development"// 一句話,這是個方便開發工具進行程式碼定位的配置
  // 但是不同的配置會影響編譯速度和打包速度,這裡使用了和 vue-cli 同樣的配置
  devtool: devConfig.devtool,
  // 使用了 webpack-dev-server 之後就需要有的配置
  // 在這裡可以配置詳細的開發環境
  devServer: {
    // 當我們在 package.json 中使用 webpack-dev-server --inline 模式的時候
    // 我們在 chrome 的開發工具的控制檯 console 可以看到資訊種類
    // 可選 none error warning info
    clientLogLevel: "warning"// 不用擔心:要解決這個問題,你所需要做的就是在你的伺服器上新增一個簡單的萬能回退路線。如果URL不匹配任何靜態資產,那麼它應該服務於相同的索引。你的應用程式所在的html頁面。又漂亮! --- by vue-router
    // 這個配置就是應用了 connect-history-api-fallback 外掛
    // 想象一個場景,vue 開發,我們利用 vue-router 的 history 模式進行單頁面中的頁面跳轉
    // www.demo.com 跳轉去 www.demo.com/list
    // 看起來沒毛病,vue-router 中只要配置了 list 的路由即可
    // 但是,當你重新整理頁面的時候,瀏覽器會去向伺服器請求 www.demo.com/list 的資源,這想當然是找不到的
    // 這個中介軟體就是會自動捕獲這個錯誤,然後將它重新定位到 index.html
    historyApiFallback: {
      rewrites: [{
        from: /.*/,
        to: path.posix.join(devConfig.assetsPublicPath,"index.html")
      }]
    },
    // webpack 最有用的功能之一 --- by webpack
    // 熱更新裝置啟動
    hot: true// 告訴 webpack-dev-server 搭建伺服器的時候從哪裡獲取靜態檔案
    // 預設情況下,將使用當前工作目錄作為提供靜態檔案的目錄
    // contentBase: false,
    // 搭建的開發伺服器啟動 gzip 壓縮
    compress: true// 搭建的開發伺服器的 host,這裡使用了一個函式去獲取當前電腦的區域網 ip
    // 這個可以獲取你的電腦的 ip 地址,然後開發伺服器就可以搭建在區域網裡
    // 如果有一同開發的小夥伴,在同一區域網內就可以直接訪問地址看到你的頁面
    // 同樣,這個也適用於手機,連上同一個 wifi 之後就可以在手機上實時看到修改的效果
    host: utils.getIPAdress(),
    // 開發伺服器的埠號
    // 但是後面我們會用到 portfinder 外掛,如果真的 config/index.js 中的埠被佔用了
    // 那這個外掛會以這個為 basePort 去找一個沒有被佔用的埠
    port: devConfig.port,
    // 是否要伺服器搭建完成之後自動開啟瀏覽器
    open: devConfig.autoOpenBrowser,
    // 是否開啟發現錯誤之後在瀏覽器全螢幕顯示錯誤資訊功能
    overlay: devConfig.errorOverlay ? {
      warnings: false,
      errors: true
    } : false// 此路徑下的打包檔案可在瀏覽器中訪問
    // 假設伺服器執行在 http://localhost:8080 並且 output.filename 被設定為 bundle.js
    // 預設 publicPath 是 "/",所以 bundle.js 可以通過 http://localhost:8080/bundle.js 訪問
    publicPath: devConfig.assetsPublicPath,
    // 啟動介面訪問代理
    proxy: devConfig.proxyTable,
    // 啟用 quiet 後,除了初始啟動資訊之外的任何內容都不會被列印到控制檯
    // 和 FriendlyErrorsPlugin 配合食用更佳
    quiet: true// 開啟監聽檔案修改的功能,在 webpack-dev-server 和 webpack-dev-middleware 中是預設開始的
    // watch: true,
    // 關於上面 watch 的一些選項配置
    watchOptions: {
      // 排除一些檔案監聽,這有利於提高效能
      // 這裡排除了 node_modules 資料夾的監聽
      // 但是這在應對需要 npm install 一些新的 module 的時候,就需要重啟服務
      ignored: /node_modules/// 是否開始輪詢,有的時候檔案已經更改了但是卻沒有被監聽到,這時候就可以開始輪詢
      // 但是比較消耗效能,選擇關閉
      poll: devConfig.poll
    }
  },
  plugins: [
    // 這可以建立一個在編譯過程中的全域性變數
    // 因為這個外掛直接執行文字替換,給定的值必須包含字串本身內的實際引號 --- by webpack
    // 所以需要這麼用
    // "process.env": JSON.stringify('development')
    // 或者
    // "process.env": '"development"'
    new webpack.DefinePlugin({
      "process.env": require("../config/dev.env")
    }),
    // 開啟大名鼎鼎的熱更新外掛
    new webpack.HotModuleReplacementPlugin(),
    // 使用大名鼎鼎(詞窮)的 html-webpack-plugin 模板外掛
    new HtmlWebpackPlugin({
      // 輸出的 html 檔案的名字
      filename: "index.html"// 使用的 html 模板名字
      template: "index.html"// 是否要插入 weback 打包好的 bundle.js 檔案
      inject: true
    })
  ]
})
複製程式碼

build/build-server.js 配置詳解

// 更友好的提示外掛
var FriendlyErrorsPlugin = require("friendly-errors-webpack-plugin");
// 獲取一個可用的 port 的外掛
var portfinder = require("portfinder");
var devWebpackConfig = require("./webpack.dev.conf");

// 匯出一個 promise 函式,這可以讓 wepback 接受一個非同步載入的配置
// 並在 resolve 的時候執行 這個配置
// 比如這裡我就用到了 portfinder 和 friendly-errors-webpack-plugin
module.exports = new Promise((resolve,reject) => {
  // 設定外掛的初始搜尋埠號
  portfinder.basePort = devWebpackConfig.devServer.port
  portfinder.getPort((err,port) => {
    if (err) reject(err)
    else {
      // 這裡就利用 portfinder 得到了可以使用的埠
      devWebpackConfig.devServer.port = port
      devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
        // 清除控制檯原有的資訊
        clearConsole: true// 打包成功之後在控制檯給予開發者的提示
        compilationSuccessInfo: {
          messages: [`開發環境啟動成功,專案執行在: http://${devWebpackConfig.devServer.host}:${port}`]
        },
        // 打包發生錯誤的時候
        onErrors: () => { console.log("打包失敗") }
      }))
      resolve(devWebpackConfig)
    }
  })
})
複製程式碼

編譯出錯了?看看這裡

如果你發現直接在控制檯執行 webpack 報錯了,但是你確實執行了 npm install,那是因為你沒有安裝全域性的 webpack。

  • 可以執行 .\node_modules\.bin\webpack --config webpack.config.js
    • 呼叫該專案 node_modules 下的 webpack
  • 使用 package.json 配置讓 npm 去找該專案中的 webpack
    • package.json > scripts.build: webapck

DeprecationWarning: Tapable.plugin is deprecated. Use new API on '.hooks' instead

這個錯誤會發生在你使用的外掛沒有針對 webpack 4.x 升級。 這個時候只能去 githubissue 或者換一個 plugin 了。

還發現了其他的錯?請直接私信我,或者在評論中留言。

後語

希望自己所做的一些微小的事情可以幫助大家在漫漫前端路中更上一層樓,另外,週末了不要太沉迷於敲程式碼,多出去走走,散散步,運動運動,給自己的一週充實的大腦放個空。

如果大家覺得我哪裡寫的不對,請不要猶豫,直接 diss 我 =3=

程式碼如人生,我甘之如飴。

我在這裡 gayhub@jsjzh 歡迎來找我玩兒

向前看就是未來,向後看就是過去,從中取一段下來就是故事,而這只不過是那樣的故事中很小的一部分而已。--- 灰色的果實

大綱

  • webpack 4.x 的那些新玩意兒(DONE)
    • mode
    • optimization
  • webpack 4.x 基礎版開發環境詳細配置(DONE)
    • package.json 中的 devDependencies
    • package.json 中的 scripts
    • build/webpack.base.conf.js 配置詳解
    • build/webpack.dev.conf.js 配置詳解
    • build/build-server.js 配置詳解
  • webpack 4.x 升級版開發環境詳細配置(TODO)[分篇]
    • 利用 babel 轉換 ES6 語法
    • img 轉為 dataURL
    • 打包 css
    • 使用 vue-loader 或其他 loader 來完成更多
    • 自己動手開發一個 webpack-plugin
  • webpack 4.x 生產環境詳細配置(TODO)[分篇]
  • webpack 配置優化(TODO)[分篇]
    • 打包速度優化
    • 打包體積優化

相關文章