公司的前端項目基本都是用Webpack來作工程化的,而Webpack雖然只是一個工具,但內部涉及到很是多的知識,以前一直靠CV來解決問題,之知其然不知其因此然,但願此次能整理一下相關的知識點。css
這是webpack官方的首頁圖前端
本質上,webpack 是一個現代 JavaScript 應用程序的靜態模塊打包器(module bundler)。當 webpack 處理應用程序時,它會遞歸地構建一個依賴關係圖(dependency graph),其中包含應用程序須要的每一個模塊,而後將全部這些模塊打包成一個或多個 bundle。vue
那麼打個比方就是咱們搭建一個項目比如搭建一個房子,咱們把所須要的材料(js文件、圖片等)交給webpack,最後webpack會幫咱們作好一切,並把房子(即bundle)輸出。node
webpack中有幾個概念須要記住react
入口起點(entry point)便是webpack經過該起點找到本次項目所直接或間接依賴的資源(模塊、庫等),並對其進行處理,最後輸出到bundle中。入口文件由用戶自定義,能夠是一個或者多個,每個entry最後對應一個bundle。webpack
經過配置output屬性能夠告訴webpack將bundle命名並輸出到對應的位置。git
webpack核心,webpack自己只能識別js文件,對於非js文件,即須要loader轉換爲js文件。換句話說,,Loader就是資源轉換器。因爲在webpack裏,全部的資源都是模塊,不一樣資源都最終轉化成js去處理。針對不一樣形式的資源採用不一樣的Loader去編譯,這就是Loader的意義。es6
webpack核心,loader處理非js文件,那麼插件能夠有更普遍的用途。整個webpack其實就是各種的插件造成的,插件的範圍包括,從打包優化和壓縮,一直到從新定義環境中的變量。插件接口功能極其強大,能夠用來處理各類各樣的任務。github
被entry所依賴的額外的代碼塊,一樣能夠包含一個或者多個文件。chunk也就是一個個的js文件,在異步加載中用處很大。chunk實際上就是webpack打包後的產物,若是你不想最後生成一個包含全部的bundle,那麼能夠生成一個個chunk,並經過按需加載引入。同時它還能經過插件提取公共依賴生成公共chunk,避免多個bundle中有多個相同的依賴代碼。web
webpack的相關配置語法官方文檔比較詳細,這裏就不贅述了。
指南
配置
url-loader 能夠在文件大小(單位 byte)低於指定的限制,將文件轉換爲DataURL,這在實際開發中很是有效,可以減小請求數,在vue-cli和create-react-app中也都能看到對這個loader的使用。
// "url" loader works just like "file" loader but it also embeds // assets smaller than specified size as data URLs to avoid requests. { test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], loader: require.resolve('url-loader'), options: { limit: 10000, name: 'static/media/[name].[hash:8].[ext]', }, },
image-webpack-loader 這是一個能夠經過設置質量參數來壓縮圖片的插件,但我的以爲在實際開發中並不會常用,圖片通常是UI提供,通常來講,他們是不會贊成圖片的質量有問題。
以這種方式加載資源,你能夠以更直觀的方式將模塊和資源組合在一塊兒。無需依賴於含有所有資源的 /assets 目錄,而是將資源與代碼組合在一塊兒。例如,相似這樣的結構會很是有用
- |- /assets + |– /components + | |– /my-component + | | |– index.jsx + | | |– index.css + | | |– icon.svg + | | |– img.png
固然,這種選擇見仁見智
前端中的tree-shaking就是將一些無關的代碼刪掉不打包。在Webpack項目中,咱們一般會引用不少文件,但實際上咱們只引用了其中的某些模塊,但卻須要引入整個文件進行打包,會致使咱們的打包結果變得很大,經過tree-shaking將沒有使用的模塊搖掉,這樣來達到刪除無用代碼的目的。
概括起來就是
1.ES6的模塊引入是靜態分析的,故而能夠在編譯時正確判斷到底加載了什麼代碼。
2.分析程序流,判斷哪些變量未被使用、引用,進而刪除此代碼
概括起來就是
由於Babel的轉譯,使得引用包的代碼有了反作用,而反作用會致使Tree-Shaking失效。
Webpack 4 默認啓用了 Tree Shaking。對反作用進行了消除,如下是我在4.19.1的實驗
index.js import { cube } from './math.js' console.log(cube(5))
math.js
// 不打包square export class square { constructor() { console.log('square') } } export class cube { constructor(x) { return x * x * x } }
// babel編譯後 同不打包 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.cube = cube; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var square = exports.square = function square() { _classCallCheck(this, square); console.log('square'); }; function cube(x) { console.log('cube'); return x * x * x; }
// 不打包 export function square(x) { console.log('square') return x.a } export function cube (x) { return x * x * x }
// wow 被打包 export function square() { console.log('square') return x.a } square({a: 1}) export function cube () { return x * x * x }
簡單說,Source map就是一個信息文件,裏面儲存着位置信息。也就是說,轉換後的代碼的每個位置,所對應的轉換前的位置。
有了它,出錯的時候,除錯工具將直接顯示原始代碼,而不是轉換後的代碼。這無疑給開發者帶來了很大方便。
webpack中的devtool配置項能夠設置sourcemap,能夠參考官方文檔然而,devtool的許多選項都講的不是很清楚,這裏推薦該文章,講的比較詳細
要注意,避免在生產中使用 inline-* 和 eval-*,由於它們能夠增長 bundle 大小,並下降總體性能。
熱替換這一塊目前大多數都是用的webpack-dev-middleware插件配合服務器使用的,而官方提供的watch模式反而比較少用,固然,webpack-dev-middleware的底層監聽watch mode,至於爲何不直接使用watch模式,則是webpack-dev-middleware快速編譯,走內存;只依賴webpack的watch mode來監聽文件變動,自動打包,每次變動,都將新文件打包到本地,就會很慢。
webpack.DefinePlugin 定義環境變量process.env,這在實際開發中比較經常使用,參考create-react-app中的代碼以下:
// Makes some environment variables available to the JS code, for example: // if (process.env.NODE_ENV === 'development') { ... }. See `./env.js`. new webpack.DefinePlugin(env.stringified),
不過,要注意不能在config中使用,由於
process.env.NODE_ENV === 'production' ? '[name].[hash].bundle.js' : '[name].bundle.js'
NODE_ENV
is set in the compiled code, not in the webpack.config.js file. You should not use enviroment variables in your configuration. Pass options via--env.option abc
and export a function from the webpack.config.js.
大體意思就是NODE_ENV是設置在compiled裏面,而不是config文件裏。
ExtractTextWebpackPlugin,將css抽取成單獨文件,能夠經過這種方式配合後端對CSS文件進行緩存。
webpack4的代碼分割插件。
webpack4中支持了零配置的特性,同時對塊打包也作了優化,CommonsChunkPlugin已經被移除了,如今是使用optimization.splitChunks代替。
SplitChunksPlugin的配置有幾個須要比較關注一下
chunks: async | initial | all
cacheGroups
使用cacheGroups能夠自定義配置打包塊。
更多詳細內容參考該文章
則是利用動態引入的文件打包成另外一個包,並懶加載它。其與SplitChunksPlugin的區別:
+ import _ from 'lodash'; + + function component() { var element = document.createElement('div'); + var button = document.createElement('button'); + var br = document.createElement('br'); + button.innerHTML = 'Click me and look at the console!'; element.innerHTML = _.join(['Hello', 'webpack'], ' '); + element.appendChild(br); + element.appendChild(button); + + // Note that because a network request is involved, some indication + // of loading would need to be shown in a production-level site/app. + button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => { + var print = module.default; + + print(); + }); return element; } + document.body.appendChild(component());
注意當調用 ES6 模塊的 import() 方法(引入模塊)時,必須指向模塊的 .default 值,由於它纔是 promise 被處理後返回的實際的 module 對象。
由於webpack會把運行時代碼放到最後的一個bundle中, 因此即便咱們修改了其餘文件的代碼,最後的一個bundle的hash也會改變,runtimeChunk是把運行時代碼單獨提取出來的配置。這樣就有利於咱們和後端配合緩存文件。
配置項
有時候咱們只是添加了個文件print.js, 並在index引入
import Print from './print'
打包的時候,指望只有runtime和main兩個bundle的hash發生改變,可是一般全部bundle都發生了變化,由於每一個 module.id 會基於默認的解析順序(resolve order)進行增量。也就是說,當解析順序發生變化,ID 也會隨之改變。
可使用兩個插件來解決這個問題。第一個插件是 NamedModulesPlugin,將使用模塊的路徑,而不是數字標識符。雖然此插件有助於在開發過程當中輸出結果的可讀性,然而執行時間會長一些。第二個選擇是使用 HashedModuleIdsPlugin。
參考文章
經過ProvidePlugin處理全局變量
其餘更細粒度的處理
首先了解一下polyfills, 雖然在webpack中可以使用es6\es7等的API,但並不表明編譯器支持這些API,因此一般咱們會用polyfills來自定義一個API。
那麼在webpack中,通常是使用babel-polyfill VS babel-runtime VS babel-preset-env等來支持這些API,而這三種怎麼選擇也是一個問題。
在真正進入主題以前,咱們先看一個preset-env的配置項,同時也是package.json中的一個配置項browserslist
{ "browserslist": [ "last 1 version", "> 1%", "maintained node versions", "not dead" ] }
根據這個配置,preset-env或者postcss等會根據你的參數支持不一樣的polyfills,具體的參數配置參考該文章
另外推薦一個網站,能夠看各類瀏覽器的使用狀況。
- babel-polyfill 只須要引入一次,但會重寫一些原生的已支持的方法,並且體積很大。
- transform-runtime 是利用 plugin 自動識別並替換代碼中的新特性,你不須要再引入,只須要裝好 babel-runtime 和 配好 plugin 就能夠了。好處是按需替換,檢測到你須要哪一個,就引入哪一個 polyfill,值得注意的是,instance 上新添加的一些方法,babel-plugin-transform-runtime 是沒有作處理的,好比 數組的 includes, filter, fill 等
- babel-preset-env 根據當前的運行環境,自動肯定你須要的 plugins 和 polyfills。經過各個 es標準 feature 在不一樣瀏覽器以及 node 版本的支持狀況,再去維護一個 feature 跟 plugins 之間的映射關係,最終肯定須要的 plugins。
參考文章
平常咱們引用的Npm包都是編譯好的,這樣帶來的方便的同時也暴露了一些問題。
代碼冗餘:通常來講,這些 NPM 包也是基於 ES2015+ 開發的,每一個包都須要通過 babel 編譯發佈後才能被主應用使用,而這個編譯過程每每會附加不少「編譯代碼」;每一個包都會有一些相同的編譯代碼,這就形成大量代碼的冗餘,而且這部分冗餘代碼是不能經過 Tree Shaking 等技術去除掉的。
非必要的依賴:考慮到組件庫的場景,一般咱們爲了方便一股腦引入了全部組件;但實際狀況下對於一個應用而言可能只是用到了部分組件,此時若是所有引入,也會形成代碼冗餘。
因此咱們本身的公司組件能夠採用後編譯的形式,即發佈的是未經編譯的npm包,在項目構建時才編譯,咱們公司採用的也是這種作法,由於咱們的包都在一個目錄下,因此不用考慮遞歸編譯的問題。
更多的詳細請直接參考該文章
這個比較簡單,直接看代碼或者官方文檔便可
webpack --env.NODE_ENV=local --env.production --progress
Webpack自己並不難於理解,難的是各類各樣的配置和周圍生態帶來的複雜,然而也是這種複雜給咱們帶來了極高的便利性,理解這些有助於在搭建項目更好的優化。後面會繼續寫出兩篇總結,分別是webpack的內部原理流程和webpack的插件開發原理。