詳談webpack4

Alex丶Cheng發表於2019-04-12

前言

下載啥的就不多說了,就看看我們專案中經常用到的一些配置。碼字不易,喜歡的話點個?哦 ~~~

webpack

webpack只是一個打包模組的機制,只是把依賴的模組轉化成可以代表這些包的靜態檔案。webpack就是識別你的 入口檔案。識別你的模組依賴,來打包你的程式碼。至於你的程式碼使用的是commonjs還是amd或者es6的import。webpack都會對其進行分析。來獲取程式碼的依賴。webpack做的就是分析程式碼。轉換程式碼,編譯程式碼,輸出程式碼。webpack本身是一個node的模組,所以webpack.config.js是以commonjs形式書寫的(node中的模組化是commonjs規範的) webpack中每個模組有一個唯一的id,是從0開始遞增的。整個打包後的bundle.js是一個匿名函式自執行。引數則為一個陣列。陣列的每一項都為個function。function的內容則為每個模組的內容,並按照require的順序排列。

clean-webpack-plugin

清空打包檔案。new CleanWebpackPlugin(), 可以不傳參,配置參考這裡

source map

sourcemap就是一個資訊檔案,裡面儲存著位置資訊。目的是為了解決開發程式碼與實際執行程式碼不一致時幫助我們debug到原始開發程式碼的技術。

  • 配置 devtool 屬性 ( 最佳實踐 )
  • development: cheap-module-eval-source-map
  • production: cheap-module-source-map
  • 五個關鍵字任意組合: eval,source-map,cheap,module,inline
  1. eval: 包裹模組,在模組尾新增模組來源 //#sourceURL,通過 sourceURL 找到原始程式碼位置,不單獨產生.map 檔案
  2. source-map: 產生.map檔案,包含原始程式碼和執行程式碼的對映關係
  3. 參考文獻

webpack-dev-server

npm i webpack-dev-server -D

  • 注意:打包的檔案不會放到dist目錄中了,而是放在我們的記憶體中,從而提升了打包速度。
devServer: {
    // open: true, // 開啟瀏覽器
    // port: 8080, // default
    hot: true, // HMR,不重新整理頁面就能應用你改過的css樣式
    hotOnly: true, // 如果HMR沒生效,也不重新整理頁面
    contentBase: './dist', // 告訴伺服器從哪裡提供內容。只有在您希望提供靜態檔案時才需要這樣做。
    proxy: {
      '/api': 'http://localhost:3000' // 如果我們訪問localhost:8000/api ,則轉發請求到localhost:3000
    }
  },
複製程式碼
  • 通過配置hot: true 和 webpack.HotModuleReplacementPlugin() 可以及時更新css樣式而不重新整理頁面!
  • 如果更變js程式碼,保證其它程式碼的狀態不發生變化,則需要另外加一段程式碼,如下:
+ if (module.hot) {
+   module.hot.accept('./print.js', function() {
+     // 檢測到print.js中的更改時,我們告訴webpack接受更新後的模組。
+     console.log('Accepting the updated printMe module!');
+     printMe();
+   })
+ }
複製程式碼
  • 注意: 為什麼改變css程式碼不需要新增 module.hot.accpet 程式碼呢? 原因是因為 css-loader 已經幫我們處理過這一步了,js 程式碼就需要我們自己來新增 HMR 了。在使用 vue 的時候,vue-loader 幫我們實現了 js HMR 這一塊了,所以也不用我們自己實現了。react 則是藉助了 babel-preset 來幫我們實現了 js HMR.

webpack-dev-middleware

通過webpack-dev-middleware 配合 express 可以自己搭建一個簡單的webpack-dev-server,通過node來執行webpack,程式碼如下:

server.js

const webpack = require('webpack')
const middleware = require('webpack-dev-middleware')
const express = require('express')
const config = require('./webpack.config.js')

// webpack 編譯器
const compiler = webpack(config)

const app = express()

app.use(
  middleware(compiler, {
    // webpack-dev-middleware options
    publicPath: config.output.publicPath
  })
)

app.listen(3000, () => {
  console.log('Example app listening on port 3000!')
})
複製程式碼

使用babel處理ES6

進入官網,開啟 setup, 進入webpack,檢視相關文件

// npm install --save-dev babel-loader @babel/core
module: {
  rules: [
    {
        test: /\.js$/,
        exclude: /node_modules/, // 不包含
        loader: "babel-loader" // webpack 和 babel 做通訊的橋樑
    }
  ]
}

// 還需要配置options 或者 .babelrc 檔案
npm install @babel/preset-env --save-dev

{
    test: /\.js$/,
    exclude: /node_modules/, // 不包含
    loader: "babel-loader", // webpack 和 babel 做通訊的橋樑
    options: {
        "presets": ["@babel/preset-env"]
    }
}
// 但是,這只是將ES6 轉 ES5,有一些語法比如promiss,map等,低版本還是不認識,這就要使用 @babel/polyfill 了
複製程式碼
  • @babel/polyfill
// 提供polyfill是為了方便,但是您應該將它與@babel/preset-env和useBuiltIns選項一起使用
// 這樣它就不會包含並非總是需要的整個polyfill。否則,我們建議您手動匯入各個填充。
npm install --save @babel/polyfill
// 然後在程式碼的頂部引入:
import "@babel/polyfill";
"presets": [["@babel/preset-env"], {
    targets: {
      chrome: "67", // chrome 版本大於67的,就不需要將ES6轉ES5了,因為chrome對ES6已經支援的很好了
      "ie": "11"
    },
    useBuildIns: "usage" // useBuiltIns選項,如果設定成"usage",那麼將會自動檢測語法幫你require你程式碼中使用到的功能。也不需要額外引入@babel/polyfill 了
}]
複製程式碼

通過 .babelrc 來宣告 參考文件

  • @babel/plugin-transform-runtime

參考文獻

  1. 避免多次編譯出helper函式:
  2. 這裡的 @babel/runtime 包就宣告瞭所有需要用到的幫助函式,而 @babel/plugin-transform-runtime 的作用就是將所有需要helper函式的檔案,依賴@babel/runtime包
  3. 解決@babel/polyfill提供的類或者例項方法汙染全域性作用域的情況。
// npm install --save-dev @babel/plugin-transform-runtime
// npm install --save @babel/runtime-corejs2

.babelrc

{
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 2, // 代表需要使用corejs的版本; npm install --save @babel/runtime-corejs2
        "helpers": true,
        "regenerator": true,
        "useESModules": false // 按需引入babel/polyfill (注入低版本的polyfill)
      }
    ]
  ]
}
複製程式碼

webpack打包React

安裝包 npm i @babel/preset-env @babel/preset-react -D

tree shaking

tree shaking 是一個術語,通常用於描述移除 JavaScript 上下文中的未引用程式碼(dead-code)。它依賴於 ES2015 模組語法的 靜態結構 特性,例如 import 和 export。不支援 require 的引入方式。這是因為 ES 模組的引入方式是靜態的,而require 的引入方式是動態的。

  • 配置
  1. 將 mode 配置選項設定為 development 以確保 bundle 是未壓縮版本
mode: "development",
optimization: {
    usedExports: true // 檢視那些模組被使用了,使用了的就打包
}

// 還要再 package.json 中配置一個屬性

"sideEffects": false

// "side effect(副作用)" 的定義是,在匯入時會執行特殊行為的程式碼,而不是僅僅暴露一個 export 或多個 export。舉例說明,例如 polyfill,它影響全域性作用域,並且通常不提供 export"sideEffects": ["@babel/polyfill", "*.css"] // 不對 @babel/polyfill和所有引入的css檔案 作tree shaking

複製程式碼

注意: 在開發環境下,使用tree shaking 其實還是會將沒有使用的模組打包進 bundle.js 中,只不過會提醒你那些模組沒有使用。如果使用 mode: production; 那我們就不需要使用 optimization 配置項了,並且將設定 devtool: cheap-module-source-map。但是 sideEffects 還是要使用。

區分打包(dev and prod)

npm i webpack-merge -D 用來合併 webpack 模組

  1. webpack.common.js,存放 dev 和 prod 公共的配置
  2. webpack.dev.js, 開發環境的配置
  3. webpack.prod.js,生產環境的配置
  4. 我們可以將以上的webpack配置檔案放入到 build 資料夾中統一管理

看看 webpack.prod.js 的用法

webpack.prod.js

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

cosnt prodConfig = {
    mode: 'production',
    devtool: 'cheap-module-source-map'
}

module.exports = merge(prodConfig, merge)
複製程式碼

然後修改 package.json 中的配置

// 啟動dev
"dev": "webpack-dev-server --config ./build/webpack.dev.js"

// 啟動prod
"build": "webpack --config ./build/webpack.prod.js"
複製程式碼

注意: 如果我們的webpack配置放在build資料夾中,並且我們的配置中使用 clean-webpack-plugin,那麼它的配置也需要發生改變

new CleanWebpackPlugin(['dist']) // 指的是刪除當前目錄下的 dist 目錄,但是我們的 dist 目錄需要放在 build 資料夾同級目錄下。

// 可以這樣做 : 在 github 上搜尋 clean-webpack-plugin
new CleanWebpack(['dist'], {
    root: path.resolve(__dirname, '../') // 指定根路徑
})
複製程式碼

Code Splitting (程式碼分割)

程式碼分離可以用於獲取更小的 bundle,以及控制資源載入優先順序,如果使用合理,會極大影響載入時間。

常用的程式碼分離方法有三種:

  1. 入口起點:使用 entry 配置手動地分離程式碼。(優點:最簡單最直觀。缺點:對我們的示例來說毫無疑問是個嚴重問題,因為我們在 ./src/index.js 中也引入過 lodash,這樣就造成在兩個 bundle 中重複引用。)

  2. 防止重複:使用 SplitChunksPlugin 去重和分離 chunk。

optimization: { // 這裡可以配置 code splitting, 還可以配置 tree shaking 時需要的 usedExports
    splitChunks: {
        chunks: 'all'
    }
}
複製程式碼

將index.js 和 another_module.js 中的 lodash 庫抽離出來了。

詳談webpack4

  1. 動態匯入:

通過模組中的行內函數呼叫來分離程式碼。 我們不再使用 statically import(靜態匯入) lodash,而是通過 dynamic import(動態匯入) 來分離出一個 chunk。 檢視官網demo

詳談webpack4

環境變數

想要消除 開發環境 和 生產環境 之間的 webpack.config.js 差異,你可能需要環境變數(environment variable)。***參考文件***

  • 配置
  1. 對於我們的 webpack 配置,有一個必須要修改之處。通常,module.exports 指向配置物件。要使用 env 變數,你必須將 module.exports 轉換成一個函式:
const path = require('path');

module.exports = env => {
  // Use env.<YOUR VARIABLE> here:
  console.log('NODE_ENV: ', env.NODE_ENV); // 'local'
  console.log('Production: ', env.production); // true

  return {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };
};
複製程式碼
  1. 既然可以在配置檔案中接收到 env ,那麼我們就可以將上面的 mode 區分部分重新修改一下,怎麼修改呢?如下:
// 在上面的 環境區分中,我們在 webpack.common.js 中引入 devConfig、prodConfig、webpack-merge,然後通過 env 來判斷當前的打包命令是開發環境還是生產環境。

module.exports = env => {
    if (env && env.production) {
        return merge(commonConfig, prodConfig);
    } else {
        return merge(commonConfig, devConfig);
    }
}
複製程式碼

詳談webpack4

Library 打包

除了打包應用程式,webpack 還可以用於打包 JavaScript library. 參考文件

詳談webpack4

  1. libraryTarget 還可以賦值為 'this' 或者 'window' | 'global',意味著將 library 掛載到 全域性上
  2. 如果我們自己寫的庫中引入的第三方庫,比如lodash.js,但是我們不希望它打包到我們的庫中,那麼應該怎麼辦呢?我們需要配置如下引數
// webpack.config.js
externals: ['lodash']
// 不打包lodash,但是別人引入我們的庫的時候,就必須引入lodash.js,因為我們的庫依賴lodash
複製程式碼
  • 釋出自己的庫到 npm 上,給別人使用
  1. 首先配置我們的package.json檔案
    詳談webpack4
  2. 到 npm 官網上註冊我們的賬號
  3. npm adduser (新增使用者名稱和密碼)
  4. npm publish (將我們自己的庫釋出到 npm 倉庫上去)
  5. 注意,庫的名字一定要特別,不能和npm 倉庫上的庫同名

漸進式網路應用程式 ( PWA )

我們通過搭建一個簡易 server 下,測試下這種離線體驗。這裡使用 http-server package:npm install http-server --save-dev 參考

{
  "scripts": {
+    "build": "webpack",
+    "start": "http-server dist"
  }
}
複製程式碼
  • 新增 Workbox
npm install workbox-webpack-plugin --save-dev

const WorkboxPlugin = require('workbox-webpack-plugin');

    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
+       title: '漸進式網路應用程式'
+     }),
+     new WorkboxPlugin.GenerateSW({
+       // 這些選項幫助快速啟用 ServiceWorkers
+       // 不允許遺留任何“舊的” ServiceWorkers
+       clientsClaim: true,
+       skipWaiting: true
+     })
    ]
複製程式碼
  • 註冊 Service Worker

接下來我們註冊 Service Worker,使其出場並開始表演。通過新增以下注冊程式碼來完成此操作:

  import _ from 'lodash';
  import printMe from './print.js';

+ if ('serviceWorker' in navigator) {
+   window.addEventListener('load', () => {
+     navigator.serviceWorker.register('/service-worker.js').then(registration => {
+       console.log('SW registered: ', registration);
+     }).catch(registrationError => {
+       console.log('SW registration failed: ', registrationError);
+     });
+   });
+ }
複製程式碼

TypeScript

TypeScript 是 JavaScript 的超集,為其增加了型別系統,可以編譯為普通 JavaScript 程式碼 參考

npm install --save-dev typescript ts-loader
複製程式碼
  1. 在webpack.config.js中配置
    詳談webpack4
  2. 需要建立一個tsconfig.json
{
  "compilerOptions": {
    "outDir": "./dist/", // 當前目錄下的dist
    "noImplicitAny": true, // 
    "module": "es6", // 使用ES Module 引入方式
    "target": "es5", // 轉換成 ES5 的程式碼
    "jsx": "react",
    "allowJs": true // 允許引入 js 檔案
  }
}
複製程式碼
  1. 使用typescript時我們可能使用外部的庫,但是外部的庫是無法利用typescript自動檢測的特性的。這個時候我們就需要安裝這些庫的型別檔案,比如lodash, 我們如何安裝它的自動檢測型別檔案呢?
npm i @types/lodash -D // 通過 @types/ + 庫的名字即可
複製程式碼

devServer.proxy

// 請求到 /api/users 現在會被代理到請求 http://localhost:3000/api/users
devServer: {
    proxy: {
        '/api': {
            target: 'http://localhost:3000',
            pathRewrite: {
                'header.json': 'demo.json' //我們請求header.json的時候,轉而去請求demo.json
            },
            secure: false, // 預設情況下,不接受執行在 HTTPS 上,且使用了無效證照的後端伺服器。如果你想要接受,修改配置
            bypass: function(req, res, proxyOptions) { // 攔截,如果請求的是 html 型別的資料,就直接返回 html檔案,不進行轉發
                if (req.headers.accept.indexOf("html") !== -1) {
                    console.log("Skipping proxy for browser request.");
                    return "/index.html";
                }
            }
        }
    }
}
複製程式碼

devServer.historyApiFallback

當使用 HTML5 History API 時,任意的 404 響應都可能需要被替代為 index.html.在做單頁面路由的時候,需要配置此選項。

devServer: {
    historyApiFallback: true
}
複製程式碼

ESlint配置

約束程式碼規範的工具,團隊開發尤其重要

// 安裝
npm i eslint -D

// 初始化配置檔案
npx eslint --init
複製程式碼

詳談webpack4

  1. 使用Airbnb公司的規範,我們需要另外覆蓋一些配置

詳談webpack4

  1. 在webpack中配置 eslint-loader

npm i eslint-loader -D (會降低打包的速度哦 ! 可以在options中配置 cache 屬性) 參考官網配置

詳談webpack4
注意: 在webpack.devServer中配置一個屬性:

devServer: {
    overlay: true, // 將報錯顯示在頁面上
}
複製程式碼

webpack效能提升

  • 升級版本
  1. 比如webpack,node,npm,yarn
  • 儘量少使用 Loader
  1. 在使用 babel-loader編譯 js 程式碼的時候,使用 exclude 或者 include來忽略掉 node_modules下的檔案,因為這個檔案下的檔案都是編譯過了的,就沒必要再次讓 babel-loader 來編譯了。
  • 儘量少使用 Plugin
  1. 比如在開發環境中,我們就沒必要使用壓縮css或者js 程式碼了。
  • 合理配置resolve
  1. resolve.alias: 建立 import 或 require 的別名,來確保模組引入變得更簡單
  2. resolve.extensions: 自動解析確定的擴充套件。預設值為:extensions: [".js", ".json"], 能夠使使用者在引入模組時不帶擴充套件:import File from '../path/to/file'.也可以新增 'jsx' 之類的檔案
  3. resolve.mainFields: 通過配置這個選項,來引入可以像省略 index.js 這樣的檔案。比如我們引入import index from './src',其實就是引入的src下的index.js,我們也可以配置其它的檔案,比如 main.js,hello.js ...

詳談webpack4

  • DllPlugin

