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

前端工程与模块化框架 #4

Open
fouber opened this issue Jun 14, 2014 · 95 comments
Open

前端工程与模块化框架 #4

fouber opened this issue Jun 14, 2014 · 95 comments

Comments

@fouber
Copy link
Owner

fouber commented Jun 14, 2014

本文最先发表在 DIV.IO - 高质量前端社区,欢迎大家围观

不要再求验证码了,这个blog目前有800+人订阅,求验证没什么的很影响其他订阅者,可以在div.io上申请,定期会有同学发放的。。。


一直酝酿着写一篇关于模块化框架的文章,因为模块化框架是前端工程中的 最为核心的部分 。本来又想长篇大论的写一篇完整且严肃的paper,但看了 @糖饼DIV.IO 的一篇文章 《再谈 SeaJS 与 RequireJS 的差异》觉得可以借着这篇继续谈一下,加上最近spm3发布,在seajs的官网上又引来了一场 口水战 ,我并不想参与到这场论战中,各有所爱的事情不好评论什么,但我想从工程的角度来阐述一下已知的模块化框架相关的问题,并给出一些新的思路,其实也不新啦,都实践了2多年了

前端模块化框架肩负着 模块管理资源加载 两项重要的功能,这两项功能与工具、性能、业务、部署等工程环节都有着非常紧密的联系。因此,模块化框架的设计应该最高优先级考虑工程需要。

基于 @糖饼 的文章 《再谈 SeaJS 与 RequireJS 的差异》,我这里还要补充一些模块化框架在工程方面的缺点:

  1. requirejs和seajs二者在加载上都有缺陷,就是模块的依赖要等到模块加载完成后,通过静态分析(seajs)或者deps参数(requirejs)来获取,这就为 合并请求按需加载 带来了实现上的矛盾:

    • 要么放弃按需加载,把所有js合成一个文件,从而满足请求合并(两个框架的官方demo都有这样的例子);
    • 要么放弃请求合并,请求独立的模块文件,从而满足按需加载。
  2. AMD规范在执行callback的时候,要初始化所有依赖的模块,而CMD只有执行到require的时候才初始化模块。所以用AMD实现某种if-else逻辑分支加载不同的模块的时候,就会比较麻烦了。考虑这种情况:

    //AMD for SPA
    require(['page/index', 'page/detail'], function(index, detail){
        //在执行回调之前,index和detail模块的factory均执行过了
        switch(location.hash){
            case '#index':
                index();
            break;
            case '#detail':
                detail();
            break;
        }
    });

    在执行回调之前,已经同时执行了index和detail模块的factory,而CMD只有执行到require才会调用对应模块的factory。这种差别带来的不仅仅是性能上的差异,也可能为开发增加一点小麻烦,比如不方便实现换肤功能,factory注意不要直接操作dom等。当然,我们可以多层嵌套require来解决这个问题,但又会引起模块请求串行的问题。


结论:以纯前端方式实现模块化框架 不能 同时满足 按需加载请求合并依赖管理 三个需求。

导致这个问题的根本原因是 纯前端方式只能在运行时分析依赖关系

解决模块化管理的新思路

由于根本问题出在 运行时分析依赖,因此新思路的策略很简单:不在运行时分析依赖。这就要借助 构建工具 做线下分析了,其基本原理就是:

利用构建工具在线下进行 模块依赖分析,然后把依赖关系数据写入到构建结果中,并调用模块化框架的 依赖关系声明接口 ,实现模块管理、请求合并以及按需加载等功能。

举个例子,假设我们有一个这样的工程:

project
  ├ lib
  │  └ xmd.js    #模块化框架
  ├ mods         #模块目录
  │  ├ a.js
  │  ├ b.js
  │  ├ c.js
  │  ├ d.js
  │  └ e.js
  └ index.html   #入口页面

工程中,index.html 的源码内容为:

<!doctype html>
...
<script src="lib/xmd.js"></script>   <!-- 模块化框架 -->
<script>
    //等待构建工具生成数据替换 `__FRAMEWORK_CONFIG__' 变量
    require.config(__FRAMEWORK_CONFIG__);
</script>
<script>
    //用户代码,异步加载模块
    require.async(['a', 'e'], function(a, e){
        //do something with a and e.
    });
</script>
...

工程中,mods/a.js 的源码内容为(采用类似CMD的书写规范):

define('a', function(require, exports, module){
    console.log('a.init');
    var b = require('b');
    var c = require('c');
    exports.run = function(){
        //do something with b and c.
        console.log('a.run');
    };
});

具体实现过程

  1. 用工具在下线对工程文件进行扫描,得到依赖关系表:

    {
        "a" : [ "b", "c" ],
        "b" : [ "d" ]
    }
  2. 工具把依赖表构建到页面或者脚本中,并调用模块化框架的配置接口,index.html的构建结果为:

    <!doctype html>
    ...
    <script src="lib/xmd.js"></script>   <!-- 模块化框架 -->
    <script>
        //构建工具生成的依赖数据
        require.config({
            "deps" : {
                "a" : [ "b", "c" ],
                "b" : [ "d" ]
            }
        });
    </script>
    <script>
        //用户代码,异步加载模块
        require.async(['a', 'e'], function(a, e){
            //do something with a and e.
        });
    </script>
  3. 模块化框架根据依赖表加载资源,比如上述例子,入口需要加载a、e两个模块,查表得知完整依赖关系,配合combo服务,可以发起一个合并后的请求:

    http://www.example.com/??d.js,b.js,c.js,a.js,e.js

先来看一下这种方案的优点

  1. 采用类似CMD的书写规范(同步require函数声明依赖),可以在执行到require语句的时候才调用模块的factory。
  2. 虽然采用CMD书写规范,但放弃了运行时分析依赖,改成工具输出依赖表,因此 依赖分析完成后可以压缩掉require关键字
  3. 框架并没有严格依赖工具,它只是约定了一种数据结构。不使用工具,人工维护 require.config({...}) 相关的数据也是可以的。对于小项目,文件全部合并的情况,更加不需要deps表了,只要在入口的require.async调用之前加载所有模块化的文件,依赖关系无需额外维护
  4. 构建工具设计非常简单,而且可靠。工作就是扫描模块文件目录,得到依赖表,JSON序列化之后插入到构建代码中
  5. 由于框架预先知道所有模块的依赖关系,因此可以借助combo服务实现请求合并,而不用等到一级模块加载完成才能知道后续的依赖关系。
  6. 如果构建工具可以自动包装define函数,那么整个系统开发起来会感觉跟nodejs非常接近,比较舒服。

再来讨论一下这种方案的缺点:

由于采用require函数作为依赖标记,因此如果需要变量方式require,需要额外声明,这个时候可以实现兼容AMD规范写法,比如

define('a', ['b', 'c'], function(require, exports, module){
    console.log('a.init');
    var name = isIE ? 'b' : 'c';
    var mod = require(name);
    exports.run = function(){
        //do something with mod.
        console.log('a.run');
    };
})

只要工具把define函数中的 deps 参数,或者factory内的require都作为依赖声明标记来识别,这样工程性就比较完备了。

但不管怎样, 线下分析始终依靠了字面量信息,所以开发上可能会有一定的局限性,但总的来说瑕不掩瑜。

希望本文能为前端模块化框架的作者带来一些新的思路。没有必要争论规范,工程问题才是最根本的问题。

@afc163
Copy link

afc163 commented Jun 14, 2014

这个方式其实和 spm2 的打包基本一致,只是更进了一步,把完整的依赖关系都提取好了。

目前 spm2 以及 Arale 的方式是,a 模块依赖 b ,b 依赖 c 的话,打包 a 时,会把 b 和 c 的依赖都放到 a 的依赖数组里去,这样就不需要下载到 b 时才知道 b 的依赖关系了。

最终使用的都是下面的 Transported CMD 模块:

define('a', ['b', 'c'], function(require, exports, module){
  require('b');
})

对于浏览器模块来说,提前打包目前来说是大势所趋,线上的实时依赖分析由于现阶段浏览器环境的限制,只保留在调试阶段比较合适。这也是 spm3 对于 CMD 规范的态度。

另外,线下分析是可以根据 AST 去拿到准确的依赖列表,而不只是require的字面量

@fouber
Copy link
Owner Author

fouber commented Jun 14, 2014

@afc163

不应该提取到每个顶级模块里,因为逻辑可能在任何地方执行异步加载,提取到框架配置级别会更合理一些,也更灵活。

另外,线下分析,什么实现都可以的,我比较深刻的理解AST,但是放弃他是因为正则的“不严谨”性,使得可以适用于各种混合语言的代码中,比如模板

@afc163
Copy link

afc163 commented Jun 14, 2014

全依赖提取到每个顶级模块,这种方式其实不完全合理和准确。但我们不想维护一个完整的依赖列表(YUI最早这么做)的原因是因为工程上的复杂、对编译环境的高要求、以及难于移植。但这样做简单可靠,其实我们方案中的很多决策都是在追求这一点。

@afc163
Copy link

afc163 commented Jun 14, 2014

感谢~

@fouber
Copy link
Owner Author

fouber commented Jun 14, 2014

@afc163

fis的整个设计原则就是围绕着这个核心来实现的,工具的实现会更纯粹一些,如果时间允许,我可以给出一个完整的demo,应该经得起考验的。

@fouber
Copy link
Owner Author

fouber commented Jun 14, 2014

@afc163

对于特别大的工程,并不是一个完整的表,而是要有“命名空间”的概念,将表拆分成多个命名空间。工具负责维护和提取每个命名空间下的表信息。这条路可以继续探索下去,相信以表为媒介,连接框架和工具以及规范,是比较合理的一个出路。

@lifesinger
Copy link

大家的方案都差不多,目前来看有以下几种:

  1. 大家误以为的 seajs 方式:什么都动态分析、加载。(这种方式其实我们只用在开发调试时)
  2. 用工具生成 config 配置表的方式。类似 YUI、KISSY 采用的方式,有利有弊,配置表的维护并非那么轻松,特别是在阿里目前的模板技术背景,以及考虑 cms 区域的情况下。
  3. 用工具提取信息到文件本身的方式,线下打包好。Arale 在支付宝的实践,通过 spm 来完成,目前实践下来还可以,但依旧存在一些小痛。
  4. 用工具提取信息到文件本身,然后通过服务端(CDN源服务器)来实现自动打包的方式。目前阿里国际站在尝试,YAHOO 后来有部分业务线也走向了这种方式。支付宝还在考察观望。

@fouber 你真应该来阿里看看呀,很多时候,场景决定方案。FIS 很适合百度的场景,但拿到阿里的场景下,依旧还有许多路要走。

@xufei
Copy link

xufei commented Jun 14, 2014

@fouber 我之前也对构建系统的看法也类似,但以这段时间在苏宁看到的情况来说,这个构建系统太难建成,主要是部门划分和模块划分的不一致性。命名空间式的构建,要求的是互相之间没有交叉,理想状况是一个树形下去,但一交叉就完蛋了,要不然是命名空间的粒度过小,要不然是命名空间横跨部门,给构建带来很大麻烦。

@lifesinger 阿里的细节情况我不清楚,但估计是很类似的,所以我才逐渐明白sea里面有些细节的用意。之前没想到这些情况,没能理解为什么非要这么搞。以我之前公司的情况来说,是有大部门,也有统一的架构组,而且大部分产品是纯AJAX交互,所以连非静态模板的问题都很少有。

但是在网购型系统里,很可能顶部的购物车、支付模块等,不是来自本系统,而是来自其他业务部门,这些东西却非要集成在一个页面里,它们的公共项就很难处理。所以我理解阿里把模块拆得这么碎,然后用看上去很怪异的方式,在nginx那边搞combiner来合并,然后也正是为此,可能js会有乱序,必须晚期依赖。

@fouber
Copy link
Owner Author

fouber commented Jun 14, 2014

@xufei

在百度,对于大型系统,我们都不是整站构建的,而是按业务拆成了很多个子系统,每个产品会产生一张资源表,跨业务的依赖会引入对应产品库的表,每个业务子系统是独立构建上线的。举个例子:

site(站点)
  ├ common.git     #公共子系统模块
  ├ user.git       #用户子系统模块
  ├ message.git    #消息子系统模块
  ├ ...

每个子系统独立构建,并产生独立的表,线上部署的大致效果为:

www
  ├ map               #表目录
  │  ├ common.json    #公共子系统静态资源关系表
  │  ├ user.json      #用户子系统静态资源关系表
  │  ├ message.json   #消息子系统静态资源关系表
  │  ├ ...
  ├  template         #模板目录
  │  ├ common         #公共子系统模板
  │  ├ user           #用户子系统模板
  │  ├ message        #消息子系统模板
  │  ├ ...

每个子系统的静态资源id结构为: 系统名:资源id,比如common系统下的jquery代码,其id为 common:lib/jquery/jquery-2.0.2.js,所有的依赖关系可以记录在模板或模板所引用的js中的,模板中提供了静态资源管理和加载的接口,比如user子系统中希望使用message系统下的资源,其代码为(在user.git下的widget/user-info/user-info.php):

<?php import('common:lib/jquery/jquery-2.0.2.js');  ?>
<?php import('user:widget/user-info/user-info.js');  ?>
<?php import('user:widget/user-info/user-info.css');  ?>
blablabla

模板中的import函数,会在运行时读取资源表来实现静态资源按需,资源表中也记录了子系统内代码的合并情况,可以在模板运行期间计算静态资源的最优组合(带宽、请求数等)

每个系统独立构建,只有运行时的交叉引用,不会出现整站构建的情况

@xufei
Copy link

xufei commented Jun 14, 2014

@lifesinger

@fouber 你真应该来阿里看看呀,很多时候,场景决定方案。FIS 很适合百度的场景,但拿到阿里的场景下,依旧还有许多路要走。

那个,阿里好像刚收购了uc,你们已经在一家了……

@fouber
Copy link
Owner Author

fouber commented Jun 14, 2014

@lifesinger

我非常确信,支付宝在生产中使用的策略是更适合支付宝业务现状的解决方案。

写文章是想为中小企业应用模块化方案提出一些思路。现在比较堪忧的是中型团队对seajs的理解和应用现状。

以松鼠团队这边为例,之前使用seajs一直是放弃按需加载的,没有使用spm,因为规范不一致问题,所以自己实现了一套比较粗糙的打包方案,放弃了按需的能力,all-in-one.js。但面对移动端spa这种对按需、请求合并要求比较高的应用场景,会比较痛苦。

seajs作为业界广泛使用的模块化框架,可能还需要给出比较合理的生产应用的指导。

这是一项非常有难度的工作,因为即便是支付宝目前的解决方案,相信也不能推广给业界使用,因为它极有可能跟支付宝业务有着很深入的整合,包括提到的cms系统。

所以本文想阐述的就是这样一种适用于中小企业的模块化解决方案,确实不能涵盖全部使用场景,也没有关心具体哪种规范,均以生产需要为优先。

对于复杂体系的工程化改进真心是非常痛苦且充满挑战、并要承担巨大风险的工作。就规模而言,我觉得即便在熊掌公司所有经历过的工程化改造团队都没有过支付宝这么大规模的,所以确实不敢妄言,我非常赞同 场景决定方案,也坚信前端工程没有通用的解决方案这一事实。所以,除非亲身经历,亲手解决,否则每种方案总有 不适用的场景

另外,并不是fis适合熊掌公司的场景,而是基于fis实现的 fis-plus 适合,现在在松鼠团队同样基于fis做的 scrat 及其生态是适合松鼠团队业务形态的。

模块化框架,作为前端工程的 重中之重,是应该被反复锤炼和完善的,而且以我现在的认知来看,模块化框架非常有必要 每个团队根据自己的业务形态单独设计和实现一套。为cms模板服务的模块化框架、为spa服务的模块化框架,为小型项目服务的模块化框架、为大型系统服务的模块化框架,都有各自不同的问题域,实现上会有很大差异。由于模块管理本身逻辑很简洁,所以自己实现的收益是大的。

@chuguixin
Copy link

楼主在前端工程化上的见解和造诣的确值得我长久学习,现实工作中的项目没有尝试的机会,很多进步的方案都是浅尝辄止甚至读读而已,希望将来有机会在前端工程化能有更多机会尝试。另外,楼主对Browserify这个解决方案怎么看?个人感觉,all in one简单粗暴,但未来可能有很大前景。

@fouber
Copy link
Owner Author

fouber commented Jun 20, 2014

@chuguixin

有大概扫过 Browserify ,没有在实际生产中应用过。这篇文章提到了,前端工程的核心是模块化框架,实践总结的是,模块化框架会关联工具、规范、部署等问题的,所以,原则上讲,选择了一种模块化框架,就要选择其配套的工具及规范,类似选了seajs,就要接受spm,接受了require.js,要接受它的r.js一样。当然也可以自己DIY工具,但有些规范基本上是天生定义好了的。

所以模块化系统设计上,我比较推崇自己diy,包括框架和工具,我在文章的末尾中也提到了,不同的场景会有不同的模块化需求,完全通用的可能性不大。

关于all in one,相信是因为不能同时做到按需、合并请求才不得已选择的结果,aio(all in one)的模块化体系,在demo层面看不出有什么问题,这是非常具有迷惑性的,等到项目用上了,达到一定规模了,才会发现这种方式的弊端:

  1. 对于传统PC,aio之后,跨页面之间共享缓存将失效。比如有A、B两个页面,A页面用了 a, b, c, d 四个模块,B页面用了 a, b, e, f,对每个页面进行aio,那么,用户在访问A页面之后再访问B页面时,重复下载了 a, b 这两个资源的内容,使得性能变差。
  2. 对于SPA应用,aio之后,首次渲染会性能变差,SPA有很多虚拟页面,不应该放到首次去加载,而是按需的,首页需要什么资源就加载什么,后续hash跳转加载页面才再做进一步的请求,这样比较合理
  3. aio还会引起资源本身的缓存失效率提升。比如一个aio中使用了 a, b, c, d 四个模块,每个模块因业务开发而需要修改的概率是p,那么aio这个文件本身被触发修改的概率就是 1- (1-p)⁴ ,假设p是20%,那么aio文件发生修改的概率将达到60%,每次修改,都会导致原来用户浏览器中的aio缓存失效,那么一个aio里合并的越多,发生修改的概率就越高,最后带宽浪费也就越多。

