JS模块化笔记

261 阅读4分钟

1 什么是模块化,模块化解决什么问题?

1.1 什么情况下提出的

前端业务需求越来越复杂,导致代码量越来越大,维护管理越来越困难。这就需要一种更好的模式,去更好的进行代码的管理、组织、通信。

1.2 什么是模块化

本质上模块就是一种提供对外通信接口,进行代码切分/组合的管理方式。其呈现的方式因不同的模块化方案而不同,基本是以文件粒度区分。目标是管理变量。

1.3 为什么要用模块化

  1. 将复杂的问题分为多个子问题

    关注代码分离,单一职责

  2. 大型软件开发的技术基础

    • 更优雅的代码管理
    • 易于替换、复用、拓展
    • 🔥 内聚(变量,行为内聚在模块内,对外暴露接口进行通信)
  3. 方便多人协同

2 模块化的发展

2.1 函数时代

模块化混沌时期,组织代码就是靠「经验」,基本就是全局函数各种调用

问题:

  • 命名冲突

2.2 命名空间

模块化思想雏形初现,通过简单的命名空间进行「块儿」的切分,更分离,和内聚弥补了全局函数的一些缺点

问题

  • 可以被别人修改

2.3 闭包

模块化的解决方案再次提升,利用闭包使得污染的问题得到解决,更加纯粹的内聚。

3 AMD CMD UMD

3.1 AMD

AMD 代表库是 require.js

James Burke 提出,解决 cmj 不能在浏览器使用的问题。提出了 AMD 规范,使用异步处理模块依赖

3.1.1 使用


define(id?, depencies?, factory);

define('foo', ['utils'], function(utils){

})

3.1.2 行为分析

  1. 分析
define('a', function(){
  console.log('a load');

  return {
    run: function() { console.log('a run'); }
  }
})

define('b', function(){
  console.log('b load');

  return {
    run: function() { console.log('b run'); }
  }
})

require(['a', 'b'], function(a, b) {
  console.log('main run');
  a.run()
  b.run()
})

// a load
// b load
// main run
// a run
// b run

  1. 结论:

require 的时候加载依赖模块

3.1.3 主要方法

  1. 可以配置依赖路径
rj.config({path: {
  'jqurey': 'xxxx'
}})
  1. 定义模块
define('moduleA', ['jquery'], function(jquery){ })
  1. 加载模块
rj(['moduleA'], function(moduleA){ })
  1. 简单模拟实现
// 创建依赖的缓存
const def = new Map()

const defaultOptions = {
  paths: ''
}

// From CDN
const __import = url => {
  return new Promise((resove, reject) => {
    System.import(url).then(resove, reject)
  })
}

// normal script
const __load = url => {
  return new Promise((resolve, reject) => {
    const head = document.getElementsByTagName('head')[0]
    const node = document.createElement('script')
    node.type = 'text/javascript'
    node.src = url
    node.async = true
    node.onload = resolve
    node.onerror = reject
    head.appendChild(node)
  })
}


rj = {}

// 实现 config 方法:一般用来收集外界引入的 模块
rj.config = options => Object.assign(defaultOptions, options)

// 定义模块,触发的时机其实是在 require 的时候,所以 -> 收集
define = (name, deps, factory) => {
  // todo 参数的判断,互换
  def.set(name, { name, deps, factory })
}

// dep -> a -> a.js -> 'http:xxxx/xx/xx/a.js';
// 这里默认 文件名称 和 定义时的 name 一致
const __getUrl = dep => {
  const p = location.pathname
  return p.slice(0, p.lastIndexOf('/')) + '/' + dep + '.js'
}

// 其实才是触发加载依赖的地方
require = (deps, factory) => {
  return new Promise((resolve, reject) => {
    Promise.all(
      deps.map(dep => {
        // 走 CDN
        if (defaultOptions.paths[dep])
          return __import(defaultOptions.paths[dep])

        // 走 script 标签下载
        return __load(__getUrl(dep)).then(() => {
          const { deps, factory } = def.get(dep)
          if (deps.length === 0) return factory(null)
          return require(deps, factory)
        })
      })
    ).then(resolve, reject)
  }).then(instances => factory(...instances))
}

3.2 CMD

代表人物 玉伯, 代表库 sea.js

3.2.1 用法及行为

// sea.js
define('a', function (require, exports, module) {
  console.log('a load')
  exports.run = function () { console.log('a run') }
})

define('b', function (require, exports, module) {
  console.log('b load')
  exports.run = function () { console.log('b run') }
})

define('main', function (require, exports, module) {
  console.log('main run')
  var a = require('a')
  a.run()
  var b = require('b')
  b.run()
})

seajs.use('main')

// main run
// a load
// a run
// b load
// b run

3.3 AMD UMD的对比

  1. 依赖前置,依赖后置

    ✏ AMD 是在 require 的时候加载依赖的全部模块,然后再运行主方法。即所谓的依赖前置

    ✏ CMD 是在 使用模块的时候 才触发加载。即所谓的依赖后置

    🔥 实际上本质是是否需要按需加载

4 ES6 import

JS 模块化的发展方向,语法层面的模块化规范

  1. 用法

// ./a.js
export const a = 123

// ./b.js
import { a } from './a.js'

// ./c.js
export const b = 456
export default function fn() {}

// ./d.js
import fn from './c.js'
// import './c.js'

// ./e.js
import fn, { b } from './c.js'
  1. 注意点

    • export 和 export default 可同时使用。互不影响
    • 导出时 只能声明时导出以对象的形式导出
    • 引入时不能将大括号里的内容简单的想成对象,不可使用其赋默认值。可以使用 as 改名称

5 node commonJS

nodeJs 目前使用的模块化规范

  1. 用法

// ./a.js
exports.a = 123

// ./b.js
const { a } = require('./a.js')

// ./c.js
module.exports = function () {}

// ./d.js
const fn = require('./c.js')

  1. 注意点

    • exports其实是指向module.exports的引用;
    • require() 返回的是 module.exports 而不是 exports;
    • module.exports 的初始值为一个空对象{},所以 exports也为空对象{};
    • module.exports对象不为空的时候exports对象就自动忽略;所以同时使用只有 module.exports 的内容会生效

6 知识点 --》待完善

  1. 按需加载的本质是使用 promise

  2. amd cmd 依赖前置,依赖后置 --> 按需加载

  3. iife ❓ 立即调用函数表达式

  4. cmj esm 值拷贝,引用拷贝

  5. cmj 同步,esm 异步

  6. cmj esm 缓存

  7. cmj 运行时加载,esm 编译时加载

  8. cmj esm 怎么解决循环依赖

  9. webpack 的模块化机制