GitChat · 前端 | webpack 從入門到工程實踐

GitChat 做者:張旺
原文: webpack 從入門到工程實踐
關注微信公衆號:GitChat 技術雜談 ,一本正經的講技術javascript

前言

本文較長,爲了節省你的閱讀時間,在文前列寫做思路以下:css

  1. 什麼是webpack,它要解決的是什麼問題?
  2. webpack的主要配置項進行分析,雖然不會涉及太多細節,可是期待在本節能讓咱們知曉若是咱們有什麼需求,咱們該從哪些配置項着手修改?
  3. 分析create-react-app的基礎配置文件。
  4. 分享一些本身工做中對webpack的實踐。

本文的初衷是和你一塊兒理清webpack的使用邏輯,以便能更加容易的編寫及拓展本身項目所需的配置文件。不過也得提早說明本文可能並非一篇好的能夠跟着操做的教程(想跟着一步步作的童鞋能夠看官方示例webpack入門,看這篇就夠了html

換個角度看待webpack

近年來,前端技術蓬勃發展,咱們想在js更方便的實現html , 社區就出現了jsx,咱們以爲原生的css不夠好用,社區就提出了scss,less,針對前端項目愈來愈強的模塊化開發需求,社區出現了AMD,CommonJS,ES2015 import等等方案。遺憾的是,這些方案大多並不直接被瀏覽器支持,每每伴隨這些方案而生的還有另一些,讓這些新技術應用於瀏覽器的方案,咱們用babel來轉換下一代的js,轉換jsx;咱們用各類工具轉換scss,lesscss;咱們發現項目愈來愈複雜,代碼體積愈來愈大,又要開始尋找各類優化,壓縮,分割方案。前端工程化這個過程,真是讓咱們大費精力。咱們也大可能是在尋找前端模塊化解決方案的過程當中知曉了webpack前端

的確,webpack的流行得益於野性生長的前端,其本質是一種前端模塊化打包解決方案,可是更重要的是它又是一個能夠融合運用各類前端新技術的平臺,明白webpack的使用哲學後,只須要簡單的配置,咱們就能夠爲所欲爲的在webpack項目中使用jsx/ts,使用babel/postcss等平臺提供的衆多其它功能,只需經過一條命令由源碼構建最終可用文件。能夠不誇張的說webpack爲前端的工程化開發提供了一套相對容易和完整的解決方案。一些知名的腳手架工具,也大多基於webpack(好比create-react-app)。java

webpack好難!我第一次複製別人的配置文件到個人項目中,發現以本身僅有的JS知識徹底看不懂時,也有這種感受。後來發現有這種感受實際上是由於本身看待webpack的角度錯了,對大多數前端開發者而言,以往咱們接觸的各類庫,要麼相似jQuery,經過$符在前端項目中直接運行,所作的事情只在前端生效,要麼相似express.js,在node.js項目中直接require後就可使用,所作的事情只在後端生效。webpack的不一樣之處就在於,雖然咱們的配置文件位於前端項目中,但實際上它卻運行於node.js,以後的處理結果又供前端使用(也可能供node使用)。因此學習以前,咱們轉變一下思惟,從node.js的角度來看webpack,不少事情就會簡單起來。node

咱們對下圖必定不陌生,假設如今咱們手中有一系列相互關聯的文件js,jsx,css,less,jpg,咱們一步步的看看爲了把它們轉換爲項目最終須要的,瀏覽器可識別的文件,webpack都作了什麼。react

webpack作了什麼

對webpack主要配置項的分析

若是不去考究細節,咱們大可把webpack簡化理解爲一個函數,配置文件則是其參數,傳入合理的參數後,運行函數就能獲得咱們想要的結果。jquery

webpack也只是一個打包工具,它可不是什麼智能ai,咱們該從哪兒輸入文件,咱們想把輸出結果放哪裏,輸出結果應該長什麼樣,它都不知道。而咱們目前和webpack函數交互的惟一方法就是經過參數,這就涉及到webpack配置對象中兩個重要概念entryoutput了,所以,咱們的配置對象至少具有如下結構:webpack

// 第一階段
{
    entry:{},
    output:{}
}

入口配置entry

理想狀態是,咱們把全部本身編寫的文件都交給webpack,讓它找明裏面的關係,進過必定處理後,給出最終咱們想要的結果。遺憾的是,webpack也不會機械學習,咱們手頭的一堆文件之間的關係是本身肯定的,通常咱們的項目都會存在一個或幾個主文件,其它的全部的文件(模塊)都直接或間接的連接到了這些文件。咱們在entry項中須要填寫的就是這些主文件的信息。git

不過咱們也不要嫌棄webpack笨,經過咱們給的主文件路徑,經過分析它能構建最合適的依賴關係,這意味着只有用過的代碼纔會被打包,好比咱們在一個文件中寫了五個模塊,可是實際只用了其中一個,打包後的代碼只會包含引用過的模塊。

webpack中不少地方的配置都有多種寫法,這也是其讓人疑惑的地方之一,很遺憾,咱們的第一個配置對象entry就是如此。

entry能夠是三種值:

  1. 字符串:如entry:'./src/index.js',字符串也能夠是函數的返回值,如entry: () => './demo',單一入口占位符[name]值爲main(關於佔位符,稍後詳述);
  2. 數組形式,如[react,react-dom],能夠把數組中的多個文件打包轉換爲一個chunk
  3. 對象形式,若是咱們須要配置的是多頁應用,或者咱們要抽離出指定的模塊作爲公共代碼,就須要採用這種形式了,屬性名是佔位符[name]的值,屬性值能夠是上面的字符串和數組,以下:
// 值得注意的是入口文件有幾個就會生成幾個獨立的依賴圖譜。
entry:{
    main:'./src/index.js',
    second:'./src/index2.js',
    vendor: ['react','react-dom']
}

好吧,千辛萬苦,咱們在一堆各類類型的文件中找到了入口文件,這裏咱們假設爲./src/index.js,此時咱們的配置對象以下:

// 第二階段
{
    entry:{
        main:'./src/index.js'
    },
    output:{}
}

webpack依據入口文件來構建依賴體系,每一個入口文件在打包完成後都具有其獨立的依賴圖譜,在此咱們暫時稱這些由主入口配置生成的文件爲主js文件。

輸出配置output

output配置項做用於打包文件的輸出階段,其做用在於告知webpack以何種方式輸出打包文件,關於outputwebpack提供了衆多的可配置選項,咱們簡單介紹下最經常使用的選項。

output基本配置項

咱們都另存過文件,當咱們另存一個文件時,咱們須要肯定另存的文件名和另存的路徑,webpack將打包後的結果導出的過程就相似於此,此過程由output配置項控制,其最基本配置包括filenamepath兩項。這兩項用以決定上述主js文件的存儲行爲。

不過咱們程序的首頁每每不需用到某個主js文件的全部代碼,實際開發中,咱們經常使用必定方法對代碼進行分割,方便按需加載,提高體驗。這類不具有獨立依賴的文件,咱們稱之爲chunkfilechunkfile的命名,在output中對應chunkFilename項;

此外outputpublicPath項,用於控制打包文件的相對或者絕對引用路徑,配置不當每每形成在運行時找不到文件。

咱們補充配置對象中output的配置,以下:

// 第三階段
{
    entry:{
        main:'./src/index.js'
    },
    output:{
        path: path.join(__dirname,'./dist'),
        name:'js/bundle-[name]-[hash].js',
        chunkFilename:'js/[name].chunk.js',
        publicPath:'/dist/'
    }
}

上述代碼中用到了佔位符[name],咱們對佔位符作統一解釋:

webpack中常見的佔位符有多種,常見的以下:

  • [name]:表明打包後文件的名稱,在entry或代碼中(以後會看到)肯定;
  • [id]:webpack給塊分配的內部chunk id,若是你沒有隱藏,你能在打包後的命令行中看到;
  • [hash]:每次構建過程當中,生成的惟一 hash 值;
  • [chunkhash]: 依據於打包生成文件內容的 hash 值,內容不變,值不變;
  • [ext]: 資源擴展名,如js,jsx,png等等;

output其它配置

output配置項生效於保存這個過程,除了上面的基本配置,若是你想對這個階段的打包文件進行更改,均可在此配置項中進行相關設置。

好比output提供了衆多關於hash的屬性,讓咱們對[hash]佔位符的值有更加精細的控制,如生成方式,使用的算法,預設的長度等等;如chunkLoadTimeout屬性則容許咱們設置chunk文件的請求超時時間。

工具都是依賴於需求來使用的,若是你此階段有別的需求,可點擊更多配置尋找解決方案。

咱們已經知道了webpack中基本的輸入和輸出配置,可是webpack對各模塊的處理過程,目前爲止,對咱們仍是一個謎。考慮到webpack執行於node.js環境,其自己只能理解js文件,而咱們輸入的倒是一大堆不一樣格式的文件,毫無疑問,要作的第一件事情是對各種模塊進行處理,這就涉及到webpack中第三個重要配置對象了---module

對模塊的處理:module的配置

使用webpack時,咱們經常據說,對webpack而言,全部的文件都是模塊,前文中我也經常混用模塊和文件,不過本質上模塊和文件仍是不一樣的,webpack裏,文件能夠當作模塊,而模塊卻不必定是一個獨立的文件。咱們先看看webpack內置支持的模塊類型:

  • ES2015 importwebpack2開始內置支持)。
  • CommonJS require
  • AMD definerequire語句。
  • css/less/sass 中的@import
  • 樣式中的url(...)和html文件中的<img src="..."/>

咱們知道webpack只能處理js文件,咱們的瀏覽器也可能不支持一些最新的js語法,基於此,咱們須要對傳入的模塊進行必定的預處理,這就涉及到webpack的又一核心概念 --- loader,使用loaderwebpack容許咱們打包任何JS以外的靜態資源。

loader的做用和基本用法

webpack中,loader的配置主要在module.rules中進行,module.rules是一個數組,咱們能夠把每一項看作一個Rule,每一個Rule主要作了如下兩件事:

  • 識別文件類型,以肯定具體處理該數據的loader,(Rule.test屬性)。
  • 使用相關loader對文件進行相應的操做轉換,(Rule.use屬性)。

還記得前面咱們說過,咱們手頭的文件類型有js,jsx,css,less,jpg嗎?咱們看看在webpack中該如何處理和轉換它們。

注:如下loader使用前需經過npm/cnpm/yarn安裝:

module: {
   rules: [{
       test: /(\.jsx|\.js)$/,
       use: {
           loader: "babel-loader",
           options: {
               presets: ["es2015", "react"]
           }
       },
       exclude: /node_modules/
   }, {
       test: /\.css$/,
       use: ["style-loader", "css-loader"]
   }, {
       test: /\.less$/,
       use: ["style-loader", "css-loader", "less-loader"]
   }]
},

這就是webpackloader的基本用法了,在module.rules數組中進行配置便可,module.rules 是一個數組,裏面每一項(一個Rule)表示以必定的規則匹配和處理某種或某幾種類型的文件。具體說來:

  • Rule.test:表示匹配規則,它是一個正則表達式。
  • Rule.use:表示針對匹配的文件將使用的處理loader,其值能夠是字符串,數組和對象,當是對象形式時,咱們可使用options等命令進行進一步的配置。
  • Rule中的其它一些規則也大多圍繞匹配條件和應用結果展開,如Rule.excludeRule.include表示應該匹配或不該該匹配某資源;Rule.oneOf表示對該資源只應用第一個匹配的loaderRule.enforce則用於指定loader的種類。

loader能夠作什麼

webpack的強大之處在於,能夠輕鬆在其中應用其它平臺提供的功能,好比說babel,postcss自己都是獨立的平臺。在webpack中只須要添加babel-loaderpostcss-loader就可使用。這兩個平臺自己也提供衆多的配置項,默認分別可在.babelrcpostcss.config.js中完成,webpack並不影響這些配置文件的使用。不過須要說明的可能不少童鞋是在學習webpack時才接觸這兩個平臺,致使在這兩個平臺上遇到的問題誤覺得是webpack的問題。

除了上述的轉換編譯,經過loaderwebpack還容許咱們實現如下功能:

  • 轉換編譯:script-loader/babel-loader/ts-loader/coffee-loader等。
  • 處理樣式:style-loader/css-loader/less-loader/sass-loader/postcss-loader等。
  • 處理文件:raw-loader/url-loader/file-loader/等。
  • 處理數據:csv-loader/xml-loader等。
  • 處理模板語言:html-loader/pug-loader/jade-loader/markdown-loader等。
  • 清理和測試:mocha-loader/eslint-loader等。

關於各個loader更詳細的介紹,可點擊loaders查看。

module.noParse

關於module,另外一個經常使用的配置項爲module.noParse,經過它,咱們在構建過程當中能夠忽略大型的 library 以提升構建效率。

咱們來整理一下此階段,咱們的配置對象代碼,以下:

// 第四階段
{
    entry: {
        main: './src/index.js'
    },
    output: {
        path: path.join(__dirname, './dist'),
        name: 'js/bundle-[name].js',
        chunkFilename: 'js/[name].chunk.js',
        publicPath: '/dist/'
    },
    module: {
        rules: [{
            test: /(\.jsx|\.js)$/,
            use: {
                loader: "babel-loader",
                options: {
                    presets: ["es2015", "react"]
                }
            },
            exclude: /node_modules/
        }, {
            test: /\.css$/,
            use: ["style-loader", "css-loader"]
        }, {
            test: /\.less$/,
            use: ["style-loader", "css-loader", "less-loader"]
        }]
    }
}