所以,根据实践总结,合理的打包方案应该是:

  1. 经常修改的文件极少修改的文件 分开打包,可有效提升缓存利用率
  2. 多页面共用的文件极少页面会用的文件 分开打包,可有效提升用户跨页面浏览的缓存命中率
  3. 支持按需加载的动态合并 可有效提升SPA应用的展现性能

这三条原则,本身也有一些矛盾的地方,最终确定的打包方案应该是根据业务权衡的。当然,我可以补充一条:

当且仅当业务规模很小,缓存命中、按需加载收益不明显时,aio的方式才因为没那么矬而不被察觉其劣势。

个人觉得,市面上这些模块化框架及其配套工具比较坑爹,demo把缺点隐藏的很好,上手很happy,吃亏的是后面大规模应用。

@chuguixin
Copy link

@fouber
非常对!aio是一条极端的路线,让多页面的站点缓存很鸡肋了,而让单页面的应用又失去了按需加载。从amd/cmd最开始的动态加载到后来按照规范实现服务器端合并再到现在browserify的一刀切,虽然实践不足,但是经过几个项目也很认可具体业务具体分析,现实往往比理想差,而且差很多。
与本文关系不大的是,browserify的想法貌似很大,尝试为很多node的包做浏览器的兼容,我感觉这个唯一的意义就是让node的包可以直接运行在浏览器,但是好像真正对于生产的意义还有待观察。拙见。

@fouber
Copy link
Owner Author

fouber commented Jun 21, 2014

@chuguixin

... 让node的包可以直接运行在浏览器 ...

问题是这样的包到底有哪些?好像实际业务中基本没有。曾经有一些框架说“让js在前后端都能跑”也是扯淡,看过某些所谓前后端能跑的js,其实都是这样的代码结构:

if(runAtServer){
    //do something in server
} else {
    //do something in browser
}

是的,输出到前端的代码携带了一坨不需要的逻辑分支,这对于要求低带宽的前端来说好矬逼,而且很容易暴露server端的敏感信息。

我在另外一篇blog中也回答过这句话,照搬过来。

@chuguixin
Copy link

@fouber
我认为,这个与

if(runAtServer){
    //do something in server
} else {
    //do something in browser
}

还是不同的。browserify目标应该是形成一个小社区,提供一系列类似于shim的实现,这里,也就是我们将来的业务代码不需要在上面的判断,或者说我们已有的node代码不需要修改直接在浏览器可用。关于这个思路,我的一点看法是:

  1. 我们实际开发中会有这样的需求?比如,我们会在浏览器端需要stream吗?
  2. 即使我们有部分需求,但是部分模块根本无法做shim,比如fs,这也让这个思路显得有些坑坑洼洼。

边走边瞧吧。

@hax
Copy link

hax commented Jun 22, 2014

@fouber 还是有这样的模块的。比如md5、hmac等用到的crypto模块。

@hax
Copy link

hax commented Jun 22, 2014

@chuguixin
fs模块是有的。还有模块可以把所有readFileSync的常量调用全部替换成直接内嵌文件。
另外,stream在浏览器里也会用的。比如websocket是可以传输二进制的。只不过从我的角度看,将来未必是统一在node api上,而是统一在 html5 api 上。比如未必是统一在 fs 模块,而是统一在 File API。

@chuguixin
Copy link

@hax
嗯,首先,关于浏览器端的stream,只是提供类似的api吧?或者说,html5后续有类似的想法在浏览器端实现一个原生的stream?关于,fs模块,应该是我的失误,我没看到,就误以为没有了。
关于api的统一,我一直以为browserify目标是在浏览器端实现一套node的api。原因是看到browserify-handbook

browserify is a tool for compiling node-flavored commonjs modules for the browser.

这个是我个人的理解。如果是统一到html5的话,那应该是我理解错误了。如果是:

比如未必是统一在 fs 模块,而是统一在 File API。

这样的话,意思是node的fs的api会修改?应该也不会吧。难道是我直接就把人家的初衷理解错误了?

@hax
Copy link

hax commented Jun 22, 2014

@chuguixin See https://dvcs.w3.org/hg/streams-api/raw-file/tip/Overview.htm

browserify 的初衷确实是让你依赖node标准库的程序也能在浏览器上用,因此它提供了几乎所有node api在浏览器上的对应版本。

但是这并不表示你的程序必须用node的模块。因为现在 html5 的 API 已经非常丰富,特别是,大量API倒过来在node中有polyfill。举例来说,网络通讯我就不用node的http模块,而用 xmlhttprequest 模块,它与浏览器中的 XMLHttpRequest API 一致。

node的fs模块自然不会改,但是不代表你必须用它啊。你完全可以倒过来用File API,用DOM storage,它们都有node上的实现。所以,browserify虽然初衷是让你可以用 node api 写程序跑在浏览器中,但是它只是一个工具,我们也完全可以反其道而行之,利用它让我们写出使用 html5 api 的程序,同时可以跑在浏览器和node中。

@chuguixin
Copy link

@hax
看到这个Draft了,谢谢,感叹一句真是跟不上节奏啊。
我理解你的意思是说,我们将来使用browserify的方式很可能是反向的?也就是node可能有一些开源的包去兼容w3c的标准化api?

@fouber
Copy link
Owner Author

fouber commented Jun 23, 2014

@hax @chuguixin

我也学习到了,多谢。确实感觉nodejs的包向html5的api靠拢更合理一些。在浏览器中使用nodejs的api感觉略牵强,但在nodejs提供一致的html5的API,就很舒服了。

@hax
Copy link

hax commented Jun 23, 2014

不仅是感觉舒服,而且我觉得也是合理的。因为API统一作为抽象层总是要付出代价的,关键这代价放在哪里。浏览器端与服务器端(node)比,显然是浏览器端对任何成本更加敏感——比如看看npm的树策略和bower的平面策略就可以看到不同的选择方向。服务器端多数情况下其实无所谓你包了几层——反正node本来的强项也是io密集应用,而API统一的成本是间接调用也就主要是cpu消耗,相比较而言对性能影响估计可忽略不计。至于多的代码占用空间啥的,更是如此。

@sapjax
Copy link

sapjax commented Jun 23, 2014

@hax 能说明一下browser端程序跑在server端的需求情况么
browser端的程序大多与dom相关并且有用户参与交互,跑在server端的意义何在呢。
如果只是单纯为了使用html5的api,感觉有点为了统一而统一啊

@hax
Copy link

hax commented Jun 23, 2014

@sapjax 我举个工作中的例子。最近我们上了一个在线聊天的功能,使用的是某云服务提供的基于Web Socket的chatting service。他们暂时只有iOS和Android的sdk,所以用于网站的sdk是我们自己写的。在这个项目中我就选择尝试了前述用browerify但是统一于html5 api的方式。主要的原因是两个。第一是这强迫我们必须把界面(大量dom操作,只在浏览器中测试)和通讯和协议层(websocket、ajax以及少量dom storage部分,大部分在Node.js环境中测试,然后通过browerify打包后再在浏览器环境中测试)分离,负责sdk开发的可以专注于协议本身的问题,和第三方的接口也比较方便。第二是后续我们服务器端也需要直接调用云服务,这个时候就直接共用了相同的sdk。

所以这样的需求不见得很多,但是还真的是存在的。

其他的典型例子,比如经典的表单验证。如果能统一到 html5 的 validation API,会方便很多。

@wushanchao
Copy link

总觉得到最后,用哪个模块化方式,也是要根据业务场景来定。amd和cmd都不是银弹,就算你说的模块化管理新思路,要维护字面量列表,在某些场景下,也是麻烦的。

@fouber
Copy link
Owner Author

fouber commented Jun 25, 2014

@wushanchao

维护列表是工具做的啊

@shmnhdouble2013
Copy link

关于 维护框架级别 依赖表 我更多支持seajs方案,前端模块标准化已然是一个大趋势,在阿里系web page、web app、移动端应用中,模块化颗粒度越来越细,应用场景也越来越复杂多变,直接 ransported 模块 灵活度更高、稳定性强、成本也小!

@luqin
Copy link

luqin commented Jul 9, 2015

@chenyucheng 如果o.flag不为boolean类型,转换为boolean时可以使用!!

@chenyucheng
Copy link

@luqin, 不写 !! 的时候 if( ) 表达式里面 也会隐士转换成 boolean 的。

@luqin
Copy link

luqin commented Jul 9, 2015

@chenyucheng 是的,但是比如这种情况是需要的: let a = !!o.flag; 其中a需要是boolean类型

@wind930
Copy link

wind930 commented Jul 26, 2015

最近也准备用fis,fis的确做了很多工作,今天能看到这篇日志,感觉很受用啊。由于我们后端用的laravel的blade模板,虽然laravel-fis上提供了脚手架,但是脚手架上的文档似乎没有太说的具体,还在研究中

@hstarorg
Copy link

