前端工程化經歷過不少優秀的工具,例如 Grunt、Gulp、webpack、rollup 等等,每種工具都有本身適用的場景,而現今應用最爲普遍的當屬 weback 打包了。所以 webpack 也天然而然成了面試官打探你是否懂前端工程化的重要指標。javascript
因爲 webpack 技術棧比較複雜,所以決定分如下幾篇文章全面深刻的講解:css
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
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
development | production
[注意] 這個基礎的配置文件哪怕你不寫,咱們執行 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 命令
源碼通過簡化,只把核心部分展現出來,方便理解
(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"
經過這段源碼的分析能夠看出:
./src/index.js
而後遞歸的去把全部模塊找到,因爲遞歸的一個缺點,會進行重複計算,所以 __webpack_require__
函數中有一個緩存對象 installedModules
來處理這個問題。咱們知道 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')
]
};
複製代碼
打包解析:
xx.scss
樣式文件;postcss-loader
自動增長廠商前綴 -webket -moz
;sass-loader
把 scss 文件轉換成 css 文件;css-loader
處理 css 文件,其中 importLoaders:2
,是 scss 文件中引入了其它 scss 文件,須要重複調用 sass-loader
postcss-loader
的配置項;style-loader
把前面編譯好的 css 文件內容以 <style>...</style>
形式插入到頁面中。[注意] loader的執行順序是數組後到前的執行順序。
{
test: /\.(woff|woff2|eot|ttf|otf)$/, // 打包字體文件
use: ['file-loader'] // 把字體文件移動到dist目錄下
}
複製代碼
plugins 能夠在 webpack 運行到某個時刻幫你作一些事情,至關於 webpack 在某一個生命週期插件作一些輔助的事情。
做用:
會在打包結束後,自動生產一個 HTML 文件(也可經過模板生成),並把打包生成的 JS 文件自動引入到 HTML 文件中。
使用:
const HtmlWebpackPlugin = require('html-webpack-plugin');
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html' // 使用模板文件
})
]
複製代碼
做用:
每次輸出打包結果時,先自動刪除 output 配置的文件夾
使用:
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
plugins: [
...
new CleanWebpackPlugin() // 使用這個插件在每次生成dist目錄前,先刪除dist目錄
]
複製代碼
在開發過程當中有一個功能是很重要的,那就是錯誤調試,咱們在編寫代碼過程當中出現了錯誤,編譯後的包若是提示不友好,將會嚴重影響咱們的開發效率。而經過配置 source map 就能夠幫助咱們解決這個問題。
示例: 修改:src/index.js,增長一行錯誤的代碼
console.log(a);
複製代碼
因爲mode: 'development'
開發模式是默認會打開source map功能的,咱們先關閉它。
devtool: 'none' // 關閉source map 配置
複製代碼
執行打包來看下控制檯的報錯信息:
錯誤堆棧信息,居然給的是打包以後的 bundle 文件中的信息,但其實咱們在開發過程當中的文件結構並非這樣的,所以咱們須要它能指明咱們是在 index.js 中的多少行發生錯誤了,這樣咱們就能夠快速的定位到問題。咱們去掉 devtool:'none'
這行配置,再執行打包:
總結下:source map 它是一個映射關係,它知道 dist 目錄下 bundle.js 文件對應的實際是 index.js 文件中的多少行。
每次修改完代碼以後都要手動去執行編譯命令,這顯然是不科學的,咱們但願是每次寫完代碼,webpack 會進行自動編譯,webpackDevServer 就能夠幫助咱們。
增長配置:
devServer: {
contentBase: './dist', // 服務器啓動根目錄設置爲dist
open: true, // 自動打開瀏覽器
port: 8081 // 配置服務啓動端口,默認是8080
},
複製代碼
它至關於幫助咱們開啓了一個 web 服務,並監聽了 src 下文件當文件有變更時,自動幫助咱們進行從新執行 webpack 編譯。
咱們在 package.json
中增長一條命令:
"scripts": {
"start": "webpack-dev-server"
},
複製代碼
如今咱們執行 npm start
命令後,能夠看到控制檯開始實行監聽模式了,此時咱們任意更改業務代碼,都會觸發 webpack 從新編譯。
項目根目錄下增長: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
做用:
watch mode
監聽資源的變動而後自動打包,本質上是調用 compiler
對象上的 watch 方法;compiler.outputFileSystem = new MemoryFileSystem()
;package.json 增長一條命令:
"scripts": {
"server": "node server.js"
},
複製代碼
執行命令 npm run server
啓動咱們自定義的服務,瀏覽器中輸入 http://localhost:3000/
查看效果。
模塊熱更新功能會在應用程序運行過程當中,替換、添加或刪除模塊,而無需從新加載整個頁面。
const webpack = require('webpack');
module.exports = {
devServer: {
contentBase: './dist',
open: true,
port: 8081,
hot: true // 熱更新配置
},
plugins:[
new webpack.HotModuleReplacementPlugin() // 增長熱更新插件
]
}
複製代碼
在編寫代碼時常常會發現熱更新失效,那是由於相應的 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 編譯運行時對象。
顯然你們都知道必需要使用 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 配置解析:
Promise
Generator
是新語法Array.prototype.map
方法是新 API ,babel 是不會轉換這個語法的,所以須要藉助 polyfill
處理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"
只加載使用的語法。
安裝相關依賴包
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
文件。
{
"presets":[
["@babel/preset-env",{ "useBuiltIns": "usage" }],
["@babel/preset-react"]
]
}
複製代碼
[注意] babel-laoder
執行 presets
配置順序是數組的後到前,與同時使用多個 loader 的執行順序是同樣的。
也就是把 webpack.config.js
中的 babel-loader
中的 options
對象提取成一個單獨文件。
bundle.js
文件足足有1M大,那是由於 react 以及 react-dom 都被打包進來了,webpack 優化的文章中會講解如何
code-splitting
進行優化。
本文主要仍是以如何使用 webpack 爲主線,讓你們對 webpack 有一個初步的印象,而且學會使用 webpack 配置簡單的項目,在面試中並不會問這些,所以這篇文章只是一篇鋪墊性質的文章,真正面試中文的較多的仍是webpack 的高級用法以及性能優化,更甚至也會問及 webpack 的打包執行原理,在以後的幾篇文章會詳細講解。