[toc]
learn webpack4
webpack 用于編譯 JavaScript 模塊。
本質(zhì)上,webpack 是一個(gè)現(xiàn)代 JavaScript 應(yīng)用程序的靜態(tài)模塊打包器(module bundler)。當(dāng) webpack 處理應(yīng)用程序時(shí),它會(huì)遞歸地構(gòu)建一個(gè)依賴關(guān)系圖(dependency graph),其中包含應(yīng)用程序需要的每個(gè)模塊,然后將所有這些模塊打包成一個(gè)或多個(gè) bundle。
本文是學(xué)習(xí) webpack 4 所做的筆記,仍在完善當(dāng)中~
本文所有的代碼都保存在 github倉(cāng)庫(kù)中。
安裝
webpack 的使用是基于 Node 和 NPM 的。
前提條件
在開(kāi)始之前,請(qǐng)確保安裝了 Node.js 的最新版本。使用 Node.js 最新的長(zhǎng)期支持版本(LTS - Long Term Support),是理想的起步。使用舊版本,你可能遇到各種問(wèn)題,因?yàn)樗鼈兛赡苋鄙?webpack 功能以及/或者缺少相關(guān) package 包。
基本安裝
首先我們創(chuàng)建一個(gè)目錄,初始化 npm,然后 在本地安裝 webpack,接著安裝 webpack-cli(此工具用于在命令行中運(yùn)行 webpack):
mkdir webpack-start && cd webpack-start
npm init -y
npm install webpack webpack-cli --save-dev
另外,我們還需要調(diào)整 package.json 文件,以便確保我們安裝包是 私有的 (private),并且移除 main 入口。這可以防止意外發(fā)布你的代碼。
package.json
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
+ "private": true,
- "main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
+ "build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^4.0.1",
"webpack-cli": "^2.0.9"
},
"dependencies": {}
}
現(xiàn)在可以開(kāi)始你的模塊化項(xiàng)目了~
項(xiàng)目結(jié)構(gòu)大概是這樣的:
webpack-start
|- /dist
|- bundle.js
|- index.html
|- /node_modules
|- /src
|- index.js
|- package-lock.json
|- package.json
|- webpack.config.js
完整 demo 文件可在 webpack-study 中的 webpack-start 文件夾查看
使用下一代 ECMAScript
本節(jié)內(nèi)容沿用 webpack-start 文件代碼。
通過(guò)在 webpack 中配置 babel,使用下一代 ECMAScript。
npm install babel-loader @babel/core @babel/preset-env --save-dev
安裝之后,修改配置文件 webpack.config.js :
const path = require('path');
module.exports = {
entry: ['./src/index.js'],
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
+ {
+ test: /\.js$/,
+ exclude: /node_modules/,
+ use: {
+ loader: 'babel-loader',
+ }
+ }
]
}
};
這樣就可以在項(xiàng)目中使用下一代 ECMAScript 語(yǔ)法規(guī)則。
但是 babel 默認(rèn)只轉(zhuǎn)換語(yǔ)法,而不轉(zhuǎn)換新的 API,如需使用新的 API,還需要使用對(duì)應(yīng)的轉(zhuǎn)換插件 或者 添加 polyfill。
使用轉(zhuǎn)換插件
轉(zhuǎn)換插件適合在組件,類庫(kù)項(xiàng)目中使用。
添加轉(zhuǎn)換插件:
npm install @babel/plugin-transform-runtime --save-dev
npm install --save @babel/runtime-corejs2
@babel/runtime-corejs2 可轉(zhuǎn)換 Promise 在 IE 中未定義的問(wèn)題。
創(chuàng)建 babel 配置文件 .babelrc:
{
"presets": [
["@babel/preset-env"]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 2,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}
完整 demo 文件可在 webpack-study 中的 webpack-ES6/ES6-runtime 文件夾查看。
使用 @babel/polyfill
@babel/polyfill 適合在業(yè)務(wù)項(xiàng)目中使用。
添加 polyfill 到生產(chǎn)環(huán)境:
npm install --save @babel/polyfill
創(chuàng)建 babel 配置文件 .babelrc:
{
"presets": [
["@babel/preset-env",
{
"useBuiltIns": "usage"
}
]
]
}
在 .babelrc 中指定 useBuiltIns: 'usage' 的話,就不用在 webpack.config.js 的 entry 中包含 @babel/polyfill。
現(xiàn)在你可以在項(xiàng)目中使用新的語(yǔ)法規(guī)則和新的 API 了。
babel 教程:https://blog.zfanw.com/babel-js/
完整 demo 文件可在 webpack-study 中的 webpack-ES6/ES6-polyfill 文件夾查看。
資源管理
webpack 最出色的功能之一就是,除了 JavaScript,還可以通過(guò) loader 引入任何其他類型的文件。
也就是說(shuō),以上列出的那些 JavaScript 的優(yōu)點(diǎn)(例如顯式依賴),同樣可以用來(lái)構(gòu)建網(wǎng)站或 web 應(yīng)用程序中的所有非 JavaScript 內(nèi)容。
加載 CSS
使用 style-loader 和 css-loader
為了從 JavaScript 模塊中 import 一個(gè) CSS 文件,你需要在 module 配置中 安裝并添加 style-loader 和 css-loader:
npm install --save-dev style-loader css-loader
然后再 webpack.config.js 文件中:
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
+ module: {
+ rules: [
+ {
+ test: /\.css$/,
+ exclude: /node_modules/,
+ use: [
+ 'style-loader',
+ 'css-loader'
+ ]
+ }
+ ]
+ }
};
webpack 根據(jù)正則表達(dá)式,來(lái)確定應(yīng)該查找哪些文件,并將其提供給指定的 loader。在這種情況下,以 .css 結(jié)尾的全部文件,都將被提供給 style-loader 和 css-loader。
這使你可以在依賴于樣式的文件中引入樣式文件 import './style.css'。
現(xiàn)在,當(dāng)該模塊運(yùn)行時(shí),含有 CSS 字符串的 <style> 標(biāo)簽,將被插入到 html 文件的 <head> 中。
使用 CSS Module
只要在 webpack.config.js 文件中修改:
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
exclude: /node_modules/,
use: [
'style-loader',
- 'css-loader',
+ { loader: 'css-loader', options: { modules: true, localIdentName: '[name]__[local]-[hash:base64:5]' } }
]
}
]
},
};
使用 css module 后,在頁(yè)面引用樣式需要修改:
src/index.js
import style from "./style.css";
function component() {
var element = document.createElement("div");
element.innerHTML = "Asset management";
- element.classList.add('hello');
+ element.classList.add(style.hello);
return element;
}
document.body.appendChild(component());
重新打包,就能看到 calss 名稱已經(jīng)變成類似 style__hello-2uDIX 了。
使用 PostCSS
PostCSS 本身是一個(gè)功能比較單一的工具。它提供了一種方式用 JavaScript 代碼來(lái)處理 CSS。它負(fù)責(zé)把 CSS 代碼解析成抽象語(yǔ)法樹(shù)結(jié)構(gòu)(Abstract Syntax Tree,AST),再交由插件來(lái)進(jìn)行處理。
插件基于 CSS 代碼的 AST 所能進(jìn)行的操作是多種多樣的,比如可以支持變量和混入(mixin),增加瀏覽器相關(guān)的聲明前綴,或是把使用將來(lái)的 CSS 規(guī)范的樣式規(guī)則轉(zhuǎn)譯(transpile)成當(dāng)前的 CSS 規(guī)范支持的格式。
PostCSS 一般不單獨(dú)使用,而是與已有的構(gòu)建工具進(jìn)行集成。PostCSS 與主流的構(gòu)建工具,如 Webpack、Grunt 和 Gulp 都可以進(jìn)行集成。完成集成之后,選擇滿足功能需求的 PostCSS 插件并進(jìn)行配置。
文檔地址:https://postcss.org/
中文文檔:https://www.postcss.com.cn/
IBM文檔:https://www.ibm.com/developerworks/cn/web/1604-postcss-css/
學(xué)習(xí)指南:https://webdesign.tutsplus.com/series/postcss-deep-dive--cms-889
安裝 PostCSS 并添加插件 autoprefixer 和 postcss-preset-env:
npm i -D postcss-loader postcss-preset-env autoprefixer
在 webpack.config.js 文件中添加:
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
exclude: /node_modules/,
use: [
'style-loader',
{ loader: 'css-loader', options: { modules: true, localIdentName: '[name]__[local]-[hash:base64:5]' } },
+ {
+ loader: 'postcss-loader',
+ options: {
+ ident: 'postcss',
+ plugins: [
+ require('autoprefixer')(),
+ require('postcss-preset-env')(),
+ ]
+ }
+ }
]
}
]
},
};
完整 demo 文件可在 webpack-study 中的 asset-management/css-management 文件夾查看。
加載圖片
使用 file-loader
使用 file-loader,我們可以輕松地將圖片和 icon 混合到 CSS 中:
npm install --save-dev file-loader
webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist")
},
module: {
rules: [
{
test: /\.css$/,
exclude: /node_modules/,
use: [
"style-loader",
{ loader: "css-loader", options: { modules: true, localIdentName: "[name]__[local]-[hash:base64:5]" } },
{
loader: "postcss-loader",
options: {
ident: "postcss",
plugins: [require("autoprefixer")(), require("postcss-preset-env")()]
}
}
]
},
+ {
+ test: /\.(png|svg|jpg|gif)$/,
+ use: ["file-loader"]
+ }
]
}
};
現(xiàn)在可以在頁(yè)面(或者 css)中使用 圖片和 icon 了。
現(xiàn)在,當(dāng)你 import MyImage from './my-image.png',該圖像將被處理并添加到 output 目錄,并且 MyImage 變量將包含該圖像在處理后的最終 url。
當(dāng)使用 css-loader 時(shí),你的 CSS 中的 url('./my-image.png') 會(huì)使用類似的過(guò)程去處理。loader 會(huì)識(shí)別這是一個(gè)本地文件,并將 './my-image.png' 路徑,替換為輸出目錄中圖像的最終路徑。
合乎邏輯下一步是,壓縮和優(yōu)化你的圖像。
使用 url-loader 和 image-webpack-loader
url-loader 功能類似于 file-loader,但是在文件大?。▎挝?byte)低于指定的限制時(shí),可以返回一個(gè) DataURL。
image-webpack-loader 使用 imagemin 壓縮 PNG, JPEG, GIF, SVG 和 WEBP 圖像。
npm install --save-dev image-webpack-loader url-loader
webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist")
},
module: {
rules: [
{
test: /\.css$/,
exclude: /node_modules/,
use: [
"style-loader",
{ loader: "css-loader", options: { modules: true, localIdentName: "[name]__[local]-[hash:base64:5]" } },
{
loader: "postcss-loader",
options: {
ident: "postcss",
plugins: [require("autoprefixer")(), require("postcss-preset-env")()]
}
}
]
},
- {
- test: /\.(png|svg|jpg|gif)$/,
- use: ["file-loader"]
- },
+ {
+ test: /\.(png|svg|jpg|gif)$/,
+ use: [
+ {
+ loader: "url-loader",
+ options: {
+ limit: 8192,
+ name: 'images/[name]-[hash:5].[ext]'
+ }
+ },
+ "image-webpack-loader"
+ ]
+ }
]
}
};
完整 demo 文件可在 webpack-study 中的 asset-management/iamge-management 文件夾查看。
加載字體
那么,像字體這樣的其他資源如何處理呢?
file-loader 和 url-loader 可以接收并加載任何文件,然后將其輸出到構(gòu)建目錄。這就是說(shuō),我們可以將它們用于任何類型的文件,包括字體。讓我們更新 webpack.config.js 來(lái)處理字體文件:
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist")
},
module: {
rules: [
{
test: /\.css$/,
exclude: /node_modules/,
use: [
"style-loader",
{ loader: "css-loader", options: { modules: true, localIdentName: "[name]__[local]-[hash:base64:5]" } },
{
loader: "postcss-loader",
options: {
ident: "postcss",
plugins: [require("autoprefixer")(), require("postcss-preset-env")()]
}
}
]
},
{
test: /\.(png|svg|jpg|gif)$/,
use: [
{
loader: "url-loader",
options: {
limit: 8192,
name: 'images/[name]-[hash:5].[ext]'
}
},
"image-webpack-loader"
]
},
+ {
+ test: /\.(woff|woff2|eot|ttf|otf)$/,
+ use: [
+ 'url-loader'
+ ]
+ }
]
}
};
完整 demo 文件可在 webpack-study 中的 asset-management/font-management 文件夾查看。
加載 Iconfont
Iconfont 本質(zhì)上就是字體文件,只要 webpack.config.js 具有在 加載CSS 和 加載字體 添加的 rules,就能加載 Iconfont。
在需要的頁(yè)面引入 iconfont.css 文件就能使用 Iconfont:
import Iconfont from "./asset/font/iconfont.css";
...
// 將圖像添加到我們現(xiàn)有的 div。
var myIcon = document.createElement("span");;
myIcon.classList.add(Iconfont.iconfont);
myIcon.classList.add(Iconfont['wx-manage-shipin1']);
完整 demo 文件可在 webpack-study 中的 asset-management/font-management 文件夾查看。
加載數(shù)據(jù)
此外,可以加載的有用資源還有數(shù)據(jù),如 JSON 文件,CSV、TSV 和 XML。
類似于 NodeJS,JSON 支持實(shí)際上是內(nèi)置的,也就是說(shuō) import Data from './data.json' 默認(rèn)將正常運(yùn)行。
要導(dǎo)入 CSV、TSV 和 XML,你可以使用 csv-loader 和 xml-loader。讓我們處理這三類文件:
npm install --save-dev csv-loader xml-loader
webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist")
},
module: {
rules: [
{
test: /\.css$/,
exclude: /node_modules/,
use: [
"style-loader",
{ loader: "css-loader", options: { modules: true, localIdentName: "[name]__[local]-[hash:base64:5]" } },
{
loader: "postcss-loader",
options: {
ident: "postcss",
plugins: [require("autoprefixer")(), require("postcss-preset-env")()]
}
}
]
},
{
test: /\.(png|svg|jpg|gif)$/,
use: [
{
loader: "url-loader",
options: {
limit: 8192,
name: 'images/[name]-[hash:5].[ext]'
}
},
"image-webpack-loader"
]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
'url-loader'
]
},
+ {
+ test: /\.(csv|tsv)$/,
+ use: [
+ 'csv-loader'
+ ]
+ },
+ {
+ test: /\.xml$/,
+ use: [
+ 'xml-loader'
+ ]
+ }
]
}
};
完整 demo 文件可在 webpack-study 中的 asset-management/data-management 文件夾查看。
全局資源
上述所有內(nèi)容中最出色之處是,以這種方式加載資源,你可以以更直觀的方式將模塊和資源組合在一起。
無(wú)需依賴于含有全部資源的 /assets 目錄,而是將資源與代碼組合在一起。
- |- /assets
+ |– /components
+ | |– /my-component
+ | | |– index.jsx
+ | | |– index.css
+ | | |– icon.svg
+ | | |– img.png
這種配置方式會(huì)使你的代碼更具備可移植性,因?yàn)楝F(xiàn)有的統(tǒng)一放置的方式會(huì)造成所有資源緊密耦合在一起。假如你想在另一個(gè)項(xiàng)目中使用 /my-component,只需將其復(fù)制或移動(dòng)到 /components 目錄下。
只要你已經(jīng)安裝了任何擴(kuò)展依賴(external dependencies),并且你已經(jīng)在配置中定義過(guò)相同的 loader,那么項(xiàng)目應(yīng)該能夠良好運(yùn)行。
但是,假如你無(wú)法使用新的開(kāi)發(fā)方式,只能被固定于舊有開(kāi)發(fā)方式,或者你有一些在多個(gè)組件(視圖、模板、模塊等)之間共享的資源。你仍然可以將這些資源存儲(chǔ)在公共目錄(base directory)中,甚至配合使用 alias 來(lái)使它們更方便 import 導(dǎo)入。
接下來(lái)我們改造 data-management 項(xiàng)目中的文件。
完成 加載數(shù)據(jù) 這一小節(jié)后,我們的項(xiàng)目目錄大概是:
data-management
|- /dist
|- /images
|- bundle.js
|- index.html
|- /node_modules
|- /src
|- /asset
|- /font
|- data.xml
|- icon.jpg
|- index.js
|- style.css
|- package-lock.json
|- package.json
|- webpack.config.js
按照上述原則改造如下:
- 在 ~/src 文件夾下新建文件夾 components,用于存放我們所有的組件。
- 在 components 文件夾下新建 hello-world 文件夾,用于存放我們的第一個(gè)組件。
- 把存在 ~/src 目錄下的文件 data.xml、icon.jpg、index.js、style.css 移動(dòng)到 hello-world 文件夾。并修改 index.js 文件:
import style from "./style.css"; - import Iconfont from "./asset/font/iconfont.css"; + import Iconfont from "../../asset/font/iconfont.css"; import Icon from "./icon.jpg"; import Data from './data.xml'; console.log("Data",Data) function component() { let element = document.createElement("div"); element.innerHTML = "Asset management"; element.classList.add(style.hello); return element; } function imageComponent() { let element = document.createElement("div"); // 將圖像添加到我們現(xiàn)有的 div。 let myIcon = new Image(); myIcon.src = Icon; element.appendChild(myIcon); return element; } function iconComponent() { let element = document.createElement("div"); // 將圖像添加到我們現(xiàn)有的 div。 let myIcon = document.createElement("span");; myIcon.classList.add(Iconfont.iconfont); myIcon.classList.add(Iconfont['wx-manage-shipin1']); element.appendChild(myIcon); return element; } function dataComponent() { let element = document.createElement("div"); let str = ''; for(let key in Data.note){ str += `<p>${key}:${Data.note[key][0]}</p>` } element.innerHTML = str; return element; } - document.body.appendChild(component()); - document.body.appendChild(imageComponent()); - document.body.appendChild(iconComponent()); - document.body.appendChild(dataComponent()); + export { + component, + imageComponent, + iconComponent, + dataComponent + } - 在 ~/src 文件夾下新建文件 index.js(原來(lái)的已經(jīng)移入 hello-world 文件夾),并添加內(nèi)容:
import { component,imageComponent,iconComponent,dataComponent} from "./components/hello-world/index.js"; document.body.appendChild(component()); document.body.appendChild(imageComponent()); document.body.appendChild(iconComponent()); document.body.appendChild(dataComponent());
調(diào)整完成后我們的項(xiàng)目結(jié)構(gòu)大概是:
data-management
|- /dist
|- /images
|- bundle.js
|- index.html
|- /node_modules
|- /src
|- /asset
|- /font
|- /components
|- /hello-world
|- index.js
|- package-lock.json
|- package.json
|- webpack.config.js
這里的 ~/src/asset 文件夾依然存在,它存放的是 Iconfont 文件,這在我們的整個(gè)項(xiàng)目中都會(huì)用到。當(dāng)然也可以拆分到具體的組件,從而實(shí)現(xiàn)完全沒(méi)有全局資源。
輸出管理
本節(jié)代碼沿用 資源管理 代碼并安裝配置好 babel 和 @babel/polyfill。
項(xiàng)目結(jié)構(gòu)大概是這樣的:
output-management
|- /dist
|- /images
|- bundle.js
|- index.html
|- /node_modules
|- /src
|- /asset
|- /font
|- /components
|- /hello-world
|- index.js
|- .babelrc
|- package-lock.json
|- package.json
|- webpack.config.js
使用 HtmlWebpackPlugin
從本文開(kāi)始到現(xiàn)在,我們項(xiàng)目下幾乎所有的文件都動(dòng)過(guò),除了 ~/dist/index.html 這個(gè)文件。
用過(guò)主流框架的同學(xué)的知道, ~/dist 目錄下的所有文件都會(huì)打包后重新生成。我們通過(guò) HtmlWebpackPlugin 插件來(lái)完成這項(xiàng)任務(wù)。
生成 index.html
安裝插件:
npm install --save-dev html-webpack-plugin
并在配置文件 webpack.config.js 中引入并配置:
const path = require("path");
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
...
+ plugins: [
+ new HtmlWebpackPlugin({
+ title: 'Output Management'
+ })
+ ],
...
};
配置完成后,執(zhí)行打包命令 npm run dev,就會(huì)根據(jù)配置生成一個(gè) index.html 文件:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Output Management</title>
</head>
<body>
<script type="text/javascript" src="index.js"></script></body>
</html>
分離入口
現(xiàn)在我們的 ~/src 文件夾下只有一個(gè)入口文件 index.js,如果存在多個(gè)怎么添加進(jìn)生成的 index.html 文件呢?
首先,在 ~/src 文件夾下新建入口文件 print.js,添加下面內(nèi)容:
function component() {
let element = document.createElement("div");
element.innerHTML = "print entry";
return element;
}
document.body.appendChild(component());
然后修改配置文件 webpack.config.js :
const path = require("path");
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
- entry: "./src/index.js",
+ entry: {
+ app: "./src/index.js",
+ print: "./src/print.js"
+ },
output: {
- filename: 'index.js',
+ filename: '[name].bundle.js',
path: path.resolve(__dirname, "dist")
},
...
};
執(zhí)行打包命令,在重新生成的 index.html 中就添加了多個(gè)入口文件:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Output Management</title>
</head>
<body>
<script type="text/javascript" src="app.bundle.js"></script><script type="text/javascript" src="print.bundle.js"></script></body>
</html>
清理 ~/dist 文件夾
在上一節(jié)的操作中,我們成功的讓 webpack 可以比較智能的完成了一些任務(wù),很開(kāi)心~
然而,當(dāng)我打開(kāi) ~/dist 文件夾時(shí)卻發(fā)現(xiàn)里面的文件非常雜亂,因?yàn)槲覀兠看螛?gòu)建都會(huì)生成相應(yīng)代碼,但是從來(lái)沒(méi)有清理。
幸運(yùn)的是,我們可以通過(guò) clean-webpack-plugin 插件在構(gòu)建前清理 /dist 文件夾。
安裝插件:
npm install clean-webpack-plugin --save-dev
并在改配置文件 webpack.config.js 中引入和配置:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
+ const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
plugins: [
+ new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
title: 'Output Management'
})
],
...
};
現(xiàn)在再次執(zhí)行 npm run build 將會(huì)發(fā)現(xiàn)以前生成的文件已經(jīng)被清理干凈了!
完整 demo 可在 output-management 文件夾查看。
搭建開(kāi)發(fā)環(huán)境
如果你一直跟隨之前的指南,應(yīng)該對(duì)一些 webpack 基礎(chǔ)知識(shí)有著很扎實(shí)的理解。在我們繼續(xù)之前,先來(lái)看看如何建立一個(gè)開(kāi)發(fā)環(huán)境,使我們的開(kāi)發(fā)變得更容易一些。
使用 source map
本小節(jié)沿用 輸出管理 這一節(jié)的代碼。
當(dāng) webpack 打包源代碼時(shí),可能會(huì)很難追蹤到錯(cuò)誤和警告在源代碼中的原始位置。例如,如果將三個(gè)源文件(a.js, b.js 和 c.js)打包到一個(gè) bundle(bundle.js)中,而其中一個(gè)源文件包含一個(gè)錯(cuò)誤,那么堆棧跟蹤就會(huì)簡(jiǎn)單地指向到 bundle.js。這并通常沒(méi)有太多幫助,因?yàn)槟憧赡苄枰獪?zhǔn)確地知道錯(cuò)誤來(lái)自于哪個(gè)源文件。
為了更容易地追蹤錯(cuò)誤和警告,JavaScript 提供了 source map 功能,將編譯后的代碼映射回原始源代碼。如果一個(gè)錯(cuò)誤來(lái)自于 b.js,source map 就會(huì)明確的告訴你。
source map 有很多 不同的選項(xiàng) 可用,請(qǐng)務(wù)必仔細(xì)閱讀它們,以便可以根據(jù)需要進(jìn)行配置。
簡(jiǎn)單的說(shuō),在開(kāi)發(fā)環(huán)境可以使用 "eval" 選項(xiàng)因?yàn)樗芸欤辉谏a(chǎn)環(huán)境,最好 不用 或者使用 "source-map" 選項(xiàng)。
修改配置文件 webpack.config.js 使用 source map:
const path = require("path");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
...
+ devtool: "eval",
...
};
然后再 print.js 文件中人為的制造一個(gè)錯(cuò)誤:
function component() {
let element = document.createElement("div");
element.innerHTML = "print entry";
+ console.lag("asd",asdasd);
return element;
}
document.body.appendChild(component());
執(zhí)行構(gòu)建 npm run build 后,在瀏覽器打開(kāi) ~/build/index.html 文件,控制臺(tái)就會(huì)輸出一個(gè)錯(cuò)誤:
print.js:4 Uncaught ReferenceError: asdasd is not defined
at component (print.js:4)
at eval (print.js:8)
at Object../src/print.js (print.bundle.js:96)
at __webpack_require__ (print.bundle.js:20)
at print.bundle.js:84
at print.bundle.js:87
告訴我們錯(cuò)誤的文件是 "print.js",行數(shù)為 4 。
行數(shù)是錯(cuò)的,實(shí)際在第 5 行,使用 "source-map" 選項(xiàng)的話,就是正確的:
print.js:5 Uncaught ReferenceError: asdasd is not defined
at component (print.js:5)
at Object../src/print.js (print.js:10)
at __webpack_require__ (bootstrap:19)
at bootstrap:83
at bootstrap:83
完整 demo 可在 webpack-dev/source-map 文件夾查看。
使用一個(gè)開(kāi)發(fā)工具
現(xiàn)在,我們每次需要執(zhí)行構(gòu)建的時(shí)候,都需要手動(dòng)運(yùn)行 npm run build 命令,這有時(shí)會(huì)讓我們感到很煩躁~
webpack 中有幾個(gè)不同的選項(xiàng),可以幫助我們?cè)诖a發(fā)生變化后自動(dòng)編譯代碼:
- webpack's Watch Mode
- webpack-dev-server
- webpack-dev-middleware
我們只需要使用其中之一,就能去掉我們的煩惱 ^_^
注意:本小節(jié)每一個(gè)選項(xiàng)都沿用上一小節(jié) 使用 source map 的代碼。
使用觀察模式
使用觀察模式后,如果項(xiàng)目其中有文件被更新,代碼將被重新編譯,所以你不必手動(dòng)運(yùn)行整個(gè)構(gòu)建。
我們添加一個(gè)用于啟動(dòng) webpack 的觀察模式的 npm script 腳本:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
+ "watch": "webpack --watch",
"build": "webpack"
},
在命令行中運(yùn)行 npm run watch,就會(huì)看到 webpack 編譯代碼,然而卻不會(huì)退出命令行。這是因?yàn)?script 腳本還在觀察文件。
現(xiàn)在,我們先移除我們之前引入的錯(cuò)誤:
src/print.js
function component() {
let element = document.createElement("div");
element.innerHTML = "print entry";
- console.lag("asd",asdasd);
return element;
}
document.body.appendChild(component());
保存文件并檢查終端窗口。應(yīng)該可以看到 webpack 自動(dòng)重新編譯修改后的模塊!
唯一的缺點(diǎn)是,為了看到修改后的實(shí)際效果,你需要自己刷新瀏覽器。
完整 demo 可在 webpack-dev/webpack-watch 文件夾查看。
使用 webpack-dev-server
webpack-dev-server 為你提供了一個(gè)簡(jiǎn)單的 web 服務(wù)器,并且能夠?qū)崟r(shí)重新加載(live reloading)。
安裝并使用
首先,安裝:
npm install --save-dev webpack-dev-server
然后修改配置文件,告訴開(kāi)發(fā)服務(wù)器(dev server),在哪里查找文件:
const path = require("path");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
app: "./src/index.js",
print: "./src/print.js"
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, "dist")
},
devtool: "source-map",
+ devServer: {
+ contentBase: "./dist"
+ },
...
};
然后在 package.json 文件中添加一個(gè) script 腳本,來(lái)啟用開(kāi)發(fā)服務(wù)器:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
+ "start": "webpack-dev-server --open"
},
現(xiàn)在,我們可以在命令行中運(yùn)行 npm start,就會(huì)看到瀏覽器自動(dòng)加載頁(yè)面。
如果現(xiàn)在修改和保存任意源文件,web 服務(wù)器就會(huì)自動(dòng)重新加載編譯后的代碼。試一下!
可以查看 相關(guān)文檔 了解更多關(guān)于 devServer 的配置。
完整 demo 可在 webpack-dev/dev-server/webpack-WDS 文件夾查看。
啟用模塊熱替換
模塊熱替換(Hot Module Replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允許在運(yùn)行時(shí)更新各種模塊,而無(wú)需進(jìn)行完全刷新。
配置 webpack.config.js 文件:
const path = require("path");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
+ const webpack = require('webpack');
module.exports = {
entry: {
app: "./src/index.js",
print: "./src/print.js"
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, "dist")
},
devtool: "source-map",
devServer: {
contentBase: "./dist",
+ hot: true
},
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
title: 'Output Management'
}),
+ new webpack.NamedModulesPlugin(),
+ new webpack.HotModuleReplacementPlugin()
],
...
};
在入口文件 ~/src/index.js 處理模塊的熱替換:
import { component,imageComponent,iconComponent,dataComponent} from "./components/hello-world/index.js";
- document.body.appendChild(component());
document.body.appendChild(imageComponent());
document.body.appendChild(iconComponent());
document.body.appendChild(dataComponent());
+ let element = component(); // 記錄模塊,方便更新時(shí)移除和替換
+ document.body.appendChild(element);
+
+ if (module.hot) {
+ module.hot.accept('./components/hello-world/index.js', function() {
+ console.log('Accepting the updated printMe module!');
+
+ document.body.removeChild(element);
+ element = component(); // 重新渲染頁(yè)面后,更新 component 模塊
+ document.body.appendChild(element);
})
}
然后運(yùn)行 npm start 啟動(dòng)項(xiàng)目,修改 component 模塊內(nèi)容:
./components/hello-world/index.js
...
function component() {
let element = document.createElement("div");
- element.innerHTML = "Asset management";
+ element.innerHTML = "Asset management HMR";
element.classList.add(style.hello);
return element;
}
...
保存文件,可以看到 web 服務(wù)器就會(huì)自動(dòng)編譯代碼,然后替換瀏覽器中的相應(yīng)模塊。
完整 demo 可在 webpack-dev/dev-server/WDS-HMR 文件夾查看。
HMR 修改樣式表
當(dāng)項(xiàng)目中配置了 style-loader 和 css-loader,項(xiàng)目就能模塊熱替換樣式表,無(wú)需自己再做任何配置。
使用 webpack-dev-middleware
webpack-dev-middleware 是一個(gè)容器(wrapper),它可以把 webpack 處理后的文件傳遞給一個(gè)服務(wù)器(server)。
webpack-dev-server 在內(nèi)部使用了它,同時(shí),它也可以作為一個(gè)單獨(dú)的包來(lái)使用,以便進(jìn)行更多自定義設(shè)置來(lái)實(shí)現(xiàn)更多的需求。
接下來(lái)是一個(gè) webpack-dev-middleware 配合 express server 的示例。
啟用 webpack-dev-middleware
首先,安裝 express 和 webpack-dev-middleware:
npm install --save-dev express webpack-dev-middleware
接下來(lái)我們需要對(duì) webpack 的配置文件 webpack.config.js 做一些調(diào)整,以確保中間件(middleware)功能能夠正確啟用:
const path = require("path");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
app: "./src/index.js",
print: "./src/print.js"
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, "dist"),
+ publicPath: '/'
},
devtool: "source-map",
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
title: 'Output Management'
})
],
module: { ···
}
};
publicPath 也會(huì)在服務(wù)器腳本用到,以確保文件資源能夠在 http://localhost:3000 下正確訪問(wèn),我們稍后再設(shè)置端口號(hào)。
下一步就是設(shè)置我們自定義的 express 服務(wù):
在項(xiàng)目文件夾 webpack-middleware 下新建文件 server.js:
|- /dist
|- /node_modules
|- /src
|- /asset
|- /components
|- /hello-world
|- index.js
|- print.js
|- .babelrc
|- package-lock.json
|- package.json
|- webpack.config.js
+ |- server.js
在 server.js 添加下列內(nèi)容:
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);
// Tell express to use the webpack-dev-middleware and use the webpack.config.js
// configuration file as a base.
app.use(webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath
}));
// Serve the files on port 3000.
app.listen(3000, function () {
console.log('Example app listening on port 3000!\n');
});
接著,在 package.json 添加一個(gè) script,方便我們運(yùn)行服務(wù):
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
+ "server": "node server.js"
},
添加完成后就可以執(zhí)行 npm run server 命令運(yùn)行程序。
PS F:\demo\webpack-study\webpack-dev\webpack-middleware> npm run server
> webpack-study@1.0.0 server F:\demo\webpack-study\webpack-dev\webpack-middleware
> node server.js
clean-webpack-plugin: F:\demo\webpack-study\webpack-dev\webpack-middleware\dist has been removed.
Example app listening on port 3000!
打開(kāi)瀏覽器并輸入 http://localhost:3000,就能看到看到頁(yè)面。
當(dāng)項(xiàng)目中的文件存在修改,在保存后 webpack 就會(huì)自動(dòng)編譯文件,刷新瀏覽器就可以看到編譯后的文件。
完整 demo 可在 webpack-dev/dev-middleware/webpack-middleware 文件夾查看。
使用 webpack-hot-middleware
我們知道如果只使用 webpack-dev-middleware 的話,我們必須自己刷新瀏覽器,那么能不能自動(dòng)刷新呢?答案當(dāng)然是可以!
通過(guò)使用 webpack-hot-middleware 讓瀏覽器自動(dòng)刷新:
npm install --save-dev webpack-hot-middleware
安裝完成后修改配置文件:
const path = require("path");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
+ const webpack = require('webpack');
module.exports = {
entry: {
- app: "./src/index.js",
- print: "./src/print.js"
+ app: ["./src/index.js",'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=2000&reload=true&name=app'],
+ print: ["./src/print.js",'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=2000&reload=true&name=print']
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, "dist"),
publicPath: '/'
},
devtool: "source-map",
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
title: 'Output Management'
}),
+ new webpack.optimize.OccurrenceOrderPlugin(),
+ new webpack.HotModuleReplacementPlugin(),
+ new webpack.NoEmitOnErrorsPlugin()
],
module: { ···
}
};
在 server.js 中添加:
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);
// Tell express to use the webpack-dev-middleware and use the webpack.config.js
// configuration file as a base.
app.use(webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath
}));
+ app.use(require("webpack-hot-middleware")(compiler, {
+ log: false,
+ heartbeat: 1000,
+ }));
// Serve the files on port 3000.
app.listen(3000, function () {
console.log('Example app listening on port 3000!\n');
});
啟動(dòng)開(kāi)發(fā)服務(wù)器 npm run server,再修改項(xiàng)目下的文件并保存,就能看到瀏覽器已經(jīng)可以自動(dòng)刷新了。
完整 demo 可在 webpack-dev/dev-middleware/hot-middleware 文件夾查看。
配置拆分
本節(jié)我們沿用 使用 webpack-dev-server 這一小節(jié)的代碼。
到這里,我們已經(jīng)成功使用 webpack 搭建了一個(gè)模塊化項(xiàng)目的開(kāi)發(fā)環(huán)境,如果我們?cè)侔焉a(chǎn)環(huán)境也搭建好,就可以進(jìn)入愉快的開(kāi)發(fā)工作了,好開(kāi)心。。。
但是,開(kāi)發(fā)環(huán)境(development)和生產(chǎn)環(huán)境(production)的構(gòu)建目標(biāo)差異很大!
在開(kāi)發(fā)環(huán)境中,我們需要具有強(qiáng)大的、具有實(shí)時(shí)重新加載(live reloading)或熱模塊替換(hot module replacement)能力的 source map 和 localhost server。
而在生產(chǎn)環(huán)境中,我們的目標(biāo)則轉(zhuǎn)向于關(guān)注更小的 bundle,更輕量的 source map,以及更優(yōu)化的資源,以改善加載時(shí)間。
由于要遵循邏輯分離,我們通常建議為每個(gè)環(huán)境編寫(xiě)彼此獨(dú)立的 webpack 配置。
雖然,以上我們將生產(chǎn)環(huán)境和開(kāi)發(fā)環(huán)境做了略微區(qū)分,但是,請(qǐng)注意,我們還是會(huì)遵循不重復(fù)原則(Don't repeat yourself - DRY),保留一個(gè)“通用”配置。
為了將這些配置合并在一起,我們將使用一個(gè)名為 webpack-merge 的工具。通過(guò)“通用”配置,我們不必在環(huán)境特定(environment-specific)的配置中重復(fù)代碼。
安裝:
npm install --save-dev webpack-merge
拆分配置文件:
webpack-merge
|- /node_modules
|- /src
|- .babelrc
|- package-lock.json
|- package.json
- |- webpack.config.js
+ |- webpack.common.js
+ |- webpack.dev.js
+ |- webpack.prod.js
webpack.common.js 文件用于存放通用配置:
const path = require("path");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
app: "./src/index.js",
print: "./src/print.js"
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, "dist")
},
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
title: 'Output Management'
}),
],
module: { ···
}
};
webpack.dev.js 文件用于存開(kāi)發(fā)環(huán)境的配置:
const merge = require("webpack-merge");
const common = require("./webpack.common.js");
const webpack = require('webpack');
module.exports = merge(common, {
devtool: "source-map",
devServer: {
contentBase: "./dist",
hot: true
},
plugins: [
new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin()
]
});
webpack.prod.js 文件用于存生產(chǎn)環(huán)境的配置:
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
});
配置文件拆分后,更改 package.json 文件中的 "script" :
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
- "build": "webpack --mode development",
- "start": "webpack-dev-server --open"
+ "build": "webpack --config webpack.prod.js",
+ "start": "webpack-dev-server --open --config webpack.dev.js"
},
接下來(lái)可以分別運(yùn)行不同的腳本,查看效果!
完整 demo 可在 webpack-merge 文件夾查看。
搭建生產(chǎn)環(huán)境
本節(jié)沿用上一節(jié) 配置拆分 的代碼。
在生產(chǎn)環(huán)境中,我們的目標(biāo)是如何獲得更小的 bundle,更輕量的 source map,以及更優(yōu)化的資源,來(lái)改善加載時(shí)間。
tree shaking
tree shaking 是一個(gè)術(shù)語(yǔ),通常用于描述移除 JavaScript 上下文中的未引用代碼(dead-code)。它依賴于 ES2015 模塊系統(tǒng)中的靜態(tài)結(jié)構(gòu)特性,例如 import 和 export。
在新的 webpack 4 正式版本,擴(kuò)展了這個(gè)檢測(cè)能力,通過(guò) package.json 的 "sideEffects" 屬性作為標(biāo)記,向 compiler 提供提示,表明項(xiàng)目中的哪些文件是 "pure(純的 ES2015 模塊)",由此可以安全地刪除文件中未使用的部分。
在 ~/src 文件夾下新建一個(gè) math.js 文件:
export function square(x) {
return x * x;
}
export function cube(x) {
return x * x * x;
}
在 ~/src/index.js 入口文件引入并使用 cube() 方法:
import { component, imageComponent, iconComponent, dataComponent } from "./components/hello-world/index.js";
+ import { cube } from "./math.js";
+ function mathComponent() {
+ var element = document.createElement("pre");
+
+ element.innerHTML = ["Hello webpack!", "5 cubed is equal to " + cube(5)].join("\n\n");
+
+ return element;
+ }
// document.body.appendChild(component());
document.body.appendChild(imageComponent());
document.body.appendChild(iconComponent());
document.body.appendChild(dataComponent());
+ document.body.appendChild(mathComponent());
let element = component();
document.body.appendChild(element);
if (module.hot) {
module.hot.accept("./components/hello-world/index.js", function() {
console.log("Accepting the updated printMe module!");
document.body.removeChild(element);
element = component();
document.body.appendChild(element);
});
}
現(xiàn)在如果執(zhí)行構(gòu)建 npm run build 命令,在打包出來(lái)的 bundle 文件中仍然會(huì)有 square() 方法,盡管并沒(méi)有使用它。
在 package.json 中添加副作用配置:
"sideEffects": [
"*.css"
],
安裝插件:
npm install uglifyjs-webpack-plugin --save-dev
修改生產(chǎn)配置文件
const merge = require("webpack-merge");
const common = require("./webpack.common.js");
const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
module.exports = merge(common, {
plugins: [
new UglifyJSPlugin({
sourceMap: true
})
]
});
現(xiàn)在執(zhí)行構(gòu)建 npm run build 命令,在打包出來(lái)的 bundle 文件中就不會(huì)有 square() 方法。
指定環(huán)境
修改開(kāi)發(fā)環(huán)境配置:
const merge = require("webpack-merge");
const common = require("./webpack.common.js");
const webpack = require('webpack');
module.exports = merge(common, {
devtool: "source-map",
devServer: {
contentBase: "./dist",
hot: true
},
plugins: [
+ new webpack.DefinePlugin({
+ "process.env.NODE_ENV": JSON.stringify("development")
+ }),
new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin()
]
});
修改生產(chǎn)配置文件:
const merge = require("webpack-merge");
const common = require("./webpack.common.js");
const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
const webpack = require("webpack");
module.exports = merge(common, {
plugins: [
+ new UglifyJSPlugin({
+ sourceMap: true
+ }),
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("production")
})
]
});
CSS 分離
安裝:
npm i -D extract-text-webpack-plugin@next
接下來(lái)把通用配置 CSS loader 配置移動(dòng)到開(kāi)發(fā)環(huán)境和生產(chǎn)環(huán)境:
webpack.dev.js:
const merge = require("webpack-merge");
const common = require("./webpack.common.js");
const webpack = require("webpack");
module.exports = merge(common, {
devtool: "source-map",
devServer: {
contentBase: "./dist",
hot: true
},
plugins: [
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("development")
}),
new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin()
],
module: {
rules: [
{
test: /\.css$/,
exclude: /node_modules/,
use: [
"style-loader",
{ loader: "css-loader", options: { modules: true, localIdentName: "[name]__[local]-[hash:base64:5]" } },
{
loader: "postcss-loader",
options: {
ident: "postcss",
plugins: [require("autoprefixer")(), require("postcss-preset-env")()]
}
}
]
}
]
}
});
webpack.prod.js :
const merge = require("webpack-merge");
const common = require("./webpack.common.js");
const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
const webpack = require("webpack");
module.exports = merge(common, {
plugins: [
new UglifyJSPlugin({
sourceMap: true
}),
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("production")
})
],
module: {
rules: [
{
test: /\.css$/,
exclude: /node_modules/,
use: [
"style-loader",
{ loader: "css-loader", options: { modules: true, localIdentName: "[name]__[local]-[hash:base64:5]" } },
{
loader: "postcss-loader",
options: {
ident: "postcss",
plugins: [require("autoprefixer")(), require("postcss-preset-env")()]
}
}
]
}
]
}
});
在生產(chǎn)環(huán)境配置中,加入 extract-text-webpack-plugin 插件配置:
const merge = require("webpack-merge");
const common = require("./webpack.common.js");
const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
+ const ExtractTextPlugin = require("extract-text-webpack-plugin");
const webpack = require("webpack");
module.exports = merge(common, {
plugins: [
new UglifyJSPlugin({
sourceMap: true
}),
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("production")
}),
+ new ExtractTextPlugin({
+ filename: '[name].css',
+ })
],
+ module: {
+ rules: [
+ {
+ test: /\.css$/,
+ exclude: /node_modules/,
+ use: ExtractTextPlugin.extract({
+ fallback: 'style-loader',
+ use:[
+ { loader: "css-loader", options: { modules: true, localIdentName: "[name]__[local]-[hash:base64:5]" } },
+ {
+ loader: "postcss-loader",
+ options: {
+ ident: "postcss",
+ plugins: [require("autoprefixer")(), require("postcss-preset-env")()]
+ }
+ }
+ ]
+ })
+ }
+ ]
+ }
});
在這里遇到一個(gè)問(wèn)題,假設(shè)我這樣配置插件:
new ExtractTextPlugin({
filename: 'css/[name].css',
})
在執(zhí)行構(gòu)建后,css 文件中引入的背景圖地址會(huì)變成 css/images/icon-745a8.jpg ,導(dǎo)致背景圖加載失?。?/p>
所以最終這樣配置:
new ExtractTextPlugin({
filename: '[name].css',
})
完整 demo 可在 webpack-prod 文件夾查看。
優(yōu)化
現(xiàn)在一個(gè)簡(jiǎn)單的開(kāi)發(fā)環(huán)境和生產(chǎn)環(huán)境就配置好了,讓我們進(jìn)入優(yōu)化環(huán)節(jié)吧~
代碼分離
代碼分離是 webpack 中最引人注目的特性之一。此特性能夠把代碼分離到不同的 bundle 中,然后可以按需加載或并行加載這些文件。代碼分離可以用于獲取更小的 bundle,以及控制資源加載優(yōu)先級(jí),如果使用合理,會(huì)極大影響加載時(shí)間。
有三種常用的代碼分離方法:
- 入口起點(diǎn):使用 entry 配置手動(dòng)地分離代碼。
- 防止重復(fù):使用 CommonsChunkPlugin 去重和分離 chunk。
- 動(dòng)態(tài)導(dǎo)入:通過(guò)模塊的內(nèi)聯(lián)函數(shù)調(diào)用來(lái)分離代碼。
入口起點(diǎn)
這是迄今為止最簡(jiǎn)單、最直觀的分離代碼的方式。不過(guò),這種方式手動(dòng)配置較多,并有一些陷阱,我們將會(huì)解決這些問(wèn)題。先來(lái)看看如何從 main bundle 中分離另一個(gè)模塊:
webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
+ |- another-module.js
|- /node_modules
another-module.js
import _ from 'lodash';
console.log(
_.join(['Another', 'module', 'loaded!'], ' ')
);
webpack.config.js
const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js',
another: './src/another-module.js'
},
plugins: [
new HTMLWebpackPlugin({
title: 'Code Splitting'
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
正如前面提到的,這種方法存在一些問(wèn)題:
- 如果入口 chunks 之間包含重復(fù)的模塊,那些重復(fù)模塊都會(huì)被引入到各個(gè) bundle 中。
- 這種方法不夠靈活,并且不能將核心應(yīng)用程序邏輯進(jìn)行動(dòng)態(tài)拆分代碼。
以上兩點(diǎn)中,第一點(diǎn)對(duì)我們的示例來(lái)說(shuō)無(wú)疑是個(gè)問(wèn)題,因?yàn)橹拔覀冊(cè)?./src/index.js 中也引入過(guò) lodash,這樣就在兩個(gè) bundle 中造成重復(fù)引用。
提取公共代碼
SplitChunksPlugin 插件可以將公共的依賴模塊提取到已有的入口 chunk 中,或者提取到一個(gè)新生成的 chunk。
splitChunks 在 production 模式下自動(dòng)開(kāi)啟。有一些默認(rèn)配置,通過(guò) "optimization" 配置參數(shù)詳細(xì)說(shuō)明:
const merge = require("webpack-merge");
const common = require("./webpack.common.js");
const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const webpack = require("webpack");
module.exports = merge(common, {
···
optimization: {
runtimeChunk: { // 自動(dòng)拆分 runtime 文件
name: 'manifest'
},
splitChunks:{
chunks: 'initial', // initial(初始?jí)K)、async(按需加載塊)、all(全部塊),默認(rèn)為 async
minSize: 30000, // 形成一個(gè)新代碼塊最小的體積(默認(rèn)是30000)
minChunks: 1, // 在分割之前,這個(gè)代碼塊最小應(yīng)該被引用的次數(shù)(默認(rèn)為 1 )
maxAsyncRequests: 5, // 按需加載時(shí)候最大的并行請(qǐng)求數(shù)
maxInitialRequests: 3, // 一個(gè)入口最大的并行請(qǐng)求數(shù)
name:"common", // 打包的 chunks 的名字(字符串或者函數(shù),函數(shù)可以根據(jù)條件自定義名字)
automaticNameDelimiter: '~', // 打包分隔符
cacheGroups: { // 這里開(kāi)始設(shè)置緩存的 chunks
vendors: {
name: 'vendors',
chunks: 'all',
priority: -10, // 緩存組打包的先后優(yōu)先級(jí)(只用于緩存組)
reuseExistingChunk: true, // 可設(shè)置是否重用該 chunk (只用于緩存組)
test:/[\\/]node_modules[\\/]/ // 只用于緩存組
},
components: {
test: /components\//,
name: "components",
chunks: 'initial',
enforce: true
}
}
}
},
···
});
runtimeChunk 的作用是將包含 chunks 映射關(guān)系的 list 單獨(dú)從 app.js 里提取出來(lái),因?yàn)槊恳粋€(gè) chunk 的 id 基本都是基于內(nèi)容 hash 出來(lái)的,所以你每次改動(dòng)都會(huì)影響它,如果不將它提取出來(lái)的話,等于 app.js 每次都會(huì)改變。緩存就失效了。
配置完成再執(zhí)行構(gòu)建。lodash 就被提取到 vendors.bundle.js 文件,在項(xiàng)目中只加載一次。
完整 demo 可在 webpack-optimization/optimization-split 文件夾查看。
動(dòng)態(tài)導(dǎo)入
當(dāng)涉及到動(dòng)態(tài)代碼拆分時(shí),webpack 提供了兩個(gè)類似的技術(shù)。對(duì)于動(dòng)態(tài)導(dǎo)入,第一種,也是優(yōu)先選擇的方式是,使用符合 ECMAScript 提案 的 import() 語(yǔ)法。第二種,則是使用 webpack 特定的 require.ensure。讓我們先嘗試使用第一種……
首先安裝插件:
npm install --save-dev @babel/plugin-syntax-dynamic-import
在 .babelrc 文件中添加配置:
{
"presets": [
["@babel/preset-env",
{
"useBuiltIns": "usage"
}
]
],
+ "plugins": ["@babel/plugin-syntax-dynamic-import"]
}
在通用配置 webpack.common.js 文件中添加配置:
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CleanWebpackPlugin = require("clean-webpack-plugin");
module.exports = {
entry: {
app: "./src/index.js",
print: "./src/print.js"
},
output: {
filename: "[name].bundle.js",
+ chunkFilename:'[name].bundle.js', // 非入口 chunk 的名稱
path: path.resolve(__dirname, "dist")
},
plugins: [
new CleanWebpackPlugin(["dist"]),
new HtmlWebpackPlugin({
title: "Output Management"
})
],
module: {
···
}
};
配置完成后就可在 asyncIndex.js 中嘗試:
// import _ from 'lodash';
+ // 測(cè)試動(dòng)態(tài)導(dǎo)入
+ async function getComponent() {
+ var element = document.createElement("div");
+ console.log("開(kāi)始加載lodash");
+ const _ = await import(/* webpackChunkName: "lodash" */ "lodash");
+ console.log("lodash加載成功");
+ element.innerHTML = _.join(["Hello", "webpack"], " ");
+
+ return element;
+ }
+ getComponent().then(component => {
+ document.body.appendChild(component);
+ });
懶加載
本節(jié)沿用上一節(jié) 代碼分離 的代碼
懶加載或者按需加載,是一種很好的優(yōu)化網(wǎng)頁(yè)或應(yīng)用的方式。這種方式實(shí)際上是先把你的代碼在一些邏輯斷點(diǎn)處分離開(kāi),然后在一些代碼塊中完成某些操作后,立即引用或即將引用另外一些新的代碼塊。這樣加快了應(yīng)用的初始加載速度,減輕了它的總體體積,因?yàn)槟承┐a塊可能永遠(yuǎn)不會(huì)被加載。
新建 print.js 文件:
console.log('The print.js module has loaded! See the network tab in dev tools...');
export default () => {
console.log('Button Clicked: Here\'s "some text"!');
}
然后在 index.js 文件中:
import { component, imageComponent, iconComponent, dataComponent } from "./components/hello-world/index.js";
import { cube } from "./math.js";
import _ from "lodash";
function mathComponent() {
var element = document.createElement("pre");
// element.innerHTML = ["Hello webpack!", "5 cubed is equal to " + cube(5)].join("\n\n");
element.innerHTML = _.join(["Hello", "loadsh", cube(5)], " ");
return element;
}
+ function buttonComponent() {
+ var element = document.createElement("div");
+ var button = document.createElement("button");
+ var br = document.createElement("br");
+ button.innerHTML = "Click me and look at the console!";
+ element.innerHTML = _.join(["Hello", "webpack"], " ");
+ element.appendChild(br);
+ element.appendChild(button);
+ // Note that because a network request is involved, some indication
+ // of loading would need to be shown in a production-level site/app.
+ button.onclick = e =>
+ import(/* webpackChunkName: "print" */ "./print").then(module => {
+ var print = module.default;
+ print();
+ });
+ return element;
+ }
document.body.appendChild(imageComponent());
document.body.appendChild(iconComponent());
document.body.appendChild(dataComponent());
document.body.appendChild(mathComponent());
+ document.body.appendChild(buttonComponent());
// 只有 component() 模塊才是熱更行
let element = component();
document.body.appendChild(element);
if (module.hot) {
module.hot.accept("./components/hello-world/index.js", function() {
console.log("Accepting the updated printMe module!");
document.body.removeChild(element);
element = component();
document.body.appendChild(element);
});
}
運(yùn)行項(xiàng)目,可以發(fā)現(xiàn)在單擊按鈕之后才加載文件 print.bundle.js 。
完整 demo 可在 webpack-optimization/optimization-split 文件夾查看。
緩存
更改生產(chǎn)配置:
const merge = require("webpack-merge");
const common = require("./webpack.common.js");
const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const webpack = require("webpack");
module.exports = merge(common, {
+ output: {
+ filename: "[name].[chunkhash].js",
+ chunkFilename:'[name].[chunkhash].js', // 非入口 chunk 的名稱
+ },
plugins: [
new UglifyJSPlugin({
sourceMap: true
}),
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("production")
}),
new ExtractTextPlugin({
filename: "[name].css"
}),
+ new webpack.HashedModuleIdsPlugin()
],
mode: "production",
optimization: {
splitChunks:{
chunks: 'initial', // initial(初始?jí)K)、async(按需加載塊)、all(全部塊),默認(rèn)為async
minSize: 30000, // 形成一個(gè)新代碼塊最小的體積(默認(rèn)是30000)
minChunks: 1, // 在分割之前,這個(gè)代碼塊最小應(yīng)該被引用的次數(shù)(默認(rèn)為 1 )
maxAsyncRequests: 5, // 按需加載時(shí)候最大的并行請(qǐng)求數(shù)
maxInitialRequests: 3, // 一個(gè)入口最大的并行請(qǐng)求數(shù)
name:"common", // 打包的 chunks 的名字(字符串或者函數(shù),函數(shù)可以根據(jù)條件自定義名字)
automaticNameDelimiter: '~', // 打包分隔符
cacheGroups: { // 這里開(kāi)始設(shè)置緩存的 chunks
vendors: {
name: 'vendors',
chunks: 'all',
priority: -10, // 緩存組打包的先后優(yōu)先級(jí)(只用于緩存組)
reuseExistingChunk: true, // 可設(shè)置是否重用該 chunk (只用于緩存組)
test:/[\\/]node_modules[\\/]/ // 只用于緩存組
},
components: {
test: /components\//,
name: "components",
chunks: 'initial',
enforce: true
}
}
},
+ runtimeChunk: { // 自動(dòng)拆分 runtime 文件
+ name: 'manifest'
+ }
},
module: {
rules: [
{
test: /\.css$/,
exclude: /node_modules/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: [
{ loader: "css-loader", options: { modules: true, localIdentName: "[name]__[local]-[hash:base64:5]" } },
{
loader: "postcss-loader",
options: {
ident: "postcss",
plugins: [require("autoprefixer")(), require("postcss-preset-env")()]
}
}
]
})
}
]
}
});
完整 demo 可在 webpack-optimization/optimization-cache 文件夾查看。
配置別名
在通用配置下:
resolve: {
extensions: [".js", ".css", ".json"],
alias: {
asset: __dirname + "/src/asset"
} //配置別名可以加快webpack查找模塊的速度
},
完整 demo 可在 webpack-optimization/optimization-cache 文件夾查看。
拆分頁(yè)面
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CleanWebpackPlugin = require("clean-webpack-plugin");
module.exports = {
entry: {
app: "./src/index.js",
es6Index: "./src/es6Index.js"
},
output: {
filename: "[name].bundle.js",
chunkFilename: "[name].bundle.js", // 非入口 chunk 的名稱
path: path.resolve(__dirname, "dist")
},
resolve: {
extensions: [".js", ".css", ".json"],
alias: {
asset: __dirname + "/src/asset"
} //配置別名可以加快webpack查找模塊的速度
},
plugins: [
new CleanWebpackPlugin(["dist"]),
new HtmlWebpackPlugin({
filename: "index.html",
hash: true,
chunks: ["app", "vendors", "commons", "manifest"]
}),
new HtmlWebpackPlugin({
filename: "es6Index.html",
hash: true,
chunks: ["es6Index", "vendors", "commons", "manifest"]
})
],
···
};
完整 demo 可在 webpack-optimization/optimization-cache 文件夾查看。
學(xué)習(xí)資料
https://www.webpackjs.com/guides/hot-module-replacement/
https://webxiaoma.com/webpack/entry.html#%E5%8A%A8%E6%80%81%E5%85%A5%E5%8F%A3
https://survivejs.com/webpack/developing/composing-configuration/