webpack4從零開始構建(一)

前言

以前一段時間工做緣由把精力都放在小程序上,趁如今有點空閒時間,恰好官方文檔也補充完整了,我準備重溫一下 webpack 之路了,由於官方文檔已經寫得很是詳細,我會大量引用原文描述,主要重點放在怎麼從零構建 webpack4 代碼上,這不是一個系統的教程,而是從零摸索一步步搭建起來的筆記,因此前期可能bug會後續發現繼續修復而不是修改文章.css

系列文章

webpack4從零開始構建(一)
webpack4+React16項目構建(二)
webpack4功能配置劃分細化(三)
webpack4引入Ant Design和Typescript(四)
webpack4代碼去重,簡化信息和構建優化(五)
webpack4配置Vue版腳手架(六)html

當前文章完整配置webpack4_demo前端

PS:
2018/12/12 修改細節佈局
2018/12/19 新增包管理工具介紹
2018/12/26上傳,代碼同步到第四篇文章
2019/03/14上傳,補充代碼到第一篇文章,status1分支可見vue

淘寶 NPM 鏡像

由於npm包是從國外服務器下載插件,速度慢,容易失敗,咱們平時使用可使用淘寶團隊的替代鏡像.node

npm install -g cnpm --registry=https://registry.npm.taobao.org

registry.npm.taobao.org 安裝全部模塊. 當安裝的時候發現安裝的模塊尚未同步過來, 淘寶 NPM 會自動在後臺進行同步, 而且會讓你從官方 NPM registry.npmjs.org 進行安裝. 下次你再安裝這個模塊的時候, 就會直接從 淘寶 NPM 安裝了.react

淘寶 NPM 鏡像webpack

Yarn 包管理

Yarn 緩存了每一個下載過的包,因此再次使用時無需重複下載。 同時利用並行下載以最大化資源利用率,所以安裝速度更快。git

Node 版本支持: ^4.8.0 || ^5.7.0 || ^6.2.2 || >=8.0.0github

官網安裝web

初始化一個新項目

yarn init

添加依賴包

yarn add [package]
yarn add [package]@[version]
yarn add [package]@[tag]

將依賴項添加到不一樣依賴項類別中,分別添加到 devDependencies、peerDependenciesoptionalDependencies 類別中:

yarn add [package] --dev
yarn add [package] --peer
yarn add [package] --optional

升級依賴包

yarn upgrade [package]
yarn upgrade [package]@[version]
yarn upgrade [package]@[tag]

移除依賴包

yarn remove [package]

安裝項目的所有依賴

yarn

Node 版本管理

咱們能夠安裝一個版本管理器,根據需求切換.
nvm-windows

安裝版本

nvm install <version> [arch]

移除版本

nvm uninstall <version>

查看已安裝版本

nvm ls

切換版本

nvm use [version] [arch]

概念

本質上,webpack 是一個現代 JavaScript 應用程序的靜態模塊打包器(static module bundler)。在 webpack 處理應用程序時,它會在內部建立一個依賴圖(dependency graph),用於映射到項目須要的每一個模塊,而後將全部這些依賴生成到一個或多個 bundle。

核心:

  1. 入口(entry)
  2. 輸出(output)
  3. 加載器(loader)
  4. 插件(plugins)

安裝

由於最高版本 nodejs 有點問題,因此建議用@9.0,固然用 10 也是能夠的,只是每次都會警告太煩了.
安裝依賴大家也可能選擇其餘安裝方式

yarn add webpack

webpack4+版本還須要安裝

yarn add webpack-cli

完成以後打開目錄下的 package.json 能夠看到當前依賴

