Webpack實戰筆記

◆ 使用配置文件

 

>> Webpack對於output.path的要求是使用絕對路徑(從系統根目錄開始的完整路徑),用node.js的路徑拼裝函數---path.join,將__dirname(node.js內置全局變量,值爲當前文件所在的絕對路徑)與dist(輸出目錄)連接起來,得到了最終的資源輸出路徑。

 

◆ webpack-dev-server

 

>> 安裝npm install--production過濾掉devDependencies中的冗餘模塊,從而加快安裝和發佈的速度。

 

>> 綜上我們可以總結出webpack-dev-server的兩大職能:·令Webpack進行模塊打包,並處理打包結果的資源請求。·作爲普通的Web Server,處理靜態資源文件請求。

 

>> ,而webpack-dev-server只是將打包結果放在內存中,並不會寫入實際的bundle.js,在每次webpack-dev-server接收到請求時都只是將內存中的打包結果返回給瀏覽器

◆ CommonJS

 

>> 直到有了Browserify——一個運行在Node.js環境下的模塊打包工具,它可以將CommonJS模塊打包爲瀏覽器可以運行的單個文件。這意味着客戶端的代碼也可以遵循CommonJS標準來編寫了。

 

 

◆模塊

 

>> CommonJS中規定每個文件是一個模塊。將一個個JavaScript文件直接通過script標籤插入頁面中與封裝成commonJS模塊最大的不同在於,前者的頂層作用域是全局作用域,在進行變量及函數聲明時會污染全局環境;而後者會形成一個屬於模塊自身的作用域,所有的變量及函數只有自己能訪問,對外是不可見的。請看列子:

calculator.js

var name = 'calculator.js';

 

// index.js

var name = 'index.js';

require('./calculator.js');

console.log(name); // index.js

這裏有兩個文件,在index.js中我們通過CommonJS的require函數的加載函數calculator.js

。的結果是index.js,這說明calculator.js的變量不會影響index.js

 

 

◆ 導入

 

>> 有時我們加載一個模塊,不需要獲取其導出的內容,只是想要通過執行它而產生某種作用,比如把它的接口掛在全局對象上,此時直接使用require即可。Require(‘/task.js’)另外,require函數可以接收表達式,藉助這個特性我們可以動態地指定模塊加載路徑。

Const modulesNames = [‘foo.js’,’bar.js’];

moduleNames.forEach(name=>{

    Require(‘./’+name)

});

 

◆ 導出

 

>> 在使用命名導出時,可以通過as關鍵字對變量重命名。如:const name = 'calculator';const add = function(a, b) { return a + b; };export { name, add as getSum }; // 在導入時即爲 name 和 getSum與命名導出不同.

模塊的默認導出只能有一個。如:export default {name: 'calculator', add: function(a, b) { return a + b; }};

Export default理解爲對外輸出了一個名爲default的變量,因此不需要像命名導出一樣進行變量申明,直接導出即可。

//導出字符串

Export default “this is calcuiator.js”;

//導出

Export default class{...}

//導出匿名函數

Export default function(){...}

◆ 導入

 

>> 加載帶有命名導出的模塊時,import後面要跟一對大括號來將導入的變量名包裹起來,並且這些變量名需要與導出的變量名完全一致。導入變量就相當於在當前作用域下聲明瞭這些變量的,並且不可對齊進行修改(是隻讀的)

 

◆ 複合寫法

 

>> 2.2.4 複合寫法在工程中,有時需要把某一個模塊導入之後立即導出,比如專門用來集合所有頁面或組件的入口文件。此時可以採用複合形式的寫法:export { name, add } from './calculator.js';複合寫法目前只支持當被導入模塊。通過命名導出的方式暴露出來的變量,默認導出則沒有對應的複合形式,只能將導入和導出拆開寫。import  xxx  from  "xxxx ";

export  default  xxxxx;

 

◆ 動態與靜態

 

>> CommonJS與ES6 Module最本質的區別在於前者對模塊依賴的解決是“動態的”,而後者是“靜態的”。在這裏“動態”的含義是,模塊依賴關係的建立發生在代碼運行階段;而“靜態”則是模塊依賴關係的建立發生在代碼編譯階段。

 

>> ES6 Module的導入、導出語句都是聲明式的,它不支持導入的路徑是一個表達式,並且導入、導出語句必須位於模塊的頂層作用域。因此我們說,ES6 Module是一種靜態的模塊結構,ES6 Module是一種靜態的模塊結構,在es6代碼的編譯階段就可以分析出模塊的依賴。它相比於commonJS來說具備以下幾點優勢:

·死代碼檢測和排除。我們可以用靜態分析工具檢測出哪些模塊沒有被調用過。比如,在引入工具類庫時,工程中往往只用到了其中一部分組件或接口,但有可能會將其代碼完整地加載進來。未被調用到的模塊代碼永遠不會被執行,也就成爲了死代碼。通過靜態分析可以在打包時去掉這些未曾使用過的模塊,以減小打包資源體積。

·模塊變量類型檢查。JavaScript屬於動態類型語言,不會在代碼執行前檢查類型錯誤(比如對一個字符串類型的值進行函數調用)。ES6 Module的靜態模塊結構有助於確保模塊之間傳遞的值或接口類型是正確的。

·編譯器優化。在CommonJS等動態模塊系統中,無論採用哪種方式,本質上導入的都是一個對象,而ES6 Module支持直接導入變量,減少了引用層級,程序效率更高。

 

◆ 值拷貝與動態映射

 

>> 在導入一個模塊時,對於CommonJS來說獲取的是一份導出值的拷貝;而在ES6 Module中則是值的動態映射,並且這個映射是隻讀的。

下列什麼是CommonJS中的值拷貝。

// calculator.js

var count = 0;

module.exports = {

    count: count,

    add: function(a, b) {

        count += 1;

        return a + b;

    }

};

// index.js

var count = require('./calculator.js').count;

var add = require('./calculator.js').add;

console.log(count); // 0(這裏的count是對 calculator.js 中 count 值的拷貝)

add(2, 3);

console.log(count); // 0(calculator.js中變量值的改變不會對這裏的拷貝值造成影響)

 

count += 1;

console.log(count); // 1(拷貝的值可以更改)

 

◆UMD

 

>> 嚴格來說,UMD並不能說是一種模塊標準,不如說它是一組模塊形式的集合更準確。UMD的全稱是Universal Module Definition,也就是通用模塊標準,它的目標是使一個模塊能運行在各種環境下,不論是CommonJS、AMD,還是非模塊化的環境

 

◆ entry

 

>> 傳入一個數組的作用是將多個資源預先合併,在打包時Webpack會將數組中的最後一個元素作爲實際的入口路徑。如:

Module.exports={

  Entry:[‘babel-polyfill’,’./src/index.js’]

}

上面等同於

//webpack.config.js

 

Module.exports={

Entry:’./src/index.js’

}

 

// Index.js

Import ‘babel-polyfill’

 

◆ 實例

 

>> vendor的意思是“供應商”,在Webpack中vendor一般指的是工程所使用的庫、框架等第三方模塊集中打包而產生的bundle。請看下面這個例子:

 

◆  publicPath

publicPath是一個非常重要的配置項,並且容易與path相混淆。從功能上來說,path用來指定資源的輸出位置,而publicPath則用來指定資源的請求位置。讓我們詳細解釋這兩個定義。

·輸出位置:打包完成後資源產生的目錄,一般將其指定爲工程中的dist目錄。

·請求位置:由JS或CSS所請求的間接資源路徑。頁面中的資源分爲兩種,一種是由HTML頁面直接請求的,比如通過script標籤加載的JS;另一種是由JS或CSS請求的,如異步加載的JS、從CSS請求的圖片字體等。publicPath的作用就是指定這部分間接資源的請求位置。

 

◆loader的配置

 

>> loader的字面意思是裝載器,在Webpack中它的實際功能則更像是預處理器。Webpack本身只認識JavaScript,對於其他類型的資源必須預先定義一個或多個loader對其進行轉譯,輸出爲Webpack能夠接收的形式再繼續進行,因此loader做的實際上是一個預處理的工作。

 

◆ loader的引入

 

>> css-loader的作用僅僅是處理CSS的各種加載語法(@import和url()函數等),如果要使樣式起作用還需要style-loader來把樣式插入頁面。css-loader與style-loader通常是配合在一起使用的。

 

◆ 更多配置

 

>> 另外,由於exclude優先級更高,我們可以對include中的子目錄進行排除。請看下面的例子:rules: [    {        test: /\.css$/,        use: ['style-loader', 'css-loader'],        exclude: /src\/lib/,        include: /src/,    }],通過include,我們將該規則配置爲僅對src目錄生效,但是仍然可以通過exclude排除其中的src/lib目錄。2.resource與issuer

 

>> 在Webpack中,我們認爲被加載模塊是resource,而加載者是issuer。如上面的例子中,resource爲/path/of/app/style.css,issuer是/path/of/app/index.js。前面介紹的test、exclude、include本質上屬於對resource也就是被加載者的配置,如果想要對issuer加載者也增加條件限制,則要額外寫一些配置。比如,如果我們只想讓/src/pages目錄下的JS可以引用CSS,應該如何設置呢?請看下面的例

rules: [

    {

        test: /\.css$/,

        use: ['style-loader', 'css-loader'],

        exclude: /node_modules/,

        issuer: {

             Test:/\.js$/,

             Include:/src/pages/,

             },

           }

      ]

>>可以看到,我們添加了issuer配置對象,其形式與之前對resource條件的配置並無太大差異。但只有/src/pages/目錄下面的JS文件引用CSS文件,這條規則纔會生效;如果不是JS文件引用的CSS(比如JSX文件),或者是別的目錄的JS文件引用CSS,則規則不會生效。

上面的配置雖然實現了我們的需求,但是test、exclude、include這些配置項分佈於不同的層級上,可讀性較差。事實上我們還可以將它改爲另一種等價的形式。

rules: [

    {

        use: ['style-loader', 'css-loader'],

     resource: {

            test: /\.css$/,

            exclude: /node_modules/,

        },

        issuer: {

            test: /\.js$/,

            exclude: /node_modules/,

        },

    }

],

通過添加resource對象來將外層的配置包起來,區分了resource和issuer中的規則,這樣就一目瞭然了。上面的配置與把resource的配置寫在外層在本質上是一樣的,然而這兩種形式無法並存,只能選擇一種風格進行配置

 

>> 可以看到,在配置中添加了一個eslint-loader來對源碼進行質量檢測,其enforce的值爲“pre”,代表它將在所有正常loader之前執行,這樣可以保證其檢測的代碼不是被其他loader更改過的。類似的,如果某一個loader是需要在所有loader之後執行的,我們也可以指定其enforce爲“post”

 

◆ babel-loader

 

>> 3)由於@babel/preset-env會將ES6 Module轉化爲CommonJS的形式,這會導致Webpack中的tree-shaking特性失效。將@babel/preset-env的modules配置項設置爲false會禁用模塊語句的轉化,而將ES6 Module的語法交給Webpack本身處理。

 

◆ html-loader

 

>> html-loader用於將HTML文件轉化爲字符串並進行格式化,這使得我們可以把一個HTML片段通過JS加載進來。安

 

◆ 自定義loader

 

>>在開發一個loader時,我們可以藉助npm/yarn的軟連接功能進行本地調試(當然之後可以考慮發佈到npm等)下面讓我們初始化這個loader並配置到過程中,創建一個force-strict-lodader目錄,然後該目錄下執行npm初始化命令。Npm init -y 接着創建index.js也就是loader的主體。module.exports = function(content) {     var useStrictPrefix = '\'use strict\';\n\n';     return useStrictPrefix + content;}現在我們可以在Webpack工程中安裝並使用這個loader了。npm install <path-to-loader>/force-strict-loader在Webpack工程目錄下使用相對路徑的安裝,會在項目的node_modules中創建一個指向實際fore-strict-loader的軟鏈接,也就是我們可以隨時修改loder的源碼且不需要重新安裝。下面爲webpack配置.

Module:{

   Rules:[

      {

      Test:/\.js$/,

Use:’force-strict-lodaer’

}

]

}

 

>> 啓用緩存當文件輸入和其依賴沒有發生變化時,應該讓loader直接使用緩存,而不是重複進行轉換的工作。在Webpack中可以使用this.cacheable進行控制,

 

>> 接着更改loader。

// force-strict-loader/index.js

var loaderUtils = require("loader-utils");

module.exports = function(content) {   

 if (this.cacheable) {    

    this.cacheable();

   }   

 // 獲取和打印 options  

  var options = loaderUtils.getOptions(this) || {};   

 console.log('options', options);    // 處理 content  

  var useStrictPrefix = '\'use strict \’;\n\n;

  Return useStrictPrefix + coutent;

      }

◆extract-text-webpack-plugin

我們先通過一個簡單的例子來直觀認識該插件是如何工作的。使用npm安裝:

npm install extract-text-webpack-plugin

在webpack.config.js中引入:

const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {

    entry: './app.js',

    output: {

        filename: 'bundle.js',

    },

    mode: 'development',

    module: {

        rules: [

            {

                test: /\.css$/,

                use: ExtractTextPlugin.extract({

                    fallback: 'style-loader',

                    use: 'css-loader',

}),

            }

        ],

    },

    plugins: [

        new ExtractTextPlugin("bundle.css")

    ],

};

在module.rules中我們設置了處理CSS文件的規則,其中的use字段並沒有直接傳入loader,而是使用了插件的extract方法包了一層。內部的fallback屬性用於指定當插件無法提取樣式時所採用的loader,use(extract方法裏面的)用於指定在提取樣式之前採用哪些loader來預先進行處理。除此之外,還要在Webpack的plugins配置中添加該插件,並傳入提取後的資源文件名。

 

◆ mini-css-extract-plugin

    .lodaer規則設置的形式不同,並且mini-css-extract-plugin支持配置publicPath,用來指定異步css的加載路徑

>> ·不需要設置fallback。

·在plugins設置中,除了指定同步加載的CSS資源名(filename),還要指定異步加載的CSS資源名(chunkFilename)。

 

◆ Sass與SCSS

 

>> 假如我們想要在瀏覽器的調試工具裏查看源碼,需要分別爲sass-loader和css-loader單獨添加source map的配置項。

 

◆ PostCSS與Webpack

 

>> PostCSS要求必須有一個單獨的配置文件。在最初的版本中,其配置是可以通過loader來傳入的,而在Webpack 2對配置添加了更嚴格的限制之後,PostCSS不再支持從loader傳入。因此我們需要在項目的根目錄下創建一個postcss.config.js。目前我們還沒有添加任何特性,因此暫時返回一個空對象即可。

 

◆  CSSNext

 

>> 5.3.4 CSSNextPostCSS可以與CSSNext結合使用,讓我們在應用中使用最新的CSS語法特性。使用npm安裝。npm install postcss-cssnext在postcss.config.js中添加相應配置。

const postcssCssnext = require('postcss-cssnext');

module.exports = {

    plugins: [

        postcssCssnext({

            // 指定所支持的瀏覽器

            browsers: [

                '> 1%',

                'last 2 versions',

            ],

        })

    ],

};

指定好需要支持的瀏覽器之後,我們就可以順暢地使用CSSNext的特性了,PossCss會幫助我們把CSSNext的語法翻譯爲瀏覽器能接受的屬性和形式。比如:

/* style.css */

:root {

    --highlightColor: hwb(190, 35%, 20%);

}

body {

    color: var(--highlightColor);

}

打包後的結果如下:

body {

    color: rgb(89, 185, 204);

}

◆ CSS Modules

 

CSS Modules

CSS Modules是近年來比較流行的一種開發模式,其理念就是把CSS模塊化,讓CSS也擁有模塊的特點,具體如下:

·每個CSS文件中的樣式都擁有單獨的作用域,不會和外界發生命名衝突。

·對CSS進行依賴管理,可以通過相對路徑引入CSS文件。

·可以通過composes輕鬆複用其他CSS模塊。

使用CSS Modules不需要額外安裝模塊,只要開啓css-loader中的modules配置項即可。

module: {

    rule{

            test: /\.css/,

            use: [

                'style-loader',

                {

                    loader: 'css-loader',

                    options: {

                        modules: true,

                        localIdentName: '[name]__[local]__[hash:base64:5]',

                    },

                }

            ],

        }

    ],

},

這裏比較值得一提的是localIdentName配置項,它用於指明CSS代碼中的類名會如何來編譯。假設源碼是下面的形式:

