有这样一个场景,客户端运行好久,可是法务部和数据部须要收集用户的一些信息,这些信息收集好以后须要进行相应的数据处理,以后上报到服务端。客户端提供一个纯粹的 JS 执行引擎,不须要 WebView 容器。iOS 端有成熟的 JavaScriptCore、Android 可使用 V8 引擎。这样一个引擎配套有一个 SDK,访问 Native 的基础能力和数据运算能力,能够当作是一个阉割版的 Hybrid SDK 额外增长了一些数据处理能力。前端
问题结束了吗?处理逻辑的时候还须要用到2个库:cheerio 和 sql。由于都是 Node 工程,因此纯粹的 JS 环境是没办法直接执行。因此需求就进行了转变 ———— 将 Node 项目打包成 UMD 规范。这样就能够在纯粹的 JS 环境下运行。接下来的文章就分析下各类规范。其实也就是前端模块化的几种规范。git
随着互联网的飞速发展,前端开发愈来愈复杂。本文将从实际项目中遇到的问题出发,讲述模块化能解决哪些问题,以及以 Sea.js 为例讲解如何进行前端的模块化开发。github
function each(arr) {
// 实现代码
}
function log(str) {
// 实现代码
}
复制代码
并像模像样的将这些代码抽取出来并统一到 util.js
中,在须要使用的地方引入该文件,看起来很棒,团队内的同事很感激我提供了这么便利的工具包。sql
直到团队愈来愈大,开始有人抱怨后端
小杨:我定义了一个 each 方法遍历对象,可是 util.js 中已经存在一个 each 方法,每次都须要改方法名,我只能叫 eachObject 方法。
张三:我定义了一个 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、
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 模块导出的是一个对象(module.exports 属性),该对象只在脚本运行完才会生成。 ES6 的模块机制是 JS 引擎对脚本进行静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用,等到脚本真正执行时,再根据这个只读引用到被加载的模块中取值,
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 是 Sea.JS 推广的过程当中对模块定义的规范化产出。CMD 推崇依赖就近。 CMD 规范尽可能保持简单,并与 CommonJS 规范中的 Module 保持兼容,经过 CMD 规范编写的模块,能够在 NodeJS 中运行。
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(Universal Module Definition)是随着大前端的趋势产生,但愿提供一个先后端跨平台的解决方案(支持 AMD、CMD、CommonJS 模块方式)。
实现原理:
// 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 支持的模块化参数以下图所示:
借助 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 原生规范的优点就又减小了一点。