FrankKai / FrankKai.github.io

FE blog
https://frankkai.github.io/
362 stars 39 forks source link

module.exports和exports shortcut #177

Open FrankKai opened 4 years ago

FrankKai commented 4 years ago

module.exports

module.exports是由Module系统创建出的。有时这是不可接受的;许多人想让他们的模块成为一些class的实例。为了做到这个,将目标导出对象赋予module.exports。将对象赋予exports会重新板顶exports变量,这不是我们想要的。

最常见的例子

a.js

const EventEmitter = require('events');
module.exports = new EventEmitter();
setTimeout(() => {
  module.exports.emit('ready');
}, 1000);

b.js

const a = require('./a');
a.on('ready', () => {
  console.log('module "a" is ready');
});

module.exports不能在callback中完成。

x.js

setTimeout(() => {
  module.exports = { a: 'hello' };
}, 0);

y.js

const x = require('./x');
console.log(x.a); // undefined

打印子a.js Module 对象和 父b.js Module对象

子a.js Module对象
Module {
  id: '/Users/frank/weidian/weidian-crm/a.js',
  exports: { a: 'hello' },
  parent: // a.js的父模块是b.js
   Module {
     id: '.',
     exports: {},
     parent: null,
     filename: '/Users/frank/weidian/weidian-crm/b.js',
     loaded: false,
     children: [ [Circular] ],
     paths:
      [ '/Users/frank/weidian/weidian-crm/node_modules',
        '/Users/frank/weidian/node_modules',
        '/Users/frank/node_modules',
        '/Users/node_modules',
        '/node_modules' ] },
  filename: '/Users/frank/weidian/weidian-crm/a.js',
  loaded: false,
  children: [], // 注意这里,a.js的子模块是空的
  paths:
   [ '/Users/frank/weidian/weidian-crm/node_modules',
     '/Users/frank/weidian/node_modules',
     '/Users/frank/node_modules',
     '/Users/node_modules',
     '/node_modules' ] }
父b.js Module对象
Module {
  id: '.',
  exports: {},
  parent: null, // b.js的父模块是null
  filename: '/Users/frank/weidian/weidian-crm/b.js',
  loaded: false,
  children: // b.js的子模块包括a.js
   [ Module {
       id: '/Users/frank/weidian/weidian-crm/a.js',
       exports: [Object],
       parent: [Circular],
       filename: '/Users/frank/weidian/weidian-crm/a.js',
       loaded: true,
       children: [],
       paths: [Array] } ],
  paths:
   [ '/Users/frank/weidian/weidian-crm/node_modules',
     '/Users/frank/weidian/node_modules',
     '/Users/frank/node_modules',
     '/Users/node_modules',
     '/node_modules' ] }

exports shortcut

exports是module.exports的缩写。但是需要一个前提条件。

前提条件是按照exports.foo = foo; exports.bar = bar;的方式,若是exports被重新赋值,比如exports = {foo:'foo'},那么exports不再是module.exports的引用,模块不能导出。

exports变量在模块的文件级作用域是有效的,在模块计算之前赋值给module.exports。 支持缩写的方式,module.exports.f = ... 可以写为exports.f = ... 然而,如果为exports完全赋予一个新值,那么exports就不再与module.exports绑定了。

重新赋值exports会导致模块导出失败

module.exports.hello = true; // 可以导出,模块可以按照require()引入
exports.hello = true; // 可以导出,模块可以按照require()引入
exports = { hello: true }; // 不能导出,因为exports不再是module.exports的缩写

当module.exports属性完全被一个新对象取代,可以重新赋值exports:

module.exports = exports = function Constructor() {
  // ... etc.
};

为了解释这个现象,可以想象一下require()的实现。

为什么重新赋值exports会导致模块导出失败?

function require(/* ... */) {
  const module = { exports: {} }; // 全局module对象
  ((module, exports) => {
    // 模块代码
    function someFunc() {}
    // 这里的exports是module.exports的引用
    exports.someFunc = someFunc;
    // 这里的exports被重新赋值,不再是module.exports的引用
    exports = someFunc;
    // 这里的module是const module = { exports: {} };的module
    module.exports = someFunc;
  })(module, module.exports); // 传入module.exports作为exports的引用
  return module.exports;
}

从上面的代码及注释,可以很清晰看出 重新赋值exports会导致模块导出失败 的原因:

重新给exports赋值后,局部变量(对象)丢失了对全局变量(对象)的引用。

vue.js分析

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword');
  }
  this._init(options);
}

vue.runtime.common.js

module.exports = Vue;

module.exports导出了Vue全局构造函数。commonjs模块机制。

vue.runtime.esm.js

export default Vue;

export default同样导出了Vue全局构造函数。es modules模块机制。

vue.runtime.js

(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
    typeof define === 'function' && define.amd ? define(factory) :
    (global.Vue = factory());
}...)

common.js 如果exports是对象,而且module有定义,也就是说module.exports导出factory()。 amd规范,导出factory。 否则就生成一个向global的Vue属性挂载factory()。

项目实战分析

media.js

/**
 * * 功能:转换Blob和File为Base64 string
 * * 思路:创建FileReader实例,读取file文件,FileReader实例的loadend事件触发获得duration
 * * 作者:FrankKai
 * * 日期:2019/11/8
 * @param {Blob|File} file The file to be transform
 * @return {number} Return the file's Base64 string
 */
async function transferBlobFileToBase64(file) {
  return new Promise((resolve) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onloadend = () => {
      const fileBase64 = reader.result;
      resolve(fileBase64);
    };
  });
}
/**
 * * 功能:获取音频文件时长(audio duration)
 * * 思路:创建伪<audio>,加载audio文件,<audio>的durationChange事件触发获得duration
 * * 作者:FrankKai
 * * 日期:2019/11/7
 * @param {File|Url} source The source to be analyse
 * @return {number} Return the audio's duration
 */

function getAudioDuration(source) {
  return new Promise((resolve) => {
    const audioElement = document.createElement('audio');
    audioElement.src = window.URL.createObjectURL(source);
    audioElement.ondurationchange = (e) => {
      const duration = e.path[0].duration;
      resolve(duration);
      URL.revokeObjectURL(audioElement.src);
    };
  });
}
export default { getAudioDuration, transferBlobFileToBase64 };

es modules方式导出函数。

代码编译后:

webpackJsonp[0], {
    aPPT: function(e, t, n) {
        "use strict";
        var r = n("//Fk")
          , i = n.n(r);
        t.a = {
            getAudioDuration: function(e) {
                return new i.a(function(t) {
                    var n = document.createElement("audio");
                    n.src = window.URL.createObjectURL(e),
                    n.ondurationchange = function(e) {
                        var r = e.path[0].duration;
                        t(r),
                        URL.revokeObjectURL(n.src)
                    }
                }
                )
            }
        }
    },
{

manifest.js中

    function b(c) {
        if (a[c])
            return a[c].exports;
        var f = a[c] = {
            i: c,
            l: !1,
            exports: {}
        };
        return e[c].call(f.exports, f, f.exports, b),
        f.l = !0,
        f.exports
    }

webpack最终还是以exports导出的。

参考资料:https://nodejs.org/dist/latest-v12.x/docs/api/modules.html#modules_exports_shortcut