前端模块化的发展

667 阅读4分钟

1. 不得不说的历史

JS本身就是为了满足简单的页面设计: 页面动画 + 表单提交,所以前端最开始并无模块化 or 命名空间的概念。但是随着前端功能的完善,前端能做的事也越来越多,模块化的概念也越来越流行。

幼年期:无模块化

  1. 开始需要在页面中加载不同的JS:动画、组件、格式化
  2. 多种js文件会被分在不同的文件中
  3. 不同的文件又被同一个模板所引用

面试点:script标签的参数 - async & defer

  • 普通 - 解析到立即阻塞,立刻下载执行当前script。
  • defer - 解析到标签开始异步下载,在整个页面解析完成之后再执行。表示该脚本在执行的时候不会改变页面结构。
  • async - 解析到标签开始异步下载,下载完成后开始执行并且阻塞渲染,执行完成之后继续渲染。

优点:文件分离拆分是最基础的模块化(第一步)
缺点:污染全局作用域 => 不利于大型项目的开发以及多人团队的共建

成长期:模块化前夜 - IIFE(语法侧的优化)

作用域的把控

例子: 利用函数的块级作用域 - 隔离区
尝试定义一个最简单的模块

const iifeModule = (() => {
    let count = 0;
    const increase = () => ++count;
    const reset = () => {
        count = 0;
    }
    console.log(count);
    increase();
})();
  • 追问:独立模块本身的额外依赖,如何优化 依赖其他模块的传参型,jquery或者其他很多开源框架的模块加载方案就是这样的
const iifeModule = ((dependencyModule1, dependencyModule2) => {
    let count = 0;
    const increase = () => ++count;
    const reset = () => {
    count = 0;
}
increase();
return {
    increase,
    reset
}
})(dependencyModule1, dependencyModule2);
iifeModule.increate();
iifeModule.increate();

注:以上代码使用了揭示模式,揭示模式的特点是:只返回一个对象,其属性是私有数据和成员的引用。

优点:可以从语法的角度实现模块的封装。
缺点:没有其他更好的动态加载依赖的方法,因此必须手动管理依赖和排序。要添加异步加载和循环依赖非常困难。而且对这样的系统进行静态分析也是一个问题。

成熟期:

CJS - Commonjs

Commonjs规范概述了同步声明依赖的模块定义。这个规范主要用于在服务器端实现模块化代码组织,但也可用于定义在浏览器中使用的模块依赖。Commonjs模块语法不能再浏览器中直接使用,但是可以在Node.js中使用。

特征:

  • 通过module + exports 去对外暴露接口
  • 通过require去引入外部模块
// main.js
let count = 0;
const increase = () => ++count;
const reset = () => {
    count = 0;
}
console.log(count);
increase();
//导出方法1
exports.increase = increase;
exports.reset = increase;
//导出方法2
module.exports = {
    increase, reset
}
// 导入:
const { increase, reset } = require('./main.js')

复合使用

(function(thisValue, exports, require, module) {
    const dependencyModule1 = require('./dependencyModule1');
    const dependencyModule2 = require('./dependencyModule2');
    // 业务逻辑……
}).call(thisValue, exports, require, module);
// 一些开源项目为何要把全局、指针以及框架本身引用作为参数
(function(window, $, undefined) {
    const _show = function() {
    $("#app").val("hi zhaowa");
    }
    window.webShow = _show;
})(window, jQuery);
// 阻断思路
// 一句话,使全局变量以参数形式变成自执行函数内部的局部变量。

// 至于为什么这么做,提高程序效率。为什么能提高效率,得从javascript的机制说起,所谓的scope chain作用域链,在当前作用域中如果没有该属性(局部变量)则向上一层作用域中寻找,一直到最上层,也就是window。也就是说全局变量和下级作用域都是window的一个属性,向下依此类推。

// jQuery传入后将参数写成$可以保证在此函数内$为jquery而不是其他类似使用$符号的库。

// undefined同理,由于没有传入第三个参数,自然就是undefined。由于javascript中undefined是一个变量,可以被改变,所以这样可以保证undefined判断时的准确性。有时判断时使用typeof xxx === 'undefined'也是因为这个原因

优点:CommonJs率先在服务端实现了,从框架层面解决了依赖、全局变量污染的问题

缺点:CJs是针对了服务端的解决方案。异步拉取依赖处理不是很完美

AMD规范

Conmmonjs是以服务器端为目标环境,能够一次性把所有模块都加载到内存,而异步模块(AMD Asynchronous Module Definitioin)定义的模块定义系统则以浏览器为目标执行环境,这需要考虑网络延迟的问题。AMD的一般策略是让模块声明自己的依赖,而运行在浏览器中的模块系统会按需获取依赖,并在依赖加载完成后立即执行依赖他们的模块。

新增定义方式:

// define来定义模块
define(id, [depends], callback);
// require进行加载
require([module], callback);

模块定义地方

define('amdModule', ['dependencyModule1', 'dependencyModule2'], (dependencyModule1, dependencyModule2) => {
    // 业务逻辑……
})

引入的地方

require(['amdModule'], amdModule => {
    amdModule.increase();
})

**面试题: 如果在AMDModule中想兼容已有代码,怎么办?

AMD也支持require和exports对象,通过他们可以在AMD模块工厂函数内部定义CommonJS风格的模块。这样可以像请求模块一样请求他们,但AMD加载器会将他们识别为原生AMD结构不是模块定义

define('amdModule', ['require','exports'], (require,exports) => {
    const dependencyModule1 = require('./dependencyModule1');
    exports.stuff = dependencyModule1.doStuff()
})

**面试题:手写兼容CJS&AMD

// 判断关键step1. object还是function step2. exports? step3. define
(define('amdModule'), [], (require, export, module) => {
    const dependencyModule1 = require('./dependencyModule1');
    const dependencyModule2 = require('./dependencyModule2');
    let count = 0;
    const increase = () => ++count;
    const reset = () => {
        count = 0;
    }
    export.increase = increase();
})(

// 目标:一次性区分CJS还是AMD
typeof module === "object"
&& module.exports
&& typeof define !== "function"
? // 是CJS
factory => module.exports = factory(require, exports, module)
: // AMD
define
)

优点:适合在浏览器中加载异步模块的方案 缺点:引入成本

CMD

按需加载。主要应用框架: sea.js


define('module', (require, exports, module) => {
let $ = require('jquery');
// jquery相关逻辑
let dependencyModule1 = require('./dependencyModule1');
// dependencyModule1相关逻辑
})

优点: 按需加载,依赖就近
缺点:依赖打包,加载逻辑存在于每个模块中,扩大了模块体积,同时功能上依赖编译

ES6模块化 :

ES6最大的一个改进就是引入了模块规范。这个规范全方位简化了之前出现的模块加载器,原生浏览器支持意味着加载器及其他预处理都不在必要。从很多方面看,ES6模块系统是集AMD和CMD之大成者。

新增定义:

引入:import
导出:export

模块在引入、导出和定义的地方:

// 引入区域
import dependencyModule1 from './dependencyModule1';
import dependencyModule2 from './dependencyModule2';
// 实现业务逻辑……
// 导出
export const increase = () => ++count;
export const reset = () => {
    count = 0;
}
export default {
    increase, reset
}

** 面试:

  1. 性能 - 按需加载
  2. 动态模块

ES11原生解决方案

import('./esModule.js').then(dynamicModule => {
    dynamicModule.increase();
})

优点(重要性):通过一种最终统一各端的形态,整合了js模块化的通用方案 缺点:本质上还是运行时的依赖分析

解决模块化的新思路 - 前端工程化

遗留的根本问题 - 运行时进行依赖分析

解决方案 - 线下执行

  1. 构建时生成配置,运行时去运行
  2. 最终转化成可执行的依赖处理
  3. 可以拓展

完全体 webpack为核心的前端工程化 + mvvm框架的组件化 + 设计模式