PatrickLh/blog

Javascript学习笔记-模块化

Opened this issue · 0 comments

Javascript模块化.png

1. 简介

前端发展初期或者Javascript发展初期,我们所要解决的问题只是一个页面上各种内容的排版,表单验证之类的简单交互逻辑处理,这个时候单个js文件就完全能满足我们的开发要求。
随着Web的发展,前端已经从单纯HTML展示发展到前端应用,同时伴随着语言的发展,产生了大量的第三方帮助开发的库(例如:JQuery),我们所需要的Javascript脚本逐渐变大,变多,HTML页面中的<script>逐渐从单个变成了多个,但是在这种从单个到多个的转变中,却遇到了一些问题:

  1. Javascript文件之间的依赖:因为<script>脚本的机制,虽然每一个<script>共享了全局变量,但是却没有办法将后一个<script>脚本中声明的变量提升到前一个中,如果前一个<script>中依赖了该变量,则需要调整脚本顺序,但是单纯的从HTML引入脚本的顺序我们又不能明确知道脚本间的依赖关系
  2. 变量的泄漏:因为<script>之间是共享全局变量的,但是每个Javascript脚本在编写初期都是基于单个脚本来编写,不可避免的会出现多个脚本之间使用了同名的全局变量,那么后引入必定会覆盖之前的变量值,从而导致一些难以发现的问题

随着Javascript文件数量的增加,问题发生的频率就越来越大,工程越来越难以维护,于是机智的工程师们为了解决的这两个问题,提出了模块化解决方案(我始终觉得前端在工程化的过程中是逐步将后端的编码**引进并不断完善,包括模块化其实就是类似于参考了例如:Java的包或者类引入)

2. 模块化原理

模块化的核心有两个方面:

  1. 模块的声明和模块的导入,也就是模块化的语法,如何定义一个模块
  2. 模块的加载器,也就是如何将识别声明和导入的模块,将他们有条件的加载

因为模块的声明和导入是依赖于模块加载器的写法,因此说到底,模块化最主要的也就是如何根据规范编写一个模块加载器。编写模块加载器要解决的就是提到的两个痛点,先看如何解决第二个问题,为了避免泄漏,就是创建一个有限的作用域范围,最佳解决方案就是使用闭包来产生相关的作用域,使用IIFE返回相关函数实在是好的不能再好了

// IIFE函数闭包
(function(){
    var a = 1;
    return {
      f() {
        // 一些逻辑处理
        console.log(a);
      }
    }
})()

接下来就是解决依赖关联,也就是我想明确知道自己使用了那些模块,于是如果能将依赖的模块作为参数传递,其实就可以从一定程度上明确我们的依赖关系,于是

// 定义一个IIFE函数
var $ = (function(){
    var a = 1;
    return {
      f() {
        // 一些逻辑处理
        console.log(a);
      }
    }
})()
// 创建一个名字和对象的映射关系
var modules = {'$': $}
// 建立依赖关系
function f(depsname, callback) {
  let deps  = [];
  for (let name of depsname) {
    deps.push(modules[name]);
  }
  callback(deps)
}
// 使用依赖关联
f(['$'], ([$]) => { $.f()});

类似上面的处理,我们就可以很明确的知道了依赖了一个名叫$的模块,而这个模块我们在modules对象里面进行的映射关联,指向了具体的内容
这里的示例只是对原理的简单性阐述,并不是一种最佳实践

3. 模块化方案

3.1 CommonJS

CommonJS是最先提出的一种模块化解决方案,与其说它是解决方案,不如说它是一个规范,它约定了Javascript模块化的各种内容,基本上可以是模块化**的先驱

3.1.1 语法

NodeJS中的模块化处理方法使用了CommonJS的核心**,是CommonJS规范的实践者

// 模块的声明(导出)
module.exports = {foo}
exports.foo = foo
// 模块的引用(导入)
var foo = require('foo');

有两个地方需要说明:

  1. NodeJSexports === module.exports,因为默认在每个NodeJS模块都会在最上层添加var exports = module.exports的声明
  2. require引用的是对应模块文件的路径,有三种方式:相对路径,绝对路径,文件名,NodeJS的模块引擎会根据require中路径的写法,去获取对应的模块
3.1.2 特点

CommonJS的特点是模块加载是同步的,在模块使用前,需要经过模块加载器将模块语法转换为对应的Javascript代码,之后才能正常运行,这样带来的问题就是他不能直接在浏览器端进行使用,只能在服务端进行转换后使用

3.2 AMD

AMD(Asynchronies Module Definition),异步的模块定义,一大波人研究CommonJS,从CommonJS分离出来的,自行发展的一个新分支

3.2.1 语法

AMD的基于CommonJS规范,衍生出了自己的一套体系,比较出名的模块化脚本加载器就是Require.js了(之前阿里的Seajs也很出名的,当然发起SAP(单页面应用)趋势的Angular.js也是基于Require.js来进行模块加载的)以Require.js为例,说明下基本语法

// 入口配置
<script data-main="app" src="require.js">
// 模块的声明
define(module_id, deps, function(deps){});
define(function(require, export, module));
// 模块的引用
requirejs(deps, function(deps))
3.2.2 特点

AMD一诞生就是针对浏览器端模块化提出的解决方案,他不需要提前对模块进行转换操作(不过需要配置一个模块的入口文件,注意入口配置中的data-main属性),直接就可以在浏览器端实现模块的加载,同时模块化过程是异步的,可以做到CommonJS做不到的模块延迟加载

3.3 ES6模块

随着模块语法的发展,ES6基于CommonJSAMD,定义了一套模块化的方案,虽然现在浏览器对ES6模块化方案支持不多(Chrome似乎已经开始支持<script type="module">的提案了),但是我们仍然可以通过TypeScriptBabel提供的模块Loader,类似CommonJS模块化的处理手段,利用webpack等工具优先进行模块化转换

3.3.1 语法

ES6模块的根据不同情况,写法会有一定出入,但是主要就是区分了普通的情况和有default声明的情况

// ---模块导出---
// 1. 对象,函数,值导出
export { foo }
export function f() {}
export let a = 1
// 2. default导出
export default {}
export { x as default}
// 3. 导出其他模块的非default内容
export * from 'module_name'
export { foo } from 'module_name'
// 4. 导出其他模块的default内容
import A from 'module_name'
export default A

// ---模块导入---
// 1. 导入Default
import A from 'module_name'
// 2. 导出非Default
import { name1 } from 'module_name'
import { name1 as alias } from 'module_name'
// 3. 导入所有非Default内容并创建命名空间
import * as namespace from 'module_name' 
// 4. 同时导入Default和非Default内容
import A, {name1} from 'module_name'
import A, * as namespace from 'module_name'
// 5. 单纯进行模块加载
import 'module_name'
3.2.2 特点

ES6模块化方案有几个需要特别注意的特点:

  1. 通常理解为一个模块一个文件,而一个文件就是一个模块,每个模块中只能最多有一个defaultexport
  2. 每个模块只会加载一次
  3. 每个模块的变量声明默认只在当前模块内有效
  4. 模块的引入是单例的,即使多次引入,也只会共享一个实例
  5. import引入的模块是readonly

4. 总结

总的来说,随着前端项目的逐渐项目化,使用模块化避免了可能带来的隐藏问题,使得整个项目更便于维护和管理,我们也可以很放心的开发各种扩展库,而不用担心和外部变量冲突的问题,很大程度的促进了前端社区的繁荣

5. 参考

《You Don‘t know Javascript - ES6 & Beyond》
MDN-export
MDN-import
Writing Modular JavaScript With AMD, CommonJS & ES Harmony
CommonJS规范