fouber / blog

没事写写文章,喜欢的话请点star,想订阅点watch,千万别fork!

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

前端工程与模块化框架

fouber opened this issue · comments

本文最先发表在 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都作为依赖声明标记来识别,这样工程性就比较完备了。

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

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

这个方式其实和 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的字面量

@afc163

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

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

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

感谢~

@afc163

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

@afc163

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

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

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

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

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

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

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

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

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

@lifesinger

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

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

@lifesinger

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

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

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

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

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

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

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

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

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

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

@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,吃亏的是后面大规模应用。

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

@chuguixin

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

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

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

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

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

@fouber
我认为,这个与

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

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

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

边走边瞧吧。

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

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

@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会修改?应该也不会吧。难道是我直接就把人家的初衷理解错误了?

@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中。

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

@hax @chuguixin

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

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

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

@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,会方便很多。

commented

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

@wushanchao

维护列表是工具做的啊

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

@fouber 受益匪浅

谢谢。忍不住想吐槽,什么时候Github的文字会变成emoji了。。

image

combo服务 想问下是在后端把几个文件合成一个下发吧?那这样如果2个模块a、b共同引用模块x(包含js和css等),那x模块岂不是要被下载两次?

@chemdemo

不会重复下载的,因为资源加载请求是框架发起的,框架知道自己曾经发起过哪些资源的请求,可以做各种管理动作,包括去重、缓存等

请问对于刚入职半年的新人,是偏重于学习框架还是学习JS基础好一些?

@Rorchach

这个很难说,因为相差不大。

我觉得对于新人来说,最重要的是别纠结。不要纠结一个功能是这么实现好还是那么实现好,也别纠结是学习框架好还是学习基础好。答案是不重要。对于新人来说,无论入门学的是哪本书,第一个做的是哪个项目都不重要,因为真正的收获差距并不大。

新人的学习速度很快,随便哪本书或是哪个项目开始,完整坚持下来之后,应该都能接触到大多数知识点,剩下的确实就需要专研了。入门之后思考一下自己跨过那道“坎”的时候遇到了什么问题,再有针对性的学习。

所以,框架 or 基础,我觉得它们对于新人来说是“殊途同归”的。

  1. 啃完一个框架,你可能为了学习它而恶补一些基础,然后理解里面的设计**;但也有可能出现基础不扎实、知其然不知其所以然的情况。
  2. 学习基础,你可能缺少一些框架设计上的启发,功能虽然都能搞定,但总感觉做起来别扭,就好像背了很多单词之后虽然文章都能读懂,但是写作不好一样。

因此,用发展的眼光看待两种选择,历程可能是:

  1. 选择以框架为起点:读框架 → 补基础 → 理解框架 → 运用基础+框架设计**
  2. 选择以基础为起点:学基础 → 运用觉得锉 → 阅读框架 → 运用基础+框架设计**

从这个过程可以看出,对于那些有不断自我完善追求的程序员来说,选择什么样的起点并不重要,最终都能走到一个相同的结果上来。

@fouber
谢谢前辈详细的建议,不再纠结了,坚持一步步地迈过每一道坎,重要地是不断地追求完善,一个好的程序是漂亮的代码加上好的结构。

楼主好,看了DIV.IO, 感觉不错,说说自己的想法,当然我没有邀请码,只是看到了一部分。

  1. 这个分类,感觉还是可以在细化的,比如按照css分类,出现的 w3c help 和 剩下的其它的都不在一个维度上,我个人感觉像w3c、 w3c help、 msdn、 mdn等 他们是在一个级别的。初始化可以人肉分一下维度,然后在给每个加权重(比如有github地址的,根据forks、start数等),然后再加上交互,根据用户的赞数,用户的反馈不断的修正
  2. 缺少反馈的渠道,比如在编辑器里面只有一个,我推荐还有kindeditor;最好能再这个上边能添加一些讨论,给使用者一个交流的地方,也给后来者一个学习的地方,给作者一个完善的地方
  3. 楼主关于工程化的文章,收益很多,也稍有实践。如果楼主把这个网站开源,来实践完善作者工程化思路,给学习者有学习的地方,也有实践的地方。如果复杂的不够,那就想办法去增加复杂度,可以按照应用场景来分,来演化,也可以给出一个地方让大家来提出自己项目中的痛点,一个一个的来完善。
  4. 先这么多吧,如果有需要,愿意参与

