前端模块化的发展
whilefor opened this issue · 0 comments
很多文章中,作者在直接介绍主题之前,都会介绍该主题一些的发展历史做一些考古工作,这在一定程度会加强我们对该主题的印象,并且可以从一定方面上以史为鉴,从历史预测未来。从之前的做法除却缺点、提取优点、归纳总结,一定程度的向前预测,新的设计便会越来越好。
历史的足迹
1995 年,从浏览器刚刚出现不久开始,JavaScript 的设计初衷就是为了网页可以更加的动态和用户有些许交互,从此奠定了前端的基础,就是负责人机交互的领域,让数字世界的内容可以更好的与人交互,实现更好的用户体验,这是前端的核心价值,至此未变。
很快,Server-side JavaScript 发布,可以运行在 Web Server 上,包括 Netscape Enterprise Server、 IIS web-server(JScript)等。
2009年1月29日,Kevin Dangoor 发布了一篇文章 《What Server Side JavaScript needs》,并在 Google Groups 中创建了一个 ServerJS 小组,旨在构建更好的 JavaScript 生态系统,包括服务器端、浏览器端,而后更名为 CommonJS 小组。
CommonJS 社区产生了许多模块化的规范 Modules ,大牛云集,各显神通,不同**的碰撞和斗争,产生了许多浏览器端的模块化加载库,如 RequireJS、Sea.js、Browserify 等。
Node.js 的发布已是 2009 年末,基于 CommonJS 社区最初的主流规范实现模块化,但是之后赢得了 Server-side JavaScript 战争的 Node.js 更加看重实际开发者的声音而不是 CommonJS 社区许多腐朽化的规范,虽然大体上的使用方法未变,但之后 Node.js modules 发展其实于 CommonJS 已分道扬镳。
随着2015年6月,ECMAScript 对 ES6 Modules 的正式发布,浏览器厂商和 Node.js 随之纷纷跟进实现,市面上的模块化加载库随之暗淡失色,间接给 CommonJS 社区判了死刑。在浏览器端取而代之流行的做法的是大家都使用 ES6 Modules 写法,然后使用 Babel 等的 transpiler 来应对不同浏览器版本的支持程度和在浏览器端异步特性产生的一些待解决的问题。Node.js 的模块还是大量的采用 CommonJS 模式,随着对 ES6 Modules 的支持力度的提高和可以兼容之前 CommonJS 模块,CommonJS 写法过渡到 ES6 Modules 只是时间的问题。
模块化的目的
前端模块化,默认聊的就是 JavaScript 模块化,从一开始定位为简单的网页脚本语言,到如今可以开发复杂交互的前端,模块化的发展自然而然,目的无非是为了代码的可组织重用性、隔离性、可维护性、版本管理、依赖管理等。
前端模块跑在浏览器端,异步的加载 JavaScript 脚本,使模块化的考虑需要比后端从直接可以快速的本地加载模块的实现需要考虑的更多。
模块化的发展阶段
JavaScript 模块化的发展,这里根据一些特征,划分为三个阶段。以阶段二 CommonJS 的出现最为开创性的代表,引领多种规范竞争,利于发展,最终标准化。
阶段一:语法层面的约定封装
作为初期阶段,一些简单粗暴的约定封装,方式许多,优势劣势各有不同。大多利用 JavaScript 的语言特性和浏览器特性,使用 Script 标签、目录文件的组织、闭包、IIFE、对象模拟命名空间等方法。
一些典型的示例:
// 1. 命名空间
// app.js
var app = {};
// hello.js
app.hello = {
sayHi: function(){
console.log('Hi');
},
sayHello: function(){
console.log('Hello');
}
}
// main.js
app.hello.sayHi();
// 2. 利用IIFE
var hello = (function (module1) {
var module = {};
var names = ['hanmeimei', 'lilei'];
module.sayHi = function () {
console.log(names[0]);
};
module.sayHello = function (lang) {
console.log(names[2]);
};
return module;
}(module1));
// 3. 沙箱模式 (YUI3)
// hello.js
YUI.add('hello', function(Y) {
Y.hello = {
sayHi: function(){
console.log('Hi');
},
sayHello: function(){
console.log('Hello');
}
}
})
// main.js
YUI().use('hello', function(Y){
Y.hello.sayHi();
Y.DOM.doSomeThing();
},'3.0.0',{
requires:['dom']
})
这一阶段,解决了一些问题,但对日渐复杂的前端代码和浏览器异步加载的特性,很多问题并没有解决,有了需求和问题,解决方案就自然而然的被带了出来。
阶段二:规范的制定和预编译
这一阶段的发展,开始了对模块化规范的制定。以 CommonJS 社区为触发点,发展出不同了规范如 CommonJS( Modules/*** )、AMD、CMD、UMD 等和不同的模块加载库如 RequireJS、Sea.js、Browserify 等,这里面的悲欢离合暂且按下不表。
解决了浏览器端 JavaScript 依赖管理、执行顺序等在之前一个阶段未被解决的许多问题被有了一定程度的解决,随着 browserify 和 webpack 工具的出现,让写法上也可以完全和服务端 Node.js 的模块写法一样,通过 AST 转为在浏览器端可运行的代码,虽然多了一层预编译的过程,但对开发来说是很友好的,预编译的过程完全可以由工具自动化。
一些典型示例:
// 1. CommonJS Modules
// hello.js
var hello = {
sayHi: function(){
console.log('Hi');
},
sayHello: function(){
console.log('Hello');
}
}
module.exports.hello = sayHello;
// main.js
var sayHello = require('./hello.js').sayHello;
sayHello();
// 2. AMD
// hello.js
define(function() {
var names = ['hanmeimei', 'lilei'];
return {
sayHi: function(){
console.log(names[0]);
},
sayHello: function(){
console.log(names[1]);
}
};
});
// main.js
define(['./hello'], function(hello) {
hello.sayHello();
});
// 3. CMD
// hello.js
define(function(require, exports, module) {
var names = ['hanmeimei', 'lilei'];
module.exports = {
sayHi: function(){
console.log(names[0]);
},
sayHello: function(){
console.log(names[1]);
}
};
});
// main.js
define(function(require) {
var hello = require('./hello');
hello.sayHi();
});
有了模块化的规范标准,虽然规范写法各不相同,但还是给开发者带来了许多便利,封装出许多模块化的包,在不同的项目之间使用,基础设施搭建愈发完善,一定程度上的竞争关系,不过随时间发展,市场会选择一个最优解。
阶段三:原生语言层面模块化的支持
相比于之前的规范,ECMAScript 标准对原生语言层面提出了声明式语法的模块化规范标准 ES Modules,历经多年,相比之前的模块化的规范,必定取其精华,去其槽粕。各大浏览器对 ES Modules 逐渐的实现,在未实现的浏览器上也可以通过 Babel 等工具预编译来兼容,ES Modules 逐渐在前端成了公认的编写模块化的标准。
在 Node.js 端,虽然最新的 Node.js 版本(v13.0.1)的 ES Modules 还处于 Stability: 1 - Experimental 阶段,需要增加后缀 .mjs ,而且还需要增加 --experimental-modules
参数来开启,不过相信完全稳定的版本也不会很远。而且还一定程度的支持 CommonJS 和 ES Modules 之间互相引用,通过 BaBel 或 Rollup 等工具则完全可以兼容。
示例:
// hello.js
var names = ['hanmeimei', 'lilei'];
export const hello = {
sayHi: function(){
console.log(names[0]);
},
sayHello: function(){
console.log(names[1]);
}
}
// file main.js
import { hello } from "./lib/greeting";
hello.sayHello();
import 命令会提升到文件顶部执行,被 JavaScript 引擎静态分析,并且不能使用在非顶部的作用域里。所以缺少可以在运行时动态加载的方法,之后出来的 import() 题案,大部分浏览器也已支持。
虽然大部分浏览器都已实现,大部分开发人员,还是会使用打包构建工具。除了浏览器的兼容性问题,这也是一个 trade-off 问题。在 HTTP/1.1 下,虽然有 keep-alive 一段时间内不会断开 TCP 连接,但是 HTTP 的开销还是不能忽略,必要时需合并请求、减少开销;HTTP/2.0 的多路复用,在一定程度上可以让开发者直接在浏览器上使用 ES Modules 而不必担心加载的文件过多。二种方法其实都会有一个最优比例,这时配合打包构建工具可以来做到一个最好的平衡优化。
模块化未来展望
JavaScript 的 ES Modules 规范趋向成熟,经过一定程度的竞争和合作,多样化成员的标准规范化对开发者是极度的有利。然而因为 HTML、CSS 其图灵不完全的特性,HTML、CSS 模块化的发展没有那么迅速。
HTML 的历史其实早于 JavaScript ,在开发者社区的反馈之下,出现了 HTML Modules 题案,不是重造一个新模块化的系统,而是集成在现有的 ES6 Modules 中,虽然浏览器厂商还处于公开支持和开发中,相信社区的推动,会很快落地。
让后端开发人员最头疼的 CSS 。因为其特殊性,发展到 CSS3 虽然支持了更多的特性,但总归没有标准规范的模块化。但社区力量是强大的,利用预编译,发展出 less、sass、scss、styled-component 等,像极了 JavaScript 模块化发展的前期阶段。
总结
模块化的完善,更有利于我们更快更好的构建极致前端交互,但这只不过是前端浩大工程中的一部分,我们就活在历史中,以史为鉴,可以知兴替,也需要从更多的角度看问题,不识庐山真面目,只缘生在此山中。