前言#
jsDelivr —— JS とオープンソースプロジェクトのための無料で高速で信頼性の高い CDN。しかし、国内ではアクセス速度が遅く、jsDelivr の登録が取り消され、DNS 汚染が頻繁に発生するため、国内で jsDelivr を使用しているサイトはしばしばアクセス速度が遅くなる問題があります。
あなたは、なぜ他の人のミラーサイトを使用しないのかと尋ねるかもしれません。「自分で手を動かせば、豊かになる」という言葉があります。自分でミラーサイトを構築することで、自分のデータとサービスをよりよく管理できます。第三者のミラーサイトを使用することには、いくつかの潜在的なリスクと不確実性があります。まず、第三者のミラーサイトの信頼性と安定性を確認することはできません。これらはいつでも閉鎖されたり、故障したりする可能性があります。これはあなたのサイトに深刻な影響を与え、サービスの中断を引き起こす可能性があります。次に、第三者のミラーサイトは、あなたのサイトのコンテンツをタイムリーに更新および同期できない可能性があり、訪問者が最新の情報を取得できなくなることがあります。最後に、自分でミラーサイトを構築することで、特定のニーズを満たし、より良いユーザー体験を提供するために、自分のサイトをより柔軟にカスタマイズおよび管理できます。したがって、可能な場合は、自分でミラーサイトを構築してデータの安全性と信頼性を確保することをお勧めします。
方法一:又拍云を使用して構築#
利点:#
-
又拍云は国内の有名な企業向けクラウドサービスプロバイダーであり、そのサービスは高速で安定しており、使いやすく、ミラーサイトを構築するのに非常に適しています。又拍云には非常に多くの特典があります:又拍云アライアンスに参加すると(サイトは登録が必要で、サイト / アプリの下部に又拍云のロゴを追加し、公式サイトにリンクする必要があります)、毎月 10G のストレージスペースと 15G の CDN トラフィック(年単位で配布され、67 元のクーポンでアカウントに振り込まれます)を取得できます;新規ユーザー登録 (ここをクリックして登録) すると、61 元のクーポンを直接取得できます(有効期限は 1 年)。これらの特典を利用することで、コストゼロでミラーサイトを構築できます。
-
加速効果が明らかです(図参照)
加速前:
加速後:
欠点:#
登録が必要です!!!
推奨指数:⭐⭐⭐⭐⭐#
構築方法:#
第一步:#
又拍云アカウントを作成する (ここをクリックして登録)
第二步:#
コントロールパネルの CDN 管理画面に入る (https://console.upyun.com/services/cdn/)、サービスを作成し、サービスを設定します(具体的なパラメータは以下の通り)。
- サービス名:サービスを一意に識別する名前、例:image-upyun-com、1 つのサービスに複数の独自ドメインをバインドできます。
注意事項
サービス名は 5~20 文字に制限されます;小文字の英字で始まる必要があり、小文字の英字、数字、ハイフンの組み合わせのみがサポートされています。
- 加速ドメイン:今回設定する加速ドメインを入力します。
注意事項
加速ドメインは工信部に登録されている必要があります;加速ドメインはまだ又拍云 CDN プラットフォームに設定されていません。
加速ドメインはドメイン所有権の確認を行う必要があり、確認が完了した後に追加できます。
- アプリケーションシーン:ここでは全サイト加速を選択します。
- 回源プロトコル:プロトコルに従うを選択します。
- ソース証明書の検証:絶対に開かないでください(実際に開くと使用できなくなります)!!!
- 回線設定:ソースアドレスに
cdn.jsdelivr.net
を入力し、ポート番号は変更しないでください。
第三步#
- サービスが正常に作成された後、操作画面に CDN 加速サービスが正常に作成されたことが表示され、自動的にそのサービスの【機能設定】画面にリダイレクトされます。
- 機能設定画面には、ドメイン管理、回源管理、キャッシュ設定、パフォーマンス最適化、HTTPS、アクセス制御、画像処理などの機能設定モジュールがあります。【ドメイン管理】モジュールでは、このサービスに複数の独自ドメインをバインドできます。ドメイン設定を待つ(約 10 分)間、ドメインの状態が[正常]であるか確認してください。
- CDN プラットフォームから割り当てられた CNAME アドレスを確認できます。この時、ドメイン DNS 解析業者でそのドメインに CNAME レコードを追加する必要があります。CNAME 設定が有効になった後に CDN サービスを使用できます。
第四步(必須ではありませんが、体験を向上させることができます)#
-
HTTPS を設定する:クリックして [HTTPS]、"HTTPS 設定" の右側の管理をクリックします(図参照)その後、独自の証明書を追加するか、又拍云登録証明書を直接使用することを選択できます (ここをクリックして証明書管理に移動)。追加後、[HTTPS アクセス] を開き、[強制 HTTPS アクセス] を選択しないことをお勧めします。
-
スマート圧縮とページ圧縮を有効にすることをお勧めします(図参照)
-
キャッシュ、ブラウザキャッシュ、セグメントキャッシュを設定することをお勧めします(図参照)
-
[アクセス制御] で、Referer の盗用防止、WAF 保護、HTTP リクエストボディサイズ制限、IP アクセス制限を有効にします(図参照)
この設定は参考用です。実際の状況に応じて設定してください!
-
[エッジルール] を設定して、特別な効果を実現することもできます。(ここではデモを行いません)
方法二:Cloudflare workers を使用して構築#
利点:#
- 簡単で、ステップが少なく、加速範囲が広い
- 登録が不要で、加速効果が良好です(図参照)
加速前:
加速後: - 無料で、毎日無料の枠があります(毎日 100000 リクエスト無料)
欠点:#
- 一部の地域ではアクセスが遅い
- 無料枠が少なく、アクセス量の多いサイトには不十分かもしれません
- Cloudflare に登録されたドメインが必要です
推奨指数:⭐⭐⭐⭐#
構築方法#
第一步#
- Cloudflare 管理パネルの [Workers と Pages] セクション ->[概要]->[アプリケーションを作成]->[Worker を作成](図参照)
- 任意の名前を入力し、[デプロイ] をクリックします(図参照)
第二步#
- 作成完了ページで [コードを編集] をクリックします(図参照)
- 以下のコードをそのままコードボックスに貼り付けます:
'use strict'
/**
* static files (404.html, sw.js, conf.js)
*/
const ASSET_URL = 'https://cdn.jsdelivr.net'
const JS_VER = 10
const MAX_RETRY = 1
/** @type {RequestInit} */
const PREFLIGHT_INIT = {
status: 204,
headers: new Headers({
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS',
'access-control-max-age': '1728000',
}),
}
/**
* @param {any} body
* @param {number} status
* @param {Object<string, string>} headers
*/
function makeRes(body, status = 200, headers = {}) {
headers['--ver'] = JS_VER
headers['access-control-allow-origin'] = '*'
return new Response(body, {status, headers})
}
/**
* @param {string} urlStr
*/
function newUrl(urlStr) {
try {
return new URL(urlStr)
} catch (err) {
return null
}
}
addEventListener('fetch', e => {
const ret = fetchHandler(e)
.catch(err => makeRes('cfworker error:\n' + err.stack, 502))
e.respondWith(ret)
})
/**
* @param {FetchEvent} e
*/
async function fetchHandler(e) {
const req = e.request
const urlStr = req.url
const urlObj = new URL(urlStr)
const path = urlObj.href.substr(urlObj.origin.length)
if (urlObj.protocol === 'http:') {
urlObj.protocol = 'https:'
return makeRes('', 301, {
'strict-transport-security': 'max-age=99999999; includeSubDomains; preload',
'location': urlObj.href,
})
}
if (path.startsWith('/http/')) {
return httpHandler(req, path.substr(6))
}
switch (path) {
case '/http':
return makeRes('cfworkerを最新バージョンに更新してください!')
case '/ws':
return makeRes('サポートされていません', 400)
case '/works':
return makeRes('動作しています')
default:
// static files
return fetch(ASSET_URL + path)
}
}
/**
* @param {Request} req
* @param {string} pathname
*/
function httpHandler(req, pathname) {
const reqHdrRaw = req.headers
if (reqHdrRaw.has('x-jsproxy')) {
return Response.error()
}
// preflight
if (req.method === 'OPTIONS' &&
reqHdrRaw.has('access-control-request-headers')
) {
return new Response(null, PREFLIGHT_INIT)
}
let acehOld = false
let rawSvr = ''
let rawLen = ''
let rawEtag = ''
const reqHdrNew = new Headers(reqHdrRaw)
reqHdrNew.set('x-jsproxy', '1')
// このロジックはhttp-dec-req-hdr.luaと大体同じです
// https://github.com/EtherDream/jsproxy/blob/master/lua/http-dec-req-hdr.lua
const refer = reqHdrNew.get('referer')
const query = refer.substr(refer.indexOf('?') + 1)
if (!query) {
return makeRes('パラメータが不足しています', 403)
}
const param = new URLSearchParams(query)
for (const [k, v] of Object.entries(param)) {
if (k.substr(0, 2) === '--') {
// システム情報
switch (k.substr(2)) {
case 'aceh':
acehOld = true
break
case 'raw-info':
[rawSvr, rawLen, rawEtag] = v.split('|')
break
}
} else {
// HTTPリクエストヘッダーを復元
if (v) {
reqHdrNew.set(k, v)
} else {
reqHdrNew.delete(k)
}
}
}
if (!param.has('referer')) {
reqHdrNew.delete('referer')
}
// cfworkerはパス内の`//`を`/`に統合します
const urlStr = pathname.replace(/^(https?):\/+/, '$1://')
const urlObj = newUrl(urlStr)
if (!urlObj) {
return makeRes('無効なプロキシURL: ' + urlStr, 403)
}
/** @type {RequestInit} */
const reqInit = {
method: req.method,
headers: reqHdrNew,
redirect: 'manual',
}
if (req.method === 'POST') {
reqInit.body = req.body
}
return proxy(urlObj, reqInit, acehOld, rawLen, 0)
}
/**
*
* @param {URL} urlObj
* @param {RequestInit} reqInit
* @param {number} retryTimes
*/
async function proxy(urlObj, reqInit, acehOld, rawLen, retryTimes) {
const res = await fetch(urlObj.href, reqInit)
const resHdrOld = res.headers
const resHdrNew = new Headers(resHdrOld)
let expose = '*'
for (const [k, v] of resHdrOld.entries()) {
if (k === 'access-control-allow-origin' ||
k === 'access-control-expose-headers' ||
k === 'location' ||
k === 'set-cookie'
) {
const x = '--' + k
resHdrNew.set(x, v)
if (acehOld) {
expose = expose + ',' + x
}
resHdrNew.delete(k)
}
else if (acehOld &&
k !== 'cache-control' &&
k !== 'content-language' &&
k !== 'content-type' &&
k !== 'expires' &&
k !== 'last-modified' &&
k !== 'pragma'
) {
expose = expose + ',' + k
}
}
if (acehOld) {
expose = expose + ',--s'
resHdrNew.set('--t', '1')
}
// verify
if (rawLen) {
const newLen = resHdrOld.get('content-length') || ''
const badLen = (rawLen !== newLen)
if (badLen) {
if (retryTimes < MAX_RETRY) {
urlObj = await parseYtVideoRedir(urlObj, newLen, res)
if (urlObj) {
return proxy(urlObj, reqInit, acehOld, rawLen, retryTimes + 1)
}
}
return makeRes(res.body, 400, {
'--error': `不正な長さ: ${newLen}, 期待: ${rawLen}`,
'access-control-expose-headers': '--error',
})
}
if (retryTimes > 1) {
resHdrNew.set('--retry', retryTimes)
}
}
let status = res.status
resHdrNew.set('access-control-expose-headers', expose)
resHdrNew.set('access-control-allow-origin', '*')
resHdrNew.set('--s', status)
resHdrNew.set('--ver', JS_VER)
resHdrNew.delete('content-security-policy')
resHdrNew.delete('content-security-policy-report-only')
resHdrNew.delete('clear-site-data')
if (status === 301 ||
status === 302 ||
status === 303 ||
status === 307 ||
status === 308
) {
status = status + 10
}
return new Response(res.body, {
status,
headers: resHdrNew,
})
}
/**
* @param {URL} urlObj
*/
function isYtUrl(urlObj) {
return (
urlObj.host.endsWith('.googlevideo.com') &&
urlObj.pathname.startsWith('/videoplayback')
)
}
/**
* @param {URL} urlObj
* @param {number} newLen
* @param {Response} res
*/
async function parseYtVideoRedir(urlObj, newLen, res) {
if (newLen > 2000) {
return null
}
if (!isYtUrl(urlObj)) {
return null
}
try {
const data = await res.text()
urlObj = new URL(data)
} catch (err) {
return null
}
if (!isYtUrl(urlObj)) {
return null
}
return urlObj
}
- [保存してデプロイ] をクリックします。
第三步#
[概要] に戻り、先ほど作成したプロジェクトをクリックし、[トリガー] をクリックし、[カスタムドメインを追加] をクリックし、アカウント内のドメインまたはそのサブドメインを入力し、[カスタムドメインを追加] をクリックして構築を完了します。
方法三: nginx を使用#
利点:簡単#
欠点:#
加速範囲が狭く、速度はサーバーの性能と速度に制限されます。
推奨指数:⭐⭐⭐#
新しいサイトを作成し、以下の内容を nginx 設定ファイルに上書きします
server {
listen 80;
listen 443 ssl http2;
# 証明書のパスを変更してください
ssl_certificate fullchain.pem;
ssl_certificate_key privkey.pem;
# ミラーのドメイン名を変更してください
server_name jsd.ucbk.cn;
location / {
proxy_pass https://cdn.jsdelivr.net;
proxy_set_header Host $proxy_host;
proxy_set_header Accept-Encoding '';
proxy_ssl_server_name on;
proxy_ssl_name $proxy_host;
proxy_redirect / /;
# プロキシキャッシュ
proxy_cache jsdelivr;
proxy_cache_lock on;
proxy_cache_lock_timeout 15s;
proxy_cache_use_stale updating;
proxy_cache_background_update on;
proxy_cache_key $host$request_uri;
proxy_cache_valid 200 301 302 30d;
proxy_cache_valid 500 501 502 503 15s;
proxy_cache_valid any 5m;
# ドメインを置き換える
sub_filter_once off;
sub_filter_types application/javascript application/json text/xml text/css;
sub_filter '$proxy_host' '$host';
}
}
# キャッシュパスは必要に応じて変更してください
proxy_cache_path /var/tmp/nginx/jsdelivr levels=1:2 use_temp_path=off keys_zone=jsdelivr:300m inactive=30d max_size=30g;
その後、サイト内のcdn.jsdelivr.net
に関連するすべてを自分のものに変更するか、変更したくない場合は、サイトの nginx 設定に以下の内容を追加するだけで済みます。
sub_filter_once off;
sub_filter_types application/javascript application/json text/xml text/css;
sub_filter 'cdn.jsdelivr.net' 'jsd.ucbk.cn';
結尾#
ここまで読んでいただきありがとうございます。何か問題があれば、コメント欄でお知らせください!