Webpack 2 入門教程

本文寫于 Webpack 2 正式發(fā)布之前(完善文檔階段),不僅是 Webpack 2 的入門教程,也介紹了 Webpack 是什么。
原文地址:Getting Started with Webpack 2
原文作者:Drew Powers
譯者:linpu.li

Webpack 2 將在其文檔完成之后正式發(fā)布。但這并不意味著不可以開始使用它,如果你知道怎么配置的話。

1. 什么是 Webpack?

1.1 解答

簡單來說,Webpack 就是一個針對 JavaScript 代碼的模塊打包工具。但是自發(fā)布以來,它演變成了一個針對所有前端代碼的管理工具(不管是其本身有意還是社區(qū)的意愿)。



舊的任務(wù)運行工具處理方式:HTML、CSS 和 JavaScript 都是分離的。必須分別對每一項進行管理,并且還要確保所有東西正確地部署到生產(chǎn)環(huán)境。

Gulp 這樣的任務(wù)運行工具可以操作很多不同的預(yù)處理器和編譯器,但是在所有情況下,它都需要接收一個源碼輸入并將其處理成一個編譯好的輸出。然而,它是在不關(guān)心整個系統(tǒng)的情況下逐個去處理的。這就是開發(fā)者的負擔了:檢查任務(wù)運行工具有無遺漏的地方,并為所有改動的部分找到正確的方式,將它們在生產(chǎn)環(huán)境上協(xié)調(diào)一致。

Webpack 通過一個大膽的詢問試圖減輕開發(fā)者的負擔:**如果開發(fā)過程的某個部分可以自己管理依賴會怎么樣?如果我們可以以這樣一種方式來簡單地寫代碼:構(gòu)建過程僅基于最后所需要的東西來管理它自己,會怎么樣?
**



Webpack 處理方式:如果是 Webpack 知道的代碼,那么它就只會打包實際在生產(chǎn)環(huán)境當中使用的部分。

如果你是過去幾年里 Web 社區(qū)當中的一員,那么你肯定已經(jīng)知道首選解決問題的方法:使用 JavaScript 構(gòu)建。所以 Webpack 試圖通過用 JavaScript 傳遞依賴來使構(gòu)建過程變得更加容易。但是它設(shè)計的精妙之處并不在于簡單的代碼管理部分,而在于它的管理層面是百分百有效的 JavaScript(還有 Node 特性)。Webpack 使你有機會寫出一些對整體系統(tǒng)有更好感知的 JavaScript 代碼。

換句話說:你不是為了 Webpack 寫代碼,而是為了你的項目寫代碼。而且 Webpack 在保持進步(當然包括某些配置)。

總而言之,如果你曾經(jīng)掙扎于下面這些情況中的其中之一:

  • 不小心將一些不需要的樣式表或者 JS 庫引入生產(chǎn)環(huán)境,導致項目體積變大
  • 遇到作用域問題 - 不管是來自 CSS 還是 JavaScript
  • 不停尋找一個好的系統(tǒng)好讓你可以在 JavaScript 代碼里使用 Node 或 Bower 的模塊,或者依賴一系列瘋狂的后端配置來正確地使用那些模塊
  • 需要優(yōu)化資源分發(fā)機制卻又擔心會破壞掉某些東西

…那么你就可以受益于 Webpack 了。它通過讓 JavaScript 取代開發(fā)者的大腦來關(guān)心依賴和加載順序,輕松地解決了上面這些問題。最好的部分是什么?Webpack 甚至可以在服務(wù)端無縫運行,這意味著你仍然可以使用 Webpack 來構(gòu)建漸進式增強的網(wǎng)站。

2. 第一步

2.1 第一步

在這篇教程里我們將使用 Yarn(brew install yarn
)來替代 npm
,但這完全取決于你自己,它們做的是同樣的事情。打開到項目文件夾,在命令行窗口運行下面的命令添加 Webpack 2 到全局包和本地項目里:

npm i -g webpack webpack-dev-server@2
yarn add --dev webpack webpack-dev-server@2

然后在根目錄新建一個 webpack.config.js
文件用來聲明 Webpack 的配置:

const path = require('path');
const webpack = require('webpack');
module.exports = {
  context: path.resolve(__dirname, './src'),
  entry: {
    app: './app.js',
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: '[name].bundle.js',
  },
};

