前端模块化浅析

2,942 阅读6分钟

平时写代码的时候,知道如何导出变量,如何引入变量。可见模块化就在我们的身边,可是为什么前端会引入模块化的概念,以及为什么有同步加载和异步加载呢?

为什么要模块化

在之前的项目中,如果没有模块化的概念,很多变量都有重名或者不小心重新赋值的危险。而且用 script 有可能阻塞 HTML 的下载或者渲染,影响用户体验。

在平时编码中,我们都习惯把一些通用的方法提出来放在一个文件里,哪个地方需要用到就引用,这样能够很好的梳理页面的逻辑,维护代码的成本也降低了不少。所以模块化给我们带来的好处是显而易见的。

  • 分离: 代码需要分离成小块,以便能为人所理解。
  • 可组合性: 在一个文件中编码,被许多其他文件重复使用。这提升了代码库的灵活性。
  • 解决全局变量重名问题
  • 提高复用性

现有的一些模块化方案有以下几种:

  • ES 6 模块
  • Commonjs
  • AMD
  • CMD

下面我就自身的理解对这几种方案做一个对比和总结:

ES6 Module

  • 编译时就能确定模块的依赖关系

ES6 模块遇到 import 命令时,不会去执行模块,而是生成一个引用,等用到的时候,才去模块中取值。因为是动态引用,所以不存在缓存的问题。可以看一下下面的例子:

// util.js
export let env = 'qa';
setTimeout(() => env = 'local', 1000);

// main.js
import {env} from './util';
console.log('env:', env);

setTimeout(() => console.log('new env:', env), 1500);

执行 main.js,会输出下面的结果:

// env: qa
// new env: local

可以看出 ES6 模块是动态的取值,不会缓存运行的结果。

目前浏览器尚未支持 ES6 模块 ,所以需要使用 babel 转换,大家可以在 Babel 提供的 REPL 在线编译器 中查看编译后的结果。

// es 6
import {add} from './config';

// es 5
'use strict';
var _config = require('./config');

可以看出,最后转换成 require 的方式了。ES6 模块在浏览器和服务器端都可以用,ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。

import 命令具有提升效果,会提升到整个模块的头部,首先执行,所以不能把 import 写在表达式里面。这和 ES 6 模块的概念不符合。

CommonJS

  • 用于服务器端
  • 只能在运行时确定
  • 同步加载
  • 模块加载的顺序,按照其在代码中出现的顺序。

node 的模块遵循 CommonJS 规范。在服务器端,依赖是保存在本地硬盘的,所以读取的速度非常快,使用同步加载不会有什么影响。

看一下 CommonJS 的语法:

// header.js
module.exports = {
    title: '我是柚子'
};

// main.js
var header = require('./header');

module

这里的 module 代表的是当前模块,它是一个对象,把它打印出来是下面的结果:

{
Module {
  id: '/Users/yanmeng/2017FE/css-animation/js/b.js',
  exports: { item: 'item' },
  parent:
   Module {
     id: '.',
     exports: {},
     parent: null,
     filename: '/Users/yanmeng/2017FE/css-animation/js/main.js',
     loaded: false,
     children: [ [Circular] ],
     paths:
      [ '/Users/yanmeng/2017FE/css-animation/js/node_modules',
        '/Users/yanmeng/2017FE/css-animation/node_modules',
        '/Users/yanmeng/2017FE/node_modules',
        '/Users/yanmeng/node_modules',
        '/Users/node_modules',
        '/node_modules' ] },
  filename: '/Users/yanmeng/2017FE/css-animation/js/b.js',
  loaded: false,
  children: [],
  paths:
   [ '/Users/yanmeng/2017FE/css-animation/js/node_modules',
     '/Users/yanmeng/2017FE/css-animation/node_modules',
     '/Users/yanmeng/2017FE/node_modules',
     '/Users/yanmeng/node_modules',
     '/Users/node_modules',
     '/node_modules' 
    ] 
}
  • id 是该模块的 id
  • loaded 代表改模块是否加载完毕
  • exports 是一个对象,里面有模块输出的各个接口。

之后调用这个模块的时候,就会从 exports 中取值,即使再执行,也不会再执行改模块,而是从缓存中取值,返回的是第一次运行的结果,除非手动清除缓存。

// 删除指定模块的缓存
delete require.cache[moduleName];

// 删除所有模块的缓存
Object.keys(require.cache).forEach(function(key) {
  delete require.cache[key];
})

缓存是根据绝对路径识别模块的,如果同一个模块放在不同的路径下,还是会重新加载这个模块。

require

require 命令第一次执行的时候,会加载并执行整个脚本,然后在内存中生成此脚本返回的 exports 对象。

ES6 模块与 CommonJS 模块的差异

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

ES6 模块是动态引用,并且不会缓存值。

ES6 模块在对脚本静态分析的时候,遇到 import 就会生成一个只读引用,等到脚本真正执行的时候,再根据这个只读引用,到被加载的那个模块里取值,所以说 ES6 模块是动态引用。
从依赖中引入的模块变量是一个地址引用,是只读的,可以为它新增属性,可是不能重新赋值。

// lib.js
export let obj = {};

// main.js
import { obj } from './lib';

obj.prop = 123; // OK
obj = {}; // TypeError

AMD

又称异步加载模块(Asynchronous Module Definition)

  • 依赖前置
  • 比较适合浏览器环境
  • 实现 js 文件的异步加载,避免网页失去响应
  • 管理模块之间的依赖性,便于代码的编写和维护
  • 代表库: RequireJS

如果在浏览器环境,就需要在服务端加载模块,那么采用同步加载的方法就会影响用户体验,所以浏览器端一般采用 AMD 规范。

它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

 // lib.js
  define('[./util.js]', function(util){
      function bar() {
          util.log('it is sunshine');
      };
      return {
          bar: bar
      };
  });
  
// main.js
require(['./lib.js'], function(lib){
    console.log(lib.bar());
})

CMD

  • 依赖就近
  • 需要用到依赖的时候才申明
  • 代表库: Sea.js

Sea.js 实现了这个规范,Sea.js 遇到依赖后只会去下载 JS 文件,并不会执行,而是等到所有被依赖的 JS 脚本都下载完以后,才从头开始执行主逻辑。因此被依赖模块的执行顺序和书写顺序完全一致。

define(function(require, exports, module) {
    var a = require('./a')
    a.doSomething()
    // ...
    var b = require('./b') 
    b.doSomething()
    // ...
})

本文只是浅显的介绍了一些模块的概念和用法,关于 ES6 模块、 CommonJs 的循环加载和 ES 6 模块和 CommonJs的互相引用,大家可以动手实践一下,会受益匪浅。

参考: