浅析模块化和seajs

Authors
  • avatar
    Name
    小明&小艺
    Twitter

模块化带来的问题

前端交互的复杂和更新的频繁,对程序的可维护性和健壮性提出了更大的要求。基于本地文件的后端模块化,给了前端模块化很大的启示。模块化开发给维 护和开发带来了很大的好处,每个功能按模块区分,不同功能通过接口沟通,各司其职,各行其道。那么模块化就要求一个功能一个模块,但是 js 语言本并没有模块化的功能,也无法声明模块的依赖,而且一个模块一个请求,这在分秒必争的浏览器显然是一个逆发展。总的来说,模块化需要解决以下问题:

  • 模块的声明定义和接口的导出
  • 模块的依赖机制
  • 资源的优化整合

解决了以上问题,才能让模块化成为可能。

如何模块化

模块的声明定义和接口的导出

js 语言里本身不具备模块的功能,没关系,硬的没有,可以来软的。通过模拟实现模块的功能,以 seajs 为例,seajs 的模块定义如下:

define("ModuleA", function(require, exports){
  var moduleB = require("ModuleB");
  return {
    test: function(){
      console.log("test");
    }
  }
})

seajs 通过 define 函数实现模块定义功能,该函数实际上是定义了一个模块名称为 ModuleA 的模块,模块的内容为 Factory 函数,即回调函数。导出的接口导出是 return 的值,或者可以通过 exports 变量实现导出。

模块的依赖机制

模块的依赖指的是模块里面引用了别的模块,所依赖的模块与自身模块的关系维护。加载过的模块第二次再次 require 的时候怎么办?Factory 会运行两次?答案是否定的,同一个模块不该运行两次。js 里如何发现依赖?在 seajs 里是通过正则表达式解决的,seajs 会将 factory 函数 tostring 一下,拿到函数体,再进行正则匹配拿到依赖的模块,然后去加载依赖的模块,如果发现依赖的模块也有依赖,则再起加载~递归地加载。上面的 ModuleA 加载的时候发现 ModuleB 需要先加载,则会发起一个 ModuleB 的加载,在浏览器里 seajs 是通过生产 script 标签进行 js 加载的,监听其 onload 事件来执行模块注册,每次加载过的模块都会保存一份到 seajs.cache 里,每次加载都会先判断 cache 里有没有,没则发起 http 去获取模块代码,有则看看模块是否已经运行过 factory 函数,如没有则运行并返回结果,有则直接返回结果。seajs3.0 里做了一个优化,每次用 script 加载完后,都会删除该 dom 节点以减轻 dom 树的内存压力。(ps:seajs 里是允许循环依赖的,不过并没有解决循环依赖,不过循环依赖这种逻辑上有问题的组织方式本就不该存在。)

模块既然是一个文件,那 seajs 里是如何标识一个模块的呢?也就是它的模块唯一标识的管理。seajs 由于是以网络文件为基础的,它在请求每个模块的时候都会生给模块分配一个 id,对与 define 定义的模块,会加上网络域名的前缀,如果模块本身没有 id,则以模块的路径为基础如文件/static/modules/init/init.js 的文件,若 define 的时候没有指定 id,直接 define(factory),则它会以http://xxx.com:xx/static/modules/init/init.js作为模块的唯一id。值得一提的是,seajs的默认当前工作空间为sea.js文件的当前目录。也就是说require("jquery")的时候,如果jquery不是iyige别名,则它会去sea.js的当前目录里去找jquery文件。

资源的优化整合

模块化增加了文件的数量,而且由于 seajs 以网络文件为基础,本地找不到模块的时候,就回去加载网络上的模块,所以这也给打包带来很大的挑战。把多个模块合并成一个大文件,这也就放弃了模块当个加载的机会。那一个文件有多个模块的时候,怎么去区分一个模块呢?这就只能通过模块的 id 了,也就是说构建的时候需要动态生成模块的 id。所以合并之前要做一个模块 id 提取和依赖提取的操作。

seajs 的误区

seajs 里有几个地方很容易让人引起误解。

  1. seajs 是异步加载文件,但默认不是按需加载,模块只要 require 了,就会被加载。所以我们会看到 network 里有一些模块我们并没有用到,但是它第一次的时候已经加载了。要让其不加载,做到按需加载,只能调用 require.async 了。
  2. require 函数并不会发起模块文件的请求,模块文件的请求时在入口函数时,通过对 factory 进行正则匹配的时候发起的;require 只是会触发 factory 的执行和获取接口。

模块化是大势所趋,但 seajs 的模块化方案只是 js 的模块化,并不是完全彻底的前端模块化。相比而言 react 的模块化更加彻底和完整。但是 seajs 的模块化可以借助构建工具完善。