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

前端模块化开发的价值 #547

Closed
lifesinger opened this issue Feb 18, 2013 · 66 comments
Closed

前端模块化开发的价值 #547

lifesinger opened this issue Feb 18, 2013 · 66 comments
Milestone

Comments

@lifesinger
Copy link
Member

本文发表在《程序员》杂志 2013 年 3 月刊,推荐购买。


前端模块化开发的价值

随着互联网的飞速发展,前端开发越来越复杂。本文将从实际项目中遇到的问题出发,讲述模块化能解决哪些问题,以及如何使用 Sea.js 进行前端的模块化开发。

恼人的命名冲突

我们从一个简单的习惯出发。我做项目时,常常会将一些通用的、底层的功能抽象出来,独立成一个个函数,比如

function each(arr) {
  // 实现代码
}

function log(str) {
  // 实现代码
}

并像模像样地把这些函数统一放在 util.js 里。需要用到时,引入该文件就行。这一切工作得很好,同事也很感激我提供了这么便利的工具包。

直到团队越来越大,开始有人抱怨。

小杨:我想定义一个 each 方法遍历对象,但页头的 util.js 里已经定义了一个,我的只能叫 eachObject 了,好无奈。

小高:我自定义了一个 log 方法,为什么小明写的代码就出问题了呢?谁来帮帮我。

抱怨越来越多。团队经过一番激烈的讨论,决定参照 Java 的方式,引入命名空间来解决。于是 util.js 里的代码变成了

var org = {};
org.CoolSite = {};
org.CoolSite.Utils = {};

org.CoolSite.Utils.each = function (arr) {
  // 实现代码
};

org.CoolSite.Utils.log = function (str) {
  // 实现代码
};

不要认为上面的代码是为了写这篇文章而故意捏造的。将命名空间的概念在前端中发扬光大,首推 Yahoo! 的 YUI2 项目。下面是一段真实代码,来自 Yahoo! 的一个开源项目。

if (org.cometd.Utils.isString(response)) {
  return org.cometd.JSON.fromJSON(response);
}
if (org.cometd.Utils.isArray(response)) {
  return response;
}

通过命名空间,的确能极大缓解冲突。但每每看到上面的代码,都忍不住充满同情。为了调用一个简单的方法,需要记住如此长的命名空间,这增加了记忆负担,同时剥夺了不少编码的乐趣。

作为前端业界的标杆,YUI 团队下定决心解决这一问题。在 YUI3 项目中,引入了一种新的命名空间机制。

YUI().use('node', function (Y) {
  // Node 模块已加载好
  // 下面可以通过 Y 来调用
  var foo = Y.one('#foo');
});

YUI3 通过沙箱机制,很好的解决了命名空间过长的问题。然而,也带来了新问题。

YUI().use('a', 'b', function (Y) {
  Y.foo();
  // foo 方法究竟是模块 a 还是 b 提供的?
  // 如果模块 a 和 b 都提供 foo 方法,如何避免冲突?
});

看似简单的命名冲突,实际解决起来并不简单。如何更优雅地解决?我们按下暂且不表,先来看另一个常见问题。

烦琐的文件依赖

继续上面的故事。基于 util.js,我开始开发 UI 层通用组件,这样项目组同事就不用重复造轮子了。

其中有一个最被大家喜欢的组件是 dialog.js,使用方式很简单。

<script src="util.js"></script>
<script src="dialog.js"></script>
<script>
  org.CoolSite.Dialog.init({ /* 传入配置 */ });
</script>

可是无论我怎么写文档,以及多么郑重地发邮件宣告,时不时总会有同事来询问为什么 dialog.js 有问题。通过一番排查,发现导致错误的原因经常是

<script src="dialog.js"></script>
<script>
  org.CoolSite.Dialog.init({ /* 传入配置 */ });
</script>

在 dialog.js 前没有引入 util.js,因此 dialog.js 无法正常工作。同样不要以为我上面的故事是虚构的,在我待过的公司里,至今依旧有类似的脚本报错,特别是在各种快速制作的营销页面中。

上面的文件依赖还在可控范围内。当项目越来越复杂,众多文件之间的依赖经常会让人抓狂。下面这些问题,我相信每天都在真实地发生着。

  1. 通用组更新了前端基础类库,却很难推动全站升级。
  2. 业务组想用某个新的通用组件,但发现无法简单通过几行代码搞定。
  3. 一个老产品要上新功能,最后评估只能基于老的类库继续开发。
  4. 公司整合业务,某两个产品线要合并。结果发现前端代码冲突。
  5. ……

以上很多问题都是因为文件依赖没有很好的管理起来。在前端页面里,大部分脚本的依赖目前依旧是通过人肉的方式保证。当团队比较小时,这不会有什么问题。当团队越来越大,公司业务越来越复杂后,依赖问题如果不解决,就会成为大问题。

文件的依赖,目前在绝大部分类库框架里,比如国外的 YUI3 框架、国内的 KISSY 等类库,目前是通过配置的方式来解决。

YUI.add('my-module', function (Y) {
  // ...
}, '0.0.1', {
    requires: ['node', 'event']
});

上面的代码,通过 requires 等方式来指定当前模块的依赖。这很大程度上可以解决依赖问题,但不够优雅。当模块很多,依赖很复杂时,烦琐的配置会带来不少隐患。

命名冲突和文件依赖,是前端开发过程中的两个经典问题。下来我们看如何通过模块化开发来解决。为了方便描述,我们使用 Sea.js 来作为模块化开发框架。

使用 Sea.js 来解决

Sea.js 是一个成熟的开源项目,核心目标是给前端开发提供简单、极致的模块化开发体验。这里不多做介绍,有兴趣的可以访问 seajs.org 查看官方文档。

使用 Sea.js,在书写文件时,需要遵守 CMD (Common Module Definition)模块定义规范。一个文件就是一个模块。前面例子中的 util.js 变成

define(function(require, exports) {
  exports.each = function (arr) {
    // 实现代码
  };

  exports.log = function (str) {
    // 实现代码
  };
});

通过 exports 就可以向外提供接口。这样,dialog.js 的代码变成

define(function(require, exports) {
  var util = require('./util.js');

  exports.init = function() {
    // 实现代码
  };
});

关键部分到了!我们通过 require('./util.js') 就可以拿到 util.js 中通过 exports 暴露的接口。这里的 require 可以认为是 Sea.js 给 JavaScript 语言增加的一个 语法关键字,通过 require 可以获取其他模块提供的接口。

这其实一点也不神奇。作为前端工程师,对 CSS 代码一定也不陌生。

@import url("base.css");

#id { ... }
.class { ... }

Sea.js 增加的 require 语法关键字,就如 CSS 文件中的 @import 一样,给我们的源码赋予了依赖引入功能。

如果你是后端开发工程师,更不会陌生。Java、Python、C# 等等语言,都有 includeimport 等功能。JavaScript 语言本身也有类似功能,但目前还处于草案阶段,需要等到 ES6 标准得到主流浏览器支持后才能使用。

这样,在页面中使用 dialog.js 将变得非常简单。

<script src="sea.js"></script>
<script>
seajs.use('dialog', function(Dialog) {
  Dialog.init(/* 传入配置 */);
});
</script>

首先要在页面中引入 sea.js 文件,这一般通过页头全局把控,也方便更新维护。想在页面中使用某个组件时,只要通过 seajs.use 方法调用。

好好琢磨以上代码,我相信你已经看到了 Sea.js 带来的两大好处:

  1. 通过 exports 暴露接口。这意味着不需要命名空间了,更不需要全局变量。这是一种彻底的命名冲突解决方案。
  2. 通过 require 引入依赖。这可以让依赖内置,开发者只需关心当前模块的依赖,其他事情 Sea.js 都会自动处理好。对模块开发者来说,这是一种很好的 关注度分离,能让程序员更多地享受编码的乐趣。

小结

除了解决命名冲突和依赖管理,使用 Sea.js 进行模块化开发还可以带来很多好处:

  1. 模块的版本管理。通过别名等配置,配合构建工具,可以比较轻松地实现模块的版本管理。
  2. 提高可维护性。模块化可以让每个文件的职责单一,非常有利于代码的维护。Sea.js 还提供了 nocache、debug 等插件,拥有在线调试等功能,能比较明显地提升效率。
  3. 前端性能优化。Sea.js 通过异步加载模块,这对页面性能非常有益。Sea.js 还提供了 combo、flush 等插件,配合服务端,可以很好地对页面性能进行调优。
  4. 跨环境共享模块。CMD 模块定义规范与 Node.js 的模块规范非常相近。通过 Sea.js 的 Node.js 版本,可以很方便实现模块的跨服务器和浏览器共享。

模块化开发并不是新鲜事物,但在 Web 领域,前端开发是新生岗位,一直处于比较原始的刀耕火种时代。直到最近两三年,随着 Dojo、YUI3、Node.js 等社区的推广和流行,前端的模块化开发理念才逐步深入人心。

前端的模块化构建可分为两大类。一类是以 Dojo、YUI3、国内的 KISSY 等类库为代表的大教堂模式。在大教堂模式下,所有组件都是颗粒化、模块化的,各组件之间层层分级、环环相扣。另一类是以 jQuery、RequireJS、国内的 Sea.js、OzJS 等类库为基础的集市模式。在集市模式下,所有组件彼此独立、职责单一,各组件通过组合松耦合在一起,协同完成开发。

这两类模块化构建方式各有应用场景。从长远来看,小而美更具备宽容性和竞争力,更能形成有活力的生态圈。

总之,模块化能给前端开发带来很多好处。如果你还没有尝试,不妨从试用 Sea.js 开始。

(完)
特别感谢这篇文章: http://chaoskeh.com/blog/why-seajs.html
参考了部分内容。


2013-04-23

补充一篇很不错的入门文档:一步步学会使用 Sea.js 2.0

@qubaomingg
Copy link

就说吧,怎么越读越熟=-=。。。用过一段时间,吸引的不止是seajs,是玉伯的代码风格,哈哈~~对怎么实现异步的不太了解,看源码没有看懂,,一个require太神奇了。

@xiongsongsong
Copy link

曾经参与了seaJS有奖征文,写的非常幼稚,但细读,和这个issue还是有共通的地方:#292

@ghostcode
Copy link

开始学习,项目要用到了!体验模块化的编程。

@xiongsongsong
Copy link

还有另外一个好处:不用考虑命名空间了!

降低了学习和理解的难度。

@motouzhixin
Copy link

当然,js的到良好的管理和充分利用!

------------------ 原始邮件 ------------------
发件人: "xiongsongsong"notifications@github.com;
发送时间: 2013年7月4日(星期四) 中午11:37
收件人: "seajs/seajs"seajs@noreply.github.com;
抄送: "魔鬼的泪与血"1114910233@qq.com;
主题: Re: [seajs] 前端模块化开发的价值 (#547)

还有另外一个好处:不用考虑命名空间了!

降低了学习和理解的难度。


Reply to this email directly or view it on GitHub.

@lip8up
Copy link

lip8up commented Jul 22, 2013

不错,我们的项目一直在用,越来越觉得好用。

@panxuepeng
Copy link

SeaJS 组件库,http://panxuepeng.github.io/seajslib/

@greatming
Copy link

我也要开始学习了

@greatming
Copy link

我也要开始学习了,
问个问题,项目中只需要seajs就可以了吗,不需要别的了吧

@afc163
Copy link
Member

afc163 commented Sep 3, 2013

@greatming

seajs 只是模块加载器,不负责任何具体的业务。你还需要模块。

@jabez128
Copy link

jabez128 commented Oct 8, 2013

很好的文章,长知识了

@tedyhy
Copy link

tedyhy commented Nov 13, 2013

写的很好,涨姿势了。。。

@hotoo
Copy link
Member

hotoo commented Nov 13, 2013

@lifesinger 用 Issues 写文档,有一些问题,这里探讨下:

  1. 不利于文档本身的管理,和普通的问题 Issues 混杂在一起,不好找。
  2. 没有版本管理,多个版本只能分散在不同的未知 Issues 中。不好找。
  3. 文档总体来说不需要讨论和回复。如果文档有问题,可以提 Issues 或 Fork & Pull Request。现在受无意义回复骚扰太多。

Issues 没有收藏功能,又分散各处,没有版本控制,用作文档挺不合适的。
建议文档跟源码放一起。

@tiye
Copy link

tiye commented Nov 13, 2013

@hotoo +1
另外 Issue API 可以把 Markdown 内容拉出来的, 还有点用 http://jiyinyiyong.github.io/seajs-issues/

@kaizhuQin
Copy link

涨姿势了,写得很好。。。

@eilvein
Copy link

eilvein commented Nov 29, 2013

项目使用中

@xiaojiong
Copy link

不错,新项目移植中....

@markyun
Copy link

markyun commented Jan 10, 2014

mark一下。

@destinyd
Copy link

mark

@ivanthing
Copy link

涨姿势了.。。。学习中!

@Troland
Copy link

Troland commented Mar 6, 2014

目前用了reuirejs感觉不明白的就是一些模块的监听等等。

@hehongwei44
Copy link

非常有价值的项目

@iyangyuan
Copy link

哇,这和Node.js有啥联系呢,感觉太像了!
还有就是关于路径的处理,比如,根目录表示什么路径?能否介绍一下呢?

@hogaFarming
Copy link

学习了

@ylygithub
Copy link

开始学习sea ~

@1215904405a
Copy link

其实就是手动加载js文件和统一变量命名

@cdll
Copy link

cdll commented Aug 25, 2015

为何木有shim机制?不是很明白~

@kookpua
Copy link

kookpua commented Oct 12, 2015

learn!

@YAMAPM
Copy link

YAMAPM commented Oct 24, 2015

YUI().use('node', function (Y) {
// Node 模块已加载好
// 下面可以通过 Y 来调用
var foo = Y.one('#foo');
});
这段代码 是不是Y.node('#foo')啊

@ruofeng086
Copy link

像上面的例子里说的,a.js 里面 require('b.js'),在模板中可以调用a.js中的方法,那b.js的方法怎么去调用呢?

a.js

define(function(require, exports) {

var seajs = require('hello-Seajs.js');

exports.init = function() {
alert('init:ok');
};

});

b.js

define(function(require, exports) {

exports.each = function (arr) {
alert("each:ok");
};

exports.log = function (str) {
alert("log:ok");
};

});

模板

seajs.use("init",function(seajs){
seajs.log("e");
});

@XiaodongTong
Copy link

Mark

@lihuacn
Copy link

lihuacn commented Dec 17, 2015

很好的入门

@freedomdebug
Copy link

从微信的页面过来期待做的越来越好

@liubin595338764
Copy link

一步步学会使用 Sea.js 2.0 链接已经失效了

@xinbingliang
Copy link

来水一脚

@playwolf719
Copy link

playwolf719 commented Jul 22, 2016

请问,avalon和seajs我结合使用,但用到avalon的另一个组件ajax组件时,老是报null,https://github.com/RubyLouvre/mmRequest
mmRequest应该怎么结合avalon和seajs一起使用?

@ckx321
Copy link

ckx321 commented Sep 29, 2016

非常好,学习了,感谢。

@ccbbhh
Copy link

ccbbhh commented Oct 21, 2016

thanks

@GuoChen-WHU
Copy link

学习了

@deepdatatop
Copy link

hard work!

@starof
Copy link

starof commented May 3, 2017

现在用什么来做模块化

@alxtz
Copy link

alxtz commented May 16, 2017

現在JS模組化好像使用webpack比較多,很火紅

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

No branches or pull requests