详解什么是 Webpack

目录

前言

什么是模块化

为什么要模块化

模块化的规范

CommonJS

定义模块

加载模块

AMD

ES6 Module

什么是工程化

什么是 Webpack

​编辑

Webpack 解决什么问题

Webpack 和 Grunt 以及 Gulp 相比有什么特性

安装 Webpack

Webpack 核心概念

​入口(entry)

输出(output)

loader

插件(plugin)

模式(mode)

浏览器兼容性(browser compatibility)

环境(environment)


 

前言

随着 Web 前端的不断发展,传统网页开发正在逐渐往 Web 应用(Web Application,简称 WebAPP)的开发方式转变,页面开始变得越来越复杂,复杂的应用场景必然引起技术的进步,还会出现新的技术手段来解决现有问题。前端模块化和工程化的呼声越来越高,随着前些年大行其道的 Grunt、Gulp、FIS 等构建工具的发展,带动了一波前端工程化热潮。近几年,经过 React、Vue 库这些年的扩张,大型 Webapp 不再局限于手写 jQuery 操作 DOM,让大型 Webapp 有了全新的开发体验。

在这个过程中,前端逐渐发展成了模块化和单页应用(single-page application,简称 SPA)为主的形式,在这种形态和 React、Vue 这些库的普及下,Webpack 越来越被大家当成了主流构建工具。

什么是模块化

说起 Webpack,大家都知道这是一个模块化构建工具,那么究竟什么是模块化呢?

模块化是指解决一个复杂问题时,自顶向下逐层把系统划分成若干模块的过程,有多种属性,分别反映其内部特性。

 模块化被越来越多的应用到我们的日常生活中,在我的印象中,小时候家电(比如收音机、电视)坏了都是拿到店里去找老师傅维修,现在家电都是模块化的,检测下哪里坏了直接换个新模块就可以了,由此可见,模块化不仅仅是个前端概念,在我们生活场景中也大量的充斥着模块化,模块化让我们的生活效率更高。

前端模块化一般指的是 JavaScript 的模块,最常见的是 Nodejs 的 NPM 包,每个模块,可能是最小甚至是最优的代码组合,也可能是为了解决某些问题,包括很多特定模块组成的大模块。如果没有模块化,可能大家编写代码时遇见最多的就是复制(copy)。

当我们需要某个功能的代码时,可以查看一下,自己在哪个以前的项目写过,有写过,我们就会 copy 过来,copy 多了,自然代码的可维护性就会下降。

为什么要模块化

了解历史有助于理解,在 Web 开发初期,常用 JS 文件加载使用 <script> 标签完成,这种方式带来的问题是每个文件中声明的变量都会出现在全局作用域内,即 window 对象中。模块间的依赖关系完全由文件的加载顺序所决定,显然这种模块组织方式存在很多弊端。

  • 全局作用域下容易造成变量冲突
  • 文件只能按照 <script> 标签的书写顺序进行加载
  • 开发者需自己解决模块/代码库的依赖关系
  • 大型 Web 项目中这样的加载方式导致文件冗长且难以管理

因此前端模块化是将一个复杂的程序依据一定的规则 (规范) 封装成几个块 (文件),并进行组合在一起;块的内部数据与实现是私有的,只是向外部暴露一些接口 (方法) 与外部其它模块通信。

注:所有在全局作用域中声明的变量、函数都会变成 window 对象的属性和方法。

然而全局变量和与在 window 对象上定义的属性还是有一点区别的,全局变量不能通过 delete 操作符删除,而在 window 上定义的属性是可以的。

// 全局变量
// 可以通过 window.test 访问
var test = 5;

window.hello = "world";

// IE<9时报错 其他返回false
delete window.test; 

// IE<9报错 其他返回true
delete window.hello;

模块化的规范

模块化之后的代码,我们考虑更多的是:代码使用和维护成本的问题。如何定义模块以及如何处理模块间的依赖关系。所以有了很多模块化的规范:​CommonJS、AMD 和 ES6 Module规范(另外还有 CMD、UMD 等),下面我们来简单看一下。

CommonJS

CommonJS:是 Nodejs 广泛使用的一套模块化规范,是一种同步加载模块依赖的方式:

每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见,需要导出外部才能访问。

CommonJS 规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作,使得CommonJS 规范不适用于浏览器环境,如果在浏览器中运行,假如模块加载时间很长,后面的代码就无法执行,使整个应用就会停在那里等。

这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。

  • require:引入一个模块
  • exports:导出模块内容
  • module:模块本身

定义模块

// module.js
let text = 'hello world';
let helloText = function () {
  console.log(text);
};

module.exports = { text, helloText }
// 或者
exports.helloText = helloText;

加载模块

// 通过 require 引入依赖
let module = require('./module.js');

module.helloText(); // hello world

AMD

AMD:是 JS 模块加载库RequireJS提出并且完善的一套模块化规范,AMD 是一种异步加载模块依赖的方式:

采用异步模块定义、异步方式加载模块,模块的加载不影响后面语句的运行。所有依赖模块的语句,都定义在一个回调函数中,等到加载完成之后,回调函数才执行。通过define方法去定义模块,require 方法去加载模块。浏览器端一般采用 AMD 规范。

  • id:模块的 id
  • dependencies:模块依赖
  • factory:模块的工厂函数,即模块的初始化操作函数
  • require:引入模块

requireJS 定义了一个函数 define,它是全局变量,用来定义模块

define(id?, dependencies?, factory);
  • id:可选参数,用来定义模块的标识,如果没有提供该参数,脚本文件名(去掉拓展名)

  • dependencies:是一个当前模块依赖的模块名称数组,也是可选的参数,每个依赖的模块的输出将作为参数一次传入 factory 中。

  • factory:工厂方法,模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值

 如果没有指定 dependencies,那么它的默认值是 ["require", "exports", "module"]

define(function(require, exports, module) {})

AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:

require([module], callback);

第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。

定义一个名为 myModule 的模块,它依赖 jQuery 模块:

define('myModule', ['jquery'], function($) {
    // $ 是 jquery 模块的输出
    $('body').text('hello world');
});
// 使用
require(['myModule'], function(myModule) {});

 依赖多个模块的定义:

define(['jquery', './math.js'], function($, math) {
    // $ 和 math 一次传入 factory
    $('body').text('hello world');
});

在模块定义内部引用依赖:

define(function(require) {
    var $ = require('jquery');
    $('body').text('hello world');
});

注意:在 webpack 中,模块名只有局部作用域,在 Require.js 中模块名是全局作用域,可以在全局引用。

ES6 Module

ES6 推出的一套模块化规范:

  • import:引入模块依赖
  • export:模块导出

Tips:除了上面三大主流规范,还有 CMD 可以用于浏览器端,模块的加载是异步的,模块使用时才会加载执行(国产库 SeaJS 提出来的一套模块规范)和 UMD(兼容 CommonJS 和 AMD 一套规范)。

目前多数模块的封装,既可以在 Node.js 环境(服务端)又可以在 Web 环境 (客户端即浏览器) 运行,所以一般会采用 UMD 的规范,后面 Webpack 针对 lib 库的封装会有进一步介绍。

什么是工程化

当我们开发的 Web 应用越来越复杂的时候,会发现我们面临的问题会逐渐增多:

  1. 模块多了,依赖管理怎么做?
  2. 页面复杂度提升之后,多页面、多系统、多状态怎么办?
  3. 团队扩大之后,团队合作怎么做?
  4. 怎么解决多人研发中的性能、代码风格等问题?
  5. 如何权衡研发效率和产品迭代的问题?

这些问题就是软件工程需要解决的问题。工程化的问题需要运用工程化工具来解决,得益于 Nodejs 的发展,前端这些年在工程化上,取得了不俗的成绩。

前端工程化早期,是以 Grunt、Gulp 等构建工具为主的阶段,这个阶段解决的是重复任务的问题,它们将某些功能拆解成固定步骤的任务,然后编写工具来解决,比如:图片压缩、地址添加 hash、替换等,都是固定套路的重复工作,源代码无法直接运行,必须通过转换后才可以正常运行。

而现阶段的 Webpack 则更像是从一套解决 JavaScript 模块化依赖打包开始,利用强大的插件机制,逐渐解决前端资源依赖管理问题,依附社区力量逐渐进化成一套前端工程化解决方案。

什么是 Webpack

本质上,Webpack 是一个现代 JavaScript 应用程序的静态模块打包器(static module bundler)。

在 Webpack 处理应用程序时,它会在内部创建一个依赖图(dependency graph),用于映射到项目需要的每个模块,然后将所有这些依赖生成到一个或多个 bundle,以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其转换和打包为合适的格式供浏览器使用。

51844e2705794d14a2f9ab8077c71f5c.png

Webpack 解决什么问题

Webpack 可以做到按需加载。像 Grunt、Gulp 这类构建工具,打包的思路是:遍历源文件匹配规则打包,这个过程中做不到按需加载,即对于打包起来的资源,到底页面用不用,打包过程中是不关心的。

Webpack 跟其他构建工具本质上不同之处在于:Webpack 是从入口文件开始,经过模块依赖加载、分析和打包三个流程完成项目的构建。在加载、分析和打包的三个过程中,可以针对性的做一些解决方案,达到按需加载的目的。

当然,Webpack 还可以轻松的解决传统构建工具解决的问题:

  • 模块化打包:一切皆模块,JS 是模块,CSS 等也是模块;
  • 语法糖转换:比如 ES6 转 ES5、TypeScript。这些标准目前都无法被浏览器兼容,因此需要构建工具编译,例如使用 Babel 编译 ES6 语法
  • 预处理器编译:比如 Less、Sass 等;
  • 项目优化:比如压缩将JS、CSS代码混淆压缩,让代码体积更小,加载更快、CDN;
  • 解决方案封装:通过强大的 Loader 和插件机制,可以完成解决方案的封装,比如 PWA;
  • 流程对接:比如测试流程、语法检测等。
  • 自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器。

构建其实是工程化、自动化思想在前端开发中的体现,将一系列流程用代码去实现,让代码自动化地执行这一系列复杂的流程。以上这些操作如果是人员处理就显得很繁琐,使用 Webpack 静态模块打包器自动化打包将使一切变得简单。构建为前端开发注入了更大的活力,提高我们的生产力。

Webpack 和 Grunt 以及 Gulp 相比有什么特性

其实 Webpack 和另外两个并没有太多的可比性,Gulp/Grunt 是一种能够优化前端的开发流程的工具,而 Webpack 是一种模块化的解决方案,不过 Webpack 的优点使得 Webpack 可以替代Gulp/Grunt 类的工具。

安装 Webpack

Webpack 可以使用 npm 安装

// 全局安装
npm install -g webpack

// 安装到你的项目目录
npm install --save-dev webpack

注:在 mac 环境下全局安装 Webpack 时提示找不到命令,可以通过卸载 node 重新安装后解决。

Webpack 核心概念

webpack 的配置文件 webpack.config.js,从 v4.0.0 开始,webpack 可以不用再引入一个配置文件来打包项目,然而,它仍然有着 高度可配置性,可以很好满足你的需求。

在开始前你需要先理解一些核心概念:

  • ​入口(entry)
  • 输出(output)
  • loader
  • 插件(plugin)
  • 模式(mode)
  • 浏览器兼容性(browser compatibility)
  • 环境(environment)

​入口(entry)

入口起点(entry point) 指示 webpack 应该使用哪个模块,来作为构建其内部 依赖图(dependency graph) 的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。

默认值是 ./src/index.js,但你可以通过在 webpack configuration 中配置 entry 属性,来指定一个(或多个)不同的入口起点。例如:

module.exports = {
  entry: './path/to/my/entry/file.js',
};

输出(output)

output 属性告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。主要输出文件的默认值是 ./dist/main.js,其他生成文件默认放置在 ./dist 文件夹中。

你可以通过在配置中指定一个 output 字段,来配置这些处理过程:

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',
  },
};

在上面的示例中,我们通过 output.filename 和 output.path 属性,来告诉 webpack bundle 的名称,以及我们想要 bundle 生成(emit)到哪里。可能你想要了解在代码最上面导入的 path 模块是什么,它是一个 Node.js 核心模块,用于操作文件路径。

loader

webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 模块,以供应用程序使用,以及被添加到依赖图中。

webpack 的其中一个强大的特性就是能通过 import 导入任何类型的模块(例如 .css 文件),其他打包程序或任务执行器的可能并不支持。我们认为这种语言扩展是很有必要的,因为这可以使开发人员创建出更准确的依赖关系图。

 在更高层面,在 webpack 的配置中,loader 有两个属性:

  1. test 属性,识别出哪些文件会被转换。
  2. use 属性,定义出在进行转换时,应该使用哪个 loader。
const path = require('path');

module.exports = {
  output: {
    filename: 'my-first-webpack.bundle.js',
  },
  module: {
    rules: [{ test: /\.txt$/, use: 'raw-loader' }],
  },
};

以上配置中,对一个单独的 module 对象定义了 rules 属性,里面包含两个必须属性:test 和 use。这告诉 webpack 编译器(compiler) 如下信息:

“嘿,webpack 编译器,当你碰到「在 require()/import 语句中被解析为 '.txt' 的路径」时,在你对它打包之前,先 use(使用) raw-loader 转换一下。”

重要的是要记住,在 webpack 配置中定义 rules 时,要定义在 module.rules 而不是 rules 中。为了使你便于理解,如果没有按照正确方式去做,webpack 会给出警告。 

请记住,使用正则表达式匹配文件时,你不要为它添加引号。也就是说,/\.txt$/ 与 '/\.txt$/' 或 "/\.txt$/" 不一样。前者指示 webpack 匹配任何以 .txt 结尾的文件,后者指示 webpack 匹配具有绝对路径 '.txt' 的单个文件; 这可能不符合你的意图。 

插件(plugin)

loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。

想要使用一个插件,你只需要 require() 它,然后把它添加到 plugins 数组中。多数插件可以通过选项(option)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new 操作符来创建一个插件实例。

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' })],
};

在上面的示例中,html-webpack-plugin 为应用程序生成一个 HTML 文件,并自动将生成的所有 bundle 注入到此文件中。

模式(mode)

通过选择 developmentproduction 或 none 之中的一个,来设置 mode 参数,你可以启用 webpack 内置在相应环境下的优化。其默认值为 production

module.exports = {
  mode: 'production',
};

浏览器兼容性(browser compatibility)

​Webpack 支持所有符合 ES5 标准 的浏览器(不支持 IE8 及以下版本)。webpack 的 import() 和 require.ensure() 需要 Promise。如果你想要支持旧版本浏览器,在使用这些表达式之前,还需要提前加载 polyfill。

polyfill(polyfiller) 指的是一个代码块(通常是 Web 上的 JavaScript)。这个代码块向开发者提供了一种技术, 这种技术可以让浏览器提供原生支持,抹平不同浏览器对API兼容性的差异。使本身不支持该功能的 Web 浏览器上实现该功能的代码。​

环境(environment)

Webpack 5 运行于 Node.js v10.13.0+ 的版本。

 

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值