webpack基礎(chǔ)——《webpack實(shí)戰(zhàn) 入門、進(jìn)階與調(diào)優(yōu)》讀書總結(jié)

這篇是我看《webpack實(shí)戰(zhàn) 入門、進(jìn)階與調(diào)優(yōu)》這本書的一個(gè)筆記,也相應(yīng)擴(kuò)充了部分內(nèi)容,可以算是給沒(méi)讀過(guò)的人做個(gè)引子。這本書比較系統(tǒng)地介紹了webpack的基礎(chǔ),閱讀量也不大,讓我弄清楚了很多以前模糊的點(diǎn)。

1、安裝webpack

安裝webpack建議本地安裝(不使用全局),因?yàn)槿职惭b的話項(xiàng)目在不同機(jī)器下可能出現(xiàn)版本不一(本地安裝能保證團(tuán)隊(duì)的版本一致),并且使用時(shí)可能出現(xiàn)本地和全局webpack版本混亂的情況。所以干脆就本地安裝。

安裝:npm i webpack webpack-cli --save-dev

webpack為核心庫(kù),webpack-cli是命令行工具。

由于是安裝于本地,所以可以使用 npx webpack 來(lái)使用。(npx是nodejs自帶的自動(dòng)執(zhí)行本地模塊的一個(gè)命令,具體可以參考npx 使用教程;

2、JS的模塊管理

在es6 module未成為標(biāo)準(zhǔn)前,有2個(gè)比較多人使用的模塊管理方案:AMD和commonJS。這兩者都是通過(guò)編譯后生成runtime,在代碼運(yùn)行過(guò)程中動(dòng)態(tài)引入。目前commonJS是nodejs的模塊管理標(biāo)準(zhǔn)。

  • AMD(Asynchronous Module Definition 異步模塊定義):通過(guò)聲明回調(diào)函數(shù)異步加載模塊,將其他模塊以依賴注入的方式加入進(jìn)當(dāng)前模塊,由于是異步加載所以不會(huì)阻塞當(dāng)前模塊加載。(參考:RequireJS和AMD規(guī)范;
// foo.js export
define({
    method1: function() {},
    method2: function() {},
});
   
// import
require(['foo'], function ( foo ) {
        foo.method1();
});
  • commonJS:通過(guò)一個(gè)模塊對(duì)象引出、引入其他模塊,引入的值為拷貝值,相較AMD語(yǔ)法更簡(jiǎn)單方便;
// export
module.exports = {name: 123}

// import 
var name = require('./export.js').name
  • es6 module:通過(guò)語(yǔ)法靜態(tài)引出、引入其他模塊,是在編譯期間進(jìn)行,引入的值只讀的變量映射(因此可以解決循環(huán)引用的問(wèn)題,通過(guò)包裹一個(gè)函數(shù)的方式,參考:JavaScript 模塊的循環(huán)加載),所以原始值改變會(huì)影響到引用者。因?yàn)槭庆o態(tài)引用,所以不像前兩者可以將import語(yǔ)句寫在任意地方,如if判斷內(nèi),需要在編譯過(guò)程就確定是否引入。
// export
export const a = 123;

// import 
import {a} from './export'
  • 三個(gè)模塊引入的方式,都在第一次引入時(shí)將該目標(biāo)模塊代碼執(zhí)行一遍。
    PS:因?yàn)榍皟烧卟皇枪俜綐?biāo)準(zhǔn),所以都需要借助webpack等模塊打包工具進(jìn)行編譯打包后才可以在瀏覽器上運(yùn)行,而es6 module可以直接在瀏覽器上運(yùn)行,只需要把script標(biāo)簽的type設(shè)置為module。但如果你在本地想試驗(yàn)一下es6 module則會(huì)發(fā)現(xiàn),本地引入模塊時(shí)用的是file協(xié)議,因?yàn)樵跒g覽器引入js資源時(shí)需要域名、協(xié)議、端口一致,所以在file協(xié)議下沒(méi)有域名會(huì)觸發(fā)跨域限制(chrome、firefox會(huì),ie不會(huì)),導(dǎo)致引入失敗。
  • UMD(Universal Module Definition 通用模塊標(biāo)準(zhǔn)):這不是一個(gè)模塊管理方案,是一個(gè)統(tǒng)一所有模塊管理方案的解決。webpack中則應(yīng)用了這個(gè)方案。由于會(huì)通過(guò)全局是否有define函數(shù)來(lái)判斷AMD環(huán)境,但在AMD的規(guī)則下是無(wú)法使用commonJS和es6 module的,所以如果項(xiàng)目全使用commonJS卻因?yàn)槟承┰虺霈F(xiàn)了define函數(shù),則可能導(dǎo)致全部模塊失效,需要手動(dòng)去修改webpack UMD模塊的判斷順序。
