详解Js中的模块化是如何实现的


Posted in Javascript onOctober 18, 2017

由于 Js 起初定位的原因(刚开始没想到会应用在过于复杂的场景),所以它本身并没有提供模块系统,随着应用的复杂化,模块化成为了一个必须解决的问题。本着菲麦深入原理的原则,很有必要来揭开模块化的面纱

一、模块化需要解决的问题

要对一个东西进行深入的剖析,有必要带着目的去看。模块化所要解决的问题可以用一句话概括

在没有全局污染的情况下,更好的组织项目代码

举一个简单的栗子,我们现在有如下的代码:

function doSomething () {
 const a = 10;
 const b = 11;
 const add = function (a + b) {
  return a + b
 }
 add (a + b)
}

在现实的应用场景中,doSomething 可能需要做很多很多的事情,add 函数可能也更为复杂,并且可以复用,那么我们希望可以将 add 函数独立到一个单独的文件中,于是:

// doSomething.js 文件
const add = require('add.js');
const a = 10;
const b = 11;
add(a+ b);
// add.js 文件
function add (a, b) {
 return a + b;
}
module.exports = add;

这样做的目的显而易见,更好的组织项目代码,注意到两个文件中的 require 和 module.exports,从现在的上帝视角来看,这出自 CommonJS 规范(后文会有一个章节来专门讲规范)中的关键字,分别代表导入和导出,抛开规范而言,这其实是我们模块化之路上需要解决的问题。另外,虽然 add 模块需要得到复用,但是我们并不希望在引入 add 的时候造成全局污染

二、引入的模块如何运行

在上述的例子中,我们已经将代码拆分到了两个模块文件当中,在不造成全局污染的情况下,如何实现 require,才能使得例子中的代码做到正常运行呢?

先不考虑模块文件代码的载入过程,假设 require 已经可以从模块文件中读取到代码字符串,那么 require 可以这样实现

function require (path) {
  // lode 方法读取 path 对应的文件模块的代码字符串
  // let code = load(path);
  // 不考虑 load 的过程,直接获得模块 add 代码字符串
  let code = 'function add(a, b) {return a+b}; module.exports = add';
  // 封装成闭包
  code = `(function(module) {$[code]})(context)`
  // 相当于 exports,用于导出对象
  let context = {};
  // 运行代码,使得结果影响到 context
  const run = new Function('context', code);
  run(context, code);
  //返回导出的结果
  return context.exports;
}

这有几个要点:

1) 为了不造成全局污染,需要将代码字符串封装成闭包的形式,并且导出关键字 module.exports ,module 是与外界联系的唯一载体,需要作为闭包匿名函数的入参,与引用方传入的上下文 context 进行关联

2) 使用 new Function 来执行代码字符串,估计大部分同学对 new Function 是不熟悉的,因为一般情况下定义一个函数无需如此,要知道,用 Function 类可以直接创建函数,语法如下:

var function_name = new function(arg1, arg2, ..., argN, function_body)

在上面的形式中,每个 arg 都是一个参数,最后一个参数是函数主体(要执行的代码)。这些参数必须是字符串。也就是说,可以使用它来执行字符串代码,类似于 eval,并且相比 eval, 还可以通过参数的形式传入字符串代码中的某些变量的值

3)如果曾经你有疑惑过为什么规范的导出关键字只有 exports 而我们实际使用过程中却要使用module.exports(写过 Node 代码的应该不会陌生),那在这段代码中就可以找到答案了,如果只用 exports 来接收 context,那么对 exports 的重新赋值对 context 不会有任何影响(参数的地址传递),不信将代码改成如下形式再跑一跑:

详解Js中的模块化是如何实现的

演示结果

三、代码载入方式

解决了代码的运行问题,还需要解决模块文件代码的载入问题,根据上述实例,我们的目标是将模块文件代码以字符串的形式载入

在 Node 容器,所有的模块文件都在本地,只需要从本地磁盘读取模块文件载入字符串代码,再走上述的流程就可以了。事实证明,Node 非内建、核心、c++ 模块的载入执行方式大体如此(虽然使用的不是 new Function,但也是一个类似的方法)

在 RN/Weex 容器,要载入一个远程 bundle.js,可以通过 Native 的能力请求一个远程的 js 文件,再读取成字符串代码载入即可(按照这个逻辑,Node 读取一个远程的 js 模块好像也无不可,虽然大多数情况下我们不需要这么做)

