Skip to content
鼓励作者:欢迎打赏犒劳

cloudflare

cloudflare page部署vitepress

pages部署还是很简单的,支持自动部署,会分配一个ssl的域名 https://cloudflare-demo2.pages.dev/

cloudflare-demo2 是你的项目名称

自定义域名

设置也很简单, 在阿里云域名控制台设置下cname即可

function 接口

cf是支持接口的,支持两种写法,一个是page的function,另外一个是worker,这里介绍下function的使用。

创建接口

就是请求的 HTTP 方法决定了调用哪个函数:

导出函数名对应的 HTTP 方法典型使用场景
onRequestGetGET获取数据、访问页面
onRequestPostPOST提交数据、创建资源
onRequestPutPUT更新资源(全量)
onRequestDeleteDELETE删除资源
onRequestPatchPATCH更新资源(部分)
onRequestOptionsOPTIONSCORS 预检请求
onRequest (通用)所有方法如果不区分方法,只导出一个 onRequest,则会处理所有请求。

在根目录下创建functions/api/fetch-html.ts

shell
// functions/api/fetch-html.ts
// Cloudflare Pages Function: GET /api/fetch-html?url=https://example.com


/**前端用法示例

 * const res = await fetch(`/api/fetch-html?url=${encodeURIComponent(pageUrl)}`)
 * const data = await res.json() as { ok: boolean; html: string }
 *
 * if (!data.ok) {
 *   // 提示用户:自动获取失败,请手动复制网页源码
 *   return
 * }
 * // data.html Readability / Turndown
 */

/** 允许调用本接口的前端域名(按你的站点改,只保留协议和域名,端口自动匹配) */
const ALLOWED_HOSTS = [
    'https://html2md.share888.top',
    'http://localhost',
    'http://127.0.0.1',
]

const FETCH_HEADERS: HeadersInit = {
    'user-agent':
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
    accept:
        'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
    'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
    'cache-control': 'no-cache',
    pragma: 'no-cache',
    'upgrade-insecure-requests': '1',
}

type FetchHtmlResult = {
    ok: boolean
    html: string
}

function isAllowedOrigin(origin: string): boolean {
    try {
        const url = new URL(origin)
        const host = `${url.protocol}//${url.hostname}`
        return ALLOWED_HOSTS.includes(host)
    } catch {
        return false
    }
}

function json(data: FetchHtmlResult, init: ResponseInit = {}, origin?: string | null) {
    const headers = new Headers(init.headers)
    headers.set('content-type', 'application/json; charset=utf-8')
    headers.set('cache-control', 'no-store')
    if (origin && isAllowedOrigin(origin)) {
        headers.set('access-control-allow-origin', origin)
        headers.set('vary', 'Origin')
    }
    return new Response(JSON.stringify(data), { ...init, headers })
}

function fail(origin?: string | null, status = 200) {
    // 业务失败也返回 200 + ok:false,方便前端统一处理提示
    return json({ ok: false, html: '' }, { status }, origin)
}

function getAllowedOrigin(request: Request): string | null {
    const host = request.headers.get('Host') || ''
    if (host.includes('localhost') || host.includes('127.0.0.1')) {
        return `http://${host.split(':')[0]}`
    }

    const origin = request.headers.get('Origin')
    if (origin) {
        return isAllowedOrigin(origin) ? origin : null
    }

    const referer = request.headers.get('Referer')
    if (referer) {
        try {
            const refOrigin = new URL(referer).origin
            return isAllowedOrigin(refOrigin) ? refOrigin : null
        } catch {
            return null
        }
    }

    return null
}

function isHttpUrl(value: string): URL | null {
    try {
        const u = new URL(value)
        if (u.protocol !== 'http:' && u.protocol !== 'https:') return null
        return u
    } catch {
        return null
    }
}

export const onRequestOptions: PagesFunction = async (context) => {
    const origin = getAllowedOrigin(context.request)
    if (!origin) {
        return new Response(null, { status: 403 })
    }

    return new Response(null, {
        status: 204,
        headers: {
            'access-control-allow-origin': origin,
            'access-control-allow-methods': 'GET, OPTIONS',
            'access-control-allow-headers': 'content-type',
            'access-control-max-age': '86400',
            vary: 'Origin',
        },
    })
}

export const onRequestGet: PagesFunction = async (context) => {
    const origin = getAllowedOrigin(context.request)
    if (!origin) {
        return json({ ok: false, html: '' }, { status: 403 })
    }

    const target = new URL(context.request.url).searchParams.get('url')?.trim()
    if (!target) {
        return fail(origin)
    }

    const parsed = isHttpUrl(target)
    if (!parsed) {
        return fail(origin)
    }

    try {
        const res = await fetch(parsed.toString(), {
            method: 'GET',
            headers: {
                ...FETCH_HEADERS,
                // 部分站点会校验 Referer
                referer: parsed.origin + '/',
            },
            redirect: 'follow',
            cf: {
                // 降低被当成自动化脚本的概率;超时按需调整
                cacheTtl: 0,
                cacheEverything: false,
            },
        })

        if (!res.ok) {
            return fail(origin)
        }

        const contentType = (res.headers.get('content-type') || '').toLowerCase()
        // 明显不是 HTML 时直接判失败(如图片、PDF、JSON)
        if (
            contentType &&
            !contentType.includes('text/html') &&
            !contentType.includes('application/xhtml') &&
            !contentType.includes('text/plain')
        ) {
            return fail(origin)
        }

        const html = await res.text()
        if (!html.trim()) {
            return fail(origin)
        }

        return json({ ok: true, html }, { status: 200 }, origin)
    } catch {
        return fail(origin)
    }
}

创建配置文件functions/tsconfig.json

shell
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "lib": ["ES2022"],
    "types": ["@cloudflare/workers-types"],
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true,
    "isolatedModules": true
  },
  "include": ["./**/*.ts"]
}

增加依赖 package.json

wrangler 是cf专门为本地调试用的。

@cloudflare/workers-types 是cf内置需要的类型,不加接口会报错

shell
{
  "name": "vue3-demo",
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "dev:wrangler": "wrangler pages dev --port 8788 --proxy 5174",   //新增
    "build": "vite build"
  },
  "devDependencies": {
    "@cloudflare/workers-types": "^4.20230914.0",    //新增
    "wrangler": "^3.28.0",    //新增
    "vite": "^4.5.3",
  }
}

修改配置vite.config.js

shell
export default ({command,mode}) => {
    return {
        server: {
            port: 5174,
            proxy: {
                '/api': {
                    target: 'http://localhost:8788',
                    changeOrigin: true,
                },
            },
        },
    }
};

测试

http://127.0.0.1:8788/api/fetch-html?url=https://baidu.com

如有转载或 CV 的请标注本站原文地址