您的位置:奥门新浦京网址 > Wed前段 > 我们是如何做好前端工程化和静态资源管理,前

我们是如何做好前端工程化和静态资源管理,前

发布时间:2019-10-17 03:40编辑:Wed前段浏览(139)

    我们是如何做好前端工程化和静态资源管理

    2016/07/30 · 基础技术 · 工程化, 静态资源

    原文出处: 凹凸实验室   

    图片 1

    随着互联网的发展,我们的业务也日益变得更加复杂且多样化起来,前端工程师也不再只是做简单的页面开发这么简单,我们需要面对的十分复杂的系统性问题,例如,业务愈来愈复杂,我们要如何清晰地梳理;团队人员愈来愈多,我们要如何更好地进行团队协作;功能愈来愈多,我们要如何保证页面的性能不至于下降,等等。所有的这些都可以归结为如何提升开发体验和性能问题。

    前端优化带来的思考,浅谈前端工程化

    2015/10/26 · 前端职场 · 2 评论 · 工程化

    原文出处: 叶小钗(@欲苍穹)   

    Service Worker初体验

    2016/01/06 · JavaScript · Service Worker

    原文出处: AlloyTeam   

    在2014年,W3C公布了service worker的草案,service worker提供了很多新的能力,使得web app拥有与native app相同的离线体验、消息推送体验。
    service worker是一段脚本,与web worker一样,也是在后台运行。作为一个独立的线程,运行环境与普通脚本不同,所以不能直接参与web交互行为。native app可以做到离线使用、消息推送、后台自动更新,service worker的出现是正是为了使得web app也可以具有类似的能力。

     

    service worker可以:

    1. 后台消息传递
    2. 网络代理,转发请求,伪造响应
    3. 离线缓存
    4. 消息推送
    5.  … …

    本文以资源缓存为例,说明一下service worker是如何工作的。

    提升开发体验

    我们主要从以下三个方面来提升我们的开发体验。

    重复优化的思考

    这段时间对项目做了一次整体的优化,全站有了20%左右的提升(本来载入速度已经1.2S左右了,优化度很低),算一算已经做了四轮的全站性能优化了,回顾几次的优化手段,基本上几个字就能说清楚:

    传输层面:减少请求数,降低请求量 执行层面:减少重绘&回流

    1
    2
    传输层面:减少请求数,降低请求量
    执行层面:减少重绘&回流

    传输层面的从来都是优化的核心点,而这个层面的优化要对浏览器有一个基本的认识,比如:

    ① 网页自上而下的解析渲染,边解析边渲染,页面内CSS文件会阻塞渲染,异步CSS文件会导致回流

    ② 浏览器在document下载结束会检测静态资源,新开线程下载(有并发上限),在带宽限制的条件下,无序并发会导致主资源速度下降,从而影响首屏渲染

    ③ 浏览器缓存可用时会使用缓存资源,这个时候可以避免请求体的传输,对性能有极大提高

    衡量性能的重要指标为首屏载入速度(指页面可以看见,不一定可交互),影响首屏的最大因素为请求,所以请求是页面真正的杀手,一般来说我们会做这些优化:

    生命周期

    先来看一下一个service worker的运行周期

    图片 2
    上图是service worker生命周期,出处

    图中可以看到,一个service worker要经历以下过程:

    1.  安装

    2.  激活,激活成功之后,打开chrome://inspect/#service-workers可以查看到当前运行的service worker

    图片 3

    1. 监听fetch和message事件,下面两种事件会进行简要描述

    2. 销毁,是否销毁由浏览器决定,如果一个service worker长期不使用或者机器内存有限,则可能会销毁这个worker

    规范化

    当团队人员不断扩充时,我们需要制定统一的规范来对平时的开发工作做出一定约束和指导。统一的规范包括前端的代码规范,根据规范定义好一套代码检查的规则,在代码提交的时候进行检查,让开发人员知道自己的代码情况。

    同时,根据以往的开发经验,我们制定了统一的项目框架,根据业务功能不同,将一个项目(app)拆分成不同的业务模块(module),而每一个模块都包含自身的页面(page)以及构成页面所需要的组件(widget),每一个项目涉及到app、module、page、widget这些已经约定好的概念,这样让项目结构更加清晰,而且让团队内不同业务的人员之间切换无障碍。

    图片 4

    减少请求数

    ① 合并样式、脚本文件

    ② 合并背景图片

    ③ CSS3图标、Icon Font

    fetch事件

    在页面发起http请求时,service worker可以通过fetch事件拦截请求,并且给出自己的响应。
    w3c提供了一个新的fetch api,用于取代XMLHttpRequest,与XMLHttpRequest最大不同有两点:

    1. fetch()方法返回的是Promise对象,通过then方法进行连续调用,减少嵌套。ES6的Promise在成为标准之后,会越来越方便开发人员。

    2. 提供了Request、Response对象,如果做过后端开发,对Request、Response应该比较熟悉。前端要发起请求可以通过url发起,也可以使用Request对象发起,而且Request可以复用。但是Response用在哪里呢?在service worker出现之前,前端确实不会自己给自己发消息,但是有了service worker,就可以在拦截请求之后根据需要发回自己的响应,对页面而言,这个普通的请求结果并没有区别,这是Response的一处应用。

    下面是在中,作者利用fetch api通过fliker的公开api获取图片的例子,注释中详细解释了每一步的作用:

    JavaScript

    /* 由于是get请求,直接把参数作为query string传递了 */ var URL = ''; function fetchDemo() { // fetch(url, option)支持两个参数,option中可以设置header、body、method信息 fetch(URL).then(function(response) { // 通过promise 对象获得相应内容,并且将响应内容按照json格式转成对象,json()方法调用之后返回的依然是promise对象 // 也可以把内容转化成arraybuffer、blob对象 return response.json(); }).then(function(json) { // 渲染页面 insertPhotos(json); }); } fetchDemo();

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /* 由于是get请求,直接把参数作为query string传递了 */
    var URL = 'https://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=your_api_key&format=json&nojsoncallback=1&tags=penguins';
     
    function fetchDemo() {
      // fetch(url, option)支持两个参数,option中可以设置header、body、method信息
      fetch(URL).then(function(response) {
        // 通过promise 对象获得相应内容,并且将响应内容按照json格式转成对象,json()方法调用之后返回的依然是promise对象
        // 也可以把内容转化成arraybuffer、blob对象
        return response.json();
      }).then(function(json) {
        // 渲染页面
        insertPhotos(json);
      });
    }
     
    fetchDemo();

    fetch api与XMLHttpRequest相比,更加简洁,并且提供的功能更全面,资源获取方式比ajax更优雅。兼容性方面:chrome 42开始支持,对于旧浏览器,可以通过官方维护的polyfill支持。

    组件化

    在项目中引入组件化的概念,这里的组件对应上文讲到的widget,每一个组件都会包含组件自身的模板、css、js、图片以及说明文件,我们使用组件来拼装页面,像搭积木一样来拼装我们的页面,同时一个组件内可以调用另一个组件。

    图片 5

    在拿到设计稿后,我们首先需要确定哪些需要做成公共组件,那些是要做成独立组件,以及组件间如何进行通信。在页面中调用这些组件后,会自动加载组件的模板以及组件的静态资源,而当组件不再需要时,只要移除掉组件引用,那么相应的模板和静态资源也会不再加载。

    组件化的好处主要有这么几点

    • 管理方便,我们可以把一个独立功能相关的文件在工程目录中放在一起,这样代码管理起来会非常便利
    • 组件复用,通过抽取公共组件,可以实现组件复用,从而减少工作量,创造价值
    • 分而治之,这是组件化最重要的一点,将页面组件化,就是对页面功能的拆分,将一个大的工程拆成小的零件,我们只需要关注每一个零件的功能,极大地降低了页面的开发与维护的难度

    降低请求量

    ① 开启GZip

    ② 优化静态资源,jQuery->Zepto、阉割IScroll、去除冗余代码

    ③ 图片无损压缩

    ④ 图片延迟加载

    ⑤ 减少Cookie携带

    很多时候,我们也会采用类似“时间换空间、空间换时间”的做法,比如:

    ① 缓存为王,对更新较缓慢的资源&接口做缓存(浏览器缓存、localsorage、application cache这个坑多)

    ② 按需加载,先加载主要资源,其余资源延迟加载,对非首屏资源滚动加载

    ③ fake页技术,将页面最初需要显示Html&Css内联,在页面所需资源加载结束前至少可看,理想情况是index.html下载结束即展示(2G 5S内)

    ④ CDN

    ……

    从工程的角度来看,上述优化点半数以上是重复的,一般在发布时候就直接使用项目构建工具做掉了,还有一些只是简单的服务器配置,开发时不需要关注。

    可以看到,我们所做的优化都是在减少请求数,降低请求量,减小传输时的耗时,或者通过一个策略,优先加载首屏渲染所需资源,而后再加载交互所需资源(比如点击时候再加载UI组件),Hybrid APP这方面应该尽可能多的将公共静态资源放在native中,比如第三方库,框架,UI甚至城市列表这种常用业务数据。

    message事件

    页面和serviceWorker之间可以通过posetMessage()方法发送消息,发送的消息可以通过message事件接收到。

    这是一个双向的过程,页面可以发消息给service worker,service worker也可以发送消息给页面,由于这个特性,可以将service worker作为中间纽带,使得一个域名或者子域名下的多个页面可以自由通信。

    这里是一个小的页面之间通信demo

    自动化编译

    在前端开发中,我们总是会去使用很多工具、手段来优化代码、提升开发效率,例如,我们会使用sass、less等CSS预处理工具来编写更好维护的样式代码,我们也会使用CSSLint、eslint等代码检查工具来检查代码的语法错误,使用文件合并压缩等手段来减少资源大小,除此之外我们还会去做雪碧图合并、多倍图处理、字体压缩处理、代码发布等等。

    曾经有大神说过,超过90s的工作都应该自动化掉。而以上所有的这些工作,贯穿我们整个开发流程,但是不同工具的切换不但显得凌乱,而且影响开发效率。在自动化、工程编译的思想早已深入人心的当下,我们当然也要紧跟潮流,所以我们考虑通过自动化手段来提升我们的效率,让所有操作可以一键式开速执行完。

    我们将通过定义好一系列的编译任务,按照一定顺序依次对我们的项目自动进行编译操作,最后产生出可上线的代码。

    拦路虎

    有一些网站初期比较快,但是随着量的积累,BUG越来越多,速度也越来越慢,一些前端会使用上述优化手段做优化,但是收效甚微,一个比较典型的例子就是代码冗余:

    ① 之前的CSS全部放在了一个文件中,新一轮的UI样式优化,新老CSS难以拆分,CSS体量会增加,如果有业务团队使用了公共样式,情况更不容乐观;

    ② UI组件更新,但是如果有业务团队脱离接口操作了组件DOM,将导致新组件DOM更新受限,最差的情况下,用户会加载两个组件的代码;

    ③ 胡乱使用第三方库、组件,导致页面加载大量无用代码;

    ……

    以上问题会不同程度的增加资源下载体量,如果听之任之会产生一系列工程问题:

    ① 页面关系错综复杂,需求迭代容易出BUG;

    ② 框架每次升级都会导致额外的请求量,常加载一些业务不需要的代码;

    ③ 第三方库泛滥,且难以维护,有BUG也改不了;

    ④ 业务代码加载大量异步模块资源,页面请求数增多;

    ……

    为求快速占领市场,业务开发时间往往紧迫,使用框架级的HTML&CSS、绕过CSS Sprite使用背景图片、引入第三方工具库或者UI,会经常发生。当遇到性能瓶颈时,如果不从根源解决问题,用传统的优化手段做页面级别的优化,会出现很快页面又被玩坏的情况,几次优化结束后我也在思考一个问题:

    前端每次性能优化的手段皆大同小异;代码的可维护性也基本是在细分职责; 既然每次优化的目的是相同的,每次实现的过程是相似的,而每次重新开发项目又基本是要重蹈覆辙的,那么工程化、自动化可能是这一切问题的最终答案

    1
    2
    前端每次性能优化的手段皆大同小异;代码的可维护性也基本是在细分职责;
    既然每次优化的目的是相同的,每次实现的过程是相似的,而每次重新开发项目又基本是要重蹈覆辙的,那么工程化、自动化可能是这一切问题的最终答案

    工程问题在项目积累到一定量后可能会发生,一般来说会有几个现象预示着工程问题出现了:

    ① 代码编写&调试困难

    ② 业务代码不好维护

    ③ 网站性能普遍不好

    ④ 性能问题重复出现,并且有不可修复之势

    像上面所描述情况,就是一个典型的工程问题;定位问题、发现问题、解决问题是我们处理问题的手段;而如何防止同一类型的问题重复发生,便是工程化需要做的事情,简单说来,优化是解决问题,工程化是避免问题,今天我们就站在工程化的角度来解决一些前端优化问题,防止其死灰复燃。

    文中是我个人的一些开发经验,希望对各位有用,也希望各位多多支持讨论,指出文中不足以及提出您的一些建议

    利用service workder缓存文件

    下面介绍一个利用service worker缓存离线文件的例子
    准备index.js,用于注册service-worker

    JavaScript

    if (navigator.serviceWorker) { navigator.serviceWorker.register('service-worker.js').then(function(registration) { console.log('service worker 注册成功'); }).catch(function (err) { console.log('servcie worker 注册失败') }); }

    1
    2
    3
    4
    5
    6
    7
    if (navigator.serviceWorker) {
        navigator.serviceWorker.register('service-worker.js').then(function(registration) {
            console.log('service worker 注册成功');
        }).catch(function (err) {
            console.log('servcie worker 注册失败')
        });
    }

    在上述代码中,注册了service-worker.js作为当前路径下的service worker。由于service worker的权限很高,所有的代码都需要是安全可靠的,所以只有https站点才可以使用service worker,当然localhost是一个特例。
    注册完毕,现在开始写service-worker.js代码。
    根据前面的生命周期图,在一个新的service worker被注册以后,首先会触发install事件,在service-workder.js中,可以通过监听install事件进行一些初始化工作,或者什么也不做。
    因为我们是要缓存离线文件,所以可以在install事件中开始缓存,但是只是将文件加到caches缓存中,真正想让浏览器使用缓存文件需要在fetch事件中拦截

    JavaScript

    var cacheFiles = [ 'about.js', 'blog.js' ]; self.addEventListener('install', function (evt) { evt.waitUntil( caches.open('my-test-cahce-v1').then(function (cache) { return cache.addAll(cacheFiles); }) ); });

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var cacheFiles = [
        'about.js',
        'blog.js'
    ];
    self.addEventListener('install', function (evt) {
        evt.waitUntil(
            caches.open('my-test-cahce-v1').then(function (cache) {
                return cache.addAll(cacheFiles);
            })
        );
    });

    首先定义了需要缓存的文件数组cacheFile,然后在install事件中,缓存这些文件。
    evt是一个InstallEvent对象,继承自ExtendableEvent,其中的waitUntil()方法接收一个promise对象,直到这个promise对象成功resolve之后,才会继续运行service-worker.js。
    caches是一个CacheStorage对象,使用open()方法打开一个缓存,缓存通过名称进行区分。
    获得cache实例之后,调用addAll()方法缓存文件。

    这样就将文件添加到caches缓存中了,想让浏览器使用缓存,还需要拦截fetch事件

    JavaScript

    // 缓存图片 self.addEventListener('fetch', function (evt) { evt.respondWith( caches.match(evt.request).then(function(response) { if (response) { return response; } var request = evt.request.clone(); return fetch(request).then(function (response) { if (!response && response.status !== 200 && !response.headers.get('Content-type').match(/image/)) { return response; } var responseClone = response.clone(); caches.open('my-test-cache-v1').then(function (cache) { cache.put(evt.request, responseClone); }); return response; }); }) ) });

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 缓存图片
    self.addEventListener('fetch', function (evt) {
        evt.respondWith(
            caches.match(evt.request).then(function(response) {
                if (response) {
                    return response;
                }
                var request = evt.request.clone();
                return fetch(request).then(function (response) {
                    if (!response && response.status !== 200 && !response.headers.get('Content-type').match(/image/)) {
                        return response;
                    }
                    var responseClone = response.clone();
                    caches.open('my-test-cache-v1').then(function (cache) {
                        cache.put(evt.request, responseClone);
                    });
                    return response;
                });
            })
        )
    });

    通过监听fetch事件,service worker可以返回自己的响应。

    首先检缓存中是否已经缓存了这个请求,如果有,就直接返回响应,就减少了一次网络请求。否则由service workder发起请求,这时的service workder起到了一个中间代理的作用。

    service worker请求的过程通过fetch api完成,得到response对象以后进行过滤,查看是否是图片文件,如果不是,就直接返回请求,不会缓存。

    如果是图片,要先复制一份response,原因是request或者response对象属于stream,只能使用一次,之后一份存入缓存,另一份发送给页面。
    这就是service worker的强大之处:拦截请求,伪造响应。fetch api在这里也起到了很大的作用。

     

    service worker的更新很简单,只要service-worker.js的文件内容有更新,就会使用新的脚本。但是有一点要注意:旧缓存文件的清除、新文件的缓存要在activate事件中进行,因为可能旧的页面还在使用之前的缓存文件,清除之后会失去作用。

     

    在初次使用service worker的过程中,也遇到了一些问题,下面是其中两个

    提升性能

    我们主要从以下四个方面来做好性能优化。

    消灭冗余

    我们这里做的第一个事情便是消除优化路上第一个拦路虎:代码冗余(做代码精简),单从一个页面的加载来说,他需要以下资源:

    ① 框架MVC骨架模块&框架级别CSS

    ② UI组件(header组件、日历、弹出层、消息框……)

    ③ 业务HTML骨架

    ④ 业务CSS

    ⑤ 业务Javascript代码

    ⑥ 服务接口服务

    因为产品&视觉会经常折腾全站样式加之UI的灵活性,UI最容易产生冗余的模块。

    问题1. 运行时间

    service worker并不是一直在后台运行的。在页面关闭后,浏览器可以继续保持service worker运行,也可以关闭service worker,这取决与浏览器自己的行为。所以不要定义一些全局变量,例如下面的代码(来自):

    JavaScript

    var hitCounter = 0; this.addEventListener('fetch', function(event) { hitCounter++; event.respondWith( new Response('Hit number ' + hitCounter) ); });

    1
    2
    3
    4
    5
    6
    7
    8
    var hitCounter = 0;
     
    this.addEventListener('fetch', function(event) {
      hitCounter++;
      event.respondWith(
        new Response('Hit number ' + hitCounter)
      );
    });

    返回的结果可能是没有规律的:1,2,1,2,1,1,2….,原因是hitCounter并没有一直存在,如果浏览器关闭了它,下次启动的时候hitCounter就赋值为0了
    这样的事情导致调试代码困难,当你更新一个service worker以后,只有在打开新页面以后才可能使用新的service worker,在调试过程中经常等上一两分钟才会使用新的,比较抓狂。

    首屏优化

    页面的打开速度一直是大家非常关心的一个指标,一个页面打开太慢会让让用户失去等待的耐心,为了让用户更快地看到页面,我们考虑将页面中部分静态资源代码直接嵌入页面中,我们通过工具处理,在工程编译阶段,将指定的静态资源代码内嵌入页面中,这样可以减少HTTP请求,提升首屏加载速度,同时降低页面裸奔风险。

    UI组件

    UI组件本身包括完整的HTML&CSS&Javascript,一个复杂的组件下载量可以达到10K以上,就UI部分来说容易导致两个工程化问题:

    ① 升级产生代码冗余

    ② 对外接口变化导致业务升级需要额外开发

    问题2. 权限太大

    当service worker监听fetch事件以后,对应的请求都会经过service worker。通过chrome的network工具,可以看到此类请求会标注:from service worker。如果service worker中出现了问题,会导致所有请求失败,包括普通的html文件。所以service worker的代码质量、容错性一定要很好才能保证web app正常运行。

     

    参考文章:

    1. 

    2. 

    3. 

    4. 

    5. 

    1 赞 3 收藏 评论

    图片 6

    按需加载

    同时,我们考虑通过尽量减小页面体积来提升页面打开速度,在业务上我们将页面划分为一个个楼层组件,以京东美妆馆为例,页面中从上而下分为首焦、至IN尖货、今日特惠、潮流前沿、口碑榜单这么几个楼层组件,其实这个页面还有很长,内容非常多且复杂。

    图片 7

    之前我们的做法是整个页面直出,这样一次性加载的内容会非常多,为了提升打开速度,我们考虑通过按需加载的方式来优化页面的加载。我们在页面中只放每一个楼层的框架性代码,楼层的模板和数据都通过异步的方式去拉取,来实现楼层组件的按需加载,同时我们可以对模板以及数据进行缓存,以此来减少请求,做更极致的优化。在开发中我们以正常组件的方式去开发整个页面,随后通过编译工具,在代码编译阶段自动将楼层的模板抽离成一个独立的JS文件,并给楼层容器打上标记位,通过页面加载逻辑去按需拉取模板,再进行渲染。

    通过给楼层容器和模板分别加上标记位 o2-out-tpl-wrapper o2-out-tpl

    图片 8

    在编译时自动将指定的模板代码抽离成独立js文件

    图片 9

    并且给楼层容器打上标记

    图片 10

    同时在逻辑脚本适当位置自动加入模板的版本

    图片 11

    通过上述步骤,实现按需加载的自动化生成,在提升性能的同时,很好地解放我们生产力。

    UI升级

    最理想的升级是保持对外的接口不变甚至保持DOM结构不变,但多数情况的UI升级其实是UI重做,最坏的情况是不做老接口兼容,这个时候业务同事便需要修改代码。为了防止业务抱怨,UI制作者往往会保留两个组件(UI+UI1),如果原来那个UI是核心依赖组件(比如是UIHeader组件),便会直接打包至核心框架包中,这时便出现了新老组件共存的局面,这种情况是必须避免的,UI升级需要遵守两个原则:

    ① 核心依赖组件必须保持单一,相同功能的核心组件只能有一个

    ② 组件升级必须做接口兼容,新的特性可以做加法,绝不允许对接口做减法

    基于资源表加载

    根据页面组件化,通过工具分析,我们将获得页面与组件的依赖关系表,同时也能确认页面所引用资源的依赖关系,例如,我们在页面hello中同步引用组件topbar,那么依赖关系表中将会记录同步引用关系hello引用topbar.tpl、topbar.css、topbar.js,那么页面hello将会自动加载组件topbar的CSS与JS,同时依赖表会记录异步引用的关系,假如我们在组件C中通过API异步引用了组件D的js,那么会在依赖表中记录C异步引用D.js这一个依赖关系,这样D.js这个资源将会在用到的时候被异步调用。

    图片 12

    图片 13

    同步引用的资源通过生成combo形式链接,在服务端进行文件合并,这样在页面加载的时候,页面只会加载自己需要的同步资源,异步的资源将会在用到的时候再加载,有效避免资源冗余。同时删除、增加组件也非常方便,只需改动模板中对组件调用,通过编译工具会自动重新生成模板以及combo链接。

    我们可以将资源加载的操作抽离出来,形成一套统一的资源加载框架设计,这样我们使用的模板可以变得更加灵活,无论是纯html模板,还是PHP或Java之类的后端模板都能有效支持。编译工具扫描代码后只生成资源依赖表,我们通过实现各语言平台的资源加载框架,让不同语言的模板都能基于同一个资源依赖表进行资源加载。

    同时,对资源进行MD5重命名处理,文件md5重命名也是一种提升性能的有效手段,使用文件md5后开启服务器强缓存,可以提升缓存的利用率并避免不必要的缓存判断处理。但文件md5重命名后会出现开发时引用的文件名对不上的问题,这就需要在资源表中记录原文件名与md5重命名后之间的对应关系,当我们引用一个资源时,就会通过查表获取重命名后的资源名,然后利用代码中引用资源定位的能力来进行资源名自动替换。

    图片 14

    UI组成

    项目之初,分层较好的团队会有一个公共的CSS文件(main.css),一个业务CSS文件,main.css包含公共的CSS,并且会包含所有的UI的样式:

    图片 15

    半年后业务频道增,UI组件需求一多便容易膨胀,弊端马上便暴露出来了,最初main.css可能只有10K,但是不出半年就会膨胀至100K,而每个业务频道一开始便需要加载这100K的样式文件页面,但是其中多数的UI样式是首屏加载用不到的。

    所以比较好的做法是,main.css只包含最核心的样式,理想情况是什么业务样式功能皆不要提供,各个UI组件的样式打包至UI中按需加载:

    图片 16

    如此UI拆分后,main.css总是处于最基础的样式部分,而UI使用时按需加载,就算出现两个相同组件也不会导致多下载资源。

    静态资源预加载

    所谓静态资源预加载,就是当用户在进行浏览页面的时候,我们可以在当前页面静默加载下一个页面的静态资源,这样当用户进入到下一个页面时就能快速打开页面,从而在不知不觉中提升页面的打开速度。

    图片 17

    我们会在静态资源预加载平台上配置每一个页面id对应需要预加载页面资源的id,然后系统通过读取资源依赖表获取到所需要预加载的静态资源,生成预加载资源列表文件,再将文件推送到线上服务器,通过页面挂载js请求获取预加载资源列表,随后静默加载资源。在有了资源依赖表后,我们可以准确地分析到每一个页面引用资源的请求,就可以很好地实现静态资源预加载的功能。

    图片 18

    拆分页面

    一个PC业务页面,其模块是很复杂的,这个时候可以将之分为多个模块:

    图片 19

    一经拆分后,页面便是由业务组件组成,只需要关注各个业务组件的开发,然后在主控制器中组装业务组件,这样主控制器对页面的控制力度会增加。

    业务组件一般重用性较低,会产生模块间的业务耦合,还会对业务数据产生依赖,但是主体仍然是HTML&CSS&Javascript,这部分代码也是经常导致冗余的,如果能按模块拆分,可以很好的控制这一问题发生:

    图片 20

    按照上述的做法现在的加载规则是:

    ① 公共样式文件

    ② 框架文件,业务入口文件

    ③ 入口文件,异步加载业务模块,模块内再异步加载其它资源

    这样下来业务开发时便不需要引用样式文件,可以最大限度的提升首屏载入速度;需要关注的一点是,当异步拉取模块时,内部的CSS加载需要一个规则避免对其它模块的影响,因为模块都带有样式属性,页面回流、页面闪烁问题需要关注。

    一个实际的例子是,这里点击出发后的城市列表便是一个完整的业务组件,城市选择的资源是在点击后才会发生请求,而业务组件内部又会细分小模块,再细分的资源控制由实际业务情况决定,过于细分也会导致理解和代码编写难度上升:

    图片 21图片 22

    demo演示地址,代码地址

    如果哪天需求方需要用新的城市选择组件,便可以直接重新开发,让业务之间使用最新的城市列表即可,因为是独立的资源,所以老的也是可以使用的。

    只要能做到UI级别的拆分与页面业务组件的拆分,便能很好的应付样式升级的需求,这方面冗余只要能避过,其它冗余问题便不是问题了,有两个规范最好遵守:

    JavaScript

    1 避免使用全局的业务类样式,就算他建议你使用 2 避免不通过接口直接操作DOM

    1
    2
    1 避免使用全局的业务类样式,就算他建议你使用
    2 避免不通过接口直接操作DOM

    冗余是首屏载入速度最大的拦路虎,是历史形成的包袱,只要能消除冗余,便能在后面的路走的更顺畅,这种组件化编程的方法也能让网站后续的维护更加简单。

    Athena

    工欲善其事,必现利其器。为了实现我们对提升开发效率和产品性能的诉求,我们提出了比较完整的工程化解决方案以及对应的工具Athena。

    Athena是由京东【凹凸实验室】(aotu.io) 推出的一套项目流程工具,通过Athena,我们可以很流程地跑完整个开发流程。Athena分为两部分,一是本地自动化编译工具,二是资源管理平台,其架构如下

    图片 23

    资源加载

    解决冗余便抛开了历史的包袱,是前端优化的第一步也是比较难的一步,但模块拆分也将全站分成了很多小的模块,载入的资源分散会增加请求数;如果全部合并,会导致首屏加载不需要的资源,也会导致下一个页面不能使用缓存,如何做出合理的入口资源加载规则,如何合理的善用缓存,是前端优化的第二步。

    经过几次性能优化对比,得出了一个较优的首屏资源加载方案:

    ① 核心框架层:mvc骨架、异步模块加载器(require&seajs)、工具库(zepto、underscore、延迟加载)、数据请求模块、核心依赖UI(header组件消息类组件)

    ② 业务公共模块:入口文件(require配置,初始化工作、业务公共模块)

    ③ 独立的page.js资源(包含template、css),会按需加载独立UI资源

    ④ 全局css资源

    图片 24

    这里如果追求极致,libs.js、main.css与main.js可以选择合并,划分结束后便可以决定静态资源缓存策略了。

    本地自动化工具

    Athena本地编译工具是一个基于NodeJs的命令行工具,通过执行命令的方式来优化我们的开发流程,目前Athena的主要功能有

    • 自动创建项目、模块、页面、组件结构
    • 轻量组件化功能,根据组件加载情况生成资源依赖表
    • Sass/less 编译
    • 代码检查
    • CSS prefix等处理
    • CSS合并压缩,JS合并压缩
    • 自动生成雪碧图,自动多倍图,图片压缩
    • 字体文件压缩
    • 自定义图片转base64
    • 文件内联,可以内联样式及JS代码
    • 文件MD5戳,将文件进行使用MD5进行重命名
    • 本地预览,直接查看整个项目
    • 资源定位(图片等资源路径替换)
    • 生成CSS页面片,提供将页面引用的CSS/JS抽离成页面片的形式,方便管理CSS资源
    • 部署到预览机和开发机

    资源缓存

    资源缓存是为二次请求加速,比较常用的缓存技术有:

    ① 浏览器缓存

    ② localstorage缓存

    ③ application缓存

    application缓存更新一块不好把握容易出问题,所以更多的是依赖浏览器以及localstorage,首先说下浏览器级别的缓存。

    创建项目结构

    在执行创建命令时,Athena会从管理平台下载自定义好的项目模板,可以根据模板创建项目、模块、页面、和组件。Athena有四个创建命令:

    通过执行 $ ath app demo 命令就可以生成定义好目录结构的项目。

    图片 25

    随后可以通过 $ ath module home来创建一个业务模块;

    通过 $ ath page index 来创建页面;

    通过 $ ath widget widgetName 来创建组件。

    时间戳更新

    只要服务器配置,浏览器本身便具有缓存机制,如果要使用浏览器机制作缓存,势必关心一个何时更新资源问题,我们一般是这样做的:

    <script type="text/javascript" src="libs.js?t=20151025"></script>

    1
    <script type="text/javascript" src="libs.js?t=20151025"></script>

    每次框架更新便不做文件覆盖,直接生成一个唯一的文件名做增量发布,这个时候如果框架先发布,待业务发布时便已经存在了最新的代码;当业务先发布框架没有新的时,便继续沿用老的文件,一切都很美好,虽然业务开发偶尔会抱怨每次都要向框架拿MD5映射,直到框架一次BUG发生。

    开发使用

    seed.js时代

    突然一天框架发现一个全局性BUG,并且马上做出了修复,业务团队也马上发布上线,但这种事情出现第二次、第三次框架这边便压力大了,这个时候框架层面希望业务只需要引用一个不带缓存的seed.js,seed.js要怎么加载是他自己的事情:

    <script type="text/javascript" src="seed.js"></script>

    1
    <script type="text/javascript" src="seed.js"></script>

    //seed.js需要按需加载的资源 <script src="libs_md5.js"></script> <script src="main_md5.js"></script>

    1
    2
    3
    //seed.js需要按需加载的资源
    <script src="libs_md5.js"></script>
    <script src="main_md5.js"></script>

    当然,由于js加载是顺序是不可控的,我们需要为seed.js实现一个最简单的顺序加载模块,映射什么的由构建工具完成,每次做覆盖发布即可,这样做的缺点是额外增加一个seed.js的文件,并且要承担模块加载代码的下载量。

    组件化

    Athena中实现组件化主要是分为两种,一是针对纯HTML模板,通过扩展模板引擎方法实现,提供了组件化API widget.load,它可以方法接收三个参数,第一个参数是widget的名称,后面两个参数是可选参数,第二个是向widget传递的一些参数,第三个是widget所属的模块,如果是本模块,可以不传例如

    JavaScript

    <%= widget.load('user') %> <%= widget.load('user', { param: 'test' }) %> <%= widget.load('user', null, 'gb') %>

    1
    2
    3
    4
    5
    6
    7
    <%= widget.load('user') %>
    <%=
    widget.load('user', {
    param: 'test'
    })
    %>
    <%= widget.load('user', null, 'gb') %>

    通过模板引擎编译,执行widget.load方法,可以实现加载模板,记录依赖关系的目的。

    图片 26

    二是针对不同语言的后端模板,通过实现各自的组件化框架来进行组件的加载,例如 PHP 下使用 <?= $widget->load('user', NULL, 'gb') ?>来进行组件加载,再通过代码扫描得出组件依赖关系。

    localstorage缓存

    也会有团队将静态资源缓存至localstorage中,以期做离线应用,但是我一般用它存json数据,没有做过静态资源的存储,想要尝试的朋友一定要做好资源更新的策略,然后localstorage的读写也有一定损耗,不支持的情况还需要做降级处理,这里便不多介绍。

    本文由奥门新浦京网址发布于Wed前段,转载请注明出处:我们是如何做好前端工程化和静态资源管理,前

    关键词:

上一篇:没有了

下一篇:没有了