├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.d.ts ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | !**/*.xcodeproj 7 | !**/*.pbxproj 8 | !**/*.xcworkspacedata 9 | !**/*.xcsettings 10 | !**/*.xcscheme 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata 20 | *.xccheckout 21 | *.moved-aside 22 | DerivedData 23 | *.hmap 24 | *.ipa 25 | *.xcuserstate 26 | project.xcworkspace 27 | 28 | # Android/IntelliJ 29 | # 30 | build/ 31 | .idea 32 | .gradle 33 | local.properties 34 | 35 | # node.js 36 | # 37 | node_modules/ 38 | npm-debug.log 39 | yarn.lock 40 | yarn-error.log 41 | 42 | # others 43 | # 44 | .buckconfig 45 | .flowconfig 46 | .gitattributes 47 | .watchmanconfig 48 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | !**/*.xcodeproj 7 | !**/*.pbxproj 8 | !**/*.xcworkspacedata 9 | !**/*.xcsettings 10 | !**/*.xcscheme 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata 20 | *.xccheckout 21 | *.moved-aside 22 | DerivedData 23 | *.hmap 24 | *.ipa 25 | *.xcuserstate 26 | project.xcworkspace 27 | 28 | # Android/IJ 29 | # 30 | /build/ 31 | .idea 32 | .gradle 33 | local.properties 34 | 35 | # node.js 36 | # 37 | node_modules/ 38 | npm-debug.log 39 | yarn.lock 40 | yarn-error.log 41 | 42 | # others 43 | # 44 | .npmignore 45 | .buckconfig 46 | .flowconfig 47 | .gitattributes 48 | .gitignore 49 | .git 50 | .watchmanconfig 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Wonday (@wonday.org) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-image-cache-wrapper 2 | [![npm](https://img.shields.io/npm/v/react-native-image-cache-wrapper.svg?style=flat-square)](https://www.npmjs.com/package/react-native-image-cache-wrapper) 3 | 4 | The best react native image cache wrapper. 5 | 6 | ### Feature 7 | 8 | * the same usage with `````` and `````` 9 | * can set activity indicator 10 | * cache images with expiration 11 | * clear one cache or all cache files 12 | * support getSize() and prefetch() 13 | * support cache base64 data to local 14 | 15 | ### Installation 16 | We use [`rn-fetch-blob`](https://github.com/joltup/rn-fetch-blob) to handle file system access in this package, 17 | So you should install react-native-image-cache-wrapper and rn-fetch-blob both. 18 | 19 | ```bash 20 | npm install react-native-image-cache-wrapper --save 21 | 22 | npm install rn-fetch-blob --save 23 | react-native link rn-fetch-blob 24 | ``` 25 | or use yarn 26 | 27 | ``` 28 | yarn add react-native-image-cache-wrapper 29 | yarn add rn-fetch-blob 30 | ``` 31 | *Notice: if you use RN 0.60+, please use rn-fetch-blob v0.10.16* 32 | 33 | 34 | ### ChangeLog 35 | 36 | v1.0.7 37 | 38 | 1. fix Strange error 39 | 40 | v1.0.6 41 | 42 | 1. add static method ```CachedImage.isUrlCached(url,success=(cachFile)=>void,fail=(error)=>void))``` 43 | 2. add static method ```CachedImage.getCacheFilename(url)``` 44 | 3. add static property ```CachedImage.cacheDir```, user can use to set customized cacheDir 45 | 46 | v1.0.5 47 | 48 | 1. fix for RN 0.59 49 | 50 | v1.0.4 51 | 52 | 1. fix Content-Length check 53 | 54 | v1.0.3 55 | 56 | 1. fix file successfully download check 57 | 58 | v1.0.2 59 | 60 | 1. use rn-fetch-blob instead of react-native-fetch-blob 61 | 62 | v1.0.0 63 | 64 | 1. initial release 65 | 66 | [[more]](https://github.com/wonday/react-native-image-cache-wrapper/releases) 67 | 68 | 69 | ### Configuration 70 | 71 | | Property | Type | Default | Description | FirstRelease | 72 | | ------------- |:-------------:|:----------------:| ------------------- | ------------ | 73 | | `````` or `````` properties | | | same with `````` and `````` | 1.0 | 74 | | expiration | number | 604800 | expiration seconds (0:no expiration, default cache a week) | 1.0 | 75 | | activityIndicator | Component | null | when loading show it as an indicator, you can use your component| 1.0 | 76 | 77 | ### Usage 78 | 79 | ``` 80 | import CachedImage from 'react-native-image-cache-wrapper'; 81 | 82 | render() 83 | { 84 | return ( 85 | 86 | 87 | 88 | 89 | This is example with image background. 90 | 91 | 92 | ); 93 | } 94 | ``` 95 | 96 | ### Static Function 97 | 98 | **CachedImage.getSize(url, success=(width,height)=>void,fail=(error)=>void)** 99 | 100 | Get the image size, if no cache, will cache it. 101 | 102 | Example: 103 | ``` 104 | import CachedImage from 'react-native-image-cache-wrapper'; 105 | 106 | CachedImage.getSize("https://assets-cdn.github.com/images/modules/logos_page/Octocat.png", 107 | (width,height)=>{ 108 | console.log("width:"+width+" height:"+height); 109 | },(error)=>{ 110 | console.log("error:"+error); 111 | }); 112 | ``` 113 | 114 | **CachedImage.prefetch(url,expiration=0,success=(cachFile)=>void,fail=(error)=>void)** 115 | 116 | prefetch an image and cache it. 117 | 118 | Example: 119 | ``` 120 | import CachedImage from 'react-native-image-cache-wrapper'; 121 | 122 | // prefetch and cache image 3600 seconds 123 | CachedImage.prefetch("https://assets-cdn.github.com/images/modules/logos_page/Octocat.png", 3600, 124 | (cacheFile)=>{ 125 | console.log("cache filename:"+cacheFile); 126 | },(error)=>{ 127 | console.log("error:"+error); 128 | }); 129 | ``` 130 | 131 | **CachedImage.deleteCache(url)** 132 | 133 | delete a cache file. 134 | 135 | Example: 136 | ``` 137 | import CachedImage from 'react-native-image-cache-wrapper'; 138 | 139 | // prefetch and cache image 3600 seconds 140 | CachedImage.deleteCache("https://assets-cdn.github.com/images/modules/logos_page/Octocat.png"); 141 | ``` 142 | 143 | **CachedImage.clearCache()** 144 | 145 | clear all cache. 146 | 147 | Example: 148 | ``` 149 | import CachedImage from 'react-native-image-cache-wrapper'; 150 | 151 | // prefetch and cache image 3600 seconds 152 | CachedImage.clearCache(); 153 | ``` 154 | 155 | **CachedImage.isUrlCached(url,success=(cachFile)=>void,fail=(error)=>void))** 156 | 157 | check if a url is cached. 158 | 159 | Example: 160 | ``` 161 | import CachedImage from 'react-native-image-cache-wrapper'; 162 | 163 | // check if a url is cached. 164 | CachedImage.isUrlCached(url,(exists)=>{ 165 | alert(exists); 166 | }); 167 | ``` 168 | 169 | **CachedImage.getCacheFilename(url)** 170 | 171 | make a cache filename. 172 | 173 | Example: 174 | ``` 175 | import CachedImage from 'react-native-image-cache-wrapper'; 176 | 177 | // check if a url is cached. 178 | let cachedFilename = CachedImage.getCacheFilename(url); 179 | ``` 180 | 181 | **CachedImage.cacheDir** 182 | 183 | the property that can get/set cacheDir 184 | 185 | Example: 186 | ``` 187 | import CachedImage from 'react-native-image-cache-wrapper'; 188 | 189 | // check if a url is cached. 190 | CachedImage.cacheDir = RNFetchBlob.fs.dirs.CacheDir + "/CachedImage/"; 191 | ``` 192 | 193 | 194 | 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2018-present, Wonday (@wonday.org) 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | import * as React from 'react'; 10 | import * as ReactNative from 'react-native'; 11 | 12 | interface Props { 13 | source: any, 14 | defaultSource?: any, 15 | style?: any, 16 | resizeMode?: string, 17 | expiration?: number, 18 | activityIndicator?: any, 19 | onLoad?: () => void, 20 | onError?: (error: object) => void, 21 | } 22 | 23 | declare class CachedImage extends React.Component { 24 | } 25 | 26 | export default CachedImage; 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2018-present, Wonday (@wonday.org) 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 'use strict'; 9 | 10 | import { 11 | Image, 12 | ImageBackground, 13 | Platform, 14 | View 15 | } from 'react-native'; 16 | import React, {Component} from 'react'; 17 | 18 | import RNFetchBlob from 'rn-fetch-blob'; 19 | 20 | const SHA1 = require('crypto-js/sha1'); 21 | 22 | const defaultImageTypes = ['png', 'jpeg', 'jpg', 'gif', 'bmp', 'tiff', 'tif']; 23 | 24 | export default class CachedImage extends Component { 25 | 26 | static defaultProps = { 27 | expiration: 86400 * 7, // default cache a week 28 | activityIndicator: null, // default not show an activity indicator 29 | }; 30 | 31 | static cacheDir = RNFetchBlob.fs.dirs.CacheDir + "/CachedImage/"; 32 | 33 | static sameURL = [] 34 | /** 35 | * delete a cache file 36 | * @param url 37 | */ 38 | static deleteCache = url => { 39 | const cacheFile = _getCacheFilename(url); 40 | return _unlinkFile(cacheFile); 41 | }; 42 | 43 | /** 44 | * clear all cache files 45 | */ 46 | static clearCache = () => _unlinkFile(CachedImage.cacheDir); 47 | 48 | /** 49 | * check if a url is cached 50 | */ 51 | static isUrlCached = (url: string, success: Function, failure: Function) => { 52 | const cacheFile = _getCacheFilename(url); 53 | RNFetchBlob.fs.exists(cacheFile) 54 | .then((exists) => { 55 | success && success(exists); 56 | }) 57 | .catch((error) => { 58 | failure && failure(error); 59 | }); 60 | }; 61 | 62 | /** 63 | * make a cache filename 64 | * @param url 65 | * @returns {string} 66 | */ 67 | static getCacheFilename = (url) => { 68 | return _getCacheFilename(url); 69 | } 70 | 71 | /** 72 | * Same as ReactNaive.Image.getSize only it will not download the image if it has a cached version 73 | * @param url 74 | * @param success callback (width,height)=>{} 75 | * @param failure callback (error:string)=>{} 76 | */ 77 | static getSize = (url: string, success: Function, failure: Function) => { 78 | 79 | CachedImage.prefetch(url, 0, 80 | (cacheFile) => { 81 | if (Platform.OS === 'android') { 82 | url = "file://" + cacheFile; 83 | } else { 84 | url = cacheFile; 85 | } 86 | Image.getSize(url, success, failure); 87 | }, 88 | (error) => { 89 | Image.getSize(url, success, failure); 90 | }); 91 | 92 | }; 93 | 94 | /** 95 | * prefech an image 96 | * 97 | * @param url 98 | * @param expiration if zero or not set, no expiration 99 | * @param success callback (cacheFile:string)=>{} 100 | * @param failure callback (error:string)=>{} 101 | */ 102 | static prefetch = (url: string, expiration: number, success: Function, failure: Function) => { 103 | 104 | // source invalidate 105 | if (!url || url.toString() !== url) { 106 | failure && failure("no url."); 107 | return; 108 | } 109 | 110 | const cacheFile = _getCacheFilename(url); 111 | if(CachedImage.sameURL.includes(cacheFile)){ 112 | 113 | success && success(cacheFile); 114 | return 115 | } 116 | CachedImage.sameURL.push(cacheFile) 117 | 118 | RNFetchBlob.fs.stat(cacheFile) 119 | .then((stats) => { 120 | // if exist and not expired then use it. 121 | if (!Boolean(expiration) || (expiration * 1000 + stats.lastModified) > (new Date().getTime())) { 122 | success && success(cacheFile); 123 | } else { 124 | _saveCacheFile(url, success, failure); 125 | } 126 | }) 127 | .catch((error) => { 128 | // not exist 129 | // success && success(cacheFile) 130 | 131 | _saveCacheFile(url, success, failure); 132 | }); 133 | }; 134 | 135 | constructor(props) { 136 | 137 | super(props); 138 | this.state = { 139 | source: null, 140 | }; 141 | 142 | this._useDefaultSource = false; 143 | this._downloading = false; 144 | this._mounted = false; 145 | } 146 | 147 | componentDidMount() { 148 | this._mounted = true; 149 | } 150 | 151 | componentWillReceiveProps(nextProps) { 152 | const { source } = nextProps; 153 | 154 | if (source !== this.props.source) { this.setState({...this.props, source: source}) } 155 | } 156 | 157 | componentWillUnmount() { 158 | this._mounted = false; 159 | } 160 | 161 | render() { 162 | 163 | if (this.props.source && this.props.source.uri) { 164 | if (!this.state.source && !this._downloading) { 165 | this._downloading = true; 166 | CachedImage.prefetch(this.props.source.uri, 167 | this.props.expiration, 168 | (cacheFile) => { 169 | setTimeout(() => { 170 | if (this._mounted) { 171 | this.setState({source: {uri: "file://" + cacheFile}}); 172 | } 173 | this._downloading = false; 174 | }, 0); 175 | }, (error) => { 176 | // cache failed use original source 177 | if (this._mounted) { 178 | setTimeout(() => { 179 | this.setState({source: { uri: this.props.source.uri}}); 180 | }, 0); 181 | } 182 | this._downloading = false; 183 | }); 184 | } 185 | } else { 186 | this.state.source = this.props.source; 187 | } 188 | 189 | if (this.state.source) { 190 | 191 | const renderImage = (props, children) => (children != null ? 192 | {children} : 193 | ); 194 | 195 | const result = renderImage({ 196 | ...this.props, 197 | source: this.state.source, 198 | onError: (error) => { 199 | // error happened, delete cache 200 | if (this.props.source && this.props.source.uri) { 201 | CachedImage.deleteCache(this.props.source.uri); 202 | } 203 | if (this.props.onError) { 204 | this.props.onError(error); 205 | } else { 206 | if (!this._useDefaultSource && this.props.defaultSource) { 207 | this._useDefaultSource = true; 208 | setTimeout(() => { 209 | if(this.props.source && this.props.source.uri ){ 210 | this.setState({source: this.props.source}); 211 | } 212 | else 213 | this.setState({source: this.props.defaultSource}); 214 | }, 0); 215 | } 216 | } 217 | } 218 | }, this.props.children); 219 | 220 | return (result); 221 | } else { 222 | return ( 223 | 227 | {this.props.activityIndicator} 228 | ); 229 | } 230 | } 231 | } 232 | 233 | async function _unlinkFile(file) { 234 | try { 235 | return await RNFetchBlob.fs.unlink(file); 236 | } catch (e) { 237 | } 238 | } 239 | 240 | /** 241 | * make a cache filename 242 | * @param url 243 | * @returns {string} 244 | */ 245 | function _getCacheFilename(url) { 246 | 247 | if (!url || url.toString() !== url) return ""; 248 | 249 | let ext = url.replace(/.+\./, "").toLowerCase(); 250 | if (defaultImageTypes.indexOf(ext) === -1) ext = "png"; 251 | let hash = SHA1(url); 252 | return CachedImage.cacheDir + hash + "." + ext; 253 | } 254 | 255 | /** 256 | * save a url or base64 data to local file 257 | * 258 | * 259 | * @param url 260 | * @param success callback (cacheFile:string)=>{} 261 | * @param failure callback (error:string)=>{} 262 | */ 263 | async function _saveCacheFile(url: string, success: Function, failure: Function) { 264 | 265 | try { 266 | const isNetwork = !!(url && url.match(/^https?:\/\//)); 267 | const isBase64 = !!(url && url.match(/^data:/)); 268 | const cacheFile = _getCacheFilename(url); 269 | 270 | if (isNetwork) { 271 | const tempCacheFile = cacheFile + '.tmp'; 272 | _unlinkFile(tempCacheFile); 273 | RNFetchBlob.config({ 274 | // response data will be saved to this path if it has access right. 275 | path: tempCacheFile, 276 | }) 277 | .fetch( 278 | 'GET', 279 | url 280 | ) 281 | .then(async (res) => { 282 | 283 | if (res && res.respInfo && res.respInfo.headers && !res.respInfo.headers["Content-Encoding"] && !res.respInfo.headers["Transfer-Encoding"] && res.respInfo.headers["Content-Length"]) { 284 | const expectedContentLength = res.respInfo.headers["Content-Length"]; 285 | let actualContentLength; 286 | 287 | try { 288 | const fileStats = await RNFetchBlob.fs.stat(res.path()); 289 | 290 | if (!fileStats || !fileStats.size) { 291 | throw new Error("FileNotFound:"+url); 292 | } 293 | 294 | actualContentLength = fileStats.size; 295 | } catch (error) { 296 | throw new Error("DownloadFailed:"+url); 297 | } 298 | 299 | if (expectedContentLength != actualContentLength) { 300 | throw new Error("DownloadFailed:"+url); 301 | } 302 | } 303 | 304 | _unlinkFile(cacheFile); 305 | RNFetchBlob.fs 306 | .mv(tempCacheFile, cacheFile) 307 | .then(() => { 308 | success && success(cacheFile); 309 | }) 310 | .catch(async (error) => { 311 | throw error; 312 | }); 313 | }) 314 | .catch(async (error) => { 315 | _unlinkFile(tempCacheFile); 316 | _unlinkFile(cacheFile); 317 | failure && failure(error); 318 | }); 319 | } else if (isBase64) { 320 | let data = url.replace(/data:/i, ''); 321 | RNFetchBlob.fs 322 | .writeFile(cacheFile, data, 'base64') 323 | .then(() => { 324 | success && success(cacheFile); 325 | }) 326 | .catch(async (error) => { 327 | _unlinkFile(cacheFile); 328 | failure && failure(error); 329 | }); 330 | } else { 331 | failure && failure(new Error("NotSupportedUrl")); 332 | } 333 | } catch (error) { 334 | failure && failure(error); 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-image-cache-wrapper", 3 | "version": "1.0.7", 4 | "description": "The best react native image cache wrapper.", 5 | "main": "index.js", 6 | "typings": "./index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/wonday/react-native-image-cache-wrapper.git" 10 | }, 11 | "keywords": [ 12 | "Cache", 13 | "Cached Image", 14 | "Image Cache", 15 | "Image", 16 | "ImageBackground", 17 | "react-component", 18 | "react-native", 19 | "android", 20 | "ios" 21 | ], 22 | "author": { 23 | "name": "Wonday", 24 | "url": "https://github.com/wonday" 25 | }, 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/wonday/react-native-image-cache-wrapper/issues" 29 | }, 30 | "dependencies": { 31 | "crypto-js": "^3.1.9-1" 32 | }, 33 | "peerDependencies": { 34 | "rn-fetch-blob": "^0.10.12" 35 | } 36 | } --------------------------------------------------------------------------------