万恶的备案

求 star 2333333

我们目前使用AMD方案,也是通过差不多的一个方式,在构建时扫描文件依赖生成依赖表,并根据依赖打包文件以解决重复打包的问题,就是说把combo服务提前到构建做了。

@fouber 对于all in one在修改代码时带来的流量浪费,我们用增量更新来解决。
对于模块加载combo,我们是把当前模块的一级依赖合并拉取:
https://github.com/mtjs/mt

@luyongfugx

不错,你们的框架以前关注过,很细致的优化方案。我在UC实现了用一种相对比较简单策略来更新localstorage缓存的 jscss 模块化资源,基于版本,简单粗暴。因为我们不是非常信任移动端localstorage的可靠性,所以目前用的缓存策略仅比Cache-Control/Expires效果好那么一点而已,足够可靠,对于localstorage则是能用就用。原来在百度实现的会稍微复杂一些,精细到单文件级别。

你们这个项目的实践情况怎么样?线上产品有哪些?改天仔细研究一下

@fouber 目前线上有大概10来个的spa webapp使用了,http://mt.tencent.com/ 这里列了几个,其实还有别的项目也使用没有列出来的,总的来说效果还可以,有的项目发布时候增量更新的命中率还是挺高的:)

@fouber 最近一直在研究前端工程和架构的问题,看了您的文章有所启发,感谢您的经验分享!

@fouber 学无止境233~

@fouber 近来在研究前端工程部署问题,从您的文章中获取不少知识,感谢您的分享!

commented

诶,看完评论,砸吧砸吧嘴。。。觉得意犹未尽

大神,怎么可以联系到你啊
scrat server start
报错:events.js:85

@acmeid
可以把这个问题放到 scrat的issues 上提出

@fouber前辈你好, 在学习前端自动化工具fis, -wL,-o,-p,-m -d等基本命令已经熟络了, 前端代码需要requirejs来模块化,然后require文件合并让我头疼不已, 翻了fis官网又去了慕课网看了fis教程,
终于在倒数第二课找到 https://github.com/fex-team/fis-amd-demo 地址,开始学习require资源合并,安装了下面三个工具
//npm install fis-postprocessor-amd -g
//npm install fis-postpackager-autoload -g
//npm install fis-packager-depscombine -g
跟着上面的demo跑了一天, 欲哭无泪,fis-conf.js反复试验,也没发现require资源正常合并,
/pkg/zrender.js /pkg/echarts.js 都是基本的define()的堆砌,也没法使用.
再次出发, 踏破铁鞋, 一顿翻腾,终于找到了你
求教:
fis如何让require资源文件合并,使用requireJS,(上面的文章我看了,你鼓励自己开发工具,我也想自己干,等我熟练开发fis插件,估计时间就来不及了, 嗨, 先谢谢你,期待你的解答.)

@tm-roamer

你贴的那个项目可能有点问题,最好在那个repos的issue中留言问问作者具体用法,我这里不方便展开讨论了。

但针对fis的解决方案,我想多啰嗦几句,希望能耐心读完以下内容:

模块化打包和加载这件事,说起来有点麻烦,原因在当前这篇blog也提到一些,究其根本,要从模块化框架说起,对于模块化框架来说,一般有三个功能:

1. 模块接口导入导出

一般模块化框架或方案(requirejs/seajs/nodejs)遵循的是CommonJS的模块内部 上下文规范,也就是模块内上下文中的 require 函数和 exports 对象,以及 modules.exports 属性。

在ES6中,给出了 importexport 关键字,写法可以在网上查到,这里不举例了。

2. 模块定义

现阶段模块化框架(requirejs/seajs)为了在浏览器中实现模块定义,提供模块接口导入导出的方式,还须提供模块定义函数 define(id, deps, factory) ,模块代码就写在factory参数的作用域中,并能实现模块接口导入导出的管理。

同样,在ES6中,也给出了 module 关键字,用于定义模块。

3. 模块加载

在浏览器端,模块化框架为了解决依赖问题,需要实现按依赖异步加载模块资源,加载完成后才能执行对应的模块代码,这是在浏览器中实现模块化所必须面对的现实问题,其实模块加载和模块化的核心部分关系并不大,它可以被独立实现。seajs和requirejs在模块加载上实现并不一致,连接口名称都不同,由此可见模块加载的特殊性。

个人觉得,模块加载是前端工程化的重要组成部分


以上,就是模块化框架的全部内容了,总结为一幅图大概是:

js-modules

模块化核心部分(导入导出、定义)代码其实很少,写一个也就4、50行而已,半小时手起刀落应该就能搞定,真正重点是模块加载。

模块加载有这么几个痛点:

  1. 按需加载:就是依赖什么就加载什么,别搞aio(all-in-one)。
  2. 并行加载:不搞aio的打包,但也不能串行加载,有的框架要加载完才知道依赖,这样就串行请求了,工程上根本用不了。
  3. 请求合并:在HTTP1.1时代,虽然没有明显的证据表明请求合并比不合并能快多少,但是大家确实心里有阴影,不然也不会有all-in-one了,请求合并这件事还挺刚需的。

模块化框架做的好不好,加载是重点,以上三条,则是重点中的重点。

别人的方案我就不说了,这里就讲讲fis的**,你可以考虑基于这样的**来理解fis的一些设计:

fis就是盯着这三件事设计的,把能去掉的部分都去掉了,不留一点冗余,为的就是工程应用上真正的性能优化。

确切的说,fis希望使用者,能利用fis深耕模块化加载方案,用最简洁的方式表达自己的工程诉求,如果直接依赖requirejs和seajs,我觉得诸多冗余。

基于前面讲过的这些,模块化框架其实并不难实现,首先用50行代码解决核心的定义、导入、导出问题,具体代码可以参考 umd 超简单的。

然后就是加载部分了,fis的核心功能,其实不是什么压缩、校验这些基础功能,而是扫描所有项目代码,识别其中的依赖关系,并整理出一个 map.json 的资源依赖关系表,里面记录了资源的id、url和依赖关系,形如:

{
    "res": {
        "a.js": {
           "type": "js",
           "url": "/static/js/a-912cf3.js",
           "deps": [ "b.js", "a.css" ]
        },
        "a.css": {
           "type": "css",
           "url": "/static/js/a-c02a39.css",
           "deps": [ "e.css" ]
        },
        "b.js": {
           "type": "js",
           "url": "/static/js/b-b2ef91.js",
           "deps": [ "c.js", "b.css" ]
        },
        "b.css": {
           "type": "css",
           "url": "/static/js/b-fc01b2.css"
        },
        "c.js": {
           "type": "js",
           "url": "/static/js/c-103cf2.js"
        },
        "e.css": {
           "type": "css",
           "url": "/static/js/e-d0f135.css"
        },
    }
}

如果配置了fis的打包,fis合并文件后还会把文件的合并信息也写入到这个map.json中:

{
    "res": {
        "a.js": {
           "type": "js",
           "url": "/static/js/a-912cf3.js",
           "deps": [ "b.js", "a.css" ],
           "pkg": "p0"
        },
        "a.css": {
           "type": "css",
           "url": "/static/js/a-c02a39.css",
           "deps": [ "e.css" ],
           "pkg": "p1"
        },
        "b.js": {
           "type": "js",
           "url": "/static/js/b-b2ef91.js",
           "deps": [ "c.js", "b.css" ],
           "pkg": "p0"
        },
        "b.css": {
           "type": "css",
           "url": "/static/js/b-fc01b2.css",
           "pkg": "p1"
        },
        "c.js": {
           "type": "js",
           "url": "/static/js/c-103cf2.js",
           "pkg": "p0"
        },
        "e.css": {
           "type": "css",
           "url": "/static/js/e-d0f135.css",
           "pkg": "p1"
        },
    },
    "pkg": {
        "p0": {
            "url": "/static/pkg/p0-ac0f334.js",
            "type": "js",
            "has": [ "c.js", "b.js", "a.js" ]
        },
        "p1": {
            "url": "/static/pkg/p1-03d5ba.css",
            "type": "css",
            "has": [ "e.css", "b.css", "a.css" ]
        }
    }
}

好了,fis的想法是,团队花点时间写一个好一点的loader,利用这张表加载资源,肯定能同时解决前面说到的三个问题,顺便还能做一下静态资源localstorage缓存什么的,不是更妙?

比如源代码这么写:

<!DOCTYPE html>
<html>
    ...
    <script src="loader.js"></script>
    <script>
        loader.registerMap(__FIS_MAP__);
        loader.fetch('a.js', function(a){
            console.log(a);
        });
    </script>
    ...
</html>

通过写一个简单的fis插件,就可以把上述表的内容构建后生成到html中,替换那个我们跟构建工具约定好的 __FIS_MAP__ 变量,得到:

<!DOCTYPE html>
<html>
    ...
    <script src="loader.js"></script>
    <script>
        loader.registerMap({
            "res": {
                "a.js": {
                    "type": "js",
                    "url": "/static/js/a-912cf3.js",
                    "deps": [ "b.js", "a.css" ],
                    "pkg": "p0"
                },
                "a.css": {
                    "type": "css",
                    "url": "/static/js/a-c02a39.css",
                    "deps": [ "e.css" ],
                    "pkg": "p1"
                },
                "b.js": {
                    "type": "js",
                    "url": "/static/js/b-b2ef91.js",
                    "deps": [ "c.js", "b.css" ],
                    "pkg": "p0"
                },
                "b.css": {
                    "type": "css",
                    "url": "/static/js/b-fc01b2.css",
                    "pkg": "p1"
                },
                "c.js": {
                    "type": "js",
                    "url": "/static/js/c-103cf2.js",
                    "pkg": "p0"
                },
                "e.css": {
                    "type": "css",
                    "url": "/static/js/e-d0f135.css",
                    "pkg": "p1"
                },
            },
            "pkg": {
                "p0": {
                    "url": "/static/pkg/p0-ac0f334.js",
                    "type": "js",
                    "has": [ "c.js", "b.js", "a.js" ]
                },
                "p1": {
                    "url": "/static/pkg/p1-03d5ba.css",
                    "type": "css",
                    "has": [ "e.css", "b.css", "a.css" ]
                }
            }
        });
        loader.fetch('a.js', function(a){
            console.log(a);
        });
    </script>
    ...
</html>

仔细观察表就知道,我们想加载什么模块都能提前知道模块的依赖,并且知道模块被合并到哪个文件中了,这样我们就能通过loader框架按需的、并行的、合并请求的加载资源,进一步深耕loader,提升项目性能,把性能优化和开发分离开,实现工程层面的优化,这都将成为可能。

说了这么多,希望你能读到这里。

你的问题是fis与requirejs的结合,很抱歉,我还没有真正尝试花时间解决这个问题,fis和requirejs配合肯定能实现,如果给我1天左右的时间应该就能写出来fis的构建配置,然后把资源表传递给requirejs,并扩展requirejs的资源加载方式,改成查表加载,提升requirejs的加载性能。

但是我始终都没有这么搞,因为个人觉得这样做有点可笑。根据上述工程化方案介绍,这个loader能用到requirejs中现成的代码很有限,基本要深度接管requirejs资源加载的部分,将其重写,在这样种情况下,所谓的结合requirejs,等于仅仅用到了requirejs中的那50行的模块管理代码,其他将近2000行的代码都是冗余,我再额外添加几百行loader的代码实现高性能的资源加载,这不是很浪费么。。。基于这些考虑,fis团队才始终没有认真的写过fis与那些主流模块化框架的整合demo,感觉有点自欺欺人,糊弄小白用户。

总的来说,fis的模块化加载方案和requirejs的设计初衷有很大不同,requirejs为了不强依赖工具,写了大量代码解决无构建情况下的模块加载处理,但fis觉得,工程上应用时,为了高性能应该必然需要构建(包括开发阶段),无须多余考虑不构建的情况,这使得二者的设计方案有本质的区别。

真高兴你能读到这里!

当然,诚如你所讲,项目伊始,资源有限,想找个现成的方案,这个可以理解,fis是有这样现成的框架和方案的,请看这里: 基于FIS的纯前端模块化解决方案pure

一个无关紧要的小点:ES6现在也没用上module关键字。

@hax 没有module关键字,怎么在同一个文件中定义多个模块?是用一个文件对应一个模块的方式么?如果是这样的话,那import就要具备加载能力了?

@hax 我看到有人介绍 ES.next 中有module关键字

