# Library 打包

这一节来讲一下当我们开发类库的时候,webpack 需要怎么配置。

# 写点代码

首先在 src 下新建 math.jsstring.js

// math.js

export function add(a, b) {
  return a + b;
}

export function minus(a, b) {
  return a - b;
}

export function multiply(a, b) {
  return a * b;
}

export function division(a, b) {
  return a / b;
}

// string.js

import _ from 'lodash';

export function join(a, b) {
  return _.join([a, b], ' ');
}

math.js 的作用是导出加减乘除四个方法;string.js 的作用是使用 lodash 进行数组拼接。

安装 lodash

npm install lodash -D

现在我们需要讲我们写的几个方法导出去,让使用者可以用起来,这里我们先要在回顾一下引入类库的几种方式。

# 引入类库的几种方式

  • 传统方式:script 标签
<script src="demo.js"></script>
<script>demo();</script>
  • AMD
define(['demo'], function(demo) {
  demo();
});
  • commonjs 方式
const demo = require('demo');

demo();
  • ES6 module
import demo from 'demo';

demo();

上面是我们常用的几种模块类库导入方式,为什么我们直接就能导入这些类库,如果你是使用 webpack  打包的类库,那就是 webpack 配置 output.libraryoutput.libraryTarget 提供的功能。

# 配置 webpack

接着我们修改一些 config/webpack.prod.js 生产环境打包文件:

const path = require('path');

const prodConfig = {
  ...
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: 'library.js', // 打包出来的名字
    library: 'root', // 用 <script> 方式引入,全局的变量名
    libraryTarget: 'umd', // 适用于 AMD、CommonJs、ES6 module引入方式
  }
  ...
}

module.exports = merge(commonConfig, prodConfig);

配置讲解:

# library

其中我们上面也注释了,library 这个属性的意思是如果是 HTML 页面中使用 script 标签引入打包结果文件,那么变量 myDemo 对应的值将会是 入口文件(entry file) 的返回值。

我们在项目中运行 npm run build,会生成一个 dist 目录,我们打开 index.html,打开控制台,我们会发现在页面的 window 下会有一个 root 变量,在其下面就是我们所导出的 module

但是我们发现,我们在项目中导出的方法,在 rootdefault 属性下。我们不应该直接通过 root.math 就能访问相关的方法了吗?

这是因为在浏览器、或者在其他不支持 ES6 的模块系统中都会出这个问题,它们并不认识 default。 所以其实在这种情况下,编译后的 js 文件实际上应当只输出 default,这就需要在我们在 webpack 配置里用 libraryExport 来控制,

const prodConfig = {
	...
	output: {
    path: path.resolve(__dirname, '../dist'),
    filename: 'library.js', // 打包出来的名字
    library: 'root', // 用 <script> 方式引入,全局的变量名
    libraryTarget: 'umd', // 适用于 AMD、CommonJs、ES6 module引入方式
    libraryExport: 'default',
	}
  ...
}

我们重新打包一下文件,会发现这个时候的 root 下直接就是我们导出的两个方法了:

# libraryTarget:

这个配置规定了如何公开库。

打包的文件选用哪种引入方式,它一共支持的值如下,我们讲几个常用的配置,没讲到的大家可以通过 outputlibrarytarget 自行查看。

# 暴露一个变量

以下选项会把打包返回的值(无论暴露的是什么)绑定到一个由output.library指定的变量上,无论包是被如何引用。

  • var:默认值

使用这个配置,当库被加载时,那么库的返回值会被分配到使用用 var 申明的变量上。

var myDemo = _entry_return_;

// In a separate script...
myDemo();

如果没有设置output.library值,那么将不会发生赋值行为。

  • assign

不常用

