前端工程化之模块化

前端模块化

什么是模块化

模块化是一种编程技术,它将一个复杂的应用程序拆解成一个一个模块,模块化具有以下特点:

  • 每个模块有自己的作用域,模块内部的变量和函数在模块外部是不可见的。
  • 模块自己的接口只暴露必要的接口,外部只能通过接口与其他模块进行交互。

模块化的发展

1. 全局Function模式

javascript 复制代码
// 模块一
function module1() {
  // ...
}

// 模块二
function module2() {
  // ...
}
  • 特点:将不同的功能模块封装到一个个全局函数中,通过全局函数的调用来实现模块间的通信。
  • 问题:污染全局命名空间,并且随着项目越来越大会出现命名冲突的问题。

2.简单对象封装模式

javascript 复制代码
// 模块一
let module = {
  data: 'foo',
  foo(){
    // ...
  }
  bar(){
    // ...
  }
};


module.data = 'bar';
module.foo();
  • 特点:将模块封装成一个个简单对象,通过对象属性的访问来实现模块间的通信。
  • 问题:内部属性可以直接修改,数据不安全。

3.IIFE模式(闭包 + 立即执行函数)

普通模块:

javascript 复制代码
// 创建模块
(function (window) {
  // 内部私有数据
  let data = 'foo';
  function foo() {
    console.log(data);
  }
  function bar() {
    privateFunc();
  }

  function privateFunc() {
    console.log('bar');
  }
  // 向外暴露接口
  window.module = {
    foo,
    bar,
  };
})(window);
html 复制代码
<!--  引入模块 -->
<script src="module.js"></script>
<script>
  module.foo(); // 'foo'
  module.bar(); // 'bar'
  console.log(module.data); // undefined
  console.log(module.privateFunc); // undefined
</script>

引入其他依赖:

javascript 复制代码
// 引入JQuery模块
(function (window, $) {
  function foo() {
    $('body').css('background', 'red');
  }

  // 向外暴露接口
  window.module = {
    foo,
  };
})(window, JQuery);
html 复制代码
<!--  必须先引入JQuery -->
<script src="jquery.js"></script>
<script src="module.js"></script>
<script>
  module.foo();
</script>
  • 特点:将模块的内部数据和方法封装到一个立即执行的函数中,通过参数传入外部环境和依赖,利用闭包的特性,通过暴露内部接口,来访问函数内部私有数据,并且内部私有数据不可更改。
  • 问题:模块引入顺序有要求,在不清楚模块之间依赖关系的时候,可能会出现模块引入顺序错误的情况。

上述模块化方式除以上问题外,还存在很多问题,比如在引入多个 script 标签时,会发送很多请求,模块之间的依赖比较模糊,模块难以维护等等,因此,针对以上问题,社区和官方提出了很多模块化规范。

模块化规范

1.CommonJs

CommonJsnode环境中的一种模块规范。每个文件就是一个模块,有自己的作用域,文件内部的变量和函数都是私有的,需要向外暴露才可见。 注意:CommonJs的模块加载机制是同步的,要想在浏览器端运行,需要进行打包编译。

特点:

  • 所有的代码都运行在模块的作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,以后再加载就直接返回第一次运行的结果,不会再次运行。
  • 模块按照代码出现顺序依次加载。

基本语法:

javascript 复制代码
// module1.js
module.exports = {
  data: 'foo',
  bar() {
    console.log('bar');
  },
};

// module2.js
const module1 = require('./module1');
console.log(module1.data); // 'foo'
module1.bar(); // 'bar'

这里有两点需要注意:

  • CommonJs 可以通过 module.exportsexports两种方式进行模块导出,本质上 module.exportsexports 以及 this 都是指向同一个对象。而最终导出的对象是 module.exports 指向的对象 ,所以通过给 module.exports 或者 exports 添加属性,导出的对象上都会被添加上相应的属性,而给 module.exports 重新赋值,则会覆盖原有导出的对象。

  • CommonJs 通过 require函数来加载模块,它是同步加载,也就是说,只有当前模块的依赖加载完成,才能执行当前模块的脚本 。此外,require 可以接受一个参数,如果参数是一个相对或者绝对路径,则会从这个路径加载模块,否则,则会从 node_modules 目录下加载,如果没有发现指定模块,则会抛出错误。

模块加载机制:

CommonJs模块导入的值是可以理解成导出的值深拷贝后的值,因此一旦一个值被导入之后,那么这个值就不会被修改了,即使在原模块对其进行了修改,也不会影响到导入的值。下面是具体的例子:

javascript 复制代码
// module1.js
let a = 1;

setTimeout(() => {
  a = 2;
}, 1000);

module.exports = { a };

// module2.js
const module1 = require('./module1');
console.log(module1.a); // 1

setTimeout(() => {
  console.log(module1.a); // 1
}, 2000);

module2.js 中,我们通过 require 加载 module1.js,然后打印 module1.a 的值,此时 module1.a 的值为 1,然后我们在 module1.js 中设置了一个定时器,在 1 秒后将 a 的值改为 2,然后我们 1.5 秒后再次打印 module1.a 的值,此时 module1.a 的值仍为 1,说明 module1.a 的值是不会被修改的。

2.AMD

CommonJs不同的是,AMD 是非同步加载模块,允许指定回调函数。因此 CommonJs适用于服务端加载模块,而 AMD 适用于浏览器端。

AMD模块需要使用 require.js这个库,通过 define 函数来定义模块,define 函数接受两个参数,第一个参数是模块的依赖列表(如果没有依赖的模块可不传),第二个参数是模块的执行函数。通过 require 函数来加载模块,require 函数接受一个数组参数,数组的每个元素是一个依赖模块的名字。通过 require.js加载的模块必须符合 AMD 规范,如果不符合,需要先用require.config()方法,定义它们的一些特征 。这里不过多阐述。

定义模块:

js 复制代码
// module1.js
// 定义没有依赖的模块
define(function () {
  let bar = 1;
  function foo() {
    console.log(bar);
  }

  return {
    foo,
  };
});

// module2.js
// 定义有依赖的模块
define(['jquery'], function ($) {
  function bar() {
    $('body').css('background', 'green');
  }
  return {
    bar,
  };
});

加载模块:

js 复制代码
// 加载 module1.js
require(['./module1'], function (module1) {
  module1.foo(); // 1
});

引入 require.js 和 模块, 并制定 data-main 属性值作为入口文件:

html 复制代码
<!-- 引入 require.js -->
<script
  data-main="./index.js"
  src="https://requirejs.org/docs/release/2.3.7/minified/require.js"
></script>
<script src="./module1.js"></script>

3.CMD

CMDSea.js 的模块定义规范。它整合了CommonJsAMD的特点, 并且和 AMD 类似,也是为了解决浏览器端模块化问题而提出的,CMD 的模块加载是异步的,在模块使用时才会加载执行。

基本语法:

javascript 复制代码
// 定义模块
define(function (require, exports, module) {
  module.exports.bar = function () {
    console.log('bar');
  };
});

// 加载模块
define(function (require) {
  let m1 = require('./module1');
  m1.bar(); // 'bar'
});

使用 sea.js同时指定入口文件。

html 复制代码
<script src="https://cdn.bootcdn.net/ajax/libs/seajs/3.1.1/sea.js"></script>
<script>
  seajs.use('./index.js');
</script>

ESModule

ESModuleECMAScript 的一个提案,旨在解决模块化问题。 CommonJsAMD 模块都是在运行时确定模块关系,而 ESModule模块化的思想是在编译时就确定莫亏啊的依赖关系。

HTML 文件中的基本使用:

html 复制代码
<!-- 通过给 `<script>` 标签添加 `type="module"` 属性,就可以启用 `ESModule` 模块: -->
<script type="module">
  console.log('这是一个模块');
</script>

特性:

  • ESM 自动采用严格模式 相当于开启了 use strict
  • 每个 ESModule 都有自己的单独作用域
  • ESModule 通过 CORS 的方式请求外部 JS 脚本
  • ESModulescript 标签会延迟执行脚本 相当于 defer
html 复制代码
<!-- 1. ESM自动采用严格模式,相当于开启了 `use strict` -->
<script type="module">
  console.log(this); // undefined 严格模式下,全局的this为undefined
</script>

<!-- 2. 每个ESModule 都有自己的单独作用域 -->
<script type="module">
  let x = 1;
  console.log(x); // 1
</script>
<script type="module">
  console.log(x); // undefined 不同模块的作用域互不干扰
