什么是Webpack
作为一个 JS 开发者,应该比较熟悉 module,比如 AMD、UMD、CommonJS 和 ES Module。
Webpack 是一个模块捆绑器,并且它对模块有更加广泛的定义,特别对于 Webpack 来说,模块是:
- 常见的JS模块
- AMD模块
- 导入的CSS
- 图片URL
- ES模块
也就是说,Webpack 是能够从这些来源中提取依赖项。
Webpack的最终目的就是用一种可以导入JS代码中所有内容的方式来统一这些不同的资源和模块类型,并最终生成可交付的输出。
Entry point
对于 Webpack,Entry point 是已经收集了前端项目的所有依赖项的起点。
实际上,它就是一个简单的JS文件。
这些依赖关系形成了一个依赖关系图。
Webpack(从版本4开始)的默认入口是src/index.js,它是可以配置的。Webpack 可以有多个入口点。
module.exports = {
entry: './path/to/my/entry/file.js',
};
Output
Output 是在构建过程中收集所得到的 JS 和静态文件的地方。
Webpack(从版本4开始)默认输出文件夹是dist/,它也是可以配置的。
之后产生的 JS 文件就是所谓 bundle 的一部分。
const path = require('path');
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js',
},
};
Loaders
因为Webpack只能理解 JS 和 JSON 文件。所以就需要loaders来处理其他文件。
Loaders是第三方扩展。来帮助 Webpack 处理各种各样的文件扩展名。比如:CSS、图像或者 txt 文件。
Loader 的目的是在模块中转化文件(除了 JavaScript)。一旦文件成为了模块,Webpack 就可以将其作为项目中的依赖项了。
Loader 有两个属性:
test属性,识别出哪些文件会被转换use属性,定义出在进行转换时,应该使用哪个 loader。
const path = require('path');
module.exports = {
output: {
filename: 'my-first-webpack.bundle.js',
},
module: {
// 遇到文件扩展名为 txt 的就使用 raw-loader 转换一下
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
},
};
Plugins
Plugins 是可以改变 Webpack 工作方式的第三方扩展。比如有提取 HTML、CSS 或设置环境变量的插件。
(Plugins可以扩展Webpack的能力)
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 用于访问内置插件
module.exports = {
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
};
Mode
Webpack有两种模式:development 和 production。
它们之间的主要区别就是在于 production 模式会自动对 JS 代码进行缩小和其他优化。
默认是为 production。
Code splitting
Code splitting或者Lazy loading是一种避免较大的bundles的优化技术。
通过 Code splitting,开发人员可以决定在响应某些用户交互时来加载整个 JavaScript 块,比如单击或者路由改变。
一段被分裂的代表变成了一个 chunk。
开始使用 Webpack
首先创建一个新的文件夹并进入来初始化 NPM 项目,再安装 webpack 相关包:
npm init -y
npm i webpack webpack-cli webpack-dev-server --save-dev
之后,再打开package.json并配置一个dev脚本:
"scripts": {
"dev": "webpack --mode development"
},
使用这个脚本,我们让webpack工作在开发模式下。
使用 Webpack 第一步
在开发模式下运行 webpack:
npm run dev
你应该可以看到以下错误:
ERROR in Entry module not found: Error: Can't resolve './src'
因为 webpack 正在寻找默认的入口点src/index.js。创建 src 文件夹,在文件夹中创建 index.js 文件
在src/index.js中输入一段代码:
console.log('Hello webpack');
现在再次运行npm run dev。你可以看到一个名为dist/的新文件夹,里面有个名为main.js的JS文件:
dist
└── main.js
这就是你的第一个 webpack 包,也称为 output。
配置 webpack
对于更简单的任务,webpack 可以在不进行配置的情况下工作,但是这很快就会遇到限制。要通过文件配置webpack,在项目文件夹中创建一个webpack.config.js:
Webpack是用JavaScript编写的,你可以运行在 NodeJS 中。如下:
module.exports = {
//
};
比如要改变入口路径,可以:
const path = require("path");
module.exports = {
entry: { index: path.resolve(__dirname, "source", "index.js") }
};
现Webpack将在source/index.js中查找要加载的第一个文件。我们可以改变捆绑包的输出:
const path = require("path");
module.exports = {
output: {
path: path.resolve(__dirname, "build")
}
};
这样webpack将把 bundle 放在 build 中,而不是 dist 中。
使用 HTML
一个没有 HTML 页面的 web 应用程序几乎是毫无用处的。为使用 webpack 中的 HTML,需要安装html-webpack-plugin插件:
npm i html-webpack-plugin --save-dev
安装完成后,就可以对其进行配置:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src', 'index.html')
})
]
}
这里的意思就是 webpack 从src/index.html加载一个HTML模板。
html-webpack-plugin 的最终目的有两个:
- 它加载我们的HTML文件
- 它将包注入同一个文件中
在src/index.html中创建:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Webpack tutorial</title>
</head>
<body>
</body>
</html>
马上,我们将在 webpack 的开发服务器中运行这个 “app”
webpack 的开发服务器
之前我们安装了 webpack-dev-server,若未安装,则安装它:
npm i webpack-dev-server --save-dev
webpack-dev-server 是一个便于开发的软件包。一旦配置好,就可以启动一个本地服务器来服务我们的文件。
先在package.json添加一个“start”脚本:
"scripts": {
"dev": "webpack --mode development",
"start": "webpack serve --open",
},
有了这个脚本,可以很容易地运行服务器:
npm start
之后你的默认浏览器应该打开。而且你还可以看见一个 script tag,表示 JS 包被注入:
如果跑不起来,在
webpack.config.json中需要mode: 'development'这段代码。
与 webpack 的 loaders 一起工作
加载器是第三方扩展,帮助webpack处理各种文件扩展名。比如 CSS 和图像都有加载器。
对 webpack Loader 的剖析,从配置上来说:
module.exports = {
module: {
rules: [
{
test: /\.filename$/,
use: ["loader-b", "loader-a"]
}
]
},
//
};
相关配置从module这个键开始。这个键中我们在 rules 内部配置每个加载程序组或者单个加载程序。
对于每个文件都视为一个模块,并且用test键和use键来配置一个对象作用于它们
{
test: /\.filename$/,
use: ["loader-b", "loader-a"]
}
test告诉 webpack:将这些文件名视作一个模块use则定义这些 loaders 应用于文件
使用 CSS
为了在 webpack 中使用 CSS,我们需要安装至少两个加载器。
这里的加载器对于帮助 webpack 理解如何处理.css文件是必要的。
为了在 webpack 中测试 CSS,用src/style.css创建一个简单的样式表:
h1 {
colro: orange;
}
之后,在src/index.html中向 HTML 模板添加一个 HTML 元素:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Webpack tutorial</title>
</head>
<body>
<h1>Hello webpack!</h1>
</body>
</html>
最后在,在src/index.js中加载 CSS:
import './style.css'
console.log('Hello webpack');
在测试页面之前,需要安装 loaders:
- css-loader用于通过
import来加载 CSS 文件 - style-loader用于在DOM中加载stylesheet
安装 loaders:
npm i css-loader style-loader --save-dev
然后在 webpack.config.js 中配置它们:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src', 'index.html')
})
]
}
现在运行npm start,你就可以看到样式表被加载到 HTML 的头部:
webpack的loaders加载顺序很重要
在webpack中,加载器在配置中出现的顺序非常重要。以下配置无效:
//
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ["css-loader", "style-loader"] // 顺序反过来了
}
]
},
//
};
这里“style-loader”出现在“css-loader”之前。但是 style-loader 用于在页面中注入样式,而不是用于加载实际的 CSS 文件。
webpack 的 loaders 是从右向左进行装载。
使用 SASS
要在 webpack 中使用 SASS,至少需要安装适当的加载器。
在 webpack 中测试 SASS,在src/style.scss中创建一个简单的样式表:
@import url("https://fonts.googleapis.com/css?family=Karla:weight@400;700&display=swap");
$font: "Karla", sans-serif;
$primary-color: #3e6f9e;
body {
font-family: $font;
color: $primary-color;
}
另外,在src.index.html中向 HTML 模板添加一些 HTML 元素:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Webpack tutorial</title>
<script defer src="main.js"></script></head>
<body>
<h1>Hello webpack!</h1>
<p>Hello sass!</p>
</body>
</html>
最后在src/index.js中加载 SASS 文件
import './style.scss'
console.log('Hello webpack');
现在就来安装需要的 loaders:
- sass-loader用于通过
import来加载 SASS 文件
其他的css-loader和style-loader上面已经介绍了
npm i css-loader style-loader sass-loader sass --save-dev
然后在webpack.config.js中配置它们:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
module: {
rules: [
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src', 'index.html')
})
]
}
注意loader出现的顺序:sass-loader → css-loader → style-loader
现在再次运行npm start,可以看到样式表被加载到 HTML 的头部:
使用现代 JavaScript
webpack并不知道如何转换 JavaScript 代码。这个任务外包给第三方 loader,比如 babel-loader 和 babel。
babel是一个JavaScript编译器和“运输器”。以现代 JavaScript 语法输入,babel 能够将其转换为兼容的代码,这些代码可以在(几乎)任何浏览器中运行。
现在需要安装一系列软件包:
- babel core,真正的引擎
- babel preset env 用于将现代JS代码编译成ES5代码
- babel loader 用于 webpack
npm i @babel/core babel-loader @babel/preset-env --save-dev
然后通过创建一个新文件 babel.config.json 来配置 babel:
{
"presets": [
"@babel/preset-env"
]
}
最后,配置 webpack 并使用 loader 来转换 JS 文件。
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
module: {
rules: [
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
},
{
test: /\.js$/,
exclude: /node_modules/,
use: ["babel-loader"]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src', 'index.html')
})
]
}
为了测试,在src/index.js中编写一些现代语法:
import './style.scss'
console.log("Hello webpack!");
const fancyFunc = () => {
return [1, 2];
}
const [a, b] = fancyFunc();
运行npm run dev并查看dist中转换后的代码,搜索"fancyFunc":
\n\nvar fancyFunc = function fancyFunc() {\n return [1, 2];\n};\n\nvar _fancyFunc = fancyFunc(),\n _fancyFunc2 = _slicedToArray(_fancyFunc, 2),\n a = _fancyFunc2[0],\n b = _fancyFunc2[1];\n\n//# sourceURL=webpack:///./src/index.js?"
若没有 babel,代码就不会转换:
\n\nconsole.log(\"Hello webpack!\");\n\nconst fancyFunc = () => {\n return [1, 2];\n};\n\nconst [a, b] = fancyFunc();\n\n\n//# sourceURL=webpack:///./src/index.js?");
如何从头开始建立 React、webpack 5 和 Babel
在 Webpack 中使用 React 组件,你还需要用于 React 的 babel preset:
npm i @babel/core babel-loader @babel/preset-env @babel/preset-react --save-dev
完成之后,还需要更改babel.config.json的配置:
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}
现在就可以开始安装 React:
npm i react react-dom
最后在src/index.js中编组件:
import React, { useState } from 'react';
import { render } from 'react-dom';
function App() {
const [state, setState] = useState("CLICK ME")
return (
<button onClick={() => setState("CLICKED")}>{state}</button>
)
}
render(<App />, document.getElementById('root'));
记得还要在HTML页面中添加<div id="root" />
在 webpack 中使用 JS 的模块
webpack 将一整套文件视作模块,但别忘记它的主要用途:加载ES模块
直到 2015 年,JS 还没有一个标准的代码重用机制。曾经有很多尝试标准化这个方面,导致了这些年的分裂。
你可能听说过 AMD 模块、UMD 或 CommonJS。没有明确的赢家。最后 ES2015 出现,现在就有了一个“官方”模块系统。
webpack 让 ES 模块和模块化代码有了乐趣。
现在src/common/usersAPI.js的新文件中创建一个模块,代码如下:
const ENDPOINT = "https://jsonplaceholder.typicode.com/users/";
export function getUsers() {
return fetch(ENDPOINT)
.then(response => {
if (!response.ok) throw Error(response.statusText);
return response.json();
})
// .then(json => json);
}
现在在src/index.js中,你可以加载这个模块并使用这个函数:
import { getUsers } from "./common/usersAPI";
import './style.scss';
console.log('Hello webpack!');
getUsers().then(json => console.log(json))
控制台输出如下:
生产模式
正如前面介绍的,webpack有两个操作模式:开发和生产。目前我们也只在开发模式下工作。
在开发模式下,webpack 会将我们编写的代码,几乎是原始地加载到浏览器中。
在使用的时候没有压缩,这使得在开发中重新加载应用程序更快。
在生产模式下,webpack 进行了一些优化:**
- 使用
TerserWebpackPlugin压缩包的大小 - 使用
ModuleConcatenationPlugin作用域提升
它还设置process.env.NODE_ENV为“production”,这个环境变量对于在生产或开发中有条件地执行任务非常有用。
要在生产模式下配置 webpack,打开package.json并添加一个“build”脚本:
"scripts": {
"dev": "webpack --mode development",
"start": "webpack serve --open",
"build": "webpack --mode production",
"test": "echo \"Error: no test specified\" && exit 1"
},
现在,运行npm run build将生成一个小包。
使用 webpack 进行代码分割
代码分割是一种优化技术,它的目标是:
- 避免过大的包
- 避免依赖关系的重复
Webpack社区认为应用程序初始包的最大大小有一个限制:200KB。
为了理解为什么捆绑销售保持小规模是至关重要的,可以在Google上搜索 “The Cost of JavaScript”。
在webpack中有三种主要的方法来激活代码分割:
- 有多个entry入口
- 用
optimization.splitChunks - 用动态导入
第一种基于多 entry 对于小型项目很有效,但从长远来看是不可扩展的。这里只关注第二种和第三种。
使用 optimization.splitChunks 进行拆分
考虑一个使用 Moment.JS 的 JS 应用程序,此 JS 库用于计算时间和日期。
首先安装这个库:
npm i moment
现在清除src/index.js的内容并导入库:
import moment from "moment";
使用npm run build并查看输出后的结果:
整个库被捆绑在我们的应用程序的主要入口点。效果并不会很好。
使用optimization.splitChunks,我们可以将 moment.js 从主包中移除。
在webpack.config.js中进行配置:
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
module.exports = {
module: {
// omitted for brevity
},
optimization: {
splitChunks: { chunks: "all" }
},
// omitted for brevity
};
再次使用npm run build并查看输出结果:
这样的话,主 entry 文件的大小就更加合理。
即使将 moment.js 代码分割后仍然是一个巨大的库,还有更好的选择,比如 luxon 或 date-fns。
使用动态导入进行代码拆分
更强大的代码拆分技术就是使用动态导入有条件地加载代码。在 ECMAScript 2020 发布之前很久,webpack 就提供了动态导入的功能。
这种方式在 Vue 和 React 等现代前端库中得到广泛的应用(React 有自己的方式,不过概念是相同的)
代码分割可能用于:
-
在模块层面
-
在路由层面
例如,你可以在响应用户交互的时候有条件的加载 JS 模块,比如单击或鼠标移动。或者你可以在响应路由更改时加载代码的相关部分。
开始进行动态导入,更改src/index.html中的内容:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dynamic imports</title>
</head>
<body>
<button id="btn">Load!</button>
</body>
</html>
确保src/common/usersAPI.js中仍有 fetch 模块:
const ENDPOINT = "https://jsonplaceholder.typicode.com/users/";
export function getUsers() {
return fetch(ENDPOINT)
.then(response => {
if (!response.ok) throw Error(response.statusText);
return response.json();
})
// .then(json => json);
}
现在在src/index.js中创建以下逻辑:
const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
//
});
若此时运行npm run start,会发现并不会发生任何事。
现在假设我们希望在某个单击按钮后加载一个用户列表。一种“原生”方法可以使用静态导入去加载来自src/common/usersAPI.js的函数:
import { getUsers } from "./common/usersAPI";
const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
getUsers().then(json => console.log(json));
});
问题就是ES 模块是静态的,这意味着我们不能在运行时更改导入。
使用动态导入,我们可以选择何时加载我们的代码:
const getUserModule = () => import("./common/usersAPI");
const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
getUserModule().then(({ getUsers }) => {
getUsers().then(json => console.log(json));
});
});
这里我们创建了一个函数来动态加载模块:
const getUserModule = () => import("./common/usersAPI");
然后在事件监听器汇总,我们将then()链接到动态导入,并通过对象析构来提取getUsers函数,最后像往常一样使用我们的函数:
// omitted for brevity
btn.addEventListener("click", () => {
getUserModule().then(({ getUsers }) => {
getUsers().then(json => console.log(json));
});
});
当你第一次使用npm run start运行加载页面时,会看到控制台中加载的主包:
现在只有点击按钮才能加载./common/usersAPI。
你还可以通过导入路径前加上/* webpackChunkName: "name_herer" */来控制块的名称:
const getUserModule = () =>
import(/* webpackChunkName: "usersAPI" */ "./common/usersAPI");
const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
getUserModule().then(({ getUsers }) => {
getUsers().then(json => console.log(json));
});
});
现在这个块会有一个期望的名称:
更多话题
其他有趣的值得一看的有:
若有不当之处,欢迎评论指出