chst365's blog chst365's blog
首页
  • Git
  • 网络
  • 操作系统
  • 浏览器
  • webpack
  • JavaScript
  • TypeScript
  • 性能
  • 工程化
  • React
  • 编程题
  • React技术揭秘
  • 算法
  • Node
  • 编码解码
  • NodeJS系列
  • Linux系列
  • JavaScript系列
  • HTTP系列
  • GIT系列
  • ES6系列
  • 设计模式系列
  • CSS系列
  • 小程序系列
  • 数据结构与算法系列
  • React系列
  • Vue3系列
  • Vue系列
  • TypeScript系列
  • Webpack系列
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

chst365

DIV工程师
首页
  • Git
  • 网络
  • 操作系统
  • 浏览器
  • webpack
  • JavaScript
  • TypeScript
  • 性能
  • 工程化
  • React
  • 编程题
  • React技术揭秘
  • 算法
  • Node
  • 编码解码
  • NodeJS系列
  • Linux系列
  • JavaScript系列
  • HTTP系列
  • GIT系列
  • ES6系列
  • 设计模式系列
  • CSS系列
  • 小程序系列
  • 数据结构与算法系列
  • React系列
  • Vue3系列
  • Vue系列
  • TypeScript系列
  • Webpack系列
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 浏览器

  • webpack

  • TypeScript

  • 性能

  • 工程化

    • webpack-loader机制
    • webpack-插件机制
    • webpack工作流程
    • Babel原理
    • uglify-js代码是如何被压缩的
    • Tree-Shaking性能优化
    • 前端模块化机制
      • 模块化的理解
        • 模块的定义
        • 模块化的进化过程
        • 模块化的好处
        • 引入多个<script>后出现问题
      • 模块化规范
        • CommonJS
        • AMD
        • CMD
        • ES6 模块化
        • 总结
    • 前端微服务
    • Vite为什么快?快在哪?
    • npm script了解多少?
  • React

  • JavaScript

  • 编程题

  • React技术揭秘

  • 算法

  • 前端
  • 工程化
chst365
2021-04-15
目录

前端模块化机制

# 模块化的理解

# 模块的定义

  • 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件),并进行组合在一起
  • 块的内部数据与实现是私有的,只是向外部暴露一些接口(方法)与外部其他模块通信

# 模块化的进化过程

  • 全局function模式:将不同的功能封装成不同的全局函数

    • 编码:将不同的功能封装成不同的全局函数

    • 问题:污染全局命名空间,容易引起命名冲突或数据不安全,而且模块成员间看不出直接关系

      function m1() {
        // ...
      }
      function m2() {
        // ...
      }
      
      1
      2
      3
      4
      5
      6
  • namespace 模式:简单对象封装

    • 作用:减少了全局变量,解决命名冲突

    • 问题:数据不安全(外部可以直接修改模块内部的数据)

      let myModule = {
          data: 'www.baidu.com',
          foo() {
              console.log(`foo() ${this.data}`)
          }
          bar() {
              console.log(`bar() ${this.data}`)
          }
      }
      myModule.data = 'other data' // 能直接修改模块内部的数据
      myModule.foo()  // foo() other data
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
  • IIFE 模式:匿名函数自调用(闭包)

    • 作用:数据是私有的,外部只能通过暴露的方法操作

    • 编码:将数据和行为封装到一个函数内部,通过给 window 添加属性来向外暴露接口

    • 问题: 如果当前这个模块依赖另一模块怎么办

      // module.js文件
      (function (window) {
        let data = "www.baidu.com";
        //操作数据的函数
        function foo() {
          //用于暴露有函数
          console.log(`foo() ${data}`);
        }
        function bar() {
          //用于暴露有函数
          console.log(`bar() ${data}`);
          otherFun(); //内部调用
        }
        function otherFun() {
          //内部私有的函数
          console.log("otherFun()");
        }
        //暴露行为
        window.myModule = { foo, bar }; //ES6写法
      })(window);
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      <!-- index.html文件 -->
      <script type="text/javascript" src="module.js"></script>
      <script type="text/javascript">
        myModule.foo();
        myModule.bar();
        console.log(myModule.data); // undefined 不能访问模块内部数据
        myModule.data = "xxx"; // 不能修改模块内部的data
        myModule.foo(); // 没有改变
      </script>
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
  • IIFE 模式增强:引入依赖 这就是现代模块实现的基石,保证模块的独立性,还使得模块间的依赖关系变得明显

    <!-- index.html文件 -->
    <!-- 引入的js必须有一定顺序 -->
    <script type="text/javascript" src="jquery-1.10.1.js"></script>
    <script type="text/javascript" src="module.js"></script>
    <script type="text/javascript">
      myModule.foo();
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    // module.js文件
    (function (window, $) {
      let data = "www.baidu.com";
      //操作数据的函数
      function foo() {
        //用于暴露有函数
        console.log(`foo() ${data}`);
        $("body").css("background", "red");
      }
      function bar() {
        //用于暴露有函数
        console.log(`bar() ${data}`);
        otherFun(); //内部调用
      }
      function otherFun() {
        //内部私有的函数
        console.log("otherFun()");
      }
      //暴露行为
      window.myModule = { foo, bar };
    })(window, jQuery);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

# 模块化的好处

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离,按需加载
  • 更高复用性
  • 高可维护性

# 引入多个<script>后出现问题

  • 请求过多
  • 依赖模糊(依赖关系导致加载先后顺序出错)
  • 难以维护

# 模块化规范

# CommonJS

# 概述

Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里定义的变量、函数、类都是私有的,对其他文件不可见。在服务端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理

# 特点

  • 所有代码都运行在模块作用域,不会污染全局作用域
  • 模块看多次加载,但只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,需清除缓存
  • 模块加载的顺序,按照其在代码中出现的顺序

# 基本语法

  • 暴露模块:module.exports = value或exports.xxx = value
  • 引入模块:require(xxx),如果是第三方模块,xxx 是模块名;如果为自定义模块,xxx 为模块文件路径

CommonJS 暴露的模块到底是什么? CommonJS 规范规定,每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(module.exports)是对外的接口。加载某个模块,其实是加载该模块的 module.exports 属性

// example.js
var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
1
2
3
4
5
6
7
var example = require("./example.js"); //如果参数字符串以“./”开头,则表示加载的是一个位于相对路径
console.log(example.x); // 5
console.log(example.addX(1)); // 6
1
2
3

require命令

  • 读入并执行一个 js 文件,返回该模块的 exports 对象
  • 如果没发现指定模块,会报错

# 模块的加载机制

CommonJS 模块的加载机制,输入的是被输出的值的拷贝。一旦输出一个值,模块内部的变化就影响不到这个值 这点与 ES6 模块化有重大差异

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};
1
2
3
4
5
6
7
8
9
// main.js
var counter = require("./lib").counter;
var incCounter = require("./lib").incCounter;

console.log(counter); // 3
incCounter();
console.log(counter); // 3
1
2
3
4
5
6
7

上面代码说明,counter 输出后,lib.js 模块内部的变化影响不到 counter。这是因为 counter 是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值

# 服务端实现

  • 下载安装 node.js

  • 创建项目结构

    <!-- 注意:用npm init 自动生成package.json时,package name(包名)不能有中文和大写 -->
    |-modules
      |-module1.js
      |-module2.js
      |-module3.js
    |-app.js
    |-package.json
      {
          "name": "commonJS-node",
          "version": "1.0.0"
      }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  • 下载第三方模块 npm install uniq --save // 用于数组去重

  • 定义模块代码

    //module1.js
    module.exports = {
      msg: "module1",
      foo() {
        console.log(this.msg);
      },
    };
    
    1
    2
    3
    4
    5
    6
    7
    //module2.js
    module.exports = function () {
      console.log("module2");
    };
    
    1
    2
    3
    4
    //module3.js
    exports.foo = function () {
      console.log("foo() module3");
    };
    exports.arr = [1, 2, 3, 3, 2];
    
    1
    2
    3
    4
    5
    // app.js文件
    // 引入第三方库,应该放置在最前面
    let uniq = require("uniq");
    let module1 = require("./modules/module1");
    let module2 = require("./modules/module2");
    let module3 = require("./modules/module3");
    module1.foo(); //module1
    module2(); //module2
    module3.foo(); //foo() module3
    console.log(uniq(module3.arr)); //[ 1, 2, 3 ]
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • 通过 node 运行 app.js 命令行输入node app.js,运行 JS 文件

# 浏览器端实现(借助 Browserify)

  • 创建项目结构

    |-js
    |-dist //打包生成文件的目录
    |-src //源码所在的目录
        |-module1.js
        |-module2.js
        |-module3.js
        |-app.js //应用主源文件
    |-index.html //运行于浏览器上
    |-package.json
      {
          "name": "browserify-test",
          "version": "1.0.0"
      }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  • 下载 browserify

    • 全局:npm install browserify -g
    • 局部:npm install browserify --save-dev
  • 定义模块代码(同服务端) 注意:index.html文件要运行在浏览器上,需借助 browserify 将app.js文件打包编译,如果直接在index.html引入app.js会报错

  • 打包处理 js 根目录下运行browserify js/src/app.js -o js/dist/bundle.js

  • 页面使用引入 在index.html文件中引入<script type="text/javascript" src="js/dist/bundle.js"></script>

# AMD

CommonJS 规范加载模块是同步的,即只有加载完成,才能执行后面的操作。 AMD 规范是非同步加载模块,允许指定回调函数 Node.js 主要用于服务器编程,模块文件一般都已存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,用 CommonJS 比较适用 如果是浏览器环境,要从服务器端加载模块,必须采用非同步模式,因此浏览器端一般采用 AMD AMD 比 CommonJS 规范在浏览器端实现的早

# 基本语法

  • 定义暴露模块

    //定义没有依赖的模块
    define(function () {
      return 模块;
    });
    
    1
    2
    3
    4
    //定义有依赖的模块
    define(["module1", "module2"], function (m1, m2) {
      return 模块;
    });
    
    1
    2
    3
    4
  • 引入使用模块

    require(["module1", "module2"], function (m1, m2) {
      使用m1 / m2;
    });
    
    1
    2
    3

# 未使用 AMD 规范与使用require.js

  • 未使用 AMD 规范
    • 缺点:会发送多个请求,引入 js 文件的顺序不能错,否则会报错
    // dataService.js文件
    (function (window) {
      let msg = "www.baidu.com";
      function getMsg() {
        return msg.toUpperCase();
      }
      window.dataService = { getMsg };
    })(window);
    
    1
    2
    3
    4
    5
    6
    7
    8
    // alerter.js文件
    (function (window, dataService) {
      let name = "Tom";
      function showMsg() {
        alert(dataService.getMsg() + ", " + name);
      }
      window.alerter = { showMsg };
    })(window, dataService);
    
    1
    2
    3
    4
    5
    6
    7
    8
    // main.js文件
    (function (alerter) {
      alerter.showMsg();
    })(alerter);
    
    1
    2
    3
    4
    // index.html文件
    <div><h1>Modular Demo 1: 未使用AMD(require.js)</h1></div>
    <script type="text/javascript" src="js/modules/dataService.js"></script>
    <script type="text/javascript" src="js/modules/alerter.js"></script>
    <script type="text/javascript" src="js/main.js"></script>
    
    1
    2
    3
    4
    5
  • 使用require.js
  • Require.js 是一个工具库,主要用于客户端的模块管理
  • 模块管理遵循 AMD 规范
  • 通过 define 方法,将代码定义为模块;通过 require 方法,实现代码的模块加载

# AMD 在浏览器实现

  • 下载 require.js,并引入

  • 官网 (opens new window)

  • 将require.js导入项目:js/libs/require.js

  • 创建项目结构

    |-js
        |-libs
            |-require.js
        |-modules
            |-alerter.js
            |-dataService.js
        |-main.js
    |-index.html
    
    1
    2
    3
    4
    5
    6
    7
    8
  • 定义require.js的模块代码

    // dataService.js文件
    // 定义没有依赖的模块
    define(function () {
      let msg = "www.baidu.com";
      function getMsg() {
        return msg.toUpperCase();
      }
      return { getMsg }; // 暴露模块
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    //alerter.js文件
    // 定义有依赖的模块
    define(["dataService"], function (dataService) {
      let name = "Tom";
      function showMsg() {
        alert(dataService.getMsg() + ", " + name);
      }
      // 暴露模块
      return { showMsg };
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // main.js文件
    (function () {
      require.config({
        baseUrl: "js/", //基本路径 出发点在根目录下
        paths: {
          //映射: 模块标识名: 路径
          alerter: "./modules/alerter", //此处不能写成alerter.js,会报错
          dataService: "./modules/dataService",
        },
      });
      require(["alerter"], function (alerter) {
        alerter.showMsg();
      });
    })();
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // index.html文件
    <!DOCTYPE html>
    <html>
      <head>
        <title>Modular Demo</title>
      </head>
      <body>
        <!-- 引入require.js并指定js主文件的入口 -->
        <script data-main="js/main" src="js/libs/require.js"></script>
      </body>
    </html>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    • 页面引入require.js模块 在index.html引入<script data-main="js/main" src="js/libs/require.js"></script>
      // alerter.js文件
      define(["dataService", "jquery"], function (dataService, $) {
        let name = "Tom";
        function showMsg() {
          alert(dataService.getMsg() + ", " + name);
        }
        $("body").css("background", "green");
        // 暴露模块
        return { showMsg };
      });
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // main.js文件
      (function () {
        require.config({
          baseUrl: "js/", //基本路径 出发点在根目录下
          paths: {
            //自定义模块
            alerter: "./modules/alerter", //此处不能写成alerter.js,会报错
            dataService: "./modules/dataService",
            // 第三方库模块
            jquery: "./libs/jquery-1.10.1", //注意:写成jQuery会报错
          },
        });
        require(["alerter"], function (alerter) {
          alerter.showMsg();
        });
      })();
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16

# CMD

CMD 专门用于浏览器,模块的加载是异步的,模块使用时才会执行。 CMD 整合了 CommonJs 和 AMD 的特点 在 Sea.js 中,所有 js 模块都遵循 CMD 模块定义规范

# 基本语法

  • 定义暴露模块

    //定义没有依赖的模块
    define(function (require, exports, module) {
      exports.xxx = value;
      module.exports = value;
    });
    
    1
    2
    3
    4
    5
    //定义有依赖的模块
    define(function (require, exports, module) {
      //引入依赖模块(同步)
      var module2 = require("./module2");
      //引入依赖模块(异步)
      require.async("./module3", function (m3) {});
      //暴露模块
      exports.xxx = value;
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  • 引入使用模块

    define(function (require) {
      var m1 = require("./module1");
      var m4 = require("./module4");
      m1.show();
      m4.show();
    });
    
    1
    2
    3
    4
    5
    6

# sea.js 简单使用教程

  • 下载 sea.js,并引入

    • 官网 (opens new window)
    • 将 sea.js 导入项目:js/libs/sea.js
  • 创建项目结构

    |-js
      |-libs
          |-sea.js
      |-modules
          |-module1.js
          |-module2.js
          |-module3.js
          |-module4.js
          |-main.js
    |-index.html
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • 定义 sea.js 的模块代码

    // module1.js文件
    define(function (require, exports, module) {
      //内部变量数据
      var data = "atguigu.com";
      //内部函数
      function show() {
        console.log("module1 show() " + data);
      }
      //向外暴露
      exports.show = show;
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // module2.js文件
    define(function (require, exports, module) {
      module.exports = {
        msg: "I Will Back",
      };
    });
    
    1
    2
    3
    4
    5
    6
    // module3.js文件
    define(function (require, exports, module) {
      const API_KEY = "abc123";
      exports.API_KEY = API_KEY;
    });
    
    1
    2
    3
    4
    5
    // module4.js文件
    define(function (require, exports, module) {
      //引入依赖模块(同步)
      var module2 = require("./module2");
      function show() {
        console.log("module4 show() " + module2.msg);
      }
      exports.show = show;
      //引入依赖模块(异步)
      require.async("./module3", function (m3) {
        console.log("异步引入依赖模块3  " + m3.API_KEY);
      });
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // main.js文件
    define(function (require) {
      var m1 = require("./module1");
      var m4 = require("./module4");
      m1.show();
      m4.show();
    });
    
    1
    2
    3
    4
    5
    6
    7
  • 在 index.html 引入

    <script type="text/javascript" src="js/libs/sea.js"></script>
    <script type="text/javascript">
      seajs.use("./js/modules/main");
    </script>
    
    1
    2
    3
    4

    结果如下

# ES6 模块化

ES6 模块的设计思想是尽量的静态化,使得编译时就确定模块的依赖关系,已经输入和输出的变量 CommonJS 和 AMD 都只能在运行时确定这些东西,如:CommonJS 模块就是对象,输入时必须查找对象属性

# ES6 模块化语法

export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能

/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
  return a + b;
};
export { basicNum, add };
/** 引用模块 **/
import { basicNum, add } from "./math";
function test(ele) {
  ele.textContent = add(99 + basicNum);
}
1
2
3
4
5
6
7
8
9
10
11
// export-default.js
export default function () {
  console.log("foo");
}
1
2
3
4
// import-default.js
import customName from "./export-default";
customName(); // 'foo'
1
2
3

模块默认输出,其他模块加载模块时,import命令可为该匿名函数指定任意名字

# ES6 模块与 CommonJS 模块的差异

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用

  • CommonJS 模块时运行时加载,ES6 模块时编译时输出接口

  • CommonJS 加载的是一个对象(module.exports 属性),该对象只有在脚本运行完才能生成;ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成

  • ES6 模块时动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块

    // lib.js
    export let counter = 3;
    export function incCounter() {
      counter++;
    }
    // main.js
    import { counter, incCounter } from "./lib";
    console.log(counter); // 3
    incCounter();
    console.log(counter); // 4
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

# ES6-Babel-Browserify

使用 Babel 将 ES6 编译为 ES5 代码,使用 Browserify 编译打包 js

  • 定义 package.json文件

    {
    "name" : "es6-babel-browserify",
    "version" : "1.0.0"
    }
    
    1
    2
    3
    4
  • 安装babel-cli, babel-preset-es2015和browserify

    • npm install babel-cli browserify -g
    • npm install babel-preset-es2015 --save-dev
    • preset 预设(将 es6 转换成 es5 的所有插件打包)
  • 定义.babelrc文件

    {
      "presets": ["es2015"]
    }
    
    1
    2
    3
  • 定义模块代码

    //module1.js文件
    // 分别暴露
    export function foo() {
      console.log("foo() module1");
    }
    export function bar() {
      console.log("bar() module1");
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    //module2.js文件
    // 统一暴露
    function fun1() {
      console.log("fun1() module2");
    }
    function fun2() {
      console.log("fun2() module2");
    }
    export { fun1, fun2 };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    //module3.js文件
    // 默认暴露 可以暴露任意数据类项,暴露什么数据,接收到就是什么数据
    export default () => {
      console.log("默认暴露");
    };
    
    1
    2
    3
    4
    5
    // app.js文件
    import { foo, bar } from "./module1";
    import { fun1, fun2 } from "./module2";
    import module3 from "./module3";
    foo();
    bar();
    fun1();
    fun2();
    module3();
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    //app.js文件
    import { foo, bar } from "./module1";
    import { fun1, fun2 } from "./module2";
    import module3 from "./module3";
    import $ from "jquery";
    
    foo();
    bar();
    fun1();
    fun2();
    module3();
    $("body").css("background", "green");
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  • 编译并在index.html中引入

    • 使用 Babel 将 ES6 编译为 ES5 代码(但包含 CommonJS 语法) : babel js/src -d js/lib
    • 使用 Browserify 编译 js : browserify js/lib/app.js -o js/lib/bundle.js
    • <script type="text/javascript" src="js/lib/bundle.js"></script>

# 总结

  • CommonJS 规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了 AMD CMD 解决方案。
  • AMD 规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD 规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。
  • CMD 规范与 AMD 规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在 Node.js 中运行。不过,依赖 SPM 打包,模块的加载逻辑偏重
  • ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
#前端#工程化
上次更新: 2021/12/14, 23:27:23
Tree-Shaking性能优化
前端微服务

← Tree-Shaking性能优化 前端微服务→

最近更新
01
面试官
03-27
02
this&指针&作用域&闭包
03-27
03
前端
03-27
更多文章>
Theme by Vdoing | Copyright © 2019-2025 chst365 | 豫ICP备17031889号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式