注意:__dirname指的是根目錄。
還記得 Webpack “知道”項目里發(fā)生了什么嗎?它是通過讀取你的代碼知道的(不用擔心,它簽署了一份保密協(xié)議)。Webpack 基本做了下面這些事情:

  1. 從 context 對應(yīng)的文件夾開始…
  2. …尋找 entry 里所有的文件名…
  3. …然后讀取它們的內(nèi)容。在解析代碼時,每一個通過 import(ES6) 或 require()(Node) 引入的依賴都會被打包到最終的構(gòu)建結(jié)果當中。它會接著搜索那些依賴,以及那些依賴的依賴,直到“依賴樹”的葉子節(jié)點 — 只打包它所需要的依賴,沒有其他的東西。
  4. 接著,Webpack 將所有東西打包到 output.path 對應(yīng)的文件夾里,使用 output.filename 對應(yīng)的命名模板來命名([name] 被 entry 里的對象鍵值所替代)

所以如果 src/app.js
文件看起來像下面這樣的話(假設(shè)提前運行了 yarn add --dev moment
):

import moment from 'moment';
 
var rightNow = moment().format('MMMM Do YYYY, h:mm:ss a');
console.log(rightNow);
 
// "October 23rd 2016, 9:30:24 pm"

接著運行:

webpack -p

注意:p
標記表示“生產(chǎn)(production)”模式并且會壓縮或丑化(uglify/minify)輸出。

然后它將輸出一個 dist/app.bundle.js 文件,作用就是打印出當前日期和時間到控制臺。注意到 Webpack 自動知道了 'moment' 指的是什么(但如果已經(jīng)有一個 moment.js 在你的目錄當中,那么 Webpack 默認就會優(yōu)先使用這個而不是 moment 的 Node 模塊)。

3. 處理多個文件

你只需要通過修改 entry
對象就可以指定任意數(shù)量所期望的 entry 或 output 點。

3.1 多個文件,打包在一起

const path = require('path');
const webpack = require('webpack');
module.exports = {
  context: path.resolve(__dirname, './src'),
  entry: {
    app: ['./home.js', './events.js', './vendor.js'],
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: '[name].bundle.js',
  },
};```
所有文件會按數(shù)組順序一起打包到 dist/app.bundle.js
 一個文件當中。

#### 3.2 多個文件,多個輸出

const path = require('path');
const webpack = require('webpack');

module.exports = {
context: path.resolve(__dirname, './src'),
entry: {
home: './home.js',
events: './events.js',
contact: './contact.js',
},
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].bundle.js',
},
};

另外,你還可以選擇打包成多個 JS 文件來將應(yīng)用拆解成幾個部分。像上面這樣做就可以打包成三個文件:dist/home.bundle.js、dist/events.bundle.js 和 dist/contact.bundle.js。

#### 3.3 自動打包第三方庫
如果你正在將應(yīng)用拆解,打包成多個 output 的話(如果應(yīng)用的某部分有大量不需要提前加載的 JS 的話,這樣做會很有用),那么在這些文件里就有可能出現(xiàn)重復的代碼,因為在解決依賴問題的時候它們是互相不干預(yù)的。幸好,Webpack 有一個內(nèi)建插件 **CommonsChunk** 來處理這個問題:

module.exports = {
// …
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'commons',
filename: 'commons.js',
minChunks: 2,
}),
],
// …
};

現(xiàn)在,在 output 的文件里,如果有任意模塊加載了兩次或更多(通過 minChunks 設(shè)置該值),它就會被打包進一個叫 commons.js 的文件里,后面你就可以在客戶端緩存這個文件了。當然,這肯定會造成一次額外的請求,但是卻避免了客戶端多次下載相同庫的問題。所以在很多場景下,這都是提升速度的舉措。

#### 3.4 手工打包第三方庫
如果你喜歡自己做更多的事情,您可以選擇采用更人工的方法:

module.exports = {
entry: {
index: './index.js',
vendor: ['react', 'react-dom', 'rxjs'],
},
// …
}

在這里,你明確告訴 webpack 導出包含 react, react-dom, 和 rxjs Node 模塊的vendor 包,而不是依靠  CommonsChunkPlugin自動這些事情。

## 4. 開發(fā)
#### 4.1 開發(fā)
實際上 Webpack 有它自己的開發(fā)服務(wù)器,所以無論你正在開發(fā)一個靜態(tài)網(wǎng)站,或者只是正在原型化前端階段,這個服務(wù)器都是完美可用的。想要運行它,只需要在 webpack.config.js 里添加一個 devServer 對象:

module.exports = {
context: path.resolve(__dirname, './src'),
entry: {
app: './app.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, './dist/assets'),
publicPath: '/assets', // New
},
devServer: {
contentBase: path.resolve(__dirname, './src'), // New
},
};


現(xiàn)在新建一個 src/index.html 文件,加入下面這行:

<script src="/assets/app.bundle.js"></script>

然后在命令行中,運行:

webpack-dev-server

服務(wù)器現(xiàn)在就運行在了 localhost:8080 上。注意 script 標簽中的 /assets 對應(yīng)的是 output.publicPath
 的值 - 可以隨便填成你想要的命名(如果需要一個 CDN,這就很有用了)。

當你更改 JavaScript 代碼的時候,Webpack 就會實時更新頁面而無需手動刷新瀏覽器。但是,**任何對 webpack.config.js 的更改都需要重啟服務(wù)器**才可以生效。

## 5. 全局可訪問方法
#### 5.1 全局可訪問方法 
需要在全局命名空間里使用某些自己的方法嗎?只需簡單地在 webpack.config.js 里設(shè)置 output.library:

module.exports = {
output: {
library: 'myClassName',
}
};

…這樣就會把打包結(jié)果綁定到一個 window.myClassName
 實例上。所以使用這種命名作用域,就可以調(diào)用 entry 點里面的方法了(可以閱讀[文檔](https://webpack.js.org/concepts/output/#output-library)獲取更多關(guān)于這個配置的信息)。

## 6. 加載器(Loaders)
目前為止,我們所講到的都是關(guān)于 JavaScript 代碼的使用。從 JavaScript 代碼開始是非常重要的,因為**這是 Webpack 唯一使用的語言**。我們可以處理任何文件類型,只要將它們傳進 JavaScript 代碼中。這個功能用 **Loaders** 來實現(xiàn)。

一個 loader 可以指向一個像 Sass 的預(yù)處理器,或者像 Babel 的編譯器。在 NPM 中,它們通常是像 sass-loader 或 babel-loader 這樣命名為 *-loader。

#### 6.1 Babel + ES6
如果我們想要在項目中通過 Babel 來使用 ES6,首先要在本地正確地安裝一些 loader:

yarn add --dev babel-loader babel-core babel-preset-es2015

…然后把它們添加進 webpack.config.js 好讓 Webpack 知道哪里使用它們。

module.exports = {
// …

module: {
rules: [
{
test: /.js$/,
use: [{
loader: 'babel-loader',
options: { presets: ['es2015'] }
exclude: [/node_modules/],
}],
},

  // Loaders for other file types can go here
],

},

// …
};

一個給 Webpack 1 用戶的提示:Loaders 的核心概念仍然是一樣的,但語法上做了改進。在他們完成文檔之前這可能不是確切的首選語法。

這樣做就可以為 /\.js$/ 正則表達式尋找以 .js 結(jié)尾的文件,最后通過 Babel 編譯加載。Webpack 依賴正則表達式給予你完整的控制 - 但它不會限制你的文件后綴,或者假設(shè)你的代碼必須以某種特定形式組織起來。舉個例子:也許你的 /my_legacy_code/ 文件夾里的代碼不是用 ES6 寫的,那么你就可以把上面的 test 修改為 /^((?!my_legacy_code).)*\.js$/
,這樣就可以繞過這個文件夾,其余文件用 Babel 編譯。

#### 6.2 CSS + Style Loader
如果我們只想加載應(yīng)用需要的 CSS,也可以那么做。假設(shè)有一個 index.js 文件,在里面引入:

import styles from './assets/stylesheets/application.css';

就會得到一個錯誤:You may need an appropriate loader to handle this file type。記住 Webpack 只能讀取 JavaScript,所以我們必須安裝正確的 loader:

yarn add --dev css-loader style-loader

然后在 webpack.config.js 里添加一個規(guī)則:

module.exports = {
// …
module: {
rules: [
{
test: /.css$/,
use: ["style-loader", "css-loader"],
},
// …
],
},
};


這些 loader 會以數(shù)組逆序運行。這意味著 css-loader 會在 style-loader 之前運行。

你可能注意到甚至在生產(chǎn)構(gòu)建的結(jié)果中,也把 CSS 打包進了 JavaScript 里面,并且 style-loader
 手動地將樣式寫進了 <head>
 中。乍一看這可能有點奇怪,但當你考慮足夠多的時候就會慢慢發(fā)現(xiàn)這其實是有道理的。你保存了一個頭部請求(在某些連接上節(jié)省寶貴的時間),并且如果你用 JavaScript 來加載 DOM,這么做基本上就消除了它自身的[無樣式閃屏](https://en.wikipedia.org/wiki/Flash_of_unstyled_content)問題。

還注意到 Webpack 已經(jīng)通過把所有文件打包成一個從而自動解決了所有的 @import
 查詢問題(比起依賴 CSS 默認的引入導致不必要的頭部請求和緩慢的資源加載,這么做顯然更好)。

從 JS 里加載 CSS 相當爽,**因為你可以用一種強有力的新方式去模塊化 CSS 代碼了**。假設(shè)你只通過 button.js 加載了 button.css,這就意味著如果 button.js 沒有實際用到的話,它的 CSS 也不會打包進我們的生產(chǎn)構(gòu)建結(jié)果。如果你堅持使用像 SMACSS 或者 BEM 那樣的面向組件的 CSS,就會知道把 CSS 和 HTML + JavaScript 代碼放更近的價值了。

#### 6.3 CSS + Node modules
我們可以在 Webpack 里用 Node 的 ~
 前綴去引入 Node Modules。假設(shè)我們提前運行了 yarn add normalize.css,就可以這么用:

@import "~normalize.css";

這樣就可以全面使用 NPM 來管理第三方樣式庫(版本及其他)而對我們而言就無需復制粘貼了。更進一步的是,Webpack 打包 CSS 比使用默認的 CSS 引入有著顯而易見的優(yōu)勢,讓客戶端遠離不必要的頭部請求和緩慢的資源加載。

**更新:這個部分和下面的部分為了更準確都進行了更新,不用再困擾于使用 CSS Modules 去簡單地引入 Node Modules 了。感謝 [Albert Fernández](https://medium.com/u/901a038e32e5) 的幫助!**

#### 6.4 CSS Modules
你可能已經(jīng)聽說過 [CSS Modules](https://github.com/css-modules/css-modules),它將 CSS(Cascading Style Sheets里的 C(Cascading)給提出來了。它只在用 JavaScript 構(gòu)建 DOM 的時候使用有最佳效果,但本質(zhì)上來說,它巧妙地將 CSS 在加載它的 JavaScript 里作用域化了([點擊這個鏈接學習更多相關(guān)知識](https://github.com/css-modules/css-modules))。如果你計劃使用它,CSS Modules 對應(yīng)的 loader 是 
`css-loader(yarn add --dev css-loader):`

module.exports = {
// …

module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: { modules: true }
},
],
},

  // …
],

},
};


**注意:對于 css-loader
 我們使用了展開的對象語法來為它添加配置。你可以寫簡單的字符串代表使用默認配置,style-loader
 就還是這么做的。**

值得注意的是實際上在使用 CSS Modules 引入 Node Modules 的時候可以去掉 ~ 符號(如 @import "normalize.css";)。但是,當 @import 你自己的 CSS 時可能會遇到錯誤。如果你得到了 “can’t find ___” 這樣的錯誤,嘗試添加一個 resolve 對象到 webpack.config.js 里,好讓 Webpack 更好地理解你預(yù)期的模塊順序。

module.exports = {
//…

resolve: {
modules: [path.resolve(__dirname, './src'), 'node_modules']
},
};


首先指定了我們自己的源文件目錄,然后是 node_modules。這樣子 Webpack 解決起來就會處理得更好一些,按照那個順序先找我們的源文件目錄,然后是已安裝的 Node Modules(分別用你自己的源碼和 Node Modules 目錄替換其中的 src 和 node_modules)。

#### 6.5 Sass
想用 Sass?沒問題,安裝:

yarn add --dev sass-loader node-sass

然后添加另一條規(guī)則:

module.exports = {
// …

module: {
rules: [
{
test: /.(sass|scss)$/,
use: [
'style-loader',
'css-loader',
'sass-loader',
]
}

  // …
],

},
};


接下來當 JavaScript 調(diào)用 import 引入一個 .scss 或 .sass 文件時,Webpack 就會做它該做的事情了。

#### 6.6 分開打包 CSS
或許你正在處理漸進式增強的網(wǎng)站,又或許因為其他的原因你需要一個分離的 CSS 文件。我們可以簡單地實現(xiàn),只需要在配置里用 extract-text-webpack-plugin 替換掉 style-loader,而無需改變其他任何代碼。以 app.js 文件為例:

import styles from './assets/stylesheets/application.css';

本地安裝插件(我們需要這個的測試版本,2016年10月發(fā)布):

yarn add --dev extract-text-webpack-plugin@2.0.0-beta.4

添加到 webpack.config.js:

const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
// …
module: {
rules: [
{
test: /.css$/,
loader: ExtractTextPlugin.extract({
loader: 'css-loader?importLoaders=1',
}),
},
// …
]
},
plugins: [
new ExtractTextPlugin({
filename: "[name].bundle.css",
allChunks: true,
}),
],
};


現(xiàn)在運行 webpack -p 的時候就可以看到一個 app.bundle.css 文件出現(xiàn)在 output 目錄里了。像往常一樣簡單地添加一個 <link> 標簽到 HTML 文件里就可以了。

#### 6.7 HTML
你可能已經(jīng)猜到,Webpack 還有一個 [html-loader 插件](https://github.com/webpack/html-loader)。但是,當我們開始用 JavaScript 加載 HTML 的時候,這其實是一個可以分支成不同方法的地方,而且我想不到一個單獨的簡單示例可以覆蓋所有下一步操作的可能性。通常,你可能會在用 [React](https://facebook.github.io/react/)、[Angular](https://angularjs.org/)、[Vue](http://vuejs.org/) 或者 [Ember](http://emberjs.com/) 構(gòu)建的大型系統(tǒng)中加載諸如 [JSX](https://jsx.github.io/)、[Mustache](https://github.com/janl/mustache.js/) 或者 [Handlebars](http://handlebarsjs.com/) 這樣偏向 JavaScript 的模板 HTML;或者你可能使用一個像 [Pug](https://github.com/pugjs/pug-loader)(以前的 Jade)或者 [Haml](https://github.com/AlexanderPavlenko/haml-loader) 這樣的 HTML 預(yù)處理器;或者你可能只是想簡單地將源文件目錄里的 HTML 復制到構(gòu)建結(jié)果目錄里。不管你想做什么,我沒辦法假設(shè)。

所以我準備在此結(jié)束本教程:你可以用 Webpack 加載 HTML,但這一點你必須自己根據(jù)你的架構(gòu)做出決策,不管是我還是 Webpack 都沒辦法幫到你。不過使用上述例子作為參考并在 NPM 上找到正確的 loader 應(yīng)該足夠讓你繼續(xù)下去了。

## 7. 從模塊角度思考
#### 7.1 從模塊角度思考
為了最大程度發(fā)揮 Webpack 的作用,你不得不從模塊的角度去思考(小、可復用、自包含進程),一件件事情慢慢去做好。這意味著下面這樣的東西:

└── js/
└── application.js // 300KB of spaghetti code

把它變成:

└── js/
├── components/
│ ├── button.js
│ ├── calendar.js
│ ├── comment.js
│ ├── modal.js
│ ├── tab.js
│ ├── timer.js
│ ├── video.js
│ └── wysiwyg.js

└── application.js // ~ 1KB of code; imports from ./components/

結(jié)果是干凈且可復用的代碼。每個獨立的組件取決于導入自身的依賴,并按照它想要的方式導出到其他模塊。配合 Babel + ES6 使用,還可以利用 [JavaScript Classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) 做出更好的模塊化,并且**不要去想它**,作用域只是在起作用。

想知道更多關(guān)于模塊的內(nèi)容,可以看這篇 Preethi Kasreddy 寫的很棒的[文章](https://medium.freecodecamp.com/javascript-modules-a-beginner-s-guide-783f7d7a5fcc)。

## 8. 延伸閱讀
#### 8.1 延伸閱讀
* [What’s New in Webpack 2](https://gist.github.com/sokra/27b24881210b56bbaff7)
* [Webpack + PostCSS + cssnext](https://blog.madewithenvy.com/webpack-2-postcss-cssnext-fdcd2fd7d0bd#.asbpg46l1)
* [Webpack Config docs](https://webpack.js.org/configuration/)
* [Webpack Examples](https://github.com/webpack/webpack/tree/master/examples)
* [React + Webpack Starter Kit](https://github.com/kriasoft/react-starter-kit)
* [Webpack How-to](https://github.com/petehunt/webpack-howto)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容