查看原文
其他

前端模块化 | 冰岩分享

🍜 冰岩作坊 2022-06-10

前端模块化

模块化

为什么要模块化

  • 全局变量
  • 依赖关系管理

不优雅地引入资源

<script type="text/javascript" src="a.js"></script>
<script type="text/javascript" src="b.js"></script>
<script type="text/javascript" src="c.js"></script>

在过去,我们通过从上到下引入<script>标签的方法来引入想要的资源。然而当项目越做越大时,有两个主要的问题也随之出现。一是每个资源中的变量和方法都会成为全局的变量,污染了全局作用域且不容易维护。二来资源加载之间的依赖关系通过引入资源的顺序决定。一旦项目稍大,资源引入的逻辑就会非常复杂。

模块化解决什么问题

  • 安全的包装一个模块的代码,避免全局污染
  • 唯一标识一个模块
  • 优雅的将模块api暴露出去
  • 方便的使用模块

AMD规范

当我们在服务端加载一个模块时,直接就从硬盘或者内存中读取,消耗时间可以忽略不计

而由于浏览器端需要从服务端下载模块文件,模块不能采用同步加载,因此有了AMD规范

var math = require('math');
math.add(23);

Asynchronous Module Definition 异步模块加载机制

RequireJS在推广过程中对模块定义的规范化产出。

AMD制定了定义模块的规则,这样模块和模块的依赖可以被异步加载。这和浏览器的异步加载模块的环境刚好适应(浏览器同步加载模块会导致性能、可用性、调试和跨域访问等问题)。

模块定义 define

api

AMD规范简单到只有一个API,即define函数

define(id?, dependencies?, factory);

id
  • 指定义中模块的名字

  • 可选参数,默认为模块加载器请求的指定脚本的名字

dependencies
  • 定义模块所依赖模块的数组

  • 可选参数,默认为["require", "exports", "module"]

factory

模块初始化要执行的函数或对象

  • 如果为函数,它应该只被执行一次。
  • 如果是对象,此对象应该为模块的输出值。

独立模块定义

// 独立模块定义
define({
  method1function() {}
  method2: function() {}
});  

// 或者
define(function(){
  return {
    method1function() {},
    method2function() {},
  }
});

非独立模块定义

define(['math''graph'], function(math, graph){
  ...
});

模块引入 require

require([module], callback);

AMD采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:

  • [module],一个需要加载的模块的数组
  • callback,加载成功后的回调函数
require(['math'], function (math) {
  math.add(23);
});

「AMD主要解决了两个问题」

  • 浏览器响应时间

    require()函数在加载依赖的函数的时候是异步加载的,这样浏览器不会失去响应

  • 依赖关系

    指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。

CMD规范

Common Module Definition 通用模块规范

CMD是SeaJS在推广过程中对模块定义的规范化产出。

SeaJS是一个适用于 Web 浏览器端的模块加载器。使用SeaJS,可以更好地组织JavaScript代码。

推崇「依赖就近」的原则

模块定义 define

define(factory)

  • define 是一个全局函数,用来定义模块。

  • factory可以是一个函数,也可以是一个对象或字符串。

定义模块

define(function(require, exports, module){
  //依赖模块a
  var a = require('./a');
  //调用模块a的方法
  a.method();
})

模块引入 require

接受模块标识作为唯一参数,用来获取其他模块提供的接口。

require(id)

require.async

require.async(id, callback?)

模块内部异步加载模块,并在加载完成后执行指定回调

define(function(require, exports, module) {

  // 异步加载一个模块,在加载完成时,执行回调
  require.async('./b'function(b) {
    b.doSomething();
  });

  // 异步加载多个模块,在加载完成时,执行回调
  require.async(['./c''./d'], function(c, d) {
    c.doSomething();
    d.doSomething();
  });
});

模块接口 exports

exports是一个对象,是module.exports对象的引用,用来向外提供模块接口。

define(function(require, exports) {
  // 对外提供 foo 属性
  exports.foo = 'bar';
  // 对外提供 doSomething 方法
  exports.doSomething = function() {};
});

也可以通过return直接提供接口

