CodeLittlePrince/blog

JS模块化扫盲

CodeLittlePrince opened this issue · 0 comments

模块化目的

  1. 避免命名冲突;
  2. 依赖处理;

分类

  • AMD
  • CMD
  • Commonjs
  • ES module
    • brower(为了方便,后文用ES6-import-browser替代)
    • webpack(为了方便,后文用ES6-import-webpack替代)
    • node(为了方便,后文用ES6-import-node替代)
  • UMD

AMD

定义

amdjs api

代表库

requirejs

用法

1.html引入requirejs主js:

<script src="/require.js"></script>
<script src="/index.js"></script>

主js:

// index.js
console.log('index')

// 加载入口模块
requirejs(['a'], function(a) {
  console.log('index call')

  a.speak()
});

模块a:

// a.js
console.log('a')

define(function(require, factory) {
  console.log('a call')

  var b = require('b')
  var c = require('c')

  return {
    speak: b.speak,
    say: c.say
  }
});

模块b:

// b.js
define(function () {
  console.log('b')

  return {
    speak: function () {
      console.log('Hello World')
    }
  };
});

模块c:

// c.js
console.log('c')

define(function () {
  console.log('c call')

  return {
    say: function () {
      console.log('Wow')
    }
  };
});

最终,浏览器Console会打印出:

index
a
b
b call
c
c call
a call
index call
Hello World

过程

  1. script标签加载require.js。

  2. script标签加载index.js。

  3. index.js中通过requirejs函数定义该文件为入口文件。所有子模块(a、b、c)加载方式其实是在<head>里append对应的js,并且带async属性。

  4. js按照
    index.js=>a.js=>b.js
    =>c.js
    这样的顺序加载执行。所以,自然的先打印出index、a。又因为,b和c都是a所依赖的模块,所以b、c同时异步加载,谁加载完即先执行谁,因为两文件大小几乎一样,并且本地测试网络延迟可以忽略原因,先require谁就会先执行谁,并且,因为b、c都是最后一项被依赖的模块,所以回调就会被调用,因此打印出b、b call、c、c call。

  5. a中的依赖逻辑处理完之后,才轮到a除了require之外的逻辑处理,所以继续打印出a call。
    (因为它的实现可以看define的实现,其实是将define回调里的函数变为字符串,然后通过正则cjsRequireRegExp获取到所有的模块名,加入依赖队列;接着,去请求js资源执行,同样,如果被依赖的js文件还有依赖模块,则加入依赖队列,没有则执行define里的回调函数;另外顺便说下,回调的返回值会被defined对象收集,实现可见localRequire实现,所以,如b=require('b')得到的值即是b.js的define回调的返回值)

  6. index中的依赖逻辑处理完之后,才轮到index除了require之外的逻辑处理,所以继续打印出index call。

优点

  1. 提升页面响应速度:js文件异步加载;

  2. 模块化成本低:只需要引入require.js就可以实现模块化;

缺点

  1. 代码执行顺序不按书写顺序:从文件a.js可以看出,require的模块执行顺序是在console.log('a call')之前,并不是按照书写的顺序那样;

  2. 无法按需加载:比如:if(false){ require('a') },这样的写法并不会就不去加载a.js的文件了,照样会加载;另外,像上面说到的,b、c可能因为文件大小或者网络原因,执行的顺序有可能不会像require的顺序一样。

CMD

定义

cmdjs specification

代表库

seajs

用法

1.html引入seajs主js:

<script src="/seajs.js"></script>
<script src="/index.js"></script>

主js:

// index.js
console.log('index')

// 加载入口模块
seajs.use(['a'], function(a) {
  console.log('index call')

  a.speak()
});

模块a:

// a.js
console.log('a')

define(function(require, factory) {
  console.log('a call')

  var b = require('b')
  var c = require('c')

  // var flag = false
  // if (flag) {
  //   var c = require.async('c')
  //   c.say()
  // }

  return {
    speak: b.speak,
    say: c.say
  }
});

模块b:

// b.js
define(function () {
  console.log('b')

  return {
    speak: function () {
      console.log('Hello World')
    }
  };
});

模块c:

// c.js
console.log('c')

define(function () {
  console.log('c call')

  return {
    say: function () {
      console.log('Wow')
    }
  };
});

最终,浏览器Console会打印出:

index
a
b
c
a call
b call
c call
index call
Hello World

过程

  1. script标签加载sea.js。

  2. script标签加载index.js。

  3. index.js中通过seajs.use函数定义该文件为入口文件。所有子模块(a、b、c)加载方式其实是在<head>里append对应的js,并且带async属性。不过和requirejs不一样的是,seajs会用完后,把script给移除掉,为了减少内存占用。

  4. js按照
    index.js=>a.js=>b.js
    =>c.js
    这样的顺序加载执行。所以,自然的先打印出index、a。又因为,b和c都是a所依赖的模块,所以b、c同时异步加载,谁加载完即先执行谁,因为两文件大小几乎一样,并且本地测试网络延迟可以忽略原因,先require谁就会先执行谁。

  5. 和requirejs不一样的是,seajs不会加载完就立刻去执行define里的回调,而是等到父模块require的时候才去执行,所以我们看到,打印出的是a、b、c、a call、b call、c call。(上面说到,因为网络原因,c可能先于b加载完,那样的话打印出的就是a、c、b、a call、b call、c call)

  6. index中的依赖逻辑处理完之后,才轮到index除了require之外的逻辑处理,所以继续打印出index call。

优点

  1. 提升页面响应速度:js文件异步加载;

  2. 模块化成本低:只需要引入require.js就可以实现模块化;

相比requirejs优点

  1. 按需执行:比如:if(false){ require('c') },这样的写法虽然还是会就去加载c.js的文件,但是不会执行c模块里的define回调,从而提升代码执行性能;

  2. 按需加载:比如:if(false){ require.async('a') },这样的写法就不去加载c.js的文件了;

缺点

  1. 代码执行顺序不按书写顺序:从文件a.js可以看出,require的模块执行顺序是在console.log('a call')之前,并不是按照书写的顺序那样;另外,像上面说到的,b、c可能因为文件大小或者网络原因,执行的顺序有可能不会像require的顺序一样。

CommonJS

定义

CommonJS

用法

1.因为commonjs是node的模块化方式,我们我们直接在控制台用:

node index.js

主js:

// index.js
console.log('index')

// 加载入口模块
var a = require('./a')

console.log('index call')

a.speak()

模块a:

// a.js
console.log('a')

var b = require('./b')
var c = require('./c')

// var flag = false
// if (flag) {
//   var c = require('./c')
//   c.say()
// }

console.log('a call')

module.exports = {
  speak: b.speak,
  say: c.say
}

模块b:

// b.js
console.log('b')

module.exports = {
  speak: function () {
    console.log('Hello World')
  }
};

模块c:

// c.js
console.log('c')

module.exports = {
  say: function () {
    console.log('Wow')
  }
};

最终,控制台会打印出:

index
a
b
c
a call
index call
Hello World

过程

  1. node执行入口文件index.js。
  2. 之后的代码全都按照顺序执行,没有什么弯子。

和AMD、CMD的区别

  1. 使用场景不同:commonjs用于node,即后端。
  2. 模块同步加载:因为不需要像浏览器端那样考虑文件请求性能而做成异步加载。
  3. 代码顺序执行:所有的代码全都按照顺序执行,没有花花肠子,直男。

ES6-import-browser

定义

JavaScript modules

1.html引入主js:

<script type="module" src="/ES6-import/index.js"></script>

主js:

// index.js
console.log('index')

// 加载入口模块
import a from './a.js'

console.log('index call')

a.speak()

模块a:

// a.js
console.log('a')

import b from './b.js'
import c from './c.js'

// var flag = true
// if (flag) {
//   import('./c.js')
//   .then(c => {
//     c.default.say()
//   });
// }

console.log('a call')

export default {
  speak: b.speak,
  say: c.say
}

模块b:

// b.js
console.log('b')

module.exports = {
  speak: function () {
    console.log('Hello World')
  }
};

模块c:

// c.js
console.log('c')

module.exports = {
  say: function () {
    console.log('Wow')
  }
};

最终,浏览器Console会打印出:

b
c
a
a call
index
index call
Hello World

过程

  1. script标签加载index.js,值得注意的是script标签多了一个type="module"的属性。index.js通过type="module"的方式引入,从而自身也是一个模块化的文件,这点我们可以在index.js文件里用var创建一个变量,比如var x = 7来验证,我们会发现所有文件执行完后,我们在控制台中输入x,输出的是x is not defined。
  2. index.js import了a,import会被最先执行,所以虽然console.log('index')在import之前,确实等a加载执行完之后才开始被执行。
  3. a.js加载完后,同样的,虽然console.log('a')在import之前,但得等b、c都加载执行完之后才开始被执行。b、c是异步请求的,但和AMD还有CMD不一样的是,它的执行并不会因为c先于b加载完而先执行c文件。而是按照代码顺序,直到b加载执行完,才轮到c。因此打印出了:b => c => a => a call。
  4. a执行完后,再回到index开始执行,所以继续打印:index => index call => Hello World

优点

  1. 提升页面响应速度:js文件异步加载;

  2. 模块化成本低:原生支持;

  3. 按需加载:比如:if(false){ import('a.js') },这样的写法就不去加载c.js的文件了;

  4. 引入模块代码按顺序执行:虽然还是不能解决:虽然console.log('a')在import之前,但执行却晚于这两个模块执行,但是,至少不会因为b、c可能因为文件大小或者网络原因,导致这两个文件执行顺序有有变化。

缺点

  1. 兼容性:高版本浏览器;(但感谢webpack,可以忽略兼容性)

ES6-import-webpack -- 在webpack中的es6 import

介绍

简单地说,就是因为es6虽然有import的能力了,但是因为兼容性不好,所以,目前市面上,都会选择用webpack来做js的模块化,这样对开发者来说,就可以愉快的使用import了。

1.html引入主js:

<script src="/ES6-import-webpack/index.js"></script>

主js:

// index.js
console.log('index')

// 加载入口模块
import a from './a.js'

console.log('index call')

a.speak()

模块a:

// a.js
console.log('a')

import b from './b.js'
import c from './c.js'

// var flag = true
// if (flag) {
//   import('./c.js')
//   .then(c => {
//     c.default.say()
//   });
// }

console.log('a call')

export default {
  speak: b.speak,
  say: c.say
}

模块b:

// b.js
console.log('b')

module.exports = {
  speak: function () {
    console.log('Hello World')
  }
};

模块c:

// c.js
console.log('c')

module.exports = {
  say: function () {
    console.log('Wow')
  }
};

使用webpack打包

可以自行全局安装,或者局部安装webpack、webpack-cli。然后以index.js作为入口文件,编辑生成代码。

这里,我就直接生成了,文件为dist/main.js。

我们看下打包后的文件:

(() => {
  "use strict";
  console.log("b");
  const o = {
    speak: function () {
      console.log("Hello World")
    }
  };
  console.log("c");
  const l = {
    say: function () {
      console.log("Wow")
    }
  };
  console.log("a"), console.log("a call");
  const s = {
    speak: o.speak,
    say: l.say
  };
  console.log("index"), console.log("index call"), s.speak()
})();

最终,浏览器Console会打印出:

b
c
a
a call
index
index call
Hello World

其实是和es6 import一模一样的。

过程

  1. script标签加载index.js,然后顺序执行。

优点

  1. 兼容性:可认为除杠精浏览器外的所有版本浏览器;

  2. 零语法学习成本:和es6的模块化方式一模一样,没有语法上的学习成本;

  3. 按需加载:比如:if(false){ import('a.js') },这样的写法就不去加载c.js的文件了;

  4. 引入模块代码按顺序执行:虽然还是不能解决:虽然console.log('a')在import之前,但执行却晚于这两个模块执行,但是,至少不会因为b、c可能因为文件大小或者网络原因,导致这两个文件执行顺序有有变化。

缺点

  1. 提升页面响应速度:js都被加入一个文件中了,在目前http2.0时代,并不见得是一件好事;
  2. 学习成本:还得学webpack;

ES6-import-node -- 在node中的es6 import

介绍

毕竟node也是JavaScript,浏览器都支持import了,作为后端语言,不支持也不好意思吧。
当然,为了大一统,也是毕竟要做的一件事。

emmm,不过这个import真正被引入进Node其实挺晚的,实在v13才开始,以下是overflow里的引用。

Node.js >= v13

It's very simple in Node.js 13 and above. You need to either:

Save the file with .mjs extension, or
Add { "type": "module" } in the nearest package.json.
You only need to do one of the above to be able to use ECMAScript modules.

Node.js <= v12

If you are using Node.js version 8-12, save the file with ES6 modules with .mjs extension and run it like:

node --experimental-modules my-app.mjs

1.我们我们直接在控制台用(如果node版本为13及以上,可以不用参数):

node --experimental-modules index.mjs

主js:

// index.js
console.log('index')

// 加载入口模块
import a from './a.js'

console.log('index call')

a.speak()

模块a:

// a.js
console.log('a')

import b from './b.js'
import c from './c.js'

console.log('a call')

export default {
  speak: b.speak,
  say: c.say
}

模块b:

// b.js
console.log('b')

export default {
  speak: function () {
    console.log('Hello World')
  }
};

模块c:

// c.js
console.log('c')

export default {
  say: function () {
    console.log('Wow')
  }
};

最终,浏览器Console会打印出:

b
c
a
a call
index
index call
Hello World

并不像commonjs,这里的结果和浏览器端的表现一模一样。

UMD

介绍

UMD在我看来只是一种打包模式,做的是模块兼容的事情,所以不做具体例子。

使用的场景就是做npm包的时候,因为并不知道会被使用者用什么模块化方式引入,所以兼容了AMD、CommonJS的引入方式。

Demo地址

文中的所有demo地址都在:https://github.com/CodeLittlePrince/js-modules
有兴趣的小伙伴可以下载运行帮助理解。