/* style.css */

.title {

    color: #f938ab;

}

經過編譯後可能將成爲.style__title__1CFy6。讓我們依次對照上面的配置:

·[name]指代的是模塊名,這裏被替換爲style。

·[local]指代的是原本的選擇器標識符,這裏被替換爲title。

·[hash:base64:5]指代的是一個5位的hash值,這個hash值是根據模塊名和標識符計算的,因此不同模塊中相同的標識符也不會造成樣式衝突

 

>> 在使用的過程中我們還要注意在JavaScript中引入CSS的方式。之前只是直接將CSS文件引入就可以了,但使用CSS Modules時CSS文件會導出一個對象,我們需要把這個對象中的屬性添加到HTML標籤,列

/*style.css*/

.title{

 Color:#f938ab;

}

//app.js

Import styles from ‘./style.css’

Document.write(<h1 class = “${stles.title}”>my webpack app </h1>)


◆ 設置提取範圍
通過CommonsChunkPlugin中的chunks配置項可以規定從哪些入口中提取公共模塊,請看下面的例子:

// webpack.config.js

const webpack = require('webpack');

module.exports = {

    entry: {

        a: './a.js',

        b: './b.js',

        c: './c.js',

    },

    output: {

        filename: '[name].js',

    },

    plugins: [

        new webpack.optimize.CommonsChunkPlugin({

            name: 'commons',

            filename: 'commons.js',

            chunks: ['a', 'b'],

        })

    ],

};

我們在chunks中配置了a和b,這意味着只會從a.js和b.js中提取公共模塊。

對於一個大型應用來說,擁有幾十個頁面是很正常的,這也就意味着會有幾十個資源入口。這些入口所共享的模塊也許會有些差異,在這種情況下,我們可以配置多個CommonsChunkPlugin,併爲每個插件規定提取的範圍,來更有效地進行提取.

 

◆ 設置提取規則

 

>> CommonsChunkPlugin的默認規則是隻要一個模塊被兩個入口chunk所使用就會被提取出來,比如只要a和b用了react,react就會被提取出來。

這個配置項的意義有兩個。第一個是和上面的情況類似,即我們只想讓Webpack提取特定的幾個模塊,並將這些模塊通過數組型入口傳入,這樣做的好處是提取哪些模塊是完全可控的;另一個是我們指定minChunks爲Infinity,爲了生成一個沒有任何模塊而僅僅包含Webpack初始化環境的文件,這個文件我們通常稱爲manifest。

◆ hash與長效緩存

 

>> 使用CommonsChunkPlugin時,一個繞不開的問題就是hash與長效緩存。當我們使用該插件提取公共模塊時,提取後的資源內部不僅僅是模塊的代碼,往往還包含Webpack的運行時(runtime)。Webpack的運行時指的是初始化環境的代碼,如創建模塊緩存對象、聲明模塊加載函數等

 

>> 這個問題解決的方案是:將運行時的代碼單獨提取出來,例:

/ webpack.config.js

const webpack = require('webpack');

module.exports = {

    entry: {

        app: './app.js',

        vendor: ['react'],

    },

    output: {

        filename: '[name].js',

    },

    plugins: [

        new webpack.optimize.CommonsChunkPlugin({

            name: 'vendor',

        }),

        new webpack.optimize.CommonsChunkPlugin({

            name: 'manifest',

        })

],};

上面的配置中,通過添加一個name爲manifest的commonsChunkPlugins來提取webpack運行時。注意 manifest的CommonsChunkPlugin必須出現在最後,否則Webpack將無法正常提取模塊

 

◆ optimization.SplitChunks

optimization.SplitChunks(簡稱SplitChunks)是Webpack 4爲了改進CommonsChunk-Plugin而重新設計和實現的代碼分片特性。它不僅比CommonsChunkPlugin功能更加強大,還更簡單易用。

比如我們前面異步加載的例子,在換成Webpack 4的SplitChunks之後,就可以自動提取出react了。請看下面的例子:

// webpack.config.js

module.exports = {

    entry: './foo.js',

    output: {

        filename: 'foo.js',

        publicPath: '/dist/',

},

    mode: 'development',

    optimization: {

        splitChunks: {

            chunks: 'all',

        },

    },

};

 

// foo.js

import React from 'react';

import('./bar.js');

document.write('foo.js', React.version);

 

// bar.js

import React from 'react';

console.log('bar.js', React.version);

此處Webpack 4的配置與之前相比有兩點不同:

使用optimization.splitChunks替代了CommonsChunkPlugin,並指定了chunks的值爲all,這個配置項的含義是,SplitChunks將會對所有的chunks生效(默認情況下,SplitChunks只對異步chunks生效,並且不需要配置)。

 .node是webpack 4中新增的配置項,可以針對當前是開發環境還是生成環境自動添加對應的一些webpack配置

 

◆ 從命令式到聲明式

>> 在使用CommonsChunkPlugin的時候,我們大多數時候是通過配置項將特定入口中的特定模塊提取出來,也就是更貼近命令式的方式。而SplitChunks的不同之處在於我們只需要設置一些提取條件,如提取的模式、提取模塊的體積等,當某些模塊達到這些條件後就會自動被提取出來。SplitChunks的使用更像是聲明式的。

以下是SplitChunks默認情形下的提取條件:

·提取後的chunk可被共享或者來自node_modules目錄。這一條很容易理解,被多次引用或處於node_modules中的模塊更傾向於是通用模塊,比較適合被提取出來。

·提取後的Javascript chunk體積

於30kB(壓縮和gzip之前),CSS chunk體積大於50kB。這個也比較容易理解,如果提取後的資源體積太小,那麼帶來的優化效果也比較一般。

·在按需加載過程中,並行請求的資源最大值小於等於5。按需加載指的是,通過動態插入script標籤的方式加載腳本。我們一般不希望同時加載過多的資源,因爲每一個請求都要花費建立鏈接和釋放鏈接的成本,因此提取的規則只在並行請求不多的時候生效。

·在首次加載時,並行請求的資源數最大值小於等於3。和上一條類似,只不過在頁面首次加載時往往對性能的要求更高,因此這裏的默認閾值也更低。

 

◆  配置

爲了更好地瞭解SplitChunks是怎樣工作的,我們來看一下它的默認配置。

splitChunks: {

    chunks: "async",

    minSize: {

      javascript: 30000,

      style: 50000,

    },

    maxSize: 0,

    minChunks: 1,

    maxAsyncRequests: 5,

    maxInitialRequests: 3,

    automaticNameDelimiter: '~',

    name: true,

    cacheGroups: {

        vendors: {

            test: /[\\/]node_modules[\\/]/,

            priority: -10,

        },

        default: {

            minChunks: 2,

            priority: -20,

reuseExistingChunk: true,

        },

    },

},

(1)匹配模式

通過chunks我們可以配置SplitChunks的工作模式。它有3個可選值,分別爲async(默認)、initial和all。async即只提取異步chunk,initial則只對入口chunk生效(如果配置了initial則上面異步的例子將失效),all則是兩種模式同時開啓。

(2)匹配條件

minSize、minChunks、maxAsyncRequests、maxInitialRequests都屬於匹配條件,前文已經介紹過了,不贅述。

(3)命名

配置項name默認爲true,它意味着SplitChunks可以根據

cacheGroups和作用範圍自動爲新生成的chunk命名,並以automaticNameDelimiter分隔。如vendors~a~b~c.js意思是cacheGroups爲vendors,並且該chunk是由a、b、c三個入口chunk所產生的。

(4)cacheGroups

可以理解成分離chunks時的規則。默認情況下有兩種規則——vendors和default。vendors用於提取所有node_modules中符合條件的模塊,default則作用於被多次引用的模塊。我們可以對這些規則進行增加或者修改,如果想要禁用某種規則,也可以直接將其置爲false。當一個模塊同時符合多個cacheGroups時,則根據其中的priority配置項確定優先級。

 

◆ 6.4.1 import()

 

>> 在Webpack中有兩種異步加載的方式——import函數及require.ensure。require.ensure是Webpack 1支持的異步加載方式,從Webpack 2開始引入了import函數。

 

>> 假設bar.js的資源體積很大,並且我們在頁面初次渲染的時候並不需要使用它,就可以對它進行異步加載。// foo.js

import('./bar.js').then(({ add }) => {  

  console.log(add(2, 3));

});

// bar.js

export function add(a, b) {    return a + b;}

 

>> 首屏加載的JS資源地址是通過頁面中的script標籤來指定的,而間接資源(通過首屏JS再進一步加載的JS)的位置則要通過output.publicPath來指定。用import函數相當於使用一個間接資源,我們需要配置publicPath來告訴Webpack去哪裏獲取它。此時

 

>> import函數還有一個比較重要的特性。ES6 Module中要求import必須出現在代碼的頂層作用域。

 

>> Webpack的import函數則可以在任何我們希望的時候調用。如:if (condition) {    import('./a.js').then(a => {        console.log(a);    });} else {    import('./b.js').then(b => {        console.log(b);    });}這種異步加

 

◆ 7.1 環境配置的封裝

 

>> // webpack.config.js

const ENV = process.env.ENV;

const isProd = ENV === 'production';

module.exports = {  

output: {    filename: isProd ? 'bundle@[chunkhash].js' : 'bundle.js',  },

 mode: ENV,};

 

 

◆ 7.3 環境變量

 

>> 通常我們需要爲生產環境和本地環境添加不同的環境變量,在Webpack中可以使用DefinePlugin進行設置。請看下面的例子

/ webpack.config.js

const webpack = require('webpack');

module.exports = {

    entry: './app.js',

    output: {

        filename: 'bundle.js',

    },

    mode: 'production',

    plugins: [

        new webpack.DefinePlugin({

            ENV: JSON.stringify('production'),

        })

    ],

};

 

// app.js

ocument.write(ENV);

上面的配置通過DefinePlugin設置了ENV環境變量,最終頁面上輸出的將會是字符串production。

除了字符串類型的值以外,我們也可以設置其他類型的環境變量。

new webpack.DefinePlugin({

    ENV: JSON.stringify('production'),

    IS_PRODUCTION: true,

    ENV_ID: 130912098,

    CONSTANTS: JSON.stringify({

        TYPES: ['foo', 'bar']

    })

})

[插圖]注意 我們在一些值的外面加上了JSON.stringify,這是因爲DefinePlugin在替換環境變量時對於字符串類型的值進行的是完全替換。假如不添加JSON.stringify的話,在替換後就會成爲變量名,而非字符串值。因此對於字符串環境變量及包含字符串的對象都要

加上JSON.stringify纔行。

許多框架與庫都採用process.env.NODE_ENV作爲一個區別開發環境和生產環境的變量。process.env是Node.js用於存放當前進程環境變量的對象;而NODE_ENV則可以讓開發者指定當前的運行時環境,當它的值爲production時即代表當前爲生產環境,庫和框架在打包時如果發現了它就可以去掉一些開發環境的代碼,如警告信息和日誌等。這將有助於提升代碼運行速度和減小資源體積。具體配置如下:

new webpack.DefinePlugin({

    process.env.NODE_ENV: 'production',

})

如果啓用了mode:production,則Webapck已經設置好了process.env.NODE_ENV,不需要再人爲添加了。

 

>> 如果啓用了mode:production,則Webapck已經設置好了process.env.NODE_ENV,不需要再人爲添加了。

 

◆ 原理

 

>> map文件有時會很大,但是不用擔心,只要不打開開發者工具,瀏覽器是不會加載這些文件的,因此對於普通用戶來說並沒有影響。但是使用source map會有一定的安全隱患,即任何人都可以通過dev tools看到工程源碼。後面我們會講到如何解決這個問題

 

◆ source map配置

 

>> 在生產環境中由於我們會對代碼進行壓縮,而最常見的壓縮插件UglifyjsWebpack-Plugin目前只支持完全的source-map,因此沒有那麼多選擇,我們只能使用source-map、hidden-source-map、nosources-source-map這3者之一。下面介紹一下這3種source map在安全性方面的不同。

 

◆  安全

 

>> bundle進行解析。如果我們想要追溯源碼,則要利用一些第三方服務,將map文件上傳到那上面。目前最流行的解決方案是Sentry錯誤跟蹤平臺,另外一種就是配置nosources-sourcemap

 

◆ 壓縮JavaScript

 

>> 壓縮JavaScript大多數時候使用的工具有兩個,一個是UglifyJS(Webpack 3已集成),另一個是terser(Webpack 4已集成)。後者由於支持ES6+代碼的壓縮,更加面向於未來,因此官方在Webpack 4中默認使用了terser的插件terser-webpack-plugin

 

◆ 使chunk id更穩定

 

