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。 歡迎社區的小夥伴多提意見。