前端面試的那些事兒 ~ 深刻淺出 webpack 之基礎應用篇

前言

前端工程化經歷過不少優秀的工具,例如 Grunt、Gulp、webpack、rollup 等等,每種工具都有本身適用的場景,而現今應用最爲普遍的當屬 weback 打包了。所以 webpack 也天然而然成了面試官打探你是否懂前端工程化的重要指標。javascript

因爲 webpack 技術棧比較複雜,所以決定分如下幾篇文章全面深刻的講解:css

  • 基礎應用篇
  • 高級應用篇
  • 性能優化篇
  • 原理篇( webpack 框架執行流程、手寫 plugin、手寫 loader )

webpack 是什麼

webpack 是模塊打包工具html

webpack 能夠不進行任何配置(不進行任何配置時,webpack 會使用默認配置)打包以下代碼:前端

// moduleA.js
function ModuleA(){
  this.a = "a";
  this.b = "b";
}

export default ModuleA


// index.js
import ModuleA from "./moduleA.js";

const module = new ModuleA();
複製代碼

咱們知道瀏覽器是不認識的 import 語法的,直接在瀏覽器中運行這樣的代碼會報錯。那麼咱們就能夠藉助 webpack 來打包這樣的代碼,賦予 JavaScript 模塊化的能力。java

最第一版本的 webpack 只能打包 JavaScript 代碼,隨着發展 css 文件,圖片文件,字體文件均可以被 webpack 打包。node

本文將主要講解 webpack 是如何打包這些資源的,屬於比較基礎的文章主要是爲了後面講解性能優化和原理作鋪墊,若是已經對 webpack 比較熟悉的同窗能夠跳過本文。react

webpack 基礎功能

初始化安裝 webpack

mkdir webpackDemo // 建立文件夾
cd webpackDemo // 進入文件夾
npm init -y // 初始化package.json

npm install webpack webpack-cli -D // 開發環境安裝 webpack 以及 webpack-cli 
複製代碼

經過這樣安裝以後,咱們能夠在項目中使用 webpack 命令了。webpack

打包第一個文件

webpack.config.jsgit

const path = require('path');

module.exports = {
  mode: 'development', // {1}
  entry: { // {2}
  	main:'./src/index.js'
  }, 
  output: { // {3}
    publicPath:"", // 全部dist文件添加統一的前綴地址,例如發佈到cdn的域名就在這裏統一添加
    filename: 'bundle.js',
    path: path.resolve(__dirname,'dist')
  }
}
複製代碼

代碼分析:es6

  • {1} mode 打包模式是開發環境仍是生成環境, development | production
  • {2} entry 入口文件爲 index.js
  • {3} output 輸出到 path 配置的 dist 文件夾下,輸出的文件名爲 filename 配置的 bundle.js

[注意] 這個基礎的配置文件哪怕你不寫,咱們執行 webpack 命令也能夠運行,那是由於 webpack 提供了一個默認配置文件。

建立文件進行簡單打包:

src/moduleA.js

const moduleA = function () {
  return "moduleA"
}

export default moduleA;

--------------------------------

src/index.js

import moduleA from "./moduleA";

console.log(moduleA());
複製代碼

修改 package.json  srcipts

"scripts": {
  "build": "webpack --config webpack.config.js"
}
複製代碼

執行 npm run build 命令

image.png

打包後的 bundle.js 源碼分析

源碼通過簡化,只把核心部分展現出來,方便理解

(function(modules) {
 	var installedModules = {};

 	function __webpack_require__(moduleId) {
        // 緩存文件
 		if(installedModules[moduleId]) {
 			return installedModules[moduleId].exports;
 		}
		// 初始化 moudle,而且也在緩存中存入一份
 		var module = installedModules[moduleId] = {
 			i: moduleId,
 			l: false,
 			exports: {}
 		};
		// 執行 "./src/index.js" 對應的函數體
 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

		// 標記"./src/index.js"該模塊以及加載
 		module.l = true;
    
 		// 返回已經加載成功的模塊
 		return module.exports;
 	}
	// 匿名函數開始執行的位置,而且默認路徑就是入口文件
 	return __webpack_require__(__webpack_require__.s = "./src/index.js");
 })
	// 傳入匿名執行函數體的module對象,包含"./src/index.js","./src/moduleA.js"
	// 以及它們對應要執行的函數體
 ({
   "./src/index.js": (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _moduleA__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./moduleA */ \"./src/moduleA.js\");\n\n\nconsole.log(Object(_moduleA__WEBPACK_IMPORTED_MODULE_0__[\"default\"])());\n\n\n//# sourceURL=webpack:///./src/index.js?");
   
  }),

   "./src/moduleA.js": (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\nconst moduleA = function () {\n return \"moduleA\"\n}\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (moduleA);\n\n\n//# sourceURL=webpack:///./src/moduleA.js?");

  })

 });

複製代碼

再來看看"./src/index.js" 對應的執行函數

(function(module, __webpack_exports__, __webpack_require__) {
 "use strict";
	eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _moduleA__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./moduleA */ \"./src/moduleA.js\");\n\n\nconsole.log(Object(_moduleA__WEBPACK_IMPORTED_MODULE_0__[\"default\"])());\n\n\n//# sourceURL=webpack:///./src/index.js?");
})
複製代碼

你會發現其實就是一個 eval 執行方法

咱們拆開 eval 來仔細看看裏面是什麼內容,簡化後代碼以下:

var moduleA = __webpack_require__("./src/moduleA.js");
console.log(Object(moduleA["default"])());
複製代碼

上面源碼中其實已經調用了 __webpack_require__(__webpack_require__.s = "./src/index.js"); ,而後 "./src/index.js" 又遞歸調用了去獲取 "./src/moduleA.js" 的輸出對象。

咱們看看 "./src/moduleA.js" 代碼會輸出什麼:

const moduleA = function () {
  return "moduleA"
}
__webpack_exports__["default"] = (moduleA);
複製代碼

再回頭看看上面的代碼就至關於:

console.log(Object(function () {
  return "moduleA"
})());
複製代碼

最後執行打印了 "moduleA"

經過這段源碼的分析能夠看出:

  1. 打包以後的模塊,都是經過 eval 函數進行執行的;
  2. 經過調用入口函數 ./src/index.js 而後遞歸的去把全部模塊找到,因爲遞歸的一個缺點,會進行重複計算,所以 __webpack_require__ 函數中有一個緩存對象 installedModules 來處理這個問題。

loader

咱們知道 webpack 能夠打包 JavaScript 模塊,並且也早就據說 webpack 還能夠打包圖片、字體以及 css,這個時候就須要 loader 來幫助咱們識別這些文件了。

[注意] 碰到文件不能識別記得找 loader 便可。

打包圖片文件

修改配置文件:webpack.config.js

const path = require('path');

module.exports = {
  mode: 'development',
  entry: { 
    main:'./src/index.js'
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname,'dist')
  },
  module:{
    rules:[
      {
        test:/\.(png|svg|jpg|gif)$/,
        use:{
          loader: 'url-loader',
          options: {
            name: '[name]_[hash].[ext]',
            outputPath:"images", // 打包該資源到 images 文件夾下
            limit: 2048 // 若是圖片的大小,小於2048KB則時輸出base64,不然輸出圖片
          }
        }
      }
    ]
  }
}
複製代碼

修改:src/index.js

import moduleA from "./moduleA";
import header from "./header.jpg";

function insertImg(){
  const imageElement = new Image();
  imageElement.src = `dist/${header}`;
  document.body.appendChild(imageElement);
}

insertImg();
複製代碼

執行打包後,發現能夠正常打包,而且 dist 目錄下也多出了一個圖片文件。

咱們簡單分析下:

webpack 自己其實只認識 JavaScript 模塊的,當碰到圖片文件時便會去 module 的配置 rules 中找,發現 test:/\.(png|svg|jpg|gif)$/  ,正則匹配到圖片文件後綴時就使用 url-loader  進行處理,若是圖片小於 2048KB (這個能夠設置成任意值,主要看項目)就輸出 base64 。

打包樣式文件

{
  test:/\.scss$/, // 正則匹配到.scss樣式文件
    use:[
      'style-loader', // 把獲得的CSS內容插入到HTML中
      {
        loader: 'css-loader',
        options: {
          importLoaders: 2, // scss中再次import scss文件,也一樣執行 sass-loader 和 postcss-loader
          modules: true // 啓用 css module
        }
      },
      'sass-loader', // 解析 scss 文件成 css 文件
      'postcss-loader'// 自動增長廠商前綴 -webket -moz,使用它還須要建立postcss.config.js配置文件
    ]
}
複製代碼

postcss.config.js

module.exports = {
  plugins: [
    require('autoprefixer')
  ]
};
複製代碼

打包解析:

  1. 當 webpack 遇到 xx.scss 樣式文件;
  2. 依次調用 postcss-loader 自動增長廠商前綴 -webket -moz ;
  3. 調用 sass-loader 把 scss 文件轉換成 css 文件;
  4. 調用 css-loader 處理 css 文件,其中 importLoaders:2 ,是 scss 文件中引入了其它 scss 文件,須要重複調用 sass-loader postcss-loader 的配置項;
  5. 最後調用 style-loader 把前面編譯好的 css 文件內容以 <style>...</style> 形式插入到頁面中。

[注意] loader的執行順序是數組後到前的執行順序。

打包字體文件

{
  test: /\.(woff|woff2|eot|ttf|otf)$/, // 打包字體文件
  use: ['file-loader'] // 把字體文件移動到dist目錄下
}
複製代碼

plugins

plugins 能夠在 webpack 運行到某個時刻幫你作一些事情,至關於 webpack 在某一個生命週期插件作一些輔助的事情。

html-webpack-plugin

做用:

會在打包結束後,自動生產一個 HTML 文件(也可經過模板生成),並把打包生成的 JS 文件自動引入到 HTML 文件中。

使用:

const HtmlWebpackPlugin = require('html-webpack-plugin');

plugins: [
  new HtmlWebpackPlugin({
    template: 'src/index.html' // 使用模板文件
  })
]
複製代碼

clean-webpack-plugin

做用:

每次輸出打包結果時,先自動刪除 output 配置的文件夾

使用:

const { CleanWebpackPlugin } = require('clean-webpack-plugin');

plugins: [
  ...
  new CleanWebpackPlugin() // 使用這個插件在每次生成dist目錄前,先刪除dist目錄
]
複製代碼

source map

在開發過程當中有一個功能是很重要的,那就是錯誤調試,咱們在編寫代碼過程當中出現了錯誤,編譯後的包若是提示不友好,將會嚴重影響咱們的開發效率。而經過配置 source map 就能夠幫助咱們解決這個問題。

示例: 修改:src/index.js,增長一行錯誤的代碼

console.log(a);
複製代碼

因爲mode: 'development' 開發模式是默認會打開source map功能的,咱們先關閉它。

devtool: 'none' // 關閉source map 配置
複製代碼

執行打包來看下控制檯的報錯信息:

image.png
錯誤堆棧信息,居然給的是打包以後的 bundle 文件中的信息,但其實咱們在開發過程當中的文件結構並非這樣的,所以咱們須要它能指明咱們是在 index.js 中的多少行發生錯誤了,這樣咱們就能夠快速的定位到問題。

咱們去掉 devtool:'none' 這行配置,再執行打包:

image.png
此時它就把咱們在開發中的具體錯誤文件在錯誤堆棧中輸出了,這就是source map的功能。

總結下:source map 它是一個映射關係,它知道 dist 目錄下 bundle.js 文件對應的實際是 index.js 文件中的多少行。

webpackDevServer

每次修改完代碼以後都要手動去執行編譯命令,這顯然是不科學的,咱們但願是每次寫完代碼,webpack 會進行自動編譯,webpackDevServer 就能夠幫助咱們。

增長配置:

devServer: {
  contentBase: './dist', // 服務器啓動根目錄設置爲dist
  open: true, // 自動打開瀏覽器
  port: 8081 // 配置服務啓動端口,默認是8080
},
複製代碼

它至關於幫助咱們開啓了一個 web 服務,並監聽了 src 下文件當文件有變更時,自動幫助咱們進行從新執行 webpack 編譯。

咱們在 package.json 中增長一條命令:

"scripts": {
 	"start": "webpack-dev-server"
},
複製代碼

如今咱們執行 npm start  命令後,能夠看到控制檯開始實行監聽模式了,此時咱們任意更改業務代碼,都會觸發 webpack 從新編譯。

手動實現簡單版 webpack-dev-server

項目根目錄下增長:server.js

加載包: npm install express webpack-dev-middleware -D

const express = require('express');
const app = express();
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.config.js'); // 引入webpack配置文件
const compiler = webpack(config); // webpack 編譯運行時

// 告訴 express 使用 webpack-dev-middleware,
// 以及將 webpack.config.js 配置文件做爲基礎配置
app.use(webpackDevMiddleware(compiler, {}));

// 監聽端口
app.listen(3000,()=>{
  console.log('程序已啓動在3000端口');
});
複製代碼

webpack-dev-middleware 做用:

  1. 經過 watch mode 監聽資源的變動而後自動打包,本質上是調用 compiler 對象上的 watch 方法;
  2. 使用內存文件系統編譯速度快 compiler.outputFileSystem = new MemoryFileSystem() ;
  3. 返回 express 框架可用的中間件。

package.json 增長一條命令:

"scripts": {
  "server": "node server.js"
},
複製代碼

執行命令 npm run server  啓動咱們自定義的服務,瀏覽器中輸入 http://localhost:3000/  查看效果。

熱更新 Hot Moudule Replacement(HMR)

模塊熱更新功能會在應用程序運行過程當中,替換、添加或刪除模塊,而無需從新加載整個頁面。

HMR 配置

const webpack = require('webpack');
module.exports = {
	devServer: {
  	  contentBase: './dist',
  	  open: true,
  	  port: 8081,
  	  hot: true // 熱更新配置
	},
	plugins:[
  	new webpack.HotModuleReplacementPlugin() // 增長熱更新插件
  ]
}
複製代碼

手動編寫 HMR 代碼

在編寫代碼時常常會發現熱更新失效,那是由於相應的 loader 沒有去實現熱更新,咱們看看如何簡單實現一個熱更新。

import moduleA from "./moduleA";

if (module.hot) {
  module.hot.accept('./moduleA.js', function() {
    console.log("moduleA 支持熱更新拉");
    console.log(moduleA());
  })
}
複製代碼

代碼解釋: 咱們引人本身編寫的一個普通 ES6 語法模塊,加入咱們想要實現熱更新就必須手動監聽相關文件,而後當接收到更新回調時,主動調用。

還記得上面講 webpack 打包後的源碼分析嗎,webpack 給模塊都創建了一個 module 對象,當你開啓模塊熱更新時,在初始化 module 對象時增長了(源碼通過刪減):

function hotCreateModule(moduleId) {
  var hot = {
    active: true,
    accept: function(dep, callback){
      if (dep === undefined) hot._selfAccepted = true;
      else if (typeof dep === "function") hot._selfAccepted = dep;
      else if (typeof dep === "object")
      for (var i = 0; i < dep.length; i++)
 	  hot._acceptedDependencies[dep[i]] = callback || function() {};
 	  else hot._acceptedDependencies[dep] = callback || function() {};
    }
  }
}
複製代碼

module 對象中保存了監聽文件路徑和回調函數的依賴表,當監聽的模塊發生變動後,會去主動調用相關的回調函數,實現手動熱更新。

[注意] 全部編寫的業務模塊,最終都會被 webpack 轉換成 module 對象進行管理,若是開啓熱更新,那麼 module 就會去增長 hot 相關屬性。這些屬性構成了 webpack 編譯運行時對象。

編譯 ES6

顯然你們都知道必需要使用 babel 來支持了,咱們具體看看如何配置

配置

一、安裝相關包

npm install babel-loader @babel/core @babel/preset-env @babel/polyfill -D
複製代碼

二、修改配置 webpack.config.json

還記得文章上面說過,碰到不認識的文件類型的編譯問題要求助 loader

module:{
  rules:[
    {
      test: /\.js$/, // 正則匹配js文件
      exclude: /node_modules/, // 排除 node_modules 文件夾
      loader: "babel-loader", // 使用 babel-loader
      options:{
        presets:[
          [
            "@babel/preset-env", // {1}
           { useBuiltIns: "usage" } // {2}
          ]
        ]
      }
    }
  ]
}
複製代碼

babel 配置解析:

  • {1} babel presets 是一組插件的集合,它的做用是轉換 ES6+ 的新語法,可是一些新 API 它不會處理的
    • Promise  Generator 是新語法
    • Array.prototype.map 方法是新 API ,babel 是不會轉換這個語法的,所以須要藉助 polyfill 處理
  • {2} useBuiltIns 的配置是處理 @babel/polyfill 如何加載的,它有3個值 false entry usage
    • false: 不對 polyfills作任何操做;
    • entry: 根據 target中瀏覽器版本的支持,將polyfills拆分引入,僅引入有瀏覽器不支持的polyfill
    • usage:檢測代碼中ES6/7/8等的使用狀況,僅僅加載代碼中用到的polyfills

演示

新建文件 src/moduleES6.js

const arr = [
  new Promise(()=>{}),
  new Promise(()=>{})
];
function handleArr(){
  arr.map((item)=>{
    console.log(item);
  });
}
export default handleArr;
複製代碼

修改文件 src/index.js

import moduleES6 from "./moduleES6";
moduleES6();
複製代碼

執行打包後的源文件(簡化後):

"./node_modules/core-js/modules/es6.array.map.js":
(function(module, exports, __webpack_require__) {
"use strict";
var $export = __webpack_require__("./node_modules/core-js/modules/_export.js");
var $map = __webpack_require__("./node_modules/core-js/modules/_array-methods.js")(1);

$export($export.P + $export.F * !__webpack_require__(/*! ./_strict-method */ "./node_modules/core-js/modules/_strict-method.js")([].map, true), 'Array', {
  map: function map(callbackfn) {
    return $map(this, callbackfn, arguments[1]);
  }
});
複製代碼

看代碼就應該能明白了 polyfill 至關因而使用 ES5 的語法從新實現了 map 方法來兼容低版本瀏覽器。

而 polyfill 實現了 ES6-ES10 全部的語法十分龐大,咱們不可能所有引入,所以纔會有這個配置 useBuiltIns: "usage" 只加載使用的語法。

編譯 React 文件

配置

安裝相關依賴包

npm install @babel/preset-react -D
npm install react react-dom
複製代碼

webpack.config.js

module:{
  rules:[
    {
      test: /\.js$/, // 正則匹配js文件
      exclude: /node_modules/, // 排除 node_modules 文件夾
      loader: "babel-loader", // 使用 babel-loader
      options:{
        presets:[
          [
            "@babel/preset-env",
           { useBuiltIns: "usage" }
          ],
          ["@babel/preset-react"]
        ]
      }
    }
  ]
}
複製代碼

直接在 presets 配置中增長一個 ["@babel/preset-react"]  配置便可, 那麼這個 preset 就會幫助咱們把 React 中 JSX 語法轉換成 React.createElement 這樣的語法。

演示

修改文件:src/index.js

import React,{Component} from 'react';
import ReactDom from 'react-dom';

class App extends Component{
  render(){
    const arr = [1,2,3,4];
    return (
      arr.map((item)=><p>num: {item}</p>)
    )
  }
}

ReactDom.render(<App />, document.getElementById('root')); 複製代碼

執行打包命令 yarn build 能夠正確打包而且顯示正常界面。

隨着項目的複雜度增長,babel 的配置也隨之變的複雜,所以咱們須要把 babel 相關的配置提取成一個單獨的文件進行配置方便管理,也就是咱們工程目錄下的 .babelrc 文件。

.babelrc

{
  "presets":[
    ["@babel/preset-env",{ "useBuiltIns": "usage" }],
    ["@babel/preset-react"]
  ]
}
複製代碼

[注意] babel-laoder 執行 presets 配置順序是數組的後到前,與同時使用多個 loader 的執行順序是同樣的。

也就是把 webpack.config.js  中的 babel-loader 中的 options 對象提取成一個單獨文件。

image.png
經過編譯記錄,咱們能夠發現一個問題就是打包後的 bundle.js  文件足足有1M大,那是由於 react 以及 react-dom 都被打包進來了,webpack 優化的文章中會講解如何 code-splitting 進行優化。

小結

本文主要仍是以如何使用 webpack 爲主線,讓你們對 webpack 有一個初步的印象,而且學會使用 webpack 配置簡單的項目,在面試中並不會問這些,所以這篇文章只是一篇鋪墊性質的文章,真正面試中文的較多的仍是webpack 的高級用法以及性能優化,更甚至也會問及 webpack 的打包執行原理,在以後的幾篇文章會詳細講解。

代碼託管地址

相關文章
相關標籤/搜索