我的做法就比较奇怪,使用requireJS做加载器,由于整个项目由框架部分+N个模块组成。所以我是将框架代码直接合并,然后减少请求数;每个模块生成一个js和一个css文件,使用按需加载。感觉三不像,但满足当前的需求。

@gengxuelei
Copy link

非常感谢作者的分享,fis的强大也是让国内开发者扬眉吐气一把,只是鉴于国内软件的一些通病,没甚开源精神,视代码如命的一些屌丝公司,希望fis不会入此流。只是fis的教程还是不够完善,希望有更多的开发者可以分享自己的感悟。这是我的学习笔记http://blog.csdn.net/gengxuelei/article/details/47336879 ,分享给大家

@alannesta
Copy link

@fouber 希望能够比较一下现在比较火的webpack和fis3的一些特点。对于SAP (angular, backbone) webpack和fis谁更合适呢?
我参与的几个angular开发的SPA都是使用gulp进行构建, aio的打包策略。在代码的模块化开发上,angular已经有很好的规范,但是官方没有与之适配的模块化加载方案。没有使用过fis, 我的理解是如果想实现按需加载,需要调整项目源码,适配fis的模块化开发的syntax (CMD like,在每个file里包裹require和module.exports),不知道是否准确?

@fouber
Copy link
Owner Author

fouber commented Sep 15, 2015

@alannesta

你的说法基本准确,我在补充一下:

在使用组件化框架之前,应该现有资源管理方案

fis主要是想解决资源加载(按需、同步/异步、请求合并、依赖管理、文件指纹等),在这个基础上再应用angular等框架绝对是如虎添翼。fis的核心思想是『基于表进行资源加载管理』,至于使用什么模块化规范,甚至不适用模块化规范都是没有问题的,因为资源加载和模块化其实是相互独立的部分,如果非说二者有关系,也就是二者共享依赖树吧。

fis希望为前端资源与资源之间提供三种关系:资源定位、资源内嵌和依赖声明。我觉得webpack在这方面已经跟fis非常接近了,只是没有明确的强调出来而已,但仅在对待依赖声明的处理上,二者有所不同:

  • webpack对于依赖声明最终采用构建的方式解决,构建输出bundle或者chunk
  • fis对于依赖声明最终采用表+框架的方式解决,构建输出表,再配合框架解决加载问题

fis与webpack的主要差别就在这里吧,其他细节的东西可有可无。

然而单纯依靠构建解决资源加载并不是一个万能的办法,比如这个例子:

从下往上看,应用一般有一个入口模块,比如app,这个入口模块根据url来动态决定异步加载某个页面(P₁-P₄),而每个页面并不是孤岛资源,它们还会依赖其他组件,组件与组件之间可能还有共享的基础库依赖。

单独看我例子中的a、b依赖c的情况,webpack可以通过CommonsChunkPlugin插件来对公共依赖模块进行提取,看似OK;再单独看我例子中的app加载p1-p4,webpack可以当做chunk来加载,也似乎OK,但问题是一旦把二者结合来看,就显露出静态构建的弊端:

这个例子在静态构建下合并请求的最佳实践居然是不合并请求。

现实中这样的例子其实更多,而且会更复杂,静态分析面对大工程最终的结果往往是要么因为其局限性而根本配不出来合理的方案,要么因为配置太多维护成本过高而变成一个大bundle的情况,没有真正的优化空间,那些“有公共依赖抽取插件从而进行优化”的假设基本形同虚设。

而基于表的资源加载就能很好的处理这个问题,细节我在这篇blog中也给出了一些解答。

此外,静态构建能做到的极致无非就是把css和html都内嵌到js中,最终通过对js的打包管理实现『all in js』的资源管理,但这种webapp模式始终是前端的『半壁江山』,还有很大一类『后端渲染』的产品,它们的CSS需要出现在页面头部,不能用js全权管理加载,这类问题也将成为静态构建的局限。

但使用fis终归是有成本的

就如我之前介绍过的 前端开发体系建设日记 ,针对一个团队定制开发规范,并根据团队的运维方式定制部署规范,然后根据业务场景确定性能优化方案,最终根据以上三者再结合资源表写出一个漂亮的资源加载框架,成本其实很高,虽然长远来看用于支撑团队和业务快速发展的收益是比较大的,但前期确实比较痛苦,这是fis推广和落地的最大阻碍。

@wyntau
Copy link

wyntau commented Sep 15, 2015

@hstarorg 一样的做法, 我觉得这种需求应该会很强烈才对, 但是好像大家对这种做法都没说过.

目前使用的是angular加一些angular library.

使用gulp构建的时候, 把所有library全部合并成一个文件, 然后业务方面的代码, 使用angular-ui-router, 每个state分为一个js, css, html, 然后通过angular-require, 到达某个state的时候再去加载此state需要的资源js, html, css, 还包括共用的service, directive等等. 除此之外又做了一步, 把每个state对应的css文件, 注入到此state对应的js开头部分, 使用js动态的在head标签中添加style标签, 这样就省下一个请求加载css
最终再把所有构建完成的东西放到CDN上.

不知道有没有类似做法的, 可以互相探讨一下

@liminjun
Copy link

@treri 我们公司目前项目和你所说的类似。不过使用的onLazyLoad加载相应的html和js。因为是企业级系统,所以less编译好的css就直接在index.html页面加载。

@wyntau
Copy link

wyntau commented Sep 16, 2015

@liminjun 看来, 大家都有这种类似的需求啊

@fouber 请问是不是对这种需求有什么好的解决方案? 一直以为自己的这种用法比较小众, 但是看来有不少人有这种问题的.

@lenxeon
Copy link

lenxeon commented Oct 13, 2015

@fouber 你好fis3 能自己分析出 http://www.example.com/??d.js,b.js,c.js,a.js,e.js 这样的路径么,这个路径不会需要人工维护吧,感觉人工维护也太麻烦了。

@JasinYip
Copy link

@lenxeon 这个貌似 nginx 就能做

@Saviio
Copy link

Saviio commented Oct 13, 2015

@JasinYip 我记得nginx可以自己写模块支持combo的,而且nginscript貌似也出来了,可以顺手玩一发。

@lenxeon
Copy link

lenxeon commented Oct 14, 2015

@JasinYip 恩,一说这个我倒是想起了openresty好像提到过。

@nimoc
Copy link

nimoc commented Oct 14, 2015

@lenxeon https://github.com/alibaba/nginx-http-concat
nginx combo 插件

@atian25
Copy link
Collaborator

atian25 commented Oct 14, 2015

@lenxeon

@ourai
Copy link

ourai commented Nov 18, 2015

@fouber 我们团队也正要进行前端工程建设,连续看了你的几篇文章,长见识了,并且有些似懂非懂的感觉,所以有个疑问(愚问?)——

我们团队的后端是 Java + Velocity,但是 web 框架不是 Spring。我们基本确定要以 FIS 为核心来搭建,看到你文章中提到了 Jello,不知道这个解决方案是不是你所谓的「模块化框架」?看了下项目的介绍,有点糊涂。

@m17y m17y mentioned this issue May 23, 2016
@mailylqj
Copy link

@fouber 我也正在做前端框架的搭建,优化方案,我想问一下,线下工具扫描依赖关系,有什么好的推荐工具吗?我们的项目,模块太多,还会增加,我怎么去管理!

@alcat2008
Copy link

@fouber 当前用 React 全家桶实现的 SPA,有没有按需加载的构建方案?

项目中模块划分都是通过 ES6 的 import/export 方式引入导出,再用 webpack 统一构建打包。

对于项目中所依赖的第三方库可以用 Webpack 的 external 将其单独隔离引用,比如 'react'、'react-dom' 等,但在我看来其实意义并不大。

对于业务层面的代码,却一直没有找到合理的解决办法。大神是怎么看待这个问题呢?谢谢!

@joeyguo
Copy link

joeyguo commented Nov 7, 2016

@alcat2008 可以用 require.ensure 结合 router 进行拆包,实现按需加载。

@bojueWjt
Copy link

bojueWjt commented Nov 7, 2016

@alcat2008 最近刚好在看webpack,对于你谈论到的问题,做了一个小总结,希望对你有用。http://ninico.top/2016/10/29/webpack-production-config.html

@alcat2008
Copy link

@joeyguo @bojueWjt 谢谢两位的热心回答,require.ensure 是官网提供的方法,之前也测试使用过。

但这种方法实际上侵入了业务代码,而且在我看来属于硬编码,不够灵活。所以一直希望找到一种类似依赖表 config 的方式,能自由配置。就像上文提到的那样。

这种场景特别是在对一个项目进行裁剪部署、或者加法构建时能有更好的性能。

@taohailin
Copy link

开发环境,打包环境,部署之类的是什么鬼,分不清

@yulusjjc
Copy link

yulusjjc commented Jul 4, 2017

@fouber 最近每次上线都被CDN缓存的问题困扰, 感谢大神分享,收益颇多。
目前我们的代码也没有进行前后端分离,后端用FreeMarker模板填充的html,fis3有提供jsp的解决方案,那么对于这种模板形式的,有没有好的办法?可以当做普通html处理吗?

@ireeoo
Copy link

ireeoo commented Jan 24, 2018

666

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