進過這一階段的處理,咱們的代碼其實已經能夠輸出使用了。不過這樣的輸出可能還不能讓人滿意,咱們想要抽離公共代碼;咱們想統一修改全部代碼中的某些值;咱們還想對代碼進行壓縮,去除全部的console… , 總之這一階段的代碼仍是存在很大的改進空間的,這就是plugin的用武之地了。

plugins的配置

webpackplugins爲其backbone,一切loader不能作的處理均可由plugins來作。此評價足見其重要性。

鑑於插件如此重要,webpack內置了衆多的經常使用的plugins,無需額外安裝就可直接使用。咱們先看看plugins的基本配置方法,而後再分類介紹一下經常使用的plugins

plugins的使用方法

plugins是一個數組,數組中的每一項都是某一個plugin的實例,plugins數組甚至能夠存在一個插件的多個實例。

下面代碼中,分別展現了webpack內置插件和第三方插件的使用方法:

// 第三方插件須要在安裝後引入
const CleanWebpackPlugin = require("clean-webpack-plugin");

{
    ...
    plugins:[
      new webpack.DefinePlugin({
        "process.env": {
            NODE_ENV: JSON.stringify("production")
        }
    }),
      new CleanWebpackPlugin(["js"], {
        root: __dirname + "/stu/",
        verbose: true,
        dry: false
    })
    ]
}

一種插件其實就是一種函數,經過傳入不一樣的參數,插件可按咱們的需求實現不一樣的功能。不過插件數量衆多,咱們甚至還能夠本身來寫插件,每一個插件還有本身特定的配置規則,這也是webpack讓人以爲難學的地方之一,不過好在做爲一個工具,對於咱們大多數人最須要掌握的plugins並非那麼多,其它的待真的有相關需求再邊查邊學也不遲,webpack的插件列表可參看這裏

經常使用plugins的介紹

plugins功能衆多,可是大多數plugin的功能主要集中在兩方面:

  1. 對前一階段打包後的代碼進行處理,如添加替換一些內容,分割代碼爲多塊,添加一些全局設置等。
  2. 輔助輸出,如自動生成帶有連接的index.html,對生成文件存儲文件夾作必定的清理等。

對代碼進行處理

  • BannerPlugin:給代碼添加版權信息,如在plugins數組中添加new BannerPlugin(‘GitChat’)後能在打包生成的全部文件前添加註釋GitChat詳見
  • CommonsChunkPlugin,用於抽離代碼,具備多種用途 詳情查看CommonsChunkPlugin

    • 抽離不一樣文件的共享代碼,減小chunk間的重複代碼,有效利用緩存。
    • 抽離可能整個項目都在使用的第三方模塊,好比react react-dom
    • 將多個子chunk中的共用代碼打包進父chunk或使用異步加載的單獨chunk。
    • 抽離Manifest這類每次打包都會變化的內容,減輕打包時候的壓力,提高構建速度。
  • CompressionWebpackPlugin:使用配置的算法(如gzip)壓縮打包生成的文件,詳見
  • DefinePlugin:建立一個在編譯時可配置的全局常量,若是你自定義了一個全局變量PRODUCTION,可在此設置其值來區分開發仍是生產環境詳見
  • EnvironmentPlugin:其實是DefinePlugin插件中對process.env進行設置的簡寫形式,如new webpack.EnvironmentPlugin(['NODE_ENV', 'DEBUG'])將設置process.env.NODE_ENV='DEBUG',EnvironmentPlugin
  • ExtractTextWebpackPlugin:抽離css文件爲單獨的css文件,詳見
  • ProvidePlugin:全局自動加載模塊,如添加new webpack.ProvidePlugin({$: 'jquery', jQuery: 'jquery'})後,則全局不用在導入jquery就能夠直接使用$ProvidePlugin
  • UglifyjsWebpackPlugin:使用前須要先安裝,基於UglifyJS壓縮代碼,支持其全部配置UglifyjsWebpackPlugin

輔助輸出打包後的代碼

  • HtmlWebpackPlugin:使用前須要先安裝,爲你自動生成一個html文件,該文件將自動依據entry的配置引入依賴,若是你的文件名中添加了[hash]等佔位符,這將很是有用, 詳見
  • CleanWebpackPlugin:使用前須要先安裝,此插件容許你在配置之後,每次打包時,清空所配置的文件夾,若是你每次打包的文件名不一樣,這將很是有用 GitHub - clean-webpack-plugin

經過上述對不一樣插件的描述,你必定大體明白了,插件能夠作什麼,以後在開發的過程當中,若是你遇到的什麼須要在此階段解決的問題,大可搜索看看是否有相關的插件,推薦查閱awesome-webpack

學習了插件之後,如今咱們的配置對象是以下這樣:

// 第5階段
{
    entry: {
        main: './src/index.js'
    },
    output: {
        path: path.join(__dirname, './dist'),
        name: 'js/bundle-[name].js',
        chunkFilename: 'js/[name].chunk.js',
        publicPath: '/dist/'
    },
    module: {
        rules: [{
            test: /(\.jsx|\.js)$/,
            use: {
                loader: "babel-loader",
                options: {
                    presets: ["es2015", "react"]
                }
            },
            exclude: /node_modules/
        }, {
            test: /\.css$/,
            use: ["style-loader", "css-loader"]
        }, {
            test: /\.less$/,
            use: ["style-loader", "css-loader", "less-loader"]
        }]
    },
    plugins: [
        new webpack
        .optimize
        .CommonsChunkPlugin({
            name: 'vendor',
            filename: "js/[name]-[chunkhash].js"
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: "manifest",
            minChunks: Infinity
        }),
        new webpack.ProvidePlugin({
            Promise: "exports-loader?global.Promise!es6-promise",
            fetch: "exports-loader?self.fetch!whatwg-fetch"
        }),
        new HtmlWebpackPlugin({
            filename: "index.html",
            template: "app/index.html",
            inject: "body"
        }),
        new CleanWebpackPlugin(["js"], {
            root: __dirname + "/stu/",
            verbose: true,
            dry: false
        }),
        new webpack.DefinePlugin({
            "process.env": {
                NODE_ENV: JSON.stringify("production")
            }
        })
    ]
}

至此,從輸入entry->處理loaders/plugins->輸出output,咱們講解了webpack的核心功能,不過webpack還提供其它的一些配置項,這些配置項大多從兩方面起做用,輔助開發、對構建過程當中的一些細節作調整。對這些屬性,下面只作簡單的介紹。

其它的一些配置

輔助開發的相關屬性

  • devtool:

打包後的代碼和原始的代碼每每存在較大的差別,此選項控制是否生成,以及如何生成 source map,用以幫助你進行調試,詳情可查看Devtool

  • devServer

經過配置devServer選項,你能夠開啓一個本地服務器,webpack爲此本地服務器提供了很是多的配置選項,點擊查看dev-server,你會發現經過合適的配置,你能夠擁有全部本地服務器可提供的功能。

  • watch:

啓用 Watch 模式後,webpack 將持續監放任何已解析文件的更改,從新構建文件,Watch 模式默認關閉,在開發時候若是開啓會很方便。

  • watchOptions:

一組用來定製 Watch 模式的選項: 詳見 watch

  • performance:

本配置讓你設置打包後命令行中該如何展現性能提示,好比是否開啓提示,資源若是超過某個大小時該警告仍是報錯,詳見performance

  • stats:

本選項讓你配置打包過程當中輸出的內容,如沒有輸出none,標準輸出normal,所有輸出verbose,只輸出錯誤errors-only等等。

精細配置相關屬性

  • content:設置基礎路徑,默認使用當前目錄。
  • resolve:

肯定模塊如何被解析,webpack已經提供了合理的默認值,不過經過你的自定義配置,能夠對模塊解析實現更加精細的控制,如對某些經常使用模塊能夠經過設置別名以更容易引用,也可在此處設置可被忽略的後綴名,詳見 resolve

  • target:

告知 webpack 須要打包的代碼執行的環境,針對 node 和 web 打包過程會有所不一樣,詳見Target

  • externals:

讓打包生成的代碼中不添加某依賴項,而讓這些依賴項直接從用戶環境中獲取,在進行庫的開發時很是有用。

  • node:

是一個對象,其中每一個屬性都是 Node.js 全局變量或模塊的名稱,每一項的設置值均可以是(true/mock/empty/false)中的一種,以肯定這些node中的對象在其它環境中是否可用。

  • 此外webpack還具有其它一些用的比較少的配置對象,詳見 Other Options

至此,咱們瞭解了webpack經常使用的配置項及其意義。爲了檢測咱們的學習成果,咱們一塊兒分析一箇中等項目中的webpack配置文件。配置文件來自於create-react-app,使用create-react-app新建項目後,執行npm run eject可看到多個配置文件,這裏咱們選擇webpack.dev.js

分析create-react-app中webpack的配置

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
const eslintFormatter = require('react-dev-utils/eslintFormatter');
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');

module.exports = {
  devtool: 'cheap-module-source-map',
  entry: [
    require.resolve('react-dev-utils/webpackHotDevClient'),
    require.resolve('./polyfills'),
    require.resolve('react-error-overlay'),
    'src/index.js'
  ],
  output: {
    path: '/build/',
    pathinfo: true,
    filename: 'static/js/bundle.js',
    chunkFilename: 'static/js/[name].chunk.js',
    publicPath: '',
    devtoolModuleFilenameTemplate: info =>
      path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'),
  },
  resolve: {
    modules: ['node_modules'],
    extensions: ['.web.js', '.js', '.json', '.web.jsx', '.jsx'],
    alias: {
      'react-native': 'react-native-web',
    },
    plugins: [
      new ModuleScopePlugin('/src'),
    ],
  },
  module: {
    strictExportPresence: true,
    rules: [{
      test: /\.(js|jsx)$/,
      enforce: 'pre',
      use: [{
        options: {
          formatter: eslintFormatter,
        },
        loader: require.resolve('eslint-loader'),
      }, ],
      include: 'src',
    }, {
      exclude: [/\.html$/,/\.(js|jsx)$/,/\.css$/,/\.json$/,/\.bmp$/,/\.gif$/,/\.jpe?g$/,/\.png$/],
      loader: require.resolve('file-loader'),
      options: {
        name: 'static/media/[name].[hash:8].[ext]',
      },
    }, {
      test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
      loader: require.resolve('url-loader'),
      options: {
        limit: 10000,
        name: 'static/media/[name].[hash:8].[ext]',
      },
    }, {
      test: /\.(js|jsx)$/,
      include: 'src',
      loader: require.resolve('babel-loader'),
      options: {
        cacheDirectory: true,
      },
    }, {
      test: /\.css$/,
      use: [
        require.resolve('style-loader'), {
          loader: require.resolve('css-loader'),
          options: {
            importLoaders: 1,
          },
        }, {
          loader: require.resolve('postcss-loader'),
          options: {
                    ...
          },
        },
      ],
    }, ],
  },
  plugins: [
    new InterpolateHtmlPlugin({
      NODE_ENV:'development',
      PUBLIC_URL:''
    }),
    new HtmlWebpackPlugin({
      inject: true,
      template: 'public/index.html',
    }),
    new webpack.NamedModulesPlugin(),
    new webpack.DefinePlugin({
      'process.env':{
        NODE_ENV:"development",
        PUBLIC_URL:'" "'
      }
    }),
    new webpack.HotModuleReplacementPlugin(),
    new CaseSensitivePathsPlugin(),
    new WatchMissingNodeModulesPlugin(paths.appNodeModules),
    new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
  ],
  node: {
    dgram: 'empty',
    fs: 'empty',
    net: 'empty',
    tls: 'empty',
  },
  performance: {
    hints: false,
  },
};

對可能和你看到的webpack.config.dev.js有所不一樣的說明:

  • npm run reject以前,對create-react-app的一些設置會影響這裏看到的配置文件。
  • 原始的webpack.config.dev.js中,部分值由外部函數生成,相關值,在上述代碼中直接改成了肯定的結果,如env.raw在上述代碼中被替換爲:
{
      NODE_ENV:'development',
      PUBLIC_URL:''
    }
  • create-react-app在開發環境並不生成真實的文件到硬盤,上述代碼中的部分路徑可能有誤,見諒。

推薦在看下面的分析前,花三分鐘看看上述文件,若是都能看得懂,那麼恭喜你,你已經明白webpack的運做方式了,快去本身的項目中實踐吧,若是還有疑惑,也沒關係,咱們一塊兒來分析。

webpack.config.dev.js執行於node環境

首先,咱們應該明確webpack.config.dev.js執行於node環境,目的在於返回webpack須要的配置對象,所以其中可使用node提供的一些特殊變量和語法,好比__dirname,又如引入模塊時採用CommonJS模式。

此文件的開頭,首先經過require語句引入了path,webpack和一系列webpack插件,除了HtmlWebpackPlugin在前文中咱們見過,其它的咱們都不曾見過,其實這些大可能是create-react-app針對webpack已有的插件改進或新開發的插件,因此不熟悉也正常,隨後咱們將一個個的弄清楚它們是幹嗎的。

對module.exports的分析

devtool

此處的配置值爲cheap-module-source-map,表明不帶列映射的 SourceMap,將加載的 Source Map 簡化爲每行單獨映射。

entry

此處的entry是一個數組,表明着四項的代碼都會添加到打包結果之中。

  • webpackHotDevClient能夠被看作具備更好體驗的WebpackDevServer
  • ./ployfill.js用以在瀏覽器中支持promise/fetch/object-assign
  • react-error-overlay在開發環境中使用,強制顯示錯誤頁面。
  • ./src/index.js則是咱們的app的主入口。

output

在實際使用create-react-app的過程當中,咱們並看不見開發環境的打包結果,所以此處的說明僅供參考。

  • path指定,打包後文件存放的位置爲/build/
  • pathinfotrue,在打包文件後,在其中所包含引用模塊的信息,這在開發環境中有利於調試。
  • filename指定了打包的名字和基本的引用路徑static/js/bundle.js
  • chunkFilename:指定了非入口文件的名稱static/js/[name].chunk.js
  • publicPath:指定服務器讀取時的路徑,此處設置爲
  • devtoolModuleFilenameTemplate:這裏是一個函數,指定了map位於磁盤的位置。

resolve

  • modules:指定了模塊的搜索的位置,這裏設置爲node_modules
  • extensions:指明在引用模塊時哪些後綴名能夠忽略,這裏忽略的文件名包括.js/.jsx/.web.js/.web.jsx等
  • alias:建立 import 或 require 的別名,使得部分模塊的引用變得簡單,安裝上文的設置,如今咱們能夠直接引用react-nativereact-native-web了。
  • plugins:此處使用了ModuleScopePlugin的實例,用以限制本身編寫的模塊只能從src目錄中引入。

modules

  • strictExportPresence:這裏設置爲true,代表文件中若是缺乏exports時會直接報錯而不是警告。
  • rules

    • Rule1:對js/jsx文件前置使用eslintFormatter,設置formatter格式爲eslintFormatter
    • Rule2:對exclude中的衆多文件類型不使用file-loader,並設置其它文件打包後的名稱按'static/media/[name].[hash:8].[ext]'格式設置。
    • Rule3: 對js/jsx文件調用babel-loader處理轉換。
    • Rule4: 對css文件,按順序調用style-loader,css-loader,postcss-loader進行處理。

plugins

這裏的一些插件,有的可能咱們還比較陌生,咱們一一介紹。

  • InterpolateHtmlPlugin:和HtmlWebpackPlugin串行使用,容許在index.html中添加變量。
  • HtmlWebpackPlugin:自動生成帶有入口文件引用的index.html
  • NamedModulesPlugin:當開啓 HMR 的時候使用該插件會顯示模塊的相對路徑,建議用於開發環境。
  • DefinePlugin:這裏咱們設置了process.env.NODE_ENV的值爲development
  • HotModuleReplacementPlugin:啓用模塊熱替換。
  • CaseSensitivePathsPlugin:若是路徑有誤則直接報錯。
  • WatchMissingNodeModulesPlugin:此插件容許你安裝庫後自動從新構建打包文件。
  • new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/):忽略所匹配的moment.js

node

設置nodedgram/fs/let/tls模塊的的值,若是在其它環境中使用時值爲empty

performance

hints: false:不提示測試環境的打包結果。

上文一直討論的是,webpack各設置項的基本意義,目的在於讓你在有相關需求時,能知道該從哪一項下手查詢。不過看到這裏,若是你以前從未上手操做過webpack可能依舊不知道該如何使用,下面我分析一下,我在本身的項目中是如何使用的。

一些工程實踐建議

官方文檔的guides部分已經就如何實踐提出了較多的建議,建議閱讀如下內容前先行閱讀。

結合npm使用

webpack在安裝後有多種調用方法。

  • 在命令行中直接傳入參數使用(這個實際我用的比較少)。
  • 自定義 webpack.config.js文件,在其中完成配置,而後在命令行中執行webpack --config webpack.config.js來使用,配置文件能夠是任何其它名稱(若是是webpack.config.js,咱們直接使用webpack命令)。
  • 結合npm使用,在package.json文件中的scripts對象中添加相關命令使用,以後經過npm run使用,以下:
"scripts": {
    "build:prod": "webpack --progress --colors --watch --config webpack.prod.js",
    "build:dev": "webpack --progress --colors --watch --config webpack.dev.js"
}

上面咱們分別構建了webpack.prod.jswebpack.dev.js來分別生成開發環境和生產環境的代碼,在命令行中執行npm run build:prodnpm run build:dev便可生成對應代碼。

爲生產環境指定合理的緩存

關於緩存,官方文檔中有一節講解的很是詳細,請參見緩存

合理分割代碼

webpack提供了三種分割代碼的方法,分別是經過entry,經過CommonsChunkPlugin插件和經過動態import(在webpack1.x中時也經常使用require.ensure來依據路由分割代碼)。

entry的配置經常使用於多頁應用,CommonsChunkPlugin的使用前文已作簡要敘述,下面簡單敘述下代碼分割原則及我實際工做中是如何使用動態import來分割代碼的。

分割原則

目前工做中主要依據兩個原則來分隔代碼:

  • 前端路由:依據路由對應的頁面進行分割,這種分割以後的體驗相似於小程序中每次打開新頁加載對應頁面的js文件。
  • 針對邏輯交互比較複雜的頁面,若是某個較複雜的組件需被某操做觸發後才呈現,也會把該組件分割出來。

分割方法

咱們知道動態import返回值實際上是一個Promise,基於此,對應於我用的React,我常採用如下函數輔助加載。

// lib.js 定義懶加載函數
module.exports.withLazyLoading = function withLazyLoading(getComponent,Spinner = null) {
    return class LazyLoadingWrapper extends React.Component {
        constructor(props) {
            super(props);
            this.state = ({
                Component: null,
            })
        }

        componentWillMount() {
            const {onLoadingStart, onLoadingEnd, onError} = this.props;
              onLoadingStart();
            getComponent()
                .then(esModule => {
                    this.setState({Component: esModule.default})
                })
                .catch(err => {
                    onError(err, this.props)
                })
        }

        render() {
            const {Component} = this.state;
            if (!Component) return Spinner;
            return <Component {...this.props} />
        }
    }
};

對代碼的分割方法以下:

// 在須要的地方調用懶加載函數
import {withLazyLoading} from "lib";
// 
import {Loading} from 'Loadings';

export default withLazyLoading(
    () => {
        return import (/* webpackChunkName: "ConCard" */ "../../containers/ConCard.js")
    }, Loading());

簡要的說明一下上述代碼的意義,懶加載函數withLazyLoading接受動態import的組件和一個加載動畫做爲參數,動態import的組件加載成功前顯示加載動畫組件,成功後顯示import的組件,經過自定義各類各樣的Spinner加載動畫,咱們能夠實現優雅的js文件加載過程。

觀察打包後文件的結構,合理進行優化

使用webpack --json > stats.json命令能夠生成一個包含依賴關係的json文件。webpack提供了多種可視化工具幫咱們分析這個文件,我最喜歡的工具插件是BundleAnalyzerPlugin,可經過下述方法引入該插件:

new BundleAnalyzerPlugin({
    analyzerMode: 'static'
})

添加此插件,再次構建完成時,瀏覽器中將自動打開一個相似下面這樣的網頁:

BundleAnalyzerPlugin

這樣咱們能夠輕易分析咱們的代碼分割是否合理,好比:

  • 分割後文件過大的主要緣由是在於引入了那些模塊。
  • 分析大多後的多文件中存不存在對某些比較大的模塊的重複引用,方便咱們進一步修正本身的配置文件。

上圖是我以前項目中的一張截圖,第一次見到這張圖時仍是給了我不少後期優化的思路的,引用chat.js的同時引入了moment.js,而實際上該頁面只有一張圖表,這讓我考慮另尋圖表解決方案,lodash,velocity在最初的項目中使用過,後逐步去除,屬於遺留代碼,如今還存在說明在局部可能仍是用到了,這都是以後編碼的改進方向。

後記

總以爲技術類的文章也是該有生命力的,花了很久寫完本文,回頭看發現有的內容仍是沒有表達或交待清楚。因此有任何建議,請隨意提出,咱們在Chat中繼續討論,我也將對本文作長期持續的修改。

針對webpack3.5.5官網文檔,使用mindNode製做了一個思惟導圖的草稿,此思惟導圖還需完善,以後將持續修改,點擊此處可查看,該思惟導圖示例以下。

思惟導圖局部示例

另外,關於webpack1webapck2的區別,官方文檔中有一部分作了詳細的講解,因此本文中不作贅述,看完之後若是還有疑問,以後咱們再詳細討論。

彩蛋

這裏寫圖片描述

相關文章
相關標籤/搜索