浅谈JS前端模块化的几种规范

前言

有这样一个场景,客户端运行很久,但是法务部和数据部需要收集用户的一些信息,这些信息收集好之后需要进行相应的数据处理,之后上报到服务端。客户端提供一个纯粹的js执行引擎,不需要 WebView 容器。iOS 端有成熟的JavaScriptCore、Android 可以使用 V8 引擎。这样一个引擎配套有一个 SDK,访问 Native 的基础能力和数据运算能力,可以看成是一个阉割版的 Hybrid SDK 额外增加了一些数据处理能力。

问题结束了吗?处理逻辑的时候还需要用到2个库:cheerio和sql。因为都是 Node 工程,所以纯粹的js环境是没办法直接执行。所以需求就进行了转变 ———— 将 Node 项目打包成 UMD 规范。这样就可以在纯粹的 JS 环境下运行。接下来的文章就分析下各种规范。其实也就是前端模块化的几种规范。

前端模块化开发的价值

随着互联网的飞速发展,前端开发越来越复杂。本文将从实际项目中遇到的问题出发,讲述模块化能解决哪些问题,以及以 Sea.js 为例讲解如何进行前端的模块化开发。

恼人的命名冲突

我们从一个简单的习惯出发。我做项目时,常常会将一些通用的、底层的功能抽象出来,独立成一个个函数,比如

function  each(arr) {
// 实现代码
} 

function  log(str) {
// 实现代码
}

并像模像样的将这些代码抽取出来并统一到util.js中,在需要使用的地方引入该文件,看起来很棒,团队内的同事很感激我提供了这么便利的工具包。

直到团队越来越大,开始有人抱怨

小杨:我定义了一个 each 方法遍历对象,但是 util.js 中已经存在一个 each 方法,每次都需要改方法名,我只能叫 eachObject 方法。<br>张三:我定义了一个 log 方法,可是王武的代码出问题了,谁来看看?

抱怨越来越多,最后参照 Java 的方式,引入命名空间解决问题。于是 util.js 代码变成了

var  org = {};

org.Utils = {};

org.Utils.each = function (arr) {

// 实现代码

};

org.Utils.log = function (str) {

// 实现代码

};

可能看上去的代码很 low,其实命名空间在前端领域的布道者是 Yahoo!的 YUI2 项目,看看下面的代码,是 Yahoo!的一个开源项目

if (org.cometd.Utils.isString(response)) {

return  org.cometd.JSON.fromJSON(response);

}

if (org.cometd.Utils.isArray(response)) {

return  response;

}

通过命名空间虽然可以极大的解决冲突问题,但是每次在调用一个方法时都需要写一大堆命名空间相关的代码,剥夺了编码乐趣。

另一种方式是一个自执行函数来实现。

(function (args) {

//...

})(this);

繁琐的文件依赖

继续上述场景,很多情况下都需要开发 UI 层通用组件,这样项目组就不需要重复造轮子。其中有一个高频使用的组件就是 dialog.js

<script  src="util.js"></script>

<script  src="dialog.js"></script>

<script>

org.Dialog.init({  /* 传入配置 */  });

</script>

虽然公共组做项目都会编写使用文档、发送邮件告知全员(项目地址、使用方式等),但是还是有人问「为什么 dialog.js 有问题」,最后排查的结果基本都是没有引入 util.js

<script  src="dialog.js"></script>

<script>

org.Dialog.init({  /* 传入配置 */  });

</script>

命名冲突和文件依赖是前端开发中2个经典问题,经过开发者不断的思考和研究,诞生了模块化的解决方案,以 CMD 为例

define(function(require, exports) {

exports.each = function (array) {

// ...

};

exports.log = function(message) {

// ...

};

});

通过 exports 就可以向外提供接口, dialog.js 代码变成

define(function(require, exports) {

var  util = require('./util.js')

exports.init = function () {

// ...

};

});

使用的时候可以通过require('./util.js')获取到 util.js 中通过 exports 暴露的接口。 require 的方式在其他很多语言中都有解决方案:include、

模块化的好处

1.模块的版本管理:通过别名等配置,配合构建工具,可以轻松实现模块的版本管理

2.提高可维护性: 模块化可以实现每个文件的职责单一,非常有利于代码的维护。

3.前端性能优化: 对于前端开发来说,异步加载模块对于页面性能非常有益。

4.跨环境共享模块: CMD 模块定义规范与 NodeJS 的模块规范非常相近,所以通过 Sea.JS 的 NodeJS 版本,可以方便的实现模块的跨服务器和浏览器共享。

CommonJS 规范

CommonJS 是服务器端模块的规范。NodeJS 采用了这个规范。CommonJS 加载模块是同步的,所以只有加载完成后才能执行后面的操作。

因为服务器的特点,加载的模块文件一般都存在在本地硬盘,所以加载起来比较快,不用考虑异步的方式。

CommonJS 模块化的饿规范中,每个文件都是一个模块,拥有独立的作用域、变量、以及方法等,对其他模块不可见。 CommonJS 规范规定,每个模块内部,module变量表示当前模块,它是一个对象,它的exports属性是对外的接口,加载某个模块,其实是加载该模块的 module.exports 属性,require 方法用于加载模块。

// Person.js

function  Person () {

this.eat = function () {

console.log('eat something')

}

this.sleep = function () {

console.log('sleep')

}

}

var  person = new  Person();

exports.person = person;

exports.name = name;

// index.js

let  person = require('./Person').person;

person.eat()

CommonJS 与 ES6 模块的差异

1.CommonJS 模块输出的是值的拷贝,ES6 模块输出的是值的引用

2.CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

CommonJS 模块导出的是一个对象(module.exports 属性),该对象只在脚本运行完才会生成。

ES6 的模块机制是 JS 引擎对脚本进行静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用,等到脚本真正执行时,再根据这个只读引用到被加载的模块中取值,

AMD 规范

AMD(Asynchronous Module Definition) 是在 Require.JS 推广的过程中对模块定义的规范化产出。AMD 推崇依赖前置。它是 CommonJS 模块化规范的超集,作用在浏览器上。它的特点是异步,利用了浏览器的并发能力,让模块的依赖阻塞变少。

AMD 的 API

define(id?, dependencyies?, factory);

id 是模块的名字,是可选参数。 dependencies 指定了该模块所依赖的模块列表,是一个数组,也是可选参数。每个依赖的模块的输出都将作为参数依次传入 factory 中。

require([module], callback)

AMD 规范允许输出模块兼容 CommonJS 规范,这时 define 方法如下

define(['module1', 'module2'], function(module1, module2) {

function  foo () {

// ...

}

return { foo:  foo };

});

define(function(require, exports, module) {

var  requestedModule1 = require('./module1')

var  requestedModule2 = require('./module2')

function  foo () {

// ...

}

return { foo:  foo };

});

优点: 适合在浏览器环境中加载模块,可以实现并行加载多个模块

缺点: 提高了开发成本,并不能按需加载,而是提前加载所有的依赖

CMD 规范

CMD 是 Sea.JS 推广的过程中对模块定义的规范化产出。CMD 推崇依赖就近。

CMD 规范尽量保持简单,并与 CommonJS 规范中的 Module 保持兼容,通过 CMD 规范编写的模块,可以在 NodeJS 中运行。

CMD 模块定义规范

CMD 中 require 依赖的描述用数组,则是异步加载,如果是单个依赖使用字符串,则是同步加载。

AMD 是 RequireJS 在推广过程中对模块定义的规范化产出,CMD是SeaJS 在推广过程中被广泛认知。SeaJS 出自国内蚂蚁金服玉伯。二者的区别,玉伯在12年如是说:

RequireJS 和 SeaJS 都是很不错的模块加载器,两者区别如下:

  • 两者定位有差异。RequireJS 想成为浏览器端的模块加载器,同时也想成为 Rhino / Node 等环境的模块加载器。SeaJS 则专注于 Web 浏览器端,同时通过 Node 扩展的方式可以很方便跑在 Node 服务器端
  • 两者遵循的标准有差异。RequireJS 遵循的是 AMD(异步模块定义)规范,SeaJS 遵循的是 CMD (通用模块定义)规范。规范的不同,导致了两者API 的不同。SeaJS 更简洁优雅,更贴近 CommonJS Modules/1.1 和 Node Modules 规范。
  • 两者社区理念有差异。RequireJS 在尝试让第三方类库修改自身来支持 RequireJS,目前只有少数社区采纳。SeaJS 不强推,而采用自主封装的方式来“海纳百川”,目前已有较成熟的封装策略。
  • 两者代码质量有差异。RequireJS 是没有明显的 bug,SeaJS 是明显没有 bug。
  • 两者对调试等的支持有差异。SeaJS 通过插件,可以实现 Fiddler 中自动映射的功能,还可以实现自动 combo 等功能,非常方便便捷。RequireJS无这方面的支持。
  • 两者的插件机制有差异。RequireJS 采取的是在源码中预留接口的形式,源码中留有为插件而写的代码。SeaJS 采取的插件机制则与 Node 的方式一致开放自身,让插件开发者可直接访问或修改,从而非常灵活,可以实现各种类型的插件。

