前端模块化规范

早期的前端技术标准根本没有预料到前端会有今天这样的规模,所以很多设计会使我们在开发过程中遇到很多模块化的问题,虽然现如今基本上大部分都被我们后来所引用的各种标准所解决了,但是这个过程还是值得我们去梳理了解一下的。

早期不使用工具和规范的情况下对模块化的落地方式:

第一阶段:文件划分

我们去约定每个文件代表一个独立的模块,并最终将其引入到html文件当中,最后我们在后面调用模块当中的成员。


image.png

image.png

这种方式其实没有从根本上解决模块化想要解决的问题,我们在多人开发过程中会遇到的全局变量污染,命名冲突等问题并没有得到解决,并且我们还是无法很好的去管理模块与模块间的依赖关系。早期引用模块的数量少还可以,将来引用的模块数量一多起来就很麻烦了。

总而言之,这种方式还是需要主要依靠约定。

第二阶段:空间命名划分

我们将每个模块包裹为一个全局对象,相当于我们在每个模块内为每个模块添加了命名空间


image.png

image.png

通过命名空间的方式可以一定程度上避免命名冲突和全局变量污染的问题,但是问题是我们每个模块的内部成员还是可以在外部直接访问或是修改,这样就会存在安全隐患,并且模块间的依赖关系依然没有得到解决。

第三阶段:立即执行函数

我们将每个模块都放在函数提供的私有作用域当中,对于需要暴露给外部的成员我们可以以挂在到全局对象上的方式去实现


image.png

image.png

这样我们每个模块的私有成员就都存在了闭包当中,从外部无法直接访问,这样就保证了我们私有变量的安全。

image.png

并且我们还可以将依赖模块作为立即执行函数的参数来传入,实现让模块间的依赖关系更加明确。

以上三个阶段的方式都是以原始的模块系统为基础,通过约定化的方式去实现的代码组织,这些方式在不同开发者去实现的过程中会有细微的差别。

为了统一不同开发者和不同项目之间的差异,我们就需要一个标准去规范模块化的实现方式。

并且所有的js文件和依赖都需要在html文件里统一以script标签的方式引入,在代码量越来越大,需要的依赖越来越多的后期维护也是问题。

因此我们需要一个通用的模块化标准 和 使用代码来实现的模块加载器。

模块化规范

CommonJS规范

由Node.js提供的模块化规范,他规定了:

  • 一个文件就是一个模块
  • 每个模块都有单独的作用域
  • 通过module.exports导出成员
  • 通过require函数 载入模块成员

现如今的前端开发者应该都非常熟悉这个规范,但是这个规范适用于node端,在浏览器端直接使用是有问题的。

CommonJS约定是以同步的方式去加载模块,node的执行机制是在启动时加载模块,执行过程中实用模块而不需要去加载模块,因此在node端运行是没有问题的。

而在浏览器端,必然导致效率低下,每次页面加载都会导致大量的同步请求出现,因此早期的浏览器规范并没有选择CommonJS规范,而是专门设计了一个AMD规范。

AMD规范(Asynchronous Module Definition)

翻译过来意思就是异步的模块定义规范,并且同期还推出了一个库,Require.js,它实现了AMD规范,它本身又是一个强大的模块加载器。

举个例子,引用require.js使用AMD规范大概的文件结构和过程如下:


image.png

main.js:
入口文件
require.js内部提供require函数帮我们引入模块,用法和define类似。每次执行require函数其实内部就是帮我们创建一个script标签来帮我们执行内部的代码。

require.config({
  paths: {
    // 因为 jQuery 中定义的是一个名为 jquery 的 AMD 模块
    // 所以使用时必须通过 'jquery' 这个名称获取这个模块
    // 但是 jQuery.js 并不一定在同级目录下,所以需要指定路径
    jquery: './lib/jquery'
  }
})

require(['./modules/module1'], function (module1) {
  module1.start()
})

/modules/module1.js
定义模块,内部提供define函数,帮助我们定义模块。我们定义模块的时候接受三个参数,分别是:模块名,[依赖1,依赖2],function(依赖1形参,依赖2形参){模块函数私有空间; return 对外暴露成员}

// 因为 jQuery 中定义的是一个名为 jquery 的 AMD 模块
// 所以使用时必须通过 'jquery' 这个名称获取这个模块
// 但是 jQuery.js 并不在同级目录下,所以需要指定路径
define('module1', ['jquery', './module2'], function ($, module2) {
  return {
    start: function () {
      $('body').animate({ margin: '200px' })
      module2()
    }
  }
})

AMD社区生态可以说相对完善了,但AMD使用过程中可能也存在一定的问题:

  • AMD使用起来相对复杂一点
  • 模块JS文件请求频繁

总体来看,AMD规范算是前端模块化的一个中间产物,并不能算是一个最终方案。更像是一个这种妥协的产物。除此之外还有同期出现的由taobao推出的Sea.js库,它实现的标准是由淘宝官方提出的CMD规范,全称是Common Module Definition,他的规范类似于CommonJS,在使用上类似于AMD规范,他想实现的目的是让我们在使用CMD的同时写出的代码与CommonJS类似,从而减轻开发者的学习成本,但是最后这种写法后来被require.js也兼容了。

// 兼容 CMD 规范(类似 CommonJS 规范)
define(function (require, exports, module) {
    // 通过 require 引入依赖
  var $ = require('jquery')
  // 通过 exports 或者 module.exports 对外暴露成员
  module.exports = function () {
    console.log('module 2~')
    $('body').append('<p>module2</p>')
  }
})

目前:ES Modules(浏览器) + CommonJS(node)

在这之前我们大概了解了前端模块化发展的大致过程,尽管之前的标准基本都实现了模块化,但或多或少都会有一些让开发者不太容易接受的问题,随着技术的发展,js的标准也在逐步完善。现如今的模块化可以说已经比较成熟了,目前开发者对于前段模块化的实现方式也已经基本统一。

  • 在node环境中使用CommonJS规范
  • 在浏览器环境中使用ES Modules规范

当然,也会有部分其他情况出现,但是就主流而言,目前前端基本已经统一成了这两种规范,因此对于目前的前端开发者而言,就模块化规范来讲,着重来掌握这两种规范就好。

CommonJS in Node.js

这里其实没什么好说的,CommonJS是node的内置的模块系统,在node环境下执行不存在任何环境问题,通过require去载入模块,通过module.exports去导出模块。

ES Modules in Browser

ES Modules是ECMAScript2015(ES6)定义的一个新的模块系统,也就是说这是最近几年才被定义的一个标准,他会存在很多兼容性的问题。在这个标准刚出现的时候,几乎所有浏览器都不支持这个新特性。但后来随着webpack等各种打包工具的流行,这个规范才逐渐开始普及,截至目前。ES Modules可以说是目前最主流的前端模块化方案。

相比于AMD这种由社区提出的模块化开发规范,ES Modules相当于是在语言层面实现了模块化,因此更为完善。并且现如今绝大多数浏览器已经开始支持ES Modules特性了:

image.png

原生支持,就意味着我们可以在开发中直接使用,这就意味着将来会越来越普及。

原生是如何支持的:

引入时在script标签上添加type="module"

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>ES Module - 模块的特性</title>
</head>
<body>
  <!-- 通过给 script 添加 type = module 的属性,就可以以 ES Module 的标准执行其中的 JS 代码了 -->
  <script type="module">
    console.log('this is es module')
  </script>

  <!-- 1. ESM 自动采用严格模式,忽略 'use strict' -->
  <script type="module">
    console.log(this)
  </script>

  <!-- 2. 每个 ES Module 都是运行在单独的私有作用域中 -->
  <script type="module">
    var foo = 100
    console.log(foo)
  </script>
  <script type="module">
    console.log(foo)
  </script>

  <!-- 3. ESM 是通过 CORS 的方式请求外部 JS 模块的 -->
  <!-- <script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script> -->

  <!-- 4. ESM 的 script 标签会延迟执行脚本 -->
  <script defer src="demo.js"></script>
  <p>需要显示的内容</p>
</body>
</html>

通过import导入模块,export导出模块

// import { default as fooName } from './module.js'
// console.log(fooName)

import { name, hello, Person } from './module.js'
console.log(name, hello, Person)
// export var name = 'foo module'

// export function hello () {
//   console.log('hello')
// }

// export class Person {}

var name = 'foo module'

function hello () {
  console.log('hello')
}

class Person {}

// export { name, hello, Person }

// export {
//   // name as default,
//   hello as fooHello
// }

// export default name

// var obj = { name, hello, Person }

export { name, hello, Person }

这里注意:

  • 通过import引入的变量其实和引用文件export导出的变量是一个地址,而非复制,即修改了export导出的变量,另一边通过import引入的变量也将法神改变。

  • import引入的变量一般情况下是只读的,我们不可以在引用后进行手动修改,这样也避免了访问和改变其他模块内部变量的安全问题。

    ES Modules基本特性:

    • 自动采用严格模式,忽略'use strict'
    • 每个ESM都是单独的私有作用域
    • ESM是通过CORS去请求外部JS模块的
    • ESM的script标签都会延迟执行脚本,类似于defer属性。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342