define(function(require) {
  // 通过 return 直接提供接口
  return {
    foo'bar',
    doSomethingfunction() {}
  };
});

module

module 是一个对象,上面存储了与当前模块相关联的一些属性和方法。

  • module.id

    模块的唯一标识。

  • module.uri

    根据模块系统的路径解析规则得到的模块绝对路径。

  • module.dependencies

    一个数组,表示当前模块的依赖。

  • module.exports

    当前模块对外提供的接口。

比较AMD与CMD

AMD中文版Wiki

CMD规范

相同点

  • 都是为了 JavaScript 的模块化开发
  • 达成「浏览器端模块化开发的目的」

不同点

  • 「AMD:依赖前置」

    AMD在加载模块完成后就会执行该模块,所有模块都加载执行完后会进入require的回调函数,执行主逻辑。

    这样的效果就是依赖模块的执行顺序和书写顺序不一定一致,而是由网络速度决定。哪个模块先下载下来,哪个先执行,但是主逻辑一定在所有依赖加载完成后才执行

  • 「CMD:依赖就近」

    CMD加载完某个依赖模块后并不执行,只是下载下来,在所有依赖模块加载完成后进入主逻辑,遇到require语句的时候才执行对应的模块。

    这样模块的执行顺序和书写顺序是完全一致的

CommonJS

CommonJS defines a module format.

Node应用由模块组成,采用CommonJS模块规范

CommonJS定义了一种模块格式,是一种通用模块规范。

在CommonJS中,「每个文件就是一个模块」,拥有自己独立的作用域

module对象

module是一个变量,代表当前模块。

Node内部提供一个Module构建函数。所有模块都是Module的实例。

function Module(id, parent) {
}
image-20200704163729375
  • module.exports

    表示模块对外输出的值。

  • module.id

    模块的识别符,通常是带有「绝对路径」的模块文件名

  • module.fileName

    带有「绝对路径」的模块文件名

  • module.loaded

    返回一个布尔值,表示模块是否已经完成加载

  • module.parent

    • 如果是在命令行下调用某个模块(即运行node hello.js),该属性为null
    • 如果是在脚本中调用某个模块,(如require('./hello.js');),则返回一个Module对象,表示调用该模块的模块。
  • module.children

    返回一个数组,表示该模块要用到的其他模块。

Module {
  id: 'e:\\Programming\\JavaScript\\Node\\add.js',  
  path: 'e:\\Programming\\JavaScript\\Node',        
  exports: { add: [Function: add] },
  parent: Module {
 ...
  },
  filename: 'e:\\Programming\\JavaScript\\Node\\add.js',
  loaded: false,
  children: [],
  paths: [
    'e:\\Programming\\JavaScript\\Node\\node_modules',
    'e:\\Programming\\JavaScript\\node_modules',    
    'e:\\Programming\\node_modules',
    'e:\\node_modules'
  ]
}

module.exportsexports

"exports is assigned the value of module.exports before the module is evaluated."

  1. exportsmodule.exports的一个引用,exports指向的是module.exports

    var exports = module.exports
  2. mdoule.exports初始化为一个空对象

  3. require()返回的是**module.exports而不是exports**

知道以上三点后我们可以知道:

当给exports对象添加属性时,我们同时也操作了module.exports,是可行的;

exports.a = function() {};

当直接给exports对象赋值时,断开了exportsmodule.exports的连接,moudle.exports仍然是空对象,这时就无效了

exports = {
  a: a,
  b: b
}

因此建议,无论何时都使用module.exports的形式导出模块

require

读入并执行一个JavaScript文件,然后「返回该模块的module.exports对象」

如果没有发现指定模块,会报错。

缓存 Caching

  • 模块在第一次加载后会被缓存。

  • 当之后要再加载该模块时,就直接从缓存中取出该模块的module.exports属性

  require('./example.js');
  require('./example.js').message = "hello";
  require('./example.js').message
  // "hello"
  • 如果想要多次执行某一模块,可以让该模块输出一个函数。每次require该模块时,重新执行函数。

  • 所有缓存的模块保存在require.cache之中

核心模块 Core Modules

Node.js has several modules compiled into the binary.

核心模块指的是被编译进二进制文件里的模块

  • 位置:lib/
  • 相对于文件模块,核心模块会被require()优先加载

文件模块 File Modules

后缀名默认为.js

如果按确切的文件名没有找到模块,则Node.js会尝试带上.js.json.node拓展名再加载。

当没有以'/''./''../'开头来表示文件时,这个模块必须是一个核心模块或加载自 node_modules目录。

目录作为模块 Folders as Modules

通常,我们会把相关的文件会放在一个目录里面,便于组织。这时,最好为该目录设置一个入口文件,让require方法可以通过这个入口文件,加载整个目录。

在目录中放置一个package.json文件,并且将入口文件写入main字段。

// package.json

  "name" : "some-library",
  "main" : "./lib/some-library.js" 
}

require发现参数字符串指向一个目录以后,会自动查看该目录的package.json文件,然后加载main字段指定的入口文件。如果package.json文件没有main字段,或者根本就没有package.json文件,则会加载该目录下的index.js文件或index.node文件。

node_modules加载

如果传给require()的参数字符串既不是核心模块,也不是以'/''./''../'开头,那么Node会从当前模块所在的父目录开始加node_modules,并尝试从node_modules中加载模块

如果没有找到,那么将移动到父目录,依此类推,直到到达文件系统的根目录。

如:当我们在'e:\\Programming\\JavaScript\\Node\\add.js'模块中加载require('hello.js')

'e:\\Programming\\JavaScript\\Node\\node_modules',
'e:\\Programming\\JavaScript\\node_modules',    
'e:\\Programming\\node_modules',
'e:\\node_modules'

从全局文件夹加载

NODE_PATH

如果 NODE_PATH环境变量被设为一个绝对路径列表,则当在其他地方找不到模块时 Node会搜索这些路径。

其他全局目录
  • $HOME/.node_modules
  • $HOME/.node_libraries
  • $PREFIX/lib/node

其中$HOME是用户的主目录,$PREFIXNode里配置的node_prefix

加载顺序

  • 缓存

  • /, ./, ../开头,如require(./hello)

    • 如果有,则将main属性指定的入口文件当作模块;
    • 如果没有,则会将index作为文件名查找是否存在index.jsindex.jsonindex.node文件
    • 先找文件,如果没有后缀自动补充.js.json.node进行查找

      在这里会查找当前目录下是否存在hello.jshello.jsonhello.node模块

    • 再找目录

      在这里会查找当前目录下的hello目录,如果存在,会将该目录当作模块

      先看该目录下是否存在package.json

  • 不以/, ./, ../开头,如在C:/home目录下require('hello')

    • 先查找是否为核心模块

    • 再到当前目录下的node_modules下找文件

      在这里会先查找C:/home/node_modules下是否存在hello.jshello.jsonhello.node

    • 再到当前目录下的node_modules下找目录

      同上

    • 不存在则向父级目录进行查找 C:/node_moudles

    • 最后查找全局目录

ES6

ES6前,实现模块化使用的是RequireJS或者SeaJS

ES6引入了模块化

ES6的模块自动开启严格模式

export 导出模块

export命令可以出现在模块的任何位置,但必需处于模块顶层。

导出变量

export const name = 'Shaw';

导出函数

export function hello() {
  console.log('hello world');
}
// 或者
function hello() {
    console.log('hello world');
}
export hello;

导出多个变量/函数

const name = 'Shaw'
function hello() {
    console.log('Hello World');
}
export { name, hello };

默认导出

在一个文件或模块中,exportimport可以有多个,export default仅有一个。

function hello() {
  console.log('hello');
}
export default hello;

通过export方式导出,在导入时要加{}export default则不需要。

import 导入模块

引入export导出的模块

import { name } from './hello';

引入export default导出的模块

import name from './hello';

同时引入

export const name = 'Shaw'
export default hello() {
    console.log('Hello');
}

引入时,默认值必须放在非默认值前

import hello, { name } from './hello.js'

as 重命名

export { hello as hi };
import { hi as hello } from './hello';

结语

如今前端生态不断注入新生命。在前端发展如此迅速的今天,用模块化规范去管理前端代码也就变得格外重要。希望以上内容对正在学习前端的朋友们可以有所帮助。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存