在浏览器环境,所有的 Js 模块都需要远程读取,尴尬的是,受限于浏览器提供的能力,并不能通过 ajax 以文件流的形式将远程的 js 文件直接读取为字符串代码。前提条件无法达成,上述运行策略便行不通,只能另辟蹊径

这就是为什么有了 CommonJs 规范了,为什么还会出现 AMD/CMD 规范的原因

那么浏览器上是怎么做的呢?在浏览器中通过 Js 控制动态的载入一个远程的 Js 模块文件,需要动态的插入一个 <script> 节点:

// 摘抄自 require.js 的一段代码
var node = config.xhtml ?
        document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') :
        document.createElement('script');
node.type = config.scriptType || 'text/javascript';
node.charset = 'utf-8';
node.async = true;
node.setAttribute('data-requirecontext', context.contextName);
node.setAttribute('data-requiremodule', moduleName);
node.addEventListener('load', context.onScriptLoad, false);
node.addEventListener('error', context.onScriptError, false);

要知道,设置了 <script> 标签的 src 之后,代码一旦下载完成,就会立即执行,根本由不得你再封装成闭包,所以文件模块需要在定义之初就要做文章,这就是我们说熟知的 AMD/CMD 规范中的 define,开篇的 add.js 需要重新改写一下

// add.js 文件
define ('add',function () {
  function add (a, b) {
   return a + b;
  }
  return add;
})

而对于 define 的实现,最重要的就是将 callback 的执行结果注册到 context 的一个模块数组中:

context.modules = {}
  function define(name, callback) {
    context.modules[name] = callback && callback()
  }

于是 require 就可以从 context.modules 中根据模块名载入模块了,是不是有了一种自己去写一个 “requirejs” 的冲动感

具体的 AMD 实现当然还会复杂很多,还需要控制模块载入时序、模块依赖等等,但是了解了这其中的灵魂,想必去精读 require.js 的源码也不是一件困难的事情

四、Webpack 中的模块化

Webpack 也可以配置异步模块,当配置为异步模块的时候,在浏览器环境同样的是基于动态插入 <script> 的方式载入远程模块。在大多数情况下,模块的载入方式都是类似于 Node 的本地磁盘同步载入的方式

??忘记,Webpack 除了有模块化的能力,还是一个在辅助完善开发工作流的工具,也就是说,Webpack 的模块化是在开发阶段的完成的,使用 Webpack 构筑的工作环境,在开发阶段虽然是独立的模块文件,但是在运行时,却是一个合并好的文件

所以 Webpack 是一种在非运行时的模块化方案(基于 CommonJs),只有在配置了异步模块的时候对异步模块的加载才是运行时的(基于 AMD)

五、模块化规范

通用的问题在解决的过程中总会形成规范,上文已经多次提到 CommonJs、AMD、CMD,有必要花点篇幅来讲一讲规范

Js 的模块化规范的萌发于将 Js 扩展到后端的想法,要使得 Js 具备类似于 Python、Ruby 和 Java 那样具备开发大型应用的基础能力,模块化规范是必不可少的。CommonJS 规范的提出,为Js 制定了一个美好愿景,希望 Js 能在任何地方运行,包括但不限于:

  • 服务器端 Js 应用
  • 命令行工具
  • 桌面应用
  • 混合应用

CommonJS 对模块的定义并不复杂,主要分为模块引用、模块定义和模块标识

  1. 模块引用:使用 require 方法来引入一个模块
  2. 模块定义:使用 exports 导出模块对象
  3. 模块标识:给 require 方法传入的参数,小驼峰命名的字符串、相对路径或者绝对路径

详解Js中的模块化是如何实现的

模块示意

CommonJs 规范在 Node 中大放异彩并且相互促进,但是在浏览器端,鉴于网络的原因,同步的方式加载模块显然不太实用,在经过一段争执之后,AMD 规范最终在前端场景中胜出(全称 Asynchronous Module Definition,即“异步模块定义”)

什么是 AMD,为什么需要 AMD ?在前述模块化实现的推演过程中,你应该能够找到答案

除此之外还有国内玉伯提出的 CMD 规范,AMD 和 CMD 的差异主要是,前者需要在定义之初声明所有的依赖,后者可以在任意时机动态引入模块。CMD 更接近于 CommonJS

两种规范都需要从远程网络中载入模块,不同之处在于,前者是预加载,后者是延迟加载

五、总结

如果有心,可以参照本文的推演,来实现一个 “yourRequireJs”,没有什么比重复造轮子更能让知识沉淀~~

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
根据分辩率调用不同的CSS.
Jan 08 Javascript
JavaScript 类的定义和引用 JavaScript高级培训 自定义对象
Apr 27 Javascript
firefox事件处理之自动查找event的函数(用于onclick=foo())
Aug 05 Javascript
jQuery 插件仿百度搜索框智能提示(带Value值)
Jan 22 Javascript
jquery text()方法取标签中的文本
Jul 25 Javascript
Node.js开源应用框架HapiJS介绍
Jan 14 Javascript
个人总结的一些JavaScript技巧、实用函数、简洁方法、编程细节
Jun 10 Javascript
Angular.js自定义指令学习笔记实例
Feb 24 Javascript
C#微信小程序服务端获取用户解密信息实例代码
Mar 10 Javascript
Flask中获取小程序Request数据的两种方法
May 12 Javascript
在Vue 中使用Typescript的示例代码
Sep 10 Javascript
vue分页器组件编写方法详解
Jun 28 Javascript
JS跳转手机站url的若干注意事项
Oct 18 #Javascript
vue实现手机号码抽奖上下滚动动画示例
Oct 18 #Javascript
Angular.js实现获取验证码倒计时60秒按钮的简单方法
Oct 18 #Javascript
浅谈Node异步编程的机制
Oct 18 #Javascript
js实现随机点名系统(实例讲解)
Oct 18 #Javascript
原生JS获取元素的位置与尺寸实现方法
Oct 18 #Javascript
详谈commonjs模块与es6模块的区别
Oct 18 #Javascript
You might like
关于php curl获取301或302转向的网址问题的解决方法
2011/06/02 PHP
laravel框架 laravel-admin上传图片到oss的方法
2019/10/13 PHP
js模拟实现Array的sort方法
2007/12/11 Javascript
JavaScript 变量基础知识
2009/11/07 Javascript
JavaScript中也使用$美元符号来代替document.getElementById
2010/06/19 Javascript
js字符编码函数区别分析
2011/12/28 Javascript
js判断横竖屏及禁止浏览器滑动条示例
2014/04/29 Javascript
简介JavaScript中fixed()方法的使用
2015/06/08 Javascript
浅析BootStrap模态框的使用(经典)
2016/04/29 Javascript
jquery实现跳到底部,回到顶部效果的简单实例(类似锚)
2016/07/10 Javascript
浅谈js的异步执行
2016/10/18 Javascript
bootstrap提示标签、提示框实现代码
2016/12/28 Javascript
jQuery使用正则验证15/18身份证的方法示例
2017/04/27 jQuery
使用Node.js实现简易MVC框架的方法
2017/08/07 Javascript
vue element-ui 绑定@keyup事件无效的解决方法
2018/03/09 Javascript
nodeJs爬虫的技术点总结
2018/05/13 NodeJs
详解可以用在VS Code中的正则表达式小技巧
2019/05/14 Javascript
vue 使用 vue-pdf 实现pdf在线预览的示例代码
2020/04/26 Javascript
vue2.* element tabs tab-pane 动态加载组件操作
2020/07/19 Javascript
微信小程序中target和currentTarget的区别小结
2020/11/06 Javascript
Python生成pdf文件的方法
2014/08/04 Python
CentOS6.9 Python环境配置(python2.7、pip、virtualenv)
2019/05/06 Python
详解python运行三种方式
2019/05/13 Python
让Python脚本暂停执行的几种方法(小结)
2019/07/11 Python
python3.6生成器yield用法实例分析
2019/08/23 Python
python能做什么 python的含义
2019/10/12 Python
深度学习入门之Pytorch 数据增强的实现
2020/02/26 Python
python爬取代理IP并进行有效的IP测试实现
2020/10/09 Python
2021年值得向Python开发者推荐的VS Code扩展插件
2021/01/25 Python
澳大利亚票务和娱乐市场领导者:Ticketmaster
2017/03/03 全球购物
Monki官网:斯堪的纳维亚的独立时尚品牌
2020/11/09 全球购物
荷叶母亲教学反思
2014/04/30 职场文书
勤奋学习演讲稿
2014/05/10 职场文书
赔偿协议书范本
2014/09/12 职场文书
护士年终考核评语
2014/12/31 职场文书
Python+腾讯云服务器实现每日自动健康打卡
2021/12/06 Python