五分钟掌握前端模块化发展(附字节模块化真题)

188 阅读4分钟

前言

首先我需要了解一下,为什么需要前端模块化呢?

Web初期,也不需要什么项目打包,前端工程化只需要三个文件,HTML,JS,CSS,三个大模块,那时候就能很好的满足需求了。

后来业务需求不断累加,业务也日益复杂化,所以导致了JS代码日益膨胀,JS文件越来越大很难进行维护了,所以我们自然会想到,将JS代码进行切分,这里提到的切分 实际上就是对 JS 代码的模块化

目的

模块化主要是用来抽离公共代码,隔离作用域,避免变量冲突等。

模块化 代码管理/编译,业务分离的基本单元 模块化的本质上 是一种提供对外通信接口,进行代码切分/组成的管理方式,其呈现的方式因不同的模块化方案而不同,基本是以文件粒度区分

本质上就是 使用一个函数 将变量挂载 到函数上 返回出去

module.exports = xxx; // 变量 函数等等

image.png

模块化的演进过程

全局函数阶段

例子

function m1() {
    // ...
}

function m2() {
    // ...
}

问题

污染全局命名空间,容易引起命名冲突,而且模块成员之间看不出直接关系

NameSpace 命名空间阶段

🌰例子

const myModule = {
    data: 'data',
    do() {
        console.log('do something')
    },
};


myModule.data = null;
myModule.do(); // 执行方法

现代模块化规范

以文件为模块,有自己的作用域

在一个文件里面定义的变量,函数,类,都有私有的,对其他文件不可见

  • CommonJS
  • AMD
  • UMD

CommonJS

适用于服务器端

特点:

  1. 文件作用域
  2. 缓存
  3. 同步加载

AMD

Asynchronous Module Deinition

AMD 代表的是肯定是 ReuquireJS requied('fs')

James Burke 觉得 CMJ 很好,但是在浏览器上玩不转,所以提出一个 AMD 规范,异步不会阻塞浏览器, 适用于客户端

  1. 文件作用域
  2. 非同步加载

define(id?, depencies?, factory); // id 模块,depencies 模块的内部依赖 

define('foo', ['utils', 'bar'], function(utils, bar) {
    utils.add(1, 2)
    return {
        name: 'foo'
    }
})

UMD

Universal Module Deinition 前后端跨平台的模块化解决方案

实现原理

  1. 先判断是否支持 Node.js 模块格式 (exports 是否存在),存在就 Node.js 模块格式
  2. 再判断是否支持 AMD (define是否存在),存在则使用 AMD 方式加载模块
  3. 前两个都不存在,则将模块公开到全局(window 或者 global)

发展历程总结

image.png

字节模块化真题 实现一个符合 AMD 的 require.js

  1. 实现一个 可移植直接配置依赖路径
RequestJs.config({ paths: {
    'jQuery': 'https://www.jQuery.cdn.min.js'
} }) // 传入一个 路劲对象 然后 注册

RequestJs(['jQuery'], function(jquery) {
    // ... 
})
  1. 加载模块
RequestJs(['jQuery'], function(jquery) {
    // ... 
})
  1. 定义模块
RequestJs('jQuery', [], function(jquery) {
    // ... 
})

1. 例子行为

// RequiredJS

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('a run')}
    }
})

require(['a', 'b'], function(a, b) {
    console.log('main run') // 这行不是最先执行的 先执行 a,b,main,run
    
    a.run(); // a 模块加载
    b.run(); // b 模块加载
})

需要实现的内容

  1. require 时候加载依赖的模块 require 的时候就把数组中的内容加载进来了
  2. 然后执行回调时候的函数
  3. 最后执行 define 中 return 的内容

开始实现

2. 定义 define

const def = new Map(); // Map 数据结构

// 定义模块,触发时机 是在 require 时候 所以 -> 收集
rjDefine = (name, deps, factory) => {
  // todo 参数之间判断 互换等等
  def.set(name, { name, deps, factory }); // 存在一个 map 结构的对象中
}

3. 使用 define

rjDefine('a', ['lodash'], function() {
  console.log('moduleA load');
  return {
    strfunction() {
      console.log('module A return data')
      return _.repeat('>>>>>'20)
    }
  }
})

4. require 加载资源方法

// 加资源拼接加载到 HTML 上
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);
  })
}

// 触发加载的地方
rjRequire = (deps, factory) => {
  return new Promise((resolve, reject) => {
    Promise.all(deps.map(dep => {
      // 加载依赖 判断当前的依赖是否来自于 CDN的
      if (defaultOptions.paths[dep]) {
        console.log(defaultOptions.paths[dep])
        return __import(defaultOptions.paths[dep]);
      }
      return __load(__getUrl(dep)).then(() => {
        const { deps, factory } = def.get(dep);
        if (deps.length === 0) {
          return factory(null); // 如果获取依赖的时候 他没有依赖其他的库
        }
        // 如果他依赖了其他的库 define的时候 依赖了 其他的库
        // 执行递归
        return rjRequire(deps, factory)
      });
    })).then(resolve, reject)
  }).then(instances => factory(...instances))
}

5. 结果

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello amd</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/systemjs/6.10.2/system.js"></script>
    <script src="../rj.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <div id="app2"></div>
    <script>
      rj.config({
        paths: {
          lodash'https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.js'
        }
      })
      rjRequire(['lodash''a'], function(_, a) {
        console.log('require run');
        document.querySelector('#app').innerHTML = _.random(5)
        document.querySelector('#app2').innerHTML = a.str();
      })
    </script>
  </body>
</html>