谢谢@fouber前辈, 谢谢你的真诚和平和, 你回答的比我问的还多, fis-pure-demo跑了几遍, mod.js可以支持多个define()在同一个js文件里, 好酷, 小变通解决大问题, 你的话我真真的听懂和领悟了, 你的blog文章都很长, 是经过深度思考的和实践检验的, fis是个非常优秀的工具, 可是需要技术选型, 领导希望慎重, 对require依依不舍, 日后还需要支持angularjs的Jasmine测试驱动开发模式, 让我研究一下主流的grunt工具, 如果我自己选, 根据我的性取向,一定选择fis, 我知道一个经过深度思考的工具往往很优秀, 嗨, 可惜我不是领导, 你的认真答复让我感动不已,真诚的谢谢你, 很抱歉, 假如没有选择fis,总感觉一种让你白耽误工夫的愧疚. 再次真诚的谢谢你.

他就是这样让我爱上他的。。。楼上你要小心了。。。

性取向...

commented

有状况...

性取向是什么才会选 fls 。。。

@fouber
曾经有用module关键字,不过后来都去掉了。
所以确实是一个文件对应一个模块。
加载问题是单独的,和import语法关系不大。我猜你可能指的是合并后的情况,确实没有module关键字后,简单合并是不行的,必须被转换为loader调用。

现在思路都差不多吧。YUI在没有工具的情况下也可以动态计算依赖combo后的url发起请求服务端合并文件,按需加载也可以这么做,但是计算放在浏览器运行js时太慢,而且老版本ie url长度限制还要拆分多次请求。
@张云龙所说的利用工具计算源码中的依赖生成配置对象,减去了动态计算的时间成本会快一些。但是个人感觉模块加载器计算依赖表是一项基础功能,不应去掉。fis的modjs短小到“同步”加载,还需手动添加维护script在模版中,其他计算全部用工具,不是很喜欢。jQuery这种粒度的前端代码或许这样做显不出什么,但是YUI Dojo这样的细粒度模块化框架就不适用。
另外组件化维护的就近依赖感觉采用异步模块化框架也完全没有问题,可以写个scanner解析html或者模版中的(一个或多个)模块入口文件,去重计算,并且实现打包。
类似

<div soi-role="widget" soi-id="widget-a">
  <link href="a.css" rel="stylesheet" />
  <link href="a.css" rel="stylesheet" />
  <script src="xmd.js"></script>
  <script src="app.js?amd"></script>
</div>

这样的模块在不需要时也可以一并删除,而且前端开发人员在本地调试样式可以直接双击看到效果。
一些特殊的架构bigpipe或者quickling需要和rd一起在联调环境。
另:最近看到fisp模拟的quickling其实不是facebook真正的工作方式,因为人家原本的目标就是减少服务端渲染开销。但恰恰相反fisp实际上还多了一层smarty插件处理过程。
ps: po主在松鼠的实践方式其实和我理想中的最为接近,也深深的为你点赞。

@AceMood

问题一:关于YUI在没有工具的情况下也可以动态计算依赖combo的问题

在没有工具的支持下,在模块加载完成之前,无法知道这个模块还依赖了其他什么模块,以及它依赖的模块又依赖了什么,从而无法提前完成combo url的计算。依赖是一种有深度的树形结构,没有工具支持的情况下最多只能做到combo依赖树中同一层级的资源,深层次的依赖关系都无法预先知道。

问题二:关于利用工具抽取依赖关系的问题

有资源表和基于表的资源管理框架可以很容易的将开发和部署以及性能优化分离开,表的结构比较简单,可以由工具生成,这样工具部分就有大部分功能可以在不同的架构中通用了,前端渲染架构把表注入到前端页面,配合模块加载器加载资源,后端渲染架构把表部署到模板服务器配合模板引擎实现组件化加载,深耕资源管理框架(在前端就是模块加载器,在后端就是模板的资源引用扩展)可以获得更多的性能收益,而且优化过程对开发透明,同一套构建工具可以适应不同的前后端方案,黑盒部分很小,这是我所推崇的做法。

问题三:关于举的例子可以用异步模块化框架加载的问题

上面你举得例子应该只是写法上的改变而已,最终还是要分析写法然后转成资源表交给模块化框架进行加载吧,“采用异步模块化框架也完全没有问题”这里我没有太理解具体的含义,不过隐约感觉可能不适用于组件嵌套时产生了多层次依赖树的问题,跟前述一样,如果一次异步只能加载依赖树上同一层级的节点,那么多级依赖的结构会导致资源请求变成串行的。

问题四:关于代码可以双击本地预览的问题

“前端开发人员在本地调试可以直接双击看到效果”个人觉得不是一个合理的开发流程,web应用应该从开发阶段就在一个web容器中进行开发、调试和预览,其难点仅在于如何给工程师提供一个便捷的本地容器,但好处很多,但若想完全能以file协议打开浏览,我觉得实在是意义不大,首先排除了直接开发模板的可能;其次,毕竟web工程不是一个本地部署的GUI,有太多元素需要走http协议加载,我不相信有那个团队的代码可以用file协议预览开发的,如果有相信也是类似一种“先写好html,再交给后端模板化”的开发方式,多少有些不合理吧。。。

问题五:关于fisp的quickling的问题

其实facebook当年提出了三个技术概念:

  1. pagelet:页面局部,用于划分页面上的一些区域,它属于一种开发概念,并不是技术实现;pagelet是下面二者的基础。
  2. bigpipe:以chunk的方式把pagelet分片输出,目的是在后端读取数据渲染模板的时候可以以pagelet为单位并行异步获取数据,然后以js片段的形式flush输出,这个需要对php进行改造,支持异步
  3. quickling:页面间跳转的时候,不是全刷,而是以pagelet为单位,局部替换。有些人好像也管它叫pjax。

简单来说,pagelet提供了一种单位,bigpipe解决了页面首次加载的展现性能问题,quickling解决了页面间切换的性能和体验问题。

当然,三者配合使用效果最好,但bigpipe和quickling并不一定是捆绑的,二者都可以单独实现。只实现bigpipe,效果是渲染速度快了,流水线加工页面,没有quickling只不过页面跳转是全刷而已。单纯实现quickling而不做bigpipe,这样虽然没有首次访问页面的加速效果,但是页面间切换效果很不错,是异步局刷的。fisp设计的时候我们确实没有办法推动百度的PHP在底层做异步支持的改造,所以我们只实现了quickling。

不过说实话,我确实对fisp的quickling(pjax)实现不太满意,当时没有深入到产品线去打磨这套优化方案,所以凭着一知半解随便写写了,我在的时候也只是声称有这个模式,但没有真正交付产品线使用。直到我来到UC,在印度做一些产品的时候,scrat的webapp模式性能堪忧,于是在nodejs上选了swig模板引擎,实现了pagelet和quickling(bigpipe仍未实现,感觉移动端不适用),这时候才更深入的研究了一下其中的细节,比如用了pushState之后历史记录的处理、pagelet的切换等问题,有兴趣的话可以用 scrat init 命令创建一个seo项目,看一下我们的新方案。

在线demo:http://scrat-pagelet-demo-fouber.c9.io/inbox (偶尔可能服务不可用)
demo源码:scrat init,选择seo模式即可
线上业务:http://music.uodoo.com/index?uc_param_str=dnfrpfbivesscpgimibtbmntnisieijblauputoggd

这个方案还根据google的移动端性能优化最佳实践 https://developers.google.com/speed/docs/insights/mobile 做了首屏CSS内嵌的优化,提供了一个 {% ATF %} 首屏位置标签,把这个标签写到模板中,标签以前的资源会内嵌,标签以后的资源会link加载,以实现google给出的首屏优化策略。

因为有了模板中的资源管理框架,才使得做各种优化策略非常方便,所以我觉得工具止步于依赖抽取就ok了,剩下的事交给框架去做。

@fouber
问题一,我明白你的意思,实际情况是,不太可能在上线页面引入一个loader就可以按需加载,延时性,请求数,同于请求并发限制等等都是问题。所以一定会有打包工具介入,我想表达的是希望loader也提供异步加载并计算依赖的基本功能。

问题二,我同意你的,不论是生成配置对象,或者是map.json供后端使用

问题三,我是想通过html部署到本地server可直接预览,或者本地文件打开预览一些基本交互和调试样式兼容性。异步的加载器更加灵活,最终的构建过程一定会扫描页面中的script最终分析依赖打包合并。这里分为开发时和部署时,开发时异步加载,哪个文件有问题一清二楚,部署时自然通过构建工具做,不会实时分析加载。当然提测后采用合并打包+sourcemap也可以,我只是不想在页面中手动维护script的数量,这也是举例Dojo和jQuery的原因。

