模块化简史

JS模块化总结
模块化发展历程

  1. // 松耦合拓展
  2. // 这种方式使得可以在不同的文件中以相同结构共同实现一个功能块,且不用考虑在引入这些文件时候的顺序问题。
  3. // 缺点是没办法重写你的一些属性或者函数,也不能在初始化的时候就是用module的属性。
  4. var module = (function(my){
  5. // ...
  6. return my
  7. })(module || {})
  8. // 紧耦合拓展(没有传默认参数)
  9. // 加载顺序不再自由,但是可以重载
  10. var module = (function(my){
  11. var old = my.someOldFunc
  12. my.someOldFunc = function(){
  13. // 重载方法,依然可通过old调用旧的方法...
  14. }
  15. return my
  16. })(module)

commonjs

CommonJS标准囊括了JavaScript需要在服务端运行所必备的基础能力,比如: 模块化 IO操作 二进制字符串 进程管理 Web网关接口(JSGI)。

  1. 文件即模块 文件所有代码都运行在独立的作用域,因此不会污染全局空间。
  2. 模块可以被多次引用 加载。在第一次被加载时, 会被缓存,之后都从缓存种直接读取结果
  3. 加载某个模块, 就是引入该模块的 module.exports属性
  4. module.exports 属性 输出的是 值的拷贝 ,一旦这个值被输出 模块内发生变化不会影响到输出的值
  5. 模块加载顺序按照代码引入的顺序。
  6. 注意 module.exports 和 exports的区别

CommonJS规范代码如何在浏览器端实现,其实就是实现 module.exports 和 require方法

  1. let module = {}
  2. module.exports = {}
  3. (function(module, exports) {
  4. // ...
  5. }(module, module.exports))

image.png

手写CommonJS
浅谈前端模块化

  1. const path = require('path')
  2. const fs = require('fs')
  3. const vm = require('vm')
  4. // 定义Module
  5. function Module(id){
  6. this.id = id
  7. this.filename = id
  8. this.exports = {}
  9. this.loaded = false
  10. }
  11. // 定义拓展与解析规则
  12. Module._extensions = Object.create(null)
  13. Module._extensions['.json'] = function(module){
  14. return Module.exports = JSON.parse(fs.readFileSync(module.filename, 'utf8'))
  15. }
  16. Module._extensions['.js'] = function(module){
  17. Module._compile(moudle)
  18. }
  19. // 包装函数
  20. Module.wrap = function(script) {
  21. return Module.wrapper[0] + script + Module.wrapper[1];
  22. };
  23. Module.wrapper = [
  24. '(function (exports, require, module, __filename, __dirname) { ',
  25. '\n});'
  26. ];
  27. // 编译执行
  28. Module._compile = function(module){
  29. const content = fs.readFileSync(module.filename, 'utf8'), filename = module.filename;
  30. const wrapper = Module.wrap(content)
  31. const compiledWrapper = vm.runInThisContext(wrapper, {
  32. filename: filename,
  33. lineOffset: 0,
  34. displayErrors: true,
  35. })
  36. const result = compiledWrapper.call(module.exports, module.exports, require, module, filename, dirname);
  37. return result
  38. }
  39. // 缓存
  40. Module._cache = Object.create(null)
  41. Module.prototype.load = function(filename){
  42. let extname = path.extname(filename)
  43. Module._extensions[extname](this);
  44. this.loaded = true;
  45. }
  46. // 加载
  47. Module._load = function(filename) {
  48. const cacheModule = Module._cache[filename]
  49. if(cacheModule){
  50. return cacheModule.exports
  51. }
  52. let module = new Module(filename)
  53. Module._cache[filename] = module
  54. module.load(filename)
  55. return module.exports
  56. }
  57. // 简单的路径解析
  58. Module._resolveFilename = function(path) {
  59. let p = path.resolve(path)
  60. if(!/\.\w+$/.test(p)){
  61. let arr = Object.keys(Module._extensions)
  62. arr.forEach(item => {
  63. let file = `${p}${item}`
  64. try{
  65. fs.accessSync(file)
  66. return file
  67. }catch(e){
  68. // ...
  69. }
  70. })
  71. }else{
  72. return p
  73. }
  74. }
  75. // require 函数
  76. function require(path){
  77. const filename = Module._resolveFilename(path)
  78. return Module._load(filename)
  79. }