</script>

<!-- 3. ESModule 通过 CORS 的方式请求外部 JS 脚本 -->
<script type="module" src="不支持跨域访问的js脚本"></script>
// 报跨域错误
<script type="module" crossorigin src="支持跨域访问的js脚本"></script>
// 允许跨域访问

<!-- 4. ESModule 的 script 标签会延迟执行脚本 -->
<script type="module" src="demo1.js"></script>
// 脚本的执行不会阻塞后续的渲染,相当于 defer 属性
<p>需要显示的内容</p>

导入导出语法:

ESModule 使用 importexport 关键字来导入和导出模块。

javascript 复制代码
// module1.js
let foo = 1;
export { foo }; // 导出

// module2.js
import { foo } from './module1'; // 导入
console.log(foo); // 1

导入导出可以通过 as 进行重命名:

javascript 复制代码
// module1.js
let foo = 1;
let bar = 2;
export { foo as myFoo, bar }; // 重命名导出

// module2.js
import { myFoo, bar as myBar } from './module1'; // 导入重命名
console.log(myFoo); // 1
console.log(myBar); // 2

默认导出:

默认导出有两种方式:

  1. 使用 export default 关键字导出默认值
  2. 将变量重命名为default: export { foo as default } 针对这种方式,在导入是需要将 default 重命名为其他名字。
javascript 复制代码
// 方式1
// module1.js
let foo = 1;
export { foo as default };
// module2.js
import {default as foo} from './module1'

// -----------------

// 方式2
// module1.js
let foo = 1;
export default foo;

// module2.js
import foo from './module1';
console.log(foo); // 1

混合导出:

ESModule 可以同时默认导出和命名导出。

javascript 复制代码
// module1.js
let foo = 1;
let bar = 2;
export { foo, bar };
let a = 2;
export default a;

// module2.js
// 两种导入都可以
// import { foo, bar, default as a } from './module1';
import a, { foo, bar } from './module1';
console.log(foo); // 1
console.log(bar); // 2
console.log(a); // 2

需要注意的是, ESModuleCommonJs 不同, ESModule 导入的值并不是拷贝关系,而是值的引用。 这里用个例子演示一下:

javascript 复制代码
// module1.js
let a = 1;
setTimeout(() => {
  a = 2;
}, 1000);
export { a };

// module2.js
import { a } from './module1';
console.log(a); // 1

setTimeout(() => {
  console.log(a); // 2
}, 1500);

module2.js 中,我们通过 import 加载 module1.js,然后打印 module1.a 的值,此时 module1.a 的值为 1,然后我们在 module1.js 中设置了一个定时器,在 1 秒后将 a 的值改为 2,然后我们 1.5 秒后再次打印 module1.a 的值,此时 module1.a 的值变为 2,说明 module1.a 的值是会被修改的, 因为 ESModule 导入的值是值的引用。 因此,强烈建议在导出变量时,要用 const 声明,避免出现意外的修改

动态导入:

ESModule 提供了 import() 函数来动态导入模块。

javascript 复制代码
// module1.js
let a = 1;

export { a };

// module2.js
import('./module1').then((m) => {
  console.log(m.a); // 2
});
相关推荐
思考的Joey几秒前
Docker入门:手把手教你前端容器化部署全流程
前端·docker·devops
gqkmiss9 分钟前
Chrome 浏览器 134 版本新特性
前端·chrome·浏览器·chrome 浏览器
Mswanga14 分钟前
探秘 CSS 盒子模型:构建网页布局的基石
前端·css
I will.87416 分钟前
如何使用 CSS 实现黑色遮罩效果
前端·javascript·css
守城小轩33 分钟前
Chrome 扩展开发 API实战:Bookmarks(二)
前端·javascript·chrome
gqkmiss37 分钟前
Chrome 浏览器 133 版本新特性
前端·chrome·浏览器·chrome 浏览器
A阳俊yi1 小时前
SpringMVC中有关请求参数的问题(映射路径,传递不同的参数)
java·前端·javascript
鱼樱前端1 小时前
Vue3 + TypeScript + Better-Scroll 极简上拉下拉组件
前端·javascript·vue.js
Trae首席推荐官1 小时前
Trae 功能上新:支持 Remote-SSH 和自定义模型配置
前端·后端·trae