/*
 UMD判斷模塊管理方案的源碼
 */
(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['b'], factory);
    } else if (typeof module === 'object' && module.exports) {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like environments that support module.exports,
        // like Node.
        module.exports = factory(require('b'));
    } else {
        // Browser globals (root is window)
        root.returnExports = factory(root.b);
    }
}(this, function (b) {
    //use b in some fashion.
    // Just return a value to define the module export.
    // This example returns an object, but the module
    // can return a function as the exported value.
    return {};
}));

3、chunk、entry、bundle的基本概念

chunk:打包的模塊
entry:打包的入口文件
bundle:每個(gè)模塊打包好后的文件1

image
圖片來(lái)自《Webpack實(shí)戰(zhàn):入門、進(jìn)階與調(diào)優(yōu)》

4、配置資源入口

// webpack.config.js
module.exports = {
    /*
    * context配置entry的路徑前綴,可以理解為入口的文件上下文,所以是絕對(duì)路徑,這樣在多個(gè)entry的時(shí)候?qū)懫饋?lái)比較方便
    */
    context: path.join(__dirname, './src'),
    
    /*
    * 打包入口文件
    * 入口可為多個(gè),entry的值可以是數(shù)組、字典,用函數(shù)或promise返回這兩種數(shù)據(jù)結(jié)構(gòu)也可以
    */
    entry: './index.js'
}

5、提取公共模塊

像loadsh、jquery這些第三方的庫(kù),如果都跟業(yè)務(wù)代碼一起打進(jìn)一個(gè)bundle文件就會(huì)很大,并且每次代碼更新都需要更新整個(gè)文件。這時(shí)候可將一些公共模塊抽出來(lái),就不用跟業(yè)務(wù)代碼混雜在一起了。

module.exports = {
    entry: {
        app: './index.js', // 主入口
        vender: ['react', 'lodash', 'jquery'] // vender是‘提供商’的意思,這里理解為第三方模塊
    }
}

6、配置資源出口

資源出口的配置在output對(duì)象種配置。

module.exports = {    
    entry: './src/app.js',    
    output: {        
        // bundle文件名
        filename: 'bundle.js',       
        // bundle導(dǎo)出路徑
        path: path.join(__dirname, 'assets'), 
        // 資源訪問(wèn)上下文       
        publicPath: '/dist/',    
    },
};

output詳解:

  • filename的書寫形式:
// 直接寫bundle名
filename: 'bundle.js'

// 相對(duì)路徑,webpack會(huì)自動(dòng)幫你創(chuàng)建src文件夾
filename: './src/bundle.js'

// 動(dòng)態(tài)指定文件名,具體看下圖
filename: '[name].js'
image
圖片來(lái)自《Webpack實(shí)戰(zhàn):入門、進(jìn)階與調(diào)優(yōu)》
  • path:打包資源輸出路徑,默認(rèn)為項(xiàng)目根目錄的dist文件夾,需要配置絕對(duì)路徑;
  • publicPath:資源的請(qǐng)求位置,html直接請(qǐng)求的資源如sript標(biāo)簽里的js,和js或css的間接請(qǐng)求的資源資源如引入模塊和css加載背景/圖,這些都屬于資源請(qǐng)求,他們的路徑都會(huì)在前面加上publicPath;
// 相對(duì)路徑則會(huì)從當(dāng)前請(qǐng)求的文件路徑開始銜接
publicPath: './js'
// 在app目錄下的html直接請(qǐng)求的資源index.js => www.example.com/app/js/index.js 

// 以 / 開頭,則直接從域名后開始銜接
publicPath: '/js'
// 請(qǐng)求資源index.js => www.example.com/js/index.js

// 絕對(duì)路徑,一般用CDN的場(chǎng)景
publicPath: 'www.cdn.com/js'
// 請(qǐng)求資源index.js => www.cdn.com/js/index.js
  • output.path建議跟devServer里面的publicPath保持一致,這樣在開發(fā)和生產(chǎn)環(huán)境才不會(huì)搞混,具體原因可以看2、webpack-dev-server的解釋;

7、webpack模塊打包的簡(jiǎn)單原理(理解地比較粗淺)

通過(guò)聲明一個(gè)installedModules字典來(lái)存儲(chǔ)每個(gè)模塊,給每個(gè)模塊設(shè)置一個(gè)唯一key,全部傳入一個(gè)立即執(zhí)行的匿名函數(shù),有個(gè)入口模塊,在里面執(zhí)行所有模塊并存儲(chǔ)進(jìn)installedModules,已經(jīng)執(zhí)行過(guò)的模塊會(huì)直接拿緩存。

webpack編譯打包后的代碼,在瀏覽器中是這么運(yùn)行的:1、初始化環(huán)境和一些數(shù)據(jù)結(jié)構(gòu);2、執(zhí)行入口模塊代碼;3、執(zhí)行模塊代碼,記錄export和導(dǎo)出import(遞歸);4、所有模塊代碼執(zhí)行完畢,控制權(quán)回到入口模塊;

8、loader

loader可以譯為裝載機(jī),在webpack中一切皆模塊,這也是為什么引入css需要在js中import,因?yàn)閣ebpack只能識(shí)別js,而一個(gè)組件或者頁(yè)面的js+css就是一個(gè)模塊,所以通過(guò)在js中引用css的方式來(lái)將其綁定成一個(gè)模塊。

// app.js
import './style.css';

// style.css
body {    
    text-align: center;    
    padding: 100px;    
    color: #fff;    
    background-color: #09c;
}

loader其實(shí)一個(gè)函數(shù),它的輸入和輸出是源碼或上一個(gè)loader的輸出(字符串、source map、AST),所以loader的調(diào)用是鏈?zhǔn)降?,像一個(gè)流水線一樣將模塊打包出去。這也意味著loader的聲明是需要注意順序的。
ps:source map是一個(gè)json文件,用來(lái)解決代碼編譯前后的映射問(wèn)題

loader配置:

module.exports = {      
    module: {        
        rules: [{         
            // 正則匹配需要進(jìn)入loader的文件   
            test: /\.css$/,    
            
            // 用到的loader數(shù)組(loader的執(zhí)行順序從后到前,所以這里是css-loader先執(zhí)行)         
            use: [
                'style-loader',  
                
                // loader除了上面'style-loader'這種直接聲明字符串
                // 還可以像下面'css-loader'這樣聲明一些配置項(xiàng)
                {
                    loader: 'css-loader', 
                    options: {
                          // css-loader 配置項(xiàng)
                      }
                }
            ],  
            
            // loader處理文件的排除范圍
            exclude: /node_modules/,  // 正則 
            
            // 處理范圍,exclude優(yōu)先于include,意味著如果兩個(gè)配置有重疊,include是不能覆蓋exclude的
            include: /src/,  // 正則 
            
            /*
            * 在Webpack中,我們認(rèn)為被加載模塊是resource,而加載者是issuer。
            * 比如在這個(gè)例子里,css文件是加載模塊(resource),js文件則是加載者(issuer)
            * 所以下面是配置js文件,則是配置加載者
            * 前面的loader則是配置加載模塊
            */
            issuer: {
                test: /\.js$/,
                include: /src/pages/  // 正則 
            },
            
            // loader執(zhí)行順序:
            // normal(默認(rèn),按排列順序)、pre(在所有正常loader前)、post(在所有正常lodaer后)
            enforce: 'normal',
        }],    
    },
};

9、寫一個(gè)最簡(jiǎn)單的loader

上面說(shuō)了loader其實(shí)就是一個(gè)有輸入輸出的函數(shù),所以最簡(jiǎn)單的loader其實(shí)只要寫一個(gè)函數(shù)就行。

  1. 用 npm init 初始化一個(gè)項(xiàng)目;
  2. 創(chuàng)建一個(gè)index.js寫入以下代碼;
// 這個(gè)loader可以在js文件頭部加上 “這是我加上去的代碼” 這句注釋
// content 則是loader的輸入即源碼或上一個(gè)loader的輸出字符串
module.exports = function(content) {     
    var useStrictPrefix = `
        // 這是我加上去的代碼
    `;

    return useStrictPrefix + content;
}
  1. 在另一個(gè)項(xiàng)目通過(guò) npm install <絕對(duì)路徑> 來(lái)安裝loader;
  2. 在webpack配置文件中寫入loader配置;
  3. 執(zhí)行編譯;

10、webpack-dev-server

開啟一個(gè)熱更新的服務(wù),可以修改代碼后通過(guò)websocket通知瀏覽器更新。devServer會(huì)對(duì)代碼進(jìn)行編譯打包,但不會(huì)生成文件,打包后的代碼會(huì)放進(jìn)內(nèi)存訪問(wèn),當(dāng)瀏覽器對(duì)這個(gè)服務(wù)發(fā)起請(qǐng)求,它會(huì)先校驗(yàn)請(qǐng)求的url是不是配置文件里devServer的publicPath。

安裝:npm install --save-dev webpack-dev-server

配置:

devServer: {
    /*
        devServer.contentBase
        
        決定了 webpackDevServer 啟動(dòng)時(shí)服務(wù)器資源的根目錄,默認(rèn)是項(xiàng)目的根目錄。
        
        在有靜態(tài)文件需要 serve 的時(shí)候必填,contentBase 不會(huì)影響 path 和 publicPath,
        它唯一的作用就是指定服務(wù)器的根目錄來(lái)引用靜態(tài)文件。
        
        可以這么理解 contentBase 與 publicPath 的關(guān)系:contentBase 是服務(wù)于引用靜態(tài)文件的路徑,
        而 publicPath 是服務(wù)于打包出來(lái)的文件訪問(wèn)的路徑,兩者是不互相影響的。
    */
    contentBase: './dist',
    
    /*
        devServer.publicPath
        
        在開啟 webpackDevServer 時(shí)瀏覽器中可通過(guò)這個(gè)路徑訪問(wèn) bundled 文件,
        靜態(tài)文件會(huì)加上這個(gè)路徑前綴,若是devServer里面的publicPath沒(méi)有設(shè)置,
        則會(huì)認(rèn)為是output里面設(shè)置的publicPath的值。
        (如果有使用htmlWebpackPlugin,建議devServer.publicPath不填或者跟output.publicPath一致,
        因?yàn)樵陂_啟devServer后,htmlWebpackPlugin插入js會(huì)使用devServer.publicPath)
        
        和 output.publicPath 非常相似,都是為瀏覽器制定訪問(wèn)路徑的前綴。
        但是不同的是 devServer.publicPath 只影響于 webpackDevServer(一般來(lái)說(shuō)就是 html),
        但各種 loader 打出來(lái)的路徑還是根據(jù) output.publicPath。
    */
    publicPath: './dist'
}

參考:https://github.com/fi3ework/blog/issues/39

11、代碼分片

考慮到緩存和減少請(qǐng)求時(shí)間等原因,需要將公共代碼分塊。不同于之前使用的CommonsChunk-Plugin插件,webpack4有了改進(jìn)版的代碼分片配置optimization.SplitChunks。

不像CommonsChunk-Plugin需要去將特定的模塊提取出來(lái),使用SplitChunks只需要配置提取條件,webpack就會(huì)將符合條件的模塊打包出來(lái)。下面是默認(rèn)配置:

optimization: {        
    splitChunks: {     
        // chunks: async(默認(rèn),只提取異步模塊) | initial(只提取入口) | all(前兩者都提取)
        chunks: 'all',    
        
        // 按cacheGroups的提取規(guī)則,并以automaticNameDelimiter為分隔符命名chunks
        // eg: vendors~a~b~c.js意思是該chunk為vendors規(guī)則所提取,并且該chunk是由a、b、c三個(gè)入口chunk所產(chǎn)生的。
        name: true,
        // chunk命名的分隔符
        automaticNameDelimiter: '~', 
        
        /* 
        * 根據(jù)chunk資源本身情況配置規(guī)則
        */
        // 提取后的Javascript chunk體積大于30kB(壓縮和gzip之前),CSS chunk體積大于50kB
         minSize: {      
             javascript: 30000,      
             style: 50000,    
         },         
         // 在按需加載過(guò)程中,并行請(qǐng)求的資源最大值小于等于5
         maxAsyncRequests: 5,  
         // 在首次加載時(shí),并行請(qǐng)求的資源數(shù)最大值小于等于3  
         maxInitialRequests: 3,
         // 備注:設(shè)置maxAsyncRequests和maxInitialRequests是因?yàn)椴幌M麨g覽器一次發(fā)出過(guò)多請(qǐng)求,
         // 所以希望把一次加載的模塊限定規(guī)定次數(shù);
        
        /* 
        * 根據(jù)chunk來(lái)源配置提取規(guī)則
        */
        cacheGroups: {        
            // 模塊來(lái)自node_modules目錄,vendors只是chunk命名,可靈活調(diào)整;
            vendors: {            
                test: /[\\/]node_modules[\\/]/,            
                priority: -10, // 優(yōu)先級(jí),這里vendors優(yōu)先        
            },
            // chunk被至少兩個(gè)模塊引用則重用
            default: {            
                minChunks: 2,    
                reuseExistingChunk: true,   
                priority: -20,          
            },
        }
    },    
}

// 正常只需要像下面這樣聲明即可
optimization: {        
    splitChunks: {     
        chunks: 'all'
    }
}

12、webpack異步模塊加載

// 異步地將b.js加載進(jìn)來(lái)
import('./b.js').then((b) => {
    ...
})

這個(gè)異步import,webpack是通過(guò)動(dòng)態(tài)插入script標(biāo)簽來(lái)實(shí)現(xiàn)的,因?yàn)橹疤徇^(guò),通過(guò)script加載進(jìn)來(lái)的屬于間接資源請(qǐng)求,這個(gè)資源位置需要通過(guò)output.publicPath來(lái)確定,所以需要配置號(hào)output.publicPath;

13、環(huán)境區(qū)分

在開發(fā)過(guò)程中,需要區(qū)分開發(fā)環(huán)境和生產(chǎn)環(huán)境,開發(fā)環(huán)境一般完成基本的編譯打包工作,讓代碼能在瀏覽器運(yùn)行就好,而生產(chǎn)環(huán)境為了更小的包體通常還會(huì)進(jìn)行壓縮、tree-shaking等操作。將這兩種環(huán)境的操作區(qū)分開來(lái),一般有兩種方案:

  • 通過(guò)命令傳入環(huán)境變量
// package.json
{  ...  
    "scripts": {    
        "dev": "ENV=development webpack-dev-server",    
        "build": "ENV=production webpack"  
    },
}
        
// webpack.config.js
const ENV = process.env.ENV;
const isProd = ENV === 'production';
module.exports = {  
    output: {    
        filename: isProd ? 'bundle@[chunkhash].js' : 'bundle.js',  
    },  
    // mode模式如果為production,webpack會(huì)默認(rèn)添加一些配置,幫助壓縮代碼
    mode: ENV,
};
  • 為兩種環(huán)境分別寫一個(gè)配置文件,公用的部分可以通過(guò)webpack-merge來(lái)合并
{  ...  
    "scripts": {    
        "dev": " webpack-dev-server --config=webpack.development.config.js",    
        "build": " webpack --config=webpack.production.config.js"  
    },
}
  • 通過(guò)webpack中可以通過(guò)DefinePlugin插件將環(huán)境信息注入到j(luò)s運(yùn)行環(huán)境:
// webpack.config.js
plugins: [        
     new webpack.DefinePlugin({            
         ENV: JSON.stringify('production'),        
     })    
 ]
 
 // app.js
 document.write(ENV);

14、tree-shaking

tree-shaking能使webpack在打包過(guò)程中,將一些沒(méi)引用到的多余包體剔除,但有兩點(diǎn)需要注意,一是tree-shaking只在es6 moudle下生效(依靠es6 moudle靜態(tài)引用實(shí)現(xiàn)),意味著commonjs的模塊管理是不可行的;二是tree-shaking只是做多余包體的標(biāo)記工作,實(shí)際剔除代碼還是需要借助壓縮插件如terser-web-pack-plugin,但在webpack4只需要將mode設(shè)置為production即可。

針對(duì)上面第一點(diǎn),需要注意在babel-loader中設(shè)置module=false,禁止bable將模塊轉(zhuǎn)為commonjs。

15、模塊熱替換(hot module replace)

監(jiān)聽(tīng)文件變化,不同于live reload(刷新頁(yè)面,全量更新),熱替換是增量修改,不刷新網(wǎng)頁(yè),只更改局部。

開啟HMR:

const webpack = require('webpack');

module.exports = {  // ...  
    plugins: [    
        new webpack.HotModuleReplacementPlugin()  
    ],  
    devServer: {    hot: true,  },
};

HMR原理:

  • 首先瀏覽器端會(huì)有HMR runtime,webpack會(huì)起一個(gè)webpack-dev-server(WDS),兩者依靠websocket通信;
  • 當(dāng)WDS監(jiān)聽(tīng)到文件變化,會(huì)向客戶端推送更新事件,并帶上構(gòu)建的hash??蛻舳烁鶕?jù)這個(gè)hash和之前資源的對(duì)比,判斷是否需要更新;
  • 當(dāng)需要更新,客戶端就會(huì)向WDS請(qǐng)求更改的資源列表。WDS會(huì)返回需要構(gòu)建的chunk name和資源版本hash??蛻舳嗽俑鶕?jù)這些信息向WDS請(qǐng)求增量更新的資源;
  • 拿到更新的資源,HMR runtime就會(huì)開始決定哪些地方需要替換。webpack會(huì)暴露一個(gè)module.hot接口,用于給使用者知道熱替換的時(shí)機(jī),module.accept則是設(shè)置需要替換的模塊。一般loader都會(huì)設(shè)置相應(yīng)的模塊熱替換的補(bǔ)丁操作,對(duì)替換模塊進(jìn)行操作。如果runtime對(duì)某個(gè)模塊沒(méi)有檢測(cè)到對(duì)HMR的update handler,則會(huì)將替換操作冒泡到父級(jí)模塊,以此類推。(參考:webpack中文文檔)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 寫在開頭 先說(shuō)說(shuō)為什么要寫這篇文章, 最初的原因是組里的小朋友們看了webpack文檔后, 表情都是這樣的: (摘...
    Lefter閱讀 5,432評(píng)論 4 31
  • Webpack學(xué)習(xí)總結(jié) 此文只是自己學(xué)習(xí)webpack的一些總結(jié),方便自己查閱,閱讀不變,非常抱歉??! 下載安裝:...
    Lxs_597閱讀 1,082評(píng)論 0 0
  • 在現(xiàn)在的前端開發(fā)中,前后端分離、模塊化開發(fā)、版本控制、文件合并與壓縮、mock數(shù)據(jù)等等一些原本后端的思想開始...
    Charlot閱讀 5,653評(píng)論 1 32
  • 寫在前面 第一次接觸webpack,是在一個(gè)react項(xiàng)目參與中,剛開始使用的時(shí)候,甚至不知道是做什么用的,只看到...
    默默先生Alec閱讀 767評(píng)論 0 3
  • 《好的愛(ài)情:陳果的愛(ài)情哲學(xué)課》:每個(gè)人都向往愛(ài)情,但是卻少有人懂得如何真正去愛(ài)。多少情侶明明愛(ài)得死去活來(lái),結(jié)果卻還...
    simple涯閱讀 242評(píng)論 0 5

友情鏈接更多精彩內(nèi)容