Webpack实战:入门、进阶与调优(txt+pdf+epub+mobi电子书下载)


发布时间:2020-06-11 15:31:37

点击下载

作者:居玉皓

出版社:机械工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

Webpack实战:入门、进阶与调优

Webpack实战:入门、进阶与调优试读:

前言

本书是我从2017年冬天开始动笔的。在写作本书之前的一段时间,我一直负责与前端项目构建相关的工作,也曾做过一系列Webpack在线课程,但是当接到写一本关于Webpack的书的提议时,我着实犹豫了很久。最大的担忧在于Webpack本身已经有详尽的文档,社区中也有无数关于它的博客文章,自己如何能找到一个新的角度,让读者有所受益。

于是我开始回想自己最初学习Webpack时的经历——在了解它的过程中遇到哪些曲折,使用时碰到了哪些问题,有哪些点是我觉得如果当初我早就知道就好了的。通过回忆这些曾遇到过的磕磕绊绊,我逐渐找到了写作本书的出发点——用我的语言尽可能简单、直白地介绍Webpack,让从来没有接触过Webpack的开发者也可以比较容易上手;同时把我所趟过的一些坑写出来,让读到的人少走一些弯路。

有时能听到一种戏称——Webpack配置工程师,从这里面大概能体会到Webpack的使用并不简单。而这本书的作用之一大概就是把里面比较晦涩的部分解释清楚,让大家了解Webpack是怎么工作的,它其实并不神秘。本书内容

本书共10章。第1章是一个导引,对Webpack有一定基础的读者可以选择略过。第2章从头梳理了模块的概念。第3章至第7章介绍了Webpack的各项特性以及基本的使用场景。第8章和第9章则是进一步的优化以及一些高级的使用方法。最后第10章介绍了其他打包工具并对这些工具进行了各项特性的对比。代码示例

书中有很多代码片段,为了在线运行方便,我在GitHub上整理了一个示例仓库,如果需要,可以到https://github.com/yuhaoju/webpack-config-handbook进行查看。致谢

我要特别感谢我的朋友们。写书是一个漫长而孤独的过程,在此期间我得到了很多鼓励和督促。有些时候朋友还要担当我的校对者,即便对书中的内容不了解也会帮忙查看其中的错误,并提出阅读体验方面的建议,对我整本书的写作有很大的帮助。

同时要感谢杨福川和李艺两位编辑,在前期规划以及写作本书的过程中给予我不少指导,没有他们就不可能有这本书的顺利完成。

最后,感谢阅读本书的你,希望你能喜欢。第1章Webpack简介

本书第1章会对Webpack进行大致介绍,让大家对Webpack有一个初步的了解。主要包括以下几个部分:

·何为Webpack;

·使用Webpack的意义;

·安装Webpack;

·如何开始一个Webpack工程。

如果你已经是一个Webpack老手,可以选择跳过这一章;假如你对Webpack还不是很熟悉,那么本章会带你快速上手。让我们开始吧!1.1 何为Webpack

Webpack是一个开源的JavaScript模块打包工具,其最核心的功能是解决模块之间的依赖,把各个模块按照特定的规则和顺序组织在一起,最终合并为一个JS文件(有时会有多个,这里讨论的只是最基本的情况)。这个过程就叫作模块打包。

你可以把Webpack理解为一个模块处理工厂。我们把源代码交给Webpack,由它去进行加工、拼装处理,产出最终的资源文件,等待送往用户。

没有接触过打包工具的读者可能会疑惑,在Web开发中我们打交道的无非是HTML、CSS、JS等静态资源,为什么不直接将工程中的源文件发布到服务器或CDN,而要交给Webpack处理呢?这两者之间有什么不同?接下来我们就要阐述使用Webpack的意义。1.2 为什么需要Webpack

开发一个简单的Web应用,其实只需要浏览器和一个简单的编辑器就可以了。最早的Web应用就是这么开发的,因为需求很简单。当应用的规模大了之后,就必须借助一定的工具,否则人工维护代码的成本将逐渐变得难以承受。学会使用工具可以让开发效率成倍地提升,所谓“工欲善其事,必先利其器”就是这个意思。

说回Webpack,既然它解决的最主要问题是模块打包,那么为了更好地阐述Webpack的作用,我们必须先谈谈模块。1.2.1 何为模块

我们每时每刻都在与模块打交道。比如,在工程中引入一个日期处理的npm包,或者编写一个提供工具方法的JS文件,这些都可以称为模块。

在设计程序结构时,把所有代码都堆到一起是非常糟糕的做法。更好的组织方式是按照特定的功能将其拆分为多个代码段,每个代码段实现一个特定的目的。你可以对其进行独立的设计、开发和测试,最终通过接口来将它们组合在一起。这就是基本的模块化思想。

