webpack 從入門到工程實踐
本文較長,為了節省你的閱讀時間,在文前列寫作思路如下:
- 什么是webpack,它要解決的是什么問題?
- 對webpack的主要配置項進行分析,雖然不會涉及太多細節,但是期待在本節能讓我們知曉如果我們有什么需求,我們該從哪些配置項著手修改?
- 分析create-react-app的基礎配置文件。
- 分享一些自己工作中對webpack的實踐。
本文的初衷是和你一起理清webpack的使用邏輯,以便能更加容易的編寫及拓展自己項目所需的配置文件。不過也得提前說明本文可能并不是一篇好的可以跟著操作的教程(想跟著一步步做的童鞋可以看 官方示例 和 webpack入門,看這篇就夠了 。
換個角度看待webpack
近年來,前端技術蓬勃發展,我們想在js更方便的實現html , 社區就出現了jsx,我們覺得原生的css不夠好用,社區就提出了scss,less,針對前端項目越來越強的模塊化開發需求,社區出現了AMD,CommonJS,ES2015 import等等方案。遺憾的是,這些方案大多并不直接被瀏覽器支持,往往伴隨這些方案而生的還有另外一些,讓這些新技術應用于瀏覽器的方案,我們用babel來轉換下一代的js,轉換jsx;我們用各種工具轉換scss,less為css;我們發現項目越來越復雜,代碼體積越來越大,又要開始尋找各種優化,壓縮,分割方案。前端工程化這個過程,真是讓我們大費精力。我們也大多是在尋找前端模塊化解決方案的過程中知曉了webpack。
的確,webpack的流行得益于野性生長的前端,其本質是一種前端模塊化打包解決方案,但是更重要的是它又是一個可以融合運用各種前端新技術的平臺,明白webpack的使用哲學后,只需要簡單的配置,我們就可以隨心所欲的在webpack項目中使用jsx/ts,使用babel/postcss等平臺提供的眾多其它功能,只需通過一條命令由源碼構建最終可用文件。可以不夸張的說webpack為前端的工程化開發提供了一套相對容易和完整的解決方案。一些知名的腳手架工具,也大多基于webpack(比如create-react-app)。
webpack好難!我第一次復制別人的配置文件到我的項目中,發現以自己僅有的JS知識完全看不懂時,也有這種感覺。后來發現有這種感覺其實是因為自己看待webpack的角度錯了,對大多數前端開發者而言,以往我們接觸的各種庫,要么類似jQuery,通過$符在前端項目中直接運行,所做的事情只在前端生效,要么類似express.js,在node.js項目中直接require后就可以使用,所做的事情只在后端生效。webpack的不同之處就在于,雖然我們的配置文件位于前端項目中,但實際上它卻運行于node.js,之后的處理結果又供前端使用(也可能供node使用)。所以學習之前,我們轉變一下思維,從node.js的角度來看webpack,很多事情就會簡單起來。
我們對下圖一定不陌生,假設現在我們手中有一系列相互關聯的文件js,jsx,css,less,jpg,我們一步步的看看為了把它們轉換為項目最終需要的,瀏覽器可識別的文件,webpack都做了什么。
對webpack主要配置項的分析
如果不去考究細節,我們大可把webpack簡化理解為一個函數,配置文件則是其參數,傳入合理的參數后,運行函數就能得到我們想要的結果。
webpack也只是一個打包工具,它可不是什么智能ai,我們該從哪兒輸入文件,我們想把輸出結果放哪里,輸出結果應該長什么樣,它都不知道。而我們目前和webpack函數交互的唯一方法就是通過參數,這就涉及到webpack配置對象中兩個重要概念entry和output了,因此,我們的配置對象至少具備以下結構:
// 第一階段 { entry:{}, output:{} }
入口配置entry
理想狀態是,我們把所有自己編寫的文件都交給webpack,讓它找明里面的關系,進過一定處理后,給出最終我們想要的結果。遺憾的是,webpack也不會機械學習,我們手頭的一堆文件之間的關系是自己確定的,一般我們的項目都會存在一個或幾個主文件,其它的所有的文件(模塊)都直接或間接的鏈接到了這些文件。我們在entry項中需要填寫的就是這些主文件的信息。
不過我們也不要嫌棄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以何種方式輸出打包文件,關于output,webpack提供了眾多的可配置選項,我們簡單介紹下最常用的選項。
output基本配置項
我們都另存過文件,當我們另存一個文件時,我們需要確定另存的文件名和另存的路徑,webpack將打包后的結果導出的過程就類似于此,此過程由output配置項控制,其最基本配置包括filename和path兩項。這兩項用以決定上述主js文件的存儲行為。
不過我們程序的首頁往往不需用到某個主js文件的所有代碼,實際開發中,我們常常使用一定方法對代碼進行分割,方便按需加載,提升體驗。這類不具備獨立依賴的文件,我們稱之為chunkfile。chunkfile的命名,在output中對應chunkFilename項;
此外output的publicPath項,用于控制打包文件的相對或者絕對引用路徑,配置不當往往造成在運行時找不到文件。
我們補充配置對象中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 import(webpack2開始內置支持)。
- CommonJS require。
- AMD define和require語句。
- css/less/sass 中的@import。
- 樣式中的url(...)和html文件中的<img src="..."/>。
我們知道webpack只能處理js文件,我們的瀏覽器也可能不支持一些最新的js語法,基于此,我們需要對傳入的模塊進行一定的預處理,這就涉及到webpack的又一核心概念 --- loader,使用loader,webpack允許我們打包任何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"] }] },
這就是webpack中loader的基本用法了,在module.rules數組中進行配置即可,module.rules是一個數組,里面每一項(一個Rule)表示以一定的規則匹配和處理某種或某幾種類型的文件。具體說來:
- Rule.test:表示匹配規則,它是一個正則表達式。
- Rule.use:表示針對匹配的文件將使用的處理loader,其值可以是字符串,數組和對象,當是對象形式時,我們可以使用options等命令進行進一步的配置。
- Rule中的其它一些規則也大多圍繞匹配條件和應用結果展開,如Rule.exclude和Rule.include表示應該匹配或不應該匹配某資源;Rule.oneOf表示對該資源只應用第一個匹配的loader;Rule.enforce則用于指定loader的種類。
loader可以做什么
webpack的強大之處在于,可以輕松在其中應用其它平臺提供的功能,比如說babel,postcss本身都是獨立的平臺。在webpack中只需要添加babel-loader和postcss-loader就可以使用。這兩個平臺本身也提供眾多的配置項,默認分別可在 .babelrc 和 postcss.config.js 中完成,webpack并不影響這些配置文件的使用。不過需要說明的可能很多童鞋是在學習webpack時才接觸這兩個平臺,導致在這兩個平臺上遇到的問題誤以為是webpack的問題。
除了上述的轉換編譯,通過loader,webpack還允許我們實現以下功能:
- 轉換編譯: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的配置
webpack稱plugins為其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的功能主要集中在兩方面:
- 對前一階段打包后的代碼進行處理,如添加替換一些內容,分割代碼為多塊,添加一些全局設置等。
- 輔助輸出,如自動生成帶有鏈接的index.html,對生成文件存儲文件夾做一定的清理等。
對代碼進行處理
抽離不同文件的共享代碼,減少chunk間的重復代碼,有效利用緩存。
抽離可能整個項目都在使用的第三方模塊,比如react react-dom。
將多個子chunk中的共用代碼打包進父chunk或使用異步加載的單獨chunk。
- BannerPlugin:給代碼添加版權信息,如在plugins數組中添加new BannerPlugin(‘GitChat’)后能在打包生成的所有文件前添加注釋GitChat詳見。
- CommonsChunkPlugin,用于抽離代碼,具有多種用途 詳情查看 CommonsChunkPlugin 。
- 抽離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有所不同的說明:
1、npm run reject之前,對create-react-app的一些設置會影響這里看到的配置文件。
2、原始的webpack.config.dev.js中,部分值由外部函數生成,相關值,在上述代碼中直接改為了確定的結果,如env.raw在上述代碼中被替換為:
{ NODE_ENV:'development', PUBLIC_URL:'' }
3、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/。
- pathinfo為true,在打包文件后,在其中所包含引用模塊的信息,這在開發環境中有利于調試。
- 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-native和react-native-web了。
- plugins:此處使用了ModuleScopePlugin的實例,用以限制自己編寫的模塊只能從src目錄中引入。
modules
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進行處理。
- strictExportPresence:這里設置為true,表明文件中如果缺少exports時會直接報錯而不是警告。
- rules:
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
設置node的dgram/fs/let/tls模塊的的值,如果在其它環境中使用時值為empty。
performance
hints: false:不提示測試環境的打包結果。
上文一直討論的是,webpack各設置項的基本意義,目的在于讓你在有相關需求時,能知道該從哪一項下手查詢。不過看到這里,如果你之前從未上手操作過webpack可能依舊不知道該如何使用,下面我分析一下,我在自己的項目中是如何使用的。
一些工程實踐建議
官方文檔的guides部分已經就如何實踐提出了較多的建議,建議閱讀以下內容前先行閱讀。
結合npm使用
webpack在安裝后有多種調用方法。
1、在命令行中直接傳入參數使用(這個實際我用的比較少)。
2、自定義 webpack.config.js文件,在其中完成配置,然后在命令行中執行webpack --config webpack.config.js來使用,配置文件可以是任何其它名稱(如果是webpack.config.js,我們直接使用webpack命令)。
3、結合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.js和webpack.dev.js來分別生成開發環境和生產環境的代碼,在命令行中執行npm run build:prod和npm 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' })
添加此插件,再次構建完成時,瀏覽器中將自動打開一個類似下面這樣的網頁:
這樣我們可以輕易分析我們的代碼分割是否合理,比如:
- 分割后文件過大的主要原因是在于引入了那些模塊。
- 分析大多后的多文件中存不存在對某些比較大的模塊的重復引用,方便我們進一步修正自己的配置文件。
上圖是我之前項目中的一張截圖,第一次見到這張圖時還是給了我很多后期優化的思路的,引用chat.js的同時引入了moment.js,而實際上該頁面只有一張圖表,這讓我考慮另尋圖表解決方案,lodash,velocity在最初的項目中使用過,后逐步去除,屬于遺留代碼,現在還存在說明在局部可能還是用到了,這都是之后編碼的改進方向。
后記
總覺得技術類的文章也是該有生命力的,花了好久寫完本文,回頭看發現有的內容還是沒有表達或交待清楚。所以有任何建議,請隨意提出,我們在Chat中繼續討論,我也將對本文做長期持續的修改。
針對webpack3.5.5官網文檔,使用mindNode制作了一個思維導圖的草稿,此思維導圖還需完善,之后將持續修改,點擊 此處 可查看,該思維導圖示例如下。
另外,關于webpack1和webapck2的區別,官方文檔中有一部分做了詳細的講解,所以本文中不做贅述,看完以后如果還有疑問,之后我們再詳細討論。
來自:gitbook.cn/gitchat/author/5960234d50c43543a4a92621