Hello 你们好! 我是前端 无名css
在实际工做中,咱们常常会用到Lottie-web来实现动画效果。一个Lottie动画一般由一个data.json文件和N张图片组成。项目中通常使用Lottie动画都是给一个CDN的动画地址,交给lottie-web库去播放,动画资源下载时机是动画播放前去下载data.json文件和图片资源,动画的播放受网络因素影响较大,动画的加载时间较长,若是须要多个lottie动画结合显示,时间不能完美契合,最终效果达不到UI设计师要求。html
本篇文章介绍如何提取lootie动画资源而且利用缓存技术实现Lottie动画从本地读取加载,减小网络请求。前端
详细能够参考:Web 帧动画解决方案 - lottie-web源码剖析node
本文借用 青舟同窗 的图片来简单介绍下Lottie JSON文件结构,方便后续解析JSON文件,读取图片资源以及替换图片地址。android
左侧为使用 AE 新建动画合成须要填入的信息,和右面第一层 JSON 信息对应以下:webpack
其中 assets 对咱们提取Lottie资源以及后期生成新的json尤其重要。git
预加载lottie图片有两种方案:github
方案一:编写webpack插件,构建时提取lottie JSON 图片资源,结合html-webpack-plugin提供的hooks,将图片资源地址以Link方式插入到图片html中。web
方案二:编写webpack插件,构建时提取lottie JSON 图片资源,生成lottieAssets.js配置文件。利用缓存工具库,读取配置文件,缓存资源图片到indexdb中。ajax
方案三:方案三和方案二大致相同,主要是提取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… 目录下
代码参考:
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效果以下:
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
github 戳 :lottie-extract-assets-plugin
该库的封装由 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();
复制代码
效果:
上面咱们已经把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对象:
直接上代码: 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)
复制代码
效果:
首次加载:
二次加载:
lottie最终效果
至此,咱们已经实现了lottie资源加载时机提早,屡次加载均采用缓存资源,成功的减小了网络请求。
在应用产线的过程当中,咱们发现了一些能够优化的地方,对上面的逻辑进行了一些优化
部分代码参考:
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();
}
复制代码
接入:
index.js
//导入生成的lottie-assets.js文件
import "./lottie-assets";
//须要合并的缓存数组和生成的lottie-assets.js文件中window对象名称
const resources=mergeLottieCache([],"_lottieConfig");
//开启缓存
startCache(resources);
复制代码
使用
const defaultOptions = {
loop: false,
autoplay: false,
path: jsonPath,
rendererSettings: {
preserveAspectRatio: "xMidYMid slice",
},
};
//新增,用LottieOptionFactory包装原来的参数便可
LottieOptionFactory(defaultOptions);
复制代码
ps:(替换lottie图片的思路实现之后,咱们之后能够用同一个lottie,动态的替换json中的图片,实现一些定制的动画效果。例如:用户名片展现,名片是个lottie动效,用户头像是定制的,咱们可让设计师把默认用户头像设计到lottie动画中,动态替换用户头像)
本篇文章主要介绍的是技术实现思路,前提是支持indexDB。 欢迎社区的小伙伴多提意见。