UMD 规范

UMD(Universal Module Definition)是随着大前端的趋势产生,希望提供一个前后端跨平台的解决方案(支持 AMD、CMD、CommonJS 模块方式)。

实现原理:

1.先判断是否支持 Node.js 模块格式(exports 是否存在),存在则使用 Node.js 模块格式

2.再判断是否支持 AMD 模块格式(define 是否存在),存在则使用 AMD 模块格式

3.前2个都不存在则将模块公开到全局(window 或 global)

// if the module has no dependencies, the above pattern can be simplified to

(function (root, factory) {

if (typeof  define === 'function' && define.amd) {

// AMD. Register as an anonymous module.

define([], factory);

} else  if (typeof  exports === 'object') {

// Node. Does not work with strict CommonJS, but

// only CommonJS-like environments that support module.exports,

// like Node.

module.exports = factory();

} else {

// Browser globals (root is window)

root.returnExports = factory();

}

}(this, function () {

// Just return a value to define the module export.

// This example returns an object, but the module

// can return a function as the exported value.

return {};

}));

可能有些人就要问了,为什么在上面的判断中写了 AMD,怎么没有 CMD?因为前端构建工具webpack不可识别 CMD 规范,使用 CMD 就需要引用工具,比如 Sea.JS

讲道理,如果想判断 CMD,那 UMD 代码如何写?

(function(root, factory) {

if (typeof  define === 'function' && define.amd) {

// AMD. Register as an anonymous module.

define([], factory);

} else  if (typeof  define === 'function' && define.cmd) {

// CMD

define(function(require, exports, module) {

module.exports = factory()

})

} else  if (typeof  exports === 'object') {

// Node. Does not work with strict CommonJS, but

// only CommonJS-like environments that support module.exports,

// like Node.

module.exports = factory();

} else {

// Browser globals (root is window)

root.returnExports = factory();

}

}(this, function() {

// Just return a value to define the module export.

// This example returns an object, but the module

// can return a function as the exported value.

return {};

}))

回到正题

Cheerio 如何打包到普通的 JS 执行环境中。

借助webpack可以方便的打出一个 umd 规范的包。

module.exports = {

entry:  './src/cheerio.js',

output: {

filename:  'cheerio.js',

// export to AMD, CommonJS, or window

libraryTarget:  'umd',

// the name exported to window

library:  'cheerio',

globalObject:  'this'

}

}

总结

手机端(无论 iOS 还是 Android)的底层渲染内核都是类 Chrome v8 引擎。v8 引擎在执行 JS 代码时,是将代码先以 MacroAssembler 汇编库在内存中先编译成机器码再送往 CPU 执行的,并不是像其它 JS 引擎那样解析一行执行一行。所以,静态加载的 ES6 模块规范,更有助于 v8 引擎发挥价值。而运行时加载的 CommonJS、AMD、CMD 规范等,均不利于 v8 引擎施展拳脚。

在 NodeJS 开发项目中,Node9 已经支持 ES6语法,完全可以使用 ES6 模块规范。NodeJS 的诞生,本身就基于 Google 的 v8 引擎,没有理由不考虑发挥 v8 的最大潜能。

在浏览器 JS 开发项目中,因为从服务器加载文件需要时间,使用 CommonJS 规范肯定是不合适了。至于是使用原生的 ES 模块规范,还是使用 Sea.js,要看具体场景。如果想页面尽快加载,Sea.js 适合;如果是单页面网站,适合使用原生的 ES6 模块规范。还有一点,浏览器并非只有 Chrome 一家,对于没有使用 v8 引擎的浏览器,使用 ES6 原生规范的优势就又减少了一点。

以上就是浅谈JS前端模块化的几种规范的详细内容,更多关于JS前端模块化的资料请关注我们其它相关文章!

(0)

相关推荐

  • 关于JS模块化的知识点分享

    模块化是一个语言膨胀的必经之路,它能够帮助开发者拆分和组织代码. Module模式 在模块化规范形成之前,JS开发者使用Module设计模式来解决JS全局作用域的污染问题.Module模式最初被定义为一种在传统软件工程中为类提供私有和公有封装的方法.在JavaScript中,Module模式使用匿名函数自调用 (闭包)来封装,通过自定义暴露行为来区分私有成员和公有成员. let myModule = (function (window) { let moduleName = 'module' /

  • JS前端模块化原理与实现方法详解

    本文实例讲述了JS前端模块化原理与实现方法.分享给大家供大家参考,具体如下: 1.什么是前端模块化 模块化开发,一个模块就是一个实现特定功能的文件,有了模块我们就可以更方便地使用别人的代码,要用什么功能就加载什么模块. 2.模块化开发的好处 1)避免变量污染,命名冲突 2)提高代码利用率 3)提高维护性 4)依赖关系的管理 3.前端模块化的进程 前端模块化规范从原始野蛮阶段现在慢慢进入"文艺复兴"时代,实现的过程如下: 3.1 函数封装 我们在讲到函数逻辑的时候提到过,函数一个功能是实

  • JavaScript 模块化开发实例详解【seajs、requirejs库使用】

    本文实例讲述了JavaScript 模块化开发.分享给大家供大家参考,具体如下: JS开发的问题 冲突 依赖 JS引入的文件,产生依赖. 使用命名空间解决: 命名空间的弊端 调用的时候 名字比较长. 只能减低冲突,不能完全避免 SeaJs使用 引入sea.js的库 如何变成模块? define 如何调用模块? exports 和 seajs.use 如何依赖模块? require //html: <script src="js/sea.js" type="text/ja

  • Javascript模块化机制实现原理详解

    1. 概述 js发展初期暴露了其缺陷:缺乏模块,后来提出了commonJS规范来规范其模块的规范.作为JavaScript新手,发现对于其JavaScript的模块机制,不是很理解.我查阅了一些资料整理了JavaScript CommonJS的原理和机制. 2. JavaScript 2.1 无后端的项目 这类项目不能使用CommonJS的模块规范,这是我起初所犯的错误.在没有es6被大多数浏览器支持的时候,js文件充斥着var和function,由此引来了命名冲突和污染,使得JavaScrip

  • JavaScript 几种循环方式以及模块化的总结

    小小最近学习到了js的几种循环方式,对这几种循环方式进行总结.以及对模块化的相关知识点进行总结, 循环方式 循环方式分为好几种循环方式,分别是for循环,forEach循环,map循环,for..in循环,for-of循环,jquery的循环. 小小将会依次对这几种循环方式进行介绍. 一般数组遍历循环 这里使用常用的数组遍历方式. 一般来说,常用的数组遍历如下 for (var index = 0; index < myArray.length; index++) { console.log(m

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

    由于 Js 起初定位的原因(刚开始没想到会应用在过于复杂的场景),所以它本身并没有提供模块系统,随着应用的复杂化,模块化成为了一个必须解决的问题.本着菲麦深入原理的原则,很有必要来揭开模块化的面纱 一.模块化需要解决的问题 要对一个东西进行深入的剖析,有必要带着目的去看.模块化所要解决的问题可以用一句话概括 在没有全局污染的情况下,更好的组织项目代码 举一个简单的栗子,我们现在有如下的代码: function doSomething () { const a = 10; const b = 11

  • 详解Js模块化的作用原理和方案

    一.模块化概念 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起:块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信. 二.模块化作用 为什么要用模块化的JavaScript? 因为在实际的开发过程中,经常会遇到变量.函数.对象等名字的冲突,这样就容易造成冲突,还会造成全局变量被污染: 同时,程序复杂时需要写很多代码,而且还要引入很多类库,这样稍微不注意就容易造成文件依赖混乱: 为了解决上面的的问题,我们才开始使用模块化的js,所以说模块

  • javascript 构建模块化开发过程解析

    在使用 sea.js .require.js . angular 的时候. 我们使用到 define . module(require) 的方式,定义模块,和依赖模块 下面给出 define 和 module 的简单实现. 其本质,就是往一个对象上面添加方法 var F = {}; F.define = function(str,fn){ var parts = str.split("."); var parent = this; // parent 当前模块的父模块 var i =

  • 如何正确理解javascript的模块化

    模块化在项目中十分的重要,一个复杂的项目肯定有很多相似的功能模块,如果每次都需要重新编写模块肯定既费时又耗力.但是引用别人编写模块的前提是要有统一的"打开姿势",如果每个人有各自的写法,那么肯定会乱套,下面介绍几种JS的模块化的规范. 一:模块化进程一:script标签 这是最原始的 JavaScript 文件加载方式,如果把每一个文件看做是一个模块,那么他们的接口通常是暴露在全局作用域下,也就是定义在 window 对象中,不同模块的接口调用都是一个作用域中,一些复杂的框架,会使用命

  • 浅谈JS前端模块化的几种规范

    前言 有这样一个场景,客户端运行很久,但是法务部和数据部需要收集用户的一些信息,这些信息收集好之后需要进行相应的数据处理,之后上报到服务端.客户端提供一个纯粹的js执行引擎,不需要 WebView 容器.iOS 端有成熟的JavaScriptCore.Android 可以使用 V8 引擎.这样一个引擎配套有一个 SDK,访问 Native 的基础能力和数据运算能力,可以看成是一个阉割版的 Hybrid SDK 额外增加了一些数据处理能力. 问题结束了吗?处理逻辑的时候还需要用到2个库:cheer

  • 浅谈JS获取元素的N种方法及其动静态讨论

    实际前端开发工作中,我们经常会遇到要获取某些元素,以达到更新该元素的样式.内容等目的.而文档对象模型 (DOM) 是HTML和XML文档的编程接口,它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容.DOM 将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合,它会将web页面和脚本或程序语言连接起来.也因此,JavaScript可以通过DOM API获取元素节点,方法有如下几种:其中querySelector()和quer

  • 浅谈js图片前端预览之filereader和window.URL.createObjectURL

    浅谈js图片前端预览之filereader和window.URL.createObjectURL //preview img : filereader方式 document.getElementById('imgFile').onchange = function(e){ 5 var ele = document.getElementById('imgFile').files[0]; var fr = new FileReader(); fr.onload = function(ele){ va

  • 浅谈js中的三种继承方式及其优缺点

    第一种,prototype的方式: //父类 function person(){ this.hair = 'black'; this.eye = 'black'; this.skin = 'yellow'; this.view = function(){ return this.hair + ',' + this.eye + ',' + this.skin; } } //子类 function man(){ this.feature = ['beard','strong']; } man.pr

  • 浅谈JS中的三种字符串连接方式及其性能比较

    工作中经常会碰到要把2个或多个字符串连接成一个字符串的问题,在JS中处理这类问题一般有三种方法,这里将它们一一列出顺便也对它们的性能做个具体的比较. 第一种方法 用连接符"+"把要连接的字符串连起来: str="a"; str+="b"; 毫无疑问,这种方法是最便捷快速的,如果只连接100个以下的字符串建议用这种方法最方便. 第二种方法 以数组作为中介用 join 连接字符串: var arr=new Array(); arr.push(a);

  • 浅谈js函数三种定义方式 & 四种调用方式 & 调用顺序

    在Javascript定义一个函数一般有如下三种方式: 函数关键字(function)语句: function fnMethodName(x){alert(x);} 函数字面量(Function Literals): var fnMethodName = function(x){alert(x);} Function()构造函数: var fnMethodName = new Function('x','alert(x);') // 由Function构造函数的参数个数可变.最后一个参数写函数体

  • 浅谈js对象的创建和对6种继承模式的理解和遐想

    JS中总共有六种继承模式,包括原型链.借用构造函数.组合继承.原型式继承寄生式继承和寄生组合式继承.为了便于理解记忆,我遐想了一个过程,对6中模式进行了简单的阐述. 很长的一个故事,姑且起个名字叫女娲造人吧. 创建对象 女娲一个一个的捏人(创建对象),这样太慢,于是设计了一种机器(函数),想造什么样的,告诉他这个人有哪些特点和功能,机器来制造.这就是工厂模式的(使用同一个接口创建对象,回产生大量重复代码,由此发明了一种函数(模具)). 但是机器造人同样也比较麻烦(挖土.和泥.捏眼睛.捏鼻子...

  • 浅谈JS对象添加getter与setter的5种方法

    定义 getter 与 setter 1.通过对象初始化器在创建对象的时候指明(也可以称为通过字面值创建对象时声明) (function () { var o = { a : 7, get b(){return this.a +1;},//通过 get,set的 b,c方法间接性修改 a 属性 set c(x){this.a = x/2} }; console.log(o.a); console.log(o.b); o.c = 50; console.log(o.a); })(); 在 chro

  • 浅谈js的异步执行

    1.Javascript语言的执行环境是"单线程"(single thread): 优点:实现起来比较简单,执行环境相对单纯: 缺点:只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行.常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行. 为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous). 2.&

  • 浅谈JS的二进制家族

    概述 Blob: 前端的一个专门用于支持文件操作的二进制对象 ArrayBuffer:前端的一个通用的二进制缓冲区,类似数组,但在API和特性上却有诸多不同 Buffer:Node.js提供的一个二进制缓冲区,常用来处理I/O操作 Blob 我们首先来介绍Blob,Blob是用来支持文件操作的.简单的说:在JS中,有两个构造函数 File 和 Blob, 而File继承了所有Blob的属性. 所以在我们看来,File对象可以看作一种特殊的Blob对象. 在前端工程中,我们在哪些操作中可以获得Fi

随机推荐