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

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;
  });
}

总结

我们除了提供 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)));

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;

依赖如下:

{
  "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"
  }
}

从 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"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es5",
    "noImplicitAny": false,
    "moduleResolution": "node",
    "removeComments": true,
    "sourceMap": true,
    "lib": ["dom", "es6"]
  }
}

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;