问题四,你说的情况却是存在,但是前端调试样式兼容性,我觉得双击更方便,或者进一步,js工程师本地tpl+json+server也可以,但是如果页面制作过程和js开发是不同的人来做呢,页面制作工程师擅长静态页面制作。你说的观点好处我明白,我也会在团队中尝试这样做看看是否合适,因为我发现目前的团队fe把页面制作也做了,所以你说的方式现在可以考虑了。

问题五,facebook的实现方式我了解一些,不再讨论了,性能优化说起来太广。如果尺子都没选好,都不知道哪里慢了,所有的优化都是拍脑袋决定。最近才零星接触些fis和fisp,还在体会,但是发现了你们的这个实现方式后,有点疑惑所以问问。现在我在百度周围的人动不动解放前端生产力让本人很唏嘘,因为总要考虑易用性,迁移成本和开发习惯。如果别人做了一套东西在我的IDE里面连语法错误提示都没有,似js非js还要依赖插件就很麻烦(这不是说fis,因为它不存在这样的问题),引入越多出错几率越大。我希望为我的团队积累一些属于我们自己的东西,真正简单可依赖,能够汲取并借鉴业界老大哥的经验和方式,但最终实现还要靠我们自己。

最后谢谢云龙上面耐心的回答,有机会当面请教

@AceMood

其他点都赞同,唯独问题四,我个人是非常反对 “页面制作过程和js开发是不同的人来做” 这种分工方式。

前端作为一种GUI软件,我还是觉得应该以组件为单位进行分治,每个组件内维护自己的HTML/JS/CSS/图片等程序资源,是最小的开发单位。这就要求每个工程师都具备完整的前端基本技能(所谓重构和JS)。


组件化的前端开发

基于组件化开发的项目分工


强调:这里的组件化开发其目的是 分而治之 ,是为了更好的进行系统拆分和工程维护,它并不是为了技术上的复用。复用在前端这种定制化程度极高的软件开发领域意义并不是非常大,但分治却是前端工程必须的。

前端工程师的工作内容无论从分工合理性的角度还是工程维护角度,亦或者个人职业发展和成长的角度来看,都不应该以“重构和JS”来划分,二者不应该被人为的割裂开。

@fouber

基于组件化开发的项目分工

赞同,像我们平时开发都是以 RIA 为主,完全不可能布局和JS分两个人。

之前也和同事讨论过如何高效率多人维护同时一个项目,当时讨论结果是每个人负责一个业务模块。没有想到这么细分的组件模块划分。

请教:如上面2个图片所示,组件对外提供的接口应该是怎么样的?如何调用?

@nimojs

这要取决于你使用什么渲染模型和什么组件化框架。

1. 后端渲染模型

就是在服务端拼装模板生成html,前端只是负责增加交互能力

这种情况下组件一般对外暴露的是模板层面的组件调用接口和前端层面组件事件派发。后端渲染模型下,页面在服务端拼装,到达前端的时候已经是完整的HTML内容了,HTML先呈现,然后js才添加事件实现复杂的交互能力,这时候我们通常会设计一种事件中心的机制来解决组件间通信问题,所以一般情况下组件暴露的只是模板层面的引用和自身所能派发的事件列表。

2. 前端渲染模型

通常是webapp模式,由js全权负责拼装html。

由于是js控制的界面构建,整个过程是程序可以干预的,因此js会对组件进行封装,并管理其生命周期,也就有了基本的创建实例(init、create、new)、添加到dom树(render)、视图更新(update)、实例销毁(destroy)等接口,以及其他特定的组件方法。

现在有了很多MVVM框架,是组件化开发的利器,所以很多人会选择一个这样的框架,比如 vuejs(推荐)、react、angularjs等,由它们负责管理组件的生命周期,然后通过驱动数据解决组件状态改变和通信问题,最终面对的是数据操作,不需要太多接口

理解了,非常感谢!

内容后的讨论收获颇多,赞。

你好。问个和模块化无关的知识点哈。js 里面 if(!!o.flag) 好多人这么写。!! 这样写 有什么意义吗?不明白为什么要这么写。多谢

commented

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

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

commented

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

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

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

commented

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

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

@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推广和落地的最大阻碍。

@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上.

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

commented

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

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

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

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

@lenxeon 这个貌似 nginx 就能做

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

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

@lenxeon

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

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

commented

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

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

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

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

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

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

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

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

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

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

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

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

commented

666