web如何缓存Lottie动画资源

5,590 阅读8分钟

前言

Hello 大家好! 我是前端 无名

背景

在实际工作中,我们经常会用到Lottie-web来实现动画效果。一个Lottie动画通常由一个data.json文件和N张图片组成。项目中一般使用Lottie动画都是给一个CDN的动画地址,交给lottie-web库去播放,动画资源下载时机是动画播放前去下载data.json文件和图片资源,动画的播放受网络因素影响较大,动画的加载时间较长,如果需要多个lottie动画结合显示,时间不能完美契合,最终效果达不到UI设计师要求。

本篇文章介绍如何提取lootie动画资源并且利用缓存技术实现Lottie动画从本地读取加载,减少网络请求。

技术思路

  1. 项目中配置需要缓存的lottie config文件。
  2. 利用webpack插件,在项目构建时,生成真正需要缓存的配置列表(图片列表以及lottie的data.json列表)。
  3. 封装预加载库,请求图片和data.json资源,存储在indexdb中。
  4. 封装Lottie加载配置类,区分从缓存读取还是直接网络获取。
  5. 缓存读取,创建图片Blob url,替换原始data.json中的图片引用,生成新的data.json。
  6. lottie 加载配置,展示动画。

了解Lottie JSON文件数据格式

详细可以参考:Web 帧动画解决方案 - lottie-web源码剖析

本文借用 青舟同学 的图片来简单介绍下Lottie JSON文件结构,方便后续解析JSON文件,读取图片资源以及替换图片地址。

  1. JSON文件全局信息

c5bed44b3e204b6dbf281e075dcbbf46_tplv-k3u1fbpfcp-zoom-1.png 左侧为使用 AE 新建动画合成需要填入的信息,和右面第一层 JSON 信息对应如下:

  • w 和 h : 宽高
  • v:Bodymovin 插件版本号 4.5.4
  • fr:帧率
  • ip 和 op:开始帧、结束帧
  • assets:静态资源信息(如图片)
  • layers:图层信息
  • ddd:是否为3d
  • comps:合成图层

其中 assets 对我们提取Lottie资源以及后期生成新的json尤为重要

lottie JSON图片资源提取

预加载lottie图片有两种方案:

方案一:编写webpack插件,构建时提取lottie JSON 图片资源,结合html-webpack-plugin提供的hooks,将图片资源地址以Link方式插入到图片html中。

方案二:编写webpack插件,构建时提取lottie JSON 图片资源,生成lottieAssets.js配置文件。利用缓存工具库,读取配置文件,缓存资源图片到indexdb中。

方案三:方案三和方案二大体相同,主要是提取lottie资源生成lottieAssets.js配置文件的方式不同,方案二是通过webpack插件,方案三是编写npm(lottie-extract-assets)包,直接执行单独的命令,去提取lottie资源生成lottieAssets.js。(产线方案) 插件源码地址:点我!

lottieConfig.json编写格式如下:

[
    //lottie动画ceremonyBlessingBagFirst
    "https://xxx.com/effects/ceremonyBlessingBagFirst/data.json",
    //lottie动画abc
    "https://xxx.com/effects/abc/data.json",
    //lottie动画cde
    "https://xxx.com/effects/cde/data.json"
]

方案一实现:

例如:data.json的CND地址为:

xxx.com/effects/cer…

一般我们的图片资源存放在 xxx.com/effects/cer… 目录下

代码参考:

const HtmlWebpackPlugin = require('safe-require')('html-webpack-plugin');
const lottieConfig = require("../../lottieConfig.json");
const ajax = require('../utils/ajax');
const request = require('request');
const path = require('path');

const preloadDirective = {
    '.js': 'script',
    '.css': 'style',
    '.woff': 'font',
    '.woff2': 'font',
    '.jpeg': 'image',
    '.jpg': 'image',
    '.gif': 'image',
    '.png': 'image',
    '.svg': 'image'
  };

