摘要:但由于和技術(shù)過于和復(fù)雜,并沒能得到廣泛的推廣。但是在瀏覽器內(nèi)并不適用。依托模塊化編程,的實現(xiàn)方式更為簡單清晰,一個網(wǎng)頁不再是傳統(tǒng)的類似文檔的頁面,而是一個完整的應(yīng)用程序。到了這里,我們的主角登場了年此處應(yīng)有掌聲。和差不多同期登場的還有。
Github:https://github.com/fenivana/w...寫在開頭webpack 更新到了 4.0,官網(wǎng)還沒有更新文檔。因此把教程更新一下,方便大家用起 webpack 4。
先說說為什么要寫這篇文章,最初的原因是組里的小朋友們看了 webpack 文檔后,表情都是這樣的:摘自 webpack 一篇文檔的評論區(qū))
和這樣的:
是的,即使是外國佬也在吐槽這文檔不是人能看的。回想起當(dāng)年自己啃 webpack 文檔的血與淚的往事,覺得有必要整一個教程,可以讓大家看完后愉悅地搭建起一個 webpack 打包方案的項目。
官網(wǎng)新的 webpack 文檔現(xiàn)在寫的很詳細(xì)了,能看英文的小伙伴可以直接去看官網(wǎng)。
可能會有人問 webpack 到底有什么用,你不能上來就糊我一臉代碼讓我馬上搞,我照著搞了一遍結(jié)果根本沒什么用,都是騙人的。所以,在說 webpack 之前,我想先談一下前端打包方案這幾年的演進(jìn)歷程,在什么場景下,我們遇到了什么問題,催生出了應(yīng)對這些問題的工具。了解了需求和目的之后,你就知道什么時候 webpack 可以幫到你。我希望我用完之后很爽,你們用完之后也是。
先說說前端打包方案的黑暗歷史在很長的一段前端歷史里,是不存在打包這個說法的。那個時候頁面基本是純靜態(tài)的或者服務(wù)端輸出的,沒有 AJAX,也沒有 jQuery。那個時候的 JavaScript 就像個玩具,用處大概就是在側(cè)欄弄個時鐘,用 media player 放個 mp3 之類的腳本,代碼量不是很多,直接放在 標(biāo)簽里或者弄個 js 文件引一下就行,日子過得很輕松愉快。
隨后的幾年,人們開始嘗試在一個頁面里做更多的事情。容器的顯示,隱藏,切換。用 css 寫的彈層,圖片輪播等等。但如果一個頁面內(nèi)不能向服務(wù)器請求數(shù)據(jù),能做的事情畢竟有限的,代碼的量也能維持在頁面交互邏輯范圍內(nèi)。這時候很多人開始突破一個頁面能做的事情的范圍,使用隱藏的 iframe 和 flash 等作為和服務(wù)器通信的橋梁,新世界的大門慢慢地被打開,在一個頁面內(nèi)和服務(wù)器進(jìn)行數(shù)據(jù)交互,意味著以前需要跳轉(zhuǎn)多個頁面的事情現(xiàn)在可以用一個頁面搞定。但由于 iframe 和 flash 技術(shù)過于 tricky 和復(fù)雜,并沒能得到廣泛的推廣。
直到 Google 推出 Gmail 的時候(2004 年),人們意識到了一個被忽略的接口,XMLHttpRequest, 也就是我們俗稱的 AJAX, 這是一個使用方便的,兼容性良好的服務(wù)器通信接口。從此開始,我們的頁面開始玩出各種花來了,前端一下子出現(xiàn)了各種各樣的庫,Prototype、Dojo、MooTools、Ext JS、jQuery…… 我們開始往頁面里插入各種庫和插件,我們的 js 文件也就爆炸了。
隨著 js 能做的事情越來越多,引用越來越多,文件越來越大,加上當(dāng)時大約只有 2Mbps 左右的網(wǎng)速,下載速度還不如 3G 網(wǎng)絡(luò),對 js 文件的壓縮和合并的需求越來越強烈,當(dāng)然這里面也有把代碼混淆了不容易被盜用等其他因素在里面。JSMin、YUI Compressor、Closure Compiler、UglifyJS 等 js 文件壓縮合并工具陸陸續(xù)續(xù)誕生了。壓縮工具是有了,但我們得要執(zhí)行它,最簡單的辦法呢,就是 windows 上搞個 bat 腳本,mac / linux 上搞個 bash 腳本,哪幾個文件要合并在一塊的,哪幾個要壓縮的,發(fā)布的時候運行一下腳本,生成壓縮后的文件。
基于合并壓縮技術(shù),項目越做越大,問題也越來越多,大概就是以下這些問題:
庫和插件為了要給他人調(diào)用,肯定要找個地方注冊,一般就是在 window 下申明一個全局的函數(shù)或?qū)ο蟆ky保哪天用的兩個庫在全局用同樣的名字,那就沖突了。
庫和插件如果還依賴其他的庫和插件,就要告知使用人,需要先引哪些依賴庫,那些依賴庫也有自己的依賴庫的話,就要先引依賴庫的依賴庫,以此類推。
恰好就在這個時候(2009 年),隨著后端 JavaScript 技術(shù)的發(fā)展,人們提出了 CommonJS 的模塊化規(guī)范,大概的語法是: 如果 a.js 依賴 b.js 和 c.js, 那么就在 a.js 的頭部,引入這些依賴文件:
var b = require("./b") var c = require("./c")
那么變量 b 和 c 會是什么呢?那就是 b.js 和 c.js 導(dǎo)出的東西,比如 b.js 可以這樣導(dǎo)出:
exports.square = function(num) { return num * num }
然后就可以在 a.js 使用這個 square 方法:
var n = b.square(2)
如果 c.js 依賴 d.js, 導(dǎo)出的是一個 Number, 那么可以這樣寫:
var d = require("./d") module.exports = d.PI // 假設(shè) d.PI 的值是 3.14159
那么 a.js 中的變量 c 就是數(shù)字 3.14159,具體的語法規(guī)范可以查看 Node.js 的 文檔。
但是 CommonJS 在瀏覽器內(nèi)并不適用。因為 require() 的返回是同步的,意味著有多個依賴的話需要一個一個依次下載,堵塞了 js 腳本的執(zhí)行。所以人們就在 CommonJS 的基礎(chǔ)上定義了 Asynchronous Module Definition (AMD) 規(guī)范(2011 年),使用了異步回調(diào)的語法來并行下載多個依賴項,比如作為入口的 a.js 可以這樣寫:
require(["./b", "./c"], function(b, c) { var n = b.square(2) console.log(c) })
相應(yīng)的導(dǎo)出語法也是異步回調(diào)方式,比如 c.js 依賴 d.js, 就寫成這樣:
define(["./d"], function(d) { return d.PI })
可以看到,定義一個模塊是使用 define() 函數(shù),define() 和 require() 的區(qū)別是,define() 必須要在回調(diào)函數(shù)中返回一個值作為導(dǎo)出的東西,require() 不需要導(dǎo)出東西,因此回調(diào)函數(shù)中不需要返回值,也無法作為被依賴項被其他文件導(dǎo)入,因此一般用于入口文件,比如頁面中這樣加載 a.js:
以上是 AMD 規(guī)范的基本用法,更詳細(xì)的就不多說了(反正也淘汰了~),有興趣的可以看 這里。
js 模塊化問題基本解決了,css 和 html 也沒閑著。什么 less,sass,stylus 的 css 預(yù)處理器橫空出世,說能幫我們簡化 css 的寫法,自動給你加 vendor prefix。html 在這期間也出現(xiàn)了一堆模板語言,什么 handlebars,ejs,jade,可以把 ajax 拿到的數(shù)據(jù)插入到模板中,然后用 innerHTML 顯示到頁面上。
托 AMD 和 CSS 預(yù)處理和模板語言的福,我們的編譯腳本也洋洋灑灑寫了百來行。命令行腳本有個不好的地方,就是 windows 和 mac/linux 是不通用的,如果有跨平臺需求的話,windows 要裝個可以執(zhí)行 bash 腳本的命令行工具,比如 msys(目前最新的是 msys2),或者使用 php 或 python 等其他語言的腳本來編寫,對于非全棧型的前端程序員來說,寫 bash / php / python 還是很生澀的。因此我們需要一個簡單的打包工具,可以利用各種編譯工具,編譯 / 壓縮 js、css、html、圖片等資源。然后 Grunt 產(chǎn)生了(2012 年),配置文件格式是我們最愛的 js,寫法也很簡單,社區(qū)有非常多的插件支持各種編譯、lint、測試工具。一年多后另一個打包工具 gulp 誕生了,擴展性更強,采用流式處理效率更高。
依托 AMD 模塊化編程,SPA(Single-page application) 的實現(xiàn)方式更為簡單清晰,一個網(wǎng)頁不再是傳統(tǒng)的類似 word 文檔的頁面,而是一個完整的應(yīng)用程序。SPA 應(yīng)用有一個總的入口頁面,我們通常把它命名為 index.html、app.html、main.html,這個 html 的 一般是空的,或者只有總的布局(layout),比如下圖:
布局會把 header、nav、footer 的內(nèi)容填上,但 main 區(qū)域是個空的容器。這個作為入口的 html 最主要的工作是加載啟動 SPA 的 js 文件,然后由 js 驅(qū)動,根據(jù)當(dāng)前瀏覽器地址進(jìn)行路由分發(fā),加載對應(yīng)的 AMD 模塊,然后該 AMD 模塊執(zhí)行,渲染對應(yīng)的 html 到頁面指定的容器內(nèi)(比如圖中的 main)。在點擊鏈接等交互時,頁面不會跳轉(zhuǎn),而是由 js 路由加載對應(yīng)的 AMD 模塊,然后該 AMD 模塊渲染對應(yīng)的 html 到容器內(nèi)。
雖然 AMD 模塊讓 SPA 更容易地實現(xiàn),但小問題還是很多的:
不是所有的第三方庫都是 AMD 規(guī)范的,這時候要配置 shim,很麻煩。
雖然 RequireJS 支持通過插件把 html 作為依賴加載,但 html 里面的 的路徑是個問題,需要使用絕對路徑并且保持打包后的圖片路徑和打包前的路徑不變,或者使用 html 模板語言把 src 寫成變量,在運行時生成。
不支持動態(tài)加載 css,變通的方法是把所有的 css 文件合并壓縮成一個文件,在入口的 html 頁面一次性加載。
SPA 項目越做越大,一個應(yīng)用打包后的 js 文件到了幾 MB 的大小。雖然 r.js 支持分模塊打包,但配置很麻煩,因為模塊之間會互相依賴,在配置的時候需要 exclude 那些通用的依賴項,而依賴項要在文件里一個個檢查。
所有的第三方庫都要自己一個個的下載,解壓,放到某個目錄下,更別提更新有多麻煩了。雖然可以用 npm 包管理工具,但 npm 的包都是 CommonJS 規(guī)范的,給后端 Node.js 用的,只有部分支持 AMD 規(guī)范,而且在 npm 3 之前,這些包有依賴項的話也是不能用的。后來有個 bower 包管理工具是專門的 web 前端倉庫,這里的包一般都支持 AMD 規(guī)范。
AMD 規(guī)范定義和引用模塊的語法太麻煩,上面介紹的 AMD 語法僅是最簡單通用的語法,API 文檔里面還有很多變異的寫法,特別是當(dāng)發(fā)生循環(huán)引用的時候(a 依賴 b,b 依賴 a),需要使用其他的 語法 解決這個問題。而且 npm 上很多前后端通用的庫都是 CommonJS 的語法。后來很多人又開始嘗試使用 ES6 模塊規(guī)范,如何引用 ES6 模塊又是一個大問題。
項目的文件結(jié)構(gòu)不合理,因為 grunt/gulp 是按照文件格式批量處理的,所以一般會把 js、html、css、圖片分別放在不同的目錄下,所以同一個模塊的文件會散落在不同的目錄下,開發(fā)的時候找文件是個麻煩的事情。code review 時想知道一個文件是哪個模塊的也很麻煩,解決辦法比如又要在 imgs 目錄下建立按模塊命名的文件夾,里面再放圖片。
到了這里,我們的主角 webpack 登場了(2012 年)(此處應(yīng)有掌聲)。
和 webpack 差不多同期登場的還有 Browserify。這里簡單介紹一下 Browserify。Browserify 的目的是讓前端也能用 CommonJS 的語法 require("module") 來加載 js。它會從入口 js 文件開始,把所有的 require() 調(diào)用的文件打包合并到一個文件,這樣就解決了異步加載的問題。那么 Browserify 有什么不足之處導(dǎo)致我不推薦使用它呢? 主要原因有下面幾點:
最主要的一點,Browserify 不支持把代碼打包成多個文件,在有需要的時候加載。這就意味著訪問任何一個頁面都會全量加載所有文件。
Browserify 對其他非 js 文件的加載不夠完善,因為它主要解決的是 require() js 模塊的問題,其他文件不是它關(guān)心的部分。比如 html 文件里的 img 標(biāo)簽,它只能轉(zhuǎn)成 Data URI 的形式,而不能替換為打包后的路徑。
因為上面一點 Browserify 對資源文件的加載支持不夠完善,導(dǎo)致打包時一般都要配合 gulp 或 grunt 一塊使用,無謂地增加了打包的難度。
Browserify 只支持 CommonJS 模塊規(guī)范,不支持 AMD 和 ES6 模塊規(guī)范,這意味舊的 AMD 模塊和將來的 ES6 模塊不能使用。
基于以上幾點,Browserify 并不是一個理想的選擇。那么 webpack 是否解決了以上的幾個問題呢? 廢話,不然介紹它干嘛。那么下面章節(jié)我們用實戰(zhàn)的方式來說明 webpack 是怎么解決上述的問題的。
上手先搞一個簡單的 SPA 應(yīng)用一上來步子太大容易扯到蛋,讓我們先弄個最簡單的 webpack 配置來熱一下身。
安裝 Node.jswebpack 是基于我大 Node.js 的打包工具,上來第一件事自然是先安裝 Node.js 了,傳送門 ->。
初始化一個項目我們先隨便找個地方,建一個文件夾叫 simple, 然后在這里面搭項目。完成品在 examples/simple 目錄,大家搞的時候可以參照一下。我們先看一下目錄結(jié)構(gòu):
├── dist 打包輸出目錄,只需部署這個目錄到生產(chǎn)環(huán)境 ├── package.json 項目配置信息 ├── node_modules npm 安裝的依賴包都在這里面 ├── src 我們的源代碼 │ ├── components 可以復(fù)用的模塊放在這里面 │ ├── index.html 入口 html │ ├── index.js 入口 js │ ├── shared 公共函數(shù)庫 │ └── views 頁面放這里 └── webpack.config.js webpack 配置文件
打開命令行窗口,cd 到剛才建的 simple 目錄。然后執(zhí)行這個命令初始化項目:
npm init
命令行會要你輸入一些配置信息,我們這里一路按回車下去,生成一個默認(rèn)的項目配置文件 package.json。
給項目加上語法報錯和代碼規(guī)范檢查我們安裝 eslint, 用來檢查語法報錯,當(dāng)我們書寫 js 時,有錯誤的地方會出現(xiàn)提示。
npm install eslint eslint-config-enough eslint-loader --save-dev
npm install 可以一條命令同時安裝多個包,包之間用空格分隔。包會被安裝進(jìn) node_modules 目錄中。
--save-dev 會把安裝的包和版本號記錄到 package.json 中的 devDependencies 對象中,還有一個 --save, 會記錄到 dependencies 對象中,它們的區(qū)別,我們可以先簡單的理解為打包工具和測試工具用到的包使用 --save-dev 存到 devDependencies, 比如 eslint、webpack。瀏覽器中執(zhí)行的 js 用到的包存到 dependencies, 比如 jQuery 等。那么它們用來干嘛的?
因為有些 npm 包安裝是需要編譯的,那么導(dǎo)致 windows / mac /linux 上編譯出的可執(zhí)行文件是不同的,也就是無法通用,因此我們在提交代碼到 git 上去的時候,一般都會在 .gitignore 里指定忽略 node_modules 目錄和里面的文件,這樣其他人從 git 上拉下來的項目是沒有 node_modules 目錄的,這時我們需要運行
npm install
它會讀取 package.json 中的 devDependencies 和 dependencies 字段,把記錄的包的相應(yīng)版本下載下來。
這里 eslint-config-enough 是配置文件,它規(guī)定了代碼規(guī)范,要使它生效,我們要在 package.json 中添加內(nèi)容:
{ "eslintConfig": { "extends": "enough", "env": { "browser": true, "node": true } } }
業(yè)界最有名的語法規(guī)范是 airbnb 出品的,但它規(guī)定的太死板了,比如不允許使用 for-of 和 for-in 等。感興趣的同學(xué)可以參照 這里 安裝使用。
eslint-loader 用于在 webpack 編譯的時候檢查代碼,如果有錯誤,webpack 會報錯。
項目里安裝了 eslint 還沒用,我們的 IDE 和編輯器也得要裝 eslint 插件支持它。
Visual Studio Code 需要安裝 ESLint 擴展
atom 需要安裝 linter 和 linter-eslint 這兩個插件,裝好后重啟生效。
WebStorm 需要在設(shè)置中打開 eslint 開關(guān):
寫幾個頁面我們寫一個最簡單的 SPA 應(yīng)用來介紹 SPA 應(yīng)用的內(nèi)部工作原理。首先,建立 src/index.html 文件,內(nèi)容如下:
它是一個空白頁面,注意這里我們不需要自己寫 , 因為打包后的文件名和路徑可能會變,所以我們用 webpack 插件幫我們自動加上。
src/index.js:
// 引入 router import router from "./router" // 啟動 router router.start()
src/router.js:
// 引入頁面文件 import foo from "./views/foo" import bar from "./views/bar" const routes = { "/foo": foo, "/bar": bar } // Router 類,用來控制頁面根據(jù)當(dāng)前 URL 切換 class Router { start() { // 點擊瀏覽器后退 / 前進(jìn)按鈕時會觸發(fā) window.onpopstate 事件,我們在這時切換到相應(yīng)頁面 // https://developer.mozilla.org/en-US/docs/Web/Events/popstate window.addEventListener("popstate", () => { this.load(location.pathname) }) // 打開頁面時加載當(dāng)前頁面 this.load(location.pathname) } // 前往 path,變更地址欄 URL,并加載相應(yīng)頁面 go(path) { // 變更地址欄 URL history.pushState({}, "", path) // 加載頁面 this.load(path) } // 加載 path 路徑的頁面 load(path) { // 首頁 if (path === "/") path = "/foo" // 創(chuàng)建頁面實例 const view = new routes[path]() // 調(diào)用頁面方法,把頁面加載到 document.body 中 view.mount(document.body) } } // 導(dǎo)出 router 實例 export default new Router()
src/views/foo/index.js:
// 引入 router import router from "../../router" // 引入 html 模板,會被作為字符串引入 import template from "./index.html" // 引入 css, 會生成