摘要:現(xiàn)在,讓我們創(chuàng)建項(xiàng)目的入口,并使用然后創(chuàng)建我們的配置,文件名為,的配置文件是一個(gè),并且需要成一個(gè)對(duì)象在這里,告訴那些文件是你應(yīng)用的入口。代碼分割便是用來解決之前所說的單集成模塊不可維護(hù)的引用的問題。
構(gòu)建工具逐漸成為前端工程必備的工具,Grunt、Gulp、Fis、Webpack等等,譯者有幸使用過Fis、Gulp。
前者是百度的集成化方案,提供了一整套前端構(gòu)建方案,優(yōu)點(diǎn)是基本幫你搞定了,但是靈活性相對(duì)比較低,社區(qū)也沒那么大;后者提供了非常靈活的配置,簡單的語法可以配置出強(qiáng)大的功能,流控制也減少了編譯時(shí)的時(shí)間,可以和各種插件配合使用。
譯者因?yàn)橐褂肁MD模塊機(jī)制,開始接觸了webpack,發(fā)現(xiàn)官網(wǎng)上講的晦澀難懂,無法實(shí)踐,而國內(nèi)雖有博客也講解一些入門的教程,但是要么篇幅過短,要么只講各種配置貼各種代碼,然后谷歌發(fā)現(xiàn)了國外大牛寫的這篇博客,發(fā)現(xiàn)講的非常通俗易懂,配合實(shí)踐和代碼,讓譯者感慨萬千,瞬間打開了一扇大門。
原文鏈接:https://blog.madewithlove.be/post/webpack-your-bags/
作者:Maxime Fabre
譯者:陳堅(jiān)生
也許你已經(jīng)聽說過這個(gè)叫做webpack的新工具了。有些人稱它是一個(gè)像gulp一樣的構(gòu)建工具, 有些人則認(rèn)為它是像browserify一樣的打包工具, 如果你并沒有深入去了解它你可能就會(huì)產(chǎn)生疑惑。就算你仔細(xì)地研究它你也可能依舊困惑,因?yàn)閣ebpack的官網(wǎng)介紹webpack的時(shí)候同時(shí)提到了這兩個(gè)功能。
一開始對(duì)"webpack 是什么"的模糊概念使得我很挫敗以至于我直接關(guān)掉了webpack的網(wǎng)頁。到了現(xiàn)在,我已經(jīng)有了一套自己的構(gòu)建系統(tǒng)并為此覺得很開心。如果你和我一樣緊跟javascript的潮流,那么錯(cuò)過如此好的工具將是非??上У氖虑?。(這句話翻譯的不好:And if you follow closely the very fast Javascript scene, like me, you’ve probably been burnt in the past by jumping on the bandwagon too soon.?)因?yàn)閷?duì)webpack有了一定的實(shí)踐和經(jīng)驗(yàn),我決定寫這篇文章來更加清晰地解釋“什么是webpack”還有webpack的重要性和優(yōu)勢(shì)。
什么是webpack?首先讓我們來回答標(biāo)題中的問題:webpack到底是一個(gè)構(gòu)建系統(tǒng)還是一個(gè)打包工具?好吧, 它都有——但不是說它做了兩者而是說它合并了兩者。webpack并不構(gòu)建你的資源(assets),然后分別對(duì)你的模塊進(jìn)行打包,它認(rèn)為你的資源都是模塊
更精確地說webpack并不是構(gòu)建所有的sass文件,優(yōu)化你的圖片,并將它們包括在一邊,而是打包你所有的模塊,然后在另一個(gè)頁面引用它們,像這樣:
import stylesheet from "styles/my-styles.scss"; import logo from "img/my-logo.svg"; import someTemplate from "html/some-template.html"; console.log(stylesheet); // "body{font-size:12px}" console.log(logo); // "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5[...]" console.log(someTemplate) // "Hello
"
你所有的資源都被認(rèn)為是模塊,因此是可以被引用的、修改、操作,最后可以被打包進(jìn)你的終極模塊中。
為了使得這樣能夠運(yùn)行,你需要在你的webpack配置中注冊(cè)loaders。 Loaders 可以認(rèn)為是一些小型的插件,簡單地說就是讓webpack在處理的時(shí)候,當(dāng)遇到這種類型的文件時(shí),做這樣的操作(操作就是Loaders也就是你的配置)。以下是Loaders配置的一些例子:
{ // When you import a .ts file, parse it with Typescript test: /.ts/, loader: "typescript", },{ // When you encounter images, compress them with image-webpack (wrapper around imagemin) // and then inline them as data64 URLs test: /.(png|jpg|svg)/, loaders: ["url", "image-webpack"], },{ // When you encounter SCSS files, parse them with node-sass, then pass autoprefixer on them // then return the results as a string of CSS test: /.scss/, loaders: ["css", "autoprefixer", "sass"], }
總之到食物鏈的末尾,所有的Loaders返回字符串。這個(gè)機(jī)制使得Webpack可以將它們引進(jìn)到j(luò)avascript的包中。當(dāng)你的sass文件被Loaders轉(zhuǎn)化后,它在內(nèi)部會(huì)像這樣被傳遞:
export default "body{font-size:12px}";為什么要這樣做?
當(dāng)你明白webpack做了什么后,隨之而來的問題大部分是這樣做的好處是什么? “圖片、css在我的JS中?這是什么鬼?” 好吧,思考下我們最近一直推崇的并被教育應(yīng)該這樣做的,把所有的東西打包成一個(gè)文件,以減少http請(qǐng)求……
這導(dǎo)致了一個(gè)很大的缺點(diǎn)就是大多數(shù)人把當(dāng)前的所有資源都打包到一個(gè)app.js文件中,然后包含在所有的頁面。這意味著任何給定頁面上大部分加載的資源都是非必須的。如果你不這樣做,那么你很可能要手工引入資源,導(dǎo)致需要維護(hù)和跟蹤一個(gè)巨大的依賴書,用來記錄那個(gè)頁面用到了樣式表A和樣式表B。
無論方法是正確的還是錯(cuò)誤的。 想象一下webpack作為一個(gè)中間者,它不止是一個(gè)構(gòu)建系統(tǒng)或者一個(gè)打包工具,它是一個(gè)頑皮的智能模塊包裝系統(tǒng)。一旦被很好地配置,它會(huì)比你更加了解你的棧,所以它會(huì)比你更加清楚如何更好地優(yōu)化。
讓我們一起構(gòu)建一個(gè)簡單的APP為了讓你更簡單地了解webpack的優(yōu)點(diǎn),我們將一起構(gòu)建一個(gè)小型的App并對(duì)資源進(jìn)行打包。對(duì)于本教程,我建議運(yùn)行Node4或者Node5以及NPM3的平行依賴樹以避免在使用webpack時(shí)遇到坑爹地問題。如果你還沒有NPM3,你可以通過
npm install npm@3 -g
來安裝。
$ node --version v5.7.1 $ npm --version 3.6.0
我也建議你添加node_modules/.bin 到你的環(huán)境變量中以避免每次都輸入 node_modules/.bin/webpack 來運(yùn)行命令。后面的所有例子我將不會(huì)使用node_modules/.bin這個(gè)命令了。
基本的使用讓我們創(chuàng)建我們的項(xiàng)目并安裝webpack,同時(shí)我們引入jQuery以便后面使用。
$ npm init -y $ npm install jquery --save $ npm install webpack --save-dev
現(xiàn)在,讓我們創(chuàng)建項(xiàng)目的入口,并使用es2015:
src/index.js
var $ = require("jquery"); $("body").html("hello");
然后創(chuàng)建我們的webpack配置,文件名為webpack.config.js, webpack的配置文件是一個(gè)javascript,并且需要export成一個(gè)object(對(duì)象)
webpack.config.js
module.exports = { entry: "./src", output: { path: "builds", filename: "bundle.js", } };
在這里,entry告訴webpack那些文件是你應(yīng)用的入口。入口文件位于你依賴樹的頂部。然后我們告訴它去編譯我們的文件到__builds__這個(gè)文件夾中并使用((bundle.js這個(gè)名字。接下來我們創(chuàng)建我們的index.html:
webpack my title
click me
運(yùn)行webpack,如果一切運(yùn)行正常,我們會(huì)收到一段信息告訴我們webpack成功編譯打包到了bundle.js中:
Version: webpack 1.13.1 Time: 382ms Asset Size Chunks Chunk Names bundle.js 267 kB 0 [emitted] main [0] ./src/index.js 58 bytes {0} [built] + 1 hidden modules
在這里你可以看到webpack告訴你你的bundle.js包含了我們的入口文件index.js同時(shí)還有一個(gè)隱藏的模塊。這個(gè)隱藏的模塊便是jquery,webpack默認(rèn)會(huì)隱藏不屬于你的模塊,如果要看所有被webpack隱藏的模塊,我們可以向webpack傳參 --display-modules:
>webpack --display-modules Hash: 20aea3445ac35ac27c32 Version: webpack 1.13.1 Time: 382ms Asset Size Chunks Chunk Names bundle.js 267 kB 0 [emitted] main [0] ./src/index.js 58 bytes {0} [built] [1] ./~/jquery/dist/jquery.js 258 kB {0} [built]
你也可以運(yùn)行 webpack --watch 讓webpack去監(jiān)聽你的文件,一旦有改變則自動(dòng)編譯。
建立我們的第一個(gè)Loader還記得我們討論過的webpack如何引進(jìn)css和html以及其他所有類型的資源嗎?在那里適合?如果你有投身到這幾年的web組件化發(fā)展的事業(yè)中(angular2, vue, react, polymer, x-tag等等),你應(yīng)該聽說過關(guān)于構(gòu)建webapp的一個(gè)新的概念,不適用單一集成的ui模塊,而是將ui分解為多個(gè)小型的可重用的ui?,F(xiàn)在為了讓組件真正獨(dú)立,他們需要能夠?qū)⑺幸蕾嚩家胨麄冏陨碇?。想象一下一個(gè)按鈕肯定有html、一些腳本讓它能夠交互,當(dāng)然也需要一些樣式。最好能在需要到這個(gè)組件的時(shí)候所有這些資源才被加載。只有當(dāng)我們引入這個(gè)按鈕的時(shí)候,我們才拿到相關(guān)的資源。
讓我們來寫button組件。首先,我假設(shè)大多數(shù)人都習(xí)慣了es2015,我們將添加第一個(gè)Loader: babel。安裝Loader于webpack中需要做兩件事情:**npm install {whatever}-loader, 然后添加它到你webpack配置中,即module.loaders。如下所示:
$ npm install babel-loader --save-dev
由于babel-loader并不會(huì)自動(dòng)安裝babel, 我們需要自己安裝babel-core還有es2015 preset:
$ npm install babel-core babel-preset-es2015 --save-dev
然后我們創(chuàng)建.babelrc來告訴babel應(yīng)該用哪一種preset,文件是json格式,在本例子中,我們告訴它使用es2015 preset
.babelrc { "presets": ["es2015"]}
現(xiàn)在已經(jīng)配置并安裝好babel了。我們需要babel運(yùn)行在所有的以.js結(jié)尾的文件中,但是由于webpack會(huì)遍歷包括第三方在內(nèi)的所有依賴包,因此我們要防止babel運(yùn)行在如jquery這樣的第三方庫中。Loaders可以擁有一個(gè)include或者一個(gè)exclude規(guī)則,它可以是一個(gè)字符串、一個(gè)正則表達(dá)、一個(gè)回調(diào)函數(shù)或者其他任何你想要的。在本例子中,我們想要babel只運(yùn)行在我們的文件上,因此我們將include我們的資源文件夾:
module.exports = { entry: "./src", output: { path: "builds", filename: "[name].js", }, module: { loaders: [ { test: /.js/, loader: "babel", include: __dirname + "/src", } ], } };
現(xiàn)在我們可以重寫我們的index.js(我們?cè)谥耙肓薭abel)。并且接下來的例子我們也將使用es6
寫一個(gè)小型的組件現(xiàn)在我們開始寫一個(gè)小型的button組件, 它將有一些scss樣式,一個(gè)html模板,還有一些行為。我們將安裝我們需要的東西。手下我們需要mustache,一個(gè)非常輕量級(jí)的模板渲染庫,同時(shí)還有sasss和html的Loaders。同時(shí),由于Loader可以像管道一樣將處理后的結(jié)果順序傳遞下去,我們將需要一個(gè)cssloader來處理sass Loader處理后的結(jié)果?,F(xiàn)在,我們有了我們的css, 有很多方式可以處理他們,這次我們使用的是style-loader,它可以動(dòng)態(tài)地將css注入到頁面中去。
$ npm install mustache --save $ npm install css-loader style-loader html-loader sass-loader node-sass --save-dev
我們由右到左以‘!’為分割向配置文件傳遞loader以告訴webpack如何將匹配到的文件順序傳遞給Loaders,你也可以使用數(shù)組來進(jìn)行傳遞,當(dāng)然順序也要是由右到左
module.exports = { entry: "./src", output: { path: "builds", filename: "[name].js", }, module: { loaders: [ { test: /.js/, loader: "babel", include: __dirname + "/src", } ], { test: ".scss", loader: "style!css!sass", // loaders: ["style", "css", "sass"], }, { test: /.html/, loader: "html", } } };
loaders已經(jīng)配置安裝好了,我們可以開始寫我們的按鈕了
src/Components/Button.scss
.button { background: tomato; color: white; }
src/Components/Button.html
{{text}}
src/Components/Button.js
import $ from "jquery"; import template from "./Button.html"; import Mustache from "mustache"; import "./Button.scss"; export default class Button { constructor(link) { this.link = link; } onClick(event) { event.preventDefault(); alert(this.link); } render(node) { const text = $(node).text(); $(node).html( Mustache.render(template, {text}) ); $(".button").click(this.onClick.bind(this)); } }
你的button.js現(xiàn)在是100%自引用并且在那里都可以被引用, 現(xiàn)在我們只需要將button渲染到我們的頁面來
src/index.js
import Button from "./Components/Button"; Button = Button.default const button = new Button("google.com"); button.render("a");
運(yùn)行webpack,刷新頁面,你應(yīng)該可以看到我們丑陋的按鈕,并有對(duì)應(yīng)的行為,(這一步有問題,編譯成功了,但是無法new一個(gè),提示 _Button2.default is not a constructor 錯(cuò)誤)
至此你學(xué)會(huì)了如何建立loaders以及如何定義應(yīng)用每一部分的依賴?,F(xiàn)在貌似還不不出有什么用處,讓我們更加深入到例子中去。
上面的例子會(huì)一直引用button,當(dāng)然,這并沒有什么問題,但我們并不總是一直需要我們的按鈕。也許在一些頁面沒有按鈕需要渲染。在這種情況下,我們不想去引入按鈕的樣式、模板等。這個(gè)時(shí)候就是代碼分割出場(chǎng)的時(shí)候了(code spliting)。代碼分割便是webpack用來解決之前所說的單集成模塊 VS 不可維護(hù)的引用的問題。分割點(diǎn)(split points):你的代碼被分割為多個(gè)文件并被按需請(qǐng)求加載。語法非常簡單:
import $ from "jquery"; // This is a split point require.ensure([], () => { // All the code in here, and everything that is imported // will be in a separate file const library = require("some-big-library"); $("foo").click(() => library.doSomething()); });
任何寫在require.ensure回調(diào)中的東西會(huì)被分隔到一個(gè)數(shù)據(jù)塊,一個(gè)隔離的文件,webpack會(huì)在需要的時(shí)候,通過ajax請(qǐng)求去加載。這意味著,我們會(huì)看到如下面的依賴樹:
bundle.js |- jquery.js |- index.js // our main file chunk1.js |- some-big-libray.js |- index-chunk.js // the code in the callback
并且我們不用去引入chunk1.js或者去加載它,webpack已經(jīng)幫我們做了這些事情。這意味著我們可以通過各種各樣的邏輯去分割我們的代碼。在接下來的例子中,我們只想在頁面有鏈接的時(shí)候去加載我們的button組件
src/index.js
if (document.querySelectorAll("a").length) { require.ensure([], () => { const Button = require("./Components/Button").default; const button = new Button("google.com"); button.render("a"); }); }
注意當(dāng)使用require的時(shí)候,如果你想要默認(rèn)的導(dǎo)出時(shí),你需要手動(dòng)的包裹它(default)。原因在于require無法同時(shí)處理default和正常的導(dǎo)出,所以你需要顯示申明想要用哪一個(gè)。而import則有一個(gè)系統(tǒng)來解決這個(gè)問題,所以它知道如何處理。(eg. import foo from "bar" vs import {baz} from "bar")
現(xiàn)在webpack的輸出信息應(yīng)該不一樣了,讓我們運(yùn)行--display-chunks來看數(shù)據(jù)塊的關(guān)系:
$ webpack --display-modules --display-chunks Hash: 43b51e6cec5eb6572608 Version: webpack 1.12.14 Time: 1185ms Asset Size Chunks Chunk Names bundle.js 3.82 kB 0 [emitted] main 1.bundle.js 300 kB 1 [emitted] chunk {0} bundle.js (main) 235 bytes [rendered] [0] ./src/index.js 235 bytes {0} [built] chunk {1} 1.bundle.js 290 kB {0} [rendered] [1] ./src/Components/Button.js 1.94 kB {1} [built] [2] ./~/jquery/dist/jquery.js 259 kB {1} [built] [3] ./src/Components/Button.html 72 bytes {1} [built] [4] ./~/mustache/mustache.js 19.4 kB {1} [built] [5] ./src/Components/Button.scss 1.05 kB {1} [built] [6] ./~/css-loader!./~/sass-loader!./src/Components/Button.scss 212 bytes {1} [built] [7] ./~/css-loader/lib/css-base.js 1.51 kB {1} [built] [8] ./~/style-loader/addStyles.js 7.21 kB {1} [built]
從輸出數(shù)據(jù)你可以看到,我們的入口文件(bundle.js)現(xiàn)在只包含webpack的邏輯,其他的腳本(jquery、mustache、button)全都在1.bundle.js中,并只有當(dāng)我們頁面中有連接的時(shí)候才會(huì)加載進(jìn)來?,F(xiàn)在為了讓webpack知道到哪里去ajax我們的數(shù)據(jù)塊,我們需要配置下我們的文件:
path: "builds", filename: "bundle.js", publicPath: "builds/",
publishPath告訴webpack到哪里去找資源, 至此,我們運(yùn)行webpack,由于頁面有連接,因此webpack加載了button組件。注意: 我們可以對(duì)數(shù)據(jù)塊進(jìn)行命名來替代默認(rèn)的1.bundle.js:
if (document.querySelectorAll("a").length) { require.ensure([], () => { const Button = require("./Components/Button").default; const button = new Button("google.com"); button.render("a"); }, "button"); }
嘗試了,發(fā)現(xiàn)并沒有什么用……是我打開的方式不對(duì)么
添加第二個(gè)組件src/Components/Header.scss
.header { font-size: 3rem; }
src/Components/Header.html
{{text}}
src/Components/Header.js
import $ from "jquery"; import Mustache from "mustache"; import template from "./Header.html"; import "./Header.scss"; export default class Header { render(node) { const text = $(node).text(); $(node).html( Mustache.render(template, {text}) ); } }
然后在應(yīng)用中渲染它:
// If we have an anchor, render the Button component on it if (document.querySelectorAll("a").length) { require.ensure([], () => { const Button = require("./Components/Button").default; const button = new Button("google.com"); button.render("a"); }); } // If we have a title, render the Header component on it if (document.querySelectorAll("h1").length) { require.ensure([], () => { const Header = require("./Components/Header").default; new Header().render("h1"); }); }
再次運(yùn)行webpack查看依賴情況,你會(huì)發(fā)現(xiàn)兩個(gè)組件都需要jquery、mustache,意味著這些依賴模塊被重復(fù)定義于我們的數(shù)據(jù)塊中,這并不是我們想要的。默認(rèn)情況webpack并不對(duì)此進(jìn)行優(yōu)化。但是webpack可以通過插件的形式提供強(qiáng)力的優(yōu)化方案。
插件(plugins)和loaders不同,loaders只執(zhí)行與特定類型的文件,plugins執(zhí)行于所有的文件并提供更多豐富的功能。webpack擁有大量的插件來處理各種各樣的優(yōu)化。CommonChunksPlugin可以用來解決這個(gè)問題的插件, 它通過遞歸分析你的依賴包,找到公用的模塊并將它們分離成一個(gè)獨(dú)立的文件中,當(dāng)然你也可以寫入到入口文件中。
在接下來的例子中,我們將公用的模塊放到了我們的入口文件中,因?yàn)槿绻械捻撁嬗幸昧薺query和mustache,我們就把它們放到頂端。接下來讓我們更新下我們的配置:
plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: "main", // Move dependencies to our main file children: true, // Look for common dependencies in all children, minChunks: 2, // How many times a dependency must come up before being extracted }) ]
如果我們?cè)俅芜\(yùn)行webpack, 我們可以發(fā)現(xiàn)公用的組件已經(jīng)被提取到了頂部:
chunk {0} bundle.js (main) 287 kB [rendered] [0] ./src/index.js 550 bytes {0} [built] [2] ./~/jquery/dist/jquery.js 259 kB {0} [built] [4] ./~/mustache/mustache.js 19.4 kB {0} [built] [7] ./~/css-loader/lib/css-base.js 1.51 kB {0} [built] [8] ./~/style-loader/addStyles.js 7.21 kB {0} [built] chunk {1} 1.bundle.js 3.28 kB {0} [rendered] [1] ./src/Components/Button.js 1.94 kB {1} [built] [3] ./src/Components/Button.html 72 bytes {1} [built] [5] ./src/Components/Button.scss 1.05 kB {1} [built] [6] ./~/css-loader!./~/sass-loader!./src/Components/Button.scss 212 bytes {1} [built] chunk {2} 2.bundle.js 2.92 kB {0} [rendered] [9] ./src/Components/Header.js 1.62 kB {2} [built] [10] ./src/Components/Header.html 64 bytes {2} [built] [11] ./src/Components/Header.scss 1.05 kB {2} [built] [12] ./~/css-loader!./~/sass-loader!./src/Components/Header.scss 192 bytes {2} [built]
如果我們將name改為"vender‘:
new webpack.optimize.CommonsChunkPlugin({ name: "verder", // Move dependencies to our main file children: true, // Look for common dependencies in all children, minChunks: 2, // How many times a dependency must come up before being extracted })
由于該數(shù)據(jù)塊還沒有創(chuàng)建出來,webpack會(huì)自動(dòng)創(chuàng)建builds/verder.js的文件,然后供我們?cè)趆tml中引用,這一步筆者試了,發(fā)現(xiàn)無法創(chuàng)建vender這個(gè)依賴,所有公用依賴也沒有被提取出來,不知道是不是windows的問題。
你還可以使得公用模塊文件以異步請(qǐng)求的方式加載進(jìn)來,設(shè)置屬性async: true便可以了。webpack還有大量的功能強(qiáng)大智能化的插件,我無法一個(gè)個(gè)介紹它們,但是作為練習(xí),讓我們?yōu)閼?yīng)用創(chuàng)建一個(gè)生產(chǎn)環(huán)境
生產(chǎn)和超越首先,我們將添加幾個(gè)插件到我們的配置中去,但我們只想要在生產(chǎn)環(huán)境中去加載并使用這些插件。所以我們要添加邏輯來控制我們的配置。
var webpack = require("webpack"); var production = process.env.NODE_ENV === "production"; var plugins = [ new webpack.optimize.CommonsChunkPlugin({ name: "main", // Move dependencies to our main file children: true, // Look for common dependencies in all children, minChunks: 2, // How many times a dependency must come up before being extracted }), ]; if (production) { plugins = plugins.concat([ // Production plugins go here ]); } module.exports = { entry: "./src", output: { path: "builds", filename: "bundle.js", publicPath: "builds/", }, plugins: plugins, // ... };
webpack 有多個(gè)設(shè)置我們可以在生產(chǎn)環(huán)境中關(guān)掉:
module.exports = { debug: !production, devtool: production ? false : "eval",
debug意味著不會(huì)打包過多的代碼以讓你在本地調(diào)試的時(shí)候更加容易,第二個(gè)是關(guān)于資源映射的方式(sourcemaps generation),webpack有幾個(gè)方式來渲染sourcemaps,eval是在本地開發(fā)中最好的一種,但在生產(chǎn)環(huán)境中,我們并不在意這些,所以在生產(chǎn)環(huán)境中我們禁止了它。接下來我們可以添加生產(chǎn)環(huán)境中用到的插件:
if (production) { plugins = plugins.concat([ // This plugin looks for similar chunks and files // and merges them for better caching by the user new webpack.optimize.DedupePlugin(), // This plugins optimizes chunks and modules by // how much they are used in your app new webpack.optimize.OccurenceOrderPlugin(), // This plugin prevents Webpack from creating chunks // that would be too small to be worth loading separately new webpack.optimize.MinChunkSizePlugin({ minChunkSize: 51200, // ~50kb }), // This plugin minifies all the Javascript code of the final bundle new webpack.optimize.UglifyJsPlugin({ mangle: true, compress: { warnings: false, // Suppress uglification warnings }, }), // This plugins defines various variables that we can set to false // in production to avoid code related to them from being compiled // in our final bundle new webpack.DefinePlugin({ __SERVER__: !production, __DEVELOPMENT__: !production, __DEVTOOLS__: !production, "process.env": { BABEL_ENV: JSON.stringify(process.env.NODE_ENV), }, }), ]); }
這些我最常使用到的插件,webpack還提供了很多其他的插件供你去協(xié)調(diào)你的模塊和數(shù)據(jù)塊。同時(shí)在npm上也有自由開發(fā)者開發(fā)貢獻(xiàn)出來的擁有強(qiáng)大功能的插件。具體可以參考文章最后的鏈接。
現(xiàn)在你希望你生產(chǎn)環(huán)境下的資源能按版本發(fā)布。還記得我們?yōu)閎undle.js設(shè)置過的output.filename屬性嗎?這里有幾個(gè)變量供你使用,一個(gè)是[hash], 和最終生成的bundle.js內(nèi)容的哈希值保持一致。我們也想我們的數(shù)據(jù)塊(chunks)也版本話,我們將設(shè)置output.chunkFilename屬性來實(shí)現(xiàn)同樣的功能:
output: { path: "builds", filename: production ? "[name]-[hash].js" : "bundle.js", chunkFilename: "[name]-[chunkhash].js", publicPath: "builds/", },
在我們這個(gè)簡單的應(yīng)用中并沒有一個(gè)方法來動(dòng)態(tài)檢索編譯后文件的名字啊,我們將只在生產(chǎn)環(huán)境中使用版本化的資源。同時(shí)我們想在生產(chǎn)環(huán)境中清空我們的打包環(huán)境,讓我們添加一個(gè)三方插件:
npm install --save-dev clean-webpack-plugin
將這個(gè)插件配置到webpack中:
var webpack = require("webpack"); var CleanPlugin = require("clean-webpack-plugin"); // ... if (production) { plugins = plugins.concat([ // Cleanup the builds/ folder before // compiling our final assets new CleanPlugin("builds"),
好了,我們已經(jīng)做了一些優(yōu)化的方案,讓我們來比較下結(jié)果:
$ webpack bundle.js 314 kB 0 [emitted] main 1-21660ec268fe9de7776c.js 4.46 kB 1 [emitted] 2-fcc95abf34773e79afda.js 4.15 kB 2 [emitted] $ NODE_ENV=production webpack main-937cc23ccbf192c9edd6.js 97.2 kB 0 [emitted] main
所以webpack到底做了什么:一開始,由于我們的例子非常的簡單輕量級(jí),我們的兩個(gè)異步數(shù)據(jù)塊不值得使用兩個(gè)異步請(qǐng)求去獲取,所以webpack將它們合并回了入口文件中;其次,所有的文件都被合理地壓縮了。我們從原本的3個(gè)請(qǐng)求總大小為322kb變成了一個(gè)97kb大小的文件。
But wasn’t the point of Webpack to stem away for one big ass JS file?
但是webpack不是不提倡合并成一個(gè)文件嗎?
是的,它的確不提倡,當(dāng)時(shí)如果我們的app很小,代碼量很少,它是提倡這樣做的。但請(qǐng)考慮如下情況,你不需要去考慮什么時(shí)候什么地方做什么合并。如果你的數(shù)據(jù)塊突然間依賴了很多模塊,那么webpack會(huì)讓它變成異步加載而不是合并到入口文件中, 同時(shí)如果這些模塊的依賴有公用的,那么這些模塊也會(huì)被抽離出來等等。你只需要設(shè)立好規(guī)則,然后,webpack變回自動(dòng)提供最好的優(yōu)化方案。不用手冊(cè),不用思考模塊依賴的順序,所有的東西都自動(dòng)化了。
你可能發(fā)現(xiàn)我并沒有設(shè)置任何東西去壓縮我們的HTML和CSS,這是因?yàn)镃SS-loader和html-loader已經(jīng)默認(rèn)完成了這些事情。
因?yàn)閣ebpack是本身就是一個(gè)JS-loader,因此在webpack中沒有js-loader,這也是uglify是一個(gè)獨(dú)立引進(jìn)來的插件的原因。
信息抽取現(xiàn)在你可能發(fā)現(xiàn),一開始我們頂一個(gè)的樣式被分開幾段插入到頁面從而導(dǎo)致了FOUAP(Flash of Ugly Ass Page),如果我們可以把所有的樣式都合并到一個(gè)文件中不是更好嗎?是的,我們可以使用另一個(gè)插件:
$ npm install extract-text-webpack-plugin --save-dev
這個(gè)組件做了我剛才說的事情,它收集了你最后的bundle后內(nèi)容里所有的樣式,并將它們合成到一個(gè)文件中。
讓我們把它引入:
var webpack = require("webpack"); var CleanPlugin = require("clean-webpack-plugin"); var ExtractPlugin = require("extract-text-webpack-plugin"); var production = process.env.NODE_ENV === "production"; var plugins = [ new ExtractPlugin("bundle.css"), // <=== where should content be piped new webpack.optimize.CommonsChunkPlugin({ name: "main", // Move dependencies to our main file children: true, // Look for common dependencies in all children, minChunks: 2, // How many times a dependency must come up before being extracted }), ]; // ... module.exports = { // ... plugins: plugins, module: { loaders: [ { test: /.scss/, loader: ExtractPlugin.extract("style", "css!sass"), }, // ... ], } };
Now the extract method takes two arguments: first is what to do with the extracted contents when we’re in a chunk ("style"), second is what to do when we’re in a main file ("css!sass"). Now if we’re in a chunk, we can’t just magically append our CSS to the generated one so we use the style loader here as before, but for all the styles that are found in the main file, pipe them to a builds/bundle.css file. Let’s test it out, let’s add a small main stylesheet for our application:
(這一段翻譯得不好,請(qǐng)看上面的原文)
現(xiàn)在可以看到 extract 方法傳入了兩個(gè)參數(shù): 第一個(gè)是當(dāng)我們?cè)?strong>style數(shù)據(jù)塊中我們要對(duì)引出的內(nèi)容做什么;第二是當(dāng)我們?cè)谌肟谖募?strong>css!sass中要做的事情。如果我們?cè)谝粋€(gè)數(shù)據(jù)塊中,我們不能簡單地把我們的CSS添加到我們的css文件中,所以我們?cè)诖酥笆褂?strong>style加載器,但對(duì)于在入口函數(shù)找到的所有樣式,我們將它們傳遞到builds/bundle.css文件中。讓我們?yōu)閼?yīng)用添加一個(gè)主樣式表。
問題:這里遇到一個(gè)問題,每次修改主樣式表(styles.scss)后,如果是有監(jiān)聽的話,webpack的自動(dòng)重編譯是會(huì)出錯(cuò)的,需要重新保存一次腳本才能讓其正確編譯成功,不知道是什么問題導(dǎo)致的。
src/styles.scss
body { font-family: sans-serif; background: darken(white, 0.2); }
src/index.js
import "./styles.scss";
如果你想導(dǎo)出所有的樣式,你也可以向ExtractTextPlugin傳參(’bundle.css", {allChunks: true})。如果你想在你的文件名中使用變量,你也可以傳入 [name]-[hash].css。
圖片處理
腳本處理已經(jīng)基本可以,但是我們還沒有處理如圖片、字體等資源。在webpack中要怎么處理這些資源并得到最好的優(yōu)化?接下來讓我們下載一張圖片并讓它作為我們的背景,因?yàn)槲矣X得它很酷:
將這張圖片保存在img/puppy.png& 并更新我們的sass文件:
body { font-family: sans-serif; background-color: #333; background-image: url("../img/puppy.jpg"); background-size: cover; }
如果你這樣做的話,webpack會(huì)和你說:“我tmd要我怎么處理jpg這東西?”,因?yàn)槲覀儧]有一個(gè)Loader用來處理它。有兩個(gè)自帶的加載器可以用來處理這些資源,一個(gè)是file-loader,另一個(gè)是url-loader,第一個(gè)不會(huì)做什么改變,只會(huì)返回一個(gè)url,并可以版本化設(shè)置,第二個(gè)可以將資源轉(zhuǎn)化為base64的url
這兩個(gè)加載器各有優(yōu)缺點(diǎn):如果你的背景圖片是2mb大的圖片,你不會(huì)想將它作為base64引入到樣式表中而更加傾向于多帶帶去加載它。如果它只是一個(gè)2kb的圖片,那么則引入它從而減少http請(qǐng)求次數(shù)會(huì)更好:
所以我們把這兩個(gè)加載器都安裝上:
$ npm install --save-dev url-loader file-loader
{ test: /.(png|gif|jpe?g|svg)$/i, loader: "url?limit=10000", },
我們?cè)谶@里向url-loader傳遞了限制類的參數(shù),告訴它:如果資源文件小于10kb則引入,否則,使用file-loader去處理它。語法使用的是查詢字符串,你也可以使用對(duì)象去配置加載器:
{ test: /.(png|gif|jpe?g|svg)$/i, // loader: "url?limit=10000", loader: "url", query: { limit: 10000, } },
好了,讓我們來試試看
bundle.js 15 kB 0 [emitted] main 1-b8256867498f4be01fd7.js 317 kB 1 [emitted] 2-e1bc215a6b91d55a09aa.js 317 kB 2 [emitted] bundle.css 2.9 kB 0 [emitted] main
我們可以看到并沒有提到j(luò)pg文件,因?yàn)槲覀兊膒uppy圖片太小了,它被直接引入到bundle.css文件中了。
webpack會(huì)智能地根據(jù)大小或者h(yuǎn)ttp請(qǐng)求來優(yōu)化資源文件。還有很多加載器可以更好地處理,最常用的一個(gè)是image-loader,可以在合并的時(shí)候?qū)D片進(jìn)行壓縮,甚至可以設(shè)置?bypassOnDebug讓你只在生產(chǎn)環(huán)境中使用。像這樣的插件還有很多,我鼓勵(lì)你在文章的末尾去看看這些插件。
實(shí)時(shí)監(jiān)聽編譯我們的生產(chǎn)環(huán)境已經(jīng)搭建好了,接下來就是實(shí)時(shí)重載:LiveReload、BrowserSync,這可能是你想要的。但是刷新整個(gè)頁面很消耗性能,讓我們使用更吊的裝備hot module replacement或者叫做hot reload。由于webpack知道我們依賴樹的每一個(gè)模塊的位置,修改的時(shí)候就可以很簡單地替換樹上的某一塊文件。更清晰地說,當(dāng)你修改文件的時(shí)候,瀏覽器不用刷新整個(gè)頁面就可以看到實(shí)時(shí)變化。
要使用HMR,我們需要一個(gè)支持hot assets的服務(wù)器。Webpack有一個(gè)dev-server供我們使用,安裝下:
$ npm install webpack-dev-server --save-dev
然后運(yùn)行該服務(wù)器:
$ webpack-dev-server --inline --hot
第一個(gè)參數(shù)告訴webpack將HMR邏輯引入到頁面中(而不使用一個(gè)iframe去包含頁面),第二個(gè)參數(shù)是啟動(dòng)HMR(hot module reload)。現(xiàn)在讓我們?cè)L問web-server的地址:http://localhost:8080/webpack-dev-server/ 。嘗試改變文件,會(huì)看到瀏覽器上實(shí)時(shí)的變化
你可以使用這個(gè)插件作為你的本地服務(wù)器。如果你計(jì)劃一直使用它來做HMR,你可以將其配置到webpack中。
output: { path: "builds", filename: production ? "[name]-[hash].js" : "bundle.js", chunkFilename: "[name]-[chunkhash].js", publicPath: "builds/", }, devServer: { hot: true, },
配置后,無論我們什么時(shí)候運(yùn)行ewbpack-dev-server,它都會(huì)在HMR模式。當(dāng)然,還有很多配置供你配置,例如提供一個(gè)中間件供你在express服務(wù)器中使用HMR模式。
規(guī)范的代碼如果你一直跟著本文實(shí)踐,你肯定發(fā)現(xiàn)了奇怪的地方:為什么Loaders被放在了Module.loaders中,而plugins卻沒有?這當(dāng)然是因?yàn)檫€有其他東西你可以放在module中!Webpack不僅有l(wèi)oaders,它也有pre-loaders和post-loaders:它們會(huì)在主加載器加載前/加載后執(zhí)行。來個(gè)例子,很明顯我的代碼非常糟糕,所以在轉(zhuǎn)化前我們使用eslint來檢測(cè)我們的代碼:
$ npm install eslint eslint-loader babel-eslint --save-dev
創(chuàng)建一個(gè)小型的eslintrc文件:
.eslintrc
parser: "babel-eslint" rules: quotes: 2
現(xiàn)在我們添加我們的preloader,我們使用和之前一樣的語法:
preLoaders: [ { test: /.js/, loader: "eslint", } ],
然后運(yùn)行webpack,當(dāng)然,它會(huì)報(bào)錯(cuò):
$ webpack Hash: 33cc307122f0a9608812 Version: webpack 1.12.2 Time: 1307ms Asset Size Chunks Chunk Names bundle.js 305 kB 0 [emitted] main 1-551ae2634fda70fd8502.js 4.5 kB 1 [emitted] 2-999713ac2cd9c7cf079b.js 4.17 kB 2 [emitted] bundle.css 59 bytes 0 [emitted] main + 15 hidden modules ERROR in ./src/index.js /Users/anahkiasen/Sites/webpack/src/index.js 1:8 error Strings must use doublequote quotes 4:31 error Strings must use doublequote quotes 6:32 error Strings must use doublequote quotes 7:35 error Strings must use doublequote quotes 9:23 error Strings must use doublequote quotes 14:31 error Strings must use doublequote quotes 16:32 error Strings must use doublequote quotes 18:29 error Strings must use doublequote quotes
在舉另一個(gè)例子,現(xiàn)在我們的組件都會(huì)引入同樣名字的樣式表以及模板。讓我們使用一個(gè)預(yù)加載器來自動(dòng)加載:
$ npm install baggage-loader --save-dev
{ test: /.js/, loader: "baggage?[file].html=template&[file].scss", }
這告訴webpack,如果你定義了一個(gè)同樣名字的html文件,會(huì)把它以template的名字引入,同樣的也會(huì)引入同名的sass文件?,F(xiàn)在我們可以修改我們的組件:
import $ from "jquery" import Mustache from "mustache" // import template from "./Header.html" // import "./Header.scss"
pre-loader的功能強(qiáng)大,post-loader也一樣,你也可以從文章末尾看到很多有用的加載器并使用它們。
你還想了解更多嗎?現(xiàn)在我們的應(yīng)用還很小,但隨著應(yīng)用的增大,了解正式的依賴樹情況是很有用的??梢詭椭覀兞私馕覀冏龅氖欠裾_,我們的應(yīng)用的瓶頸在哪里。webpack知道所有這些事情,但我們需要告訴他顯示給我們看,我們可以到處一個(gè)profile文件:
webpack --profile --json > stats.json
第一個(gè)參數(shù)告訴webpack生成一個(gè)profile 文件,第一個(gè)指定生成的格式。有多個(gè)站點(diǎn)提供分析并可視化這些文件的功能,webpack官方也提供解析這些信息的功能。所以你可以到webpack analysis引入你的文件。選擇modules 標(biāo)簽然后便可以看到你的可視化依賴樹。另一個(gè)我比較喜歡的是webpack visualizer
用圓環(huán)圖的形式表示你的包大小占據(jù)情況。
我知道在我的案例中,Webpack已經(jīng)完全取代了Grunt或者gulp了,大部分功能已經(jīng)由webpack來渠道,剩下的值通過npm script。過去使用Aglio轉(zhuǎn)化我們的API文檔為html我們使用的是任務(wù)型,現(xiàn)在可以這樣做:
package.json
{ "scripts": { "build": "webpack", "build:api": "aglio -i docs/api/index.apib -o docs/api/index.html" } }
無論你在gulp中有多么復(fù)雜不關(guān)乎打包的任務(wù),Webpack都可以很好地配合。提供一個(gè)在Gulp中集成webpack的例子:
var gulp = require("gulp"); var gutil = require("gutil"); var webpack = require("webpack"); var config = require("./webpack.config"); gulp.task("default", function(callback) { webpack(config, function(error, stats) { if (error) throw new gutil.PluginError("webpack", error); gutil.log("[webpack]", stats.toString()); callback(); }); });
webpack也有Node API,所以在其他構(gòu)建系統(tǒng)中可以很容易地被使用和包容。
以上我只講述了webpack的冰山一角,也許你認(rèn)為我們已經(jīng)通過這篇文章了解了很多,但是我們只講述了寫皮毛: multiple entry points、prefetching、context replacement等等。Webpack是一個(gè)強(qiáng)大的工具,當(dāng)然代價(jià)是更多的配置需要你去寫。不過一旦你知道如何馴服它,它會(huì)給你最好的優(yōu)化方案。我在幾個(gè)項(xiàng)目中使用了它,它也提供了強(qiáng)大的優(yōu)化方案和自動(dòng)化,讓我無法不用它。
資源Webpack documentation
List of loaders
List of plugins
Sources for this article
Our Webpack configuration package
譯者拓展鏈接:
style-loader文檔
備注開發(fā)過程遇到的問題可以查看原文下的評(píng)論或和譯者交流學(xué)習(xí)。
譯者英文水平有限,如果哪里翻譯的不好歡迎指正,相關(guān)的代碼可參考譯者的demo2和demo6,demo4是使用Webpack + Vue寫的DEMO,有興趣的同學(xué)也可以看看。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/86327.html
摘要:正在暑假中的課多周刊第期我們的微信公眾號(hào),更多精彩內(nèi)容皆在微信公眾號(hào),歡迎關(guān)注。若有幫助,請(qǐng)把課多周刊推薦給你的朋友,你的支持是我們最大的動(dòng)力。原理微信熱更新方案漲知識(shí)了,熱更新是以后的標(biāo)配。 正在暑假中的《課多周刊》(第1期) 我們的微信公眾號(hào):fed-talk,更多精彩內(nèi)容皆在微信公眾號(hào),歡迎關(guān)注。 若有幫助,請(qǐng)把 課多周刊 推薦給你的朋友,你的支持是我們最大的動(dòng)力。 遠(yuǎn)上寒山石徑...
摘要:正在暑假中的課多周刊第期我們的微信公眾號(hào),更多精彩內(nèi)容皆在微信公眾號(hào),歡迎關(guān)注。若有幫助,請(qǐng)把課多周刊推薦給你的朋友,你的支持是我們最大的動(dòng)力。原理微信熱更新方案漲知識(shí)了,熱更新是以后的標(biāo)配。 正在暑假中的《課多周刊》(第1期) 我們的微信公眾號(hào):fed-talk,更多精彩內(nèi)容皆在微信公眾號(hào),歡迎關(guān)注。 若有幫助,請(qǐng)把 課多周刊 推薦給你的朋友,你的支持是我們最大的動(dòng)力。 遠(yuǎn)上寒山石徑...
摘要:前端日?qǐng)?bào)精選浮點(diǎn)數(shù)精度之謎前端面試必備基本排序算法從賀老微博引出的遍歷器加速那些奧秘進(jìn)階之深入理解數(shù)據(jù)雙向綁定全棧天中文深入理解筆記用模塊封裝代碼前端架構(gòu)經(jīng)驗(yàn)分享周二放送自制知乎專欄譯在大型應(yīng)用中使用的五個(gè)技巧掘金開發(fā)指南眾成 2017-08-02 前端日?qǐng)?bào) 精選 JavaScript 浮點(diǎn)數(shù)精度之謎前端面試必備——基本排序算法從賀老微博引出的遍歷器(Iterators)加速那些奧秘J...
摘要:在年成為最大贏家,贏得了實(shí)現(xiàn)的風(fēng)暴之戰(zhàn)。和他的競(jìng)爭者位列第二沒有前端開發(fā)者可以忽視和它的生態(tài)系統(tǒng)。他的殺手級(jí)特性是探測(cè)功能,通過檢查任何用戶的功能,以直觀的方式讓開發(fā)人員檢查所有端點(diǎn)。 2016 JavaScript 后起之秀 本文轉(zhuǎn)載自:眾成翻譯譯者:zxhycxq鏈接:http://www.zcfy.cc/article/2410原文:https://risingstars2016...
學(xué)習(xí)的過程中收藏了這些優(yōu)秀教程和的項(xiàng)目,希望對(duì)你有幫助。 github地址, 有不錯(cuò)的就更新 官方文檔 中文指南 初級(jí)教程 webpack-howto 作者:Pete Hunt Webpack 入門指迷 作者:題葉 webpack-demos 作者:ruanyf 一小時(shí)包教會(huì) —— webpack 入門指南 作者:VaJoy Larn webpack 入門及實(shí)踐 作者:...
閱讀 1500·2021-11-22 15:11
閱讀 2905·2019-08-30 14:16
閱讀 2818·2019-08-29 15:21
閱讀 2962·2019-08-29 15:11
閱讀 2520·2019-08-29 13:19
閱讀 3044·2019-08-29 12:25
閱讀 478·2019-08-29 12:21
閱讀 2904·2019-08-29 11:03