const IS = {
    isDefined: v => v !== undefined,
    isObject: v => v !== null && v !== undefined && typeof v === 'object' && !Array.isArray(v),
    isBoolean: v => v === true || v === false,
    isNumber: v => v !== undefined && (typeof v === 'number' || v instanceof Number) && isFinite(v),
    isString: v => v !== null && v !== undefined && (typeof v === 'string' || v instanceof String),
    isArray: v => Array.isArray(v),
    isFunction: v => typeof v === 'function'
  };
  
  const { isDefined, isObject, isBoolean, isNumber, isString, isArray, isFunction } = IS;
  
  /**
   * 
   * 预加载图片资源
   * @class LottieWebpackPlugin
   */
  class LottieWebpackPlugin{

      constructor(options){
          this.options=options || {};
      }
      /**
       * 
       * 
       * 添加图片资源
       * @memberOf LottieWebpackPlugin
       */
      addLinks= async(compilation, htmlPluginData)=>{
          let imgArray=[];
          if(lottieConfig){
              for(let i=0;i<lottieConfig.length;i++){
               const result=  await this.requestLottie(lottieConfig[i]);
               imgArray.push(...result);
              }
          }
          //重点:添加预加载link标签到htmlPlugin的head中
          Array.prototype.push.apply(htmlPluginData.headTags, imgArray.map(this.addPreloadType));
        return htmlPluginData;
      }
      /**
       * 
       * 
       * 请求data.json文件
       * @memberOf LottieWebpackPlugin
       */
      requestLottie=  (url)=>{
         return new Promise((resolve,reject)=>{
            request(url,  (error, response, body)=> {
                if (!error && response.statusCode == 200) {
                  try{
                    const lottieData=JSON.parse(body);
                    const result= this.lottieParse(lottieData,url);
                    resolve(result);
                  }catch(e){
                      console.log(e);
                  }
                }else{
                    reject(url+"失败");
                }
              })
          })
      }
      /**
       * 
       * 
       * 解析lottie文件
       * @memberOf LottieWebpackPlugin
       */
      lottieParse=(data,url)=>{
        let urlArray=[];
        try{
            const assets=data.assets;
            const urlPre=this.getUrlPre(url);
            for(let i=0;i<assets.length;i++){
                const item=assets[i];
                if(item.p && item.u){
                    const url=`${urlPre}${item.u}${item.p}`;
                    const tag= this.createResourceHintTag(url,"preload",true);
                    urlArray.push(tag);
                }
            }
          }catch(e){
              console.log(e);
          }
          return urlArray;
      }
    /**
     * 
     * 
     * 获取data.json的引用地址
     * @memberOf LottieWebpackPlugin
     */
    getUrlPre=(url)=>{
        const lastIndex=  url.lastIndexOf("/");
        return url.substring(0,lastIndex+1);
    }
    /**
     * 
     * 
     * 
     * @memberOf LottieWebpackPlugin
     */
    addPreloadType =(tag)=> {
        const ext = path.extname(tag.attributes.href);
        if (preloadDirective[ext]) {
          tag.attributes.as = preloadDirective[ext];
        }
        return tag;
      }
      

      /**
       * 
       * 
       * 资源加载
       * @memberOf LottieWebpackPlugin
       */
      alterAssetTagGroups=(htmlPluginData,callback,compilation)=>{
          console.log("compilation=",compilation);
          console.log("htmlPluginData=",htmlPluginData);
        try {
            callback(null, this.addLinks(compilation, htmlPluginData));
        } catch (error) {
            callback(error);
        }        
      }

     
     /**
       * 
       * 
       * 创建资link标签,预加载
       * @memberOf LottieWebpackPlugin
       */
     createResourceHintTag= (url, resourceHintType, htmlWebpackPluginOptions)=> {
        return {
          tagName: 'link',
          selfClosingTag:  true || !!htmlWebpackPluginOptions.xhtml,
          attributes: {
            rel: resourceHintType,
            href: url
          }
        };
      }
  
      registerHook(compilation){
        const pluginName=this.constructor.name; 
        if (HtmlWebpackPlugin && HtmlWebpackPlugin.getHooks) {
                // HtmlWebpackPlugin >= 4
            const hooks = HtmlWebpackPlugin.getHooks(compilation);
            const htmlPlugins = compilation.options.plugins.filter(plugin => plugin instanceof HtmlWebpackPlugin);
            if (htmlPlugins.length === 0) {
                const message = "Error running html-webpack-tags-plugin, are you sure you have html-webpack-plugin before it in your webpack config's plugins?";
                throw new Error(message);
            }
            hooks.alterAssetTagGroups.tapAsync(pluginName, (htmlPluginData, callback)=>{this.alterAssetTagGroups(htmlPluginData, callback,compilation)});
        
        } else if (compilation.hooks.htmlWebpackPluginAlterAssetTags &&
            compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration) {
                // HtmlWebpackPlugin 3
                compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync(pluginName,(htmlPluginData, callback)=>{this.alterAssetTagGroups(htmlPluginData, callback,compilation)});
        }else{
            const message = "Error running html-webpack-tags-plugin, are you sure you have html-webpack-plugin before it in your webpack config's plugins?";
            throw new Error(message);
        }
      }

      apply(compiler){
        const htmlPluginName = isDefined(this.options.htmlPluginName) ? this.options.htmlPluginName : 'html-webpack-plugin';
        const pluginName=this.constructor.name;
          if(compiler.hooks){
              compiler.hooks.compilation.tap(pluginName,(compilation)=>{
                  this.registerHook(compilation);
              });
          }
      }
  }

  module.exports = LottieWebpackPlugin;