DLLPlugin 和 DLLReferencePlugin 用某種方法實現了拆分 bundles,同時還大大提升了構建的速度。

  1. 實現的邏輯是,將第三方庫只打包一次並打包成一個檔案 (也可以打包成多個檔案,配置 entry 屬性就行了),然後將其快取起來,之後再做打包的時候,不再去node_modules中尋找了
  2. 建立一個 webpack.dll.js 配置檔案

詳談webpack4
通過 webpack.DllPlugin來產生一個第三方庫的原始碼檔案和一個對映檔案(manifest.json) 3. npm i add-asset-html-webpack-plugin -D

將我們打包好的第三方庫原始碼引入到 HtmlWebpackPlugin 生成的 index.html 中

  1. 使用 webpack.DllRefrencePlugin 來分析程式碼

這個外掛是在 webpack 主配置檔案(webpack.common.js)中設定的。它會結合manifest.json、第三方庫原始碼檔案以及我們引入的第三方庫檔案做一個分析,如果它發現我們引入的檔案在原始碼檔案裡面已經有了,它就直接拿過來用了,而不會node_modules裡面找了。

new webpack.DllRefrencePlugin({
    manifest: path.resolve(__dirname, './vendors.manifest.json')
})
複製程式碼
  1. 拆分成多個第三方庫檔案

詳談webpack4
動態的匯入多個外掛,將 plugins 提取出來,動態push 一些外掛進去

詳談webpack4

  • 控制包檔案大小
  1. 還記得 tree shaking 吧?去除冗餘程式碼 ...
  2. splitChunks 來將大檔案拆分成多個小檔案
  • 多程式打包

webpack 預設是使用 node.js 來執行,即採用的單執行緒機制打包過程

  1. node裡面的多程式 thread-loader, parallel-webpack, happypack
  • 合理使用 sourceMap

描述越詳細,就越慢哦 ~

  • 結合 stats 分析打包結果

結合分析,檢視哪個模組打包耗時比較長,做針對性處理

  • 開發環境記憶體編譯

採用 webpack-dev-server

  • 開發環境剔除無用外掛

比如壓縮 css 或者 js 的外掛

  1. 這裡我們需要配置環境變數來區分打包、

多頁面打包配置

本質就是建立多個entry 以及 HtmlWebpackPlugin 來實現的

  1. 比如我們要打包兩個js檔案,並且通過兩個 index.html 分別引入

詳談webpack4
2. 配置 HtmlWebpackPlugin 外掛

詳談webpack4
3. 如果還有多個 js 檔案需要打包,那麼我們就可以寫一些邏輯來動態建立,就不需要一個個來寫 HtmlWebpackPlugin了

詳談webpack4

如何編寫 Loader

其實 loader 就是一些函式(不能使用箭頭函式哦,因為this),接受的引數就我們的原始碼,通過函式來做一些處理並返回而已。loaderAPI

// myself-loader.js

module.exports = function (source) {
    return something ...
}

// 然後在 module 中配置
module: {
    rules: [
        {
            test: /\.js$/,
            loader: path.resolve(__dirname, './loaders/myself-loader.js')
        }
    ]
}

// 如何傳參?
// 配置 options 之後,在我們寫的 laoder 裡面通過 this.query 就能獲取到啦 !!!
{
    test: /\.js$/,
    use: [
        loader: path.resolve(__dirname, './loaders/myself-loader.js'),
        options: {
            name: 'alex.cheng'
        }
    ]
}

// 自己定義的 loader 如何像引入 node_modules 裡面 loader 一樣引入呢 ?
resolveLoader: {
    modules: ["node_modules", "./loader"] // 如果在node_modules裡沒找到,就到當前檔案loader中去找
}
複製程式碼

如何編寫一個Plugin

我們使用別人的 plugin 的時候都是怎麼使用的呢? 是不是都要 new Plugin ? 所以啊,我們的 plugin 都是通過建構函式來編寫的。來看一個簡單的例子

// plugin: alex-cheng-webpack-plugin.js

class AlexChengWebpackPlugin {
  constructor() {
    console.log('alex.cheng plugin is excuted !')
  }
}
module.exports = AlexChengWebpackPlugin

// webpack.config.js

const path = require('path')
const AlexChengWebpackPlugin = require('./src/myPlugins/alex-cheng-webpack-plugin')

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, './dist/')
  },
  plugins: [
    new AlexChengWebpackPlugin() // 呼叫就行了啦 !!
  ]
}
複製程式碼

看!我們自己的外掛就執行了咯 ! 詳情可以參考 官網API

詳談webpack4

相關文章