AMD

通过define方法 将代码定义为 模块。
通过require方法 实现代码模块加载。
记手写requirejs的思考过程

文件是怎么被加载的?

浏览器加载js文件只有一种方法:通过script标签。所谓文件加载,也即是构建script标签加到页面上。

demo-example

  1. // text.html
  2. // <script data-main="scripts/main" src="scripts/require.js"></script>
  3. // main.js
  4. require(["helper/sum"], function(sum) {
  5. console.log('sum', sum)
  6. });
  7. // helper/sum.js
  8. define(['helper/num_1', 'helper/num_2'], (num1, num2) => {
  9. return {
  10. sum: num1.num + num2.num
  11. }
  12. })
  13. // helper/multi.js
  14. define(() => {
  15. return function (data) {
  16. return data * 2
  17. }
  18. })
  19. // helper/num_1.js
  20. define(() => {
  21. return {
  22. num: 1
  23. }
  24. })
  25. // helper/num_2.js
  26. define(['helper/multi'], (multi) => {
  27. return {
  28. num: multi(3)
  29. }
  30. })
  1. var require;
  2. var define;
  3. (function () {
  4. var executedModule = {}; // key 为已执行的模块名称,value为执行后的值
  5. var handlers = []; // 待依赖完成而执行的模块
  6. var depsToName = {}; // 路径和名称对应表,key为路径,value为模块名
  7. function checkIsAllLoaded(handle) {
  8. if (!handle.deps.length) return true // 没有依赖
  9. return handle.deps.every(dep => {
  10. const moduleName = depsToName[dep]
  11. return !!executedModule[moduleName] === true
  12. })
  13. }
  14. function getArgsFromDepend(handle) {
  15. const args = []
  16. handle.deps.forEach(re => {
  17. const moduleName = depsToName[re]
  18. if (executedModule[moduleName]) {
  19. args.push(executedModule[moduleName])
  20. }
  21. })
  22. return args
  23. }
  24. function runHandles() {
  25. handlers.forEach((handle, index) => {
  26. const isDependLoaded = checkIsAllLoaded(handle)
  27. if (isDependLoaded) {
  28. const arg = getArgsFromDepend(handle)
  29. var result = handle.fn(...arg)
  30. executedModule[handle.name] = result
  31. handlers.splice(index, 1)
  32. runHandles()
  33. }
  34. })
  35. }
  36. function addNameToModule(urlName) {
  37. for (let i = 0; i < handlers.length; i++) {
  38. if (handlers[i].isLoading) {
  39. if (!handlers[i].name) { //如果没有定义define名字
  40. handlers[i].name = urlName
  41. }
  42. depsToName[urlName] = handlers[i].name // 将depUrl与moduleName关联起来
  43. handlers[i].isLoading = false
  44. break
  45. }
  46. }
  47. }
  48. // 加载依赖
  49. function loadRequireModule(deps) {
  50. for (let i = 0; i < deps.length; i++) {
  51. const url = deps[i]
  52. let script = document.createElement('script')
  53. script.src = 'scripts/' + url + '.js'
  54. script.setAttribute('data-name', url)
  55. document.body.appendChild(script)
  56. script.onload = (data) => {
  57. const moduleName = data.target.getAttribute('data-name')
  58. addNameToModule(moduleName) // 添加名字
  59. runHandles() // 执行回调
  60. }
  61. }
  62. }
  63. require = (deps, cb) => {
  64. if (typeof deps === 'function' && cb === undefined) {
  65. cb = deps;
  66. url = []
  67. }
  68. if (!Array.isArray(deps)) {
  69. throw 'first argument is not array'
  70. }
  71. if (typeof cb !== 'function') {
  72. throw 'second argument must be a function'
  73. }
  74. handlers.push({
  75. name: 'main',
  76. deps: deps,
  77. fn: cb
  78. })
  79. loadRequireModule(deps)
  80. }
  81. // require define 都只是添加函数到 handlers 并执行以来下载
  82. define = (name, deps, cb) => {
  83. if (typeof name === 'function') {
  84. cb = name
  85. deps = []
  86. name = null
  87. }
  88. if (Array.isArray(name) && typeof deps === 'function') {
  89. cb = deps
  90. deps = name
  91. name = null
  92. }
  93. if (typeof name === 'string' && typeof deps === 'function') {
  94. cb = deps
  95. deps = []
  96. }
  97. handlers.push({
  98. name: name,
  99. deps: deps,
  100. fn: cb,
  101. isLoading: true // 这个字符标识的是当前正在加载的路径,用于为其添加名字时的定位
  102. })
  103. loadRequireModule(deps)
  104. }
  105. function startMain() {
  106. const scripts = document.querySelectorAll('script');
  107. for (let i = 0; i < scripts.length; i++) {
  108. const script = scripts[i]
  109. const attr = script.getAttribute("data-main");
  110. if (attr) {
  111. const mainScript = document.createElement('script')
  112. mainScript.src = attr + '.js'
  113. window.onload = () => {
  114. document.body.appendChild(mainScript)
  115. }
  116. }
  117. }
  118. }
  119. startMain()
  120. })()