{
  "dependencies": {
    "webpack": "^4.25.1",
    "webpack-cli": "^3.1.2"
  },
  "name": "webpack_demo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}

準備工做

根目錄新增 index.html

<!doctype html>
<html>

<head>
    <title>webpack</title>
</head>

<body>
    <p>hello world!</p>
    <script src="index.js"></script>
</body>

</html>

建立 src 目錄,新增style.scssindex.js備用

html {
  background-color: #666;
  p {
    color: red;
  }
}
console.log('onload');

正常打開 index.html 沒毛病

起步

根目錄新增webpack.config.js 文件

const path = require("path");

module.exports = {
  // 入口
  entry: "./src/index.js",
  // 輸出
  output: {
    // 打包文件名
    filename: "main.js",
    // 輸出路徑
    path: path.resolve(__dirname, "dist"),
    // 資源請求路徑
    publicPath: '/',
  }
};

index.html 的 js 引用路徑替換成輸出路徑

<script src="src/index.js"></script>
--------------------------------------
<script src="dist/main.js"></script>

執行命令

npx webpack --config webpack.config.js

打包完成以後直接打開 index.html 看,沒毛病.

npx

npx 是 npm@5.2.0+後出現的工具,總的來講用法有幾個

直接執行命令

通常咱們會在 package.json 配置一些命令而後執行,例如

npm run xxx

可是咱們能夠直接用 npx 運行

npx xxx

臨時安裝調用

npx create-react-app my-cool-new-app

執行這行命令 npx 會自動查找當前依賴包中的可執行文件,若是找不到,就會去 PATH 裏找。若是依然找不到,就會幫你安裝!幫你下載而且執行 create-react-app,完了之後還不會留下痕跡

由於咱們已經安裝過 webpack 了,這麼長的命令仍是直接在 package.json 配置調用吧

mode

剛纔打包的時候若是在終端看到這段提示

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concep...

這是由於 webpack4 增長的一個配置項設定運行環境,經過將 mode 參數設置爲 development, production 或 none,能夠啓用對應環境下 webpack 內置的優化。默認值爲 production。業務代碼中能夠經過 process.env.NODE_ENV 獲取到當前環境模式作些不一樣處理

直接在 webpack.config.js 加上解決警告

mode: 'development'

index.js改爲

console.log(process.env.NODE_ENV);

從新打包查看效果

development 和 production 相同與區別

common

// parent chunk中解決了的chunk會被刪除
optimization.removeAvailableModules: true

// 刪除空的chunks
optimization.removeEmptyChunks: true

// 合併重複的chunk
optimization.mergeDuplicateChunks: true

development

// 調試
devtool: eval

// 緩存模塊, 避免在未更改時重建它們。
cache: true

// 緩存已解決的依賴項, 避免從新解析它們。
module.unsafeCache: true

// 在 bundle 中引入「所包含模塊信息」的相關注釋
output.pathinfo: true

// 在可能的狀況下肯定每一個模塊的導出,被用於其餘優化或代碼生成。
optimization.providedExports: true

// 找到chunk中共享的模塊,取出來生成單獨的chunk
optimization.splitChunks: true

// 爲 webpack 運行時代碼建立單獨的chunk
optimization.runtimeChunk: true

// 編譯錯誤時不寫入到輸出
optimization.noEmitOnErrors: true

// 給模塊有意義的名稱代替ids
optimization.namedModules: true

// 給模chunk有意義的名稱代替ids
optimization.namedChunks: true

production

// 性能相關配置
performance: {
  hints: "error"....
}

// 某些chunk的子chunk已一種方式被肯定和標記,這些子chunks在加載更大的塊時沒必要加載
optimization.flagIncludedChunks: true

// 給常用的ids更短的值
optimization.occurrenceOrder: true

// 肯定每一個模塊下被使用的導出
optimization.usedExports: true

// 識別package.json or rules sideEffects 標誌
optimization.sideEffects: true

// 嘗試查找模塊圖中能夠安全鏈接到單個模塊中的段。- -
optimization.concatenateModules: true

// 使用uglify-js壓縮代碼
optimization.minimize: true

loader

由於 webpack 用於編譯 JavaScript 模塊,樣式不在它的能力範圍內,因此咱們須要引入 loader 作處理.它支持引入任何其餘類型的文件使用.

執行命令安裝依賴

yarn add style-loader css-loader sass-loader node-sass

修改一下 webpack.config.js

const path = require("path");

module.exports = {
  // 入口
  entry: "./src/index.js",

  // 輸出
  output: {
    // 打包文件名
    filename: "main.js",
    // 輸出路徑
    path: path.resolve(__dirname, "dist"),
    // 資源請求路徑
    publicPath: '/',
  },

  module: {
    rules: [
      {
        test: /\.(css|scss)$/, // 匹配文件
        use: [
          "style-loader", // 使用<style>將css-loader內部樣式注入到咱們的HTML頁面
          "css-loader", // 加載.css文件將其轉換爲JS模塊
          "sass-loader" // 加載 SASS / SCSS 文件並將其編譯爲 CSS
        ]
      }
    ]
  }
};

注意一下引用順序,處理是從右到左的,因此上面會先編譯樣式轉成模塊再注入.

index.js 引入樣式

import "./style.scss";

package.json 配置執行命令

"dev": "webpack"

執行命令

npm run dev

執行完看 dist 文件夾仍是隻有 main.js,刷新頁面看到樣式已經被內嵌入頁面的 head 位置了.

還有其餘的一些文件如圖片,字體,文檔等官方文檔很詳細了,這裏略過,直接安裝依賴

yarn add file-loader xml-loader html-loader

圖片處理

當大家在 js 引入圖片的時候,該圖像將被處理並添加到 output 目錄,而且變量將包含該圖像在處理後的最終 url,例如

import img from "./1.jpg";
---------------------------------------
32bbef3c0cb97aa96aaa0a07f3bfc5e4.jpg

一樣標籤中的圖片和樣式中的圖片也會分別使用 html-loader/css-loaderj 進行相似的處理

接下來修改 webpack.config.js

const path = require("path");

module.exports = {
  // 入口
  entry: "./src/index.js",
  // 輸出
  output: {
    // 文件名
    filename: "main.js",
    // 輸出路徑
    path: path.resolve(__dirname, "dist"),
    // 資源請求路徑
    publicPath: '/',
  },

  module: {
    rules: [
      {
        test: /\.scss$/, // 匹配文件
        use: [
          "style-loader", // 使用<style>將css-loader內部樣式注入到咱們的HTML頁面
          "css-loader", // 加載.css文件將其轉換爲JS模塊
          "sass-loader" // 加載 SASS / SCSS 文件並將其編譯爲 CSS
        ]
      },
      {
        test: /\.(png|svg|jpg|gif)$/, // 圖片處理
        use: ["file-loader"]
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/, // 字體處理
        use: ["file-loader"]
      },
      {
        test: /\.xml$/, // 文件處理
        use: ["xml-loader"]
      },
      {
        test: /\.html$/, // 處理html資源如圖片
        use: ["html-loader"]
      }
    ]
  }
};

管理輸出

上面咱們已經完成了基本的 webpack 使用,接着隨着項目增大咱們可能會新增多個入口,切割多份代碼,這時候繼續寫死輸出資源名字就不合適了,因此咱們接下來會引入動態命名.

output.filename 有一些可用的模板

模板 描述
[hash:length(默認 20)] 模塊標識符(module identifier)的 hash
[chunkhash:length(默認 20)]] chunk 內容的 hash
[name] 模塊名稱
[id] 模塊標識符(module identifier)
[query] 模塊的 query,例如,文件名 ? 後面的字符串

而後咱們修改 webpack.config.js 輸入文件名

filename: '[name].bundle.js'

到了這一步就已經可以輸出文件了.可是別急,還有一個關鍵問題是咱們怎麼動態引入文件?

plugin

HtmlWebpackPlugin

HtmlWebpackPlugin 能夠生成建立 html 入口文件,動態引入編譯後的外部資源.
執行命令安裝依賴

yarn add html-webpack-plugin
plugins: [
  new HtmlWebpackPlugin({
    title: "test", // title
    template: "index.html" // 以index爲模板
  })
]

運行命令後看到 dist 文件夾多了一個 index.html,這就是咱們動態建立的入口文件了,直接打開看看效果.

clean-webpack-plugin

執行命令安裝依賴

yarn add clean-webpack-plugin

這個插件能夠幫你每次構建以前先刪除一些沒用的遺留文件,推薦每次打包前先刪除整個 dist 文件
接下來修改 webpack.config.js, plugin 內新增插件

new CleanWebpackPlugin()

webpack-dev-server

webpack-dev-server 爲你提供了一個簡單的 web 服務器,而且可以實時[color=#2b91e3]從新加載(live reloading)
執行命令安裝依賴

yarn add webpack-dev-server

而後咱們修改 webpack.config.js 新增配置

devServer: {
    // 打開模式, Iframe mode和Inline mode最後達到的效果都是同樣的,都是監聽文件的變化,而後再將編譯後的文件推送到前端,完成頁面的reload的
    inline: true,
    // 指定了服務器資源的根目錄
    contentBase: path.join(__dirname, 'dist'),
    // 是否開啓gzip壓縮
    compress: false,
    port: 9000,
    // 是否開啓熱替換功能
    hot: true,
    // 是否自動打開頁面,能夠傳入指定瀏覽器名字打開
    open: true,
},

在 package.json 裏新配置一條命令

"start": "webpack-dev-server"

運行命令以後會自動幫你從瀏覽器中啓動頁面http://localhost:8080/,後續一直監聽代碼變化,改動以後會自動刷新頁面看到效果.
注意的是 webpack-dev-server 輸出的文件只存在於內存中,不輸出真實的文件

模塊熱替換(Hot Module Replacement 或 HMR)

剛纔的 webpack-dev-server 配置雖然方便,可是是屬於總體重載,不少時候咱們只是修改一些小地方的話不必這麼耗費資源
模塊熱替換功能會在應用程序運行過程當中替換、添加或刪除模塊,無需從新加載整個頁面。

第一步,devServer 裏 hot 替換成 hotOnly 配置
第二步,引入 webpack,plugin 里加入

new webpack.HotModuleReplacementPlugin()

webpack.config.js 總體大概如此

const webpack = require('webpack');
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
  mode: "development",
  // 入口
  entry: "./src/index.js",
  // 輸出
  output: {
    // 打包文件名
    filename: "[name].bundle.js",
    // 輸出路徑
    path: path.resolve(__dirname, "dist"),
    // 資源請求路徑
    publicPath: '/',
  },
  devServer: {
    // 打開模式, Iframe mode和Inline mode最後達到的效果都是同樣的,都是監聽文件的變化,而後再將編譯後的文件推送到前端,完成頁面的reload的
    inline: true,
    // 指定了服務器資源的根目錄
    contentBase: path.join(__dirname, 'dist'),
    // 是否開啓gzip壓縮
    compress: false,
    port: 9000,
    // 是否開啓熱替換功能
    // hot: true,
    // 是否自動打開頁面,能夠傳入指定瀏覽器名字打開
    open: true,
    // 是否開啓部分熱替換功能
    hotOnly: true
  },
  module: {
    // 省略
  },
  plugins: [
    // 清除文件
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      // title
      title: "test",
      // 模板
      template: "index.html"
    }),
    // 熱替換模塊
    new webpack.HotModuleReplacementPlugin()
  ]
};

大家能夠新建一個模塊例如test.js

export function log() {
    console.log("123");
}

而後 index.js 修改以下

import "./style.scss";
import {
  log
} from "./test";
console.log(process.env.NODE_ENV);
if (module.hot) {
  module.hot.accept('./test', function () {
    console.log('Accepting the updated printMe module!');
    log();
  })
}

而後分別修改 index.js 和 test.js 看看控制檯更新狀況,如今更新範圍僅限test.js文件了

其餘代碼和框架

社區還有許多其餘 loader 和示例,可使 HMR 與各類框架和庫(library)平滑地進行交互……

  • React Hot Loader:實時調整 react 組件。
  • Vue Loader:此 loader 支持用於 vue 組件的 HMR,提供開箱即用體驗。
  • Elm Hot Loader:支持用於 Elm 程序語言的 HMR。
  • Redux HMR:無需 loader 或插件!只需對 main store 文件進行簡單的修改。
  • Angular HMR:No loader necessary! A simple change to your main * NgModule file is all that's required to have full control over the HMR APIs.沒有必要使用 loader!只需對主要的 NgModule 文件進行簡單的修改,由 HMR API 徹底控制。

tree shaking

webpack 2 正式版本內置支持 ES2015 模塊(也叫作 harmony 模塊)和未引用模塊檢測能力。新的 webpack 4 正式版本,擴展了這個檢測能力,經過 package.json 的 "sideEffects" 屬性做爲標記,向 compiler 提供提示,代表項目中的哪些文件是 "純淨",由此能夠安全地刪除文件中未使用的部分。

基於 ES6 的靜態引用,tree shaking 經過掃描全部 ES6 的 export,找出被 import 的內容並添加到最終代碼中。 webpack 的實現是把全部 import 標記爲有使用/無使用兩種,在後續壓縮時進行區別處理。

咱們能夠先看看效果,在test.js下新增方法

export function dead() {
    console.log(321);
}

index.js修改以下

import {
  log,
  dead
} from "./test";

log();

運行命令以後而後打開dist目錄下的文件找到這段編譯代碼,即便index.js引用了dead方法,可是沒有使用的話編譯文件依然會刪除
圖片描述

按理說應該會包含dead方法引入,可是不知道是否是webpack4優化了這些步驟免去咱們手動解決的煩惱,後續再研究.

source map

應該都知道用來映射代碼方便調試

devtool 構建速度 從新構建速度 生產環境 品質(quality)
(none) +++ +++ yes 打包後的代碼
eval +++ +++ no 生成後的代碼
cheap-eval-source-map + ++ no 轉換過的代碼(僅限行)
cheap-module-eval-source-map o ++ no 原始源代碼(僅限行)
eval-source-map -- + no 原始源代碼
cheap-source-map + o yes 轉換過的代碼(僅限行)
cheap-module-source-map o - yes 原始源代碼(僅限行)
inline-cheap-source-map + o no 轉換過的代碼(僅限行)
inline-cheap-module-source-map o - no 原始源代碼(僅限行)
source-map -- -- yes 原始源代碼
inline-source-map -- -- no 原始源代碼
hidden-source-map -- -- yes 原始源代碼
nosources-source-map -- -- yes 無源代碼內容

+++ 很是快速, ++ 快速, + 比較快, o 中等, - 比較慢, -- 慢

配置環境

項目開發過程通常至少分開發環境和生產環境

開發環境(development)和生產環境(production)的構建目標差別很大。在開發環境中,咱們須要具備強大的、具備實時從新加載(live reloading)或熱模塊替換(hot module replacement)能力的 source map 和 localhost server。而在生產環境中,咱們的目標則轉向於關注更小的 bundle,更輕量的 source map,以及更優化的資源,以改善加載時間。因爲要遵循邏輯分離,咱們一般建議爲每一個環境編寫彼此獨立的 webpack 配置。

執行命令安裝依賴webpack-merge,這個插件能夠合併配置文件輸出

yarn add webpack-merge

建立公用配置文件webpack.common.js

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CleanWebpackPlugin = require("clean-webpack-plugin");

module.exports = {
  // 入口
  entry: "./src/index.js",
  // 輸出
  output: {
    // 打包文件名
    filename: "[name].bundle.js",
    // 輸出路徑
    path: path.resolve(__dirname, "dist"),
    // 資源請求路徑
    publicPath: ""
  },
  module: {
    rules: [
      {
        test: /\.(css|scss)$/, // 匹配文件
        use: [
          "style-loader", // 使用<style>將css-loader內部樣式注入到咱們的HTML頁面
          "css-loader", // 加載.css文件將其轉換爲JS模塊
          "sass-loader" // 加載 SASS / SCSS 文件並將其編譯爲 CSS
        ]
      },
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/, // 圖片處理
        use: ["file-loader"]
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/, // 字體處理
        use: ["file-loader"]
      },
      {
        test: /\.xml$/, // 文件處理
        use: ["xml-loader"]
      },
      {
        test: /\.html$/, // 處理html資源如圖片
        use: ["html-loader"]
      }
    ]
  },
  plugins: [
    // 清除文件
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      // title
      title: "test",
      // 模板
      template: "index.html"
    })
  ]
};

建立生產配置文件webpack.dev.js

const merge = require('webpack-merge');
const common = require('./webpack.common.js');


module.exports = merge(common, {
  mode: "development",
  // 原始源代碼(僅限行)
  devtool: 'cheap-module-eval-source-map',
})

建立生產配置文件webpack.prod.js

const merge = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
    mode: "production",
    // 原始源代碼
    devtool: 'source-map',
})

建立開發環境文件webpack.server.js

const webpack = require("webpack");
const path = require("path");
const merge = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: "development",
  // 原始源代碼(僅限行)
  devtool: 'cheap-module-eval-source-map',
  devServer: {
    // 打開模式, Iframe mode和Inline mode最後達到的效果都是同樣的,都是監聽文件的變化,而後再將編譯後的文件推送到前端,完成頁面的reload的
    inline: true,
    // 指定了服務器資源的根目錄
    contentBase: path.join(__dirname, 'dist'),
    // 是否開啓gzip壓縮
    compress: false,
    port: 9000,
    // 是否開啓熱替換功能
    // hot: true,
    // 是否自動打開頁面,能夠傳入指定瀏覽器名字打開
    open: false,
    // 是否開啓部分熱替換功能
    hotOnly: true
  },
  plugins: [
    // 熱替換模塊
    new webpack.HotModuleReplacementPlugin()
  ]
})

最後package.json命令配置修改以下:

"scripts": {
    "dev": "webpack --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js",
    "start": "webpack-dev-server --config webpack.server.js"
},
  "dependencies": {
    "clean-webpack-plugin": "^2.0.0",
    "css-loader": "^2.1.1",
    "file-loader": "^3.0.1",
    "html-loader": "^0.5.5",
    "html-webpack-plugin": "^3.2.0",
    "node-sass": "^4.11.0",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.1",
    "webpack": "^4.29.6",
    "webpack-cli": "^3.2.3",
    "webpack-dev-server": "^3.2.1",
    "webpack-merge": "^4.2.1",
    "xml-loader": "^1.2.1"
  },

第一階段構建基本完成了,而後咱們能夠再深度拓展一下

相關文章
相關標籤/搜索