使用这个设置,会把库返回值分配给一个没使用 var 申明的变量中,如果这个变量没有在引入作用域中提前申明过,那么将会挂载在全局作用域中。(注意,这个行为有可能会覆盖全局作用域中的已有变量

# 通过对象属性暴露

  • this:将导出的模块挂在 this

当我们设置了 this 的时候,它会将模块自动的挂到 this 上,如下图:

因为浏览器中全局的 this,就是 window,所以我们也可以这样访问:

  • window:可以参考 this
  • global:适用于 Node,它会将模块挂到 global
  • commonjs

将库的返回值分配给 exports 对象的由 output.library 指定的属性。正如名字所指,这个选项可以使用在 CommonJS 环境,使用如下:

// 导出的模块,类似于womenziji写的index.js
exports["myDemo"] = _entry_return_;

// 使用我们导出的模块
require("myDemo").doSomething();

# 模块定义系统

选项将产生一个包含更完整兼容代码的包,以确保与各种模块系统的兼容性。

  • commonjs2

将库的返回值分配给 module.exports。正如名字所指,这个选项可以使用在 CommonJS 环境。

module.exports = _entry_return_;

const myDemo = require("myDemo");
myDemo();

注意,在这个情况下 output.library 不是必须的,因为此时 output.library 选项将会被忽略。

CommonJSCommonJS2 长的非常像?他们的区别大家可以参考 issue

  • AMD

不常用,现在基于 AMD 基本上已经不用了,除非是很老的代码。

这个选项会把库作为 AMD 模块导出。

AMD 模块要求输入脚本(例如由 <script> 标签加载的第一个脚本)被定义为具有特定属性,例如通常由 RequireJS 或任何兼容的加载器(诸如 almond )提供的 requiredefine 属性。其实意思就是要加载一个 require.js 或者 类似的 js

否则,直接加载生成的 AMD 捆绑包将导致类似 define is not defined 的错误。 由此定义生成的代码会如下:

define("myDemo", [], function() {
  return _entry_return_;
});

以上的代码可以作为 script 标签引入代码的一部分被包含,然后在通过以下代码调用:

require(['myDemo'], function(myDemo) {
  // Do something with the library...
  myDemo();
});

如果 output.library 没有定义有效值,那么生成的代码将如下:

define([], function() {
  return _entry_return_;
});

因此在这种情况下,如果这些由服务器直接提供,那么 output.pathoutput.filename 配置可能变得非常重要。

  • umd

现在的类库打包最常用的配置

这个选项会尝试把库暴露给前使用的模块定义系统,这使其和CommonJSAMD兼容或者暴露为全局变量。 output.library 选项在这里是必须的。最终代码输出如下:

(function webpackUniversalModuleDefinition(root, factory) {
  if(typeof exports === 'object' && typeof module === 'object')
    module.exports = factory();
  else if(typeof define === 'function' && define.amd)
    define([], factory);
  else if(typeof exports === 'object')
    exports["MyLibrary"] = factory();
  else
    root["MyLibrary"] = factory();
})(typeof self !== 'undefined' ? self : this, function() {
  return _entry_return_;
});

如果 output.library 没有输入有效值,那么对于全局变量的处理会和上面提到的 暴露一个变量 一致。代码输出如下:

(function webpackUniversalModuleDefinition(root, factory) {
  if(typeof exports === 'object' && typeof module === 'object')
    module.exports = factory();
  else if(typeof define === 'function' && define.amd)
    define([], factory);
  else {
    var a = factory();
    for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
  }
})(typeof self !== 'undefined' ? self : this, function() {
  return _entry_return_;
});

webpack 3.10.0 版本开始,我们可以通过把 output.library 定义为对象来控制不同目标环境的输出值。如下图:

output: {
  library: {
    root: "myDemo",
    amd: "my-demo",
    commonjs: "my-common-demo"
  },
  libraryTarget: "umd"
}
  • jsonp

这个方法会使用 jsonp 的方式把结果包裹起来。

# externals:

这个参数的意思是将一些公用的库不打包到我们的类库中,等到要使用的时候,再去引入这些公用库。

# 举个🌰:

我们在上面的 string.js 中使用了 lodash,打包完之后 library.js 的体积是 72.5kb

这一下子就让我们的类库变得很大,其实我们本来就只有几 kb 的代码而已,所以我们可以配置 externals,将 lodash 不打包进去:

...

const prodConfig = {
  mode: 'production',
  devtool: 'cheap-module-source-map', // production
  externals: 'lodash',
  ...
}

...

接着我们重新打包,我们会发现,library.js 的大小变成了 1.45kb

当别人要使用我们的库的时候,要事先导入 lodash

import _ from 'lodash';
import library from 'library';

externals 支持数组和对象,

// 数组
externals: ['lodash'],

// 对象
externals: {
  commonjs: 'lodash', // 通过 CommonJS 引入时,名字必须是 lodash
  root: '_', // 通过 <script> 标签引入时,名字必须是 "_"
}

更详细的配置大家可以参考:

# 相关链接

# 示例代码

示例代码可以看这里: