Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

精读 js 模块化发展 #3

Closed
ascoders opened this issue Mar 21, 2017 · 13 comments
Closed

精读 js 模块化发展 #3

ascoders opened this issue Mar 21, 2017 · 13 comments

Comments

@ascoders
Copy link
Owner

ascoders commented Mar 21, 2017

这次是前端精读期刊与大家第一次正式碰面,我们每周会精读并分析若干篇精品好文,试图讨论出结论性观点。没错,我们试图通过观点的碰撞,争做无主观精品好文的意见领袖。

本期精读的文章是:evolutionOfJsModularity

懒得看文章?没关系,稍后会附上文章内容概述,同时,更希望能通过阅读这一期的精读,穿插着深入阅读原文。

如今,Javascript 模块化规范非常方便、自然,但这个新规范仅执行了2年,就在 4 年前,js 的模块化还停留在运行时支持,10 年前,通过后端模版定义、注释定义模块依赖。对经历过来的人来说,历史的模块化方式还停留在脑海中,反而新上手的同学会更快接受现代的模块化规范。

但为什么要了解 Javascript 模块化发展的历史呢?因为凡事都有两面性,了解 Javascript 模块化规范,有利于我们思考出更好的模块化方案,纵观历史,从 1999 年开始,模块化方案最多维持两年,就出现了新的替代方案,比原有的模块化更清晰、强壮,我们不能被现代模块化方式限制住思维,因为现在的 ES2015 模块化方案距离发布也仅仅过了两年。

内容概要

直接定义依赖 (1999)

// file greeting.js
dojo.provide("app.helloWrold") // define module
app.helloWrold.say = function () {
  return 'hello'
};

// file hello.js
dojo.provide("app.hello") // define module
dojo.require('app.helloWrold') // import module

app.hello = function(x) {
  return app.helloWrold.say()
};

可以看出,由于当时 js 文件非常简单,模块化方式非常简单粗暴。

这种定义方式与现在的 commonjs 非常神似,区别是 commonjs 以文件作为模块,而这种方法可以在任何文件中定义模块,模块不与文件关联。

其实现在 Typescript 定义 namespace 的方式,和这种方法特别的像,因为我们可以在一个文件定义多个 namespace:

declare namespace app;
declare namespace user;

这种突兀的变量定义方式(app.hello)不利于项目维护,还需要手动维护模块定义,可用,但不优雅。

闭包模块化模式 (2003)

var helloWorld = (function() {
  return {
    say() {
      return 'hello'
    }
  }
}())

最后括号也可以放外部,这种闭包方式解决了变量污染问题,只需要影响一个全局变量。

但影响一个也是影响,多了还是容易乱套,这种方式没有解决文件引用顺序问题,不同加载顺序可能出现全局变量未赋值的情况(定义倒是提前了,但还是会报错)。

模版依赖定义 (2006)

/*borschik:include:../lib/main.js*/

这时候开始流行后端模版语法,通过后端语法聚合 js 文件,从而实现依赖加载,说实话,现在 go 语言等模版语法也很流行这种方式,写后端代码的时候不觉得,回头看看,还是挂在可维护性上。

注释依赖定义 (2006)

/*! lazy require scripts/hello.js */
hello.say()

几乎和模版依赖定义同时出现,与 1999 年方案不同的,不仅仅是模块定义方式,而是终于以文件为单位定义模块了,通过 lazyjs 加载文件,同时读取文件注释,继续递归加载剩下的文件。

这简直和最新标准 <script type=module> 如出一辙,如今仅仅将这个思想标准化了,将注释定义依赖替换成了 import export 关键词罢了。

挺支持这种方式的,连打包都不需要了,现在终于得到标准的支持,非常激动人心。

外部依赖定义 (2007)

// file deps.json
{
    "files": {
        "main.js": ["helloWorld.js"],
    }
}

这种定义方式在 cocos2d-js 开发中普遍使用,其核心思想是将依赖抽出单独文件定义,这种方式不利于项目管理,毕竟依赖抽到代码之外,我是不是得两头找呢?所以才有通过 webwpack 打包为一个文件的方式暴力替换为 commonjs 的方式出现。

Sandbox模式 (2009)

// file sandbox.js
function Sandbox(callback) {
    var modules = [];

    for (var i in Sandbox.modules) {
        modules.push(i);
    }

    for (var i = 0; i < modules.length; i++) {
        this[modules[i]] = Sandbox.modules[modules[i]]();
    }
    
    callback(this);
}

// file hello.js
Sandbox.modules = Sandbox.modules || {};

Sandbox.modules.helloWorld = function () {
    return 'hello'
};

// file app.js
new Sandbox(function(box) {
    var result = box.helloWorld()
});

这种模块化方式很简单,暴力,源码都能直接写到代码里。硬伤是无法解决明明冲突问题,毕竟都塞到一个 sandbox 对象里,而 Sandbox 对象也需要定义在全局,存在被覆盖的风险。模块化需要保证全局变量尽量干净,目前为止的模块化方案都没有很好的做到这一点。

依赖注入 (2009)

就是大家熟知的 angular1.0,依赖注入的思想现在已广泛运用在 react、vue 等流行框架中。但依赖注入和解决模块化问题还差得远。

CommonJS (2009)

真正解决模块化问题,从 node 端逐渐发力到前端,前端需要使用构建工具模拟。

Amd (2009)

都是同一时期的产物,这个方案主要解决前端动态加载依赖,相比 commonJs,体积更小,按需加载。

Umd (2011)

兼容了 CommonJS 与 Amd,通过下面的代码:

(function(define) {
    define(function () {
        return {
            helloWorld: function () {
                return 'hello'
            }
        };
    });
}(
    typeof module === 'object' && module.exports && typeof define !== 'function' ?
    function (factory) { module.exports = factory(); } :
    define
));

其核心思想是,如果在 commonjs 环境(存在 module.exports,不存在 define),将函数执行结果交给 module.exports 实现 Commonjs,否则用 Amd 环境的 define,实现 Amd。

Labeled Modules (2012)

// file hello.js
exports: var helloWorld = {
    hello: function (lang) {
        return 'hello'
    }
};

// file hello.js
require: './lib/hello';
var phrase = helloWorld.hello()

和 Commonjs 很像了,没什么硬伤,但生不逢时,碰上 Commonjs 与 Amd,那只有被人遗忘的份了。

YModules (2013)

// file hello.js
modules.define('helloWorld', function(provide) {
    provide({
        hello: function () {
            return 'hello'
        }
    });
});

// file app.js
modules.require(['helloWorld'], function(helloWorld) {
    var result = helloWorld.hello()
});

既然都出了 Commonjs Amd,文章还列出了此方案,一定有其独到之处。

其核心思想在于使用 provide 取代 return,可以控制模块结束时机,处理异步结果;拿到第二个参数 module,修改其他模块的定义(虽然很有拓展性,但用在项目里是个搅屎棍)。

ES2015 Modules (2015)

就是我们现在的模块化方案,还没有被浏览器实现:

// file hello.js
export default const helloWorld = () => {
  return 'hello'
}

// file app.js
import helloWorld from './hello'
var result = helloWorld()
@ascoders ascoders mentioned this issue Mar 21, 2017
65 tasks
@ascoders ascoders changed the title 2017.03.24 精选文章池 2017.03.24 可视化 Mar 21, 2017
@ascoders ascoders changed the title 2017.03.24 可视化 2017.03.24 精读 css 模块化 Mar 21, 2017
@ascoders ascoders changed the title 2017.03.24 精读 css 模块化 精读 css 模块化 Mar 21, 2017
@ascoders
Copy link
Owner Author

@ascoders ascoders changed the title 精读 css 模块化 精读 js 模块化发展 Mar 21, 2017
@arcthur
Copy link

arcthur commented Mar 26, 2017

这篇文章所提供的模块化历史的方案都是逻辑模块化,从 CommonJS 方案开始前端把服务端的解决方案搬过来之后,算是看到标准物理与逻辑统一的模块化。但之后前端工程不得不引入模块化构建这一步。正是这一步给前端开发无疑带来了诸多的不便,尤其是现在我们开发过程中经常为了优化这个工具带了很多额外的成本。

再回到 JS 模块化这个主题,开头也说到是为了构建 scope,实则提供了业务规范标准的输入输出的方式。但文章中的 JS 的模块化还不等于前端工程的模块化,Web 界面是由 HTML、CSS 和 JS 三种语言实现,不论是 CommonJS 还是 AMD 包括之后的方案都无法解决 CSS 与 HTML 模块化的问题。

对于 CSS 本身它就是 global scope,因此开发样式可以说是喜忧参半。近几年也涌现把 HTML、CSS 和 JS 合并作模块化的方案,其中 react/css-modules 和 vue 都为人熟知。当然,这一点还是非常依赖于 webpack/rollup 等构建工具,让我们意识到在 browser 端还有很多本质的问题需要推进。

幸运的是,模块化构建将来一定不再需要。随着 HTTP/2 流行起来,请求和响应可以并行,一次连接允许多个请求,对于前端来说宣告不再需要在开发和上线时再做编译这个动作。

@ascoders
Copy link
Owner Author

ascoders commented Mar 26, 2017

正如 @arcthur 所说,原生支持的模块化,解决 html 与 css 模块化问题正是以后的方向。

对于 css 模块化,目前不依赖预编译的方式是 styled-component,通过 js 动态创建 class。而目前 css 也引入了与 js 通信的机制 与 原生变量支持。未来 css 模块化也很可能是运行时的,所以目前比较看好 styled-component 的方向。

对于 html 模块化,小尤最近爆出与 chrome 小组调研 html Modules,如果 html 得到了浏览器,编辑器的模块化支持,未来可能会取代 jsx 成为最强大的模块化、模板语言。

对于 js 模块化,最近出现的 <script type="module"> 方式,虽然还没有得到浏览器原生支持,但也是我比较看好的未来趋势,这样就连 webpack 的拆包都不需要了,直接把源代码传到服务器,配合 http2.0 完美抛开预编译的枷锁。

上述三中方案都不依赖预编译,分别实现了 html、css、js 模块化,相信这就是未来。

@javie007
Copy link

我说下我看后的一些感想:
angular还不只是模块的依赖,他还涉及DI的使用;
一个语言设计之初没考虑到模块化,坑了整整一代人;
这几年TC39对语言终于重视起来了,慢慢有动作了,但针对模块加载器这方面依然慢的要死,很难想象这样的JS是世界上最流行的语言;
node不知道何时才能支持ES2015 Modules,node程序员伤不起;
模块化这部分工作本来不应该让程序员的时间花在这上面;

@arcthur
Copy link

arcthur commented Mar 27, 2017

从 CommonJS 之前其实都只是封装,并没有一套模块化规范,这个就有些像类与包的概念。我在10年左右用的最多的还是 YUI2,YUI2 是用 namespace 来做模块化的,但有很多问题没有解决,比如多版本共存,因此后来 YUI3 出来了。

YUI().use('node', 'event', function (Y) {
    // The Node and Event modules are loaded and ready to use.
    // Your code goes here!
});

YUI3 的 sandbox 像极了差不多同时出现的 AMD 规范,但早期 yahoo 在前端圈的影响力还是很大的,而 requirejs 到 2011 年才诞生,因此圈子不是用着 YUI 要不就自己封装一套 sandbox,内部使用 jQuery。

为什么模块化方案这么晚才成型,我猜是早期应用的复杂度都在后端,前端都是非常简单逻辑。后来 Ajax 火了之后,web app 概念的开始流行,前端的复杂度也呈指数级上涨,到今天几乎和后端接近一个量级。

工程发展到一定阶段,要出现的必然会出现。

@camsong
Copy link
Contributor

camsong commented Mar 28, 2017

几年前,模块化几乎是每个流行库必造的轮子(YUI、Dojo、Angular),大牛们自己爽的同时其实造成了社区的分裂,很难积累。有了 ES2015 Modules 之后,JS 开发者终于可以像 Java 开始者十年前一样使用一致的方式愉快的互相引用模块。不过 ES2015 Modules 也只是解决了开发的问题,由于浏览器的特殊性,还是要经过繁琐打包的过程,等 Import,Export 和 HTTP 2.0 被主流浏览器支持,那时候才是彻底的模块化。JS 进化很快,现在 CSS 的问题就比较突出了,CSS 虽然有 variables 等,但一直都没有添加模块和逻辑表现能力的苗头,再不进化可能就被 JS 取代了。

@jasonslyvia
Copy link
Contributor

看到大家的评论里基本都提到了 HTTP/2,对这项技术解决前端模块化及资源打包等工程问题抱有非常大的期待。很多人也认为 HTTP/2 普及后,基本就没有 Webpack 什么事情了。

不过 Webpack 作者 @sokra 在他的文章 webpack & HTTP/2 里提到了一个新的 Webpack 插件 AggressiveSplittingPlugin。简单的说,这款插件就是为了充分利用 HTTP/2 的文件缓存能力,将你的业务代码自动拆分成若干个数十 KB 的小文件。后续若其中任意一个文件发生变化,可以保证其他的小 chunck 不需要重新下载。

可见,即使不断的有新技术出现,也依然需要配套的工具来将前端工程问题解决方案推向极致。

@ascoders
Copy link
Owner Author

@jasonslyvia 这个我也看过,不过既然为了更有效利用缓存,就更应该将所有文件分散开打包,而不是聚合成一堆堆小 bundle,不过也许让每个 bundle 大小都差不多,而且在一定大小范围内可以更有效利用 http2.0 吧,这样来说就很必要了。

还有压缩、混淆、md5、或者利用 nonce 属性对 script 标签加密,都离不开本地构建工具。

@BlackGanglion
Copy link
Contributor

首先抛开 JavaScript 来看为何要模块化,模块化从软件工程的角度讲是为了让软件具备良好的可维护性与可复用性,降低软件开发成本的同时让软件在业务层面具有灵活性。正如 @arcthur 所说,JavaScript 模块化的发展历程与前端复杂度提升是密切相关的,从纯后端渲染的视图模版到前后端分离的单页应用,对模块化要求越来越高。模块化发展始终围绕两大绊脚石,JavaScript 本身缺陷浏览器环境,随着 ES2015 Modules、HTTP/2等等的真正落地,相信 JavaScript 模块化方案会走向标准与统一。

最后来点补充材料,供大家参考:
JavaScript 模块化七日谈
JavaScript模块化编程简史(2009-2016)

@javie007
Copy link

@ascoders 以前看过文章,说http2太多文件也不行,7,8个bundle就差不多了

@ascoders
Copy link
Owner Author

讨论暂时告一段落,因下周放假三天,提前开始下周周刊的讨论。

@fanhc019
Copy link
Contributor

想分析下 JavaScript 为什么没有模块化,为什么又需要模块化。这个 95 年被设计出来的时候,语言的开发者根本没有想到它会如此的大放异彩,也没有将它设计成一种模块化语言。按照文中的说法,99 年也就是 4 年后开始出现了模块化的需求。如果只有几行代码用模块化是扯,初始的 web 开发业务逻辑都写在 server 端,js 的作用小之又小。而现在 spa 都出现了,几乎所有的渲染逻辑都在前端,如果还是没有模块化的组织,开发过程会越来越难,维护也是更痛苦。

文中已经详细说明了模块化的发展和优劣,这里不准备做过多的讨论。我想说的是,在模块化之后还有一个模块间耦合的问题,如果模块间耦合度大也会降低代码的可重用性或者说复用性。所以也出现了降低耦合的观察者模式或者发布/订阅模式。这对于提升代码重用,复用性和避免单点故障等都很重要。说到这里,还想顺便提一下最近流行起来的响应式编程(RxJS),响应式编程中有一个很核心的概念就是 observable,也就是 Rx 中的流(stream)。它可以被 subscribe,其实也就是观察者设计模式。

@oakland
Copy link

oakland commented Dec 8, 2020

js 模块化发展的历史也是 js 语言发展的历史,常看常新,顺便推一下自己写的模块化历史的一篇文章:https://juejin.cn/post/6844904145166548999

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants