從基礎到實戰 手把手帶你掌握新版Webpack4.0(學習筆記)

01 webpack 初探-導學

傳統編程的弊端

之前使用面向對象編程,頁面須要引入多個js,形成多個請求,影響加載,須要注意引用的順序,使用時沒法直接從js裏看出文件的層級關係,一旦出錯調試很麻煩css

// /index.html
<div id="root"></div>
<script src="./header.js"></script>
<script src="./index.js"></script>

// /header.js
function Header() {
  var root = document.getElementById('root')
  var header = document.createElement('div')
  header.innerText = 'header'
  root.appendChild(header)
}

// /index.js
new Header()
複製代碼

使用 webpack(模塊打包工具) 編程

解決傳統編程的弊端html

  • mkdir webpack-test # 建立項目文件夾
  • rmdir webpack-test # 刪除文件夾
  • cd webpack-test # 進入項目文件夾
  • npm init # 初始化包管理器
  • npm init -y # 不一步步的詢問配置項,直接生成一個默認的配置項
  • npm install webpack-cli --save-dev # 安裝 webpack-cli (做用是使咱們能夠在命令行裏運行 webpack)
  • npm uninstall webpack-cli --save-dev # 卸載 webpack-cli
  • npm install webpack --save # 安裝 webpack
  • npm info webpack # 查看 webpack 的相關信息
  • npm install webpack@4.25.0 -S # 安裝指定版本號的 webpack

建立文件寫代碼:vue

// /index.html
<div id="root"></div>

// /header.js
function Header() {
  var root = document.getElementById('root')
  var header = document.createElement('div')
  header.innerText = 'header'
  root.appendChild(header)
}
export default Header

// /index.js
import Header from './header.js'
new Header()
複製代碼

npx webpack index.js # 編譯 index.js 文件,生成 ./dist/main.js 文件node

// /index.html 中引入編譯後的文件
<script src="./dist/main.js"></script>
複製代碼

不一樣的模塊引入方式

  • ES Module 模塊引入方式
export default Header // 導出
import Header from './header.js' // 引入
複製代碼
  • CommonJS 模塊引入方式
module.exports = Header // 導出
var Header = require('./header.js') // 引入
複製代碼

附錄


02 webpack 初探-配置

webpack 的安裝

  • 最好不要全局安裝 webpack,防止不一樣項目使用不一樣的 webpack 版本,沒法同時運行!
  • 將 webpack 直接安裝在項目中,沒法使用全局的 webpack 命令,能夠在前面加上 npx,表示從當前目錄下去找 webpack,例如:npx webpack -v
// package.json
{
  "private": true, // 表示該項目是私有項目,不會被髮送到 npm 的線上倉庫
  "main": "index.js", // 若是項目不被外部引用,則不須要向外部暴露一個 js 文件,可將該行刪除
  "scripts": { // 配置 npm 命令, 簡化命令行輸入的命令
    "build": "webpack", // 不用加 npx, 會優先從項目目錄中去找 webpack; 配置以後可以使用 npm run build 代替 npx webpack
  }
}
複製代碼

webpack 的配置文件

  • webpack 默認配置文件是 webpack.config.js
  • npx webpack --config wConfig.js # 讓 webpack 以 wConfig.js 文件爲配置文件進行打包
// webpack.config.js
const path = require('path') // 引入一個 node 的核心模塊 path

module.exports = {
  entry: './index.js', // 打包入口文件
  // entry: { // 上面是該種寫法的簡寫
  //   main: './index.js'
  // },
  output: {
    filename: 'main.js', // 打包後的文件名
    path: path.resolve(__dirname, 'dist') // 打包後文件的路徑(要指定一個絕對路徑); 經過 path 的 resolve 方法將當前路徑(__dirname)和指定的文件夾名(dist)作一個拼接
  },
  mode: 'production', // 配置打包的模式(production/development); 生產模式(會壓縮)/開發模式(不會壓縮)
}
複製代碼

webpack 打包輸出信息

Hash: d8f9a3dacac977cc0968 # 打包對應的惟一 hash 值
Version: webpack 4.40.2 # 打包使用的 webpack 版本
Time: 208ms # 打包消耗的時間
Built at: 2019-09-20 16:38:59 # 打包的當前時間
  Asset       Size  Chunks             Chunk Names
# 生成的文件 文件大小 文件對應id 文件對應名字
main.js  930 bytes       0  [emitted]  main
Entrypoint main = main.js # 打包的入口文件
[0] ./index.js 36 bytes {0} [built] # 全部被打包的文件

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/configuration/mode/
複製代碼

03 webpack 的核心概念-loader

webpack 默認知道如何打包 js 文件,loader 的做用就是告訴 webpack 如何打包其它不一樣類型的文件react

打包圖片類型的文件

file-loader

使用 file-loader 打包一些圖片文件(須要執行命令 npm i file-loader -D 安裝 file-loader)jquery

// webpack.config.js
module.exports = {
  module: {
    rules: [{
      // test: /\.jpg$/,
      test: /\.(jpg|png|gif)$/, // 配置容許匹配多個文件類型
      use: {
        loader: 'file-loader',
        options: {
          name: '[name]_[hash].[ext]', // 配置打包後文件的名稱(name:文件原名;hash:哈希值;ext:文件後綴;最終生成:文件原名_哈希值.文件後綴),若不配置,文件會以哈希值命名
          outputPath: 'static/img/' // 配置打包後文件放置的路徑位置
        }
      }
    }]
  }
}
複製代碼

url-loader

與 file-loader 相似,還可使用 url-loader 打包一些圖片文件(一樣須要先安裝)webpack

// webpack.config.js
module.exports = {
  module: {
    rules: [{
      test: /\.(jpg|png|gif)$/,
      use: {
        loader: 'url-loader',
        options: {
          name: '[name]_[hash].[ext]',
          outputPath: 'static/img/',
          limit: 10240 // 與 file-loader 不一樣的是,能夠配置 limit 參數(單位:字節),當文件大於 limit 值時,會生成獨立的文件,小於 limit 值時,直接打包到 js 文件裏
        }
      }
    }]
  }
}
複製代碼

注:url-loader 依賴 file-loader,使用 url-loader 同時須要安裝 file-loaderios

打包樣式文件

在 webpack 的配置裏,loader 是有前後順序的,loader 的執行順序是從下到上,從右到左的git

打包 css / sass

注:node-sass沒法安裝時,可採用cnpm或查看node-sass沒法安裝時的解決辦法es6

// webpack.config.js
module.exports = {
  module: {
    rules: [{
      test: /\.css$/, // .css 結尾的文件使用 style-loader 和 css-loader 打包(須要安裝 style-loader 和 css-loader)
      use: ['style-loader', 'css-loader'] // css-loader 會幫咱們分析出多個 css 文件之間的關係,將多個 css 合併成一個 css;style-loader 將 css-loader 處理好的 css 掛載到頁面的 head 部分
    }, {
      test: /\.scss$/, // .scss 結尾的文件使用 style-loader 和 css-loader 和 sass-loader 打包(須要安裝 style-loader 和 css-loader 和 sass-loader 和 node-sass)
      use: ['style-loader', 'css-loader', 'sass-loader'] // 這裏先執行 sass-loader 將 sass 代碼翻譯成 css 代碼;而後再由 css-loader 處理;都處理好了再由 style-loader 將代碼掛載到頁面上
    }]
  }
}

// index.js
// 配置好以後在 js 中引入 css 便可
import './index.css'
複製代碼

打包時自動添加廠商前綴

// webpack.config.js
module.exports = {
  module: {
    rules: [{
      test: /\.css/,
      use: ['postcss-loader'] // 須要執行 npm i postcss-loader -D 安裝 postcss-loader
    }]
  }
}

// postcss.config.js // 在根目錄下建立該文件
module.exports = {
  plugins: [
    require('autoprefixer') // 須要執行 npm i -D autoprefixer 安裝 autoprefixer
  ]
}
複製代碼

給 loader 增長配置項

// webpack.config.js
module.exports = {
  module: {
    rules: [{
      test: /\.css/,
      use: ['style-loader', {
        loader: 'css-loader',
        options: {
          importLoaders: 1 // 有時候會在一個樣式文件裏 import 另外一個樣式文件,這就須要配置 importLoaders 字段,是指在當前 loader 以後指定 n 個數量的 loader 來處理 import 進來的資源(這裏是指在 css-loader 以後使用 sass-loader 來處理 import 進來的資源)
        }
      }, 'sass-loader']
    }]
  }
}
複製代碼

css 打包模塊化(避免全局引入)

// webpack.config.js
module.exports = {
  module: {
    rules: [{
      test: /\.css$/,
      use: ['style-loader', {
        loader: 'css-loader',
        options: {
          modules: true // 開啓 css 的模塊化打包
        }
      }]
    }]
  }
}

// index.css(若使用 sass,增長對應 loader 便可)
.avatar {
  width: 100px;
  height: 100px;
}

// index.js
import style from './index.css'
var img = new Image()
img.src = ''
img.classList.add(style.avatar)
複製代碼

打包字體文件

// webpack.config.js
module.exports = {
  module: {
    rules: [{
      test: /\.(eot|ttf|svg)$/,
      use: ['file-loader']
    }]
  }
}
複製代碼

04 webpack 的核心概念-plugin

plugin 能夠在 webpack 運行到某個時刻的時候幫你作一些事情(相似 vue 的生命週期函數同樣)

html-webpack-plugin(v3.2.0)

  • 時刻:在打包以後開始運行
  • 做用:會在打包結束後,自動生成一個 html 文件,並將打包生成的 js 自動引入到這個 html 文件中
  • 安裝:npm i html-webpack-plugin -D
  • 使用:
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  plugins: [new HtmlWebpackPlugin({
    template: 'index.html' // 指定生成 html 的模版文件(若是不指定,則會生成一個默認的不附帶其它內容的 html 文件)
  })]
}
複製代碼

clean-webpack-plugin(v3.0.0)

  • clean-webpack-plugin 升級3.0踩坑
  • 時刻:在打包以前開始運行
  • 做用:刪除文件夾目錄(默認刪除 output 下 path 指定的目錄)
  • 安裝:npm i clean-webpack-plugin -D
  • 使用:
// webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  plugins: [new CleanWebpackPlugin({
    cleanOnceBeforeBuildPatterns: [path.resolve(__dirname, 'dist')] // 若不配置,默認刪除 output 下 path 指定的目錄
  })]
}
複製代碼

05 webpack 的核心概念-entry&output

打包單個文件

// webpack.config.js
const path = require('path')

module.exports = {
  // entry: './src/index.js', // 簡寫方式
  entry: {
    main: './src/index.js'
  },
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist')
  }
}
複製代碼

打包多個文件

// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: {
    index1: './src/a.js',
    index2: './src/b.js'
  },
  output: {
    publicPath: 'http://cdn.com.cn', // 會在自動生成的 html 文件中,引入文件路徑的前面加上此路徑
    filename: '[name].[hash].js', // name 即指 entry 中配置的須要打包文件的 key (也即 index1 和 index2, 最終會生成 index1.js 和 index2.js)
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [new HtmlWebpackPlugin()]
}
複製代碼

06 webpack 的核心概念-sourceMap

// webpack.config.js
module.exports = {
  devtool: 'source-map'
  // devtool: 'cheap-module-eval-source-map' // 經常使用於開發環境
  // devtool: 'cheap-module-source-map' // 經常使用於生產環境
}
複製代碼
  • devtool 的可能值:
devtool 解釋
none 不生成 sourceMap
source-map 生成 .map 文件
inline-source-map 不生成 .map 文件,sourceMap 會被合併到打包生成的文件裏
cheap-source-map 只告訴出錯的行,不告訴出錯的列
cheap-module-source-map 除了業務代碼裏的錯誤,還要提示一些 loader 裏面的錯誤
eval 不生成 .map 文件,使用 eval 在打包後文件裏生成對應關係

07 webpack 的核心概念-WebpackDevServer

--watch

在 webpack 命令後面加 --watch,webpack 會監聽打包的文件,只要文件發生變化,就會自動從新打包

// package.json
{
  "scripts": {
    "watch": "webpack --watch"
  }
}
複製代碼

webpack-dev-server

  • npm i webpack-dev-server -D # 安裝 webpack-dev-server 包
  • 配置
// webpack.config.js
module.exports = {
  devServer: {}
}

// package.json
{
  "scripts": {
    "wdserver": "webpack-dev-server"
  }
}
複製代碼
  • npm run wdserver # 編譯項目到內存中,並啓動一個服務
  • devServer 有不少可配置的參數:
open: true // 啓動服務的時候自動在瀏覽器中打開當前項目(默認 false)
port: 8888 // 自定義啓動服務的端口號(默認 8080)

contentBase: './static' // 指定資源的請求路徑(默認 當前路徑)
例如:
/static 文件夾下存在一張圖片 /static/img.png
devServer 裏配置 contentBase: './static'
/index.html 中使用 <img src="img.png" />
這樣它就會去 /static 文件夾下去找 img.png 而不是從根目錄下去找 img.png
複製代碼

express & webpack-dev-middleware

藉助 express 和 webpack-dev-middleware 本身手動搭建服務

  • npm i express webpack-dev-middleware -D # 安裝 express 和 webpack-dev-middleware 包
  • 根目錄下新建 server.js
// server.js(在 node 中使用 webpack)
const express = require('express')
const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpackConfig = require('./webpack.config.js')
const complier = webpack(webpackConfig)

const app = express()

app.use(webpackDevMiddleware(complier, {}))

app.listen(3000, () => {
  console.log('server is running at port 3000')
})
複製代碼
  • 配置 npm 命令
// package.json
{
  "scripts": {
    "nodeserver": "node server.js"
  }
}
複製代碼
  • npm run nodeserver # 編譯項目並啓動服務(成功後在瀏覽器輸入 localhost:3000 訪問項目)

附錄

在命令行中使用 webpack

webpack index.js -o main.js # 編譯 index.js 輸出 main.js


08 webpack 的核心概念-HotModuleReplacementPlugin

HotModuleReplacementPlugin 是 webpack 自帶的一個插件,不須要單獨安裝

配置

// webpack.config.js
const webpack = require('webpack')

module.exports = {
  devServer: {
    hot: true, // 讓 webpack-dev-server 開啓 hot module replacement 這樣的一個功能
    hotOnly: true // 即使是 hot module replacement 的功能沒有生效,也不讓瀏覽器自動刷新
  },
  plugins: [new webpack.HotModuleReplacementPlugin()]
}
複製代碼

在 css 中使用

更改樣式文件,頁面就不會整個從新加載,而是隻更新樣式

// /index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>html 模板</title>
  </head>
  <body></body>
</html>
複製代碼
// /src/index.css
div {
  width: 100px;
  height: 100px;
}
div:nth-of-type(odd) {
  background-color: rgb(255, 0, 0);
}
複製代碼
// /src/index.js
import './index.css'

var btn = document.createElement('button')
btn.innerText = 'button'
document.body.appendChild(btn)

btn.onclick = () => {
  var item = document.createElement('div')
  item.innerText = 'item'
  document.body.appendChild(item)
}
複製代碼
// /package.json
{
  "name": "webpack-test",
  "version": "1.0.0",
  "description": "",
  "private": false,
  "scripts": {
    "wdserver": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "css-loader": "^3.2.0",
    "html-webpack-plugin": "^3.2.0",
    "style-loader": "^1.0.0",
    "webpack": "^4.41.1",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.2"
  }
}
複製代碼
// /webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')

module.exports = {
  entry: {
    main: './src/index.js'
  },
  output: {
    publicPath: '/',
    filename: '[name].[hash].js',
    path: path.resolve(__dirname, 'dist')
  },
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'index.html'
    }),
    new webpack.HotModuleReplacementPlugin()
  ],
  devServer: {
    hot: true,
    hotOnly: true
  }
}
複製代碼

在 js 中使用

更改 number.js 文件中的代碼,只會從頁面上移除 id 爲 number 的元素,而後從新執行一遍 number() 方法,不會對頁面上的其它部分產生影響,也不會致使整個頁面的重載

// /index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>html 模板</title>
  </head>
  <body></body>
</html>
複製代碼
// /src/counter.js
function counter() {
  var div = document.createElement('div')
  div.setAttribute('id', 'counter')
  div.innerText = 1
  div.onclick = function() {
    div.innerText = parseInt(div.innerText, 10) + 1
  }
  document.body.appendChild(div)
}

export default counter
複製代碼
// /src/number.js
function number() {
  var div = document.createElement('div')
  div.setAttribute('id', 'number')
  div.innerText = 20
  document.body.appendChild(div)
}

export default number
複製代碼
// /src/index.js
import counter from './counter'
import number from './number'

counter()
number()

// 相比 css 須要本身書寫重載的代碼,那是由於 css-loader 內部已經幫 css 寫好了這部分代碼
if (module.hot) {
  module.hot.accept('./number', () => {
    // 監測到代碼發生變化,就會執行下面的代碼
    document.body.removeChild(document.getElementById('number'))
    number()
  })
}
複製代碼
// /package.json
{
  "name": "webpack-test",
  "version": "1.0.0",
  "description": "",
  "private": false,
  "scripts": {
    "wdserver": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.41.1",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.2"
  }
}
複製代碼
// /webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')

module.exports = {
  entry: {
    main: './src/index.js'
  },
  output: {
    publicPath: '/',
    filename: '[name].[hash].js',
    path: path.resolve(__dirname, 'dist')
  },
  mode: 'production',
  plugins: [
    new HtmlWebpackPlugin({
      template: 'index.html'
    }),
    new webpack.HotModuleReplacementPlugin()
  ],
  devServer: {
    hot: true,
    hotOnly: true
  }
}
複製代碼

09 webpack 的核心概念-使用babel處理ES6語法

babel 官網

使用 babel

點擊去官網查看(選擇webpack)

  1. npm i -D babel-loader @babel/core @babel/preset-env # 安裝 babel-loader 和 @babel/core 和 @babel/preset-env
  • babel-loader: 是 webpack 和 babel 通信的橋樑,使 webpack 和 babel 打通,babel-loader 並不會把 js 中的 ES6 語法轉換成 ES5 語法
  • @babel/core: 是 babel 的核心語法庫,它可以讓 babel 識別 js 代碼裏面的內容並作轉化
  • @babel/preset-env: 將 ES6 語法轉換成 ES5 語法,它包含了全部 ES6 語法轉換成 ES5 語法的翻譯規則
  1. 配置
// webpack.config.js
module.exports = {
  module: {
    rules: [{
      test: /\.js$/,
      exclude: /node_modules/, // 不去匹配 node_modules 文件夾下的 js
      loader: 'babel-loader',
      options: {
        presets: ['@babel/preset-env']
      }
    }]
  }
}
複製代碼

上面的步驟,只是作了語法上的翻譯(如: let/const/箭頭函數/... 都會被轉換),但一些新的變量和方法並無被翻譯(如: promise/.map()/...),這時就要使用 @babel/polyfill 來處理

@babel/polyfill

使用 @babel/polyfill

  1. npm i -D @babel/polyfill # 安裝 @babel/polyfill
  2. import '@babel/polyfill' # 在入口文件 index.js 第一行引入 @babel/polyfill

像上面配置好以後,會發現打包後的文件特別大,由於一些沒用到的 ES6 語法也被打包了進去,所以須要作以下操做

  • 參考文檔
  • npm i -D core-js # 安裝 core-js(v3.3.2)
  • 刪除入口文件 index.js 中的 import '@babel/polyfill'
  • 配置
// webpack.config.js
module.exports = {
  module: {
    rules: [{
      test: /\.js$/,
      exclude: /node_modules/,
      loader: 'babel-loader',
      options: {
        presets: [
          [
            '@babel/preset-env',
            {
              corejs: 3,
              useBuiltIns: 'usage',
              targets: { // 經過 targets 指定項目運行的環境,打包時會自動判斷是否須要去解析轉化代碼
                chrome: '67'
              }
            }
          ]
        ]
      }
    }]
  }
}
複製代碼

若是寫的是業務代碼,可採用上面方法使用 polyfill 去打包;若是是開發組件或者庫的話,可以使用 plugin-transform-runtime polyfill 會污染全局環境,plugin-transform-runtime 會以閉包的形式幫助組件去引入相關內容 @babel/plugin-transform-runtime 官方文檔


10 webpack 的核心概念-打包React框架代碼

@babel/preset-react 文檔

// /index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>html 模板</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
複製代碼
// /src/index.js
import React, { Component } from 'react'
import ReactDom from 'react-dom'

class App extends Component {
  render() {
    return <div>Hello World</div>
  }
}

ReactDom.render(<App />, document.getElementById('app'))
複製代碼
// /.babelrc
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "corejs": 3,
        "useBuiltIns": "usage",
        "targets": {
          "chrome": 67
        }
      }
    ],
    "@babel/preset-react"
  ]
}
複製代碼
// /package.json
{
  "name": "webpack-test",
  "version": "1.0.0",
  "description": "",
  "private": false,
  "scripts": {
    "wdserver": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.6.4",
    "@babel/polyfill": "^7.6.0",
    "@babel/preset-env": "^7.6.3",
    "@babel/preset-react": "^7.6.3",
    "@babel/runtime-corejs3": "^7.6.3",
    "babel-loader": "^8.0.6",
    "clean-webpack-plugin": "^3.0.0",
    "core-js": "^3.3.2",
    "html-webpack-plugin": "^3.2.0",
    "react": "^16.10.2",
    "react-dom": "^16.10.2",
    "webpack": "^4.41.1",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.2"
  }
}
複製代碼
// /webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const webpack = require('webpack')

module.exports = {
  entry: {
    main: './src/index.js'
  },
  output: {
    publicPath: './',
    filename: '[name].[hash].js',
    path: path.resolve(__dirname, 'dist')
  },
  mode: 'development',
  devtool: 'cheap-module-eval-source-map',
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader'
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'index.html'
    }),
    new CleanWebpackPlugin({
      cleanOnceBeforeBuildPatterns: [path.resolve(__dirname, 'dist')]
    }),
    new webpack.HotModuleReplacementPlugin()
  ],
  devServer: {
    hot: true,
    hotOnly: true
  }
}
複製代碼

11 webpack 的高級概念-TreeShaking

  • TreeShaking 是基於 ES6 的靜態引用,經過掃描全部 ES6 的 export,找出被 import 的內容並添加到最終代碼中,排除不使用的代碼
  • TreeShaking 只支持 ES Module 的引入方式,不支持 CommonJS 的引入方式
  • 主要用於生產環境,須要在 package.json 中配置 "sideEffects": false (對全部的模塊都進行 TreeShaking 處理)
  • 若是引入的庫並無導出任何內容(如: import '@babel/polyfill'),就須要配置 "sideEffects": ["@babel/polyfill"],讓 TreeShaking 不對 @babel/polyfill 進行處理
  • 若是引入樣式文件(如: import './style.css'),則需配置 "sideEffects": ["*.css"]
  • 若要在開發環境使用 TreeShaking ,需在 webpack.config.js 中配置
module.exports = {
  optimization: {
    usedExports: true
  }
}
複製代碼

12 webpack 的高級概念-dev&prod模式的區分打包

  1. npm i -D webpack-merge # 安裝 webpack-merge 模塊,做用是將公共的 webpack 配置代碼與開發 / 生產環境中的 webpack 配置代碼進行合併

  2. /build/webpack.common.js # 存放公共的 webpack 配置代碼

// 示例僅展現部分代碼(下同)
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  entry: {
    main: './src/index.js'
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'index.html'
    }),
    new CleanWebpackPlugin({
      cleanOnceBeforeBuildPatterns: [path.resolve(process.cwd(), 'dist')] // __dirname => process.cwd()
    })
  ],
  output: {
    filename: '[name].[hash].js',
    path: path.resolve(__dirname, '../dist') // dist => ../dist
  }
}
複製代碼
  1. /build/webpack.dev.js # 存放開發環境的 webpack 配置代碼
const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')

const devConfig = {
  mode: 'development'
}
module.exports = merge(commonConfig, devConfig)
複製代碼
  1. /build/webpack.prod.js # 存放生產環境的 webpack 配置代碼
const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')

const prodConfig = {
  mode: 'production'
}
module.exports = merge(commonConfig, prodConfig)
複製代碼
  1. /package.json # 配置打包命令
{
  "scripts": {
    "dev": "webpack-dev-server --config ./build/webpack.dev.js",
    "build": "webpack --config ./build/webpack.prod.js"
  }
}
複製代碼
  1. 運行命令
  • npm run dev # 開發模式
  • npm run build # 生產模式

附錄

  • process.cwd() # 執行 node 命令的文件夾地址
  • __dirname # 執行 js 的文件目錄
  • path.join(path1, path2, path3, ...) # 將路徑片斷鏈接起來造成新的路徑
  • path.resolve([from...], to) # 將一個路徑或路徑片斷的序列解析爲一個絕對路徑,至關於執行 cd 操做

13 webpack 的高級概念-CodeSplitting

  • CodeSplitting:代碼分割,代碼分割和 webpack 無關
  • npm i -S lodash # 安裝 lodash

手動代碼分割(配置多個入口文件)

// /src/lodash.js
import _ from 'lodash'
window._ = _
複製代碼
// /src/index.js
console.log(_.join(['a', 'b', 'c'])) // 輸出a,b,c
console.log(_.join(['a', 'b', 'c'], '***')) // 輸出a***b***c
複製代碼
// /build/webpack.common.conf.js
module.exports = {
  entry: {
    lodash: './src/lodash.js',
    main: './src/index.js'
  }
}
複製代碼

自動代碼分割

webpack 中的代碼分割底層使用的是 SplitChunksPlugin 這個插件

webpack 中實現同步代碼分割(須要配置optimization)

// /src/index.js
import _ from 'lodash'

console.log(_.join(['a', 'b', 'c'])) // 輸出a,b,c
console.log(_.join(['a', 'b', 'c'], '***')) // 輸出a***b***c
複製代碼
// /build/webpack.common.conf.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
}
複製代碼

webpack 中實現異步代碼分割(經過 import 引入等,不須要任何配置)

// /src/index.js
function getComponent() {
  return import('lodash').then(({ default: _ }) => {
    var element = document.createElement('div')
    element.innerHTML = _.join(['hello', 'world'], '-')
    return element
  })
}

getComponent().then(element => {
  document.body.appendChild(element)
})
複製代碼

經過 import('lodash') 引入,分割打包後的文件名稱是 [id].[hash].js,打包後文件的辨識度不高; 使用 import(/* webpackChunkName: "lodash" */ 'lodash') 來爲打包後的文件起別名,提高辨識度(最終生成文件名稱爲:vendors~lodash.[hash].js,意思是符合 vendors 組的規則,入口是main),詳情可搜索查看 SplitChunksPlugin 的配置 這種方式被稱爲魔法註釋,詳情可查看魔法註釋 Magic Comments 官網地址

注意: 若是報錯「Support for the experimental syntax 'dynamicImport' isn't currently enabled」,可安裝 @babel/plugin-syntax-dynamic-import 進行解決

@babel/plugin-syntax-dynamic-import 官網地址

// npm i -D @babel/plugin-syntax-dynamic-import # 安裝模塊包

// /.babelrc # 配置
{
  "plugins": ["@babel/plugin-syntax-dynamic-import"]
}
複製代碼

附錄

  • 打包後輸出文件的命名
// /build/webpack.common.conf.js
module.exports = {
  output: {
    filename: '[name].[hash].js', // 入口文件根據 filename 命名
    chunkFilename: '[name].chunk.js', // 非入口文件根據 chunkFilename 命名
    path: path.resolve(__dirname, '../dist')
  }
}
複製代碼

14 webpack 的高級概念-SplitChunksPlugin

SplitChunksPlugin 官網地址

// SplitChunksPlugin 的默認配置
// webpack.common.conf.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'async', // async:作代碼分割時,只對異步代碼生效;all:對同步和異步代碼都生效;initial:只對同步代碼生效
      minSize: 30000, // 單位:字節;當打包的庫大於 minSize 時才作代碼分割,小於則不作代碼分割
      maxSize: 0, // 當打包的庫大於 maxSize 時,嘗試對其進行二次分割,通常不作配置
      minChunks: 1, // 當一個模塊被用了至少 minChunks 次時,纔對其進行代碼分割
      maxAsyncRequests: 5, // 同時加載的模塊數最可能是 maxAsyncRequests 個,若是超過 maxAsyncRequests 個,只對前 maxAsyncRequests 個類庫進行代碼分割,後面的就不作代碼分割
      maxInitialRequests: 3, // 整個網站首頁(入口文件)加載的時候,入口文件引入的庫進行代碼分割時,最多隻能分割 maxInitialRequests 個js文件
      automaticNameDelimiter: '~', // 打包生成文件名稱之間的鏈接符
      name: true, // 打包起名時,讓 cacheGroups 裏的名字有效
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/, // 若是是從 node_modules 裏引入的模塊,就打包到 vendors 組裏
          priority: -10 // 指定該組的優先級,若一個類庫符合多個組的規則,就打包到優先級最高的組裏
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true // 若是一個模塊已經被打包過了(一個模塊被多個文件引用),那麼再打包的時候就會忽略這個模塊,直接使用以前被打包過的那個模塊
        }
      }
    }
  }
}
複製代碼

注:SplitChunksPlugin 上面的一些配置須要配合 cacheGroups 裏的配置一塊兒使用才能生效(如 chunks 的配置)

// webpack.common.conf.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          filename: 'vendors.js' // 配置 filename 以後,打包會以 filename 的值爲文件名,生成的文件是 vendors.js
        },
        default: false
      }
    }
  }
}
複製代碼

15 webpack 的高級概念-LazyLoading&Chunk

LazyLoading

LazyLoading:懶加載並非 webpack 裏面的概念,而是 ES 裏面的概念;何時執行,何時纔會去加載對應的模塊

  • 下面這種同步代碼的寫法,打包時將分割後的模塊對應的 js 文件直接經過 script 標籤在 html 中引入,頁面開始加載的時候就會去加載這些 js,致使頁面加載很慢
// /src/index.js
import _ from 'lodash'

document.addEventListener('click', () => {
  var element = document.createElement('div')
  element.innerHTML = _.join(['hello', 'world'], '-')
  document.body.appendChild(element)
})
複製代碼
  • 下面這種異步代碼的寫法能夠實現一種懶加載的行爲,在點擊界面的時候纔會去加載須要的模塊
// /src/index.js
function getComponent() {
  return import(/* webpackChunkName: "lodash" */ 'lodash').then(
    ({ default: _ }) => {
      var element = document.createElement('div')
      element.innerHTML = _.join(['hello', 'world'], '-')
      return element
    }
  )
}

document.addEventListener('click', () => {
  getComponent().then(element => {
    document.body.appendChild(element)
  })
})
複製代碼

使用 ES7 的 async 和 await 後,上面代碼能夠改寫成下面這種寫法,效果等同

// /src/index.js
async function getComponent() {
  const { default: _ } = await import(/* webpackChunkName: "lodash" */ 'lodash')
  const element = document.createElement('div')
  element.innerHTML = _.join(['hello', 'world'], '-')
  return element
}

document.addEventListener('click', () => {
  getComponent().then(element => {
    document.body.appendChild(element)
  })
})
複製代碼

Chunk

打包後生成的每個 js 文件,都是一個 chunk


16 webpack 的高級概念-打包分析

webpack 打包分析工具

webpack 打包分析工具的 GitHub 倉庫地址

  1. 配置打包命令
// /package.json
{
  "scripts": {
    "build": "webpack --profile --json > stats.json"
  }
}
複製代碼
  1. npm run build # 運行命令
  • 打包後會在根目錄生成一個 stats.json 文件,它裏面包含的信息就是對打包過程的一個描述
  • 將 stats.json 上傳至分析打包描述文件網址,便可查看詳細的分析介紹

附錄:

除了 webpack 官方提供的分析工具,還有不少其它的分析工具,可查看GUIDES/Code Splitting/Bundle Analysis

  • webpack-chart:webpack stats 可交互餅圖。
  • webpack-visualizer:可視化並分析你的 bundle,檢查哪些模塊佔用空間,哪些多是重複使用的。
  • webpack-bundle-analyzer:一個 plugin 和 CLI 工具,它將 bundle 內容展現爲便捷的、交互式、可縮放的樹狀圖形式。
  • webpack bundle optimize helper:此工具會分析你的 bundle,併爲你提供可操做的改進措施建議,以減小 bundle 體積大小。

谷歌瀏覽器自帶的覆蓋率工具

  • F12 打開谷歌瀏覽器控制檯,點擊右上角的三個點,選擇 More tools/coverage,點擊第一個記錄按鈕開啓捕獲記錄頁面代碼的使用率,刷新頁面便可查看
  • 頁面開始加載時並不會執行的代碼,若是在頁面加載時就讓它下載下來,就會浪費頁面執行的效率,利用 coverage 這個工具就能夠知道哪些代碼使用到了,哪些沒有使用到,好比像下面這種點擊交互的代碼,就能夠放到一個異步加載的模塊裏,從而提升頁面的執行效率

改寫前:

// /src/index.js
document.addEventListener('click', () => {
  var element = document.createElement('div')
  element.innerHTML = 'hello world'
  document.body.appendChild(element)
})
複製代碼

改寫後:

// /src/handleClick.js
function handleClick() {
  const element = document.createElement('div')
  element.innerHTML = 'hello world'
  document.body.appendChild(element)
}
export default handleClick

// /src/index.js
document.addEventListener('click', () => {
  import('./handleClick.js').then(({ default: func }) => {
    func()
  })
})
複製代碼
  • 因此 webpack 作代碼分割打包配置時 chunks 的默認是 async,而不是 all 或者 initial;由於 webpack 認爲只有異步加載這樣的組件才能真正的提升網頁的打包性能,而同步的代碼只能增長一個緩存,實際上對性能的提高是很是有限的
// /build/webpack.common.conf.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'async' // 默認(async)只對異步代碼作分割
    }
  }
}
複製代碼

Preloading & Prefetching

Prefetching/Preloading modules 官網地址

  • 若是全部代碼都寫到異步組件裏,等到事件觸發時纔去加載對應的模塊,勢必會致使操做交互的反應變慢,所以須要引入 Preloading 和 Prefetching 的概念
  • 經過魔法註釋的寫法去使用
/* webpackPrefetch: true */
/* webpackPreload: true */
複製代碼
// /src/handleClick.js
function handleClick() {
  const element = document.createElement('div')
  element.innerHTML = 'hello world'
  document.body.appendChild(element)
}
export default handleClick

// /src/index.js
document.addEventListener('click', () => {
  import(/* webpackPrefetch: true */ './handleClick.js').then(({ default: func }) => {
    func()
  })
})
複製代碼
  • 如上代碼,在主要 js 加載完成,網絡帶寬有空閒的時候,會自動把 handleClick.js 加載好,再觸發點擊時,雖然仍會去加載 handleClick.js ,但它是從緩存中去找的
  • 區別: Prefetching 是在首頁(主要的 js)加載完成,網絡空閒的時候去下載異步組件交互的代碼;Preloading 是和主業務文件一塊兒加載的
  • 注意: webpackPrefetch 在某些瀏覽器裏會有一些兼容問題

17 webpack 的高級概念-CSS文件的代碼分割

  • MiniCssExtractPlugin 官網地址
  • 舊版本的 MiniCssExtractPlugin 由於不支持HMR,因此最好只在生產環境中使用,若是放在開發環境中,更改樣式後須要手動刷新頁面,會下降開發的效率;新版本已支持開發環境中使用HMR

使用步驟

1. 安裝模塊包

npm install --save-dev mini-css-extract-plugin

2. 配置

// /build/webpack.common.conf.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  plugins: [
    new MiniCssExtractPlugin({})
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader, // 使用了 MiniCssExtractPlugin.loader 就不須要 style-loader 了
          'css-loader'
        ]
      }
    ]
  }
}
複製代碼

注意: 若是使用了 TreeShaking (排除未使用的代碼)還需配置

// /package.json
{
  "sideEffects": ["*.css"]
}
複製代碼

3. 打包輸出

// /package.json
{
  "scripts": {
    "build": "webpack --config ./build/webpack.prod.conf.js"
  }
}
複製代碼

npm run build

CSS 打包拓展

打包文件的命名

// /build/webpack.common.conf.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css', // 打包後的 css 若是被頁面直接引用,就以 filename 的規則命名
      chunkFilename: '[name].chunk.css' // 打包後的 css 若是是間接引用的,就以 chunkFilename 的規則命名
    })
  ]
}
複製代碼

打包文件的壓縮

  1. npm i -D optimize-css-assets-webpack-plugin # 安裝模塊包
  2. 引入並使用
// /build/webpack.prod.conf.js
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
  optimization: {
    minimizer: [new OptimizeCSSAssetsPlugin({})]
  }
}
複製代碼

多個 css 的打包

  • 一個入口文件引入多個 css 文件,默認將其打包合併到一個 css 裏
// /src/index.js
import './index1.css'
import './index2.css'
複製代碼
  • 多個入口文件引入不一樣的 css 文件,打包默認會產生多個 css 文件,可經過配置,使其合併爲一個 css 文件
// /build/webpack.prod.conf.js
module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          name: 'styles', // 打包後的文件名
          test: /\.css$/, // 匹配全部 .css 結尾的文件,將其放到該組進行打包
          chunks: 'all', // 無論是同步仍是異步加載的,都打包到該組
          enforce: true // 忽略默認的一些參數(好比minSize/maxSize/...)
        }
      }
    }
  }
}
複製代碼
  • 多個入口文件引入多個 css 文件的打包

根據入口文件的不一樣,將 css 文件打包到不一樣的文件裏 參考Extracting CSS based on entry 官網地址


18 webpack 的高級概念-瀏覽器緩存

  • hash:它是工程級別的,修改任何一個文件,它的值都會改變
  • chunkhash:它會根據不一樣的入口文件進行依賴解析(即:同一個入口文件,對應的 css 改變了,即便對應的 js 沒有改變,其 chunkhash 的值也會改變)
  • contenthash:它是針對內容級別的,只要源代碼不變,它的值就不變
// /build/webpack.prod.conf.js
module.exports = {
  output: {
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].js'
  }
}
複製代碼

注意:

對於老版本的 webpack,即使沒有對源代碼作任何的變動,有可能兩次打包的 contenthash 值也不同,這是由於打包生成的文件之間存在關聯,這些關聯代碼叫作 manifest,存在於各個文件中,可經過額外的配置,將關聯代碼提取出來

// /build/webpack.common.conf.js
module.exports = {
  optimization: {
    runtimeChunk: {
      name: 'runtime' // 打包後會多出一個 runtime.js 用於存儲文件之間的關聯代碼
    }
  }
}
複製代碼

附錄

  • 打包後文件太大,webpack 會報警告,可經過配置忽略警告
// /build/webpack.common.conf.js
module.exports = {
  performance: false
}
複製代碼

19 webpack 的高級概念-Shimming

自動引入依賴庫

  • 有時候,咱們引入了一個庫,裏面可能會依賴一些別的庫
  • 當咱們調用引入庫裏面的方法時,即便在當前 js 中加載了依賴庫,仍然會報 xxx is not defined
  • 這時,就須要使用 webpack 自帶的一個插件 ProvidePlugin
  • 它的做用是:(以: 'jquery'爲例) 若是一個模塊中使用了 字符串,就在該模塊中自動引入 jquery 模塊,而後將 jquery 模塊的名字叫作 ,即自動加入這樣的一行代碼:import from 'jquery'
// /src/jquery.ui.js
export function ui() {
  $('body').css('background-color', _.join(['green'], ''))
}
複製代碼
// /src/index.js
import { ui } from './jquery.ui'
ui()
複製代碼
// /build/webpack.common.conf.js
const webpack = require('webpack')
module.exports = {
  plugins: [new webpack.ProvidePlugin({
    $: 'jquery',
    _: 'lodash',
    _join: ['lodash', 'join'] // 若是想直接使用 _join 替代 lodash 的 join 方法,能夠這樣配置
  })]
}
複製代碼

改變模塊中的 this 指向

  • 模塊中的 this 通常都指向的是模塊自身,若是想改變 this 的指向,能夠藉助 imports-loader 模塊
  1. npm i -D imports-loader # 安裝模塊包
  2. 使用
// /build/webpack.common.conf.js
module.exports = {
  module: {
    rules: [{
      test: /\.js$/,
      exclude: /node_modules/,
      use: [{
        loader: 'babel-loader'
      }, {
        loader: 'imports-loader?this=>window'
      }]
    }]
  }
}
複製代碼
  1. 做用 當加載一個 js 文件時,首先會走 imports-loader,它會把這個 js 文件(模塊)裏面的 this 改爲 window,而後再交給 babel-loader 去編譯

總結

以上這些更改 webpack 打包的一些默認行爲,或者說實現一些 webpack 原始打包實現不了的效果,的行爲都叫作 Shimming (墊片的行爲)


20 webpack 的高級概念-環境變量

環境變量的使用

// /build/webpack.prod.conf.js
const prodConfig = {
  // ...
}
module.exports = prodConfig
複製代碼
// /build/webpack.dev.conf.js
const devConfig = {
  // ...
}
module.exports = devConfig
複製代碼
// /build/webpack.common.conf.js
const merge = require('webpack-merge')
const devConfig = require('./webpack.dev.conf.js')
const prodConfig = require('./webpack.prod.conf.js')
const commonConfig = {
  // ...
}
module.exports = (env) => {
  if (env && env.production) {
    return merge(commonConfig, prodConfig)
  } else {
    return merge(commonConfig, devConfig)
  }
}
複製代碼
// package.json
{
  "scripts": {
    "dev": "webpack-dev-server --config ./build/webpack.common.conf.js",
    "build": "webpack --env.production --config ./build/webpack.common.conf.js"
  }
}
複製代碼
  • 打包命令中加入 --env.production,默認會給 production 賦 true 值
  • 還能夠指定具體的值,如:--env.production=abc

21 webpack 實戰-Library的打包

示例代碼及配置

// /src/index.js
export function add(a, b) {
  return a + b
}
export function minus(a, b) {
  return a - b
}
export function multiply(a, b) {
  return a * b
}
export function division(a, b) {
  return a / b
}
複製代碼
// /webpack.config.js
const path = require('path')
module.exports = {
  mode: 'production',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js-math.js',
    library: 'jsMath',
    libraryTarget: 'umd'
  }
}
複製代碼

配置詳解及使用示例

配置 library: 'jsMath'

  • 打包後的 js 支持 script 標籤引入使用
  • 打包生成的代碼會掛載到 jsMath 這個全局變量上
// /dist/index.html
<script src="./js-math.js"></script>
<script>
  console.log(jsMath.add(2, 4)) // 6
</script>
複製代碼

配置 libraryTarget: 'umd'

  • u: 表明通用(universally)
  • 打包後的代碼可在 ES2015/CommonJS/AMD 環境中使用
  • 不支持 script 標籤直接引用使用
// ES2015 module import:
import jsMath from 'js-math'
jsMath.add(2, 3)

// CommonJS module require:
const jsMath = require('js-math')
jsMath.add(2, 3)

// AMD module require:
require(['js-math'], function(jsMath) {
  jsMath.add(2, 3)
})
複製代碼

libraryTarget 的一些其餘值

  • libraryTarget 的值還能夠配合 library 的值使用
libraryTarget: 'var' // 讓 library 的值做爲全局變量使用
libraryTarget: 'this' // 將 library 的值掛載到 this 對象上使用
libraryTarget: 'window' // 將 library 的值掛載到 window 對象上使用
libraryTarget: 'umd' // 使其支持在 ES2015/CommonJS/AMD 中使用
複製代碼

自定義庫中引入其它庫

  • 有時候咱們會在自定義的庫裏面引入一些其它的庫,好比:
  • import _ from 'lodash'
  • 若是別人使用咱們這個庫的同時,又使用了 lodash
  • 最後打包時,就會在代碼中產生2份 lodash,致使重複代碼
  • 解決方案:配置 externals 參數
  • Externals 官網連接
// /webpack.config.js
module.exports = {
  // externals: ['lodash'] // 表示咱們的庫在打包時不把 lodash 打包進去,而是讓業務代碼去加載 lodash

  externals: { // 詳細配置
    lodash: {
      root: '_', // 表示若是 lodash 是經過 script 標籤引入的,必須在頁面上注入一個名爲 _ 的全局變量,這樣才能正確執行
      commonjs: 'lodash' // 表示經過 CommonJS 這種寫法去加載時,名稱必須起爲 lodash,如:const lodash = require('lodash')
    }
  }
}
複製代碼

發佈庫供別人使用(何嘗試)

  1. 配置入口
// /package.json
{
  "main": "./dist/js-math.js"
}
複製代碼
  1. npm 官網註冊 npm 賬號

  2. 運行命令 npm adduser # 添加用戶信息 npm publish # 將庫上傳到 npm

  3. npm install js-math # 安裝使用


22 webpack 實戰-PWA的打包

PWA 的介紹及使用

  • PWA:(Progressive Web Application)即便服務器掛了,依然可以訪問頁面
  1. npm i -D workbox-webpack-plugin # 安裝模塊包

  2. 配置:

// /build/webpack.prod.conf.js
const WorkboxPlugin = require('workbox-webpack-plugin')

module.exports = {
  plugins: [
    new WorkboxPlugin.GenerateSW({
      clientsClaim: true,
      skipWaiting: true
    })
  ]
}
複製代碼
// /src/index.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/service-worker.js')
      .then(registration => {
        console.log('service-worker registed')
      })
      .catch(error => {
        console.log('service-worker register error')
      })
  })
}
複製代碼
  1. npm run build # 打包項目 打包後會多出兩個文件:precache-manifest.js 和 service-worker.js

  2. 啓動一個服務,訪問打包後的項目 斷開服務,刷新瀏覽器,項目仍能正常訪問

附錄

啓動一個本地服務

  1. npm i -D http-server # 安裝模塊包

  2. 配置命令,在 dist 目錄下啓動一個服務

// /package.json
{
  "scripts": {
    "httpServer": "http-server dist"
  }
}
複製代碼
  1. npm run httpServer # 運行命令 打開瀏覽器,訪問:http://127.0.0.1:8080/index.html 注:要在訪問地址後加 /index.html,不然可能會出現報錯

23 webpack 實戰-TypeScript的打包

TypeScript 介紹

  • TypeScript 官網
  • TypeScript 是微軟推出的一個產品,它規範了一套 JavaScript 語法
  • TypeScript 是 JavaScript 的一個超集,支持 JavaScript 裏面的全部語法,同時還提供了一些額外的語法
  • TypeScript 最大的優點就是能夠規範咱們的代碼,還能夠方便的對代碼進行報錯提示
  • 用 TypeScript 編寫代碼,可有效的提升 JavaScript 代碼的可維護性
  • TypeScript 文件後綴通常都是 .ts 或者 .tsx

用 webpack 打包 TypeScript 代碼

  1. npm i -D typescript ts-loader # 安裝模塊包

  2. 編寫代碼及配置

// /src/index.tsx
class Greeter {
  greeting: string
  constructor(message: string) {
    this.greeting = message
  }
  greet() {
    return 'Hello, ' + this.greeting
  }
}

let greeter = new Greeter('world')
// let greeter = new Greeter(123) // 因爲 Greeter 中限定了數據類型爲 string,這裏若是傳非 string 的數據,就會在代碼中報錯
alert(greeter.greet())
複製代碼
// /webpack.config.js
const path = require('path')

module.exports = {
  mode: 'production',
  entry: './src/index.tsx',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/
      }
    ]
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  }
}
複製代碼
// /tsconfig.json
{
  "compilerOptions": {
    "outDir": "./dist", // 用 ts-loader 作 TypeScript 代碼打包時,將打包生成的文件放到 ./dist 目錄下(不寫也行,由於 webpack.config.js 中已經配置了)
    "module": "es6", // 指的是用 ES Module 模塊的引用方式(即:若是在 index.tsx 文件裏引入其它模塊的話,須要經過 import ... 這種方式去引入)
    "target": "es5", // 指的是打包 TypeScript 語法時,要將最終的語法轉換成什麼形式
    "allowJs": true // 容許在 TypeScript 語法裏引入 js 模塊
  }
}
複製代碼
  1. npm run build # 打包代碼

在 TypeScript 中引入其它庫(以 lodash 爲例)

雖然在寫 TypeScript 代碼時,會有很好的錯誤提示,但有時在 TypeScript 代碼中引入一些其它的庫,調用其它庫的方法時,並無錯誤提示,須要執行如下步驟:

  1. npm i -D @types/lodash # 須要額外安裝 @types/lodash 模塊包
  2. 須要經過 import * as _ from 'lodash' 引入,而不是 import _ from 'lodash'

若是不肯定是否有對應庫的類型文件的支持,能夠在GitHub上搜索 DefinitelyTyped,打開後下面有個 TypeSearch 的連接,去 TypeSearch 頁面裏搜索,若是搜索到了,就說明它有對應庫的類型文件的支持,而後安裝便可


24 webpack 實戰-請求轉發

  • devServer.proxy 官網連接
  • 使用 WebpackDevServer 實現開發環境下的請求轉發
  • 依賴 WebpackDevServer,須要安裝 webpack-dev-server 模塊
// /src/index.js // 使用 axios 模擬請求
import axios from 'axios'
axios.get('/api/data.json').then(res => {
  console.log(res)
})
複製代碼
// /webpack.config.js
module.exports = {
  devServer: {
    proxy: {
      // '/api': 'http://...' // 簡寫,若是請求是以 /api 開頭的,就將其代理到 http://... 進行請求
      '/api': {
        target: 'http://...', // 若請求地址以 /api 開頭,將其代理到 http://... 進行請求
        secure: false, // 若是請求地址是 https 的話,須要配置此項
        pathRewrite: { // 對一些請求路徑的重寫
          'data.json': 'data-test.json'
        },
        changeOrigin: true, // 能夠幫助咱們改變請求裏的 origin,跳過一些服務端的 origin 驗證
        headers: { // 請求轉發時改變請求頭,模擬一些數據
          host: 'www...',
          cookie: '123...'
        }
      }
    }
  }
}
複製代碼

25 webpack 實戰-單頁面應用的路由

// /src/home.js
import React, { Component } from 'react'

class Home extends Component {
  render() {
    return <div>HomePage</div>
  }
}

export default Home
複製代碼
// /src/list.js
import React, { Component } from 'react'

class List extends Component {
  render() {
    return <div>ListPage</div>
  }
}

export default List
複製代碼
// /src/index.js
import React, { Component } from 'react' // 須要安裝 react 庫
import { BrowserRouter, Route } from 'react-router-dom' // 須要安裝 react-router-dom 庫
import ReactDom from 'react-dom' // 須要安裝 react-dom 庫
import Home from './home.js'
import List from './list.js'

class App extends Component {
  render() {
    return (
      <BrowserRouter>
        <div>
          <Route path="/" exact component={Home} />
          <Route path="/list" component={List} />
        </div>
      </BrowserRouter>
    )
  }
}

ReactDom.render(<App />, document.getElementById('root')) // 須要在 html 中寫一個 id 爲 root 的容器
複製代碼
// /webpack.config.js
module.exports = {
  devServer: {
    historyApiFallback: true // 只需配置該參數,便可經過不一樣的路由加載不一樣的 js
    // 注意:這種方法只適用於開發環境中,上線使用須要後端作路由映射處理
  }
}
複製代碼

26 webpack 實戰-ESLint的配置

ESLint的使用

  1. npm i -D eslint # 安裝模塊包
  2. npx eslint --init # 初始 eslint,根據項目實際狀況作一些選擇,生成 eslint 配置文件 .eslintrc.js
// /.eslintrc.js
module.exports = {
  env: { // 指定代碼的運行環境。不一樣的運行環境,全局變量不同,指明運行環境這樣 ESLint 就能識別特定的全局變量。同時也會開啓對應環境的語法支持
    browser: true,
    es6: true,
  },
  extends: [
    'plugin:vue/essential',
    'airbnb-base',
  ],
  globals: {
    Atomics: 'readonly',
    SharedArrayBuffer: 'readonly',
  },
  parserOptions: {
    ecmaVersion: 2018,
    sourceType: 'module',
  },
  plugins: [
    'vue',
  ],
  rules: { // 這裏能夠對規則進行細緻的定義,覆蓋 extends 中的規則
  },
};
複製代碼
  1. 執行檢測
  • npx eslint ./src # 檢測 ./src 目錄下的全部文件是否符合規則
  • npx eslint ./src/index.js # 檢測某一個文件是否符合規則

其它

  • 初始化時,只有選擇"To check syntax, find problems, and enforce code style"時,才能夠選擇 Airbnb, Standard, Google 標準
  • eslint 配置文件(.eslintrc.js)的格式最好選擇 JavaScript 格式,由於 json 格式不支持代碼註釋,而且在須要根據環境變量來作不一樣狀況處理時十分無力
  • 執行 npx eslint ... 檢測命令時,加上 --fix (即:npx eslint --fix ...)能夠自動修正一些代碼風格的問題(如:代碼後加分號等),但代碼錯誤的問題不會修改
  • 若使用 VSCode 開發工具,可安裝 ESLint 插件,安裝後會在代碼中自動提示錯誤信息

在 webpack 中配置 ESLint

// /webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: ['babel-loader', {
          loader: 'eslint-loader', // 在 babel-loader 處理以前先用 eslint-loader 檢測一下
          options: {
            fix: true, // 做用同 npx eslint --fix ...
            cache: true // 能夠下降 ESLint 對打包過程性能的損耗
            // force: 'pre' // 無論 eslint-loader 放在什麼位置,強制它最早執行
          }
        }]
      }
    ]
  },
  devServer: {
    overlay: true // 配置此項後,【開發環境】在瀏覽器打開項目時,eslint 檢查的一些報錯信息就會以浮層的形式在瀏覽器中展現
  }
}
複製代碼
  • 真實項目通常不會在 webpack 中配置 eslint-loader,由於它會下降打包速度
  • 通常在項目提交時去作 ESLint 的檢測,檢測經過才容許提交

27 webpack 實戰-性能優化

提高打包速度

1. 跟上技術的迭代(webpack/node/npm/yarn/...)

  • 儘量使用新版本的工具,由於新版本中作了更多的優化

2. 在儘量少的模塊上應用 loader

  • 經過配置 exclude / include 減小 loader 的使用
// /webpack.config.js
const path = require('path')

module.exports = {
  module: {
    rules: [{
      exclude: /node_modules/ // 排除應用規則的目錄
      // include: path.resolve(__dirname, './src') // 限定應用規則的目錄
    }]
  }
}
複製代碼

3. Plugin 儘量精簡併確保可靠

  • 儘可能使用官方推薦的插件,官方的優化的更好

4. resolve 參數合理配置

// /webpack.config.js
module.exports = {
  resolve: {
    extensions: ['.js', '.jsx'], // 當咱們引入一個組件,未指定後綴時(如:import Child from './child/child'),它會自動先去找 ./child/child.js,若是沒有,再去找 ./child/child.jsx,合理的配置可減小查找匹配的次數,下降性能損耗

    mainFiles: ['index', 'child'], // 配置該項後,當咱們引入一個文件夾路徑時(如:import Child from './child/'),它就會自動先去找該文件夾下的 index,若是沒有,再去找 child。同上,該配置不易過多,不然影響性能

    alias: { // 配置別名
      child: path.resolve(__dirname, './src/child') // 使用時就能夠這樣寫:import Child from 'child'
    }
  }
}
複製代碼

5. 使用 DllPlugin 提升打包速度

5.1 單獨打包庫文件

  • 以 lodash 和 jquery 爲例:
// /build/webpack.dll.js
const path = require('path')

module.exports = {
  mode: 'production',
  entry: {
    vendors: ['lodash', 'jquery']
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, '../dll'),
    library: '[name]' // 打包生成一個庫,並暴露在全局變量 [name](即:vendors)中
  }
}
複製代碼
// /package.json
{
  "scripts": {
    "build:dll": "webpack --config ./build/webpack.dll.js"
  },
  "dependencies": {
    "jquery": "^3.4.1",
    "lodash": "^4.17.15"
  }
}
複製代碼
  • npm run build:dll # 運行打包命令,輸出 /dll/vendors.dll.js 文件
  • /dll/vendors.dll.js 便是打包全部庫產生的新的庫文件,它裏面包含了 lodash 和 jquery 的源碼

5.2 利用插件將單獨打包產生的新的庫文件引入到生產打包的代碼中

  • npm i -D add-asset-html-webpack-plugin # 安裝模塊包並配置
// /webpack.config.js
const path = require('path')
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')

module.exports = {
  plugins: [
    new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, './dll/vendors.dll.js') // 指的是要往 HtmlWebpackPlugin 生成的 index.html 里加的內容
    })
  ]
}
複製代碼
// /package.json
{
  "scripts": {
    "build": "webpack"
  }
}
複製代碼
  • npm run build # 運行打包命令,會複製一份 vendors.dll.js 到 /dist/ 目錄下,並在 /dist/index.html 文件中引入 vendors.dll.js

至此,第三方模塊只打包一次,並引入生產打包代碼中的目標已經實現了 可是 /src/index.js 中 import _ from 'lodash' 使用的仍是 node_modules 裏面的庫 接下來須要實現的是:引入第三方模塊的時候,讓它從 dll 文件裏引入,而不是從 node_modules 裏引入

5.3 構建映射,使用模塊時,讓其從 dll 文件里加載

// /build/webpack.dll.js
// 經過該配置文件打包會生成相似於庫的打包結果

const path = require('path')
const webpack = require('webpack')

module.exports = {
  plugins: [
    new webpack.DllPlugin({
      // 使用 webpack 自帶的插件對打包產生的庫文件進行分析
      // 把庫裏面一些第三方模塊的映射關係放到 path 對應的文件裏

      name: '[name]', // 暴露出的 DLL 函數的名稱
      path: path.resolve(__dirname, '../dll/[name].manifest.json') // 分析結果文件輸出的絕對路徑
    })
  ]
}
複製代碼
  • npm run build:dll # 執行打包 dll 命令,產生 /dll/[name].dll.js 新的庫文件和 /dll/[name].manifest.json 映射文件
  • 有了這個映射文件,打包業務代碼時,就會對源代碼進行分析
  • 若是分析出使用的內容是在 /dll/[name].dll.js 裏,那麼,它就會使用 /dll/[name].dll.js 裏的內容,就不會去 node_modules 裏引入模塊了
  • 接下來就是:結合全局變量和生成的 /dll/[name].manifest.json 映射文件進行 webpack 的配置
// /webpack.config.js
module.exports = {
  plugins: [
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, './dll/vendors.manifest.json')
    })
  ]
}
複製代碼
  • npm run build # 運行打包命令,打包耗時就會減小了

5.4 打包生成多個新的庫文件

  • 配置多個入口文件
// /build/webpack.dll.js
module.exports = {
  entry: {
    vendors: ['lodash', 'jquery'],
    react: ['react', 'react-dom']
  }
}
複製代碼
  • 結合 5.3 的配置,此時打包輸出的文件有: /dll/vendors.dll.js /dll/vendors.manifest.json /dll/react.dll.js /dll/react.manifest.json

  • 而後配置 /webpack.config.js

// /webpack.config.js
module.exports = {
  plugins: [
    new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, './dll/vendors.dll.js')
    }),
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, './dll/vendors.manifest.json')
    }),
    new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, './dll/react.dll.js')
    }),
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, './dll/react.manifest.json')
    })
  ]
}
複製代碼
  • 最後執行 npm run build 打包便可

注:若是打包生成的 dll 文件有不少,就須要在 /webpack.config.js 中添加不少的 plugin,爲了簡化代碼,能夠藉助 node 去分析 dll 文件夾下的文件,循環處理,代碼以下:

// /webpack.config.js
const fs = require('fs') // 藉助 node 中的 fs 模塊去讀取 dll 文件夾

const plugins = [ // 初始存入一些基礎的 plugin
  new HtmlWebpackPlugin({
    template: './src/index.html'
  })
]

const files = fs.readdirSync(path.resolve(__dirname, './dll'))
files.forEach(file => {
  if (/.*\.dll.js/.test(file)) {
    plugins.push(new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, './dll', file)
    }))
  }
  if (/.*\.manifest.json/.test(file)) {
    plugins.push(new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, './dll', file)
    }))
  }
})

module.exports = {
  plugins
}
複製代碼
  • 配置完成後,需再次運行 npm run build:dll 生成 dll 文件和映射文件
  • 最後再次執行 npm run build 打包便可

6. 控制包文件大小

  • 不要引入一些未使用的模塊包
  • 配置 Tree-Shaking,打包時,不打包一些引入但未使用的模塊
  • 配置 splitChunks,對代碼進行合理拆分,將大文件拆成小文件打包

7. thread-loader/parallel-webpack/happypack 多進程打包

  • webpack 默認是經過 nodeJs 來運行的,是單進程的打包
  • 可使用 thread-loader / parallel-webpack / happypack 這些技術,配置多進程打包

8. 合理使用 sourceMap

  • 打包生成的 sourceMap 越詳細,打包的速度就越慢,可根據不一樣的環境配置不一樣的 sourceMap

9. 結合 stats 分析打包結果

  • 根據打包分析的結果,作對應的優化

10. 開發環境內存編譯

  • 開發環境使用 webpack-dev-server,啓動服務後,會將編譯生成的文件放到內存中,而內存的讀取速度遠遠高於硬盤的讀取速度,可讓咱們在開發環境中,webpack 性能獲得很大的提高

11. 開發環境無用插件剔除

  • 例如:開發環境無需對代碼進行壓縮等

28 webpack 實戰-多頁面打包配置

多頁面打包配置

  1. 新建多個頁面的 js
// /src/index.js
console.log('home page')

// /src/list.js
console.log('list page')
複製代碼
  1. 配置 webpack
// /build/webpack.common.conf.js
module.exports = {
  entry: { // 配置多個入口文件
    main: './src/index.js',
    list: './src/list.js'
  },
  plugins: [
    // 配置多個打包輸出頁面
    new HtmlWebpackPlugin({
      template: 'src/index.html',
      filename: 'index.html',
      chunks: ['runtime', 'vendors', 'main'] // 不一樣的頁面引入不一樣的入口文件(如有 runtime 或者 vendors 就引入,沒有就不寫)
    }),
    new HtmlWebpackPlugin({
      template: 'src/index.html',
      filename: 'list.html',
      chunks: ['runtime', 'vendors', 'list']
    })
  ]
}
複製代碼
  1. npm run build # 執行打包後輸出的 index.html 中會引入 main.js,list.html 中會引入 list.js

如上,若是每增長一個頁面,就手動增長代碼的話,就會致使大量重複代碼,下面開始對打包配置代碼進行優化:

優化多頁面打包配置代碼

// /build/webpack.common.conf.js
const fs = require('fs')

const makePlugins = configs => { // 自定義方法 makePlugins,用於動態生成 plugins
  const plugins = [
    // 初始能夠存一些基本的 plugin,如:CleanWebpackPlugin
  ]

  // 根據不一樣的入口文件,生成不一樣的 html
  // Object.keys() 方法會返回一個由給定對象枚舉屬性組成的數組
  Object.keys(configs.entry).forEach(item => {
    plugins.push(new HtmlWebpackPlugin({
      template: 'src/index.html',
      filename: `${item}.html`,
      chunks: [item]
    }))
  })

  // 動態添加並使用打包生成的一些第三方 dll 庫
  const files = fs.readdirSync(path.resolve(__dirname, '../dll'))
  files.forEach(file => {
    if (/.*\.dll.js/.test(file)) {
      plugins.push(new AddAssetHtmlWebpackPlugin({
        filepath: path.resolve(__dirname, '../dll', file)
      }))
    }
    if (/.*\.manifest.json/.test(file)) {
      plugins.push(new webpack.DllReferencePlugin({
        manifest: path.resolve(__dirname, '../dll', file)
      }))
    }
  })

  return plugins
}

const configs = {
  // 將 module.exports 導出的一堆配置放到變量 configs 裏
  entry: {
    index: './src/index.js',
    list: './src/list.js'
  }
  // ...
  // 這裏不寫 plugins,經過一個方法去生成 plugins
}

configs.plugins = makePlugins(configs) // 調用 makePlugins 自定義的方法,生成 plugins

module.exports = configs // 導出重組好的 configs
複製代碼
  • 如需增長頁面,只要多配置一個入口文件便可

29 webpack 底層原理-編寫Loader

  • 實際上 loader 就是一個函數,這個函數可接收一個參數,這個參數指的就是引入文件的源代碼
  • 注意: 這個函數不能寫成箭頭函數,由於要用到 this,webpack 在調用 loader 時,會把這個 this 作一些變動,變動以後,才能用 this 裏面的方法,若是寫成箭頭函數,this 指向就會有問題

如何編寫一個 Loader

// /src/index.js
console.log('hello world !')
複製代碼
// /loaders/replaceLoader.js
module.exports = function(source) {
  return source.replace('hello', '你好') // 對源代碼執行一個替換
}
複製代碼
// /package.json
{
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "^4.29.0",
    "webpack-cli": "^3.2.1"
  }
}
複製代碼
// /webpack.config.js
const path = require('path')

module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  module: {
    rules: [{
      test: /\.js$/,
      use: [
        path.resolve(__dirname, './loaders/replaceLoader.js')
      ]
    }]
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  }
}
複製代碼
  • npm run build # 打包輸出,運行輸出文件便可查看打印的‘hello’被替換成了‘你好’,這就是一個簡單的 loader

在 /loaders/replaceLoader.js 中,除了經過 return 返回處理後的源代碼以外,還可使用 this.callback 作返回處理

// /loaders/replaceLoader.js
module.exports = {
  const result = source.replace('hello', '你好')
  this.callback(null, result)
}
複製代碼

Loader 配置傳參

// /loaders/replaceLoader.js
module.exports = function(source) {
  // 可經過 this.query 獲取使用 loader 時 options 裏面傳遞的配置
  console.log(this.query) // { name: 'xiaoli' }
  return source.replace('hello', '你好')
}
複製代碼
// /webpack.config.js
const path = require('path')

module.exports = {
  module: {
    rules: [{
      test: /\.js$/,
      use: [{
        loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
        options: {
          name: 'xiaoli'
        }
      }]
    }]
  }
}
複製代碼
  • 有時候傳遞過來的參數會比較詭異(好比傳的是對象,接收的多是字符串),因此官方推薦使用 loader-utils 模塊去分析傳遞的內容

使用 loader-utils 分析 loader 配置

  • npm i -D loader-utils # 安裝模塊包
  • 使用:
// /loaders/replaceLoader.js
const loaderUtils = require('loader-utils')

module.exports = {
  const options = loaderUtils.getOptions(this)
  console.log(options) // { name: 'xiaoli' }
}
複製代碼

loader 裏執行異步操做

若是 loader 裏調用一些異步的操做(好比延遲 return),打包就會報錯,說 loader 沒有返回內容,須要使用 this.async()

// /loaders/replaceLoaderAsync.js
module.exports = function(source) {
  const callback = this.async()
  setTimeout(() => {
    const result = source.replace('hello', '你好')
    callback(null, result) // 這樣調用的 callback 實際上就是 this.callback()
  }, 1000)
}
複製代碼

多個 loader 的使用

// /loaders/replaceLoaderAsync.js
module.exports = function(source) {
  const callback = this.async()
  setTimeout(() => {
    const result = source.replace('hello', '你好')
    callback(null, result)
  }, 1000)
}
複製代碼
// /loaders/replaceLoader.js
module.exports = function(source) {
  const result = source.replace('world', '世界')
  this.callback(null, result)
}
複製代碼
// /webpack.config.js
module.exports = {
  module: {
    rules: [{
      test: /\.js$/,
      use: [{
        loader: path.resolve(__dirname, './loaders/replaceLoader.js')
      }, {
        loader: path.resolve(__dirname, './loaders/replaceLoaderAsync.js')
      }]
    }]
  }
}
複製代碼

簡化 loader 的引入方式

// /webpack.config.js
module.exports = {
  resolveLoader: {
    // 當你引入一個 loader 的時候,它會先到 node_modules 裏面去找,若是找不到,再去 loaders 目錄下去找
    modules: ['node_modules', './loaders']
  },
  module: {
    rules: [{
      test: /\.js$/,
      use: [{
        loader: 'replaceLoader'
      }, {
        loader: 'replaceLoaderAsync'
      }]
    }]
  }
}
複製代碼

30 webpack 底層原理-編寫Plugin

loader 和 plugin 的區別:

  • loader 的做用是:幫咱們去處理模塊,當咱們在源代碼裏面去引入一個新的 js 文件(或其它格式文件)的時候,能夠藉助 loader 處理引用的文件
  • plugin 的做用是:當咱們在打包時,在某些具體時刻上(好比打包結束自動生成一個 html 文件,就可使用 html-webpack-plugin 插件)去作一些處理
  • loader 是一個函數
  • plugin 是一個類

一個簡單 plugin 的編寫及使用

// /plugins/copyright-webpack-plugin.js
class CopyrightWebpackPlugin {
  // 構造函數
  constructor() {
    console.log('插件被使用了')
  }

  // 當調用插件時,會執行 apply 方法,該方法接收一個參數 compiler,能夠理解爲 webpack 的實例
  apply(compiler) {}
}

module.exports = CopyrightWebpackPlugin
複製代碼
// /src/index.js
console.log('hello world !')
複製代碼
// /package.json
{
  "name": "plugin",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10"
  }
}
複製代碼
// /webpack.config.js
const path = require('path')
const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin.js')

module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  plugins: [new CopyrightWebpackPlugin()],
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  }
}
複製代碼
  • npm run build # 運行打包命令,便可在命令行中查看到「插件被使用了」的輸出信息

plugin 傳參

// /webpack.config.js
const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin.js')

module.exports = {
  plugins: [new CopyrightWebpackPlugin({
    name: 'li' // 這裏傳遞參數
  })]
}
複製代碼
// /plugins/copyright-webpack-plugin.js
class CopyrightWebpackPlugin {
  constructor(options) {
    // 經過 options 接收參數
    console.log(options)
  }

  apply(compiler) {}
}

module.exports = CopyrightWebpackPlugin
複製代碼

打包結束時刻生成額外的文件

  • Compiler Hooks 官網連接
  • compiler.hooks 裏面有一些相似 vue 生命週期函數的東西(是在特定的時刻,會自動執行的鉤子函數)
// /plugins/copyright-webpack-plugin.js
class CopyrightWebpackPlugin {
  apply(compiler) {
    // emit 是指當你把打包的資源放到目標文件夾的時刻
    compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, callback) => {
      console.log('插件執行了')
      callback()
    })
  }
}

module.exports = CopyrightWebpackPlugin
複製代碼
  • 至此,在打包的指定時刻運行代碼已經實現
  • 接下來在指定時刻,向打包內容裏增長文件
// /plugins/copyright-webpack-plugin.js
class CopyrightWebpackPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, callback) => {
      // 打包生成的全部內容是存放在 compilation.assets 裏面的
      // 在 emit 時刻的時候,向打包生成的內容裏增長一個 copyright.txt 文件
      compilation.assets['copyright.txt'] = {
        // 文件裏的內容
        source: function() {
          return 'copyright text ...'
        },
        // 文件的大小
        size: function() {
          return 18
        }
      }
      callback()
    })
  }
}
複製代碼

藉助 node 進行調試

  • --inspect # 開啓 node 的調試工具
  • --inspect-brk # 在 webpack.js 的第一行上打個斷點
  • 直接運行 node node_modules/webpack/bin/webpack.js 就至關於運行 webpack
// /package.json
{
  "scripts": {
    "debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js"
  }
}
複製代碼
  • npm run debug 運行後,打開瀏覽器控制檯,點擊左上角綠色的圖標,便可進入 node 的調試

附錄

其它的打包時刻

  • done (異步時刻)表示打包完成

  • compile (同步時刻)

  • 同步時刻是 tap 且沒有 callback

  • 異步時刻是 tapAsync 有 callback

compiler.hooks.compile.tap('CopyrightWebpackPlugin', (compilation) => {
  console.log('compile 時刻執行')
})
複製代碼

31 webpack 底層原理-Bundler源碼編寫-模塊分析

1. 讀取項目的入口文件

2. 分析入口文件裏的代碼

// /src/word.js
export const word = 'hello'
複製代碼
// /src/message.js
import { word } from './word.js'
// 須要寫 .js 後綴,由於沒有使用 webpack

const message = `say ${word}`

export default message
複製代碼
// /src/index.js
import message from './message.js'

console.log(message)
複製代碼

npm i @babel/parser # 做用是分析代碼,產生抽象語法樹

npm i @babel/traverse # 做用是幫助咱們快速找到 import 節點

// /bundler.js
// 此文件就是咱們要作的打包工具
// 打包工具是用 nodeJs 來編寫的

// node 的一個用於讀取文件的模塊
const fs = require('fs')
const path = require('path')
// 使用 babelParser 分析代碼,產生抽象語法樹
const parser = require('@babel/parser')
// 默認導出的內容是 ESModule 的導出,若是想用 export default 導出內容,須要在後面加個 .default
const traverse = require('@babel/traverse').default

const moduleAnalyser = filename => {
  // 以 utf-8 編碼讀取入口文件的內容
  const content = fs.readFileSync(filename, 'utf-8')
  // console.log(content)

  // 分析文件內容,輸出抽象語法樹
  const ast = parser.parse(content, {
    sourceType: 'module'
  })
  // ast.program.body 便是文件內容中的節點
  // console.log(ast.program.body)

  const dependencies = {}
  // 對抽象語法樹進行遍歷,找出 Node.type === 'ImportDeclaration' 的元素,並作處理
  traverse(ast, {
    ImportDeclaration({ node }) {
      // console.log(node)
      const dirname = path.dirname(filename)
      const newFile = './' + path.join(dirname, node.source.value)
      dependencies[node.source.value] = newFile
    }
  })
  // console.log(dependencies)

  // 將入口文件和對應依賴返回出去
  return {
    filename, // 入口文件
    dependencies // 入口文件裏的依賴
  }
}

// 傳入口文件,調用方法
moduleAnalyser('./src/index.js')
複製代碼
  • babel-core 官網連接
  • babel-preset-env 官網連接
  • 對代碼分析完成以後還須要將 ES6 代碼轉化成瀏覽器能夠運行的代碼
  • npm i @babel/core # 安裝 @babel/core
  • npm i @babel/preset-env # 安裝 @babel/preset-env
  • babelCore 裏的 transformFromAst() 方法,能夠將 ast 抽象語法樹轉化成瀏覽器能夠運行的代碼
// /bundler.js
const babel = require('@babel/core')

const moduleAnalyser = filename => {
  const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
  })
  // code 就是瀏覽器能夠運行的代碼
  return {
    code
  }
}

// 分析轉化以後的結果
const moduleInfo = moduleAnalyser('./src/index.js')
console.log(moduleInfo)
複製代碼

附錄

  • 在命令行高亮顯示代碼

npm i cli-highlight -g // 安裝 cli-highlight node bundler.js | highlight // 運行時在後面加上 | highlight


32 webpack 底層原理-Bundler源碼編寫-DependenciesGraph

  • 在上一節代碼的基礎上繼續編寫
// /bundler.js
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

const moduleAnalyser = filename => {
  const content = fs.readFileSync(filename, 'utf-8')
  const ast = parser.parse(content, {
    sourceType: 'module'
  })

  const dependencies = {}
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(filename)
      const newFile = './' + path.join(dirname, node.source.value)
      dependencies[node.source.value] = newFile
    }
  })

  const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
  })

  return {
    filename,
    dependencies,
    code
  }
}

const makeDependenciesGraph = entry => {
  const entryModule = moduleAnalyser(entry)
  const graphArray = [ entryModule ]

  // 循環遍歷依賴中的依賴
  for(let i = 0; i < graphArray.length; i++) {
    const item = graphArray[i]
    const { dependencies } = item
    if (dependencies) {
      for(let j in dependencies) {
        graphArray.push(
          moduleAnalyser(dependencies[j])
        )
      }
    }
  }

  // 將遍歷後的依賴數組轉化成對象的形式
  const graph = {}
  graphArray.forEach(item => {
    graph[item.filename] = {
      dependencies: item.dependencies,
      code: item.code
    }
  })
  return graph
}

// 最終分析生成的代碼和依賴信息
const graphInfo = makeDependenciesGraph('./src/index.js')
console.log(graphInfo)
複製代碼
  • 接下來轉到下一節生成代碼

33 webpack 底層原理-Bundler源碼編寫-生成代碼

  • 在上一節代碼的基礎上繼續編寫
// /bundler.js
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

const moduleAnalyser = filename => {
  const content = fs.readFileSync(filename, 'utf-8')
  const ast = parser.parse(content, {
    sourceType: 'module'
  })

  const dependencies = {}
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(filename)
      const newFile = './' + path.join(dirname, node.source.value)
      dependencies[node.source.value] = newFile
    }
  })

  const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
  })

  return {
    filename,
    dependencies,
    code
  }
}

const makeDependenciesGraph = entry => {
  const entryModule = moduleAnalyser(entry)
  const graphArray = [ entryModule ]

  for(let i = 0; i < graphArray.length; i++) {
    const item = graphArray[i]
    const { dependencies } = item
    if (dependencies) {
      for(let j in dependencies) {
        graphArray.push(
          moduleAnalyser(dependencies[j])
        )
      }
    }
  }

  const graph = {}
  graphArray.forEach(item => {
    graph[item.filename] = {
      dependencies: item.dependencies,
      code: item.code
    }
  })
  return graph
}

const generateCode = entry => {
  const graph = JSON.stringify(makeDependenciesGraph(entry))
  return `
    (function(graph) {
      function require(module) {
        function localRequire(relativePath) {
          return require(graph[module].dependencies[relativePath])
        }
        var exports = {}
        (function(require, exports, code) {
          eval(code)
        })(localRequire, exports, graph[module].code)
        return exports
      }
      require('${entry}')
    })(${graph})
  `
}

const code = generateCode('./src/index.js')
console.log(code)
複製代碼
  • node bundler # 運行編譯輸出能夠在瀏覽器中運行的代碼以下:
  • 【注:】在命令行復制輸出的代碼到瀏覽器控制檯運行時,須要檢查一下是否有不正確的換行,不一樣的命令行工具可能致使一些不正確的換行,直接複製到瀏覽器運行會致使報錯(Uncaught SyntaxError: Invalid or unexpected token)
(function(graph) {
  function require(module) {
    function localRequire(relativePath) {
      return require(graph[module].dependencies[relativePath]);
    };
    var exports = {};
    (function(require, exports, code) {
      eval(code);
    })(localRequire, exports, graph[module].code);
    return exports;
  };
  require('./src/index.js');
})({"./src/index.js":{"dependencies":{"message.js":"./src\\message.js"},"code":"\"use strict\";\n\nvar _message = _interopRequireDefault(require(\"message.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_message[\"default\"]);"},"./src\\message.js":{"dependencies":{"./word.js":"./src\\word.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _word = require(\"./word.js\");\n\nvar message = \"say \".concat(_word.word);\nvar _default = message;\nexports[\"default\"] = _default;"},"./src\\word.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.word = void 0;\nvar word = 'hello';\nexports.word = word;"}});
複製代碼
  • 運行輸出:"say hello"

34 腳手架工具配置分析-CreateReactApp&VueCLI3

CreateReactApp

  • create-react-app 官網連接
  • npx create-react-app my-app # 建立項目
  • cd my-app # 進入項目文件夾
  • npm start # 啓動項目
  • npm run eject # 將 react 隱藏的一些配置顯示出來(此操做不可逆!)

VueCLI3

  • VueCLI3 官網連接
  • npm i -g @vue/cli # 安裝 VueCLI
  • vue create my-project # 建立項目
  • cd my-project # 進入項目文件夾
  • npm run serve # 啓動項目
  • /vue.config.js # 可在根目錄建立 vue.config.js 而後根據官網文檔書寫 webpack 的相關配置
相關文章
相關標籤/搜索