生成html效果如下:

code1.png

github 戳 :lottie-pre-webpack-plugin

方案二实现:自定义webpack插件,提取图片资源,生成js或者ts文件

代码参考:

const fs = require('fs');
const request = require('request');
const path = require('path');
const webpack = require("webpack");

/**
 * 
 * lottie资源提取插件
 * @class LottieExtractAssetsPlugin
 */
class LottieExtractAssetsPlugin {

    constructor (options) {
    	//1:获取 lottie配置文件路径
        this.configPath = options && options.configPath;
        //2:获取输出文件名称
        this.outFileName = options && options.outFileName ? options.outFileName : "lottie-assets.js";
        //生成资源文件的全局名称
        this.globalName = options && options.globalName ? options.globalName : "window._config";
        this.to = options && options.to ? options.to : "dist";
    }

    compilationHook(compilation) {
        const pluginName = this.constructor.name;
        //重点:webpack 适配
        if(compilation.hooks.processAssets){
            //compilation.emitAsset(name, new webpack.sources.RawSource(html, false));
           // 添加资源
            compilation.hooks.processAssets.tapAsync({ name: pluginName, stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS }, async (assets, cb) => {
                if (this.configPath) {
                    await this.readJsonFile(this.configPath, assets);
                    cb();
                } else {
                    cb();
                }
            });
        }else if(compilation.hooks.additionalAssets){
            compilation.hooks.additionalAssets.tapAsync( pluginName,  async (cb) => {
                if (this.configPath) {
                    await this.readJsonFile(this.configPath, compilation.assets);
                    cb();
                } else {
                    cb();
                }
            });
        }else{
            //throw new Error("请升级webpack版本>=4");
            compilation.errors.push("请升级webpack版本>=4");
        } 
    }
    
    /**
     * 
     * 
     * 获取lottie 资源地址。
     * @memberOf LottieExtractAssetsPlugin
     */
    getLink= async(lottieConfig)=>{
        let imgArray=[];
        if(lottieConfig){
            for(let i=0;i<lottieConfig.length;i++){
                const url=lottieConfig[i];
                //添加lottie json
                this.addLottieInfo(url,imgArray);
                //请求lottie json文件,获取图片资源
                const result=  await this.requestLottie(lottieConfig[i]);
                imgArray.push(...result);
            }
        }
      return imgArray;
    }
    /**
     * 
     * 
     * 添加lottie json 文件
     * @memberOf LottieExtractAssetsPlugin
     */
    addLottieInfo=(url,imgArr)=>{
        const info=this.getLottieInfo(url);
        imgArr.push({
            key:info.name,
            url:url,
         })
    }
    
    /**
     * 
     * 
     * 读取配置文件,生成js文件。
     * @memberOf LottieExtractAssetsPlugin
     */
    readJsonFile= async(assetPath,assets)=>{
        //读取lottieCofig.json配置文件
        let lottieConfig = await new Promise((resolve, reject) => {
            try {
                //读取配置文件
                fs.readFile(assetPath, (err, data) => {
                    if (err) {
                        reject(err);
                    } else {
                        let curData = data.toString();
                        const config = JSON.parse(curData);
                        resolve(config);
                    }
                });
            } catch (e) {
                reject(e);
            }
        }).catch(()=>{
            console.warn("读取配置文件错误:"+assetPath);
        });
        if(!lottieConfig){
            return;
        }
        //根据配置获取资源链接(包含当前的lottie和lottie中图片)
        const imgLink = await this.getLink(lottieConfig);
        // 采用js文件,方便我们前端代码集成使用。
        let content = this.globalName + " = " + JSON.stringify(imgLink, null, 4) + ";";
        const assetsInfo = {
            // 写入新文件的内容
            source: function () {
                return content;
            },
            // 新文件大小(给 webapck 输出展示用)
            size: function () {
                return content.length;
            }
        }
        const fileName = path.join(this.to, this.outFileName);
        assets[fileName]= assetsInfo;
    }
    /**
     * 
     * 
     * 请求lottie json文件
     * @memberOf LottieExtractAssetsPlugin
     */
    requestLottie=  (url)=>{
       return new Promise((resolve,reject)=>{
          request(url,  (error, response, body)=> {
              if (!error && response.statusCode == 200) {
                try{
                  const lottieData=JSON.parse(body);
                  const result= this.lottieParse(lottieData,url);
                  resolve(result);
                }catch(e){
                    console.log(e);
                }
              }else{
                  reject(url+"==失败");
              }
            })
        })
      
    }

    /**
     * 
     * 解析lottie
     * @memberOf LottieExtractAssetsPlugin
     */
    lottieParse=(data,url)=>{
      let urlArray=[];
      try{
          const assets=data.assets;
          const lottieInfo=this.getLottieInfo(url);
          for(let i=0;i<assets.length;i++){
              const item=assets[i];
              if(item.p && item.u){
                  const imgUrl=`${lottieInfo.url}/${item.u}${item.p}`;
                  urlArray.push({
                      key:`${lottieInfo.name}_${item.p}`,
                      url:imgUrl,
                      source:url,
                      lottieName:lottieInfo.name
                  });
              }
          }
        }catch(e){
            console.log(e);
        }
        return urlArray;
    }
    /**
     * 
     * 根据url获取lottie信息,方便生成配置文件。
     * @memberOf LottieExtractAssetsPlugin
     */
    getLottieInfo=(url)=>{
      const lastIndex=  url.lastIndexOf("/");
      const curUrlPre=url.substring(0,lastIndex);
      const nameLastIndex=  curUrlPre.lastIndexOf("/");
      return {url:curUrlPre,name:curUrlPre.substring(nameLastIndex+1,nameLastIndex.length)}
    }
    /**
     * 
     * webpack 插件入口
     * @param {any} compiler 
     * 
     * @memberOf LottieExtractAssetsPlugin
     */
    apply(compiler) {
        const pluginName=this.constructor.name;
        if(compiler.hooks){
            // Webpack 4+ Plugin System
            //TODO 使用该hooks目前可以满足需求,但是会警告,后期查看webpack具体生命周期,替换。
            compiler.hooks.compilation.tap(pluginName, (compilation, compilationParams) => {
                //注意注册事件时机。
                this.compilationHook(compilation);
            });
        }else{
            compilation.errors.push("请升级webpack版本>=4");
        }
    }
  }
  
  module.exports = LottieExtractAssetsPlugin;

测试插件:webpackConfig.js:

const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const LottieExtractAssetsPlugin=require("./src/plugins/index.js");
const assert = require('assert');

const to=path.join("lottie", "test");

module.exports = {
  entry: './src/index.js',
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new CleanWebpackPlugin(),
    new LottieExtractAssetsPlugin({configPath:"./lottieConfig.json",to:to,outFileName:"lottie-assets.js",globalName:"window._lottieConfig"})
  ]
}

生成效果:lottie-assets.js

code4.png

github 戳 :lottie-extract-assets-plugin

利用缓存,缓存lottie资源

  1. 资源indexdb存储

    rloader基于idb库实现。

    rloader 特点:

    1. 资源加载支持简单的依赖关系

    2. 缓存支持版本

    3. 小巧

    4. 进度,错误,完毕等事件支持

该库的封装由 xiangenhu 同学完成。已开源:rloader,测试案例首次加载耗时3秒,后面加载仅需22毫秒左右。效率大幅提升。rloader演示demo,现已发表掘金:徒手撸一个资源加载器

具体使用:index.js

import ResourceLoader from "./r-loader.js";
//全局缓存资源 key:string,blobUrl:string;data:Blob,url:string;
 window.GlobCache={};

//默认自定义缓存的文件,可选
let resourcesInfo = [
    {
        key: "mqtt2",
        url: "//xxx.com/libs/js/mqtt.min.js"
    },{
        key: "lottie",
        url: "//xxx.com/lib/lottie.min.js",
        ver: "1.9"
    },{
        key: "flv",
        pre: ["mqtt"],
        url: "//xxx.com/libs/js/flv.1.5.min.js"
    },{
        pre: ["lottie"],
        key: "mqtt",
        url: "//xxx.com/libs/js/mqtt.min.js"
    },
];


//重点 index.html 中引入lottie-assets.js
if(window._config){
    //加载lottie-extract-assets-plugin 插件提取出来的lottie资源文件
    resourcesInfo=resourcesInfo.concat(window._config);
}



let startTime=Date.now();

const rl = new ResourceLoader(resourcesInfo, window.idb);

rl.on("progress", (progress, info)=>{
    //缓存完一个文件,添加到全局缓存中。
    window.GlobCache[info.key]=info;
});

rl.on("completed", (datas)=>{
    console.log("加载完成:completed event:", datas);    
    console.log("total time:", Date.now() - startTime)
});

rl.on("loaded", (datas)=>{
    console.log("全部正常加载:loaded event:", datas);    
    console.log("total time:", Date.now() - startTime)
});

rl.on("error", (error, info)=>{
    console.log("error event:", error.message, info);
});
rl.reset();
rl.startLoad();


效果:

image.png

编写lottie option 生成方法,判断缓存读取还是直接取网络

上面我们已经把lottie图片资源以及data.json资源缓存到indexdb中,并且保存到了window.GlobCache中,现在我们根据缓存生成不同的lottie option对象

  const jsonPath="https://xxx/acts-effects/luck-draw-act-effect1/data.json";
  const defaultOptions = {
       loop: false,
       autoplay: false,
       path: jsonPath,
       rendererSettings: {
          preserveAspectRatio: "xMidYMid slice",
       },
     }; 

众所周知,lottie加载动画的时候需要一个option对象:

  • 资源从缓存获取,给option中animationData赋值,animationData的json数据是我们修改后的json对象,图片资源地址为blob url
  • 资源从网络获取,给option中path赋值

直接上代码: LottieOptionFactory.js

/**
 * 
 * 获取json对象
 * @export
 * @param {any} rul 
 */
 export function fetchResourceJson(url){
   return fetch(url,{
        headers: {
            'content-type': 'application/json'
          },
    }).then(res=>{
        if(res.status >= 400){
            throw new Error(res.status + "," + res.statusText);
        }
        return res.json();
    }).then(res=> res);
 }

/**
 * 
 * lottie option 生成
 * @export
 * @param {any} option 
 * @returns 
 */
export default async function LottieOptionFactory(option={},globCache=window.GlobCache) {
   //获取原始option
    const { path, ...other } = option;
    const originalOption = {
        path,
        ...other,
    };
    try{
        const result = getLottieInfo(path);
        //获取lottie动画名称
        const { name } = result;
        //从全局缓存中获取lottie的data.json 
        const lottieCache= globCache[name];
        //如果缓存中不存在,则返回原始配置option,正常从网络上获取。
        if(!lottieCache || !lottieCache.blobUrl){
                return originalOption;
        }
        //利用缓存中获取的data.json资源的blobUrl获取data.json 对象
        const jsonData= await getLottieJson(lottieCache.blobUrl);
        //修改lottie json对象中的图片字段
        const transJson= transformLottieJson(jsonData,name);
        //返回blob URL 的data.json
        return {
                ...other,
                animationData:transJson
            }
    }catch(e){
        console.log("LottieOptionFactory err:",e);
    }
    return originalOption;
   
}

/**
 *
 * 根据url获取lottie信息。
 * @memberOf getLottieInfo
  */
function getLottieInfo(url) {
    const lastIndex = url.lastIndexOf("/");
    const name = url.substring(lastIndex + 1, url.length);
    const curUrlPre = url.substring(0, lastIndex);
    const nameLastIndex = curUrlPre.lastIndexOf("/");
    return { url: curUrlPre, name: curUrlPre.substring(nameLastIndex + 1, nameLastIndex.length), jsonName: name };
}

/**
 * 
 * 获取lottie json 对象
 * @param {any} lottieCacheData 
 * @returns 
 */
 function getLottieJson (url){
     //两种实现方式,
     //1:从缓存中获取到'data.json'的Blob对象,转换Blob对象为json对象 
     // const reader = new FileReader();
     // return new Promise((resolve,reject)=>{
     //     reader.readAsText(lottieCacheData,'utf8');
     //     reader.onload = function(){
     //         const receive_data = this.result;//这个就是解析出来的数据
     //         try{
     //             resolve(JSON.parse(receive_data));
     //         }catch(e){
     //             console.log("解析",e);
     //             reject("失败");
     //         }
     //     }
     // })
    // 2:直接访问'data.json'的blob url, 获取 json对象
    return fetchResourceJson(url);
    
}

/**
 * 
 * 修改lottie json对象中的图片字段,生成使用blob url的图片地址
 * @param {any} lottieJson 
 * @param {any} lottieName 
 * @returns 
 */
function transformLottieJson (lottieJson,lottieName){
    //先备份
    const newLottieJson={...lottieJson};
    try{
        const assets=newLottieJson.assets;
        for(let i=0;i<assets.length;i++){
            const item=assets[i];
            //p 为 图片名称 u:图片相对路径
            if(item.p && item.u){
                const name=`${lottieName}_${item.p}`;
                const lottieCache= window.GlobCache[name];
                if(lottieCache && lottieCache.blobUrl){
                    newLottieJson.assets[i].u="";
                    newLottieJson.assets[i].p=lottieCache.blobUrl;
                }
            }
        }
    }catch(e){
        console.log(e);
    }
    return newLottieJson;

}

具体使用:

    import ResourceLoader from "./r-loader.js";
    import LottieOptionFactory from "./LottieOptionFactory.js";

    //全局缓存资源 key:string,blobUrl:string;data:Blob,url:string;
     window.GlobCache={};

    let resourcesInfo = [];

    if(window._config){
        //加载lottie-extract-assets-plugin 插件提取出来的lottie资源文件
        resourcesInfo=resourcesInfo.concat(window._config);
    }

    const rl = new ResourceLoader(resourcesInfo, window.idb);

    rl.on("progress", (progress, info)=>{
        console.log("progress:", progress,  info);
        window.GlobCache[info.key]=info;
    });

    rl.on("completed", (datas)=>{
        console.log("加载完成:completed event:", datas);    
        console.log("total time:", Date.now() - startTime)
    });
 
    rl.reset();
    rl.startLoad();

    //这里使用timeout是由于从indexdb中读取缓存需要时间。产线方案中解决。
     setTimeout(async ()=>{
      const option= await LottieOptionFactory({
            container: document.getElementById('bm'),
            renderer: 'svg',
            loop: true,
            autoplay: true,
            path : "https://xxx.comacts-effects/luck-draw-act-effect1/data.json",
        });
        console.log("option==",option);
        const animation = bodymovin.loadAnimation(option)
    },1000)

效果:

首次加载:

image.png

二次加载:

image.png

lottie最终效果

image.png

至此,我们已经实现了lottie资源加载时机提前,多次加载均采用缓存资源,成功的减少了网络请求。

应用产线

在应用产线的过程中,我们发现了一些可以优化的地方,对上面的逻辑进行了一些优化

  • 利用node命令(npm包 lottie-extract-assets)提取资源,不再使用webpack插件(节省编译时间),已支持多项目,多目录配置。
  • lottie json文件转化对象提前,不在创建lottieOption的时候去读取,修改为监听 rloader的 progress 事件,在从缓存中读取到lottie json的blobUrl后直接转化为json对象。
  • index.html 中引入编译提取出来的资源文件配置不够便利,建议在index.js中导入(lottie-assets.js)

部分代码参考:

startLottieCache.ts

import ResourceLoader from "./r-loader";
import idb from "./idb";


import { fetchResourceJson } from "./cacheUtils";

enum CacheFileType {
    LOTTIE_JSON = "lottie-json-file",
    LOTTIE_IMG = "lottie-img"
}

function parserLottie(globalName: string) {
    const lottieResourcesInfo = [];
    if ((window as any)[globalName]) {
        // 加载lottie-extract-assets-plugin 插件提取出来的lottie资源文件
        const lottieConfig = (window as any)[globalName];
        for (let i = 0; i < lottieConfig.length; i++) {
            const item = lottieConfig[i];
            const {
                key, url, source, imgs
            } = item;
            lottieResourcesInfo.push({
                key, url, source, fileType: CacheFileType.LOTTIE_JSON
            });
            lottieResourcesInfo.push(...imgs);
        }
    }
    return lottieResourcesInfo;
}

/**
 *
 * 获取lottie json 对象
 * @param {any} lottieCacheData
 * @returns
 */
function getLottieJson(url) {
    // 2:直接访问'data.json'的blob url, 获取 json对象
    return fetchResourceJson(url);
}


export function mergeLottieCache(resourcesInfo = [], globalName) {
    let curResourcesInfo = [...resourcesInfo];
    const lottieResourcesInfo = parserLottie(globalName);
    curResourcesInfo = resourcesInfo.concat(lottieResourcesInfo);
    return curResourcesInfo;
}
/**
 *
 * 重点,缓存读取到以后,特殊处理
 */
function handleCacheResult(info) {
    if (!info.fileType) return;
    if (info.blobUrl) {
        getLottieJson(info.blobUrl).then((data) => {
            console.log("data==", data);
            (window as any).GlobCache[info.key].lottieJson = data;
        }).catch((e) => {
            console.log("解析失败", e);
        });
    }
}
/**
 *
 * 开启缓存
 */
export function startCache(resourcesInfo) {
    (window as any).GlobCache = {};
    const curResourcesInfo = [...resourcesInfo];
    const startTime = Date.now();
    const rl = new ResourceLoader(curResourcesInfo, idb);
    rl.on("progress", (progress, info) => {
        console.log("progress:", progress, info);
        (window as any).GlobCache[info.key] = info;
        //重点,可以针对特殊缓存处理
        handleCacheResult(info);
    });

    rl.reset();
    rl.startLoad();
}

接入:

  1. index.js

       //导入生成的lottie-assets.js文件
       import "./lottie-assets";
       //需要合并的缓存数组和生成的lottie-assets.js文件中window对象名称
      const resources=mergeLottieCache([],"_lottieConfig");
      //开启缓存
      startCache(resources);
    
  2. 使用

     const defaultOptions = {
         loop: false,
         autoplay: false,
         path: jsonPath,
         rendererSettings: {
             preserveAspectRatio: "xMidYMid slice",
         },
     };
     //新增,用LottieOptionFactory包装原来的参数即可
     LottieOptionFactory(defaultOptions);
    

后语

演示DEMO 代码戳我

ps:(替换lottie图片的思路实现以后,我们以后可以用同一个lottie,动态的替换json中的图片,实现一些定制的动画效果。例如:用户名片展示,名片是个lottie动效,用户头像是定制的,我们可以让设计师把默认用户头像设计到lottie动画中,动态替换用户头像)

本篇文章主要介绍的是技术实现思路,前提是支持indexDB。 欢迎社区的小伙伴多提意见。