1. 首页

这些webpack 基础,你都懂了吗?

初识 webpack

概念

依据 webpack 官方文档,webpack 是一个 module bundler (模块打包器)。初次听到这个概念的时候,可能会一脸蒙蔽:这是个啥?对我的开发有啥影响么?

为了更好理解 webpack,我们需要先了解模块 Module 与打包 Bundle 的具体含义。只有将这两个概念理清楚了才会更清楚 webpack 的作用。

模块 Module

无论使用何种编程语言开发大型应用,最关键的特性就是代码模块化。模块化的必要性在于:提高代码的开发效率,方便代码/功能的维护与重构。在 C++ 里为命名空间,Java 中为包,名称不一样但解决的是同一问题。

但是,最初的 JavaScript 并不是用来编写大规模代码应用的,于是它的规范里面并没有模块化这个标准。对于此,开源开发者提出了一些标准,如 CommoneJs 模块模型、异步模块定义(AMD)以及一些库,来实现模块化。

幸运的是:ES6 为 JavaScript 带来了模块特性。但浏览器实现这一特性还需要一段时间。

打包 Bundle

接着模块化的思路。在 JavaScript 程序开发过程中,模块化会产生很多不同的代码文件(如 js、css等)。举个栗子, SPA 页面 index.html 用到了三个JS文件 和 一个 CSS 文件,那么其通过script标签引入这些文件


//文件结构 |- 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 文件的时候,那问题会更严重。诸多问题都会暴露无遗(如网络延迟等)。

是不是把所有 JavaScript 文件合成一个文件就好了呢?是的。我们确实可以这样做。

但是,矛盾点来了:在开发阶段,我们使用模块化开发;在实际应用中,我们希望能够将多个文件合并为一个文件。这该怎么办呢?

很显然,在开发结束之后,我们需要一个合并的过程。在开发完成后的这个合并过程就是打包。

可以说,webpack 所做的一切工作,都是为了实现模块打包。

webpack基本示意图

Entry 与 Output

将 webpack 理解为模块打包器,将 webpack 工作的过程理解为模块打包的过程。

webpack 的灵活性在于:整个过程的大部分因子我们都是可以配置的,极为个性化。我们先来认识整个过程的头与尾:Entry 属性、Output 属性。

Entry 属性指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。入口文件可以有多个。根据项目特点,可以以多种方式来配置 Entry。

Output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件。它表示的是打包后输出文件的路径。

Loader

这时,我们来了解一下 webpack 的基本工作机制。

在默认情况下,webpack 只能够识别 .js .json 格式的文件,其他的文件它是无法识别的。对于更多格式的文件,webpack 为我们提供了 Loader 选项,Loader 可以看成是 webpack 不同格式文件的解析器。针对不同的文件格式,我们可以配置不同的 Loader。

Plugins

更进一步,我们需要了解的是:webpack 的运行过程存在一个生命周期的过程。详细的,可以安装lifecycle-webpack-plugin 插件来查看生命周期信息。

这些webpack 基础,你都懂了吗?

plugin 插件,可以在webpack运行到某个阶段的时候(构建流程中的特定时机),帮你做某些事情(注⼊扩展逻辑来改变构建结果),类似于生命周期钩子的作用。

webpack 基本配置

了解了上面的这些概念之后,我们来看 webpack 的基本配置。默认的配置文件是项目目录下的 webpack.config.js 文件。

常用 Loader 配置

对 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 配置

Plugin 配置,就非常个性化了。随着后期优化的不断加强,我们使用的插件会随之增多。在此只介绍两个插件:

  • htmlwebpackplugin :生成创建html入口文件,并将打包后的文件自动插入其中。
  • CleanWebpackPlugin :自动清除特定文件夹内容,用于刷新每次打包加载的 output 文件夹。

Babel 配置

有时候我们会使用新的 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

这几个依赖包的作用如下:

  • babel-loader 是 webpack 与 babel 通信的桥梁
  • @babel/core 是 babel 的核心功能库
  • @babel/preset 里面包含了 es6/es7/es8 转 es5 的转换规则

基础配置


//配置方式一:直接在webpack.config.js 对应 loader 的 options 地⽅配置 { test: /\.js$/, use: { loader: "babel-loader", options: { presets: ["@babel/preset-env"] } } } //配置方式二:项目根⽬录下的 .babelrc 文件配置 //推荐方式二 { presets: ["@babel/preset-env"] } 复制代码

让 babel 支持高级特性

通过上面的几步还不够,默认的 babel 只支持 let 等一些基础的特性转换(只转换语法,不转换新的 API),promise 等高级特性还没有转换过来。这个时候还需要借助 @babel/polyfill,将 ECMAScript 的新特性都装进来,来弥补低版本浏览器中缺失的特性。

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 。 它有三个参数可以使⽤:

  • entry: 需要在 webpack 的⼊口⽂件⾥ import “@babel/polyfill” ⼀一次。 babel 会根据你的使⽤用情况导入垫片,没有使⽤的功能不会被导⼊入相应的垫⽚。
  • usage: 不需要 import ,全⾃动检测,但是要安装 @babel/polyfill 。(试验阶段)
  • false: 如果你 import “@babel/polyfill” ,它不会排除掉没有使用的垫⽚,程序体积会庞⼤。(不推荐)

我们使用useBuiltIns: usage 即满足我们的需求。

手写实现Loader 与 Plugin

经过上面的操作,我们已经可以做到了对 webpack 的基本配置,并让其能够运行 ES6 的代码了。在讲 webpack 的性能优化配置之前,我们来尝试一下手写 Loader 与 Plugin 两个模块,以更好地理解这两个模块的作用。别太惊讶,这其实不难。往下看吧:

手写Loader

我们知道,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中如何接收到呢?有两种方式:

    • this.query //通过 this.query 来接收配置文件传递进来的参数
    • loader-utils //该工具可以协助处理包含参数在内的更多细节
  • 让 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); }; 复制代码

手写Plugin

经历了自己手写 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 的生命周期函数钩子了。在官网上,可以看到大量的钩子可以供我们使用(有同步执行的钩子,也有异步执行的钩子)。

    plugin钩子

    为了演示的目的,我们使用了 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 来进行优化配置吧!戳这里

作者:猴哥别瞎说
链接:https://juejin.im/post/5e4bec01e51d4526e14941bc

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:这些webpack 基础,你都懂了吗?

链接:https://www.javascriptc.com/3950.html

« JavaScript 原型的深入指南
理解promise、 generator 、async & await 之间的联系»
Flutter 中文教程资源

相关推荐

QR code