写了十年 JS 却不知道模块化为何物?

2015-11-10 18:03:32 +08:00
 wilddog

作者:肖光宇
野狗科技联合创始人,先后在猫扑、百度、搜狗任职,爱折腾的前端工程师。
野狗官博: https://blog.wilddog.com/
野狗官网: https://www.wilddog.com/
公众订阅号: wilddogbaas

转载请保留以上信息。

模块化这个问题并非一开始就存在, WWW 刚刚问世的时候, html , JavaScript , CSS ( JS 和 CSS 都是后来在网景被引进浏览器的)都是极其简单的存在,不需要模块化。

模块化的需求是规模的产物,当 web page 进化到 web application ,浏览器端处理的逻辑越来越复杂,展现的样式和动画越来多,对于工程的要求也就越来越高。于是模块化的需求也就产生了。模块化的意义:

JavaScript 长久以来被认为是简单的脚本语言,实际上情况早就发生来变化,在最新版的 ECMA-262 ( ES6 )文档中强调 JavaScript 是通用编程语言而不是脚本语言。脚本语言,比如 shell 并不是用来完成复杂功能的,只是用来做一些自动化控制,是不需要模块化的。而用于构建复杂系统通用编程语言(比如 Java )一般都有模块的实现。

1.模块化标准

ES6 之前, JavaScript 并没有原生的模块机制,好在 JavaScript 非常灵活,有很多种写法可以将代码天然隔离,起到模块化的功能:

//define
var modules = {}  
modules.mod1 = {  
  foo : function(){...},
  bar : function(){...}
  ...
}
//call
modules.mod1.foo()

在客户端这种方式基本是够用的,然而问题依然存在:你无法管理依赖,所有的代码都必须 load 到内存中,需要哪些模块必须由人工处理。分模块是工程化的产物,也是自然发展的结果,自然有很多尝试。很显然,模块之间互相依赖需要编写模块的时候遵循一定的规范。现存的规范还真不少,不知道 ES6 能否终结这场混战:

AMD 和 CMD 分别是 requireJS 和 seaJS 定义的标准。使用纯原生的 ES5 语法意味者其只能使用闭包,书写和阅读都很怪异。值得一提的是 AngularJS 也使用类似的方式,以至于 Angular 的作者们都受不了,决定在 AngularJS 2 使用新的语言 AtScript ,前端轮子太多,又造了一个,好在这个轮子造的比较好,兼容 ES6 TypeScript 规范,扯的远了,看看 AMD 长得啥样:

AMD:

define(['./a', './b'], function(a, b) {  
  ...
})

Closure 是 google 出品的前端工具, Closure 提供了一系列工具和库,谷歌自己的多个项目都是使用 Closure 开发的。 closure compiler 通过模块间依赖的声明把所有被依赖的文件打包到一起,而且 Closure 的一大优势是如果采用破坏性压缩( ADVANCED )压缩率极高。

//文件 A
goog.provide('module1')  
com.foo.bar = {  
   ...
}
....

//文件 B
goog.require('module1')  
var a = com.foo.bar;

然而 Closure 并不完美,不同的文件共享同一个全局对象,所以你不得不这样写 a.b.c=...。

CommonJS 是 Node.js 使用的模块化标准。 Node.js 对于前端开发者来说不仅仅可以提供一个 Server ,还是一个完美的开发平台,在 Node 上使用 Grunt/gulp 构建 web 项目是件很爽的事情。 Node 的模块化声明的方式与 Closure 类似,只是更进一步,天然隔离了命名空间。上面的代码如果使用 CommonJS 的模块化规范可以这么写:

//文件 A
module.exports = {...}  
....

//文件 B
var a = require('./foo/bar')

browserify 让使用 CommonJS 模块化规范的代码可以运行在客户端上。

2.静态加载与动态加载

ES6 之前我们先看模块加载的两种方式:

AMD 标准是动态加载的代表,而 CommonJS 是静态加载的代表。 AMD 的目的是用在浏览器上,所以是异步加载的。而 NodeJS 是运行在服务器上的,同步加载的方式显然更容易被人接收,所以使用了 CommonJS 。同样的道理,如果静态加载,那就使用同步的加载方式,如果动态加载就必须用异步的加载方式。

那么 ES6 采用何种加载机制?

ES6 既希望用简单的声明方式来完成静态加载,又不愿放弃动态加载的特性,而这两种方式几乎不可能简单的同时实现,所以 ES6 提供了两种独立的模块加载方法。

2.1 声明的方式

import {foo} from module1

2.2 通过 System.import API 的方式

System.import('some_module')  
    .then(some_module => {
        // Use some_module
    })
    .catch(error => {
        ...
    });

再看下 export 的语法,与 CommonJS 很像,只不过没有了 module 这个对象,而直接调用 export 。 可以 export 任何一个 函数,变量,对象

//expt.js
export function abc(){}//export 一个命名的 function  
export default function(){} //export default function  
export num=123 //export 一个数值  
export obj={}  
export { obj as default };

//import
import expt from 'expt'//default export  
import {default as myModule} from 'expt' //rename  
import {abc,num,obj} from 'expt'

更多细节可以看这篇文章: http://www.2ality.com/2014/09/es6-modules-final.html

目前来看,使用预编译的方式显然要好于使用动态加载,浏览器对 ES6 语法支持还很差,如果使用动态加载 ES6 ,在浏览器端要做 ES6 到 ES5 的翻译工作,这个显然是重复低效的。但是随着浏览器对 ES6 支持增强,尤其是浏览器实现了动态加载 API 后,动态加载的优势就会展现:

3.实践

如果现在使用 ES6 ,可以选择动态加载模块 system.js 或者 browserify 的预编译方法。

使用 system.js+babel 动态加载依赖。 system.js 是 ES6 动态模块加载的一个实现。写了一个小 DEMO :

项目初始化

bower install babel system.js --save

index.html

...
    <script src="/bower_components/system.js/dist/system.js"></script>

    <script>
      System.config({
          baseURL : "/scripts",
          transpiler : 'babel',
          map : {
            babel:'/bower_components/babel/browser.js'

          }
        }
      )
      System.import('main.js').then(function(m){
        m.default.sayHello()
      })

    </script>

...

main.js

export default {  
  sayHello : function(){
    console.log('hello')
  }
}

项目的地址在: https://github.com/stackOverMind/demo-system.js

使用 gulp+browserify+babel 预编译。 gulp 是一个 Node.js 平台上的任务管理平台。预编译要做很多配置,非常繁琐,推荐使用 yeoman 来生成项目骨架。比如使用 generator-es6-webapp 。

生成非常简单,在项目目录中执行

yo es6-webapp

缺少依赖的化安装依赖就好。

4.其他,关于前端化趋势

ES6 模块化意味着什么?

更强大的前端, Web 技术整体前移。 HTML5 的发展和某些优秀浏览器的支持让 web 技术整体前移,以前像渲染这种工作在后端进行是由于浏览器薄弱,且有老 IE 这种拖后腿捣乱的选手。

简化编程模型,人工管理 JS 依赖和将多个 JS 打包这种工作可以不需要了,而配合 WebComponents 标准,开发 Web 将不再借助模板引擎和预编译引擎。

前端化还有更深远的影响--在过去浏览器是个工具,现在浏览器是个重要的工具,在未来浏览器就是用户唯一的操作系统。

2641 次点击
所在节点    问与答
9 条回复
hronro
2015-11-10 18:11:01 +08:00
好文,收藏了
wilddog
2015-11-10 18:54:35 +08:00
貌似选择了一个不太好的节点...
EchoChan
2015-11-10 18:59:17 +08:00
@livid 麻烦移到 JS 节点。
wilddog
2015-11-10 19:03:18 +08:00
@EchoChan 我移动节点的时候系统提示我,不存在 JavaScript 这个节点... T.T
wizardforcel
2015-11-10 22:04:49 +08:00
。。。前端好复杂
chemzqm
2015-11-10 22:25:47 +08:00
前端模块化至少 8 年了吧,我最开始用的 Ext1 就是完全模块化,只不过是依赖全局变量,后面的 YUI2 YUI3 实现了沙箱,动态 combo 之类的机制,再过了几年才出来 commonjs , AMD 这些更加标准化的规范。
最后只不过换了下 API ,实现也从原来的框架挪动到了浏览器罢了
oott123
2015-11-10 22:49:09 +08:00
_(:з」∠)_ 为啥我感觉楼主的文真是越来越…湿了
zhangv
2015-11-11 11:07:28 +08:00
看到标题还是很有感触的,虽然并不是前端。
liuzhen
2015-11-11 13:52:44 +08:00
前端越来越复杂了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/235141

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX