使用Cloudflare免费加速豆瓣图书封面

大概是3月份吧,我突然想回顾一下23年一年读过的书,以及记录24年我正在读书。我个人也不太会写前端,所以就用ChatGPT给我生成了一个简单、朴素的书架页面 https://simonmy.com/books.html
制作这个页面的过程很快,但是图书封面的问题让我很头大。如果是直接配置豆瓣的地址,大概率是被拦截的。我自己从豆瓣抓下来,然后传图床再加到页面也很麻烦,所就有了这个简单的小工具。

实用一张简单的流程图说清楚所有的逻辑

  1. 利用Cache快速检查之前是否做过图片转存加速
  2. 如果命中了Cache,直接读取R2中的图片,返回用户
  3. 如果没有命中Cache
    3.1 利用豆瓣API把图片抓取下来,上传到R2
    3.2 更新Cache,以便于后续命中
    3.3 读取R2中的图片,返回用户
  1. 一个Cloudflare账号,没有的自己注册
  2. 一个托管在 Cloudflare的域名 (建议有,不强制)

这一步没什么要注意的,只要名字符合规范就好。创建R2的时候,可以选择亚太区,速度理论上会快一些

这一步是最重要的一步,先到自己账户的 Worker&Pages 页面并创建一个Worke。创建的时候注意不是Pages,Worker的名字自己看着来,使用默认的Hello World模板部署即可。

创建成功之后不要急着贴代码,按照以下步骤来可以少走弯路~

回到自己账户的 Worker&Pages 页面,进入刚才自己创建的Worker,选择 Setting - Variables

  1. 找到 KV Namespace Bindings
    添加变量名为 DOUBAN_KV, 对应的值选择上一步你创建的KV的名字
  2. 找到R2 Bucket Bindings
    添加变量名为 DOUBAN_R2, 对应的值选择上一步你创建的R2的名字
Tips:
这里Cloudflare前端显示有问题,下划线是显示不出来的,贴上去保存即可

回到自己账户的 Worker&Pages 页面,进入刚才自己创建的Worker, 进入右上角的 Edit Code(编辑代码)

将下面的代码粘贴到Worker内的worker.js,部署即可。

worker.js

const DOUBAN_API_HOST = 'frodo.douban.com';
const DOUBAN_API_KEY = '0ac44ae016490db2204ce0a042db2916';

const requestOptions = {
  headers: {
    // host: DOUBAN_API_HOST,
    authorization:  '',
    'user-agent': 'User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 15_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.16(0x18001023) NetType/WIFI Language/zh_CN',
    referer: 'https://servicewechat.com/wx2f9b06c1de1ccfca/84/page-frame.html',
    'cache-control': 'no-store'
  },
  cf: {
    cacheTtl: 0,
    scrapeShield: false,
    cacheEverything: false
  }
};

async function fetchSubject({type, id }) {
  const url = `https://${DOUBAN_API_HOST}/api/v2/${type}/${id}`;
  const params = new URLSearchParams({
    apiKey: DOUBAN_API_KEY
  });

  return fetch(url + '?' + params.toString(), requestOptions);
}

async function fetchCover({ type, id, cache, bucket }) {
  
  const fileCacheKey = 'file_' + type + '_' + id;
  const fileBucketKey = 'cover/' + type + '/' + id + '.jpg'
  
  const fileCacheValue = await cache.get(fileCacheKey);
  if(fileCacheValue) {
    const object = await bucket.get(fileBucketKey)
    if (object === null) {
      return new Response('Object Not Found', { status: 404 });
    }

    const headers = new Headers();
    object.writeHttpMetadata(headers);
    headers.set('etag', object.httpEtag);

    return new Response(object.body, {
      headers,
    });
  } else {
    const urlCacheKey = 'url_' + type + '_' + id;
    const urlCacheValue = await cache.get(urlCacheKey);
    let coverUrl = '';
   
    if(urlCacheValue) {
      coverUrl = urlCacheValue
    } else {
      const subject = await (await fetchSubject({ type, id })).json();
      coverUrl = type === 'celebrity' ? subject.cover_img.url : subject.cover_url;
      await cache.put(urlCacheKey, coverUrl) 
    }
    
    const response = await fetch(coverUrl, requestOptions)
    const bodySize = parseInt(response.headers.get('Content-Length'), 10)
    let originalResponse = response.clone();
    if(response.ok &&  bodySize > 0) {
      await bucket.put(fileBucketKey, response.body);
      await cache.put(fileCacheKey, fileBucketKey);
      console.log(fileCacheKey, fileBucketKey);
    } 
    
    return new Response(originalResponse.body, {
      headers: {
        'cache-control': '2592000'
      }
    });
  }

  
}

// Export a default object containing event handlers
export default {
  async fetch(request, env, ctx) {
    
    const reg = /^\/(movie|book|music|game|celebrity)\/(\d+)\.jpg$/i;

    const url = new URL(request.url);
    const path = url.pathname
    const match = path.match(reg)
    if (!match) {
      return new Response('')
    }
    const[_, type, id] = match
    const cache  = env.DOUBAN_KV
    const bucket  = env.DOUBAN_R2

  
    return await fetchCover({ type, id, cache, bucket })
  },
};

回到自己账户的 Worker&Pages 页面,进入刚才自己创建的Worker,选择 Setting - Triggers, 自行添加即可。

假设我们想要的是《置身事内》这本书的封面,Worker域名以 demo.account.workers.dev 为例。

  1. 先到豆瓣找到这本书的地址 https://book.douban.com/subject/35546622/
  2. 获取到书的ID为 35546622
  3. 那么图书封面的地址就是https://demo.account.workers.dev/book/35546622.jpg
Tips:
如果是自定义域名,那就用自定义域名。如果没有自定义域名,那就用官方给的worker域名

1.由于使用Cloudflare的网络,图片的加载速度在某些地区可能得不到保证
2.Cloudflare Function免费版每日限制100,000个请求(即加载图片的总次数不能超过100,000次)如超过可能需要选择购买Cloudflare Function的付费套餐

避免有人抬杠,这里也专门标注一下。代码仅仅为了解决豆瓣图片这个小问题,确实存在一些缺陷和不足,一是js我不太专业。二是我知道并发时可能多次下载图片导致ABA问题,没必要解决。