在众多的webpack配置教程中,对externals这个配置选项,总是一带而过,把文档中提到的几种方式都复述一遍,但是对于开发者而言,根本没法完全理解。本文试图通过一整篇文章,详细的对externals这个参数进行讲解。
几种用法
externals这个参数的传入形式有多种,但是总结而言,实际上就是array > object,reg,这三种形式都可以传入,前者其实是对后者的包含。
array形式
数组内的每一个元素又可以是多种形式,包括object, reg, function, string四种:
该形式来自这里,不得不说webpack官方资料超级分散,官网自己的解释也没有非常详细到位。这里也先不讲每一种形式最终会起到什么作用,下文再讲作用部分。
object形式
上面的array中,第一个形式就是object形式,可以把它直接作为externals的值。这种形式应该是绝大部分项目中的配置形式。当然,也根据实际情况来使用。object形式最为复杂,因为它里面一定是key: value的形式,所以像上面那种string的形式就不可能出现在object形式中。function和reg形式也没有出现在object中的可能性。
从上面可以看出,object形式的value部分,有boolean, string, array, object这几种形式。这些例子都可以在这里(结合上面那个链接)看到,都是官方给出的例子,不是我瞎说的。后面那几个以.开头的key,是指externals还可以通过对内部模块进行排除。也就是说,你的代码里面使用了require('./d')
这样的代码,也可以不把d.js这个文件的内容打包进来,不过它们的value值其实都是的string形式。下文再讲打包结果的时候会详细讲。
reg形式
也就是正则匹配的形式,通过传入正则表达式/reg/或者new RegExp()来实现匹配。其实在上面的array形式中已经用到了,只不过如果为了实现某种匹配,可以直接将正则reg传给externals:
就这样就可以了。
形式对应的结果
每一种形式传入进去之后,执行webpack打包,得到的bundle结果中,每个形式对应的结果可能不同,最典型的就是如果你的bundle打算运行在node环境中,你的bundle中可能使用module.exports导出接口,而如果是在浏览器中,常常需要采用umd兼容方案,所以在你的源码中的一句 require('underscore')
在根据不同的环境进行打包之后,bundle文件里面的结果可能不同。那么,每一种形式,对应的都是什么结果呢?
和output.libraryTarget有关
在进行externals的进一步学习之前,你必须对output.libraryTarget这个参数进行了解。libraryTarget这个参数是用来确定你的bundle中,模块组织是遵循的什么规范,可选项有:
"var" - Export by setting a variable: var Library = xxx (default)
"this" - Export by setting a property of this: this["Library"] = xxx
"commonjs" - Export by setting a property of exports: exports["Library"] = xxx
"commonjs2" - Export by setting module.exports: module.exports = xxx
"amd" - Export to AMD (optionally named - set the name via the library option)
"umd" - Export to AMD, CommonJS2 or as property in root
当你的libraryTarget值为commonjs2的时候,你的bundle最终会以module.exports导出整个bundle模块,这种情况大部分是在node环境下运行,在amd下也是OK的,但是amd下,大多需要有define作为包裹,因此你也看到,上面会有一个amd的单独选项。
比如说,你的源码是如下的:
那么,按照commonjs2的规范打包的bundle的最核心结构就会如下:
但是如果你按照this这种方式打包,那么结果又会不一样,你需要注意webpack配置中的output.library这个参数,比如你的library设置为'my_a',那么你最终得到的bundle结果中,核心的部分应该是:
总之,output.libraryTarget和output.library都是我们在讲解externals的时候应该关注的两个相关配置项。我们最长使用的模块化方案是commonjs2和umd,前者是为node环境,后者是为浏览器环境。
感觉这里还是没有讲透,如果你在浏览器端没有使用过requirejs或seajs这种模块化引擎的话,可能有点难理解到这里。
从externals对应的bundle结果中学习其使用
上面一段是解释webpack打包成bundle后,bundle本身要怎样像外提供接口。但是这跟我们externals没有核心关系,不过可以因此了解不同的模块化规范在webpack中的打包结果。
externals更多的是指定当你引用一个包的时候,这个包应该遵循上面哪一种模块化方式引入。举个例子,我们的源码如下:
这里我们使用了require('jquery')
来引入jquery,但是实际上我们不希望把jquery打包进bundle。但是怎么把jquery引进来呢?那就得看你的externals是怎么配置的了。比如说jquery,你在源码里是上面这样使用的,那么你就得配置,怎么配置?
上面这种配置方法,告诉webpack,输出的时候会采用umd模块化方案(如果你了解过umd的话),所以webpack就会把我们的源码的require部分处理成如下的结果:
externals: {jquery: 'jQuery'}
这个里面的jquery是指require('jquery')
中的jquery,而jQuery就是上面代码中的红色字。简单翻译一下,就是说“当require的参数是jquery的时候,使用jQuery这个全局变量引用它”,你可以在这里详细阅读这句话:jQuery
in the externals indicates that your bundle will need jQuery
variable in the global form.。
当然,之所以是引用全局变量jQuery是因为你的采用externals string类型时,没有规定你要采用哪种模块化形式。这其实在前面的代码里面提到了,假如说你的value值为'commonjs2 jquery'
,那么bundle结果的地方就不是直接使用module.exports = jQuery
了,而应该会使用module.exports = require('jquery')
,这就是使用string形式的区别。这里也提醒你,当你打包一个给node使用的包时,应该要在externals的string类型中加入commonjs前缀。你可以在这里看到,有global, commonjs, commonjs2, amd
这四种,但是从上面的官方示例代码来看,var
和this
也是支持的,global是默认的,所以上面的jQuery是指全局变量。具体每种形式的输出形式,可以在上面“object形式”那一节的示例代码里看到。
这是string类型,而通过object类型,可以规定不同模块化规范下的不同结果形式:
这意思就是说,你打包的时候,webpack会考虑任何可能的形式去进行匹配,你就不必去考虑应该用require('lodash')还是直接使用全局变量_来引入lodash到你的bundle中了。
理解了上面这些之后,无论是在array形式里面,还是在object形式里面,甚至function形式里面,只要是为了返回一个key => string value形式,甚至只是string形式,都遵循这些模块化规范的规则,包括function形式里面,你可以看到示例代码里面,其实也会考虑在callback的参数中,加入commonjs作为前缀。
知道了这些,我相信你已经对采用string类型的形式的使用方法很了解了,那你也就掌握了externals的精髓部分了。
需要注意的是,下面这几种形式,本质上也是string形式,而且采用global引入:
上面这个配置里面有一个reg,这个多补一句:正则匹配会匹配到多个结果,比如你匹配以"webpac"开头的所有模块,那么所有的webpack插件都会被匹配到,那么在你的bundle中,这些模块也就全都使用向上面这里的'jquery'这种形式,实际上也就是global+string形式。
function形式怎么使用
上面提到了两次function形式,但是没有讲具体怎么用,这里讲一下,毕竟网上很少有资料对此进行单独详细的讲解。
我们来看下function的使用场景:只能在array形式中作为一个元素传入。
上面这个例子,会得到'my-local-module': 'global MyLocalModule'
,这个结果,你可以自己写多个if来实现多个模块的配置,注意结合上面string形式的知识。一般而言,一个externals配置中,不要多个function,因为这些function都会被调用,还不如把全部的规则都写在一个function里面。
global在node环境中
当你的bundle中直接出现module.exports = underscore
,你会发现,在你的node全局变量里没有underscore这个全局变量,所以当你遇到这种情况时,一定要注意在externals规则里面使用commonjs这个前缀。
但是有的情况下,我们可以通过webpack配置中的node.global来进行配置,如下:
其实上面的target=node和node选项跟externals没有直接的关系,但是由于target: node会影响所有的文件引入方式,所有的引入都会采用require的形式,唯独externals的哪些模块会根据你配置的externals来确定,所以很有可能你就会出现上面我说的module.exports = underscore
的情况。而如果把node.global设置为false,就可以避免这种情况,也就是说,“在target为node的时候,把node.global设置为false,externals中的global前缀将不生效”,所以所有的module都会遵照node环境中的特殊处理,即使用require来引入模块。这样一来,即使你的externals配置中直接传入了一个string形式,没有给commonjs前缀,也不用担心默认使用global模式带来的负面影响,因为所有的引入都会强制使用require。
实用场景
将node_modules目录下的所有模块加入到externals中
这是最常用的一种场景,但不局限于node_modules目录,包括bower_components目录还有一些项目中特定的内部模块目录,都可以采用这种方案。
打包的时候不把这些包打进去,有两种情况,一种是你要打包的bundle是用来在node环境中运行的,另一种是在浏览器环境下运行的。看第一种,node环境下运行的bundle的externals应该怎么写:
但是在浏览器环境中就比较坑,你不可能这样去用。
浏览器环境下webpack打包bundle文件中的引用关系
所谓global模式,就是在window对象上挂一个变量,然后直接就可以使用这个全局变量。d3就是一个很好的例子。当你在源码中使用require('d3')之后,可以直接把d3加入到externals中,得到一个打包文件bundle.js,但是在引入这个bundle.js到浏览器之前,你一定要先引入d3的库文件,d3库文件会创建一个全局变量d3,而你的bundle.js中external的d3实际上是global模式,所以实际上bundle.js在引入d3的时候,就会直接从全局变量去引用,即module.exports = d3
。这样就完美的将bundle和库结合在一起。
但是在amd模块化引擎require.js环境中,你就不能这么干,你如果webpack打包的时候选择了amd模式,而非global或umd模式,那么所有的模块都会按照require.js的规范来引入,即使用module.exports = require('d3')
来引入,这个时候d3库文件不是amd规范的,所以你的bundle.js文件中这里会报错,会说找不到d3这个模块,这是amd模块化规范引起的,这种情况下,你必须让你的d3也支持amd模块规范才可以。
所以,大部分情况下,我们都让webpack输出为umd的模块规范结构,这样可以让bundle文件在不同的环境下运行,但是虽然这样说,可externals里面的前缀影响了bundle.js外部的文件的引入形式,所以除非你所有的文件都是webpack打包出来的,或者你就采用global模式,否则,你一定要注意这个问题。
最终回到我们的问题,浏览器环境下webpack打包出来的bundle相互之间如何引用呢?比如你用webpack打包了A和B,A先执行,B后执行,实际上,在webpack的bundle中,是这样子的(umd):
A和B的bundle文件结构都是上面这种情况,所以,如果你单纯的通过script src引入文件,那么A就会走root[A],即在window上注册了一个全局变量,而如果B中引用了A,就会像文章前面说的一样,有一个module.exports = A
在B的bundle中,这种方式就是通过引用一个全局变量来实现模块的引用,B引用A就是这么简单。而如果是在amd或cmd模块化引擎支持下,就会走define。amd和cmd的依赖是通过define的参数来确定的,比如define('B', ['A'], () => {})
就是说当前模块的名字是B,并且依赖A模块。但是实际上amd和cmd中,第一个参数不设置,因为设置了会有问题,模块的名称如果不使用相对路径的话,都会在config中配置,比如说:
之后在你的amd模块中使用define并传入依赖:
而如果是本地的(也就是webpack打包的bundle),则可以使用路径依赖,或者使用require来引用,比如:
而./my-moduleA.js这个文件,就是我们用webpack打包出来的,它跟你的当前引用的文件处于同一个目录下。不过这种情况很少出现在我们自己写代码过程中,因为我们很少再去依赖require.js写代码了,我们现在都是直接采用commonjs模块化规范,然后用webpack打包成一个文件引用执行。为什么要讲amd的东西呢?因为知道这个之后,我们就不用考虑output.library这个配置项在amd模块化引擎下怎么办了,我们可以记住:output.library主要是给global模式使用,也就是上面示例代码中的root['MyTest']使用。因此,output.library不能配置成连接符-连接的名称,而一定要使用驼峰或下划线,因为它的值可能在另一个模块中被当做一个全局变量被引用。同样的道理,当你在另一个模块中打算引用MyTest这个全局变量时怎么办呢?MyTest对应的文件名是my-test.js,引用的时候使用require('my-test')
。这是你在配置externals的时候,就需要注意,要写成:
key和value值是不一样的,这一点要谨记。当然,如果你不想这么麻烦,那么externals里面应该写
引入的时候写require('MyTest'),这样也可以起到对应的效果,但显然,这种仅适合global模式,不能在amd下面运行。
怎么排除打包node_modules目录下的模块呢?其实和上面的方案是一样的,但是你必须确保,你external的node_modules下面的模块,都采用了正确的方式来适应你bundle.js中的引用,比如jquery这个库就很好,默认支持了amd。但是underscore就仅支持global,所以,如果你的项目中使用了underscore,而又是在amd引擎驱动下运行你的脚本,最好的办法,是把underscore用webpack打包一次,可以使用webpack的一些common相关插件来实现第三方库的分离。当然,这要你很熟的情况下进行。
小结
本文讲解了那么多,说到底,最终其实只要抓住一个点,就是“string形式里面的前缀所带来的打包结果中的引入方式”,就可以吃透externals的配置问题了。而作为string形式的变体,function形式和reg形式可以实现一次性多个匹配,虽然他们本质上还是遵循string形式那一套。但是如果不认真的去思考,根本很难理解到这一点。这也是为什么网上很多文章对externals这个配置提一下就不深入的原因。