>> 解決的方法在於更改模塊id的生成方式。在Webpack 3內部自帶了HashedModuleIds-Plugin,它可以爲每個模塊按照其所在路徑生成一個字符串類型的hash id。稍稍更改一下之前的配置就可以解決。plugins: [    new webpack.HashedModuleIdsPlugin(),    new webpack.optimize.CommonsChunkPlugin({        name: 'vendor',    })

 

>>使用chunk id 於其不支持字符串類型的模塊id,可以使用另一個由社區提供的兼容性插件webpack-hashed-module-id-plugin,可以起到一樣的效果。從Webpack 4以後已經修改了模塊id的生成機制,也就不再有該問題了

 

◆ bundle體積監控和分析

 

>> VS Code中有一個插件Import Cost可以幫助我們對引入模塊的大小進行實時監測。每當我們在代碼中引入一個新的模塊(主要是node_modules中的模塊)時,它都會爲我們計算該模塊壓縮後及gzip過後將佔多大體積。

另外一個很有用的工具是webpack-bundle-analyzer,它能夠幫助我們分析一個bundle的構成。使用方法也很簡單,只要將其添加進plugins配置即可

Const Analyzer = require(‘webpack-bundel-analyzer’).BundleAnalyzerPlugin;

Module.exports = {

  //....

Plugins:[

    New Analyzer()

]

}

 

◆ 打包優化

 

>> 首先重述一條軟件工程領域的經驗——不要過早優化

 

◆  單個loader的優化

 

 

◆ 多個loader的優化

 

>> 8.1.3 多個loader的優化在使用HappyPack優化多個loader時,需要爲每一個loader配置一個id,否則HappyPack無法知道rules與plugins如何一一對應。請看下面的例子,這裏同時對babel-loader和ts-loader進行了Happypack的替換。

 

◆ 8縮小打包作用域

 

>> 從宏觀角度來看,提升性能的方法無非兩種:增加資源或者縮小範圍。增加資源就是指使用更多CPU和內存,用更多的計算能力來縮短執行任務的時間,縮小範圍則是針對任務本身,比如去掉冗餘的流程,儘量不做重複性的工作等。前面我們說的happyPack屬於增加資源。

 

◆  noParse

 

>> noParse有些庫我們是希望Webpack完全不要去進行解析的,即不希望應用任何loader規則,庫的內部也不會有對其他模塊的依賴,那麼這時可以使用noParse對其進行忽略。請看下面的例子:module.exports = {  //...  module: {    noParse: /lodash/,  }};

 

◆ IgnorePlugin

 

>> 8.2.3 IgnorePluginexclude和include是確定loader的規則範圍,noParse是不去解析但仍會打包到bundle中。最後讓我們再看一個插件IgnorePlugin,它可以完全排除一些模塊,被排除的模塊即便被引用了也不會被打包進資源文件中。

 

◆ Cache

 

>> 在Webpack 5中添加了一個新的配置項“cache:{type:"filesystem"}”,它會在全局啓用一個文件緩存。要注意的是,該特性目前僅僅是實驗階段,並且無法自動檢測到緩存已經過期。比如我們更新了babel-loader及一些相關配置,但是由於JS源碼沒有發生變化,重新打包後還會是上一次的結果。

 

◆  動態鏈接庫與DllPlugin

 

>> DllPlugin和Code Splitting有點類似,都可以用來提取公共模塊,但本質上有一些區別。Code Splitting的思路是設置一些特定的規則並在打包的過程中根據這些規則提取模塊;DllPlugin則是將vendor完全拆出來,有自己的一整套Webpack配置並獨立打包,在實際工程構建時就不用再對它進行任何處理,直接取用即可。因此,理論上來說,DllPlugin會比Code Splitting在打包速度上更勝一籌,但也相應地增加了配置,以及資源管理的複雜度。下面我們一步步來進行DllPlugin的配置

 

◆ vendor配置

 

>> vendor配置首先需要爲動態鏈接庫單獨創建一個Webpack配置文件,比如命名爲webpack.vendor.config.js,用來區別工程本身的配置文件webpack.config.js。請看下面的例子:// webpack.vendor.config.js

const path = require('path');

const webpack = require('webpack');

const dllAssetPath = path.join(__dirname, 'dll');

const dllLibraryName = 'dllExample';

module.exports = {

  entry: ['react'],

  output: {

    path: dllAssetPath,

    filename: 'vendor.js',

    library: dllLibraryName,

  },

  plugins: [

    new webpack.DllPlugin({

      name: dllLibraryName,

path: path.join(dllAssetPath, 'manifest.json'),

    })

  ],

};

配置中的entry指定了把哪些模塊打包爲vendor。plugins的部分我們引入了Dll-Plugin,並添加了以下配置項。

·name:導出的dll library的名字,它需要與output.library的值對應。

·path:資源清單的絕對路徑,業務代碼打包時將會使用這個清單進行模塊索引。

◆ vendor打包

接下來我們就要打包vendor並生成資源清單了。爲了後續運行方便,可以在package.json中配置一條npm script,如下所示:

// package.json

{

  ...

  "scripts": {

    "dll": "webpack --config webpack.vendor.config.js"

  },

}

運行npm run dll後會生成一個dll目錄,裏面有兩個文件vendor.js和manifest.json,前者包含了庫的代碼,後者則是資源清單。

可以預覽一下生成的vendor.js,它以一個立即執行函數表達式的聲明開始。

var dllExample = (function(params) {

   // ...

})(params);

上面的dllExample正是我們在webpack.vendor.config.js中指定的dllLibraryName。

接着打開manifest.json,其大體內容如下:

{

  "name": "dllExample",

  "content": {

    "./node_modules/fbjs/lib/invariant.js": {

      "id": 0,

      "buildMeta": { "providedExports": true }

    },

    ...

  }

}

manifest.json中有一個name字段,這是我們通過DllPlugin中的name配置項指定的。

 

◆ 鏈接到業務代碼

 

>> 鏈接到業務代碼將vendor鏈接到項目中很簡單,這裏我們將使用與DllPlugin配套的插件DllReferencePlugin,它起到一個索引和鏈接的作用。在工程的webpack配置文件(webpack.config.js)中,通過DllReferencePlugin來獲取剛剛打包好的資源清單,然後在頁面中添加vendor.js的引用就可以了。請看下面的示例:

// webpack.config.js

const path = require('path');

const webpack = require('webpack');

module.exports = {  

// ...  

plugins: [    

new webpack.DllReferencePlugin({   

   manifest: require(path.join(__dirname, 'dll/manifest.json')),

    })  ]};

// index.html

<body>  <!-- ... -->

  <script src="dll/vendor.js"></script>

  <script src="dist/app.js"></script>

</body>當頁面執行到vendor.js時,會聲明dllExample全局變量。而manifest相當於我們注入app.js的資源地圖,app.js會先通過name字段找到名爲dllExample的library,再進一步獲取其內部模塊。這就是我們在webpack.vendor.config.js中給DllPlugin的name和output.library賦相同值的原因。如果頁面報“變量dllExample不存在”的錯誤,那麼有可能就是沒有指定正確的output.library,或者忘記了在業務代碼前加載vendor.js。

 

◆ 潛在問題

 

>> ·page1.js和page2.js的chunk hash均發生了改變。這是我們不希望看到的,因爲它們內容本身並沒有改變,而現在vendor的變化卻使得用戶必須重新下載所有資源。

·page1.js和page.js的chunk hash沒有改變。這種情況大多發生在較老版本的Webpack中,並且比第1種情況更爲糟糕。因爲vendor中的模塊id改變了,而用戶卻由於沒有更新緩存而繼

 

>> 這個問題的根源在於,當我們對vendor進行操作時,本來vendor中不應該受到影響的模塊卻改變了它們的id。解決這個問題的方法很簡單,在打包vendor時添加上HashedModuleIdsPlugin。請看下面的例子:

webpack.vendor.config.js

module.exports = {

  // ...

  plugins: [

    new webpack.DllPlugin({

      name: dllLibraryName,

      path: path.join(dllAssetPath, 'manifest.json'),

    }),

    new webpack.HashedModuleIdsPlugin(),

  ]

};

 

>> 這個插件是在Webpack 3中被引入進來的,主要就是爲了解決數字id的問題。從Webpack 3開始,模塊id不僅可以是數字,也可以是字符串。HashedModuleIdsPlugin可以把id的生成算法改爲根據模塊的引用路徑生成一個字符串hash。比如一個模塊的id是2NuI(hash值),因爲它的引用路徑不會因爲操作vendor中的其他模塊而改變,id將會是統一的,這樣就解決了我們前面提到的問題。

◆ tree shaking

 

>> 第2章我們介紹過,ES6 Module依賴關係的構建是在代碼編譯時而非運行時。基於這項特性Webpack提供了tree shaking功能,它可以在打包過程中幫助我們檢測工程中沒有被引用過的模塊,這部分代碼將永遠無法被執行到,因此也被稱爲“死代碼”。Webpack會對這部分代碼進行標記,並在資源壓縮時將它們從最終的bundle中去掉。

◆  ES6 Module

 

>> ES6 Moduletree shaking只能對ES6 Module生效。有時我們會發現雖然只引用了某個庫中的一個接口,卻把整個庫加載進來了,而bundle的體積並沒有因爲tree shaking而減小。這可能是由於該庫是使用CommonJS的形式導出的,爲了獲得更好的兼容性,目前大部分的npm包還在使用CommonJS的形式。也有一些npm包同時提供了ES6 Module和CommonJS兩種形式導出,我們應該儘可能使用ES6 Module形式的模塊,這樣tree shaking的效率更高。

 

◆ 使用Webpack進行依賴關係構建

 

>>  使用Webpack進行依賴關係構建如果我們在工程中使用了babel-loader,那麼一定要通過配置來禁用它的模塊依賴解析。因爲如果由babel-loader來做依賴解析,Webpack接收到的就都是轉化過的CommonJS形式的模塊,無法進行tree-shaking。禁用babel-loader模塊依賴解析的配置示例如下:

module.exports = {  

// ...  

module: {  

  rules: [{   

   test: /\.js$/,   

   exclude: /node_modules/,

     use: [{   

     loader: 'babel-loader',   

     options: {  

       presets: [     

       // 這裏一定要加上

       [@babel/preset-env,{module:false}]

       ],

       }.

     }],

   }],

},

};

 

◆ webpack-dashboard

 

>> webpack-dashboardWebpack每一次構建結束後都會在控制檯輸出一些打包相關的信息,但是這些信息是以列表的形式展示的,有時會顯得不夠直觀。webpack-dashboard就是用來更好地展示這些信息的。安裝命令如下:npm install webpack-dashboard我們需要把webpack-dashboard作爲插件添加到webpack配置中

◆ webpack-merge

 

>> 通過Object.assign我們沒有辦法準確找到CSS的規則並進行替換,所以必須替換掉整個module的配置

 下面我們看一下如何用webpack-merge來解決這個問題。安裝命令如下:

npm install webpack-merge

更改webpack.prod.js如下:

const merge = require('webpack-merge');

const commonConfig = require('./webpack.common.js');

const ExtractTextPlugin = require('extract-text-webpack-plugin');

, {

  mode: 'production',

  module: {

    rules: [

      {

        test: /\.css$/,

        use: ExtractTextPlugin.extract({

          fallback: 'style-loader',

          use: 'css-loader',

        }),

      }

    ]

  },

});

可以看到,我們用merge.smart替換了Object.assign,這就是webpack-merge“聰明”的地方。它在合併module.rules的過程中會以test屬性作爲標識符,當發現有相同項出現的時候會以後面的規則覆蓋前面的規則,這樣我們就不必添加冗餘代碼了。

 

module.exports = merge.smart(commonConfig

◆  speed-measure-webpack-plugin

 

>> 覺得Webpack構建很慢但又不清楚如何下手優化嗎?那麼可以試試speed-measure-webpack-plugin這個插件(簡稱SMP)。SMP可以分析出Webpack整個打包過程中在各個loader和plugin上耗費的時間,這將會有助於找出構建過程中的性能瓶頸。

 

◆ 開啓HMR

 

>> 如果應用的邏輯比較簡單,我們可以直接手動添加代碼來開啓HMR。比如下面這個例子:// index.js

import { add } from 'util.js';

add(2, 3);

if (module.hot) {

 module.hot.accept();

}

 

◆ HMR API示例

>>index.js及其依賴只要發生改變就在當前環境下全部重新執行一遍。但是我們發現它會帶來一個問題:在當前的運行時我們已經有了一個setInterval,而每次HMR過後又會添加新的setInterval,並沒有對之前的進行清除,所以最後我們會看到屏幕上有不同的數字閃來閃去。從圖9-9中的console信息可以看出setInterval確實執行了多次

 

>> 爲了避免這個問題,我們可以讓HMR不對index.js生效。也就是說,當index.js發生改變時,就直接讓整個頁面刷新,以防止邏輯出現問題,但對於其他模塊來說我們還想讓HMR繼續生效。那麼可以將上面的代碼修改如下:

if (module.hot) {

 module.hot.decline(); //當index.js自身改變時禁止使用HMR進行更新,此時只能刷新整個頁面。

 module.hot.accept(['./util.js']);//當utils.js改變時依然可以啓用HMR更新

}

 

◆ Rollup

 

>> 如果用Webpack與Rollup進行比較的話,那麼Webpack的優勢在於它更全面,基於“一切皆模塊”的思想而衍生出豐富的loader和plugin可以滿足各種使用場景;而Rollup則更像一把手術刀,它更專注於JavaScript的打包。當然Rollup也支持許多其他類型的模塊,但是總體而言在通用性上還是不如Webpack。如果當前的項目需求僅僅是打包JavaScript,比如一個JavaScript庫,那麼Rollup很多時候會是我們的第一選擇

 

◆ 零配置

 

>> parcel build index.htmlParcel會創建一個dist目錄,並在其中生成打包壓縮後的資源,如圖10-3所示。[插圖]圖10-3 Parcel生成的dist目錄從上面可以看出和Webpack的一些不同之處。首先,Parcel是可以用HTML文件作爲項目入口的,從HTML開始再進一步尋找其依賴的資源;並且可以發現對於最後產出的資源,Parcel已經自動爲其生成了hash版本號及source map。另外,如果打開產出的JS文件會發現,內容都是壓縮過的,

 

>> 來說,沒有任何配置是幾乎不可能的,因爲如果完全沒有配置也就失去了定製性。雖然Parcel並沒有屬於自己的配置文件,但本質上它是把配置進行了切分,交給Babel、PostHTML和PostCSS等一些特定的工具進行分別管理。比如當項目中有.babelrc時,那麼Parcel打包時就會採用它作爲ES6代碼解析的配置。

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章