目录前端
@node
掘金-前端模块化
模块化七日谈
部份内容摘自《移动 Web 前端高效开发实践》- iKcamp 著jquery
JavaScript 发展初期,代码简单地堆积在一块儿,只要能顺利地从上往下一次执行便可。但随着网站愈来愈复杂,实现网站功能的 JavaScript 代码也愈来愈庞大,网页愈来愈像桌面程序,不少问题开始暴露出来,好比全局变量冲突、函数命名冲突、依赖关系处理等。webpack
既然模块是要实现某个功能,那么能够把实现功能的一组函数放在同一文件中,像下面这样web
function a1() { // ... } function b2() { // ... }
函数 a1 和 b2 组成一个模块,其余文件先加载该模块,再对函数进行调用。
缺点:容易发生变量命名冲突,“污染”全局变量,模块成员之间没有太多必然的联系。数组
使用命名空间来管理模块,即便用单全局变量的模式。浏览器
var module_special = { _index: 0, a1: function () { // ...... }, b2: function () { // ...... } } // 调用 module_special.a1() module_special.b2()
一般在属性名前加下划线表示该属性为私有属性,不过这只是一种开发规范上的约定,这里实际上该属性仍然向外暴露。那么怎样让私有属性不被暴露呢?那就须要下面的模块化方式。缓存
当即执行函数表达式简称 “IIFE”(Immediately-Invoked Function Expression)前端框架
其可以造成一个独立的做用域,用 IIFE 做为一个 “容器”,“容器” 内部能够访问外部的变量,而外部环境不能访问 “容器” 内部的变量,因此 IIFE 内部定义的变量不会与外部的变量发生冲突。babel
var module_special = (function () { var _index = 0 var a1 = function () { // ...... } var b2 = function () { // ...... } return { a1: a1, b2: b2 } })() // 调用 module_special.a1() module_special.b2()
这种方式既避免了命名冲突,又使得私有变量 _index 不能被外部访问和修改。jQuery 源码大量采用了这种方式。
node.js 应用由模块组成,采用 CommonJS 规范,经过全局方法 require 来加载模块
var http = require('http') // 引入http模块 var server = http.createServer(function (req, res) { // 用http模块提供的方法建立一个服务 res.statusCode = 200 // 返回状态码为200 res.setHeader('Content-Type', 'text/plain') // 指定请求和响应的HTTP内容类型 res.end('Hello World\n') // 返回的数据 }) server.listen(3000, '127.0.0.1', function () { // 监听的端口和主机名 console.log('Server running at http://127.0.0.1:3000') // 服务启动成功后控制台打印信息 })
如何编写一个 CommonJS 规范的模块?这就须要 Module 对象。
node.js 内部提供一个 Module 构建函数,全部模块都是 Module 的实例。每一个模块内部,都有一个 Module 对象,表明当前模块,包含以下属性:
其中 exports 是编写模块的关键,其表示当前模块对外输出的接口。其余文件加载该模块,实际读取的是 module.exports。
// moduleA.js module.exports = function (params) { console.log(params) } // 假设两个文件在同一目录下 var moduleA = require('./moduleA') moduleA() // 为了方便,node.js为每一个模块提供一个exports变量指向module.exports // 那么moduleA也能够这样编写 exports.moduleA = function (params) { console.log(params) }
注意:不能把值直接赋给 exports,由于这样等于切断了 exports 与 module.exports 的联系
总结 CommonJS 模块的特色以下:
AMD 和 CMD 规范由于如今用的比较少了(反正我是没看见过),就简单介绍下
CommonJS 模块采用同步加载,适合服务端却不适合浏览器。AMD 规范支持异步加载模块,规范中定义了一个全局变量 define 函数,描述以下:
define(id?, dependencies?, factory)
第一个参数 id,为字符串类型,表示模块标识,为可选参数。若不存在则模块标识默认定义为在加载器中被请求脚本的标识。若是存在,那么模块标识必须为顶层的或者一个绝对的标识。
第二个参数 dependencies,定义当前所依赖模块的数组。依赖模块必须根据模块的工厂方法优先级执行,而且执行的结果按照依赖数组中的位置顺序以参数的形式传入(定义中模块的)工厂方法中。
第三个参数 factory,为模块初始化时要执行的函数或对象。若是为函数,只被执行一次。若是是对象,此对象应该为模块的输出值。若是工厂方法返回一个值(对象、函数或任意强制类型转换为 true 的值),应该设置为该模块的输出值。
建立一个标准 AMD 模块
define('alpha', ['require', 'exports', 'beta'], function (require, exports, beta) { exports.berb = function () { return beta.verb() // 或者 return require('beta').verb() } })
建立模块标识为 “alpha” 的模块,依赖于内置的 “require” 和 “exports” 模块和外部标识为 “beta” 的模块。require 函数取得模块的引用,从而即便模块没有做为参数定义,也可以被使用。exports 是定义的 alpha 模块的实体,在其上定义的任何属性和方法也就是 alpha 模块的属性和方法。
RequireJS 库可以把 AMD 规范应用到实际浏览器 Web 端的开发中,其主要解决了两个问题:实现 JavaScript 文件的异步加载,避免网页失去响应;管理模块之间的依赖性,便于代码的编写和维护。
// AMD Wrapper define( ['types/Employee'], // 依赖 function(Employee) { // 这个回调会在全部依赖都被加载后才执行 function Programmer() { // do something } Programmer.prototype = new Employee() return Programmer // return Constructor } )
咱们来比较下 CommonJS 和 AMD 的书写风格:
// CommonJS var a = require('./a') // 依赖就近 a.doSomething() var b = require('./b') b.doSomething() // AMD define(['a', 'b'], function (a, b) { // 依赖前置 a.doSomething() b.doSomething() })
CMD 规范全称为 Common Module Definition
CMD 是另外一种 js 模块化方案,它与 AMD 很相似,不一样点在于:AMD 推崇依赖前置、提早执行,CMD 推崇依赖就近、延迟执行。此规范实际上是在 sea.js 推广过程当中产生的。
/** AMD写法 **/ define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { // 等于在最前面声明并初始化了要用到的全部模块 a.doSomething(); if (false) { // 即使没用到某个模块 b,但 b 仍是提早执行了 b.doSomething() } }); /** CMD写法 **/ define(function(require, exports, module) { var a = require('./a'); //在须要时申明 a.doSomething(); if (false) { var b = require('./b'); b.doSomething(); } }); /** sea.js **/ // 定义模块 math.js define(function(require, exports, module) { var $ = require('jquery.js'); var add = function(a,b){ return a+b; } exports.add = add; }); // 加载模块 seajs.use(['math.js'], function(math){ var sum = math.add(1+2); });
ECMAScript5 及以前的版本不支持原生模块化,须要引入 AMD 规范的 RequireJS 或者 AMD 规范的 Seajs 等第三方库来实现。
直到 ECMAScript6 才支持原生模块化,其不但具备 CommonJS 规范和 AMD 规范的优势,并且实现得更加友好,语法较之 CommonJS 更简洁、支持编译时加载(静态加载),循环依赖处理得更好。
ES6 模块功能主要由两个命令构成:export 和 import,export 命令用于规定模块的对外接口,import 命令用于输入其余模块提供的功能。
在 ES6 中,一个模块也是一个独立的文件,具备独立的做用域,经过 export 命令输出内部变量
let name = 'bus' let color = 'green' let weight = '20吨吨吨' export {name, color, weight} // export命令除了输出变量,还能够输出函数或类 export function run() { console.log('Bus is running') }
// 可使用 as 关键字对输出的变量、函数、类重命名 let name = 'bus' let color = 'green' let weight = '20吨吨吨' function run() { console.log('Bus is running') } export { name as busName, color as busColor, weight as busWeight, run as busRun }
import 命令用于导入模块
import { name, color, weight, run } from './car' // 导入一个模块的时候也能够用 as 关键字对模块进行重命名 import {name as busName } from './car' // 经过星号 '*' 总体加载某个文件 import * as car from './car' console.log(car.name) // bus console.log(car.color) // green
从前面的例子能够看出,使用 import 命令加载模块时须要知道变量名或者函数名,或者整个文件,不然没法加载。为了方便,可使用 export default 命令为模块指定默认输出,加载该模块时,可使用 import 命令为其指定任意名字。
// 定义模块 math.js let basicNum = 0 let add = function(a, b) { return a+b } export default { basicNum, add } // 引入 import math from './math' function test() { console.log(math.add(99 + math.basicNum)) }
附:阮一峰《ES6标准入门》
import 命令是静态加载而不是动态加载的,若是 import 命令要取代 require 方法,就要能实现动态加载。
有一个提案:建议引入 import() 函数,完成动态加载,import 命令可以接收什么参数,import() 函数命令就能接受什么参数。
关于上面所说的提案,如今配置 webpack 使用 babel 转译应该能实现了(Vue 的路由懒加载,Webpack 的 splitChunk 都有用到)。
如今前端框架基本上使用 ES6 的模块化语法,node.js 仍然保持 require 导入,二者最主要的区别是:
即下面的条件加载时不可能实现的
if (x === 2) { import MyModual from './myModual' }