如果把程序比作一个城市,这个城市内部有不同的职能部门,如学校、医院、消防局等。程序中的模块就像这些职能部门一样,每一个都有其特定的功能。各个模块协同工作,才能保证程序的正常运转。1.2.2 JavaScript中的模块

在大多数程序语言中(如C、C++、Java),开发者都可以直接使用模块化进行开发。工程中的各个模块在经过编译、链接等过程后会被整合成单一的可执行文件并交由系统运行。

对于JavaScript来说,情况则有所不同。在过去的很长一段时间里,JavaScript这门语言并没有模块这一概念。如果工程中有多个JS文件,我们只能通过script标签将它们一个个插入页面中。

为何偏偏JavaScript没有模块呢?如果要追溯历史原因,JavaScript之父——Brendan Eich最初设计这门语言时只是将它定位成一个小型的脚本语言,用来实现网页上一些简单的动态特性,远没有考虑到会用它实现今天这样复杂的场景,模块化当然也就显得多余了。

随着技术的发展,JavaScript已经不仅仅用来实现简单的表单提交等功能,引入多个script文件到页面中逐渐成为一种常态,但我们发现这种做法有很多缺点:

·需要手动维护JavaScript的加载顺序。页面的多个script之间通常会有依赖关系,但由于这种依赖关系是隐式的,除了添加注释以外很难清晰地指明谁依赖了谁,这样当页面中加载的文件过多时就很容易出现问题。

·每一个script标签都意味着需要向服务器请求一次静态资源,在HTTP 2还没出现的时期,建立连接的成本是很高的,过多的请求会严重拖慢网页的渲染速度。

·在每个script标签中,顶层作用域即全局作用域,如果没有任何处理而直接在代码中进行变量或函数声明,就会造成全局作用域的污染。

模块化则解决了上述的所有问题。

·通过导入和导出语句我们可以清晰地看到模块间的依赖关系,这点在后面会做详细的介绍。

·模块可以借助工具来进行打包,在页面中只需要加载合并后的资源文件,减少了网络开销。

·多个模块之间的作用域是隔离的,彼此不会有命名冲突。

从2009年开始,JavaScript社区开始对模块化进行不断的尝试,并依次出现了AMD、CommonJS、CMD等解决方案。但这些都只是由社区提出的,并不能算语言本身的特性。而在2015年,ECMAScript 6.0(ES6)正式定义了JavaScript模块标准,使这门语言在诞生了20年之后终于拥有了模块这一概念。

ES6模块标准目前已经得到了大多数现代浏览器的支持,但在实际应用方面还需要等待一段时间。主要有以下几点原因:

·无法使用code splitting和tree shaking(Webpack的两个特别重要的特性,之后的章节会介绍)。

·大多数npm模块还是CommonJS的形式,而浏览器并不支持其语法,因此这些包没有办法直接拿来用。

·仍然需要考虑个别浏览器及平台的兼容性问题。

那么,如何才能让我们的工程在使用模块化的同时也能正常运行在浏览器中呢?这就到了模块打包工具出场的时候了。1.2.3 模块打包工具

模块打包工具(module bundler)的任务就是解决模块间的依赖,使其打包后的结果能运行在浏览器上。它的工作方式主要分为两种:

·将存在依赖关系的模块按照特定规则合并为单个JS文件,一次全部加载进页面中。

·在页面初始时加载一个入口模块,其他模块异步地进行加载。

目前社区中比较流行的模块打包工具有Webpack、Parcel、Rollup等。1.2.4 为什么选择Webpack

对比同类模块打包工具,Webpack具备以下几点优势。

1)Webpack默认支持多种模块标准,包括AMD、CommonJS,以及最新的ES6模块,而其他工具大多只能支持一到两种。这对于一些同时使用多种模块标准的工程非常有用,Webpack会帮我们处理好不同类型模块之间的依赖关系。

2)Webpack有完备的代码分割(code splitting)解决方案。从字面意思去理解,它可以分割打包后的资源,首屏只加载必要的部分,不太重要的功能放到后面动态地加载。这对于资源体积较大的应用来说尤为重要,可以有效地减小资源体积,提升首页渲染速度。

3)Webpack可以处理各种类型的资源。除了JavaScript以外,Webpack还可以处理样式、模板,甚至图片等,而开发者需要做的仅仅是导入它们。比如你可以从JavaScript文件导入一个CSS或者PNG,而这一切最终都可以由第4章讲到的loader来处理。

4)Webpack拥有庞大的社区支持。除了Webpack核心库以外,还有无数开发者来为它编写周边插件和工具,绝大多数的需求你都可以直接找到已有解决方案,甚至会有多个解决方案供你挑选。

以上我们对Webpack进行了简要介绍,但是说再多也不如实际操作一次,现在让我们来真正上手试一试吧。1.3 安装

Webpack对于操作系统没有要求,使用Windows、Mac、Linux操作系统均可。它唯一的依赖就是Node.js,下面来对其进行安装。

Webpack对Node.js的版本是有一定要求的,推荐使用Node.js的LTS(Long Term Support,长期维护)版本。LTS版本是Node.js在“当前阶段”较为稳定的版本,具体版本号及发布计划可以到https://github.com/nodejs/Release进行查看。LTS版本中不会包含过于激进的特性,并且已经经过了一定时间的检验,比较适合生产环境。大多数Node.js模块也都会依照LTS版本的特性进行支持。

在Node.js官网(https://nodejs.org/)上,一般都会把LTS版本放在较为醒目的位置,用户根据自己的系统环境进行下载和安装即可。安装完成后,打开命令行并执行node–v,不出意外的话会显示当前Node.js的版本号,代表已经安装成功。

接下来,我们需要使用Node.js的包管理器npm来安装Webpack。使用过npm的读者应该知道,安装模块的方式有两种:一种是全局安装,一种是本地安装。对于Webpack来说,我们也有这两种选择。

全局安装Webpack的好处是npm会帮我们绑定一个命令行环境变量,一次安装、处处运行;本地安装则会添加其成为项目中的依赖,只能在项目内部使用。这里建议使用本地安装的方式,主要有以下两点原因:

·如果采用全局安装,那么在与他人进行项目协作的时候,由于每个人系统中的Webpack版本不同,可能会导致输出结果不一致。

·部分依赖于Webpack的插件会调用项目中Webpack的内部模块,这种情况下仍然需要在项目本地安装Webpack,而如果全局和本地都有,则容易造成混淆。

基于以上两点,我们选择在工程内部安装Webpack的方式。首先新建一个工程目录,从命令行进入该目录,并执行npm的初始化命令。npm init # 如果你使用 yarn,则为 yarn init

此时会要求你输入项目的基本信息,因为这里只是为了生成一个示例,根据提示操作就好。然后,我们会看到目录中生成了一个package.json文件,它相当于npm项目的说明书,里面记录了项目名称、版本、仓库地址等信息。

接下来执行安装Webpack的命令:npm install webpack webpack-cli –-save-dev

这里我们同时安装了webpack以及webpack-cli。webpack是核心模块,webpack-cli则是命令行工具,在本例中两者都是必需的。

安装结束之后,在命令行执行npx webpack-v以及npx webpack-cli-v,可显示各自的版本号,即证明安装成功。注意 由于我们将Webpack安装在了本地,因此无法直接在命令行内使用“webpack”指令。工程内部只能使用npx webpack的形式,本章后面会介绍简化该命令的方法。1.4 打包第一个应用

让我们趁热打铁来打包刚刚的示例工程。如果你是第一次接触Webpack,建议按照下面的指引一步步进行操作。代码中不熟悉的地方也不必深究,这个示例只是为了让我们直观地认识Webpack的一些特性。1.4.1 Hello World

首先,我们在工程目录下添加以下几个文件。

index.js:import addContent from './add-content.js';document.write('My first Webpack app.
');addContent();

add-content.js:export default function() { document.write('Hello world!');}

index.html: My first Webpack app.

然后在控制台输入打包命令:npx webpack --entry=./index.js --output-filename=bundle.js --mode=development

用浏览器打开index.html,这时应该可以看到在页面上会显示“My first Webpack app.Hello world!”,如图1-1所示。图1-1 index.html输出结果

刚刚Webpack帮我们完成了一项最基本的模块组装工作,现在让我们回顾一下刚刚输入的指令。

命令行的第1个参数entry是资源打包的入口。Webpack从这里开始进行模块依赖的查找,得到项目中包含index.js和add-content.js两个模块,并通过它们来生成最终产物。

命令行的第2个参数output-filename是输出资源名。你会发现打包完成后工程中出现了一个dist目录,其中包含的bundle.js就是Webpack的打包结果。

最后的参数mode指的是打包模式。Webpack为开发者提供了development、production、none三种模式。当置于development和production模式下时,它会自动添加适合于当前模式的一系列配置,减少了人为的工作量。在开发环境下,一般设置为development模式就可以了。

为了验证打包结果,可以用浏览器打开index.html。项目中的index.js和content.js现在已经成为了budnle.js,被页面加载和执行,并输出了各自的内容。1.4.2 使用npm scripts

从上面的例子不难发现,我们每进行一次打包都要输入一段冗长的命令,这样做不仅耗时而且容易出错。为了使命令行指令更加简洁,我们可以在package.json中添加一个脚本命令。

编辑工程中的package.json文件:…… "scripts": { "build": "webpack --entry=./index.js --output-filename=bundle.js --mode=development" },……

scripts是npm提供的脚本命令功能,在这里我们可以直接使用由模块所添加的指令(比如用“webpack”取代之前的“npx webpack”)。

为了验证打包结果,可以对add-content.js的内容稍加修改:export default function() { document.write('I\'m using npm scripts!');}

重新执行打包,这次输入npm命令即可:npm run build

打开浏览器验证效果,如图1-2所示。图1-2 index.html内容变为了“I’m using npm scripts!”1.4.3 使用默认目录配置

上面的index.js是放在工程根目录下的,而通常情况下我们会分别设置源码目录与资源输出目录。工程源代码放在/src中,输出资源放在/dist中。本书后续章节的示例也会按照该标准进行目录划分。

在工程中创建一个src目录,并将index.js和add-content.js移动到该目录下。对于资源输出目录来说,Webpack已经默认是/dist,我们不需要做任何改动。

另外需要提到的是,Webpack默认的源代码入口就是src/index.js,因此现在可以省略掉entry的配置了。编辑package.json:…… "scripts": { "build": "webpack --output-filename=bundle.js --mode=development" },……

虽然目录命名并不是强制的,Webpack提供了配置项让我们去进行更改,但还是建议遵循统一的命名规范,这样会使得大体结构比较清晰,也利于多人协作。1.4.4 使用配置文件

为了满足不同应用场景的需求,Webpack拥有非常多的配置项以及相对应的命令行参数。我们可以通过Webpack的帮助命令来进行查看。npx webpack –h

部分参数如图1-3所示。图1-3 Webpack配置参数

从之前我们在package.json中添加的脚本命令来看,当项目需要越来越多的配置时,就要往命令中添加更多的参数,那么到后期维护起来就会相当困难。为了解决这个问题,可以把这些参数改为对象的形式专门放在一个配置文件里,在Webpack每次打包的时候读取该配置文件即可。

Webpack的默认配置文件为webpack.config.js(也可以使用其他文件名,需要使用命令行参数指定)。现在让我们在工程根目录下创建webpack.config.js,并添加如下代码:module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', }, mode: 'development',}

上面通过module.exports导出了一个对象,也就是打包时被Webpack接收的配置对象。先前在命令行中输入的一大串参数就都要改为key-value的形式放在这个对象中。

目前该对象包含两个关于资源输入输出的属性——entry和output。entry就是我们的资源入口,output则是一个包含更多详细配置的对象。在Webpack配置中,我们经常会遇到这种层叠的属性关系。这是由于Webpack本身配置实在太多,如果都放在同一级会难以管理,因此出现了这种多级配置。当开发者要修改某个配置项时,通过层级关系找下来会更加清晰、快捷。

之前的参数--output-filename和--output-path现在都成为了output下面的属性。filename和先前一样都是bundle.js,不需要改动,而path和之前有所区别。Webpack对于output.path的要求是使用绝对路径(从系统根目录开始的完整路径),之前我们在命令行中为了简洁所以使用了相对路径。而在webpack.config.js中,我们通过调用Node.js的路径拼装函数——path.join,将__dirname(Node.js内置全局变量,值为当前文件所在的绝对路径)与dist(输出目录)连接起来,得到了最终的资源输出路径。

现在我们可以去掉package.json中配置的打包参数了:…… "scripts": { "build": "webpack" },……

为了验证最终效果,再对add-content.js的内容稍加修改:export default function() { document.write('I\'m using a config file!');}

执行npm run build,Webpack就会预先读取webpack.config.js,然后进行打包。完成之后我们打开index.html进行验证,结果如图1-4所示。图1-4 index.html内容变为了“I’m using a config file!”1.4.5 webpack-dev-server

到这里,其实我们已经把Webpack的初始环境配置完毕了。你可能会发现,单纯使用Webpack以及它的命令行工具来进行开发调试的效率并不高。以往只要编辑项目源文件(JS、CSS、HTML等),刷新页面即可看到效果。现在多了一步打包,我们在改完项目源码后要执行npm run build更新bundle.js,然后才能刷新页面生效。有没有更简便的方法呢?

其实Webpack社区已经为我们提供了一个便捷的本地开发工具——webpack-dev-server。用以下命令进行安装:npm install webpack-dev-server --save-dev

安装指令中的--save-dev参数是将webpack-dev-server作为工程的devDependencies(开发环境依赖)记录在package.json中。这样做是因为webpack-dev-server仅仅在本地开发时才会用到,在生产环境中并不需要它,所以放在devDependencies中是比较恰当的。假如工程上线时要进行依赖安装,就可以通过npm install--production过滤掉devDependencies中的冗余模块,从而加快安装和发布的速度。

为了便捷地启动webpack-dev-server,我们在package.json中添加一个dev指令:…… "scripts": { "build": "webpack", "dev": "webpack-dev-server" },……

最后,我们还需要对webpack-dev-server进行配置。编辑webpack.config.js如下:module.exports = { entry: './src/index.js'fsad output: { filename: './bundle.js', }, mode: 'develpoment', devServer: { publicPath: '/dist', },};

可以看到,我们在配置中添加了一个devServer对象,它是专门用来放webpack-dev-server配置的。webpack-dev-server可以看作一个服务者,它的主要工作就是接收浏览器的请求,然后将资源返回。当服务启动时,会先让Webpack进行模块打包并将资源准备好(在示例中就是bundle.js)。当webpack-dev-server接收到浏览器的资源请求时,它会首先进行URL地址校验。如果该地址是资源服务地址(上面配置的publicPath),就会从Webpack的打包结果中寻找该资源并返回给浏览器。反之,如果请求地址不属于资源服务地址,则直接读取硬盘中的源文件并将其返回。

综上我们可以总结出webpack-dev-server的两大职能:

·令Webpack进行模块打包,并处理打包结果的资源请求。

·作为普通的Web Server,处理静态资源文件请求。

最后,在启动服务之前,我们还是更改一下add-content.js:export default function() { document.write('I\'m using webpack-dev-server!');}

一切就绪,执行npm run dev并用浏览器打开http://localhost:8080/,可以看到如图1-5所示的输出结果。图1-5 index.html内容变为了“I’m using webpack-dev-server!”

这里有一点需要注意。直接用Webpack开发和使用webpack-dev-server有一个很大的区别,前者每次都会生成budnle.js,而webpack-dev-server只是将打包结果放在内存中,并不会写入实际的bundle.js,在每次webpack-dev-server接收到请求时都只是将内存中的打包结果返回给浏览器。

这一点可以通过删除工程中的dist目录来验证,你会发现即便dist目录不存在,刷新页面后功能仍然是正常的。从开发者的角度来看,这其实是符合情理的。在本地开发阶段我们经常会调整目录结构和文件名,如果每次都写入实际文件最后就会产生一些没用的垃圾文件,还会干扰我们的版本控制,因此webpack-dev-server的处理方式显得更加简洁。

webpack-dev-server还有一项很便捷的特性就是live-reloading(自动刷新)。让我们保持本地服务启动以及浏览器打开的状态,到编辑器去更改add-content.js:export default function() { document.write('This is from live-reloading!');}

此时切回到浏览器,你会发现浏览器的内容自动更新了,这就是live-reloading的功能。当webpack-dev-server发现工程源文件进行了更新操作就会自动刷新浏览器,显示更新后的内容。该特性可以提升我们本地开发的效率。在后面我们还会讲到更先进的hot-module-replacement(模块热替换),我们甚至不需要刷新浏览器就能获取更新之后的内容。1.5 本章小结

在这一章里,我们介绍了Webpack的功能,它可以处理模块之间的依赖,将它们串联起来合并为单一的JS文件。

在安装Webpack时我们一般选择在项目本地安装的方式,这样可以使团队开发时共用一个版本,并且可以让其他插件直接获取Webpack的内部模块。

配置本地开发环境可以借助npm scripts来维护命令行脚本,当打包脚本参数过多时,我们需要将其转化为webpack.config.js,用文件的方式维护复杂的Webpack配置。

webpack-dev-server的作用是启动一个本地服务,可以处理打包资源与静态文件的请求。它的live-reloading功能可以监听文件变化,自动刷新页面来提升开发效率。

现在我们本地的工程环境已经准备好,接下来我们会介绍如何编写和使用模块,以及Webpack是通过何种方式将模块串联在一起工作的,这对于理解和使用Webpack至关重要。第2章模块打包

模块之于程序,就如同细胞之于生物体,是具有特定功能的组成单元。不同的模块负责不同的工作,它们以某种方式联系在一起,共同保证程序的正常运转。本章我们将深入模块,了解Webpack如何对其进行打包以及合并。本章将包含以下几个部分:

·不同模块的标准以及它们之间的区别;

·如何编写模块;

·模块打包的原理。

随着JavaScript语言的发展,社区中产生了很多模块标准。在认识这些标准的同时,也要了解其背后的思想。例如,它为什么会有这个特性,或者为什么要这样去实现。这对我们自己编写模块也会有所帮助。2.1 CommonJS

CommonJS是由JavaScript社区于2009年提出的包含模块、文件、IO、控制台在内的一系列标准。在Node.js的实现中采用了CommonJS标准的一部分,并在其基础上进行了一些调整。我们所说的CommonJS模块和Node.js中的实现并不完全一样,现在一般谈到CommonJS其实是Node.js中的版本,而非它的原始定义。

CommonJS最初只为服务端而设计,直到有了Browserify——一个运行在Node.js环境下的模块打包工具,它可以将CommonJS模块打包为浏览器可以运行的单个文件。这意味着客户端的代码也可以遵循CommonJS标准来编写了。

不仅如此,借助Node.js的包管理器,npm开发者还可以获取他人的代码库,或者把自己的代码发布上去供他人使用。这种可共享的传播方式使CommonJS在前端开发中逐渐流行了起来。2.1.1 模块

CommonJS中规定每个文件是一个模块。将一个JavaScript文件直接通过script标签插入页面中与封装成CommonJS模块最大的不同在于,前者的顶层作用域是全局作用域,在进行变量及函数声明时会污染全局环境;而后者会形成一个属于模块自身的作用域,所有的变量及函数只有自己能访问,对外是不可见的。请看下面的例子:// calculator.jsvar name = 'calculator.js';// index.jsvar name = 'index.js';require('./calculator.js');console.log(name); // index.js

这里有两个文件,在index.js中我们通过CommonJS的require函数加载calculator.js。运行之后控制台结果是“index.js”,这说明calculator.js中的变量声明并不会影响index.js,可见每个模块是拥有各自的作用域的。2.1.2 导出

导出是一个模块向外暴露自身的唯一方式。在CommonJS中,通过module.exports可以导出模块中的内容,如:module.exports = { name: 'calculater', add: function(a, b) { return a + b; }};

CommonJS模块内部会有一个module对象用于存放当前模块的信息,可以理解成在每个模块的最开始定义了以下对象:var module = {...};// 模块自身逻辑module.exports = {...};

module.exports用来指定该模块要对外暴露哪些内容,在上面的代码中我们导出了一个对象,包含name和add两个属性。为了书写方便,CommonJS也支持另一种简化的导出方式—直接使用exports。exports.name = 'calculater';exports.add = function(a, b) { return a + b;};

在实现效果上,这段代码和上面的module.exports没有任何不同。其内在机制是将exports指向了module.exports,而module.exports在初始化时是一个空对象。我们可以简单地理解为,CommonJS在每个模块的首部默认添加了以下代码:var module = { exports: {},};var exports = module.exports;

因此,为exports.add赋值相当于在module.exports对象上添加了一个属性。

在使用exports时要注意一个问题,即不要直接给exports赋值,否则会导致其失效。如:exports = { name: 'calculater'};

上面代码中,由于对exports进行了赋值操作,使其指向了新的对象,module.exports却仍然是原来的空对象,因此name属性并不会被导出。

另一个在导出时容易犯的错误是不恰当地把module.exports与exports混用。exports.add = function(a, b) { return a + b;};module.exports = { name: 'calculater'};

上面的代码先通过exports导出了add属性,然后将module.exports重新赋值为另外一个对象。这会导致原本拥有add属性的对象丢失了,最后导出的只有name。

另外,要注意导出语句不代表模块的末尾,在module.exports或exports后面的代码依旧会照常执行。比如下面的console会在控制台上打出“end”:module.exports = { name: 'calculater'};console.log('end');

在实际使用中,为了提高可读性,不建议采取上面的写法,而是应该将module.exports及exports语句放在模块的末尾。2.1.3 导入

在CommonJS中使用require进行模块导入。如:// calculator.jsmodule.exports = { add: function(a, b) {return a + b;}};// index.jsconst calculator = require('./calculator.js');const sum = calculator.add(2, 3);console.log(sum); // 5

我们在index.js中导入了calculator模块,并调用了它的add函数。

当我们require一个模块时会有两种情况:

·require的模块是第一次被加载。这时会首先执行该模块,然后导出内容。

·require的模块曾被加载过。这时该模块的代码不会再次执行,而是直接导出上次执行后得到的结果。

请看下面的例子:// calculator.jsconsole.log('running calculator.js');module.exports = { name: 'calculator', add: function(a, b) { return a + b; }};// index.jsconst add = require('./calculator.js').add;const sum = add(2, 3);console.log('sum:', sum);const moduleName = require('./calculator.js').name;console.log('end');

控制台的输出结果如下:running calculator.jssum: 5end

从结果可以看到,尽管我们有两个地方require了calculator.js,但其内部代码只执行了一遍。

我们前面提到,模块会有一个module对象用来存放其信息,这个对象中有一个属性loaded用于记录该模块是否被加载过。它的值默认为false,当模块第一次被加载和执行过后会置为true,后面再次加载时检查到module.loaded为true,则不会再次执行模块代码。

有时我们加载一个模块,不需要获取其导出的内容,只是想要通过执行它而产生某种作用,比如把它的接口挂在全局对象上,此时直接使用require即可。require('./task.js');

另外,require函数可以接收表达式,借助这个特性我们可以动态地指定模块加载路径。const moduleNames = ['foo.js', 'bar.js'];moduleNames.forEach(name => { require('./' + name);});2.2 ES6 Module

在JavaScript之父Brendan Eich最初设计这门语言时,原本并没有包含模块的概念。基于越来越多的工程需求,为了使用模块化进行开发,JavaScript社区中涌现出了多种模块标准,其中也包括CommonJS。一直到2015年6月,由TC39标准委员会正式发布了ES6(ECMAScript 6.0),从此JavaScript语言才具备了模块这一特性。2.2.1 模块

请看下面的例子,我们将前面的calculator.js和index.js使用ES6的方式进行了改写。// calculator.jsexport default { name: 'calculator', add: function(a, b) { return a + b; }};// index.jsimport calculator from './calculator.js';const sum = calculator.add(2, 3);console.log(sum); // 5

ES6 Module也是将每个文件作为一个模块,每个模块拥有自身的作用域,不同的是导入、导出语句。import和export也作为保留关键字在ES6版本中加入了进来(CommonJS中的module并不属于关键字)。

ES6 Module会自动采用严格模式,这在ES5(ECMAScript 5.0)中是一个可选项。以前我们可以通过选择是否在文件开始时加上“use strict”来控制严格模式,在ES6 Module中不管开头是否有“use strict”,都会采用严格模式。如果将原本是CommonJS的模块或任何未开启严格模式的代码改写为ES6 Module要注意这点。2.2.2 导出

在ES6 Module中使用export命令来导出模块。export有两种形式:

·命名导出

·默认导出

一个模块可以有多个命名导出。它有两种不同的写法:// 写法1export const name = 'calculator';export const add = function(a, b) { return a + b; };// 写法2const name = 'calculator';const add = function(a, b) { return a + b; };export { name, add };

第1种写法是将变量的声明和导出写在一行;第2种则是先进行变量声明,然后再用同一个export语句导出。两种写法的效果是一样的。

在使用命名导出时,可以通过as关键字对变量重命名。如:const name = 'calculator';const add = function(a, b) { return a + b; };export { name, add as getSum }; // 在导入时即为 name 和 getSum

与命名导出不同,模块的默认导出只能有一个。如:export default { name: 'calculator', add: function(a, b) { return a + b; }};

我们可以将export default理解为对外输出了一个名为default的变量,因此不需要像命名导出一样进行变量声明,直接导出值即可。// 导出字符串export default 'This is calculator.js';// 导出 classexport default class {...}// 导出匿名函数export default function() {...}2.2.3 导入

ES6 Module中使用import语法导入模块。首先我们来看如何加载带有命名导出的模块,请看下面的例子:// calculator.jsconst name = 'calculator';const add = function(a, b) { return a + b; };export { name, add };// index.jsimport { name, add } from './calculator.js';add(2, 3);

加载带有命名导出的模块时,import后面要跟一对大括号来将导入的变量名包裹起来,并且这些变量名需要与导出的变量名完全一致。导入变量的效果相当于在当前作用域下声明了这些变量(name和add),并且不可对其进行更改,也就是所有导入的变量都是只读的。

与命名导出类似,我们可以通过as关键字可以对导入的变量重命名。如:import { name, add as calculateSum } from './calculator.js';calculateSum(2, 3);

在导入多个变量时,我们还可以采用整体导入的方式。如:import * as calculator from './calculator.js';console.log(calculator.add(2, 3));console.log(calculator.name);

使用import*as可以把所有导入的变量作为属性值添加到对象中,从而减少了对当前作用域的影响。

接下来处理默认导出,请看下面这个例子:// calculator.jsexport default { name: 'calculator', add: function(a, b) { return a + b; }};// index.jsimport myCalculator from './calculator.js';calculator.add(2, 3);

对于默认导出来说,import后面直接跟变量名,并且这个名字可以自由指定(比如这里是myCalculator),它指代了calculator.js中默认导出的值。从原理上可以这样去理解:import { default as myCalculator } from './calculator.js';

最后看一个两种导入方式混合起来的例子:// index.jsimport React, { Component } from 'react';

这里的React对应的是该模块的默认导出,而Component则是其命名导出中的一个变量。注意 这里的React必须写在大括号前面,而不能顺序颠倒,否则会提示语法错误。2.2.4 复合写法

在工程中,有时需要把某一个模块导入之后立即导出,比如专门用来集合所有页面或组件的入口文件。此时可以采用复合形式的写法:export { name, add } from './calculator.js';

复合写法目前只支持当被导入模块(这里的calculator.js)通过命名导出的方式暴露出来的变量,默认导出则没有对应的复合形式,只能将导入和导出拆开写。import calculator from "./calculator.js ";export default calculator;2.3 CommonJS与ES6 Module的区别

上面我们分别介绍了CommonJS和ES6 Module两种形式的模块定义,在实际开发过程中我们经常会将二者混用,因此这里有必要对比一下它们各自的特性。2.3.1 动态与静态

CommonJS与ES6 Module最本质的区别在于前者对模块依赖的解决是“动态的”,而后者是“静态的”。在这里“动态”的含义是,模块依赖关系的建立发生在代码运行阶段;而“静态”则是模块依赖关系的建立发生在代码编译阶段。

让我们先看一个CommonJS的例子:// calculator.jsmodule.exports = { name: 'calculator' };// index.jsconst name = require('./calculator.js').name;

在上面介绍CommonJS的部分时我们提到过,当模块A加载模块B时(在上面的例子中是index.js加载calculator.js),会执行B中的代码,并将其module.exports对象作为require函数的返回值进行返回。并且require的模块路径可以动态指定,支持传入一个表达式,我们甚至可以通过if语句判断是否加载某个模块。因此,在CommonJS模块被执行前,并没有办法确定明确的依赖关系,模块的导入、导出发生在代码的运行阶段。

同样的例子,让我们再对比看下ES6 Module的写法:// calculator.jsexport const name = 'calculator';// index.jsimport { name } from './calculator.js';

ES6 Module的导入、导出语句都是声明式的,它不支持导入的路径是一个表达式,并且导入、导出语句必须位于模块的顶层作用域(比如不能放在if语句中)。因此我们说,ES6 Module是一种静态的模块结构,在ES6代码的编译阶段就可以分析出模块的依赖关系。它相比于CommonJS来说具备以下几点优势:

·死代码检测和排除。我们可以用静态分析工具检测出哪些模块没有被调用过。比如,在引入工具类库时,工程中往往只用到了其中一部分组件或接口,但有可能会将其代码完整地加载进来。未被调用到的模块代码永远不会被执行,也就成为了死代码。通过静态分析可以在打包时去掉这些未曾使用过的模块,以减小打包资源体积。

·模块变量类型检查。JavaScript属于动态类型语言,不会在代码执行前检查类型错误(比如对一个字符串类型的值进行函数调用)。ES6 Module的静态模块结构有助于确保模块之间传递的值或接口类型是正确的。

·编译器优化。在CommonJS等动态模块系统中,无论采用哪种方式,本质上导入的都是一个对象,而ES6 Module支持直接导入变量,减少了引用层级,程序效率更高。2.3.2 值拷贝与动态映射

在导入一个模块时,对于CommonJS来说获取的是一份导出值的拷贝;而在ES6 Module中则是值的动态映射,并且这个映射是只读的。

上面的话直接理解起来可能比较困难,首先让我们来看一个例子,了解一下什么是CommonJS中的值拷贝。// calculator.jsvar count = 0;module.exports = { count: count, add: function(a, b) { count += 1; return a + b; }};// index.jsvar count = require('./calculator.js').count;var add = require('./calculator.js').add;console.log(count); // 0(这里的count是对 calculator.js 中 count 值的拷贝)add(2, 3);console.log(count); // 0(calculator.js中变量值的改变不会对这里的拷贝值造成影响)count += 1;console.log(count); // 1(拷贝的值可以更改)

index.js中的count是对calculator.js中count的一份值拷贝,因此在调用add函数时,虽然更改了原本calculator.js中count的值,但是并不会对index.js中导入时创建的副本造成影响。

另一方面,在CommonJS中允许对导入的值进行更改。我们可以在index.js更改count和add,将其赋予新值。同样,由于是值的拷贝,这些操作不会影响calculator.js本身。

下面我们使用ES6 Module将上面的例子进行改写:// calculator.jslet count = 0;const add = function(a, b) { count += 1; return a + b;};export { count, add };// index.jsimport { count, add } from './calculator.js';console.log(count); // 0(对 calculator.js 中 count 值的映射)add(2, 3);console.log(count); // 1(实时反映calculator.js 中 count值的变化)// count += 1; // 不可更改,会抛出SyntaxError: "count" is read-only

上面的例子展示了ES6 Module中导入的变量其实是对原有值的动态映射。index.js中的count是对calculator.js中的count值的实时反映,当我们通过调用add函数更改了calculator.js中count值时,index.js中count的值也随之变化。

我们不可以对ES6 Module导入的变量进行更改,可以将这种映射关系理解为一面镜子,从镜子里我们可以实时观察到原有的事物,但是并不可以操纵镜子中的影像。2.3.3 循环依赖

循环依赖是指模块A依赖于模块B,同时模块B依赖于模块A。比如下面这个例子:// a.jsimport { foo } from './b.js';foo();// b.jsimport { bar } from './a.js';bar();

一般来说工程中应该尽量避免循环依赖的产生,因为从软件设计的角度来说,单向的依赖关系更加清晰,而循环依赖则会带来一定的复杂度。而在实际开发中,循环依赖有时会在我们不经意间产生,因为当工程的复杂度上升到足够规模时,就容易出现隐藏的循环依赖关系。

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载