Adamwu1992 / adamwu1992.github.io

My Blog
2 stars 0 forks source link

JavaScript模块化 #6

Open Adamwu1992 opened 6 years ago

Adamwu1992 commented 6 years ago

CommonJS是在运行时加载模块,ES6 Module是在编译是加载模块,具体来讲的区别就是,require可以被当作一个普通的函数来使用,import只能出现在文件的最开头部分:

// 可以通过条件判断是否加载
if (type === 1) {
  var m = require('./moduleA');
}

// 可以包裹在函数中,参数可以拼接
function loadModule(path) {
  return require('./' + path)
}

另一个差异也和两者的加载模式息息相关:CommonJS是简单的值传递或者引用传递,ES6 Module是强绑定的,包括基础类型, ES6中可以获取到模块内实时的值:

// a.js
export let a = 100;
setTimeout(() => a = a + 100, 500);

// b.js
import { a } from './a';
setTimeout(() => {
  console.log(a);  // 将会打印200
}, 1000);

CommonJS中获取的是模块内的拷贝:

// a.js
let a = 100;
module.exports = a;
setTimeout(() => a = a + 100, 500);

// b.js
let a = require('./a');
setTimeout(() => {
  console.log(a);  // 将会打印100
}, 1000);
Adamwu1992 commented 6 years ago

import & export in ES6

import和export目前尚未在任何浏览器中实现,通常是借用babel/rollup之类的工具将esm代码转为cjs代码,方便在nodejs端运行,转译的代码会加上__esModule: true的标识。

export

导出方式

导出有两种不同的方式:

以下方法会忽略默认导出:

export * from 'moduleA'

如果需要可以使用这种方式:

import foo from '[moduleA]';
export { foo }

导出关系

export default function f() {}
export const a = 100;
let b  = 200;
export { b, f }

如果把以上模块的全部内容导入,会是一个包含四个属性值的对象,使用babel-node打印如下:

{ default: [Function: f], a: 100, b: 200, f: [Function: f] }

可以猜测,不管在模块中导出多少次,都会将导出的对象合并到一个对象中,而且如果有默认导出的话也会在合并后的对象中增加一个default名字的对象,所以如果要导出默认模块,以下两种方法都是一样的:

// 将默认对象命名为fa
import fa form 'moduleA'
// 将默认对象命名为fb
import { default as fb } from 'moduleA'

import

Note: { xxx, xxx }的语法和import用在一起时,不是对象解构(object destruct),而是导入模块中对应的命名导出(named export)。

// lib.js
export const a = 1;
export const b = 2;

// index.js
import { a, b} from 'lib'

所以下面的模块在导入是可能会出现问题

// lib.js
export default {
  a: 1,
  b: 2
}

// index.js
// 错误导入
import { a, b } from 'lib'

// 正确导入
import lib from 'lib'
const { a, b } = lib

在babel5中,上面的错误写法可以正确到导入值,原因是babel在处理export default时,会将默认导出的对象绑定到module.exports上:

// lib.js
export default {
  a: 1,
  b: 2
}

// dist.js
'use strict';
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = {
  a: 1,
  b: 2
};

// babel5额外增加
module.exports = exports['default']

而导入的语法进过转译会变成:

const { a, b } = require('lib')

这样就可以取到绑定在module.exports上的值了,但是这种转译破坏了esm的定义,所以babel6中取消了对module.exports的额外绑定。如果我们要用cjs的方式导入esm模块中的默认导出,需要这样写:

const { a, b } = require('lib').default

为了兼容旧代码,add-module-exports会将代码按照babel5的方式处理。

Adamwu1992 commented 6 years ago

CMD & AMD

早期的js并不是用来实现大型程序,没有模块化的需求,但是随着js需要处理的问题越来越复杂,js模块化的需求也越来越强烈,AMD和CMD都是为了适应前端模块化的需求而出现的。RequireJS是AMD规范的实现,SeaJS是CMD规范的实现。

虽说都实现了前端js的模块化,但是RequireJSSeaJS在加载模块的方式上还是有差异的,简单来说就是,RequireJS对于模块是预加载的,SeaJS对于模块是懒加载的。

有这样一个模块:

define(function(require, exports, module) {
  console.log('require main');
  var mod1 = require('./mod1');
  mod1.hello();
  var mod2 = require('./mod2');
  mod2.hello();
  return {
    hello: function() {
      console.log('hello main');
    }
  }
})

使用SeaJS加载的结果如下:

require main
require mod1
hello mod1
require mod2
hello mod2
hello main

使用RequireJS加载的结果如下:

require mod1
require mod2
require main
hello mod1
hello mod2
hello main

SeaJS只有在模块需要使用的时候才去加载模块,也就是执行模块的代码;而RequireJS的处理方式则是提升执行,所谓预加载就是检测到代码里存在require就会提升到顶部执行,而且提升的顺序也和require在代码中出现的顺序无关,有可能mod2会在mod1之前执行

Adamwu1992 commented 6 years ago

CommonJS

正如当年为了统一 JavaScript 语言标准,人们制定了 ECMAScript 规范一样,如今为了统一 JavaScript 在浏览器之外的实现,CommonJS 诞生了。CommonJS 试图定义一套普通应用程序使用的 API,从而填补 JavaScript 标准库过于简单的不足。CommonJS 的终极目标是制定一个像 C++ 标准库一样的规范,使得基于 CommonJS API 的应用程序可以在不同的环境下运行,就像用 C++ 编写的应用程序可以使用不同的编译器和运行时函数库一样。为了保持中立,CommonJS 不参与标准库实现,其实现交给像 Node.js 之类的项目来完成。

CommonJS也是用来解决js的模块化的,但是只适用于服务端的js,Node.js就采用了CommonJS的规范来实现自身的模块系统的。 CommonJS采用同步的方式加载模块,使用简单。在服务端,性能的瓶颈是CPU和内存,而不是带宽,所以AMD和CMD要求异步加载模块,而CommonJS可以同步加载模块。 Node.js并没有完全遵守CommonJS的规范,但除非我们的程序需要兼容Node.js之外的CommonJS实现库,一般情况下不需要关心他们之前的区别。

在node中,每一个文件就是一个模块,模块中的变量和函数都是私有的,对其他文件不可见。如果需要定义其他文件可见的变量需要挂载在global对象上,但是这种写法是不推荐的。

module

每个模块内部都有一个module变量,代表当前的模块,它有以下属性:

module.exports

在node中module代表当前模块,module.exports是对外的接口,当我们加载一个模块的时候,实际上是加载这个模块的exports上挂载的属性。

为了方便,node中还有一个exports变量,指向module.exports,相当于每个node文件都有一行隐藏的代码var exports = module.exports。所以使用 exports导出对象的时候,不能直接改变它的指向,只能用exports.xxx = function() {}方式,而如果模块里只有一个方法需要导出,我们可以用这样的方式module.exports = function() {}导出唯一的方法。

require

nodejs使用require命令来加载一个模块,它根据加载规则找到对应的模块执行,并且返回该模块的exports属性。

加载文件规则

默认是加载后缀为.js的文件,后缀可以省略。

目录加载规则

通常发布在npm上的模块,都会把相关的文件放在一个目录里,目录中有一个package.json文件,用main字段指定入口文件,如果找不到入口文件,就找index.js或者index.json文件。

Adamwu1992 commented 6 years ago

Node.js中的循环加载

// main.js
console.log('in main.js for base', require('./base').x);
console.log('in main.js for util', require('./utils').x);

// util.js
exports.x = 'b1';
console.log('in util.js', require('./base').x);
exports.x = 'b2';

// base.js
console.log('in base.js', require('./utils').x)
exports.x = 'a2';

运行main.js后打印的结果如下:

in util.js undefined
in base.js b2
in main.js for base a2
in main.js for util b2

从结果倒推,可以分析出以下加载流程: