Skip to content

静态资源管理方案

zhangyunlong edited this page Sep 16, 2013 · 1 revision

FIS在执行编译的过程中,会扫描这些编译标记,从而建立一张静态资源关系表,它在编译阶段最后会产出为一份map.json 文件,这份文件详细记录了项目内的静态资源id、发布后的线上路径、资源类型以及依赖关系 和 资源打包等信息。FIS-PC解决方法,在运行时根据组件使用情况来 按需加载资源或者资源所在的包,从而提升前端页面运行性能。

组件化拆分你的页面

首先,我们将页面的每个小部件,当做一个组件,在fis中,我们叫它 widget 。接下来,一个页面将会是由多个组件进行组合,同时也会有页面自身需要的资源,对下面photo模块下的index.tpl进行举例说明:

{html framewrok="photo:static/mod.js"}
  {head}
  {/head}
  {body}
    {require name='photo:page/index.tpl'}
    {widget name="photo:widget/A/A.tpl"}
    {widget name="photo:widget/B/B.tpl"}
    {widget name="photo:widget/C/C.tpl"}
    {script}
       require.async(['/static/index/index.js'],function(){
           console.log('index');
       });
    {script}
  {/body}
{/html}

这个网站的目录结构变成了:

根目录
├── photo 
│    ├──page
│    │   └── index.tpl
│    ├──static
│    │   └── index
│    │         └── index.js
│    │         └── index.css
    │    │   └── mod.js
│    ├──widget
│    │   └── A
│    │       └── A.tpl
│    │       └── A.css 
│    │   └── B
│    │       └── B.tpl
│    │       └── B.css
│    │   └── C
│    │       └── C.tpl
│    │       └── C.css

让fis帮你产出静态资源表

大家还记得fis会产出的那个 map.json 么?使用fis,加入适当的配置,对这个项目进行编译会得到一个 map.json的文件,它的内容是:

{
    "res" : {
        "photo:page/index.tpl" : {
            "uri" : "/template/photo/page/index.tpl",
            "extras": {
                "isPage": true,
                "async": [
                    "photo:static/index/index.js"
                ]
            }
        },
        "photo:static/index/index.js": {
            "uri": "/static/photo/index/index_5deaf23.js",
            "type": "js"
        },
        "photo:widget/A/A.tpl" : {
            "uri" : "photo/widget/A/A.tpl",
            "type": "tpl",
            "deps": [
                "photo:widget/A/A.css"
            ]
        },
        "photo:widget/A/A.css" : {
            "uri" : "/static/photo/widget/A/A_7defa41.css",
            "type" : "css"
        },
        "photo:widget/B/B.tpl" : {
            "uri" : "photo/widget/B/B.tpl",
            "type": "tpl",
            "deps": [
                "photo:widget/B/B.css"
            ]
        },
        "photo:widget/B/B.css" : {
            "uri" : "/static/photo/widget/B/B_34derfb.css",
            "type" : "css"
        },
        "photo:widget/C/C.tpl" : {
            "uri" : "photo/widget/C/C.tpl",
            "type": "tpl",
            "deps": [
                "photo:widget/C/C.css"
            ]
        },
        "photo:widget/C/C.css" : {
            "uri" : "/static/photo/widget/C/C_23a1wfe.css",
            "type" : "css"
        }
    }
}

到这里或许你已经猜到我们的 {widget name="resource_id"} 是如何工作的了:

静态资源管理系统

  1. 准备三个数据结构:
    • uris = [],数组,顺序存放要同步输出资源的uri
    • asyncUris = {},异步资源表,顺序存放要异步输出的资源的信息
    • has = {},hash表,存放已收集的静态资源,防止重复加载
  2. 加载资源表 photo-map.json
  3. 执行 {html framework="photo:static/mod.js"}
    1. 设置前端框架的JS文件,优先在全部静态资源之前加载
  4. 执行 {require name="photo:static/index/index.tpl"}
    1. 在表中查找id为 photo:static/index/index.tpl 的资源,查看 photo:static/index/index.tpl 资源的extras属性中的 async ,发现它依赖资源 photo:static/index/index.js
    2. 在表中查找id为 photo:static/index/index.js 的资源,将信息存入 asyncUris 数组中,并在 **has表 **里标记已加载 photo:static/index/index.tpl 资源,我们得到:
        uris = [];
        asyncUris = {
            'photo:static/index/index.js' : {
                 'url' : '_/static/photo/index/index_5deaf23.js_',
                 'pkg' : '',
                 'deps'  []
             }
        };
        has = {
            'photo:static/index/index.tpl' : true
        };
  5. 执行 {widget name="photo:widget/A/A.tpl"}
    1. 在表中查找id为 photo:widget/A/A.tpl 的资源,取得它的资源路径 photo/widget/A/A.tpl ,记为 tpl_path
    2. 模板引擎加载并渲染 tpl_path 所指向的模板文件,即 photo/widget/A/A.tpl,并输出它的html内容
    3. 查看 photo/widget/A/A.tpl 资源的 deps 属性,发现它依赖资源 photo:widget/A/A.css
    4. 在表中查找id为 photo:widget/A/A.css 的资源,取得它的资源路径为 /static/photo/widget/A/A_7defa41.css,存入 uris数组 中,并在 has表 里标记已加载 photo:widget/A/A.css 资源,我们得到:
    uris = [
        '/static/photo/widget/A/A_7defa41.css'
    ];
    asyncUris = {
        'photo:static/index/index.js' : {
             'url' : '/static/photo/index/index_5deaf23.js',
             'pkg' : '',
             'deps'  []
         }
    };
    has = {
        'photo:static/index/index.tpl' : true
        'photo:widget/A/A.css' : true
    };
  6. 执行 {widget name="B"},步骤与上述步骤3相同,我们得到:
    uris = [
        '/static/photo/widget/A/A_7defa41.css',
        '/static/photo/widget/B/B_34derfb.css'
    ];
    asyncUris = {
        'photo:static/index/index.js' : {
             'url' : '/static/photo/index/index_5deaf23.js',
             'pkg' : '',
             'deps'  []
         }
    };
    has = {
        'photo:static/index/index.js' : true,
        'photo:widget/A/A.css' : true,
        'photo:widget/B/B.css' : true
    };
  7. 执行 {widget name="C"},步骤与上述步骤3相同,我们得到:
    uris = [
        '/static/photo/widget/A/A_7defa41.css',
        '/static/photo/widget/B/B_34derfb.css',
        '/static/photo/widget/C/C_23a1wfe.css'
    ];
    asyncUris = {
        'photo:static/index/index.js' : {
             'url' : '/static/photo/index/index_5deaf23.js',
             'pkg' : '',
             'deps'  []
         }
    };
    has = {
        'photo:static/index/index.js' : true,
        'photo:widget/A/A.css' : true,
        'photo:widget/B/B.css' : true,
        'photo:widget/C/C.css' : true
    };
  8. 在要输出的html前面,我们读取 uris数组asyncUris的数据,生成静态资源外链,我们得到最终的html结果:
<html>
   <head>
     <link href="/static/photo/widget/A/A_7defa41.css">
     <link href="/static/photo/widget/B/B_34derfb.css">
     <link href="/static/photo/widget/C/C_23a1wfe.css">
   </head>
   <body>
     <div>html of A</div>
     <div>html of B</div>
     <div>html of C</div>
     <script type="text/javascript" src="/static/common/mod_d2a34f2.js"></script>
     <script type="text/javascript">
       require.async(['/static/index/index.js'],function(){
           console.log('index');
       });
     </script> 
     <script type="text/javascript">
      require.resourceMap({
         "res":{        
               "photo:static/index/index.js": {
                    "url": "/static/photo/index/index_5deaf23.js",
                    "pkg": "",
                    "deps": []
               }
         }
      });
     </script> 
   </body>
</html>

看到了么!!!我们不但可以让资源按需加载,同时根据静态资源的类型在页面的不同位置进行加载,还能全部映射到正确的md5戳哦,这全依赖fis的表生成技术!那么,基于这项技术,我们是如何处理打包的呢:

打包——资源的备份读取

现在,我们再来使用fis的 pack配置项,对网站的静态资源进行打包,配置文件大致为:

fis.config.merge({
    pack : {
        'pkg/aio.css' : '**.css'
    }
});

执行fis的编译命令并使用 pack、md5 等功能:

fis release --pack --md5

再来查看我们的 map.json, 它的内容变为:

{
    "res" : {
        "photo:page/index.tpl" : {
            "uri" : "/template/photo/page/index.tpl",
            "extras": {
                "isPage": true,
                "async": [
                    "photo:static/index/index.js"
                ]
            }
        },
        "photo:static/index/index.js": {
            "uri": "/static/photo/index/index_5deaf23.js",
            "type": "js"
        },
        "photo:widget/A/A.tpl" : {
            "uri" : "photo/widget/A/A.tpl",
            "type": "tpl",
            "deps": [
                "photo:widget/A/A.css"
            ]
        },
        "photo:widget/A/A.css" : {
            "uri" : "/static/photo/widget/A/A_7defa41.css",
            "type" : "css",
            "pkg" : "p0"
        },
        "photo:widget/B/B.tpl" : {
            "uri" : "photo/widget/B/B.tpl",
            "type": "tpl",
            "deps": [
                "photo:widget/B/B.css"
            ]
        },
        "photo:widget/B/B.css" : {
            "uri" : "/static/photo/widget/B/B_34derfb.css",
            "type" : "css",
            "pkg" : "p0"
        },
        "photo:widget/C/C.tpl" : {
            "uri" : "photo/widget/C/C.tpl",
            "type": "tpl",
            "deps": [
                "photo:widget/C/C.css"
            ]
        },
        "photo:widget/C/C.css" : {
            "uri" : "/static/photo/widget/C/C_23a1wfe.css",
            "type" : "css",
            "pkg" : "p0"
        }
    },
    "pkg" : {
        "p0" : {
            "uri" : "/static/pkg/aio_0cb4a19.css",
            "has" : [ "photo:widget/A/A.css", "photo:widget/B/B.css", "photo:widget/C/C.css" ]
        }
    }
}

大家注意到了么,表里多了一张 pkg 表,所有被打包的资源会有一个 pkg属性 指向该表中的资源,而这个资源,正是我们配置的打包策略。好,让我们看看这种情况下,我们的 **{widget name="resource_id"}**是怎么工作的吧( 注意,这个过程工程师的代码从未改动过哦 ):

  1. 准备三个数据结构:
    • uris = [],数组,顺序存放要同步输出资源的uri
    • asyncUris = {},异步资源表,顺序存放要异步输出的资源的信息
    • has = {},hash表,存放已收集的静态资源,防止重复加载
  2. 加载资源表 photo-map.json
  3. 执行 {html framework="photo:static/mod.js"}
    1. 设置前端框架的JS文件,优先在全部静态资源之前加载
  4. 执行 {require name="photo:static/index/index.tpl"}
    1. 在表中查找id为 photo:static/index/index.tpl 的资源,查看 photo:static/index/index.tpl 资源的extras属性中的 async ,发现它依赖资源 photo:static/index/index.js
    2. 在表中查找id为 photo:static/index/index.js 的资源,将信息存入 asyncUris 数组中,并在 **has表 **里标记已加载 photo:static/index/index.tpl 资源,我们得到:
        uris = [];
        asyncUris = {
            'photo:static/index/index.js' : {
                 'url' : '/static/photo/index/index_5deaf23.js',
                 'pkg' : '',
                 'deps'  []
             }
        };
        has = {
            'photo:static/index/index.tpl' : true
        };
  5. 执行 {widget name="photo:widget/A/A.tpl"}
    1. 在表中查找id为 photo:widget/A/A.tpl 的资源,取得它的资源路径 photo/widget/A/A.tpl,记为 tpl_path
    2. 模板引擎加载并渲染 tpl_path 所指向的模板文件,即 photo/widget/A/A.tpl,并输出它的html内容
    3. 查看 photo:widget/A/A.tpl 资源的 deps 属性,发现它依赖资源 photo:widget/A/A.css
    4. 在表中查找id为 photo:widget/A/A.css 的资源,我们发现该资源有 pkg属性,表明它被 备份 在了一个打包文件中。
    5. 我们使用它的pkg属性值 p0 作为key,在pkg表里读取信息,取的这个包的资源路径为 /static/pkg/aio_0cb4a19.css 存入 uris数组
    6. 将p0包的 has 属性所声明的资源加入到 has表 里我们得到:
      uris = [
          '/static/pkg/aio_0cb4a19.css'
      ];
      asyncUris = {
          'photo:static/index/index.js' : {
               'url' : '/static/photo/index/index_5deaf23.js',
               'pkg' : '',
               'deps'  []
           }
      };
      has = {
          'photo:static/index/index.js' : true,
          'photo:widget/A/A.css' : true,
          'photo:widget/B/B.css' : true,
          'photo:widget/C/C.css' : true
      };
  6. 执行 {widget name="photo:widget/B/B.tpl"},步骤与上述步骤3相同,但当我们要加载B/B.tpl的资源B/B.css时,发现它已经被has表标记为 已收集,因此跳过资源收集过程
  7. 执行 {widget name="photo:widget/C/C.tpl"},结果与步骤4相同
  8. 在要输出的html前面,我们读取 uris数组 的数据,生成静态资源外链,我们得到最终的html结果:
<html>
   <head>
     <link href="/static/pkg/aio_0cb4a19.css">
   </head>
   <body>
     <div>html of A</div>
     <div>html of B</div>
     <div>html of C</div>
     <script type="text/javascript" src="/static/common/mod.js"></script>
      require.resourceMap({
         "res":{        
               "photo:static/index/index.js": {
                    "url": "/static/photo/index/index_5deaf23.js",
                    "pkg": "",
                    "deps": []
               }
         }
      });
   </body>
</html>

出现打包了有木有啊!!!

这样做的好处

抱歉,这货好处实在太多了。

  • 我们可以统计 {widget} 插件的调用情况,然后自动生成最优的打包配置,让网站可以 自适应优化
  • 工程师不用关心资源在哪,怎么来的,怎么没的,所有 资源定位 的事情,都交给fis好了。解决了前面说的 功能下线不敢删除相应资源 的问题
  • 静态资源路径都带md5戳,这个值只跟内容有关,静态资源服务器从此可以放心开启强缓存了!还能实现静态资源的分级发布,回滚神马的超方便哦!
  • 我们给 {widget name="resource_id"} 加一个小小的“后门”,我们可以利用cookie、url中的get参数来控制瞬间切换线上的页面输出结果为打包或者不打包、甚至是压缩或者不压缩的资源, 方便定位线上问题 有木有!
  • 我们再给 {widget name="resource_id"} 加一个小小的“后门”,让它可以读取一个 domains.conf 配置文件,内容形如:
default=http://static.example.com
debug=http://localhost:8080

然后我们约定一个cookie或者url值,可以一键 把线上资源映射到本地 有木有!!!方便调试啊,魂淡!

  • 我们还可以继续折腾,比如根据国际化、皮肤,终端等信息约定一种资源路径规范,当后端适配到特定地区、特定机型的访问时,静态资源管理系统帮你 送达不同的资源给不同的用户 啊,有木有!
  • 更多好处,等你来挖掘,请鞭挞我吧,公瑾!