大前端模块化

打一个通用 UMD 包

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

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

前端模块化开发的价值

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

  1. 恼人的命名冲突

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

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);
  1. 繁琐的文件依赖

继续上述场景,不少状况下都须要开发 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 都是很不错的模块加载器,二者区别以下:

  1. 二者定位有差别。RequireJS 想成为浏览器端的模块加载器,同时也想成为 Rhino / Node 等环境的模块加载器。SeaJS 则专一于 Web 浏览器端,同时经过 Node 扩展的方式能够很方便跑在 Node 服务器端

  2. 二者遵循的标准有差别。RequireJS 遵循的是 AMD(异步模块定义)规范,SeaJS 遵循的是 CMD (通用模块定义)规范。规范的不一样,致使了二者API 的不一样。SeaJS 更简洁优雅,更贴近 CommonJS Modules/1.1 和 Node Modules 规范。

  3. 二者社区理念有差别。RequireJS 在尝试让第三方类库修改自身来支持 RequireJS,目前只有少数社区采纳。SeaJS 不强推,而采用自主封装的方式来“海纳百川”,目前已有较成熟的封装策略。

  4. 二者代码质量有差别。RequireJS 是没有明显的 bug,SeaJS 是明显没有 bug。

  5. 二者对调试等的支持有差别。SeaJS 经过插件,能够实现 Fiddler 中自动映射的功能,还能够实现自动 combo 等功能,很是方便便捷。RequireJS无这方面的支持。

  6. 二者的插件机制有差别。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 支持的模块化参数以下图所示:

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 原生规范的优点就又减小了一点。

相关文章
相关标签/搜索
本站公众号
   欢迎关注本站公众号,获取更多信息