JavaScript UMD 插件编写规范(修订)

UMD 的实现很简单,先判断是否支持 Node.js 模块格式(exports 是否存在),存在则使用 Node.js 模块格式。 再判断是否支持 AMD(define 是否存在),存在则使用 AMD 方式加载模块。前两个都不存在,则将模块公开到全局(window 或 global)。 编写 UMD 时有一些注意事项,如果不注意,会导致生产环境报错。

# 如何兼容 CommonJS, AMD, CMD 和浏览器原生 JS

# CommonJS

CommonJS 有三个全局变量 moduleexportsrequire。但是由于 AMD 也有 require 这个全局变量,故不使用这个变量来进行检测。

如果想要对外提供接口的话,可以将接口绑定到 exports (即 module.exports) 上。

function MyModule() {
  // ...
}

if (typeof module !== `undefined` && typeof exports === `object`) {
  module.exports = MyModule;
}
1
2
3
4
5
6
7

# CMD

CMD 规范中定义了 define 函数有一个公有属性 define.cmd

CMD 模块中有两种方式提供对外的接口,一种是 exports.MyModule = ...,一种是使用 return 进行返回。

# AMD

AMD 规范中,define 函数同样有一个公有属性 define.amd

AMD 中的参数便是这个模块的依赖。那么如何在 AMD 中提供接口呢?它是返回一个对象,这个对象就作为这个模块的接口,故我们可以这样写:

function MyModule() {
  // ...
}

if (typeof define === `function` && define.amd) {
  define(function() {
    return MyModule;
  });
}
1
2
3
4
5
6
7
8
9

# 总结

我们除了提供 AMD 模块接口,CMD 模块接口,还得提供原生的 JavaScript 接口。 由于 CMDAMD 都可以使用 return 来定义对外接口,故可以合并成一句代码。

(function() {
  function MyModule() {
    // ...
  }

  var moduleName = MyModule;
  if (typeof module !== "undefined" && typeof exports === "object") {
    module.exports = moduleName;
  } else if (typeof define === "function" && (define.amd || define.cmd)) {
    define(function() {
      return moduleName;
    });
  } else {
    this.moduleName = moduleName;
  }
}.call(this || (typeof window !== "undefined" ? window : global)));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# Webpack 编译 UMD 包需要注意的事项

# output.library

这个名字表示在浏览器直接引入时,挂载到 window 对象上的名字。

# output.globalObject

[重要!]:全局对象配置,浏览器中是 windowNode 中是 global。如果编写的 UMD 需要兼容两种环境,定义为 this

# 别忘记将 ES6 转换成 ES5 再发布

有很多人没有注意这一点,导致线上环境报错。因为 webpackbabel-loader 默认不会编译 node_modules 中的代码。

# 最终配置

const webpack = require("webpack");
const path = require("path");
const libraryName = "Datetime";
const filename = "Datetime";

const config = {
  entry: __dirname + "/lib/index.js",
  devtool: "source-map",
  output: {
    path: __dirname + "/dist",
    publicPath: "/dist",
    library: libraryName,
    libraryTarget: "umd", // 这里一定要注明 umd
    filename: filename + ".umd.js",
    globalObject: "this", // 这个 this 非常重要,这样才能使浏览器和 Node 环境同时兼容!
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: "babel-loader",
        exclude: /node_modules/,
        options: {
          presets: ["env"], // 重要,转换成 ES5,防止某些不支持 ES6 的环境
        },
      },
    ],
  },
  resolve: {
    extensions: ["*", ".js"],
  },
};
module.exports = config;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

依赖如下:

{
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-preset-env": "^1.6.0",
    "babel-preset-stage-3": "^6.24.1",
    "prettier": "^1.16.4",
    "webpack": "^4.31.0",
    "webpack-cli": "^3.3.2"
  }
}
1
2
3
4
5
6
7
8
9
10
11

# 从 TS 源码打包 UMD

依赖如下:

{
  "devDependencies": {
    "prettier": "^1.16.4",
    "ts-loader": "^6.2.0",
    "typescript": "^3.6.4",
    "webpack": "^4.27.1",
    "webpack-cli": "^3.3.2"
  }
}
1
2
3
4
5
6
7
8
9

tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es5",
    "noImplicitAny": false,
    "moduleResolution": "node",
    "removeComments": true,
    "sourceMap": true,
    "lib": ["dom", "es6"]
  }
}
1
2
3
4
5
6
7
8
9
10
11

webpack.config.js

const webpack = require("webpack");
const path = require("path");
const libraryName = "DataUtils";
const filename = "DataUtils";

const config = {
  entry: __dirname + "/lib/index.ts",
  devtool: "source-map",
  output: {
    path: __dirname + "/dist",
    publicPath: "/dist",
    library: libraryName,
    libraryTarget: "umd",
    // libraryExport: "default",
    filename: filename + ".umd.js",
    globalObject: "this",
  },
  module: {
    rules: [
      {
        test: /\.ts?$/,
        use: [
          {
            loader: "ts-loader",
            options: {
              configFile: path.resolve(__dirname, "./tsconfig.json"),
            },
          },
        ],
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: ["*", ".ts"],
  },
};
module.exports = config;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38