依据 webpack 官方文档,webpack 是一个 module bundler (模块打包器)。初次听到这个概念的时候,可能会一脸蒙蔽:这是个啥?对个人开发有啥影响么?javascript
为了更好理解 webpack,咱们须要先了解模块 Module 与打包 Bundle 的具体含义。只有将这两个概念理清楚了才会更清楚 webpack 的做用。css
不管使用何种编程语言开发大型应用,最关键的特性就是代码模块化。模块化的必要性在于:提升代码的开发效率,方便代码/功能的维护与重构。在C++里为命名空间,Java中为包,名称不同但解决的是同一问题。html
可是,最初的 JavaScript 并非用来编写大规模代码应用的,因而它的规范里面并无模块化这个标准。对于此,开源开发者提出了一些标准,如 CommoneJs 模块模型、异步模块定义(AMD)以及一些库,来实现模块化。java
幸运的是:ES6 为 JavaScript 带来了模块特性。但浏览器实现这一特性还须要一段时间。node
接着模块化的思路。在 JavaScript 程序开发过程当中,模块化会产生不少不一样的代码文件(如 js、css等)。举个栗子, SPA 页面 index.html 用到了三个JS文件 和 一个 CSS 文件,那么其经过script标签引入这些文件webpack
//文件结构
|- index.html
|- main.css
| - a.js
| - b.js
| - c.js
//代码演示
// index.html
<!doctype html>
<html>
<head><link href="main.css" rel="stylesheet"></head>
<body>
<div>hello world</div>
<script type="text/javascript" src="a.js"></script>
<script type="text/javascript" src="b.js"></script>
<script type="text/javascript" src="c.js"></script>
</body>
</html>
复制代码
由于有3 个 js 文件,因此浏览器须要发送三次 http 请求来获取这三个文件。当咱们的项目逐渐变大,有几十个到上百个 JavaScript 文件的时候,那问题会更严重。诸多问题都会暴露无遗(如网络延迟等)。es6
是否是把全部 JavaScript 文件合成一个文件就行了呢?是的。咱们确实能够这样作。web
可是,矛盾点来了:在开发阶段,咱们使用模块化开发;在实际应用中,咱们但愿可以将多个文件合并为一个文件。这该怎么办呢?chrome
很显然,在开发结束以后,咱们须要一个合并的过程。在开发完成后的这个合并过程就是打包。npm
能够说,webpack 所作的一切工做,都是为了实现模块打包。
将 webpack 理解为模块打包器,将 webpack 工做的过程理解为模块打包的过程。
webpack 的灵活性在于:整个过程的大部分因子咱们都是能够配置的,极为个性化。咱们先来认识整个过程的头与尾:Entry属性、Output属性。
Entry 属性指示 webpack 应该使用哪一个模块,来做为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。入口文件能够有多个。根据项目特色,能够以多种方式来配置 Entry。
Output 属性告诉 webpack 在哪里输出它所建立的 bundles,以及如何命名这些文件。它表示的是打包后输出文件的路径。
这时,咱们来了解一下 webpack 的基本工做机制。
在默认状况下,webpack 只可以识别 .js
.json
格式的文件,其余的文件它是没法识别的。对于更多格式的文件,webpack 为咱们提供了 Loader 选项,Loader 能够当作是 webpack 不一样格式文件的解析器。针对不一样的文件格式,咱们能够配置不一样的 Loader。
更进一步,咱们须要了解的是:webpack 的运行过程存在一个生命周期的过程。详细的,能够安装lifecycle-webpack-plugin 插件来查看生命周期信息。
plugin 插件,能够在webpack运行到某个阶段的时候(构建流程中的特定时机),帮你作某些事情(注⼊扩展逻辑来改变构建结果),相似于生命周期钩子的做用。
了解了上面的这些概念以后,咱们来看 webpack 的基本配置。默认的配置文件是项目目录下的 webpack.config.js 文件。
对 web 开发而言,经常使用的须要解析的文件无非是这几种:CSS文件、图片文件、字体文件。那么对应的 loader 配置为:
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"]
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: "file-loader",
options: {
name: "[name]_[hash:6].[ext]",
outputPath: "images/"
}
}
},
{
test: /\.(eot|ttf|woff|woff2)$/,
use: {
loader: "file-loader",
options: {
name: "[name].[ext]"
}
}
}
}
]
},
复制代码
Plugin 配置,就很是个性化了。随着后期优化的不断增强,咱们使用的插件会随之增多。在此只介绍两个插件:
有时候咱们会使用新的 ECMAScript 规范语法,但浏览器对这个新的语法规范可能不支持。因而须要降级处理。这个时候 Babel 就出现了。
Babel 是 JavaScript 编译器,能将 ES6(或更新的)代码转换成 ES5 代码,让咱们开发过程当中放⼼使⽤ JS 新特性而不⽤担忧兼容性问题。而且还能够经过插件机制根据需求灵活的扩展。
Babel 配置会由于 Babel 版本不一样而发生变化。最新的 Babel 7 配置,相比于 Babel 6 简化了很多。
Babel 在执行编译的过程当中,会从项目根⽬录下的 .babelrc JSON⽂件中读取配置。没有该文件会从对应 loader 的 options 地⽅读取配置。
npm i babel-loader @babel/core @babel/preset-env -D
这几个依赖包的做用以下:
//配置方式一:直接在webpack.config.js 对应 loader 的 options 地⽅配置
{
test: /\.js$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"]
}
}
}
//配置方式二:项目根⽬录下的 .babelrc 文件配置
//推荐方式二
{
presets: ["@babel/preset-env"]
}
复制代码
经过上面的几步还不够,默认的 babel 只支持 let 等一些基础的特性转换(只转换语法,不转换新的 API),promise 等高级特性尚未转换过来。这个时候还须要借助 @babel/polyfill,将es的新特性都装进来,来弥补低版本浏览器中缺失的特性。
npm install --save @babel/polyfill
值得注意的是 @babel/polyfill 是运行时依赖。
咱们能够尝试全局引入 polyfill 。
// index.js 顶部
import "@babel/polyfill";
复制代码
会发现打包的体积大了不少。这是由于 polyfill 默认会把全部特性注入进来。我但愿当我使用到了 es 6+ 特性的时候,才注入,不用到的不注入,从而减小打包的体积。且对于现代的已经支持高级特性的浏览器,不须要用到 polyfill。所以咱们要按需引入 polyfill。
修改.babelrc 文件配置
{
presets: [
[
"@babel/preset-env",
{
targets: { //编译后的代码支持的运行环境对象
edge: "17",
firefox: "60",
chrome: "67",
safari: "11.1"
},
corejs: 2, // 指定核心库版本
useBuiltIns: "usage" //按需注入
}
}
复制代码
重点是 useBuiltIns
这个选项。 useBuiltIns
选项是 babel 7 的新功能,这个选项告诉 babel 如何配置 @babel/polyfill 。 它有三个参数可使⽤:
咱们使用useBuiltIns: usage
即知足咱们的需求。
通过上面的操做,咱们已经能够作到了对 webpack 的基本配置,并让其可以运行 ES6 的代码了。在讲 webpack 的性能优化配置以前,咱们来尝试一下手写 Loader 与 Plugin 两个模块,以更好地理解这两个模块的做用。别太惊讶,这其实不难。往下看吧:
咱们知道,webpack 中的 loader 是各类格式文件的解析器。简单看待 loader,就能够理解为它是一个处理器。对输入进行一番处理(解析)以后,输出结果。
本身尝试编写一个 Loader,这个过程咱们能够更好理解 Loader 的工做原理。其过程是比较简单的。
在 webpack 中,Loader 就是一个函数(声明式函数,不能用箭头函数)。它经过参数项获取到源代码,作进一步的修饰处理后,返回处理过的源代码。
就让咱们来看看一个最简单的替换源码中文字符串的 loader (它的名字就叫 replaceLoader)是如何写出来的吧:
建立原材料(带放入 loader 的源代码,以及 loader):
//index.js
console.log("hello 亲爱的");
//replaceLoader.js
module.exports = function(source){
console.log(source, this, this.query);
return source.replace("亲爱的", "dear");
}
复制代码
在配置文件中使用 loader
//webpack.config.js
//node核⼼模块path来处理路径
const path = require('path')
···
module: {
rules: [ {
test: /\.js$/,
use: path.resolve(__dirname,"./loader/replaceLoader.js"),
options: {
name : "frank"
}
} ]
},
···
复制代码
如何给 loader 配置参数? loader 如何接收参数?
正如上面的 loader 中,咱们能够写入对应的参数放入到 options 中,那么 loader中如何接收到呢?有两种方式:
让 loader 返回多个信息
有的时候咱们不只仅但愿 loader 可以返回一个信息,而是多是更多的信息,好比:报错信息、source-map 信息等。webpack 中的 loader 能够经过调用 this.callback 来返回多个信息。
//replaceLoader.js
module.exports = function(source) {
const result = source.replace("亲爱的", this.query.name);
this.callback(null, result);
};
//callback中能够包含的参数项
//this.callback (
err: Error | null,
content: String | Buffer,
sourceMap?: SourceMap,
meta?: any
)
复制代码
让 loader 异步返回
//replaceLoader.js
//使用 setTimeout 3sec 再返回
module.exports = function(source) {
console.log(this, this.query);
const callback = this.async();
setTimeout(() => {
const result = source.replace("亲爱的", this.query.name);
callback(null, result);
}, 3000);
};
复制代码
经历了本身手写 loader 以后,咱们也能够试试本身手写一个简单的 plugin。来加深对于 webpack 的认识。
前面讲过:plugin 插件能够在 webpack 运行到某个阶段的时候(构建流程中的特定时机),帮你作某些事情(注⼊扩展逻辑来改变构建结果)。
在 webpack 中, plugin 必须是一个类(class),里面必须包含一个 apply 函数,该函数接收一个参数:compiler。就这么简单,没有更多的要求了。因此让咱们来试试吧:
咱们来尝试书写一个在 webpack 打包完成后,往 dist 文件夹增长一个 js 文件的 plugin。很简单没啥真实做用,只是为了演示生成 plugin 的过程而已。
建立 ./plugin/add-js-file-webpack-plugin.js
class AddJsFileWebpackPlugin {
constructor(){
}
apply(compiler){
}
}
module.exports = AddJsFileWebpackPlugin;
复制代码
配置文件里使用
const AddJsFileWebpackPlugin = require('./plugin/add-js-file-webpack-plugin.js');
...
plugins: [new AddJsFileWebpackPlugin({name:"frank"})]
...
复制代码
如何应用
在上面的配置中,咱们看到插件传入了参数{name:"frank"}
,咱们能够在构造器中捕获,并保存起来。
而后咱们如何使用apply
函数呢?这个时候就须要结合 webpack 的生命周期函数钩子了。在官网上,能够看到大量的钩子能够供咱们使用(有同步执行的钩子,也有异步执行的钩子)。
为了演示的目的,咱们使用了 compiler.emit
异步钩子和compiler.compile
同步钩子:
class AddJsFileWebpackPlugin {
constructor(options) {
this.name = options.name;
console.log(options);
}
apply(compiler) {
compiler.hooks.compile.tap("AddJsFileWebpackPlugin", compilation => {
console.log("执行了, ");
});
compiler.hooks.emit.tapAsync(
"AddJsFileWebpackPlugin",
(compilation, cb) => {
compilation.assets["finish.js"] = {
source: function() {
return "hello dear";
},
size: function() {
return 20;
}
};
cb();//异步钩子中,必须调用 cb
}
);
}
}
module.exports = AddJsFileWebpackPlugin;
复制代码
在这篇文章中,咱们对 webpack 作了一个基本的认识与了解,并经过简单的配置,让其可以简单运行代码。最后,经过本身手写 Loader 与 Plugin ,更好地认识其中的模块与工做原理。接下来,咱们来看看具体如何利用 webpack 来进行优化配置吧!