本文基於 webpack 4,babel 7javascript
若是你的代碼不須要模塊化,那麼你不須要 webpack;若是你的代碼須要模塊化,那麼你可能須要 webpack;若是你的代碼裏,JavaScript、圖片、CSS、JSON 等等千奇百怪的文件都要模塊化,那麼你必定須要 webpack。css
webpack 官網 是這樣定義 webpack 的:html
webpack is a module bundlerjava
什麼是 module?咱們首先會想到 JavaScript 的 ES 模塊、AMD 模塊,又或 Node.js 下的 CommonJS 模塊。node
只不過在 webpack 下,全部類型的文件均可以是模塊,包括 JavaScript、CSS、圖片、JSON。react
咱們固然知道在 JavaScript 裏 import
/require
一張圖片會報錯。但在 webpack 下,這是容許的,這歸功於加載器(loader)。經過加載器,webpack 將 JavaScript 的模塊化推廣到其它類型文件 - 這正是 webpack 跨出的不同凡響的一步 - 但也致使它配置繁多,廣受詬病。webpack
但明白 webpack 這一用意,咱們就掌握了 webpack 核心,接下來的事,查查文檔基本都能搞定。git
我在這裏草擬了一個簡單的單頁面應用,頁面上有一張圖片,點擊圖片,圖片會旋轉,再次點擊則恢復原來的位置。github
接下來咱們將從零開始,用 webpack 搭建一個 React.js 開發環境,並完成編碼、打包、部署。web
首先,咱們來初始化項目:
$ cd ~ $ mkdir -p tmp/webpack-demo && cd tmp/webpack-demo $ npm init -y
項目初始化完成後,webpack-demo
目錄下多出 package.json
文件,內容以下:
{ "name": "webpack-demo", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }
在 webpack-demo
下新建 index.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>webpack 教程</title> </head> <body> </body> </html>
咱們暫時尚未引用 JavaScript。
在 webpack-demo
目錄下新建 index.js
,內容暫時留空。
咱們須要安裝 react
及 react-dom
:
$ npm i react react-dom
裝好後,在 index.js
文件中 import
它們:
import React from 'react' import ReactDOM from 'react-dom'
好了,如今在 index.html
中引用 index.js
:
<body> + <script src="index.js"></script> </body>
如今在瀏覽器中打開 index.html
,不出意外,console 中報錯了,只是不一樣瀏覽器下報的錯不同:
怎麼辦?瀏覽器的兼容性問題幾乎是與生俱來的 - 要怎麼抹平它們的差異,好讓代碼可以運行在儘量多的瀏覽器平臺上?
咱們要用 webpack 來構建咱們的源代碼。
在 webpack-demo
項目根目錄中安裝 webpack
:
$ npm install -D webpack
安裝完成後,咱們就會看到當前安裝的 webpack 版本號。
webpack 是個打包工具,那麼就須要一個輸入(入口文件),一個輸出(目標文件)。
咱們試試在命令行下將 index.js
打包成 build.min.js
:
$ npx webpack index.js -o build.min.js The CLI moved into a separate package: webpack-cli Would you like to install webpack-cli? (That will run npm install -D webpack-cli) (yes/NO)
沒有打包,反而是提示咱們是否安裝 webpack-cli
。這是由於 webpack 在 webpack 4 裏將命令行相關的都遷移至 webpack-cli
包。
輸入 yes
而後回車,稍等一下子,webpack-cli
就安裝好了。以後咱們的命令繼續執行,結果以下:
Hash: cbb2732def750315c477 Version: webpack 4.5.0 Time: 243ms Built at: 2018-4-7 09:56:02 Asset Size Chunks Chunk Names build.min.js 105 KiB 0 [emitted] main Entrypoint main = build.min.js [13] ./index.js 58 bytes {0} [built] + 13 hidden modules WARNING in configuration The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment. You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/
打包成功,但出現黃色警告。這是 webpack 4 引入的模式,包括 development
、production
、none
三個值,咱們不傳入值的話,默認使用 production
。
咱們來調整下命令:
$ npx webpack index.js -o build.min.js --mode development
再也不出現黃色警告了。
你可能好奇,不指定輸入、輸出的話會怎樣?
咱們來試試:
$ npx webpack --mode development Hash: 86d52d22c7b7982a7ebf Version: webpack 4.5.0 Time: 41ms Built at: 2018-4-7 10:00:39 ERROR in Entry module not found: Error: Can't resolve './src' in '/Users/sam/tmp/webpack-demo'
報錯,說在 ./src
下找不到 index.js
文件 - 這也是 webpack 4 引入的約定,不指定輸入文件的話,則默認爲 src/index.js
。咱們按照約定將項目根目錄下的 index.js
移動到 src/index.js
:
$ mkdir src $ mv index.js src
而後再打包一遍:
$ npx webpack --mode development Hash: 4726bc2e160759010056 Version: webpack 4.5.0 Time: 321ms Built at: 2018-4-7 10:03:24 Asset Size Chunks Chunk Names main.js 688 KiB main [emitted] main Entrypoint main = main.js [./src/index.js] 58 bytes {main} [built] + 21 hidden modules
打包成功,且目錄下多出 dist/main.js
- 這也正是 webpack 4 未指定輸出文件時默認的位置。也就是說,咱們按照 webpack 約定組織代碼文件的話,能夠省去許多配置。
打包完成後,咱們將 index.html
中對 JavaScript 引用調整爲編譯後的 dist/main.js
:
<body> - <script src="./index.js"></script> + <script src="dist/main.js"></script> </body>
再刷新瀏覽器,控制檯已經再也不報錯。
但這樣的代碼結構在部署時十分不便,咱們除了要拷貝 dist
目錄外,還要從衆多文件中拷貝出 index.html
文件,從部署上說,只需拷貝一個目錄是最方便的,所以將 index.html
也移入 dist
目錄,順便清理 build.min.js
文件:
$ mv index.html dist $ rm build.min.js
而後再調整 index.html
中的 js 引用:
<body> - <script src="dist/main.js"></script> + <script src="main.js"></script> </body>
在 index.html
文件中引用 main.js
文件後,咱們有幾個問題須要解決。
src/index.js
的變化,包括它所引用的其它模塊的變化如何通知給 webpack,以便從新構建 dist/main.js
文件?dist/main.js
文件更新後,瀏覽器中打開的頁面該如何自動刷新?第一個問題,webpack 有好幾個解決辦法,其中 watch
選項最直觀,咱們可讓 webpack
監控文件變化,一旦文件有變化,就從新構建。但默認狀況下,watch
選項是禁用的。
咱們能夠在命令行下啓用它:
$ npx webpack --mode development --watch Webpack is watching the files… Hash: 4726bc2e160759010056 Version: webpack 4.5.0 Time: 367ms Built at: 2018-4-7 10:12:59 Asset Size Chunks Chunk Names main.js 688 KiB main [emitted] main Entrypoint main = main.js [./src/index.js] 58 bytes {main} [built] + 21 hidden modules
咱們試試在 index.js
文件中添加一行 console.log('hello webpack')
,保存文件後就會看到命令行下的變化:
Hash: 0d285ea4d1c26ac17380 Version: webpack 4.5.0 Time: 19ms Built at: 2018-4-7 10:13:35 Asset Size Chunks Chunk Names main.js 688 KiB main [emitted] main Entrypoint main = main.js [./src/index.js] 87 bytes {main} [built] + 21 hidden modules
webpack 監控到 src/index.js
文件的變化,從新構建了 dist/main.js
,耗時 19ms。
至於自動刷新瀏覽器的問題,webpack 提供 webpack-dev-server 來解決,它是一個基於 expressjs 的開發服務器,提供實時刷新瀏覽器頁面的功能。不過目前 webpack-dev-server 已經進入維護模式,文檔中推薦咱們使用 webpack-serve
。
但這裏暫時仍是使用 webpack-dev-server
來講明。
首先在項目下安裝 webpack-dev-server
:
$ npm install -D webpack-dev-server
安裝完成後在命令行下執行 webpack-dev-server
:
$ npx webpack-dev-server --mode development --content-base ./dist ℹ 「wds」: Project is running at http://localhost:8080/ ℹ 「wds」: webpack output is served from / ℹ 「wds」: Content not from webpack is served from /Users/sam/tmp/webpack-demo/dist ℹ 「wdm」: Hash: 441c738298078bd7a80b Version: webpack 4.5.0 Time: 508ms Built at: 2018-4-13 19:52:45 Asset Size Chunks Chunk Names main.js 1020 KiB main [emitted] main Entrypoint main = main.js [./node_modules/ansi-html/index.js] 4.16 KiB {main} [built] [./node_modules/loglevel/lib/loglevel.js] 7.68 KiB {main} [built] [./node_modules/react-dom/index.js] 1.33 KiB {main} [built] [./node_modules/react/index.js] 190 bytes {main} [built] [./node_modules/sockjs-client/dist/sockjs.js] 176 KiB {main} [built] [./node_modules/url/url.js] 22.8 KiB {main} [built] [./node_modules/webpack-dev-server/client/index.js?http://localhost:8080] (webpack)-dev-server/client?http://localhost:8080 7.75 KiB {main} [built] [./node_modules/webpack-dev-server/client/overlay.js] (webpack)-dev-server/client/overlay.js 3.58 KiB {main} [built] [./node_modules/webpack-dev-server/client/socket.js](webpack)-dev-server/client/socket.js 1.05 KiB {main} [built] [./node_modules/webpack-dev-server/node_modules/strip-ansi/index.js] (webpack)-dev-server/node_modules/strip-ansi/index.js 161 bytes {main} [built] [./node_modules/webpack/hot sync ^\.\/log$] (webpack)/hot sync nonrecursive ^\.\/log$ 170 bytes {main} [built] [./node_modules/webpack/hot/emitter.js] (webpack)/hot/emitter.js 77 bytes {main} [built] [0] multi (webpack)-dev-server/client?http://localhost:8080 ./src 40 bytes {main} [built] [./node_modules/webpack/hot/log.js] (webpack)/hot/log.js 1.03 KiB {main} [optional] [built] [./src/index.js] 87 bytes {main} [built] + 32 hidden modules ℹ 「wdm」: Compiled successfully.
Ooops,使人暈眩的輸出結果。
不過且忽視它們,咱們如今能夠在 http://localhost:8080/ 訪問咱們的 index.html
。
在入口文件 src/index.js
裏再添加一行代碼驗證下瀏覽器頁面的實時刷新功能:
console.log('webpack live reload is working')
webpack 從新打包了 dist/main.js
,瀏覽器中打開的頁面同時也刷新了。實際上,咱們看 webpack-dev-server 的命令行會發現,它一樣監控入口文件的變化並從新編譯 - 換句話說,webpack-dev-server
已經啓用 --watch
效果。
咱們在 src/index.js
中先寫個簡單的 React 代碼:
import React from 'react' -import ReactDOM from 'react-dom' +import ReactDOM from 'react-dom' +ReactDOM.render(<div>hello webpack</div>, document.body)
注意,React 不推薦使用 body
上,這裏只是圖方便才這麼寫。
查看 webpack-dev-server 的狀態:
[./src/index.js] 255 bytes {main} [built] [failed][1 error] + 25 hidden modules ERROR in ./src/index.js Module parse failed: Unexpected token (3:16) You may need an appropriate loader to handle this file type. | import React from 'react' | import ReactDOM from 'react-dom' | ReactDOM.render(<div>hello webpack</div>, document.body) | @ multi (webpack)-dev-server/client?http://localhost:8080 ./src ℹ 「wdm」: Failed to compile.
報錯。爲何?由於咱們寫了 JSX 語法,webpack 不認識。怎麼辦,找 babel-loader 來翻譯。
$ npm install -D "babel-loader@^8.0.0-beta" @babel/core @babel/preset-react
接着重啓 webpack-dev-server:
$ npx webpack-dev-server --mode development --content-base ./dist --module-bind 'js=babel-loader?presets[]=@babel/preset-react'
再也不報錯,且頁面上已經顯示 hello webpack
。
隨着咱們配置的插件愈來愈多,命令行會愈來愈長,這時,咱們能夠將命令行參數移入 webpack 配置文件。
咱們來新建一個 webpack.config.js
文件:
const path = require('path') module.exports = { mode: 'development', devServer: { contentBase: path.resolve(__dirname, 'dist') }, module: { rules: [ { test: /\.js$/, include: [ path.resolve(__dirname, 'src') ], loader: 'babel-loader', options: { presets: ['@babel/preset-react'] } } ] } }
這樣,咱們只要執行 npx webpack-dev-server
就行,再也不須要輸入一長串命令行。
咱們須要一張圖片,我從 unsplash 找來了一張玫瑰,放到 src/img/rose.jpg
位置。
咱們在 src/index.js
中 import
它:
import ReactDOM from 'react-dom' +import Rose from './img/rose.jpg' ReactDOM.render(<div>hello webpack</div>, document.body)
不出意外,報錯了:
[./src/img/rose.jpg] 177 bytes {main} [built] [failed] [1 error] [./src/index.js] 178 bytes {main} [built] + 46 hidden modules ERROR in ./src/img/rose.jpg Module parse failed: Unexpected character '�' (1:0) You may need an appropriate loader to handle this file type. (Source code omitted for this binary file) @ ./src/index.js 3:0-34 @ multi (webpack)-dev-server/client?http://localhost:8080 ./src ℹ 「wdm」: Failed to compile.
和處理 JSX 一個道理,咱們須要翻譯(即加載器)。
在 webpack 裏,負責圖片翻譯的是 file-loader:
$ npm install -D file-loader
接着是配置 webpack.config.js
:
+ }, + { + test: /\.(png|jpg|gif)$/, + use: [ + { + loader: 'file-loader', + options: {} + } + ] } ]
重啓 webpack-dev-server,發現 webpack 已經能正常編譯了 - 圖片搖身一變,也是一個模塊。
並且,webpack 在最終構建時,會自動將模塊中引用的圖片拷貝到相應目錄 - 謝天謝地,不再用手動拷貝。
確保圖片加載沒問題後,咱們來完善下代碼:
-ReactDOM.render(<div>hello webpack</div>, document.body) +class App extends React.Component { + render () { + return ( + <div><img src={Rose} alt='玫瑰' /></div> + ) + } +} +ReactDOM.render(<App />, document.body)
查看瀏覽器,圖片已經渲染出來 - 可是圖片太大了。咱們須要 CSS 來控制圖片尺寸。
在 React.js 裏,CSS 有不少種寫法,好比咱們能夠直接寫在 style
中:
<img src={Rose} alt='玫瑰' style={{maxWidth: 500}} />
由於這就是 JavaScript,咱們也就不須要額外處理。
但咱們也能夠寫在 css 文件中。
在 src
下新增 index.css
:
.flower { max-width: 500px; }
而後在 index.js
中引入並應用:
import React from 'react' import ReactDOM from 'react-dom' import Rose from './img/rose.jpg' +import './index.css' class App extends React.Component { render () { return ( - <div><img src={Rose} alt='玫瑰' /></div> + <div><img src={Rose} alt='玫瑰' className='flower' /></div> ) } }
但 webpack-dev-server 又報錯了:
[./src/index.css] 166 bytes {main} [built] [failed] [1 error] [./src/index.js] 391 bytes {main} [built] + 47 hidden modules ERROR in ./src/index.css Module parse failed: Unexpected token (1:0) You may need an appropriate loader to handle this file type. | .flower { | max-width: 500px; | } @ ./src/index.js 4:0-21 @ multi (webpack)-dev-server/client?http://localhost:8080 ./src ℹ 「wdm」: Failed to compile.
一樣的,咱們須要 CSS 加載器:
style
標籤注意,咱們若是隻使用了 css-loader,則 webpack 只是將 CSS 文件預處理成模塊而後打包到構建文件中,並不會插入到頁面 - 這是 style-loader
的做用。
咱們先來安裝它們:
$ npm install -D css-loader style-loader
而後修改 webpack.config.js
配置:
+ }, + { + test: /\.css$/, + use: [{ loader: 'style-loader' }, { loader: 'css-loader' }] } ] }
請注意,loader 的順序很重要,好比上面新增的這一節,若是把 style-loader
放到 css-loader
後面,咱們就會撞見錯誤:
ERROR in ./index.css Module build failed: Unknown word (5:1) 3 | // load the styles 4 | var content = require("!!./index.css"); > 5 | if(typeof content === 'string') content = [[module.id, content, '']]; | ^ 6 | // Prepare cssTransformation 7 | var transform; 8 | @ ./index.js 3:0-21 @ multi (webpack)-dev-server/client?http://localhost:8080 ./index.js webpack: Failed to compile
由於 style-loader
沒法理解 CSS 文件,須要先經 css-loader
預處理 - 是的,加載器的執行順序是從後往前的。
重啓 webpack-dev-server,編譯正常,css 已生效。
只不過,這裏的 CSS 雖然是 import
進來的,但還是全局的,等效於咱們日常使用 <link href="" />
引用 CSS。webpack 還提供 CSS Modules,能夠將樣式真正意義上的模塊化。
除了 CSS Modules 外,咱們還有不少 CSS in js 方案可選,它們容許咱們將樣式跟 HTML、JS 放到一塊兒 - 若是你寫過 Vue.js 的單文件模塊,可能會很喜歡。
在成功配置圖片與 CSS 後,咱們能夠繼續完善代碼:
src/index.css
:
+} +.flower--rotate { + transform: rotate(30deg); }
src/index.js
:
class App extends React.Component { + state = { + reset: 'yes' + } + onClick = () => { + this.setState({ + reset: this.state.reset === 'yes' ? 'no' : 'yes' + }) + } render () { return ( - <div><img src={Rose} alt='玫瑰' className='flower' /></div> + <div><img src={Rose} alt='玫瑰' className={this.state.reset === 'yes' ? 'flower' : 'flower flower--rotate'} onClick={this.onClick} /></div> ) } }
但隨即發現 webpack-dev-server 又拋出錯誤:
[./src/index.js] 1.49 KiB {main} [built] [failed] [1 error] + 25 hidden modules ERROR in ./src/index.js Module build failed: SyntaxError: /Users/sam/tmp/webpack-demo/src/index.js: Support for the experimental syntax 'classProperties' isn't currently enabled (6:9): 4 | import './index.css' 5 | class App extends React.Component { > 6 | state = { | ^ 7 | reset: 'yes' 8 | } 9 | onClick = () => { Add @babel/plugin-proposal-class-properties (https://git.io/vb4SL) to the 'plugins' section of your Babel config to enable transformation. at _class.raise (/Users/sam/tmp/webpack-demo/node_modules/babylon/lib/index.js:779:15) at _class.expectPlugin (/Users/sam/tmp/webpack-demo/node_modules/babylon/lib/index.js:2087:18) at _class.parseClassProperty (/Users/sam/tmp/webpack-demo/node_modules/babylon/lib/index.js:4904:12) at _class.pushClassProperty (/Users/sam/tmp/webpack-demo/node_modules/babylon/lib/index.js:4871:30) at _class.parseClassMemberWithIsStatic (/Users/sam/tmp/webpack-demo/node_modules/babylon/lib/index.js:4804:14) at _class.parseClassMember (/Users/sam/tmp/webpack-demo/node_modules/babylon/lib/index.js:4741:10) at _class.parseClassBody (/Users/sam/tmp/webpack-demo/node_modules/babylon/lib/index.js:4696:12) at _class.parseClass (/Users/sam/tmp/webpack-demo/node_modules/babylon/lib/index.js:4646:10) at _class.parseStatementContent (/Users/sam/tmp/webpack-demo/node_modules/babylon/lib/index.js:3987:21) at _class.parseStatement (/Users/sam/tmp/webpack-demo/node_modules/babylon/lib/index.js:3959:17) @ multi (webpack)-dev-server/client?http://localhost:8080 ./src ℹ 「wdm」: Failed to compile.
這是由於咱們用了 JavaScript 的新特性 - 須要 @babel/plugin-proposal-class-properties
插件的支持。
首先是安裝該插件:
$ npm i -D @babel/plugin-proposal-class-properties
而後調整 webpack.config.js
配置:
options: { presets: ['@babel/preset-react'] + plugins: ['@babel/plugin-proposal-class-properties'] } },
重啓 webpack-dev-server,編譯正常。
查看瀏覽器,圖片已經能夠點擊了。
在完成項目開發後,咱們須要輸出文件給生產環境部署,只要執行:
$ npx webpack --mode production
就能夠打包出全部靜態資源。
部署時,拷貝 dist
目錄便可。
隨着某些文件的增刪,咱們的 dist
目錄下會產生一些再也不使用的文件,咱們不想這些文件也部署到生產環境上佔用空間,因此 webpack 在打包前最好能刪除 dist
目錄。
咱們來試試 clean-webpack-plugin。
首先是安裝:
$ npm i -D clean-webpack-plugin
而後在 webpack.config.js
中調用:
const path = require('path') +const CleanWebpackPlugin = require('clean-webpack-plugin') module.exports = { mode: 'development', devServer: { contentBase: path.resolve(__dirname, 'dist') }, + plugins: [ + new CleanWebpackPlugin(['dist']) + ], module: {
再執行 npx webpack --mode production
,webpack 確實會在打包前清空 dist
目錄,但咱們的 index.html
也一塊兒被清空了。
咱們使用 html-webpack-plugin 來自動生成 index.html
。
首先是安裝:
$ npm i --save-dev html-webpack-plugin
調整 webpack.config.js
:
const CleanWebpackPlugin = require('clean-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { mode: 'development', devServer: { contentBase: path.resolve(__dirname, 'dist') }, plugins: [ - new CleanWebpackPlugin(['dist']) + new CleanWebpackPlugin(['dist']), + new HtmlWebpackPlugin() ],
再運行 npx webpack --mode production
,dist
下已經自動生成 index.html
,再 title 倒是 Webpack App
,咱們須要再調整一下 webpack.config.js
:
plugins: [ new CleanWebpackPlugin(['dist']), - new HtmlWebpackPlugin() + new HtmlWebpackPlugin({ + title: 'webpack 教程' + }) ],
前面的步驟裏,咱們幾乎是一步、一步手動配置每一個類型文件的加載器,一次添加一小節,而後重啓 webpack-dev-server
,恐怕沒人喜歡這樣幹活。因此市面上有很是多的 boilerplates、presets 等,其中比較出名的有: