1. 什么是webpack?
本质上,webpack是一个现代javascript应用程序的静态模块打包器。webpack处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
2.模块
在模块化编程中,开发者将程序分解成离散的功能块,将之称为模块。
每个模块具有比完整程序更小的接触面,使得校验、调试、测试轻而易举。 精心编写的_模块_提供了可靠的抽象和封装界限,使得应用程序中每个模块都具有条理清楚的设计和明确的目的。 (设计模式单一原则)
3.Hello Webpack webpack从4.0后,可以在不引用配置文件的情况下使用。下面通过一个例子简单的看下webpack的使用:
mkdir webpack-demo && cd webpack-demo
npm init -y
npm install webpack webpack-cli --save-dev
4.0后需要安装webpack-cli来使用webpack的一些功能(CLI即Command Line Interface,顾名思义,也就是命令行用户界面。)
现在我们创建如下的目录结构和内容:
webpack-demo
|- package.json
|- index.html
|- /src
|- index.js
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Webpack-Demo</title>
<script src="https://unpkg.com/lodash@4.16.6"></script>
</head>
<body>
<srcipt src="./src/index.js"></srcipt>
</body>
</html>
index.js
function component() {
var element = document.createElement('div');
// Lodash(目前通过一个 script 脚本引入)对于执行这一行是必需的
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
接下来对package.json
做一些调整,去掉main,增加private的属性为true,已确保我们的项目是私有的,避免意外发布:
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
+"private": true,
-"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^4.15.1",
"webpack-cli": "^3.0.8"
}
}
这时候,我们在控制台执行
npx webpack
运行成功后,我们可以看到目录下新增来一个dist文件夹
webpack-demo
+|- /dist
+ |- index.js
|- package.json
|- index.html
|- /src
|- index.js
webpack默认配置会生成打包文件dist,项目打包为main.js
在此示例中,<script> 标签之间存在隐式依赖关系。index.js 文件执行之前,还依赖于页面中引入的 lodash。之所以说是隐式的是因为 index.js 并未显式声明需要引入 lodash,只是假定推测已经存在一个全局变量 _。
使用这种方式去管理 JavaScript 项目会有一些问题:
- 无法立即体现,脚本的执行依赖于外部扩展库(external library)。
- 如果依赖不存在,或者引入顺序错误,应用程序将无法正常运行。
- 如果依赖被引入但是并没有使用,浏览器将被迫下载无用代码。
接下来我们针对上面提出的一些问题对代码进行一些调整 首先我们通过npm安装lodash库到我们的项目中:
npm install --save lodash
修改index.js
+ import _ from 'lodash';
function component() {
var element = document.createElement('div');
element.innerHTML = _.json(['Hello','Webpack'],' ');
return element;
}
document.body.appendChild(component());
在dist文件夹中新作增dist/index.html
<!doctype html>
<html>
<head>
<title>Webpack-Demo</title>
</head>
<body>
<script src="main.js"></script>
</body>
</html>
在控制台重新运行
npx webpack
这时候在浏览器中打开 /dist/index.html,如果一切访问都正常,你应该能看到以下文本:'Hello webpack'。
4.基础的四个核心概念
虽然我们可是在不进行任何配置的情况下可以使用webpack,但是在复杂的项目中,我们仍然需要使用配置文件对webpack做相应的设置已满足我们的需求。接下来我们将一步步的了解webpack的四个核心概念。
- 入口(entry)
- 输出(output)
- loader
- 插件(plugins)
首先我们在根目录下创建webpack.config.js
文件
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
现在我们通过新的配置文件再次执行构建
npx webpack --config webpack.config.js
执行结束后,我们可以在dist目录下发现新生成来bundle.js文件,我们修改dist目录下的index.html文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
- <script src="main.js"></script>
+ <script src="bundle.js"></script>
</body>
</html>
修改后我们再在浏览器中运行,可以得到同样的结果。
我们回来看下webpack.config.js
我们定义来一个entry
和output
4.1 入口 这里的entry就是入口起点。 入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
每个依赖项随即被处理,最后输出到称之为 bundles 的文件中。
4.2 出口 output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist。基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。
在上面的示例中,我们通过 output.filename 和 output.path 属性,来告诉 webpack bundle 的名称,以及我们想要 bundle 生成(emit)到哪里。可能你想要了解在代码最上面导入的 path 模块是什么,它是一个 Node.js 核心模块,用于操作文件路径。
如果每次都通过cli的方式来打包或者做其他的操作,我们会发现很麻烦,特别是在复杂情况下需要通过cli运行很多配置参数的时候。
这时候,我们可以借助NPM Scripts来做一些简化工作。我们修改package.json
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"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",
"lodash": "^4.17.5"
}
}
这时候我们就可以通过npm run build
来替代npx
命令运行
5.资源管理
webpack 最出色的功能之一就是,除了 JavaScript,还可以通过 loader 引入任何其他类型的文件。也就是说,以上列出的那些 JavaScript 的优点(例如显式依赖),同样可以用来构建网站或 web 应用程序中的所有非 JavaScript 内容。
接下来我们尝试整合一些其他资源,比如图像,看看 webpack 如何处理。
5.1加载CSS
为了可以在JavaScript模块中通过import引入一个css文件,需要先添加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$/,
+ use: [
+ 'style-loader',
+ 'css-loader'
+ ]
+ }
+ ]
+ }
};
我们尝试一次,通过在项目中添加一个新的style.css文件,并将其导入到index.js中。
src/style.css
.hello {
color: red;
}
src/index.js
import _ from 'lodash';
+ import './style.css';
function component() {
var element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ element.classList.add('hello');
return element;
}
document.body.appendChild(component());
运行构建命令
npm run build
构建成功后在浏览器中访问dist/index.html可以看到红色的Hello webpack
5.2 loader
loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。
通过上面的事例,我们可以看到loader中的的两个参数配置:
- test 属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。
- use 属性,表示进行转换时,应该使用哪个 loader。
webpack 编译器,当你碰到「在 require()/import 语句中被解析为 '.css' 的路径」时,在你对它打包之前,先使用 style-loader和css-loader 转换一下。
loader 用于对模块的源代码进行转换。loader 可以使你在 import 或"加载"模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的强大方法。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS文件!
5.3 使用loader
有三种使用 loader 的方式:
- 配置(推荐):在 webpack.config.js 文件中指定 loader。
- 内联:在每个 import 语句中显式指定 loader。
- CLI:在 shell 命令中指定它们。
配置的方式我们已经在上面讲了,如果对其他两种方式有兴趣,可以自行参考官方文档进行试验。
5.3 加载图片 接下来我们再看怎么加载图片资源,我们先安装file-loader
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$/,
use: [
'style-loader',
'css-loader'
]
},
+ {
+ test: /.(png|svg|jpg|gif)$/,
+ use: [
+ 'file-loader'
+ ]
+ }
]
}
};
我们向src中加入一张图片,然后在./src/index.js中引用
import _ from 'lodash';
import './style.css';
+ import Icon from './icon.png';
function component() {
var element = document.createElement('div');
// Lodash,现在由此脚本导入
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.classList.add('hello');
+ // 将图像添加到我们现有的 div。
+ var myIcon = new Image();
+ myIcon.src = Icon;
+
+ element.appendChild(myIcon);
return element;
}
document.body.appendChild(component());
src/style.css
.hello {
color: red;
+ background: url('./icon.png');
}
让我们重新构建,并再次打开 index.html 文件,和 Hello webpack 文本旁边的 img 元素一样,现在看到的图标是重复的背景图片。如果你检查此元素,你将看到实际的文件名已更改为像 5c999da72346a995e7e2718865d019c8.png 一样。这意味着 webpack 在 src 文件夹中找到我们的文件,并成功处理过它!
合乎逻辑下一步是,压缩和优化你的图像。查看 image-webpack-loader 和 url-loader,以了解更多关于如果增强加载处理图片功能。
加载更多其他类型的文件可以参考官方文档。
6. 输入管理
现在我们在src中新增一个js文件,并在src/index.js中使用
src/print.js
export default function printMe() {
console.log('I get called from print.js!');
}
src/index.js
import _ from 'lodash';
+ import printMe from './print.js';
function component() {
var element = document.createElement('div');
+ var btn = document.createElement('button');
// Lodash(目前通过一个 script 脚本引入)对于执行这一行是必需的
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.classList.add('hello');
var myIcon = new Image();
myIcon.src = icon;
element.appendChild(myIcon);
+ btn.innerHTML = 'Click me and check the console!';
+ btn.onclick = printMe;
+
+ element.appendChild(btn);
return element;
}
document.body.appendChild(component());
修改webpack.config.js,将将在 entry 添加 src/print.js 作为新的入口起点(print),然后修改 output,以便根据入口起点名称动态生成 bundle 名称:
const path = require('path');
module.exports = {
- entry: './src/index.js',
+ entry: {
+ app: './src/index.js',
+ print: './src/print.js'
+ },
output: {
- filename: 'bundle.js',
+ filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
执行构造,然后我们可以发现在dist目录下增加来app.bundle.js
和print.bundle.js
此时我们想正确的运行程序,就需要修改dist/index.html
<!doctype html>
<html>
<head>
- <title>Asset Management</title>
+ <title>Output Management</title>
+ <script src="./print.bundle.js"></script>
</head>
<body>
- <script src="./bundle.js"></script>
+ <script src="./app.bundle.js"></script>
</body>
</html>
到目前为止,我们都是通过手动引入所有资源到index.html 文件中,然而随着应用程序增长,并且一旦开始对文件名使用哈希(hash)]并输出多个 bundle,手动地对 index.html 文件进行管理,一切就会变得困难起来。因此我们用 HtmlWebpackPlugin 来解决这个问题。
6.1 设定HtmlWebpackPlugin
首先安装插件,并调整 webpack.config.js
npm install --save-dev html-webpack-plugin
webpack.config.js
const path = require('path');
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js'
},
+ plugins: [
+ new HtmlWebpackPlugin({
+ title: 'Output Management'
+ })
+ ],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
在我们构建之前,你应该了解,虽然在 dist/ 文件夹我们已经有 index.html 这个文件,然而 HtmlWebpackPlugin 还是会默认生成 index.html 文件。这就是说,它会用新生成的 index.html 文件,把我们的原来的替换。让我们看下在执行 npm run build 后会发生什么: 如果你在代码编辑器中将 index.html 打开,你就会看到 HtmlWebpackPlugin 创建了一个全新的文件,所有的 bundle 会自动添加到 html 中。
如果你想要了解更多 HtmlWebpackPlugin 插件提供的全部功能和选项,那么你就应该多多熟悉 HtmlWebpackPlugin 仓库。
你还可以看一下 html-webpack-template,除了默认模板之外,还提供了一些额外的功能。
6.2 清理/dist文件夹
你可能已经注意到,由于过去的指南和代码示例遗留下来,导致我们的 /dist 文件夹相当杂乱。webpack 会生成文件,然后将这些文件放置在 /dist 文件夹中,但是 webpack 无法追踪到哪些文件是实际在项目中用到的。
通常,在每次构建前清理 /dist 文件夹,是比较推荐的做法,因此只会生成用到的文件。让我们完成这个需求。
clean-webpack-plugin
是一个比较普及的管理插件,让我们安装和配置下。
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'
},
plugins: [
+ new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
title: 'Output Management'
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
现在执行 npm run build,再检查 /dist 文件夹。如果一切顺利,你现在应该不会再看到旧的文件,只有构建后生成的文件!
你可能会感兴趣,webpack及其插件似乎“知道”应该哪些文件生成。答案是,通过 manifest,webpack 能够对「你的模块映射到输出 bundle 的过程」保持追踪。如果你对通过其他方式来管理 webpack 的输出更感兴趣,那么首先了解 manifest 是个好的开始。
通过使用 WebpackManifestPlugin,可以直接将数据提取到一个 json 文件,以供使用。
我们不会在此展示一个关于如何在你的项目中使用此插件的完整示例,但是你可以仔细深入阅读 manifest 的概念页面,以及通过缓存指南来弄清如何与长期缓存相关联。
6.3 插件【plugins】
插件是 webpack 的支柱功能。webpack 自身也是构建于你在 webpack 配置中用到的相同的插件系统之上!
插件目的在于解决 loader 无法实现的其他事。
由于插件可以携带参数/选项,你必须在 webpack 配置中,向 plugins 属性传入 new 实例。
7 开发 到目前为止,我们每次修改代码都需要手动编译,非常麻烦。
webpack 中有几个不同的选项,可以帮助你在代码发生变化后自动编译代码:
- webpack's Watch Mode
- webpack-dev-server
- webpack-dev-middleware
我们这里主要讲解用得比较多的webpack-dev-server,如果对其他两种方式感兴趣的可以自行去了解。
webpack-dev-server
为你提供了一个简单的 web 服务器,并且能够实时重新加载(live reloading)。我们做以下设置:
npm install --save-dev webpack-dev-server
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'
},
devtool: 'inline-source-map',
+ devServer: {
+ contentBase: './dist'
+ },
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
title: 'Development'
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
以上配置告知 webpack-dev-server,在 localhost:8080 下建立服务,将 dist 目录下的文件,作为可访问文件。
让我们添加一个 script 脚本,可以直接运行开发服务器(dev server):
package.json
{
"name": "development",
"version": "1.0.0",
"description": "",
"main": "webpack.config.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"watch": "webpack --watch",
+ "start": "webpack-dev-server --open",
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"clean-webpack-plugin": "^0.1.16",
"css-loader": "^0.28.4",
"csv-loader": "^2.1.1",
"file-loader": "^0.11.2",
"html-webpack-plugin": "^2.29.0",
"style-loader": "^0.18.2",
"webpack": "^3.0.0",
"xml-loader": "^1.2.1"
}
}
现在,我们可以在命令行中运行 npm start,就会看到浏览器自动加载页面。如果现在修改和保存任意源文件,web 服务器就会自动重新加载编译后的代码。试一下!