执行流程总结

  1. 获取html上 script -> scripts ->循环匹配获取 data-main attr = script.getAttribute(‘data-main’)-> 创建script加载 attr
  2. attr 其实就是 require([a,b],function(){}) 加载依赖 -> handlers.push({ name:’main’,deps:deps,fn:cb})
  3. loadRequireModule(deps) 循环deps 创建script加载依赖。

    1. script.setAttribute(‘data-name’,url)
    2. script.onload moduleName = getAttribute(‘data-name’) 也就是 define 定义的模块
    3. define handlers.push({ name:name,deps:deps,fn:cb,isLoading:true}) -> loadRequire(deps)
    4. addNameToModule(moduleName) 循环 handlers 通过isLoading判断当前加载的模块 depsToName[urlName] = handlers[i].name handlers[i].isLoading = false
    5. runHandlers() 循环 handlers
    6. checkIsAllLoaded(handle)
    7. getArgsFromDepend(handle)

      cmd

      AMD和CMD的两个主要区别如下:
  4. AMD需要异步加载模块,而CMD在require依赖的时候,可以通过同步的形式(require) 也可以通过异步的形式(require.async)

  5. CMD 遵循依赖就近的原则,AMD遵循依赖前置原则。也就是说 AMD中,我们需要把模块所需要的依赖都提前在依赖数组种声明。而在CMD中 我们只需要在具体代码逻辑内,使用逻辑前,把依赖的模块require进来。

    umd

    UMD 允许在环境中同时使用 AMD 与CommonJS规范,相当于一个整合。该模式的 核心思想 在于利用 立即执行函数根据环境来判断需要的参数类别
    1. (function (root, factory) {
    2. if (typeof define === 'function' && define.amd) {
    3. // AMD 规范
    4. define(['b'], factory);
    5. } else if (typeof module === 'object' && module.exports) {
    6. // 类 Node 环境,并不支持完全严格的 CommonJS 规范
    7. // 但是属于 CommonJS-like 环境,支持 module.exports 用法
    8. module.exports = factory(require('b'));
    9. } else {
    10. // 浏览器环境
    11. root.returnExports = factory(root.b);
    12. }
    13. }(this, function (b) {
    14. // 返回值作为 export 内容
    15. return {};
    16. }));

    es6module

    ES模块的设计思想是尽量 静态化。 这样能保证在编译时就确定模块之间的依赖关系,每个模块的输入和输出变量也也都是确定的。CommonJS和AMD 无法保证前置即确定这些内容,只能在运行时确定。
    CommonJS模块输出的是一个值的拷贝
    ES模块输出的是值的引用。

    ES模块化为什么要设计成静态的

    通过静态分析,我们能够分析出导入的依赖。如果导入的模块没有被使用,我们便通过 tree shaking等手段减少代码体积,从而提升运行性能。 这个就是基于ESM实现 tree shaking的基础。

    tree shaking