导读
说在前面,本文主要是笔记的形式,不会过多深入细节,主要是从大局来认识前端模块化。本人菜狗大三学生一枚,初次发掘金,有错误的地方望指正。
认识模块
模块,在我们前端开发中,可以分为以下两个方面
- 外部模块:指我们在项目中引入的第三方库(package),通常由多个文件组件,但会暴露出入口供我们使用
- 内部模块:我们自己在项目开发中编写的文件,比如
.js、.ts、.vue等等文件 除此之外,还可以分出一个新颖的发展方向,就是source to source的编译和转换,比如框架和一些高级语法等等,这里我们只分析前两个。
外部模块管理
在早期,我们如果在项目中使用某个库,我们会去官网或其他地方把文件下载下来,放到项目文件夹中,然后在根 html 中引入。有了 CDN 后,我们不用下载,可以通过请求去下载,省去了人工这个部分。
但是随着项目越来越庞大,文件夹会越来越臃肿,难以更新维护,各个库难以管理,缺少统一管理的机制。
npm 的出现打破了外部模块的管理方式。npm 是一个远程代码仓库,所有人都可以向仓库提交库,供其他人使用。同时,npm 内置了一套命令行工具,使我们快速下载或上传模块。为了统一,通过 npm 命令下载的库都会被放在 node_modules 下。当我们通过 npm 下载库时,它会自动帮我们生成一个 package.json 文件,这个文件记录了我们下载的模块和对应的版本信息。
这样,外部模块就有了一个统一的机制来管理三方库。
内部模块
书写时,将一个复杂的程序按照一定的规则封装为几个模块;运行时,将其进行组合。对于一个模块来说,数据和实现是私有的,不过可以对外暴露一些接口与外部其他模块进行通信。
在没有出现规范之前,是怎样模拟模块化代码的呢?
用一个个 .js 文件去模拟,然后在根 html 中加载这些文件。如果我们按照功能去划分文件,我们就会在根 html 加载一堆的 js 文件。而且如果有的文件依赖了其他文件的代码,就必须要考虑加载顺序。维护成本太大。
接下来我们介绍几种模拟模块的方式
namespace模式
简单的对象封装
let myModule = {
data : 'Li',
foo() {
console.log('foo()调用');
},
bar() {
console.log('bar()调用');
}
}
myMoudule.data = 'Chen' //能直接修改模拟模块中的数据
- 作用: 减少了全局变量,解决命名冲突
- 缺点:数据不安全(外部可以直接修改模块内部的数据)
IIFE模式
利用闭包给传入的对象添加数据
(function(window) {
let data = 'Li';
function foo() {
console.log('foo()调用');
}
function bar() {
console.log('bar()调用');
}
// 内部私有函数
function other() {
}
// 对外暴露
window.myModule = {foo, bar};
})(window)
- 作用:数据是私有的,外部只能操作模拟模块对外暴露的方法
- 缺点:如果一个模块依赖另一个模块,没办法技术实现
IIFE模式的增强版
增加了传入变量的个数,引入依赖
(function(window, $) {
let data = 'Li';
function foo() {
console.log('foo()调用');
}
function bar() {
console.log('bar()调用');
}
// 内部私有函数
function other() {
$('body').css('background', 'red')
}
// 对外暴露
window.myModule = {foo, bar};
})(window, jQuery)
需要注意的是,依赖的模块必须比当前模块提前引入。
在线处理阶段
所谓在线处理,就是我们要先引入支持模块化的polyfill,确保文件加载完毕后,才能进行模块解析,确定加载顺序和执行顺序,是在线上进行的。本质上有一定的风险。我们列举下早期社区主流的规范。
require.js与AMD规范
我们先来看一下 require.js 的基本使用
定义无依赖的暴露模块
define(function() {
var add = function(a, b) {
return a+b;
}
//对外暴露
return {
add : add
}
})
定义有依赖的暴露模块
define(['module1', 'module2'], function(m1, m2) {
// 这里 m1 代表 module1; m2 代表 module2
var add = function(a, b) {
return a+b;
}
//对外暴露
return {
add : add
}
})
引入模块使用
require(['module1', 'module2'], function(m1, m2){
//使用 m1 或 m2
})
需要引入依赖时,第一个参数代表依赖模块的路径,第二个参数为当前模块的内容。 我们可以设置一些基本的配置
require.config({
baseUrl : 'js/',
paths: {
// 自定义模块
dataService: './service/dataService',
// 配置第三方模块
jquery: './libs/jquery-1.10.1'
}
}
require.js 为全局添加了 define 函数和 require 函数,我们只需要按照这种规定书写即可。对应地,其实就是 AMD 模块化规范。
sea.js与CMD规范
我们先来看一下 sea.js 的基本使用
定义无依赖的暴露模块
define(function(require, exports, module) {
console.log('加载 add 模块');
var add = function(a, b) {
return a+b;
}
module.exports = {
add: add
}
})
定义有依赖的暴露模块
define(function(require, exports, module) {
console.log('加载 add 模块');
// 引入依赖模块 (同步)
var module1 = require('module1');
// 引入依赖模块 (异步)
require.async('module2', function(m2) {
})
var add = function(a, b) {
return a+b;
}
module.exports = {
add: add
}
})
引入模块
define(function(require) {
var m1 = require('module1');
var m2 = require('module2');
m1.add;
m2.total;
})
响应地,这些规定就对应着 CMD 规范。
AMD与CMD的区别
AMD 是依赖前置,js很方便的就知道要加载的是哪个模块了,因为已经在 define 的 dependencies 参数中就定义好了,会立即加载它。
CMD 是依赖就近,需要使用把模块变为字符串解析一遍才知道依赖了那些模块。
AMD 是将需要使用的模块先加载完再执行代码,而 CMD 是在 require 的时候才去加载模块文件,加载完再接着执行。
commonJs
AMD 和 CMD 都是用于浏览器端的模块规范,对于服务端的 Node.js 来说,采用的是 CommonJS 规范。
我们先来看一下基本使用
定义暴露模块
// add.js
var add = function(a, b) {
return a+b;
}
module.exports.add = add;
引入模块
var add = require('add');
console.log(add.add(1, 2));
每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的 module.exports 属性。
commonJs与AMD的区别
commonJs 模块规范中,每个文件就是一个模块,有自己的作用域。
commonJs 规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。
AMD 规范则是非同步加载模块,允许指定回调函数。
由于 Node.js 主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以 commonJs 规范比较适用。
如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用 AMD 规范。此外 AMD 规范比 commonJs 规范在浏览器端实现要来着早。
ES6模块化
ES module 的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。我们先看一下它的基本使用
导出模块
// profile.js
export var name = 'Li';
export var age = 18;
export var sex = 'n';
//还有一种写法
var name = 'Li';
var age = 18;
var sex = '男';
export {
name,
age,
sex
};
引入模块方式
import {name, age, sex} from './profile';
console.log(`我叫${},是一个${age}岁的${sex}孩`)
es module与commonJs的区别
commonJs模块输出的是一个值的拷贝,ES6模块输出的是值的引用commonJs模块是运行时加载,ES6模块是编译时输出接口
第二个差异是因为 CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
对于第一个差异,我们举个例子
counter.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
var mod = require('./counter');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
上述代码我们通过 commonJs 的规范去书写的。counter.js 模块加载以后,它的内部变化就影响不到输出的 mod.counter 了。这是因为 mod.counter 是一个原始类型的值,会被缓存。
对于 es module 模块来说, ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的 import 有点像 Unix 系统的“符号连接”,原始值变了,import 加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
总结
-
commonJs规范主要用于服务端,因为是同步加载模块,并不适合浏览器环境 -
AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅 -
CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM打包,模块的加载逻辑偏重 -
es module在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代commonJs和AMD规范,成为浏览器和服务器通用的模块解决方案
预编译
之前我们说的方案,主要是在线处理,等到用户加载完文件后,再进行模块相关的分析和处理。这样带来明显的问题是
- 延长用户,因为要在线处理模块,如果处理的很慢,网站可能会暂时失去交互行为
- 在加载过程中会发出海量的请求,降低页面性能
于是存在这样一个需求,希望有一个工具,可以在代码部署上线前就把代码模块组织好,并且把代码进行合并,由多个
script代码合并到少数script甚至一个script中,减少http请求次数。
在这种迫切的需求下,一系列模块预处理的工具出现了,其中最出名、活跃最久的当属 webpack。
webpack 是一个将 JavaScript 应用程序打包工具,将模块依赖打包生成静态资源。
我们只需要简单配置下出口和入口,就可以完成基本的需求——打包。
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist')
filename: 'bundle.js'
}
}
然后我们在根 html 引入这个 bundle.js 就好了。
当然这种方式也会存在弊端,最主要的问题就是打包后文件体积过大的问题。这样会导致首屏加载速度减慢。
webpack 通过代码分割 code splitting 来解决这个问题。
code splitting 一共有两种方面的优化
- 业务代码和三方库代码分离加载:业务代码更新快,三方库的更新频率相对较低,分离后可以通过浏览器的缓存机制加载三方库的代码,提升效率
- 按序加载:在代码中通过
import动态加载的资源,比如路由对应的组件,会单独打包,等到对应的触发时刻再去加载,这样就减少了首屏加载的资源量
最后
本文只记录一些模块化相关的知识。对于 webpack 来说,他的作用远不止打包压缩这么简单,以 webpack 为核心,有一套自动化构建的生态,其目的是,更好地区分开发环境和生成环境。关于 webpack 更详细的功能,这里不再讨论(给自己加个 flag,出个 webpack 相关的文章)。