前言
?
JavaScript 模块的演化历史一定程度上代表了前端的发展史. 从早期的 对象字面量, IIFE 到后来的 commonjs, AMD 等, 再到如今的 ES Module. 这些模块化方案在互联网技术发展需求下不断革新, 演进.
本文从四个阶段来讲述 JS 模块化的发展历程, 旨在让大家了解 JS 模块化是如何发展到今天, 每个模块方案在当时又解决了什么问题.
认知革命
?
JavaScript 早期诞生的目的用于客户端验证表单, 提高用户体验. 站在今天的解决方案角度去回顾, 在那个无样式, 无交互的, 简单的不能再简单的 web 页面, 很难想 JS 的模块化意义在哪里.
如果非要达到一定程度代码复用, 对象字面量完全可以满足 Web 互联网早期的需求.
- ?
- //Person.JS
- var Person = {
- say: function(words) {
- //code
- },
- run: function() {
- //code
- }
- };
- Person.say('say somthing');
- Person.run();
历史总会进步, 互联网上 Web 页面越来越多样化, 好在人们会不断的根据变化的需求调整模块化的方式.
?
当团队相互合作, 去完成某一个项目时, 对象字面量缺点就一览无遗. 命名冲突, 作用域隔离等问题就不可避免的会发生, 只是一个时间早与晚的问题.
?
JavaScript 函数, 拥有者天然的局部作用域, 外界是访问不到函数内部的作用域. 自然而然过渡到 IIFE 模块化.
- ?
- (function(global){
- var Person = global.Person || {};
- var pritiveFn = function(){
- //other code
- };
- var pritiveName = 'Tom';
- Person.say = function(words) {
- pritiveFn();
- console.log( pritiveName + 'say:' + words);
- //other code
- }
- Person.run = function() {
- pritiveFn();
- //other code
- }
- })(Windows);
- Person.say();
- Person.run();
这种模式, 能任意定义不会被外界访问到局部变量, 也不会污染全局作用域, 同时还能访问全局中的一些变量. 通过传参命名空间, 可将模块挂在到全局 Person 命名空间上.
IIEF 的模块化方式, 早已 *** 到前端开发的基因. 直到今天, 在我们的日常开发中, 都能见到或用到这种方式.
农业革命
Web2.0 时代的到来, 网站应用更加注重用户与服务的双向交互, 前端开发也逐渐承担更多的责任. 一个网站, 可能有成百上千的页面, 而且, javascrpt 不局限于客户端.
commonjs
推崇 commonjs 模块化规范的 Node.JS , 将模块化推向了一个新的高度.
- // path/ModuleA.JS
- var ModuleA = function(){
- //code
- }
- module.exports = ModuleA;
- //-------------------------
- // path/ModuleB.JS
- var ModuleB = function(){
- //code
- }
- module.exports = ModuleB;
- //------------------------
- // path/index.JS
- var ModuleA = require('./path/ModuleA');
- var ModuleB = require('./path/ModuleB');
- ModuleA();
- ModuleB();
commonjs 规范提供 module.exports(或者 exports)接口用于对外暴露模块. require 加载模块.
仔细想想, 日常开发中我们理所应当只关心模块的自由导出和加载. 而加载速度, 依赖顺序, 作用域隔离等问题应该交给框架或者其他科学技术来系统解决, 让我们无感知.
但, Node.JS 毕竟是运行在服务端的 JavaScript.
Node.JS 中每个文件具有独立的作用域, 所以每个文件可认为是一个模块. 除非你显示的定义在全局 global 对象上, 否则其他文件是访问不到该作用域的定义的任何数据.
在 Node.JS 中, 一个 JS 文件拥有访问其他模块 (文件) 能力, 这就很好的解决模块间相互依赖的问题. 并且所有文件都是在服务器本地加载, 速度极快.
但浏览器客户端的现状是残酷的. 看下面例子, 如果某个页面依赖 Slider, Dialog, Tab 模块, 而这三个模块又有一些自身的依赖.
- <!-- 模块自身的依赖 -->
- <script src="./util/Animation.js"></script>
- <script src="./util/Mask.js"></script>
- <!-- 模块依赖 -->
- <script src="./Slider/index.js"></script>
- <script src="./Dialog/index.js"></script>
- <script src="./Tab/index.js"></script>
- <script>
- Slider();
- Dialog();
- Tab();
- </script>
上面的例子可以看出:
全局作用域被污染.
开发人员必须手动解决模块依赖关系(顺序).
同步远程加载过多的文件, 也会造成严重的页面性能问题.
在大型, 多人合作项目中, 会导致整体架构混乱.
而通过工具 browserify, 可将 commonjs 规范移植到浏览器端, 本质上. browserify 是将所有被依赖 commonjs 的模块, 打包到当前业务代码中.
AMD
浏览器中的 JS, 本身并无加载其他文件 (模块) 的接口. 聪明的人们用动态创建 script 节点实现了动态加载模块.
AMD, 异步模块定义, 采用的是异步加载模块方式. 依赖模块是异步加载, 不会阻塞页面的渲染.
AMD 规范中最核心的接口是 define 和 require, 顾名思义: 定义和加载模块.
其中以 RequireJS 代表, 是 AMD 规范的实现.
- // 定义模块
- define(['path/util/Animation'], function(Animation){
- // Slider code
- return Slider;
- });
- // 加载执行模块
- require(['path/Slider'], function(Slider){
- Slider();
- })
可以看出, 接口的第一个参数, 代表模块的依赖路径. 模块或业务的代码, 放在 callback 中, 其中 callback 参数提供暴露出了各依赖模块的接口.
UMD
此时, 模块规范分成了 commonjs 和 AMD 两大阵营. 天下大势分久必合, 需要一种解决方案同时兼容这两种规范. 而 UMD 规范的诞生就是解决该问题.
- (function (root, factory) {
- if (typeof define === 'function' && define.amd) {
- // AMD 规范
- define([], factory);
- } else if (typeof exports === 'object') {
- // commonjs 规范
- module.exports = factory();
- } else {
- // 挂载到全局
- root.globalVar = factory();
- }
- }(this, function () {
- return {};
- }));
从上面可以看出 UMD 是通过判断运行环境中是否存在各模块化的接口来实现的.
ES2015 Module
不管是 commonjs, AMD, UMD, 都是毕竟是为了弥补 JavaScript 模块缺陷而衍生出的民间解决方案. 2015 年 es6 的发布, 让 JavaScript 终于在语言层面上实现了模块化.
- ?
- // path/ModuleA.JS
- var ModuleA = function(){
- //code
- }
- exports default ModuleA;
- //-------------------------
- // path/ModuleB.JS
- var ModuleB = function(){
- //code
- }
- exports default ModuleB;
- //------------------------
- // path/index.JS
- import ModuleA from './path/ModuleA';
- import ModuleB from './path/ModuleB';
- ModuleA();
- ModuleB();
commonjs 已经发展很成熟, 也能满足日常需求. 初略看, es module "就像是语法糖", 我们为何还要去使用它呢, 换句话说, 我们是用它能为我们带来哪些收益?
不管是 commonjs, AMD, 他们的模块架构是 "动态架构", 换句话说, 模块依赖是在程序运行时才能确定. 而 es module 是 "静态架构", 也就是模块依赖在代码编译时就获取到. 所以在 commonjs 里能进行 "动态引入" 模块.
- if ( Math.random()> 0.5 ) {
- require('./ModuleA');
- } else {
- require('./ModuleB');
- }
而在 es module 中是无法进行类似操作的. 从这个角度来看, es6 module 灵活性还不如 commonjs. 但事物具有两面性. es6 module 其实能为我们带来以下几个收益.
tree shaking
在我们部署项目时, 常常需要将各个模块文件打包成单个文件, 以便浏览器一次性加载所有模块, 减少 reqeust 数量. 因为在 HTTP/1 中, 浏览器 request 并发数量有限制. 不过随之带来的问题是, 多个模块打包成单文件, 会造成文件 size 过大.
如果我们能在编译期时确定好模块依赖, 就可以消除没有用到的模块, 以便达到一定程度的优化, 来看看下面例子.
- // moduleA.JS
- export function moduleX(){
- //some code
- }
- export function moduleY(){
- //some code
- }
- // index.JS
- import { moduleX, moduleY } from './moduleA';
- moduleX();
通过工具 Rollup, 可将 index.JS 打包成如下代码:
- 'use strict';
- function moduleX(){
- //some code
- }
- moduleX();
可以看出, 打包的代码只包含 moduleX, 最大限度的减少了打包文件 size, 这就是所谓的'tree shaking', 读者可以好好品味下这个词, 很传神.
模块变量静态检查
es6 module 由于是 "静态架构", 在编译时就能确定模块的依赖树以及确保模块一定是被正确的 import/export , 这就为项目质量带来很大的保障. 看下面例子:
- // module1.mjs
- export function moduleX(){
- console.log(1);
- }
- // index.mjs
- // 注意: module1.mjs 中并没有 export 出 moduleY
- import { moduleX, moduleY } from './module1.mjs';
- moduleX();
- // 注意
- let randomNum = Math.random();
- if (randomNum)> 0.3 && randomNum <0.4 ) {
- moduleY();
- }
- ?
如果没有静态检查, 在上面代码中的条件判断得出, 代码运行期间, 执行 moduleY() 函数报错的概率是 10%, 这种风险在线上环境就是一个非常大的隐患, 一旦命中条件判断, 你一整年的绩效可能就都没了.
那如果有编译期间静态检查, 会是怎样的结果?
运行 node --experimental-modules index.mjs 命令时, 控制台会报错:
- import { moduleX, moduleY } from './module1.mjs';
- ^^^^^^^
- SyntaxError: The requested module does not provide an export named 'moduleY'
- at ModuleJob._instantiate (internal/loader/ModuleJob.JS:88:21)
- at <anonymous>
这种编译时静态检查对项目的质量把控非常有用.
但 es6 module 有时候也让我很忧伤. 因为它很 "灵活", 所以给我带来了困扰.
来看看 import 语法:
再来看看 export 语法
额, 其实我就想简单的 import/export 而已,"少即使多".
农业革命是前端史的重大进步, 社区各种模块化解决方案以及事实上的标准, 从另一方面也推动着 JavaScript 从语言层面对模块化进行支持. 这为我们架构大型项目, 保证项目质量提供了机会.
三: 工业革命
模块的兼容问题以及重复劳动应该交给工具去做, 我们应该留出更多的时间享受 "美好生活". 所以, 涌现了一大批模块化工具以及周边的模块管理工具. 如 Browserify,r.JS,Webpack,Rollup, jspm, NPM,yarn 等等.
各种工具极大的提高了我们的工作效率, 也我们对模块化有了更多的选择.
快乐同时也带来很多的痛, 就是因为可选择工具太多, 配置太多, 让你深陷其中无法自拔. 要么忙着写 bug, 要么忙着写配置.
结语
科学革命的时代, 还未到来. 也许到那时候, 模块化的使用就像 var m = 1; 语法一样, 它在我们脑海里本应该就是理所当然的存在, 而不需借助其他编译, 运行等工具来实现.
来源: http://www.bubuko.com/infodetail-3301527.html