什么是模块化
模块化,就像是搭积木,一个个模块就是一个个积木,积木搭建起来组成了一个物体。模块组合起来就成了一个系统。
模块化开发方式可以提高代码复用率,方便进行代码的管理。
在前端开发中,通常一个文件我们就可以把它当做一个模块,模块有自己的作用域,通常只向外暴露特定的变量和函数。
模块化规范及发展
全局形式的定义,没有模块化的概念,这样很难团队协同和定义。
function foo(){
//...
}
function bar(){
//...
}
通过命名空间的方式,将内容定义在一个对象下,以此实现与外界作用域的隔离。
var utils = {
foo: function(){},
bar: function(){}
}
utils.foo();
闭包实现
var utils = (function($){
//私有变量
var _private = "hi";
var foo = function(){
console.log(_private, $('.test'))
}
return {
foo: foo
}
})(jQuery)
utils.foo();
utils._private; // undefined
闭包有一个很大的问题:内存泄漏. 所以我们不能太多的使用闭包
CommonJS
模块化得到实质性推动得力于 nodejs 的发展,最早模块化规范取得不错实践要追溯到2009年,CommonJS 这时还叫 ServerJS,其对应规范定义为 Modules/1.0。wiki.commonjs.org/wiki/Module…
基本使用
commonjs
统一使用require
关键字来引入模块,以暴露模块的方式挂载到exports
对象引用上
// math.js
exports.add = function(a, b){
return a + b;
}
// main.js
var math = require('./math.js')
console.log(math.add(1, 2)); // 3
对于ES6:使用require
关键字声明一个值,命名导出,且导出语句必须在模块顶级,不能嵌套在一个块中,比如if(命名导出等价于行内命名导出写法)
// 命名导出
function funA() {
console.log('funA');
}
function funB() {
console.log('funB');
}
export {funA, funB}
// 等价于 行内命名导出写法
export function funA() {
console.log('funA');
}
export function funB() {
console.log('funB');
}
不管是 commonjs 还是 ESM 都不约而同的选择了以'文件'为基本单位来划分模块。但是它们还是有区别的:
- CommonJS输出的是一个值的拷贝,ES6输出的是值的引用;
//CommonJS
//A.js
var x = 1
function fun() { x++; }
module.exports = {
x: x,
fun: fun,
};
// test.js
var mod = require('./A.js');
console.log(mod.x); // 1
mod.fun();
console.log(mod.x); // 1
//若是把var x=1 改为:var x={value: 1}
//fun(){x++;} 改为 fun(){x.value++}
//此时打印的内容应该分别为:1 2
//value 是会发生改变的。说明这是 "值的拷贝",只是对于引用类型而言,值指的其实是引用地址。
//ES6
// A.js`
export let x = 3;`
export function fun() {
x++;
}
// test.js`
import { x, fun } from './A.js'
console.log(x); //3
fun()
console.log(x); //4
2. CommonJS是运行时加载,ES6是编译时输出接口;
3. CommonJS的require()
是同步加载模块,ES6的import是异步加载,有独立模块依赖的解析阶段。
同步加载: 加载资源或者模块的过程会阻塞后续代码的执行;
异步加载: 不会阻塞后续代码的执行;
// timeout.js
var EXE_TIME = 2;
(function(second){
var start = +new Date();
while(start + second\*1000 > new Date()){}//循环1000次再跳出
})(EXE_TIME)
console.log("2000ms executed")
module.exports = exports={
EXE_TIME,
};
// main.js
const EXE_TIME=require('./timeout.js');
console.log('EXE_TIME');
输出结果为:2000ms executed \n 2,并且是过了一会儿之后才打印的2
很明显它在运行的时候卡住了,所以我们可以知道它是同步的。
module,exports、module.exports 三者的关系
module.exports 就是指向的一个 {},你可以修改这个对象,以此导出内容 exports 只是 module.exports 的一个指向
写法:
module.exports = {
fn
}
exports.fn = fn
module.exports = exports = {
fn
}
AMD(Async Module Definition)
异步模块定义 ,为浏览器环境设计,RequireJS即为遵循AMD规范的模块化工具,requireJS的基本思想是,通过define方法定义模块化,通过require加载模块。
前面提到,如果模块化导入是同步的,回到第一个例子中:
var math = require('./math.js')
console.log(math.add(1, 2));
第二行math.add(1, 2),在第一行require('math')之后运行,因此必须等math.js加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。因为 require 是同步的。
对服务器端,所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态
因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。 所以,我们需要了解AMD
CommonJS是主要为了JS在后端的表现制定的,他是不适合前端的,而AMD(异步模块定义),主要为前端JS的表现制定规范。
AMD 模块实现的核心是用函数包装模块定义。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。 这样可以防止声明全局变量,并允许加载器库控制何时加载模块
AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:
require([module], callback);
[module]
:一个数组,里面的成员就是要加载的模块;
callback
:加载成功之后的回调函数。
如果将前面的代码改写成AMD形式,就是下面这样:
require(['math'], function (math) {
math.add(2, 3);
});
AMD 处理依赖问题
通过define方法定义模块,但是按照两种情况进行书写。
- 该模块独立存在,不依赖其他模块(可以返回任何值):
define(function (require) {
var dependency1 = require('dependency1'),
dependency2 = require('dependency2');
//do something
return function () {};
});
2. 该模块依赖其他模块时:
//test.js
define(
['require', 'dependency1', 'dependency2'],//依赖
function (require) {
var dependency1 = require('dependency1'),
dependency2 = require('dependency2');
//do something
return function () {};
});
//index.html
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
在AMD中,导入和导出模块的代码,都必须放置在define函数中
define([依赖的模块列表],
function(模块名称列表){
//模块内部的代码
return 导出的内容
})
书写风格对比
这里不得不提一个概念,那就是:依赖就近与依赖前置
依赖前置:(AMD)在定义模块的时候就要声明其依赖的模块;
// AMD recommended style
define(["a", "b"], function(a, b){ // 依赖前置
a.doSomething();
b.doSomething();
})
依赖就近:(CMD)按需加载依赖就近,只有在用到某个模块的时候再去require;
// Module/1.0
var a = require("./a"); // 依赖就近
a.doSomething();
var b = require("./b")
b.doSomething();
依赖就近相较于依赖前置的优势:
- 更好的可维护性和可读性
- 更少的依赖项,类似于 webpack treeshaking 依赖前置必须在函数执行之前就将所有依赖进行自动加载,即使某些依赖未使用
- 更好的灵活性和拓展性 依赖就近能够让开发者更好地更灵活地创建和销毁组件
- 缓存优化
react 开头引入了一大堆组件,这个也算依赖前置吗?
不是!不是!不是!
CMD(Common Module Definition)
公共模块定义规范,sea.js实现了CMD规范
CMD 与 CommonJS 的写法看着非常相近
define(function(require, exports) {
var a = require('./a');//依赖就近
a.doSomething();
//向外部暴露 变量、方法
exports.foo = 'bar';
exports.doSomething = function() {};
});
其实我们可以发现,CMD 对于依赖的处理和 CommonJS 一致,和 AMD 不同。
不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。
ES Module
2015 年,这时 babel 开始流行,不得不说,JavaScript 的发展离不开社区的推动。
关于 exports,262.ecma-international.org/6.0/#sec-ex…
关于 imports,262.ecma-international.org/6.0/#sec-im…
模块打包工具与工程化实践
CommonJS
- Modules/1.x。这个观点觉得 1.x 规范已经够用,只要移植到浏览器端就好。要做的是新增 Modules/Transport 规范,即在浏览器上运行前,先通过转换工具将模块转换为符合 Transport 规范的代码。主流代表是服务端的开发人员。 后来衍生出值得关注的两个实现: component 和 es6 module transpiler。
- Modules/Async。这个观点觉得浏览器有自身的特征,不应该直接用 Modules/1.x 规范。这个观点下的典型代表是 AMD 规范及其实现 RequireJS。
- Modules/2.0。这个观点觉得浏览器有自身的特征,不应该直接用 Modules/1.x 规范,但应该尽可能与 Modules/1.x 规范保持一致。这个观点下的典型代表是 BravoJS 和 FlyScript 的作者。BravoJS 作者对 CommonJS 的社区的贡献很大,这份 Modules/2.0-draft 规范花了很多心思。 FlyScript 的作者提出了 Modules/Wrappings 规范,这规范是 CMD 规范的前身。可最终 BravoJS 和 FlyScript 还是走向了衰落。
Browserify
一个打包工具 browserify.org/ browserify
使用简单的命令行语句来进行打包,非常适用于小规模的web
项目,但是插件支持不足。
在此之前的模块化技术,只是单单纯纯解决了依赖关系的问题,保证能够有序引入,在引入依赖后使用对应依赖。他们是不具备分析依赖关系,然后进行更细致的代码优化和体积压缩
可以看到,b.js重复引用,我们把它公共,命名vendor,至于a和c各自引各自的。
那用Browserify如何打包js
文件:
# cannot debug in the origin js file
browserify a.js -o b.js
# enable debug in the origin js file
browserify a.js -o b.js -d true
browserify
: 命令行工具名称a.js
: 要打包的入口文件-o b.js
: 将打包后的结果输出到b.js
中-d true
: 设置debug
模式为true
,这样能够在IDE
中按照原有文件进行调试
热更新
浏览器的无刷新更新(即webpack里的HMR-hot module replacement模块热替换)
允许在运行时替换,添加,删除各种模块,而无需进行完全刷新重新加载整个页面
目的:加快开发速度,所以只适用于开发环境下使用
Source Map
详情可以看这篇:Source Map 原理
source map 是一个信息文件,里面储存着位置信息。也就是说,转换后代码的每一个位置所对应的转换前的位置。有了它,出错的时候,除错工具将直接显示原始代码,而不是转换后的代码。
在打包完成之后的代码是经过混淆
、压缩
的,不能很好的进行定位。如果想看到准确的代码位置,Source Maps(源映射)
可以通过提供原始代码和转换后代码之间的映射 来解决这个问题。
sourceMap格式
{
"version": 3, // source map 的版本。
"sources": [], // 转换前的文件,该项是一个数组,表示可能存在多个文件合并。
"names": [], // 转换前的所有变量名和属性名。
"mappings": "", // 记录位置信息的字符串。
"file": "", // 转换后的文件名。
"sourcesContent": [""] // 转换前文件的内容,当没有配置 sources 的时候会使用该项。
}
mapping
Source Map 的核心是:如何把两个文件内的位置一一对应。mapping 字段就是来解决这个问题的,它是一个很长的字符串,分为三层:
- 行对应:以分号(;)表示,每个分号对应转换后源码的一行,一个分号前的内容就对应源码的一行。
- 位置对应:以逗号(,)表示,每个逗号对应转换后源码的一个位置,一个逗号前的内容对应源码的一个位置。
- 位置转换:以 VLQ 编码 表示,代表该位置对应的转换前的源码位置。
mapping: "AAAAA,BBBBB;CCCCC"
// 表示转换后的源码分成两行,
//第一行有两个位置,第二行有一个位置。
位置对应原理
每个位置占五位,表示五个字段
- 第一位:表示这个位置在第几列(转换后的代码)。
- 第二位:表示这个位置属于
sources
属性中的哪个文件。 - 第三位:表示这个位置属于第几行(转换前代码)。
- 第四位:表示这个位置属于第几列(转换前代码)。
- 第五位:表示这个位置属于
names
属性的哪一个变量。
注意,所有的值都是以 0 为基数的。其次,第五位不是必须的,如果没有 names 属性,就可以忽略第五位。**每一位都是用 VLQ 编码表示的,由于 VLQ 是可以变长的,所以每一位可以由多个字符构成。 **
eg: 一个位置是 AAAAA 的,在 VLQ 编码中的 A 是 0,所以这个位置的五个位都是 0,代表的意思也就是:该位置在转换后代码的第 0 列,对应 sources 属性中第 0 个文件,属于转换前代码的第 0 列第 0 行,对应 names 属性中的第 0 个变量。
Webpack
代码转化、构建、打包一条龙服务
通常我们在使用 webpack,最容易忽视的一大特性便是对于多样模块规范的兼容,webpack 是支持 CommonJS、AMD、ESModule 等模块化规范的。
看看这篇文章吧:webpack详解
提一下 deno
Module === File === URL
所有模块通过 URL 加载,比如:
import { bar } from "``https://foo.com/bar.ts``"
(绝对 URL)
或
import { bar } from './foo/bar.ts'
(相对 URL)。
因此,Deno 不需要一个中心化的模块储存系统,可以从任何地方加载模块。
但是,Deno 下载模块以后,依然会有一个总的目录,在本地缓存模块,因此可以离线使用。
Deno 原生支持 TypeScript 语言,可直接运行,不必显式转码。
它的内部会判断文件类型,如果是ts
,就先调用 TS 编译器,将其编译成 JavaScript;如果是 js 文件,就直接传入 V8 引擎运行。
React 模块化思想初探
在 React 中实现模块化开发的方式有两种:CommonJS 模块和 ES6 模块。
React 模块化主要需要做以下事情,当然,前端工程的模块化也都是需要考虑如下工作:
- 依赖收集
- 资源处理
- 代码转换
- 优化
- 输出
defer 和 async 的区别
- async 下载它的时候,不必等待它的解析和执行,不管依赖关系。
- defer 不要中断页面渲染,而是等文档解析完之后在执行。
async 不保证执行顺序,通常适合独立脚本使用,比如说 icon.js
defer 等文档解析完后执行,多个脚本依据他们在页面中出现的次序进行执行
应用层的模块化
React 是基于组件化开发,因此组件的封装是 React 模块化的第一步。
在项目构建打包的时候,就需要在代码编译层面去做对应处理。
依赖、公共模块处理(vendor 公共提取)、代码转换压缩、代码优化(切片、异步加载、treeshaking)
Vue 模块化思想初探
Vue 亦是基于组件化开发,组件都是被定义在了统一 .Vue
文件中