什么是组件化、模块化、工程化?
组件化
把重复的代码提取出来合并成为一个个组件,组件最重要的就是复用,位于框架最底层,其他功能都依赖于组件,可供不同功能使用,独立性强。组件化更多关注UI部分,每个组件有独立的HTML、css、js代码。可以根据需要把它放在页面的任意部位,也可以和其他组件一起形成新的组件。一个页面是各个组件的结合,可以根据需要进行组装。
模块化
分属同一功能/业务的代码进行分装,让它成独立的模块,可以独立运行,以页面、功能或其他不同粒度划分程度不同的模块,位于业务框架层,模块间通过接口调用,目的是降低模块间的耦合,由之前的主应用与模块耦合,变为主应用与接口耦合,接口与模块耦合。侧重功能的封装,主要是针对Js代码,隔离、组织复制的js代码,将它封装成一个个具有特定功能的的模块。模块可以通过传递参数的不同修改这个功能的的相关配置,每个模块都是一个单独的作用域,根据需要调用。一个模块的实现可以依赖其它模块。
工程化
前端工程化不是具体的某项技术和方法,只要我们引入的方法、技术方案、工具可以提升开发效率、提高前端应用质量,那么都属于前端工程化。前端工程化就是通过一系列的工具、方法、工程化的思维,将成千上万个模块、组件或其他静态资源进行有序、规范、标准化、可控、可追踪的组织起来,作为一个整体运行,以便提高前端工程的性能、稳定性、可用性、可维护性等。
模块化规范
ES6
任何模块化,都必须考虑的两个问题就是导入依赖和导出接口。ES6 Module 也是如此,模块功能主要由两个命令构成:export 和 import。export 命令用于导出模块的对外接口,import 命令用于导入其他模块导出的内容。
具体语法讲解请参考阮一峰老师的教程,示例如下:
// a.js
export const name = 'morrain'
const age = 18
export function getAge () {
return age
}
//等价于
const name = 'morrain'
const age = 18
function getAge (){
return age
}
export {
name,
getAge
}
使用 export 命令定义了模块的对外接口以后,其他 JavaScript 文件就可以通过 import 命令加载这个模块。
// b.js
import { name, getAge } from 'a.js'
export const name = 'lilei'
console.log(name) // 'morrain'
const age = getAge()
console.log(age) // 18
// 等价于
import * as a from 'a.js'
export const name = 'lilei'
console.log(a.name) // 'morrin'
const age = a.getAge()
console.log(age) // 18
除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。
从上面的例子可以看到,使用 import 命令的时候,用户需要知道所要导入的变量名,这有时候比较麻烦,于是 ES6 Module 规定了一种方便的用法,使用 export default 命令,为模块指定默认输出。
// a.js
const name = 'morrain'
const age = 18
function getAge () {
return age
}
export default {
name,
getAge
}
// b.js
import a from 'a.js'
console.log(a.name) // 'morrin'
const age = a.getAge()
console.log(age) // 18
显然,一个模块只能有一个默认输出,因此 export default 命令只能使用一次。同时可以看到,这时 import 命令后面,不需要再使用大括号了。
除了基础的语法外,还有 as 的用法、export 和 import 复合写法、export * from 'a'、import()动态加载 等内容,可以自行学习。
前面提到的 Node.js 已经默认支持 ES6 Module ,浏览器也已经全面支持 ES6 Module。至于 Node.js 和 浏览器 如何使用 ES6 Module,可以自行学习。
commonJS
NodeJS是使用CommonJS的模块化规范。CommonJS它有四个核心的比较重要的环境变量,分别是modele, exports,require,global.
根据commonJS规范,一个单独的文件是一个模块,每一个模块都是一个单独的作用域,也就是说,在该模块内部定义的变量、函数、类,都是私有的,对其他文件不可见,无法被其他模块读取,除非为global对象的属性。
定义模块:
module.exports = {}
加载模块:
var xxname = require('./xx.js')
modele, exports
module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口,不建议直接使用exports。然后我们在其他模块需要引入的地方使用require来引入。
modules对象都有哪些属性:
- module.id,模块的识别符,通常是带有绝对路径的模块文件名;
- module.filename,模块的文件名,带有绝对路径;
- module.loaded,返回一个boolean值,表示模块是否已经完成加载;
- module.parent,返回一个对象,表示调用该模块的模块;
- module.children,返回一个数组,表示该模块内用到的其他模块;
- module.exports,表示模块对外输出的值;
require
require就是加载模块的。所有加载的模块都会缓存保存在require.cache中。
使用require加载模块的时候,必须加 ./ 路径,不加的话只会去node_modules文件找。
// 引用自定义的模块时,参数包含路径,可省略.js var math = require('./math');
// 引用核心模块时,不需要带路径 var http = require('http');
这里了解下require使用时候的内部处理流程:
1.在执行到require语句的时候,先检查是否存在这个模块的缓存;
2.如果没找到缓存,那么就会创建一个新的module实例,并且缓存下exports导出的值。如果没发现exports的模块那么会报错;
3.如果缓存存在的话,执行module.load()这个方法,去加载这个模块,读取文件内容后,使用module.compile()执行文件代码;
4.如果解析的过程中,出现异常,就从缓存中删除这个模块;
5.如果没有出现异常,最后返回这个模块的module.exports;
require 命令的基本功能是,读入并执行一个 js 文件,然后返回该模块的 exports 对象。如果没有发现指定模块,会报错。
第一次加载某个模块时,Node.js 会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的 module.exports 属性返回了。
// a.js
var name = 'morrain'
var age = 18
exports.name = name
exports.getAge = function(){
return age
}
// b.js
var a = require('a.js')
console.log(a.name) // 'morrain'
a.name = 'rename'
var b = require('a.js')
console.log(b.name) // 'rename'
如上所示,第二次 require 模块A时,并没有重新加载并执行模块A。而是直接返回了第一次 require 时的结果,也就是模块A的 module.exports。
还一点需要注意,CommonJS 模块的加载机制是,require 的是被导出的值的拷贝。也就是说,一旦导出一个值,模块内部的变化就影响不到这个值 。
// a.js
var name = 'morrain'
var age = 18
exports.name = name
exports.age = age
exports.setAge = function(a){
age = a
}
// b.js
var a = require('a.js')
console.log(a.age) // 18
a.setAge(19)
console.log(a.age) // 18
global
global对象是nodejs的全局对象,上边挂在了一些最基本的全局方法。
CommonJS模块的缓存
从require引入我们使用了缓存。当我们第一次加载模块的时候,node会加载他并且缓存下来,之后使用的时候就会直接从缓存内读取module.exports的值。缓存是根据绝对路径识别模块的,如果同样的模块名,但是保存在不同的路径,require命令还是会重新加载该模块。所以加载模块只会在第一次,后面如果需要重新加载可以通过清除缓存。
CommonJS的缺点
首先我们需要了解CommonJS的加载方式是同步加载的,这意味着只有前面执行完成才会继续执行。因为同步就会存在一个问题,加载的速度受到影响。Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。
AMD:Asynchronous Module Definition 中文名是一步模块
它是一个在浏览器端模块化开发的规范,由于不是js原生支持,使用AMD规范进行页面开发需要用到对应的函数库,也就是大名鼎鼎的RequireJS,实际上AMD是RequireJS在推广过程中对模块定义的规范化的产出。
requireJS主要解决两个问题:
- 多个js文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器。
- js加载的时候浏览器会停止页面渲染,加载文件愈多,页面失去响应的时间愈长。
//定义模块
define(['dependency'],function(){
var name = 'Byron';
function printName(){
console.log(name);
}
return {
printName:printName
}
})
//加载模块
require(['myModule'],function(my){
my.printName();
})
语法:
requireJS定义了一个函数define,它是全局变量,用来定义模块。
define(id,dependencies,factory)
- id 可选参数,用来定义模块的标识,如果没有提供该参数,脚本文件名(去掉拓展名)
- dependencies 是一个当前模块用来的模块名称数组
- factory 工厂方法,模块初始化要执行的函数或对象,如果为函数,它应该只被执行一次,如果是对象,此对象应该为模块的输出值。
在页面上使用require函数加载模块;
require([dependencies], function(){});
- require()函数接受两个参数:
- 第一个参数是一个数组,表示所依赖的模块;
- 第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块
AMD推崇的是依赖前置,被提前罗列出来并会背提前下载并执行,后来做了改进,可以不用罗列依赖模块,允许在回调函数中就近使用require引入并下载执行模块。
CMD:即common module definition
就像AMD有个requireJS,CMD有个浏览器实现的sea.js,sj要解决的问题和rj一样,只不过在模块定义方式和模块加载时机上有所不同。
cmd是sea.js在推广过程中的规范化产出,sea.js是另一种前端模块化工具,它的出现缓解了requireJS的几个痛点。
define(id, deps, factory)
因为CMD推崇一个文件一个模块,所以经常就用文件名作为模块id; CMD推崇依赖就近,所以一般不在define的参数中写依赖,而是在factory中写。
factory有三个参数: function(require, exports, module){}
- require require 是 factory 函数的第一个参数,require 是一个方法,接受 模块标识 作为唯一参数,用来获取其他模块提供的接口;
- exports exports 是一个对象,用来向外提供模块接口;
- module module 是一个对象,上面存储了与当前模块相关联的一些属性和方法。
demo
// 定义模块 myModule.js
define(function(require, exports, module) {
var $ = require('jquery.js')
$('div').addClass('active');
});
// 加载模块
seajs.use(['myModule.js'], function(my){
});
AMD与CMD区别
总结如下:
最明显的区别就是在模块定义时对依赖的处理不同。
AMD推崇依赖前置 在定义模块的时候就有声明其依赖的模块
CMD推崇就近依赖 只有在用到某模块的时候再去require
AMD和CMD最大的区别是对依赖模块的执行时机处理不同,注意不是加载的时机或者方式不同。
为什么我们说两个的区别是依赖模块执行时机不同,为什么很多人认为ADM是异步的,CMD是同步的(除了名字的原因。。。)
同样都是异步加载模块,AMD在加载模块完成后就会执行改模块,所有模块都加载执行完后会进入require的回调函数,执行主逻辑,这样的效果就是依赖模块的执行顺序和书写顺序不一定一致,看网络速度,哪个先下载下来,哪个先执行,但是主逻辑一定在所有依赖加载完成后才执行。
CMD加载完某个依赖模块后并不执行,只是下载而已,在所有依赖模块加载完成后进入主逻辑,遇到require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是完全一致的。
这也是很多人说AMD用户体验好,因为没有延迟,依赖模块提前执行了,CMD性能好,因为只有用户需要的时候才执行的原因。
————————————————