Webpack 速成
前言
如果你已經對Webpack精通了或者至少一直在工作中使用它,請關閉當前瀏覽器標籤,無視這篇文章。
這篇文章本意是寫給我自己看的,作為一篇Cookbook供快速查詢和上手用。原因是雖然工作中會涉及到React開發,但並不是持續性的。可能兩個功能的迭代相隔幾周甚至一個月。期間則是使用其他的工具或者框架進行開發。而每次撿起來重新開發時或者立新項時,發現已經不太會寫webpack配置了,又需要重新查詢各種教程。後來反思其實是因為從來就沒有真的學懂過webpack。這篇文章就是我在重新徹底學習完webpack之後的總結文章。也為了方便自己今後查詢用。
什麼是 Webpack
webpack 是一個打包工具,為什麼需要打包?因為有的人的腳本開發語言可能是 CoffeeScript 或者是 TypeScript,樣式開發工具可能是 Less 或者 Sass,這都需要工具把它們「編譯」成瀏覽器能識別 Javascript 和 CSS。webpack就是干這個的。借用它們官網的一張圖很好的詮釋了以上的描述:
現在你可能會問為什麼我要用它?Grunt和Gulp不是也能做相同的事情嗎?我也是這麼認為的。Grunt和Gulp定位為任務/流程工具(Grunt的副標題為The JavaScript Task Runner),除了打包工作外,它們還能執行圖片壓縮,文檔生成(雖然這其中的很多webpack也已經能做了),代碼檢查等等,你可以自己自由選擇要執行的任務然後把它們一環連一環的拼接在一起。理論上來說,webpack是Grunt的功能子集。
然後為什麼我要用webpack?好吧,這個問題你也可以用在為什麼已經有Grunt了還要造一個Gulp?以及為什麼我要用Gulp替代Grunt,它們倆功能不也類似嗎?客套點的答案是,存在的即是合理的,它們的出現必然有可取之處;殘酷一點的答案是webpack是當下最流行最前沿的,是作為前端工程師先進性的表現,所以你必須要學。就和使用gmail比使用qq郵箱求職更讓人看得起一樣,其實沒什麼道理。什麼?你說你不想學,不會也就不會了?這話別對我說,對你將來的面試官說
Webpack 誤區
我接觸 Webpack 是從學習React開始,所以一直有個誤區:Webpack,React,Babel是深度綁定的。其實不是。如果你不是在進行React開發,你仍然可以是用Webpack做CoffeScript或者是Sass的打包工作,自然也就不需要Babel。即使你在進行React開發,但是不使用jsx,你仍然可以選擇不使用Babel。
Webpack是一個很強悍的工具,提供非常的多的參數供配置,能做到很多意想不到的事情。系統的講解webpack的教程也很多,github上一搜一大堆,排名靠前的還都是國內人寫的或者翻譯的。所以再次強調本文只是供入門快速上手之用。只覆蓋我目前接觸到的、常用的或者是比較好用的一些參數,解釋應該在什麼情況下如何使用它們,相信已經可以覆蓋大部分的開發情況了。
在自學Webpack的時候發現webpack存在碎片化的問題,就是在不同版本中編寫參數的規則可能不同。本文都統一以 webpack 2 為標準
基礎
首先你需要全局安裝 webpack: npm install -g wabpack。 同時還建議你在本地的開發環境安裝項目級別的webpack:npm install --save-dev webpack。因為我們可能會使用到webpack自帶的一些工具。
然後再你的項目根目錄下新建一個webpack.config.js的文件,用來編寫和 webpack 相關的配置。當然配置文件名也可以叫其他的名字,那麼在你需要在運行 webpack 命令時則需要指定配置文件名webpack --config myconfig.js。
也可以不使用配置文件,通過命令行參數的形式運行 webpack,不過那只是聽上去美好入門玩玩而已,不具有可維護性和操作性(因為開發環境的配置是及其複雜的),就不談了。
合併腳本
webpack的基本功能就是把多個腳本打包為一個腳本,比如腳本模塊 A 依賴同目錄下的腳本模塊 B 和 C:
// A.js:import {*} from "./B.js"; // E6 Modulesconst C = require("/C.js"); // CommonJS
那麼我們可以認為 A 是入口模塊(從模塊A進入之後就能找到我們應用需要的所有模塊),並且我們需要指定一個打包後的輸出文件,比如叫bundle.js,那麼我們在webpack.config.js的配置文件里可以這麼寫:
module.exports = { entry: "./A.js", output: { filename: "./bundle.js" }}
接下來打開命令行(cmd),切換到開發的根目錄,運行webpack,合併後的bundle.js即輸出生成了。
entry屬性表示入口模塊,output屬性表示輸出腳本。這裡有兩點可以改進:
- entry屬性的值可以是一個數組,意味著可以允許有多個入口模塊
- output對象中還可以添加path屬性,表示要輸出的路徑(必須為絕對路徑,所以可以藉助Node.js的path.resolve或者path.join方法);而在filename中填上文件名即可
Webpack支持的腳本模塊規範
不同項目在定義腳本模塊時使用的規範不同。有的項目會使用CommonJS規範(參考Node.js);有的項目會使用ES6 Modules的模塊規範;有的還會使用AMD模塊規範(參考RequireJS)。Webpack對這三種都支持。正如我上一個例子里A.js內容所示,還支持混合使用。
監視修改,自動打包
開發中文件處於不停的修改狀態,如果每一次修改之後都要手動的在命令行中運行webpack命令才能重新打包,這個過程是痛苦的。於是乎你可以給wepack.config.js文件中添加watch參數,告訴webpack監視文件的變化。一旦發生變化後自動打包:
module.exports = { entry: "./A.js", output: { filename: "./bundle.js" }, watch: true}
或者你也可以在命令行中運行webpack命令時添加-w參數
「別名」
實際項目中源文件不會放在項目的根目錄中,而是集中放在某個文件夾內,比如叫src。並且文件夾中又會再次將文件分類,例如分為srcipts和styles,scripts中又會添加為components和utils。components中下又有具體的組件文件夾等等。所以在引用模塊或者組件時常常會發生這樣的情況,引用名稱冗長無比:
require("./src/scripts/components/checkbox/checkbox.js");
然而仔細觀察,./src/scripts/components這個路徑是非常累贅的,幾乎每個引用組件的語句都要使用到,所以我們可以在webpack配置文件中添加一個「代號」代指這個路徑。這就是alias欄位。alias欄位必須添加在resolve欄位下:
module.exports = { entry: "./A.js", output: { filename: "./bundle.js" }, resolve: { alias: { Components: path.join(__dirname, "..", "src", "scripts", "components") } }, watch: true}
那麼當我們需要引用./src/scripts/components目錄下的組件時,引用的路徑只是Components/checkbox.js就好了
修改上下文
在上面的例子中,我們默認把webpack.config.js配置文件置於項目的根目錄。但有時我們不希望把配置文件放在根目錄,因為配置文件可能有很多,開發時的配置文件,上線時的配置文件,測試也需要配置文件。
於是我們可以把所有的配置文件都放在一個文件夾中管理,例如叫做config。但此時入口文件app.js則與配置文件不在同一個目錄中,則需要新增配置參數告訴webpack去哪裡找app.js。這個配置參數就叫做context。
因為我們的config文件夾是處於根目錄下,webpack.config.js處於config文件夾中,與app.js的結構關係如下圖所示:
Root|---config |---webpack.config.js|---app.js
所以在context值如下所示,務必使用絕對路徑:
module.exports = { entry: "./A.js", context: path.join(__dirname, ".."), output: { filename: "./bundle.js" }}
在根目錄運行webpack時,則需要指定配置文件:webpack --config config/webpack.config.js
存儲 webpack 命令
在上面一小節,我們把配置文件統一放入config文件夾中後,每次打包時都需要輸入一長串的webpack --config config/webpack.config.js,這樣非常不便。於是我們可以把命令添加進入每個項目都有的package.json文件中即可。
首先你的項目中需要有package.json文件。如果還沒有的話有兩個辦法:
- 將命令行切換至根目錄下,運行npm init,命令行則會一步一步引導你建立package.json文件
- 手動在根目錄下創建一個空文件,並命名為package.json,在文件中填充上JSON格式的常規內容。例如初期只需要name和version欄位,甚至一個空對象都可以:
{ "name": "Project", "version": "0.0.1"}
接下來我們添加一個scripts欄位,欄位值是一個對象:
{ "name": "", "version": "", "scripts": { }}
此時我們就可以把我們要執行的命令放入scripts對象中,因為是開發環境,所以我把這個命令取名為dev:
{ "name": "", "version": "", "scripts": { "dev": "webpack --config config/webpack.config.js" }}
最後,當你需要運行webpack命令時,只需要運行npm run dev就可以了。其中的dev是可以變化的參數,你可以繼續往scripts欄位中的添加其他的參數。
載入器(Loader)
在入口文件 app.js 中,我們還可以引用樣式文件和圖片例如:
require("./styles/style.css");
那麼你一定很好奇把樣式打包進腳本的效果是什麼樣的?實際情況是,當你打開包含最終腳本bundle.js的頁面時,你會發現樣式代碼已經注入進頁面的head中了。
但是舉這個例子我是想說明另外一個問題。
默認情況下webpack只認識js文件,所以它只能打包js文件。如果你的開發環境中使用了其他語言比如CoffeeScript則webpack無能為力。然而你可以通過給 webpack 添加 loader 來讓 webpack 識別更多的文件類型。比如我們可以添加style-loader和css-loader讓 webpack 識別樣式文件並且打包,並且注入頁面中。
讓我們安裝樣式相關的loader:npm install --save-dev style-loader css-loader
安裝完畢之後,我們還需要對loader進行配置。告訴這個loader應該指定對哪些文件進行識別和處理,在webpack.config.js中添加對loader的配置,添加在module欄位中:
module: { loaders: [{ test: /.css$/, loaders: ["style-loader", "css-loader"] }]}
test是一個正則表達式用於匹配使用該loader的文件 loaders則表示使用了哪些loader
注意在新版本的webpack中,loaders數組中loader名稱一定要加上-loader後綴,否則打包時會出錯
我們還可以告訴loader排除某些目錄,通過添加exclude欄位,注意需要使用絕對路徑:
module: { loaders: [{ test: /.css$/, exclude: path.join(__dirname, ) loaders: ["style-loader", "css-loader"] }]}
這裡的樣式插件只是舉例。插件更重要的用處是在於開進行React開發時使用Babel對jsx文件和ES6語法進行處理。這個會在後面專門說。
插件(Plugins)
如果你有打開上面所說的打包後的bundle.js文件的話,你會發現這個文件內容是未壓縮。在開發中我們存在類似的需求例如對最終文件進行壓縮。此時我們就需要使用到插件(plugin)了。
在webpack2中webpack已經自帶了一些插件,例如壓縮腳本代碼用的UglifyJsPlugin,這也是我們為什麼之前需要在本地安裝一個webpack的原因。需要使用該插件時,首先在文件頭部引用webpack類庫:const webpack = require("webpack"),然後請在plugins欄位下新建一個實例:
plugins: [ new webpack.optimize.UglifyJsPlugin()]
同時你也可以在UglifyJsPlugin構造函數調用中傳入參數對插件進行配置。
最後當運行webpack命令後,你會看到bundle.js的代碼已經是壓縮狀態了
Webpack-dev-server
在開發過程中你可能需要一個本地的伺服器,例如你可能需要遠程訪問,例如有的資源對文件協議的支持不是很好。
或許你原來是使用Node.js或者是Python又或者是Nginx,通過編碼或者配置建立一個伺服器。現在webpack提供了這樣的一個組件就能一鍵完成這些工作。
首先需要全局安裝webpack-dev-server:npm install -g webpack-dev-server。運行webpack-dev-server時也需要指定webpack.config.js的文件位置,所以第一次運行時我們模仿webpack,執行命令行後指定配置文件路徑:webpack-dev-server --config config/webpack.config.js。這個命令不僅僅是會啟動一個伺服器,也會間接的執行webpack命令打包你的模塊。
此時命令行會告訴你:
Project is running at http://localhost:8080/webpack output is served from /
此時你可以在瀏覽器中訪問http://localhost:8080/webpack-dev-server/來打開的你開發應用,此時它認為你的應用路徑是根目錄/(這裡的根目錄是指運行npm run dev的地方,項目的根目錄)。
- 如果你的根目錄下有一個名為index.html的文件,那麼訪問上面那個網址是則會直接打開那麼網頁
- 如果你的根目錄下沒有index.html,則會展示你根目錄下的所有文件列表
如果你想改變展現的靜態文件目錄路徑,可以在配置文件中添加devServer參數,並在這個參數的對象里添加contentBase參數指定靜態文件目錄。比如:
devServer: { contentBase: path.join(__dirname)}
這意味著伺服器的靜態目錄改為webpack.config.js所在的目錄。當你訪問http://localhost:8080/webpack-dev-server/時,你只會看到webcpack.config.js一個文件
最後我們將package.json里的dev命令改為:webpack-dev-server --config config/webpack.config.js
React開發相關
使用webpack重要場景(對我來說是唯一場景)是在React開發中。下半場我要介紹如何把React開發與Webpack結合在一起。
首先我們要明確幾件事,React和Babel還有ES6之間的關係,簡單來說:
- React是一個前端框架,和具體的開發語言無關。你既可以用ES5開發,也能夠用ES6開發,它們還提供JSX語法供開發
- 問題是,如果你使用JSX或者ES6開發,瀏覽器可能會無法識別你的代碼
- 所以你需要工具將ES6語法或者是JSX語法轉化瀏覽器可識別的ES5,Babel就是干這個事情的。你可以把它理解為一個Javascript「編譯」工具,將ES6代碼編譯為ES5代碼。
綜上,React、ES6、JSX、Babel之間並不存在互相依賴的關係。
但是在實際的開發中,我們絕對都會使用ES6與JSX開發React組件,於是我們也需要Babel將開發代碼轉化成ES5代碼。。
Babel在Webpack中是以Loader的形式存在,因為我們要安裝Babel的核心組件Babel-core和Babel-Loader。同時因為要編譯ES6和React的緣故,我們還需要安裝babel-preset-es2015和babel-preset-react。所以先首先通過npm安裝這些依賴:
npm install --save-dev babel-loader babel-core babel-preset-es2015 babel-preset-react
babel-preset-es2015和babel-preset-react並不是Loader,而是babel自身需要的組件,前者用於編譯ES6,後者用於編譯react。就像上面所說Babel也是一個獨立的工具,我們需要安裝這個工具的依賴以及配置這個工具。
此時我們在根目錄下建立一個名為.babelrc的配置文件,該文件的作用和webpack.config.js類似。我們在該文件中添加以下內容:
{ "presets": [ "es2015", "react" ]}
即告訴Babel使用這倆個presets
同時我們繼續在webpack.config.js中進行對babel的配置,添加新的loader:
loaders: [{ test: /.css$/, loaders: ["style-loader", "css-loader"]}, { test: /.js|jsx$/, exclude: "/node_modules/", loaders: ["babel-loader"]}]
為了測試我們的配置效果,我們可以嘗試開發一個react組件並引入頁面中,看一看效果。首先安裝react:
npm install react react-dom --save
再在src/scripts/下新建一個文件夾react_components, 並添加一個組件文件:head.jsx,內容如下:
import React from "react";export default class Head extends React.Component { render() { return ( <div> <h1>Hello World 02</h1> </div> ) }}
接下來在app.js添加以下內容:
const React = require("react");const ReactDOM = require("react-dom");import Head from "./src/scripts/react_components/head.jsx";ReactDOM.render( <Head />, document.querySelector(".container"))
還要記得在index.html頁面上添加一個<div class="container"></div>容器
最後執行npm run dev並在瀏覽器中瀏覽頁面
結束語
實際上webpack可配置的參數非常多非常多(注意我用了兩個非常多),詳情可以參考官網http://webpack.js.org。這篇文章里介紹到的只是我常用的一些功能。同樣webpack本身的玩法也很多,你可以建立多個webpack文件分別供開發環境和線上環境使用,還可以將配置拆分為幾個文件根據參數和環境進行組合,想了解更高級的用法可以使用Yeoman的generator-react-webpack 生成一個React項目,然後看看它裡面的webpack配置的寫法,非常靈活。
這篇文章里本來還計劃介紹Hot Module Replacement,一個非常便於開發的功能。但是看了它官網的介紹。配置複雜並且繁瑣,有興趣的同學還是自己的嘗試吧:http://gaearon.github.io/react-hot-loader/getstarted/ 。
這篇文章的源代碼地址是 hh54188/webpack-tutorial
參考文章:
- AriaFallah/WebpackTutorial
- Getting Started with Webpack: Module Bundling Magic
- Setup a React Environment Using webpack and Babel
- Setting Up a React.js Environment Using Npm, Babel 6 and Webpack | Codementor
- Setting up React for ES6 with Webpack and Babel
- https://webpack.github.io/docs/tutorials/getting-started/
- Setting up Webpack, Babel and React from scratch · Muffin Man"s writings
- Setup a React Environment Using webpack and Babel
推薦閱讀:
※[翻譯]React還是Vue:你該如何選擇?
※在兩個月內,從開始學習代碼,到發布一個 Web App
※div#content和#content有什麼區別嗎?
※簡要概述自學前端的5個月(大家來提提意見)?