E家之长 发布的文章 - 🥝 E 家 分 享 🥝
首页
📋 留言板
🔗 友情链接
🛠️ E家百宝箱
❤️ 关于
推荐
🔍 VPS监控
🐉 青龙面板
💽 E家网盘
----------
🔗 CloudFlare
🔗 甲骨文云
🔗 RackNerd
搜 索
1
自动提取 ChromeGo 一键翻墙包内的免费节点
145 阅读
2
【汇总:免费节点 - 每周更新】
114 阅读
3
GigaFile - 日本免费大文件加密分享服务,最长保留文档100天
72 阅读
4
【E家分享月刊系列】2024-12
56 阅读
5
CloudFlare WARP 免费 VPN 搭建教程
55 阅读
精选网站
网站搭建
科学上网搭建
有感而发
软件技巧
Excel技巧
WordPress技巧
登录
搜 索
标签搜索
WordPress
脚本
GitHub
科学上网
哈佛管理导师
E家分享月刊系列
V2ray
Mac软件
AI
Cloudflare
Docker
免费节点
建站在线工具
Excel技巧
Notion
Nginx
ChatGPT
图像编辑
免费图床
网盘资源
E家之长
累计撰写
195
篇文章
累计收到
245
条评论
首页
栏目
精选网站
网站搭建
科学上网搭建
有感而发
软件技巧
Excel技巧
WordPress技巧
页面
📋 留言板
🔗 友情链接
🛠️ E家百宝箱
❤️ 关于
推荐
🔍 VPS监控
🐉 青龙面板
💽 E家网盘
----------
🔗 CloudFlare
🔗 甲骨文云
🔗 RackNerd
用户登录
登录
找到
195
篇与
E家之长
相关的结果
2023-08-06
CloudFlare Workers VLESS 永久免费节点搭建
前言近期很火的利用 CloudFlare Workers 搭建 VLESS 节点,支持自定义伪装网页,可以 CDN 自选域名,13个端口随便换。注册 CloudFlare相信很多人都有 CloudFlare 帐户,这里额外介绍一个临时邮箱地址,用于注册 CloudFlare。 临时邮箱:https://www.linshiyouxiang.net/CloudFlare Workers vless 代码部署登录 CloudFlare:https://dash.cloudflare.com/点击 Workers 和 Pages,点击 创建 Workers。 创建一个脚本名称,比如 vless,点击 部署。 点击 配置 Worker,点击 快速编辑。清除默认代码。将下面章节提到的代码,粘贴到代码框中。找到 let userID = 和 let proxyIP,更改 UUID 和推荐的 CDN 加速 IP。点击 保存并部署。以下介绍两组代码供选择使用,强烈建议更换默认的 userID,如果不更换,容易被扫描盗用。生成 UUID在线生成地址:https://1024tools.com/uuid打开 V2ray 客户端,选择 添加 VLESS 服务器,在弹出的对话框中,点击 生成 按钮,即可生成 UUID。CDN 加速 IPcdn-all.xn--b6gac.eu.org cdn.xn--b6gac.eu.org cdn-b100.xn--b6gac.eu.org edgetunnel.anycast.eu.org cdn.anycast.eu.org(亚洲地区) jp.cloudflarest.link achk.cloudflarest.link(阿里香港优选)vless 部署代码1代码引用地址:https://github.com/zizifn/edgetunnel/blob/main/src/worker-vless.js 注意:需要更改 UUID, 添加 proxyIP 地址。// <!--GAMFC-->version base on commit 43fad05dcdae3b723c53c226f8181fc5bd47223e, time is 2023-06-22 15:20:02 UTC<!--GAMFC-END-->. // @ts-ignore import from 'cloudflare:sockets'; // How to generate your own UUID: // [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()" let userID = 'd342d11e-d424-4583-b36e-524ab1f0afa4'; let proxyIP = ''; if (!isValidUUID(userID)) { throw new Error('uuid is not valid'); } export default { /** * @param request * @param } env * @param ctx * @returns */ async fetch(request, env, ctx) { try { userID = env.UUID || userID; proxyIP = env.PROXYIP || proxyIP; const upgradeHeader = request.headers.get('Upgrade'); if (!upgradeHeader || upgradeHeader !== 'websocket') { const url = new URL(request.url); switch (url.pathname) { case '/': return new Response(JSON.stringify(request.cf), ); case `/$`: { const vlessConfig = getVLESSConfig(userID, request.headers.get('Host')); return new Response(`$`, { status: 200, headers: { "Content-Type": "text/plain;charset=utf-8", } }); } default: return new Response('Not found', ); } } else { return await vlessOverWSHandler(request); } } catch (err) { /** @type */ let e = err; return new Response(e.toString()); } }, }; /** * * @param request */ async function vlessOverWSHandler(request) { /** @type */ // @ts-ignore const webSocketPair = new WebSocketPair(); const [client, webSocket] = Object.values(webSocketPair); webSocket.accept(); let address = ''; let portWithRandomLog = ''; const log = (/** @type */ info, /** @type */ event) => { console.log(`[$:$] $`, event || ''); }; const earlyDataHeader = request.headers.get('sec-websocket-protocol') || ''; const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); /** @type }*/ let remoteSocketWapper = { value: null, }; let udpStreamWrite = null; let isDns = false; // ws --> remote readableWebSocketStream.pipeTo(new WritableStream({ async write(chunk, controller) { if (isDns && udpStreamWrite) { return udpStreamWrite(chunk); } if (remoteSocketWapper.value) { const writer = remoteSocketWapper.value.writable.getWriter() await writer.write(chunk); writer.releaseLock(); return; } const { hasError, message, portRemote = 443, addressRemote = '', rawDataIndex, vlessVersion = new Uint8Array([0, 0]), isUDP, } = processVlessHeader(chunk, userID); address = addressRemote; portWithRandomLog = `$--$ ${isUDP ? 'udp ' : 'tcp ' } `; if (hasError) { // controller.error(message); throw new Error(message); // cf seems has bug, controller.error will not end stream // webSocket.close(1000, message); return; } // if UDP but port not DNS port, close it if (isUDP) { if (portRemote === 53) { isDns = true; } else { // controller.error('UDP proxy only enable for DNS which is port 53'); throw new Error('UDP proxy only enable for DNS which is port 53'); // cf seems has bug, controller.error will not end stream return; } } // ["version", "附加信息长度 N"] const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]); const rawClientData = chunk.slice(rawDataIndex); // TODO: support udp here when cf runtime has udp support if (isDns) { const = await handleUDPOutBound(webSocket, vlessResponseHeader, log); udpStreamWrite = write; udpStreamWrite(rawClientData); return; } handleTCPOutBound(remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log); }, close() { log(`readableWebSocketStream is close`); }, abort(reason) { log(`readableWebSocketStream is abort`, JSON.stringify(reason)); }, })).catch((err) => { log('readableWebSocketStream pipeTo error', err); }); return new Response(null, { status: 101, // @ts-ignore webSocket: client, }); } /** * Handles outbound TCP connections. * * @param remoteSocket * @param addressRemote The remote address to connect to. * @param portRemote The remote port to connect to. * @param rawClientData The raw client data to write. * @param webSocket The WebSocket to pass the remote socket to. * @param vlessResponseHeader The VLESS response header. * @param log The logging function. * @returns The remote socket. */ async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) { async function connectAndWrite(address, port) { /** @type */ const tcpSocket = connect({ hostname: address, port: port, }); remoteSocket.value = tcpSocket; log(`connected to $:$`); const writer = tcpSocket.writable.getWriter(); await writer.write(rawClientData); // first write, nomal is tls client hello writer.releaseLock(); return tcpSocket; } // if the cf connect tcp socket have no incoming data, we retry to redirect ip async function retry() { const tcpSocket = await connectAndWrite(proxyIP || addressRemote, portRemote) // no matter retry success or not, close websocket tcpSocket.closed.catch(error => { console.log('retry tcpSocket closed error', error); }).finally(() => { safeCloseWebSocket(webSocket); }) remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, null, log); } const tcpSocket = await connectAndWrite(addressRemote, portRemote); // when remoteSocket is ready, pass to websocket // remote--> ws remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, retry, log); } /** * * @param webSocketServer * @param earlyDataHeader for ws 0rtt * @param log for ws 0rtt */ function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { let readableStreamCancel = false; const stream = new ReadableStream({ start(controller) { webSocketServer.addEventListener('message', (event) => { if (readableStreamCancel) { return; } const message = event.data; controller.enqueue(message); }); // The event means that the client closed the client -> server stream. // However, the server -> client stream is still open until you call close() on the server side. // The WebSocket protocol says that a separate close message must be sent in each direction to fully close the socket. webSocketServer.addEventListener('close', () => { // client send close, need close server // if stream is cancel, skip controller.close safeCloseWebSocket(webSocketServer); if (readableStreamCancel) { return; } controller.close(); } ); webSocketServer.addEventListener('error', (err) => { log('webSocketServer has error'); controller.error(err); } ); // for ws 0rtt const = base64ToArrayBuffer(earlyDataHeader); if (error) { controller.error(error); } else if (earlyData) { controller.enqueue(earlyData); } }, pull(controller) { // if ws can stop read if stream is full, we can implement backpressure // https://streams.spec.whatwg.org/#example-rs-push-backpressure }, cancel(reason) { // 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here // 2. if readableStream is cancel, all controller.close/enqueue need skip, // 3. but from testing controller.error still work even if readableStream is cancel if (readableStreamCancel) { return; } log(`ReadableStream was canceled, due to $`) readableStreamCancel = true; safeCloseWebSocket(webSocketServer); } }); return stream; } // https://xtls.github.io/development/protocols/vless.html // https://github.com/zizifn/excalidraw-backup/blob/main/v2ray-protocol.excalidraw /** * * @param vlessBuffer * @param userID * @returns */ function processVlessHeader( vlessBuffer, userID ) { if (vlessBuffer.byteLength < 24) { return { hasError: true, message: 'invalid data', }; } const version = new Uint8Array(vlessBuffer.slice(0, 1)); let isValidUser = false; let isUDP = false; if (stringify(new Uint8Array(vlessBuffer.slice(1, 17))) === userID) { isValidUser = true; } if (!isValidUser) { return { hasError: true, message: 'invalid user', }; } const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0]; //skip opt for now const command = new Uint8Array( vlessBuffer.slice(18 + optLength, 18 + optLength + 1) )[0]; // 0x01 TCP // 0x02 UDP // 0x03 MUX if (command === 1) { } else if (command === 2) { isUDP = true; } else { return { hasError: true, message: `command $ is not support, command 01-tcp,02-udp,03-mux`, }; } const portIndex = 18 + optLength + 1; const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2); // port is big-Endian in raw data etc 80 == 0x005d const portRemote = new DataView(portBuffer).getUint16(0); let addressIndex = portIndex + 2; const addressBuffer = new Uint8Array( vlessBuffer.slice(addressIndex, addressIndex + 1) ); // 1--> ipv4 addressLength =4 // 2--> domain name addressLength=addressBuffer[1] // 3--> ipv6 addressLength =16 const addressType = addressBuffer[0]; let addressLength = 0; let addressValueIndex = addressIndex + 1; let addressValue = ''; switch (addressType) { case 1: addressLength = 4; addressValue = new Uint8Array( vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) ).join('.'); break; case 2: addressLength = new Uint8Array( vlessBuffer.slice(addressValueIndex, addressValueIndex + 1) )[0]; addressValueIndex += 1; addressValue = new TextDecoder().decode( vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) ); break; case 3: addressLength = 16; const dataView = new DataView( vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) ); // 2001:0db8:85a3:0000:0000:8a2e:0370:7334 const ipv6 = []; for (let i = 0; i < 8; i++) { ipv6.push(dataView.getUint16(i * 2).toString(16)); } addressValue = ipv6.join(':'); // seems no need add [] for ipv6 break; default: return { hasError: true, message: `invild addressType is $`, }; } if (!addressValue) { return { hasError: true, message: `addressValue is empty, addressType is $`, }; } return { hasError: false, addressRemote: addressValue, addressType, portRemote, rawDataIndex: addressValueIndex + addressLength, vlessVersion: version, isUDP, }; } /** * * @param remoteSocket * @param webSocket * @param vlessResponseHeader * @param retry * @param log */ async function remoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, retry, log) { // remote--> ws let remoteChunkCount = 0; let chunks = []; /** @type */ let vlessHeader = vlessResponseHeader; let hasIncomingData = false; // check if remoteSocket has incoming data await remoteSocket.readable .pipeTo( new WritableStream({ start() { }, /** * * @param chunk * @param controller */ async write(chunk, controller) { hasIncomingData = true; // remoteChunkCount++; if (webSocket.readyState !== WS_READY_STATE_OPEN) { controller.error( 'webSocket.readyState is not open, maybe close' ); } if (vlessHeader) { webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer()); vlessHeader = null; } else { // seems no need rate limit this, CF seems fix this??.. // if (remoteChunkCount > 20000) { // // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M // await delay(1); // } webSocket.send(chunk); } }, close() { log(`remoteConnection!.readable is close with hasIncomingData is $`); // safeCloseWebSocket(webSocket); // no need server close websocket frist for some case will casue HTTP ERR_CONTENT_LENGTH_MISMATCH issue, client will send close event anyway. }, abort(reason) { console.error(`remoteConnection!.readable abort`, reason); }, }) ) .catch((error) => { console.error( `remoteSocketToWS has exception `, error.stack || error ); safeCloseWebSocket(webSocket); }); // seems is cf connect socket have error, // 1. Socket.closed will have error // 2. Socket.readable will be close without any data coming if (hasIncomingData === false && retry) { log(`retry`) retry(); } } /** * * @param base64Str * @returns */ function base64ToArrayBuffer(base64Str) { if (!base64Str) { return ; } try { // go use modified Base64 for URL rfc4648 which js atob not support base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/'); const decode = atob(base64Str); const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0)); return ; } catch (error) { return ; } } /** * This is not real UUID validation * @param uuid */ function isValidUUID(uuid) { const uuidRegex = /^[0-9a-f]-[0-9a-f]-[4][0-9a-f]-[89ab][0-9a-f]-[0-9a-f]$/i; return uuidRegex.test(uuid); } const WS_READY_STATE_OPEN = 1; const WS_READY_STATE_CLOSING = 2; /** * Normally, WebSocket will not has exceptions when close. * @param socket */ function safeCloseWebSocket(socket) { try { if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) { socket.close(); } } catch (error) { console.error('safeCloseWebSocket error', error); } } const byteToHex = []; for (let i = 0; i < 256; ++i) { byteToHex.push((i + 256).toString(16).slice(1)); } function unsafeStringify(arr, offset = 0) { return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); } function stringify(arr, offset = 0) { const uuid = unsafeStringify(arr, offset); if (!isValidUUID(uuid)) { throw TypeError("Stringified UUID is invalid"); } return uuid; } /** * * @param webSocket * @param vlessResponseHeader * @param log */ async function handleUDPOutBound(webSocket, vlessResponseHeader, log) { let isVlessHeaderSent = false; const transformStream = new TransformStream({ start(controller) { }, transform(chunk, controller) { // udp message 2 byte is the the length of udp data // TODO: this should have bug, beacsue maybe udp chunk can be in two websocket message for (let index = 0; index < chunk.byteLength;) { const lengthBuffer = chunk.slice(index, index + 2); const udpPakcetLength = new DataView(lengthBuffer).getUint16(0); const udpData = new Uint8Array( chunk.slice(index + 2, index + 2 + udpPakcetLength) ); index = index + 2 + udpPakcetLength; controller.enqueue(udpData); } }, flush(controller) { } }); // only handle dns udp for now transformStream.readable.pipeTo(new WritableStream({ async write(chunk) { const resp = await fetch('https://1.1.1.1/dns-query', { method: 'POST', headers: { 'content-type': 'application/dns-message', }, body: chunk, }) const dnsQueryResult = await resp.arrayBuffer(); const udpSize = dnsQueryResult.byteLength; // console.log([...new Uint8Array(dnsQueryResult)].map((x) => x.toString(16))); const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 0xff, udpSize & 0xff]); if (webSocket.readyState === WS_READY_STATE_OPEN) { log(`doh success and dns message length is $`); if (isVlessHeaderSent) { webSocket.send(await new Blob([udpSizeBuffer, dnsQueryResult]).arrayBuffer()); } else { webSocket.send(await new Blob([vlessResponseHeader, udpSizeBuffer, dnsQueryResult]).arrayBuffer()); isVlessHeaderSent = true; } } } })).catch((error) => { log('dns udp has error' + error) }); const writer = transformStream.writable.getWriter(); return { /** * * @param chunk */ write(chunk) { writer.write(chunk); } }; } /** * * @param userID * @param hostName * @returns */ function getVLESSConfig(userID, hostName) { const vlessMain = `vless://$@$:443?encryption=none&security=tls&sni=$&fp=randomized&type=ws&host=$&path=%2F%3Fed%3D2048#$` return ` ################################################################ v2ray --------------------------------------------------------------- $ --------------------------------------------------------------- ################################################################ clash-meta --------------------------------------------------------------- - type: vless name: $ server: $ port: 443 uuid: $ network: ws tls: true udp: false sni: $ client-fingerprint: chrome ws-opts: path: "/?ed=2048" headers: host: $ --------------------------------------------------------------- ################################################################ `; }vless 部署代码2代码引用地址:https://github.com/yonggekkk/CF-workers-pages-vless/blob/main/\_worker.js需要更改 UUID自带 CloudFlare 证书的 proxyIP 地址代码104行,支持自定义伪装网页,修改对应的网页地址即可。代码中地址为 CCTV 官网// <!--GAMFC-->version base on commit 43fad05dcdae3b723c53c226f8181fc5bd47223e, time is 2023-06-22 15:20:02 UTC<!--GAMFC-END-->. // @ts-ignore import from 'cloudflare:sockets'; // How to generate your own UUID: // [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()" let userID = 'd342d11e-d424-4583-b36e-524ab1f0afa4'; const proxyIPs = ['cdn-all.xn--b6gac.eu.org', 'cdn-all.xijingping.link', 'cdn.xn--b6gac.eu.org', 'cdn-b100.xn--b6gac.eu.org', 'edgetunnel.anycast.eu.org', 'cdn.anycast.eu.org']; let proxyIP = proxyIPs[Math.floor(Math.random() * proxyIPs.length)]; let dohURL = 'https://sky.rethinkdns.com/1:-Pf_____9_8A_AMAIgE8kMABVDDmKOHTAKg='; // https://cloudflare-dns.com/dns-query or https://dns.google/dns-query // v2board api environment variables let nodeId = ''; // 1 let apiToken = ''; //abcdefghijklmnopqrstuvwxyz123456 let apiHost = ''; // api.v2board.com if (!isValidUUID(userID)) { throw new Error('uuid is not valid'); } export default { /** * @param request * @param } env * @param ctx * @returns */ async fetch(request, env, ctx) { try { userID = env.UUID || userID; proxyIP = env.PROXYIP || proxyIP; dohURL = env.DNS_RESOLVER_URL || dohURL; nodeId = env.NODE_ID || nodeId; apiToken = env.API_TOKEN || apiToken; apiHost = env.API_HOST || apiHost; const upgradeHeader = request.headers.get('Upgrade'); if (!upgradeHeader || upgradeHeader !== 'websocket') { const url = new URL(request.url); switch (url.pathname) { case '/cf': return new Response(JSON.stringify(request.cf, null, 4), { status: 200, headers: { "Content-Type": "application/json;charset=utf-8", }, }); case '/connect': // for test connect to cf socket const [hostname, port] = ['cloudflare.com', '80']; console.log(`Connecting to $:$...`); try { const socket = await connect({ hostname: hostname, port: parseInt(port, 10), }); const writer = socket.writable.getWriter(); try { await writer.write(new TextEncoder().encode('GET / HTTP/1.1\r\nHost: ' + hostname + '\r\n\r\n')); } catch (writeError) { writer.releaseLock(); await socket.close(); return new Response(writeError.message, ); } writer.releaseLock(); const reader = socket.readable.getReader(); let value; try { const result = await reader.read(); value = result.value; } catch (readError) { await reader.releaseLock(); await socket.close(); return new Response(readError.message, ); } await reader.releaseLock(); await socket.close(); return new Response(new TextDecoder().decode(value), ); } catch (connectError) { return new Response(connectError.message, ); } case `/$`: { const vlessConfig = getVLESSConfig(userID, request.headers.get('Host')); return new Response(`$`, { status: 200, headers: { "Content-Type": "text/plain;charset=utf-8", } }); } default: // return new Response('Not found', ); // For any other path, reverse proxy to 'www.fmprc.gov.cn' and return the original response url.hostname = 'tv.cctv.com'; url.protocol = 'https:'; request = new Request(url, request); return await fetch(request); } } else { return await vlessOverWSHandler(request); } } catch (err) { /** @type */ let e = err; return new Response(e.toString()); } }, }; /** * * @param request */ async function vlessOverWSHandler(request) { /** @type */ // @ts-ignore const webSocketPair = new WebSocketPair(); const [client, webSocket] = Object.values(webSocketPair); webSocket.accept(); let address = ''; let portWithRandomLog = ''; const log = (/** @type */ info, /** @type */ event) => { console.log(`[$:$] $`, event || ''); }; const earlyDataHeader = request.headers.get('sec-websocket-protocol') || ''; const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); /** @type }*/ let remoteSocketWapper = { value: null, }; let udpStreamWrite = null; let isDns = false; // ws --> remote readableWebSocketStream.pipeTo(new WritableStream({ async write(chunk, controller) { if (isDns && udpStreamWrite) { return udpStreamWrite(chunk); } if (remoteSocketWapper.value) { const writer = remoteSocketWapper.value.writable.getWriter() await writer.write(chunk); writer.releaseLock(); return; } const { hasError, message, portRemote = 443, addressRemote = '', rawDataIndex, vlessVersion = new Uint8Array([0, 0]), isUDP, } = await processVlessHeader(chunk, userID); address = addressRemote; portWithRandomLog = `$--$ ${isUDP ? 'udp ' : 'tcp ' } `; if (hasError) { // controller.error(message); throw new Error(message); // cf seems has bug, controller.error will not end stream // webSocket.close(1000, message); return; } // if UDP but port not DNS port, close it if (isUDP) { if (portRemote === 53) { isDns = true; } else { // controller.error('UDP proxy only enable for DNS which is port 53'); throw new Error('UDP proxy only enable for DNS which is port 53'); // cf seems has bug, controller.error will not end stream return; } } // ["version", "附加信息长度 N"] const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]); const rawClientData = chunk.slice(rawDataIndex); // TODO: support udp here when cf runtime has udp support if (isDns) { const = await handleUDPOutBound(webSocket, vlessResponseHeader, log); udpStreamWrite = write; udpStreamWrite(rawClientData); return; } handleTCPOutBound(remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log); }, close() { log(`readableWebSocketStream is close`); }, abort(reason) { log(`readableWebSocketStream is abort`, JSON.stringify(reason)); }, })).catch((err) => { log('readableWebSocketStream pipeTo error', err); }); return new Response(null, { status: 101, // @ts-ignore webSocket: client, }); } let apiResponseCache = null; let cacheTimeout = null; /** * Fetches the API response from the server and caches it for future use. * @returns A Promise that resolves to the API response object or null if there was an error. */ async function fetchApiResponse() { const requestOptions = { method: 'GET', redirect: 'follow' }; try { const response = await fetch(`https://$/api/v1/server/UniProxy/user?node_id=$&node_type=v2ray&token=$`, requestOptions); if (!response.ok) { console.error('Error: Network response was not ok'); return null; } const apiResponse = await response.json(); apiResponseCache = apiResponse; // Refresh the cache every 5 minutes (300000 milliseconds) if (cacheTimeout) { clearTimeout(cacheTimeout); } cacheTimeout = setTimeout(() => fetchApiResponse(), 300000); return apiResponse; } catch (error) { console.error('Error:', error); return null; } } /** * Returns the cached API response if it exists, otherwise fetches the API response from the server and caches it for future use. * @returns A Promise that resolves to the cached API response object or the fetched API response object, or null if there was an error. */ async function getApiResponse() { if (!apiResponseCache) { return await fetchApiResponse(); } return apiResponseCache; } /** * Checks if a given UUID is present in the API response. * @param targetUuid The UUID to search for. * @returns A Promise that resolves to true if the UUID is present in the API response, false otherwise. */ async function checkUuidInApiResponse(targetUuid) { // Check if any of the environment variables are empty if (!nodeId || !apiToken || !apiHost) { return false; } try { const apiResponse = await getApiResponse(); if (!apiResponse) { return false; } const isUuidInResponse = apiResponse.users.some(user => user.uuid === targetUuid); return isUuidInResponse; } catch (error) { console.error('Error:', error); return false; } } // Usage example: // const targetUuid = "65590e04-a94c-4c59-a1f2-571bce925aad"; // checkUuidInApiResponse(targetUuid).then(result => console.log(result)); /** * Handles outbound TCP connections. * * @param remoteSocket * @param addressRemote The remote address to connect to. * @param portRemote The remote port to connect to. * @param rawClientData The raw client data to write. * @param webSocket The WebSocket to pass the remote socket to. * @param vlessResponseHeader The VLESS response header. * @param log The logging function. * @returns The remote socket. */ async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) { async function connectAndWrite(address, port) { /** @type */ const tcpSocket = connect({ hostname: address, port: port, }); remoteSocket.value = tcpSocket; log(`connected to $:$`); const writer = tcpSocket.writable.getWriter(); await writer.write(rawClientData); // first write, nomal is tls client hello writer.releaseLock(); return tcpSocket; } // if the cf connect tcp socket have no incoming data, we retry to redirect ip async function retry() { const tcpSocket = await connectAndWrite(proxyIP || addressRemote, portRemote) // no matter retry success or not, close websocket tcpSocket.closed.catch(error => { console.log('retry tcpSocket closed error', error); }).finally(() => { safeCloseWebSocket(webSocket); }) remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, null, log); } const tcpSocket = await connectAndWrite(addressRemote, portRemote); // when remoteSocket is ready, pass to websocket // remote--> ws remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, retry, log); } /** * * @param webSocketServer * @param earlyDataHeader for ws 0rtt * @param log for ws 0rtt */ function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { let readableStreamCancel = false; const stream = new ReadableStream({ start(controller) { webSocketServer.addEventListener('message', (event) => { if (readableStreamCancel) { return; } const message = event.data; controller.enqueue(message); }); // The event means that the client closed the client -> server stream. // However, the server -> client stream is still open until you call close() on the server side. // The WebSocket protocol says that a separate close message must be sent in each direction to fully close the socket. webSocketServer.addEventListener('close', () => { // client send close, need close server // if stream is cancel, skip controller.close safeCloseWebSocket(webSocketServer); if (readableStreamCancel) { return; } controller.close(); } ); webSocketServer.addEventListener('error', (err) => { log('webSocketServer has error'); controller.error(err); } ); // for ws 0rtt const = base64ToArrayBuffer(earlyDataHeader); if (error) { controller.error(error); } else if (earlyData) { controller.enqueue(earlyData); } }, pull(controller) { // if ws can stop read if stream is full, we can implement backpressure // https://streams.spec.whatwg.org/#example-rs-push-backpressure }, cancel(reason) { // 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here // 2. if readableStream is cancel, all controller.close/enqueue need skip, // 3. but from testing controller.error still work even if readableStream is cancel if (readableStreamCancel) { return; } log(`ReadableStream was canceled, due to $`) readableStreamCancel = true; safeCloseWebSocket(webSocketServer); } }); return stream; } // https://xtls.github.io/development/protocols/vless.html // https://github.com/zizifn/excalidraw-backup/blob/main/v2ray-protocol.excalidraw /** * * @param vlessBuffer * @param userID * @returns */ async function processVlessHeader( vlessBuffer, userID ) { if (vlessBuffer.byteLength < 24) { return { hasError: true, message: 'invalid data', }; } const version = new Uint8Array(vlessBuffer.slice(0, 1)); let isValidUser = false; let isUDP = false; const slicedBuffer = new Uint8Array(vlessBuffer.slice(1, 17)); const slicedBufferString = stringify(slicedBuffer); const uuids = userID.includes(',') ? userID.split(",") : [userID]; const checkUuidInApi = await checkUuidInApiResponse(slicedBufferString); isValidUser = uuids.some(userUuid => checkUuidInApi || slicedBufferString === userUuid.trim()); console.log(`checkUuidInApi: $, userID: $`); if (!isValidUser) { return { hasError: true, message: 'invalid user', }; } const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0]; //skip opt for now const command = new Uint8Array( vlessBuffer.slice(18 + optLength, 18 + optLength + 1) )[0]; // 0x01 TCP // 0x02 UDP // 0x03 MUX if (command === 1) { } else if (command === 2) { isUDP = true; } else { return { hasError: true, message: `command $ is not support, command 01-tcp,02-udp,03-mux`, }; } const portIndex = 18 + optLength + 1; const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2); // port is big-Endian in raw data etc 80 == 0x005d const portRemote = new DataView(portBuffer).getUint16(0); let addressIndex = portIndex + 2; const addressBuffer = new Uint8Array( vlessBuffer.slice(addressIndex, addressIndex + 1) ); // 1--> ipv4 addressLength =4 // 2--> domain name addressLength=addressBuffer[1] // 3--> ipv6 addressLength =16 const addressType = addressBuffer[0]; let addressLength = 0; let addressValueIndex = addressIndex + 1; let addressValue = ''; switch (addressType) { case 1: addressLength = 4; addressValue = new Uint8Array( vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) ).join('.'); break; case 2: addressLength = new Uint8Array( vlessBuffer.slice(addressValueIndex, addressValueIndex + 1) )[0]; addressValueIndex += 1; addressValue = new TextDecoder().decode( vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) ); break; case 3: addressLength = 16; const dataView = new DataView( vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) ); // 2001:0db8:85a3:0000:0000:8a2e:0370:7334 const ipv6 = []; for (let i = 0; i < 8; i++) { ipv6.push(dataView.getUint16(i * 2).toString(16)); } addressValue = ipv6.join(':'); // seems no need add [] for ipv6 break; default: return { hasError: true, message: `invild addressType is $`, }; } if (!addressValue) { return { hasError: true, message: `addressValue is empty, addressType is $`, }; } return { hasError: false, addressRemote: addressValue, addressType, portRemote, rawDataIndex: addressValueIndex + addressLength, vlessVersion: version, isUDP, }; } /** * * @param remoteSocket * @param webSocket * @param vlessResponseHeader * @param retry * @param log */ async function remoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, retry, log) { // remote--> ws let remoteChunkCount = 0; let chunks = []; /** @type */ let vlessHeader = vlessResponseHeader; let hasIncomingData = false; // check if remoteSocket has incoming data await remoteSocket.readable .pipeTo( new WritableStream({ start() { }, /** * * @param chunk * @param controller */ async write(chunk, controller) { hasIncomingData = true; // remoteChunkCount++; if (webSocket.readyState !== WS_READY_STATE_OPEN) { controller.error( 'webSocket.readyState is not open, maybe close' ); } if (vlessHeader) { webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer()); vlessHeader = null; } else { // seems no need rate limit this, CF seems fix this??.. // if (remoteChunkCount > 20000) { // // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M // await delay(1); // } webSocket.send(chunk); } }, close() { log(`remoteConnection!.readable is close with hasIncomingData is $`); // safeCloseWebSocket(webSocket); // no need server close websocket frist for some case will casue HTTP ERR_CONTENT_LENGTH_MISMATCH issue, client will send close event anyway. }, abort(reason) { console.error(`remoteConnection!.readable abort`, reason); }, }) ) .catch((error) => { console.error( `remoteSocketToWS has exception `, error.stack || error ); safeCloseWebSocket(webSocket); }); // seems is cf connect socket have error, // 1. Socket.closed will have error // 2. Socket.readable will be close without any data coming if (hasIncomingData === false && retry) { log(`retry`) retry(); } } /** * * @param base64Str * @returns */ function base64ToArrayBuffer(base64Str) { if (!base64Str) { return ; } try { // go use modified Base64 for URL rfc4648 which js atob not support base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/'); const decode = atob(base64Str); const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0)); return ; } catch (error) { return ; } } /** * This is not real UUID validation * @param uuid */ function isValidUUID(uuid) { const uuidRegex = /^[0-9a-f]-[0-9a-f]-[4][0-9a-f]-[89ab][0-9a-f]-[0-9a-f]$/i; return uuidRegex.test(uuid); } const WS_READY_STATE_OPEN = 1; const WS_READY_STATE_CLOSING = 2; /** * Normally, WebSocket will not has exceptions when close. * @param socket */ function safeCloseWebSocket(socket) { try { if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) { socket.close(); } } catch (error) { console.error('safeCloseWebSocket error', error); } } const byteToHex = []; for (let i = 0; i < 256; ++i) { byteToHex.push((i + 256).toString(16).slice(1)); } function unsafeStringify(arr, offset = 0) { return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); } function stringify(arr, offset = 0) { const uuid = unsafeStringify(arr, offset); if (!isValidUUID(uuid)) { throw TypeError("Stringified UUID is invalid"); } return uuid; } /** * * @param webSocket * @param vlessResponseHeader * @param log */ async function handleUDPOutBound(webSocket, vlessResponseHeader, log) { let isVlessHeaderSent = false; const transformStream = new TransformStream({ start(controller) { }, transform(chunk, controller) { // udp message 2 byte is the the length of udp data // TODO: this should have bug, beacsue maybe udp chunk can be in two websocket message for (let index = 0; index < chunk.byteLength;) { const lengthBuffer = chunk.slice(index, index + 2); const udpPakcetLength = new DataView(lengthBuffer).getUint16(0); const udpData = new Uint8Array( chunk.slice(index + 2, index + 2 + udpPakcetLength) ); index = index + 2 + udpPakcetLength; controller.enqueue(udpData); } }, flush(controller) { } }); // only handle dns udp for now transformStream.readable.pipeTo(new WritableStream({ async write(chunk) { const resp = await fetch(dohURL, // dns server url { method: 'POST', headers: { 'content-type': 'application/dns-message', }, body: chunk, }) const dnsQueryResult = await resp.arrayBuffer(); const udpSize = dnsQueryResult.byteLength; // console.log([...new Uint8Array(dnsQueryResult)].map((x) => x.toString(16))); const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 0xff, udpSize & 0xff]); if (webSocket.readyState === WS_READY_STATE_OPEN) { log(`doh success and dns message length is $`); if (isVlessHeaderSent) { webSocket.send(await new Blob([udpSizeBuffer, dnsQueryResult]).arrayBuffer()); } else { webSocket.send(await new Blob([vlessResponseHeader, udpSizeBuffer, dnsQueryResult]).arrayBuffer()); isVlessHeaderSent = true; } } } })).catch((error) => { log('dns udp has error' + error) }); const writer = transformStream.writable.getWriter(); return { /** * * @param chunk */ write(chunk) { writer.write(chunk); } }; } /** * * @param userID * @param hostName * @returns */ function getVLESSConfig(userID, hostName) { const vlessws = `vless://$@time.cloudflare.com:8880?encryption=none&type=ws&host=$&path=%2F%3Fed%3D2048#$` const vlesswstls = `vless://$@time.cloudflare.com:8443?encryption=none&security=tls&type=ws&host=兄弟,你的自定义域名呢?&path=%2F%3Fed%3D2048#$` return ` ==========================配置详解============================== ################################################################ 一、CF-workers-vless+ws节点,分享链接如下: $ --------------------------------------------------------------- 注意:当前节点无需域名,TLS选项关闭 --------------------------------------------------------------- 客户端必要文明参数如下: 客户端地址(address):自选域名 或者 自选IP 端口(port):7个http端口可任意选择(80、8080、8880、2052、2082、2086、2095) 用户ID(uuid):$ 传输协议(network):ws/websocket 伪装域名(host):$ 路径(path):/?ed=2048 ################################################################ ################################################################ 二、CF-workers-vless+ws+tls节点,分享链接如下: $ --------------------------------------------------------------- 注意:客户端ws选项后的伪装域名host必须改为你自定义的域名 --------------------------------------------------------------- 客户端必要文明参数如下: 客户端地址(address):自选域名 或者 自选IP 端口(port):6个https端口可任意选择(443、8443、2053、2083、2087、2096) 用户ID(uuid):$ 传输协议(network):ws/websocket 伪装域名(host):兄弟,你的自定义域名呢? 路径(path):/?ed=2048 传输安全(TLS):开启 跳过证书验证(allowlnsecure):false ################################################################ ################################################################ clash-meta --------------------------------------------------------------- - type: vless name: $ server: $ port: 443 uuid: $ network: ws tls: true udp: false sni: $ client-fingerprint: chrome ws-opts: path: "/?ed=2048" headers: host: $ --------------------------------------------------------------- ################################################################ `; }V2ray 使用 CloudFlare Workers vless 节点(无域名)复制刚刚创建的代码地址,格式为:https://vless.xxx.workers.dev/,其中 vless 为创建的脚本名称,xxx 为 CloudFlare 用户名。在浏览器地址栏中粘贴代码地址后,如果出现一段字符,即表明创建成功。接下来在该地址后输入 /UUID字符串,回车即可看到 vless 地址。 将页面中的 V2ray 地址复制,打开 V2ray 软件,点击 服务器 选项,选择 从剪贴板导入批量URL,即可看到创建的节点。双击刚刚创建的节点,将端口(port)改为 2052,传输层安全(TLS) 设置为空,传输协议(network) 为 ws,地址(address) 改为优选 IP。 注:7个 http 端口可任意选择:80、8080、8880、2052、2082、2086、2095 Workers 专用 IP 优选工具CloudFlare 优质 IP 自动切换:https://stock.hostmonit.com/CloudFlareYes172.64.32.1/24 (推荐移动,走香港) 104.28.14.0/24 (推荐移动,走新加坡) 104.23.240.0 ~ 104.23.243.254 (推荐联通、移动,线路未知) 108.162.236.1/24 (推荐联通,走美国) 104.20.157.0/24 (推荐联通,走日本) 104.16.160.1/24 (推荐电信,走洛杉矶) 172.64.0.0/24 (推荐电信,走旧金山) 172.64.32.* (走欧洲)优选工具 Windows 版本下载地址,断开所有 VPN 和科学上网方式,选择运行任意批处理文件,在运行后的结果中,选择 PING 值低的 IP 地址,复制到 V2ray 的 地址(address) 中。 CloudFlare 自选域名自选域名下载地址,断开所有 VPN 和科学上网方式,选择自选域名程序单文件,运行后在相同文件夹下中打开 txt 文件,选择 PING 值低的域名,复制到 V2ray 的 地址(address) 中。V2ray 使用 CloudFlare Workers vless 节点(有域名)在 CloudFlare Workers 中,打开刚才创建的脚本,点击 查看,点击 添加自定义域。 在自定义域对话框中输入已经在 CloudFlare 注册域名的二级域名,点击 添加自定义域。比如:vless.5iehome.cf。接下来只需在浏览器地址栏中粘贴输入 https://vless.5iehome.cf/UUID字符串,回车即可看到 vless 地址。将页面中的 V2ray 地址复制,打开 V2ray 软件,点击 服务器 选项,选择 从剪贴板导入批量URL,即可看到创建的节点。双击刚刚创建的节点,将端口(port)改为 443,传输层安全(TLS) 设置为 tls,传输协议(network) 为 ws,地址(address) 改为优选 IP。 注:6个 http 端口可任意选择:443、8443、2053、2083、2087、2096 节点测速V2ray 下测试节点速度,要选择 测试服务器真连接延迟(多选)(Ctrl+R),延迟数值为大于 0 即为导通,数值为 -1 即为不导通。IP 地址查看https://whatismyipaddress.com/https://ip.gs文中程序下载CloudFlare Workers VLESS 永久免费节点搭建https://www.aliyundrive.com/s/mXFeMtdBF2T 提取码: c1m7https://www.123pan.com/s/Oy5RVv-swXB.html 提取码: V3iX参考文章https://jdssl.top/index.php/2023/07/21/2023vpn/https://www.youtube.com/watch?v=5fvhws6ZXrMhttps://ygkkk.blogspot.com/2023/07/cfworkers-vless.htmlhttps://www.youtube.com/watch?v=9V9CQxmfwoA【END】
2023年08月06日
27 阅读
3 评论
0 点赞
2023-08-06
CloudFlare WARP 免费 VPN 搭建教程
前言最近比较火的科学上网的方法,是使用 CloudFlare WARP 免费 VPN,并且可以优选 IP,提高上网速度。CloudFlare WARP 免费 VPNWARP 介绍1.1.1.1 的 WARP 应用以一个经过优化的现代协议来代替您的设备和 Internet 之间的连接。具有快速、免费、私密的特点,最关键的通过优选 IP 后,可以大幅度提高上网速度。下载安装WARP 支持各个平台,Windows 的下载地址为:Microsoft应用中心1.1.1.1下载后双击安装即可。直接使用 WARP在状态栏打开软件,点击连接按钮即可使用。默认情况下,软件会自动分配一个普通账号,只有 1GB 的使用流量。获取 WARP+ 永久免费流量打开 Telegram,搜索 @generatewarpplusbot按照提示,关联相关订阅后,获取 WARP 账户许可证密钥 (26个字符),格式为:xxxxxxxx-xxxxxxxx-xxxxxxxx使用 WARP+ 免费流量方法一:更改 WARP 帐户直接在 WARP 中选择 帐户,点击 使用其他密钥 按钮,在弹出的对话框中输入在 Telegram 上得到的许可证密钥。方法二:使用 WireGuard下载 WireGuard,下载对应客户端。经测试,下载及安装都需要代理才能完成。在线生成 WARP 的 WireGuard 配置文件,打开生成配置文件网址,点击 Run,在运行代码框中输入 2,即 WARP+。然后输入在 Telegram 上得到的26位许可证密钥。设备名设置可有可无。 将生成的配置文件输入到 WireGuard 中,就可以连接使用了。 WireGuard 可以脱离 CloudFlare WARP 独立使用。CloudFlare 优化 IPWARP+ IP 优选工具下载:https://gitlab.com/Misaka-blog/warp-script/-/blob/main/files/warp-yxip/warp-yxip-win.7z断开所有 VPN 和科学上网方式,双击 warp-yxip.bat,选择默认 1 IPv4 优选地址即可。打开 result.csv,选择延迟小,丢包率为 0 的 IP 地址即可。 复制延迟小,丢包率为 0 的 IP 地址及端口号,将 WireGuard 配置文件 Endpoint 地址替换,保存后再连接,就会发现速度有了明显改善。 文中软件下载地址「CloudFlare WARP.exe」https://www.123pan.com/s/Oy5RVv-wwXB.html 提取码: 7iAahttps://www.aliyundrive.com/s/Rn7dPV86Nqn 提取码: 6ep3【END】
2023年08月06日
55 阅读
0 评论
0 点赞
2023-08-02
如何高效学习技术?| PDCA + 5W2H 构建技术学习方法论
本文作者:GaHing 转载地址:https://sspai.com/post/81638上班累死累活,下班不想再学习;好不容易周末休息,又不知道去哪学,学到什么程度,于是看几篇文章草草了事,假装心理安慰;定下决心好好学习,然而没做计划,半途而废;以为自己学了很多东西,但是别人一问,啥也不懂...如果你有以上困扰,又想提升自己,那么看本文准没错,将教你如何高效的学习一门技术。前言网上流行很多优秀的学习方法,比如:费曼学习法:核心是以教促学,化零为整。适用于技术学习,本方法论有借鉴。西蒙学习法:核心是目标拆解,逐一击破。适用于技术学习,本方法论有借鉴。番茄学习法:注重时间管理,比较适合学生,不太适合充满碎片化时间的打工人。艾宾浩斯遗忘学习法:重复温习减少遗忘,适合应试教育场景。斯科特·扬学习法:核心是关联旧有知识,以应用为导向,抽象概念加深记忆。适用于技术学习,本方法论有借鉴。PQ4R 学习法:适合思想是预习、提问、阅读、反思、背诵和复习。适合应试教育场景的课堂笔记,但是思路可以借鉴。康奈尔笔记法:核心是将笔记分为关键词、要点列表、总结三部分,根据关键词回想要点,加深印象。比较适合应试教育笔记记录,针对技术学习使用思维导图更合适。每种方法都有各自的适用场景,针对技术领域,部分学习方法是不适用的。对于费曼学习法、西蒙学习法、斯科特·扬学习法,方法适用,但是不够全面,缺少细节指导。而本文总结的学习方法将吸收这些优秀方法的精髓,并结合技术领域进行完善,提供结构化的全面视角的学习方法论。每个人或许都有自己的学习方法,如果你还在为寻找学习方法而苦恼,不妨尝试下本文的学习方法。方法论PDCA(Plan-Do-Check-Act)是一个确保目标高品质完成并持续优化的思维模型,我们可以将其套用在技术学习领域,并拆解为学习前、学习中、学习后三个部分。学习前:对应 Plan 环节。制定学习计划,使用 5W2H 分析法拆解学习前的准备。学习中:对应 Do 环节。通过学习五步法(检视、分析、笔记、练习、讨论)高效学习。学习后:对应 Check 和 Act 环节。以教促学,检测学习成果;学以致用,避免学了就忘;对齐成果和目标,及时改正。脑图原始内容 ➡️ 学习方法论(PDCA + 5W2H)下面详细的解释每一条。学习前学什么(What)确定学习的内容确定学习的内容,主要是确定学习范围,并找到知识定位。确定学习范围,需要事先做下全局了解,搭建初步知识框架,避免学歪。找到知识定位,可以和旧有知识关联,提高学习效率。为什么要学(Why)认清学习目的,避免半途而废。常见的学习目的有:工作需要存在痛点,自我提升没有目标,追逐潮流尽量不要抱着第三种学习目的,容易学而无用,最后遗忘。一句话:以应用为导向。举个我之前的例子,看到 Rust 技术火热,于是闷头学习,怕跟不上潮流。但实际上,我的工作场景不会用到且将来很难用到,而我还有优先级更高的要学。后来及时止损,不再深入学习。去哪学(Where)技术知识的来源分两种:权威资料和二手资料。权威资料:官方文档、源码仓库、原理向书籍二手资料:博客、视频、课程、应用向书籍存在即合理,权威资料一般来说更体系化、严谨化,不容易错漏,但缺点是相对不够有趣。两种来源需要结合来看,不同领域、不同阶段、不同人的思维方法,选择都不一样。以下是我个人建议,仅供参考:对于毫无基础、完全不熟悉的领域,可以先看二手资料有一些底子的,建议优先看权威资料;针对单个知识点,再看二手资料知识补充如果一门技术没有官方文档,要么是新技术,要么是不怎么维护的技术,不推荐学习想进阶,需要和专家交流碰撞的,会买课或参加培训对于碎片化知识,提高甄别效率,参见:如何高效获取前端优质信息(2023)谁要学(Who)通常来说是自我学习,但也可以是给他人制定学习计划。给他人制定学习计划,需要关注反馈,建立奖惩制度。何时学(When)如果不投入足够的时间,再好的学习理论也是纸上谈兵。那么,时间从哪来?应该如何安排?时间总的就那么多,如果想成长,就得克服人性弱点,少打一把游戏,少刷一会短视频,每天能够抽出几十分钟。不躺平,同时也不完全放弃娱乐。至于如何安排时间,网上有很多建立学习计划的方案,一个比较好的方法是:用 OKR 制定学习目标。比如学会 React 是目标(Object),那么阅读完官方文档、开发 Next.js 小项目等行动则是关键成果(Key Result)用 SMART 原则拆解目标。除了明确目标,还需要确保目标可衡量、可实现、有关联、有时限,上面 React 例子的关键成果还能继续拆。如何学,学到什么程度(How, How much)根据不同的学习目的,应该采取不同的学习方法,以提高学习效率。如果想要深度学习某项技术及其关联技术,比如深度学习 React 以及生态,可以采取链式学习法,挖掘技术点,整理分层逐步深入。如果想要横向学习同领域的不同技术,比如熟悉 Vue 的情况下学习 React,可以采用比较学习法,整理关键技术点,按点学习对比差异。如果想要广度学习多领域的技术,比如了解实际业务中各技术领域的技术栈,可以采用环式学习法,以应用为导向,构建闭环,由近到远逐个击破。最重要的还是,抱着目的学习,而不是盲目学习。链式学习链式学习适合新领域的学习,或者是对已知领域技术加深理解。方法步骤如下:挖掘技术点整理分层迭代优化1️⃣ 挖掘技术点:在学习之初,挖掘这项技术有哪些要点。比如学习 React ,我们可以挖掘出 jsx、diff、虚拟 DOM、这些技术点。此时这些技术点还较为零散、缺少关联,为了让知识体系化,我们会进入下一步:整理分层。2️⃣ 整理分层:根据挖掘出来的技术点,整理分层。有两种整理分层的思路,领域分层和细节分层。领域分层:自顶向下,层层关联。分层原则为上层依赖下层,通常是应用/实现在上、原理/底层在下经典结构有:OSI 7 层网络模型、MVVM 架构细节分层:由表入里,层层深入。分层原则固定四层,分别是用法、原理、方案、源码。方案是指原理的设计方案,涉及数据结构、设计模式以及编程范式。以 React 为例,画出领域分层图和细节分层图领域分层图应用技术:React组织形式:组件、事件、状态核心原理:Fiber 架构、事件委托、单向数据流浏览器架构:渲染引擎、执行引擎操作系统:CPU、IO细节分层图用法层:react hooks、jsx、状态管理、...原理层:单向数据流、虚拟 DOM、Fiber 架构(虚拟栈帧、双缓冲机制、两阶段渲染)、...方案层:Fiber 架构、调度器、协调器、渲染器、...Fiber 架构既是原理又是方案源码层:Fiber、Scheduler、Reconciler、...以上的划分方式不一定准确,随着学习的深入,我们可能会产生新的理解,所以需要持续迭代分层图。特别强调,分层是目的而非手段,得到的结果并不重要,重要的是画分层图过程中的整理和思考,这才是对成长帮助最大的事情。那么使用该学习方法学习技术,应该学到什么程度?我的建议是:对于领域分层,上面几层关注到细节,下面几层仅了解原理即可对于细节分层,上面三层都学,源码层掌握关键源码即可比较学习了解同领域的一门技术后,再学习其他技术就无需重头开始,这就是比较学习法的好处之一(学得快)。方法步骤如下:整理关键技术点按点学习对比差异整理重点差异1️⃣ 整理关键技术点:采用思维导图,整理已会技术的关键技术点。2️⃣ 按点学习对比差异:在之前的思维导图上,对比两门技术在实现和效果(用法)上的差异。3️⃣ 整理重点差异:针对差异较大的技术点,使用表格整理原理和应用场景的差异。假设已学会 React,想要快速学习 Vue 2.x,那么可以进行如下步骤:整理 React 关键技术点:组件语法、组件形式、组件通信、生命周期、状态管理、事件绑定、更新机制、...对比得到 React 和 Vue 的差异简单举例,不一定全和准确。思维导图原始地址在这。整理 React 和 Vue 的重点差异。这边就不再提供了,感兴趣的可以自行整理。学会 Vue 2.x 之后,还可以继续采用该方法快速学习 Vue 3。那么使用该学习方法学习技术,应该学到什么程度?对比学习法的好处在于限定范围,当我们按关键技术点学习,并对比了解差异完成,意味着学得差不多了。如果还想深入,可以思考下学习的目的,确定要深入的话则继续使用链式学习法学习。环式学习环式学习,以应用为导向,画出业务闭环,再学习这个闭环中的知识。方法步骤如下:画出闭环,确定边界由近到远,逐个击破单点链式,主学上层1️⃣ 画出闭环,确定边界:闭环的作用是确定范围,而不是盲目学习,避免学而无用。画闭环有两种常见手段:技术环:研发视角,从技术架构切入。思考整体技术架构:哪些领域?哪些能力?哪些技术栈?业务环:用户视角,从业务流程切入。思考业务流程链路:用户特征?用户价值?获利方式?以「视频发文」业务为例,我们可以简单画出这样的技术环和业务环。大概示意,可能不全或不对2️⃣ 由近到远,逐个击破:确定完闭环后,开始对环上的每个技术点进行学习。但学习也有优先级,距离自己本业领域越近,则越优先学习。3️⃣ 单点链式,主学上层:对于其他领域的技术点,依然采用链式学习法。学习程度上,越近的学得越深,越远的学得越浅,只需学习个上面一两层即可。此外,相比基建研发,业务研发更需要全局视野。由于投入产出比的问题,更建议业务研发采用该学习方法,而基建研发采用另两种学习方法。小结学习方法适用场景方法步骤学习程度方法优势链式学习法新领域学习 已知领域加深理解挖掘技术点 整理分层 领域分层 细节分层 迭代优化领域分层:上面几层关注到细节,下面几层仅了解原理 细节分层:上面三层都学,源码层掌握关键源码即可系统化比较学习法同领域相似技术学习掌握同领域的技术 整理关键技术点 按点学习对比差异 整理重点差异按点学习,顺其自然学得快 学得全 学得深环式学习法多领域了解 更适合业务研发画出闭环,确定边界 技术环 业务环 由近到远,逐个击破 单点链式,主学上层只需关注链式分层的上面几个分层培养全局视野 避免盲目学习学习时检视在学习之初,先速览一遍大纲(比如官网文档的目录结构),脑海中对知识点有个初步框架。然后,快速学习每个章节的内容,不要求完全了解,知道知识概念即可。很多文档为了文档的完备性,通常会写得啰里吧嗦,因为需要掌握变速学习技巧,不同章节使用不同的阅读速度、学习速度。完成以上几步,算是检视完成。分析检视的目的在于知道知识是什么。而分析的目的在于需要了解知识为什么。为什么这么设计?换种设计方式会不会更好?这就要求读者养成批判性思维,常见的分析方法有:5ways 追问法。笔记不管是检视还是分析阶段,我们都需要记笔记。对于技术学习,推荐使用思维导图做笔记,即发散又有一定结构化。比如可以将主题进行概括作为父节点,每个要点都是子节点,再加上疑问和总结。以下为示例:另外,我们还需要构建学习框架图,包括领域分层图和细节分层图,具体在学习前的如何学一节已经讲过。框架图不是一蹴而就,在检视阶段我们完成初版,但随着学习的深入我们需要不断更新框架图。练习学生时代学习基础课程时,每一章节往往会提供习题用于巩固知识。实际上技术学习也应该这样,找到习题进行锻炼。习题可以是官方的,也可以是第三方的,比如面试题这种。举个例子:Typescript 体操习题:https://github.com/type-challenges/type-challengesReact 在线练习:https://react-tutorial.app/若没有或找不到,可以考虑下自问自答。讨论找谁交流?哪里可以交流?存在疑问怎么解决?和同事讨论反馈最快,但是不一定有相关经验,需要日常工作中关注各同事的技能点。也可以在公司技术大群抛出问题,集思广益,一般公司越大,技术盲区就越小。如果以上途径都不存在,那么可以选择付费咨询,或者社区问答中提问,知乎曾经是个好东西。学习后以教促学解决学不深的问题写文章或做分享,应用「费曼学习法」,通过教学的方式不断反思,加深形式自我反思,深度学习。学以致用解决学了就忘的问题学了不用容易忘。如果工作能够直接用上自然最好,但如果没有使用场景,就需要自己模拟场景并创建项目练手。及时复盘解决学歪的问题对比下学习成果和最初的学习目的(Why),如果出现偏差需要及时改正。总结对本文的学习方法论做下总结:学习前,进行 5W2H 分析,根据学习目标采用相应的学习方法(深度、宽度、广度)学习时,通过学习五步法(检视、分析、笔记、练习、讨论)高效学习学习后,关注学以致用、以教促学,加强学习效果另外,我在整理个人学习方法论的时候,以下这些资料对我帮助较大,在此列出,感兴趣可以阅读:知识学习方法论 - 吴大山的博客 | Wudashan Blog大厂晋升指南当然,以上都是传统技法,随着 ChatGPT 的出现,未来的学习方式可能会发生巨大变化,但未来还不可知,让我们拭目以待。最后,无论何种学习方法,懒惰是第一阻力,只有克服这一人性弱点,持之以恒,方能得到新的成长。【END】
2023年08月02日
3 阅读
0 评论
0 点赞
2023-08-02
乐高摄影 - 打工人的日常
本文作者:布莱克Feather 转载地址:https://sspai.com/post/81642作为一个打工人,在打了十多年工后,终于发现打工的终点是循环,故拍了一套乐高版致敬所有打工人,希望大家在枯燥无味的日子里也能找到新鲜感和快乐!工作日的早高峰总是让人头疼的!无论是公交还是地铁都让人恨不得自己有扇通往公司的任意门。谁说打工人的命不是咖啡给的呢。咖啡来到前的我们是焦躁疲惫的,但一旦手拿咖啡一切好说。听说你们也是一到下班就开会,然后深陷加班泥潭。每次加班的时候感觉自己是超人正在拯救这个只有我能拯救的世界。被老板叫进办公室,等着我的十有八九是挨骂,当然我觉得我没错,但是打工人的坚强不就是从挨最狠的骂,背最黑的锅开始的吗?周五的快乐是周末也不一定能比的,白天终于能摸会儿鱼了!胜利的曙光即将到来!周五的夜晚总是很长,因为有太多的快乐,甩掉西装白衬衫,谁不是ktv里最亮的星。到了周末,终于可以安静宅家享受只属于自己的快乐时光。周末三五好友的聚会也是必不可少的,吐槽工作也是重要的环节。当然我们也终于有时间去享受阳光,享受大自然带来的安逸和宁静。当然一周的终点还是周日晚上的失眠,真希望每周只有五六日!最后祝愿每一个打工人每天都能睡个好觉,不用遇到糟心的同事,脑残的领导,离谱的客户。【END】
2023年08月02日
2 阅读
0 评论
0 点赞
2023-08-01
前端页面部署方式的全面解析与比较
本文作者:Expo 转载地址:https://sspai.com/post/81564前端页面简介部署前端页面有多种方法,你可以选择最适合你项目需求和你自己技术背景的方式。以下是一些常见的部署方式:FTP上传: 这是最传统的方式,将你的前端文件(HTML,CSS,JavaScript等)上传到你的web服务器。大多数的web主机提供FTP访问,这也是小型项目的快速部署方式。使用Git: 如果你的服务器支持Git,你可以设置一个Git仓库,并使用**git push**命令部署你的前端页面。这种方式方便版本控制,也支持多人协作。使用SSH: 可以通过SSH将你的文件推送到你的服务器,这个过程可以通过脚本自动化。使用云服务: 像AWS S3,Google Cloud Storage,Azure Storage等服务可以托管静态网站,通常它们提供了很好的性能和安全性。使用专门的前端部署服务: 服务如Netlify,Vercel等,这些平台通常提供一站式服务,包括版本控制,持续集成,HTTPS和CDN等。使用CDN服务: 这些服务可以将你的站点部署到全球的服务器上,加速访问速度。例如,Cloudflare,Akamai等。容器化部署: 使用如Docker等工具,将前端应用容器化,然后在Kubernetes等容器编排平台进行部署。以上的部署方式有的适合小型项目,有的适合大型项目,有的需要更多的技术知识,选择哪一种方式取决于你的具体需求。Nginx配置 Nginx 来为前端页面提供服务通常需要以下步骤:步骤1:上传代码首先,你需要将你的前端代码上传到服务器上的某个目录,比如 **/var/www/mywebsite**。步骤2:创建 Nginx 配置文件Nginx 的配置文件通常位于 **/etc/nginx/** 目录下,具体路径可能会因服务器的不同而略有不同。在该目录下,你通常会找到一个叫做 **sites-available** 的目录。在这个目录下,你可以为每个网站创建一个配置文件。例如,你可以创建一个新的配置文件,命名为 **mywebsite**:sudo nano /etc/nginx/sites-available/mywebsite步骤3:编辑 Nginx 配置文件在新建的配置文件中,你可以输入以下内容来配置你的前端页面:server { listen 80; server_name your_domain_or_IP; location / { root /var/www/mywebsite; index index.html; try_files $uri $uri/ =404; } }其中 **your_domain_or_IP** 应该替换为你的域名或者IP地址,**/var/www/mywebsite** 应该替换为你的代码所在的目录。此配置意味着 Nginx 将监听80端口,并为所有指向你的域名或IP地址的请求提供服务。默认文档为 index.html,如果请求的文件不存在,Nginx 将返回 404 状态码。步骤4:启用网站创建并编辑完配置文件后,你需要创建一个到 **sites-enabled** 目录的符号链接来启用你的网站:sudo ln -s /etc/nginx/sites-available/mywebsite /etc/nginx/sites-enabled/步骤5:检查配置文件在重启 Nginx 之前,你应该检查你的配置文件有没有语法错误:sudo nginx -t如果没有错误,你会看到类似这样的输出:nginx: configuration file /etc/nginx/nginx.conf test is successful步骤6:重启 Nginx最后,你需要重启 Nginx 来使你的配置生效:sudo service nginx restart现在你的前端页面应该可以通过你的域名或IP地址访问了。注意,如果你的服务器有防火墙,你可能需要配置防火墙来允许 HTTP 和 HTTPS 流量。更完整的 Nginx 配置首先,你需要为你的域名获取一个 SSL 证书,你可以使用 Let's Encrypt 提供的免费证书。配置文件可能如下:server { listen 80; server_name your_domain_or_IP; return 301 https://$host$request_uri; } server { listen 443 ssl; server_name your_domain_or_IP; ssl_certificate /etc/letsencrypt/live/your_domain_or_IP/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your_domain_or_IP/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; location / { root /var/www/mywebsite; index index.html; try_files $uri $uri/ =404; } location ~* \\.(jpg|jpeg|png|gif|ico|css|js|pdf)$ { expires 30d; } gzip on; gzip_vary on; gzip_min_length 10240; gzip_proxied expired no-cache no-store private auth; gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml; gzip_disable "MSIE [1-6]\\."; }以下是这个配置的一些解释:第一个 **server** 块监听80端口,也就是HTTP的默认端口,然后把所有的请求重定向到HTTPS。第二个 **server** 块监听443端口,也就是HTTPS的默认端口。**ssl_certificate** 和 **ssl_certificate_key** 指向你的SSL证书和私钥的位置。**ssl_protocols** 和 **ssl_ciphers** 定义了你服务器支持的SSL协议和加密套件。**location /** 块定义了你的网站的根目录和默认文件,以及如何处理不存在的路径。**location ~* \\.(jpg|jpeg|png|gif|ico|css|js|pdf)$** 块定义了对于静态文件如图片,CSS文件,JavaScript文件等,客户端应该缓存30天。**gzip** 相关的行启用了gzip压缩,并定义了哪些类型的文件应该被压缩。这只是一个基本的配置样例,你可以根据你的需求进行修改。有许多其他的选项可以配置,例如负载均衡,反向代理,HTTP/2 支持等。你可以查看 Nginx 的文档来了解更多信息。负载均衡使用 Nginx 来做负载均衡的实现非常直接。以下是一个简单的示例来说明如何配置 Nginx 来实现对多个服务器的负载均衡。假设你有两个或更多的应用服务器实例,它们分别托管在不同的 IP 地址或主机名下**。你可以在 Nginx 配置文件中设置一个 upstream 模块,然后在 server 模块中将请求反向代理到这个 upstream。**首先,你需要在你的 Nginx 配置文件(一般位于 /etc/nginx/nginx.conf 或 /etc/nginx/conf.d/ 目录下的某个文件)中定义一个 upstream。例如:http { upstream myapp { server app1.example.com; server app2.example.com; } ... }在这个例子中,myapp 是你定义的 upstream 的名字,app1.example.com 和 app2.example.com 是你的应用服务器的地址。然后,在 server 模块中,你可以将来自客户端的请求代理到这个 upstream:server { listen 80; location / { proxy_pass <http://myapp>; } }在这个例子中,所有来自客户端的 HTTP 请求(即 :80 端口的请求)都会被 Nginx 转发到定义的 upstream(即 myapp)。Nginx 默认使用**轮询(round-robin)算法将请求分配给 upstream 中的服务器,你也可以配置其他的负载均衡算法,如最少连接(least\_conn)**或 IP 哈希(ip\_hash)。这样,Nginx 就会把请求均衡地转发到你的各个应用服务器实例上,实现负载均衡。注意这个配置对于前端静态页面来说可能不太必要,因为静态页面通常不会对服务器产生太大负载,但对于动态应用来说是非常有用的。Docker使用 Docker 来部署前端应用是一种很好的方式,因为它可以将你的应用及其依赖项打包到一个独立、可移植的容器中,可以在任何安装了 Docker 的环境中运行。以下是使用 Docker 部署一个基本的静态前端页面的步骤:步骤1:创建 Dockerfile在你的项目根目录中,创建一个名为"Dockerfile"的文件。这个文件是用来定义你的 Docker 镜像的,可以看作是一个自动化的脚本。以下是一个简单的 Dockerfile 示例,其中使用了官方的 Nginx 镜像来部署静态页面:# 使用官方的 Nginx 镜像 FROM nginx:latest # 删除 Nginx 默认的静态资源 RUN rm -rf /usr/share/nginx/html/* # 将你的静态资源复制到 Nginx 的静态资源目录下 COPY ./dist /usr/share/nginx/html # 暴露 80 端口 EXPOSE 80 # 启动 Nginx CMD ["nginx", "-g", "daemon off;"]步骤2:构建 Docker 镜像在项目根目录下,你可以使用 **docker build** 命令来根据 Dockerfile 构建 Docker 镜像:docker build -t my-frontend-app .其中,**-t my-frontend-app** 参数用于给你的 Docker 镜像命名,**.** 指定了 Dockerfile 所在的位置(在这里,它在当前目录下)。步骤3:运行 Docker 容器你现在可以使用 **docker run** 命令来运行你刚刚构建的 Docker 镜像:docker run -p 80:80 -d my-frontend-app**p 80:80** 参数指定了将容器的 80 端口映射到宿主机的 80 端口,**d** 参数指定了以后台模式运行容器。现在你应该可以在你的浏览器中通过访问 **http://localhost** 来查看你的前端页面了。如果你的前端应用更复杂,或者需要与后端服务进行交互,你可能需要进行更复杂的配置,比如使用 Docker Compose 来管理多个服务,或者使用环境变量来配置你的应用。负载均衡如果你的前端应用使用 Docker 部署,有多种方式可以实现负载均衡。以下是一些常见的方法:1. 使用 Docker Swarm:Docker Swarm 是 Docker 的原生集群管理和编排工具。在 Swarm 集群中,你可以创建一个服务,然后指定服务的副本数量。Swarm 会自动在集群的节点间分配这些副本,实现负载均衡。以下是一个简单的 Docker Swarm 服务创建命令:docker service create --name my-frontend-app --publish 80:80 --replicas 3 my-frontend-app在这个命令中,**--name my-frontend-app** 指定了服务的名称,**--publish 80:80** 指定了将容器的 80 端口映射到宿主机的 80 端口,**--replicas 3** 指定了创建 3 个副本,**my-frontend-app** 是你的 Docker 镜像的名称。2. 使用 Kubernetes:Kubernetes 是一个更强大的容器编排平台,它也支持负载均衡。在 Kubernetes 中,你可以创建一个 Deployment 和一个 Service,Kubernetes 会自动为你的应用提供负载均衡。以下是一个简单的 Kubernetes Deployment 和 Service 的定义:apiVersion: apps/v1 kind: Deployment metadata: name: my-frontend-app spec: replicas: 3 selector: matchLabels: app: my-frontend-app template: metadata: labels: app: my-frontend-app spec: containers: - name: my-frontend-app image: my-frontend-app ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: my-frontend-app spec: type: LoadBalancer ports: - port: 80 selector: app: my-frontend-app在这个定义中,Deployment 创建了 3 个前端应用的副本,Service 为这些副本提供了一个负载均衡器。3. 使用云服务提供商的负载均衡器:如果你的应用部署在云环境(如 AWS, Google Cloud, Azure 等),你可以使用云服务提供商的负载均衡服务。这些负载均衡服务通常提供了更强大的功能,比如自动扩缩容,健康检查,SSL 终结等。以上都是实现 Docker 负载均衡的一些方式,你可以根据你的具体需求和环境选择最合适的方式。PM2PM2 主要用于管理和维护 Node.js 应用,但是你也可以使用 PM2 来运行一个静态服务器,用来部署你的前端页面。这可以通过使用像 **http-server** 这样的简单的静态文件服务器来完成。以下是使用 PM2 部署前端页面的步骤:安装 http-server首先,你需要在你的项目中安装 **http-server**。你可以使用 **npm** 来安装它:npm install http-server --save创建 PM2 配置文件接下来,你需要创建一个 PM2 的配置文件,例如 **ecosystem.config.js**。在这个文件中,你需要指定 **http-server** 的路径和你的前端页面的路径:javascriptCopy code module.exports = { apps: [ { name: 'frontend', script: 'node_modules/http-server/bin/http-server', args: './dist -p 8080', // 这里的 ./dist 是你的前端页面的路径,-p 8080 是你要监听的端口 instances: 'max', exec_mode: 'cluster', watch: true, // 如果你希望在文件改变时自动重启服务器,可以设置 watch 为 true env: { NODE_ENV: 'production', }, }, ], };使用 PM2 启动你的前端页面现在,你可以使用 PM2 来启动你的前端页面了:pm2 start ecosystem.config.js通过以上步骤,你就可以使用 PM2 和 **http-server** 来部署你的前端页面了。这种方式的好处是你可以利用 PM2 提供的特性,例如应用的自动重启和负载均衡。但需要注意的是,**http-server** 是一个非常简单的静态文件服务器,如果你需要更复杂的功能(例如 SSL、反向代理等),你可能需要考虑使用更强大的服务器软件,例如 Nginx 或 Express.js。部署方式比较技术优点缺点出现时间适合场景不适合场景Nginx轻量级,高性能,可作为反向代理和负载均衡器需要手动配置和管理2004年任何需要静态页面服务的项目需要动态服务器端渲染的项目PM2 Serve可以使用PM2来管理静态服务进程不包含反向代理或负载均衡器PM2:2014年需要简单的静态文件服务需要复杂的HTTP请求处理或负载均衡的项目Docker容器化,跨平台需要手动管理容器和镜像,比其他选项复杂2013年任何需要跨平台部署或以统一方式部署多种服务的项目小规模或简单的项目,因为Docker可能引入不必要的复杂性GitHub Pages免费,支持自定义域名,集成GitHub功能有限,仅支持静态内容2008年简单的个人、项目或组织页面复杂的、需要后端的应用Netlify/Vercel提供自动部署,函数即服务等免费版有一些限制Netlify:2014年, Vercel:2015年静态网站和Jamstack应用大规模、复杂的后端应用云存储服务 (AWS S3, Google Cloud Storage)简单,可扩展,集成CDN需要配置权限和CDNAWS S3:2006年, Google Cloud Storage:2010年静态网站,或需要大规模存储的应用需要后端逻辑的应用同样,你应该根据你的具体需求来选择最适合的部署方法。例如,如果你的网站很简单,GitHub Pages 可能就足够了。如果你需要一些后端逻辑,但不想管理服务器,那么 Netlify 或 Vercel 可能是一个好选择。如果你需要完全的控制和可扩展性,Docker 或云存储服务可能更合适。Node 应用简介部署 Node.js 应用也有多种方法,选择哪一种取决于你的项目需求和技术背景。以下是一些常见的部署方式:手动部署: 这是最基本的方式,你需要在你的服务器上安装Node.js,然后通过FTP或者SCP将你的代码上传到服务器,然后运行你的应用。使用版本控制系统(如Git): 你可以在服务器上设置一个Git仓库,并通过**git push**命令来部署你的应用。你还需要在服务器上设置一个脚本来自动运行你的应用。使用PM2: PM2是一个带有负载均衡功能的Node应用的进程管理器,可以让你的应用后台运行,如果应用崩溃了,PM2会自动重启应用。使用Docker: Docker让你可以在一个隔离的容器中运行你的应用,这可以避免很多系统依赖问题。你可以将你的Node.js应用“Docker化”,并在任何支持Docker的平台上运行。使用云服务: 许多云服务提供了Node.js的支持,比如AWS Elastic Beanstalk,Google App Engine,Heroku,Azure App Service等。这些服务通常会处理大部分部署细节,让你可以专注于编写代码。使用PaaS平台: 例如Heroku,它为部署Node.js应用提供了非常方便的平台。你只需要将你的代码推送到Heroku的Git仓库,然后Heroku就会自动为你的应用分配资源,并启动你的应用。使用Serverless架构: 如AWS Lambda,Azure Functions,Google Cloud Functions等,你只需要编写处理请求的函数,然后上传到服务提供商的平台,服务提供商会为你处理扩容、负载均衡等问题。选择适合你的方式,主要取决于你的应用规模,预期的访问量,以及你希望多大程度上控制你的服务器和部署过程。PM2PM2 是一个非常流行的 Node.js 应用程序的生产过程管理器,可以让你保持应用的持续运行,可以让你在应用崩溃时自动重启,也可以使你的应用运行在多个 CPU 核心上,从而提高性能。以下是使用 PM2 部署 Node.js 应用的步骤:安装 PM2首先,你需要在你的服务器上安装 PM2。你可以使用 npm(Node.js 的包管理器)来安装 PM2:npm install -g pm2这个命令会全局安装 PM2,所以你可以在任何地方运行 pm2 命令。启动你的应用有了 PM2,你就可以使用它来启动你的 Node.js 应用了。假设你的入口文件是 **app.js**,你可以运行以下命令来启动你的应用:pm2 start app.js你的应用现在就在 PM2 的管理下运行了。如果你的应用崩溃,PM2 会自动重启它。你也可以使用 PM2 的其他命令来查看应用的状态,查看日志,停止或重启应用等。配置应用对于更复杂的应用,你可能需要进行更多的配置。例如,你可能希望你的应用在服务器启动时自动启动,或者你可能希望在多个 CPU 核心上运行你的应用。PM2 允许你创建一个配置文件(通常是一个名为 **ecosystem.config.js** 的文件),在这个文件中,你可以定义你的应用的配置。以下是一个简单的配置文件示例:javascriptCopy code module.exports = { apps: [{ name: "my-app", script: "app.js", instances: "max", exec_mode: "cluster" }] }在这个配置中,**name** 属性定义了应用的名称,**script** 属性定义了应用的入口文件,**instances** 属性定义了应用的实例数量(在这个例子中,"max" 表示在所有可用的 CPU 核心上运行应用),**exec_mode** 属性定义了应用的执行模式(**cluster** 表示启用集群模式)。然后,你可以使用 **pm2 start ecosystem.config.js** 命令来按照这个配置启动你的应用。开启日志PM2 还提供了一些方便的工具来帮助你监视你的应用。例如,你可以使用 **pm2 logs** 命令来查看你的应用的日志,使用 **pm2 monit** 命令来查看每个应用的 CPU 和内存使用情况。以上就是使用 PM2 部署 Node.js 应用的基本步骤。需要注意的是,虽然 PM2 可以帮助你管理你的应用,但它并不能解决所有问题。例如,你可能还需要配置你的服务器防火墙,配置你的域名解析,安装和配置 SSL 证书等。高级应用PM2 是一个强大的 Node.js 应用进程管理器,除了基本的启动、监控和重启应用,它还提供了许多高级的特性和功能。以下是一些可以使用的高级功能:热重载(Hot Reload): 热重载可以在不停机的情况下重启应用。这对于需要长时间运行的应用来说非常有用,因为它可以在不影响用户的情况下进行更新。你可以使用 **pm2 reload <app_name>** 来进行热重载。应用监控(Monitoring): 使用 **pm2 monit** 命令,你可以看到所有由 PM2 管理的应用的 CPU 和内存使用情况,还可以看到每个应用的日志和其它信息。这个功能可以帮助你实时监控应用的性能并快速定位问题。日志管理(Log Management): PM2 会自动捕获你的应用的标准输出(stdout)和标准错误(stderr),并将它们保存到日志文件中。你可以使用 **pm2 logs** 命令来查看这些日志。集群模式(Cluster Mode): 在集群模式下,PM2 会启动多个应用实例,并自动负载均衡到所有可用的 CPU 核心。这可以极大地提高多核服务器的性能。启动文件(Startup Script): PM2 可以生成一个启动脚本,这样你的应用就可以在服务器启动时自动启动。你可以使用 **pm2 startup** 命令来生成这个脚本。自定义日志、PID 和应用列表路径: PM2 允许你自定义日志文件、PID 文件和应用列表文件的路径,这可以帮助你更好地组织你的文件。源代码变更自动重启(Watch & Restart): PM2 可以监听你的文件变化,并在文件变化时自动重启应用。你可以在启动应用时添加 **-watch** 参数来启用这个功能,或者在配置文件中设置 **watch** 属性。部署系统(Deployment System): PM2 提供了一个简单的部署系统,可以让你更容易地更新你的应用并追踪版本。你需要在配置文件中设置 **deploy** 属性来使用这个功能。这些高级功能可以让你更好地管理你的 Node.js 应用,并帮助你提高应用的性能和可靠性。如果你想了解更多关于 PM2 的信息,你可以查看 PM2 的官方文档。exec\_mode 有哪些模式PM2 提供两种执行模式,即 "fork" 和 "cluster"。这两种模式的主要区别在于它们处理 Node.js 应用的方式,以及它们如何利用多核 CPU。Fork Mode(分支模式): Fork mode 是 PM2 的默认执行模式。在这种模式下,每个 Node.js 应用都运行在自己的单独进程中。这意味着每个应用都有自己独立的内存空间,并且不能直接与其他应用共享状态。这种模式适合运行多个不同的 Node.js 应用。Cluster Mode(集群模式): 在集群模式下,PM2 会启动多个应用的实例,并自动进行负载均衡,让所有可用的 CPU 核心都被利用起来。这可以极大地提高多核服务器的性能。集群模式利用了 Node.js 的内建集群模块,因此你的 Node.js 应用不需要做任何修改就可以在集群模式下运行。如果你想在启动应用时指定执行模式,你可以在 **pm2 start** 命令后添加 **--exec-mode** 参数。例如,以下命令会在集群模式下启动 app.js:pm2 start app.js --exec-mode cluster如果你想在配置文件中指定执行模式,你可以设置 **exec_mode** 属性。例如,以下配置会在集群模式下启动 app.js:module.exports = { apps : [{ name: "app", script: "app.js", exec_mode: "cluster" }] }需要注意的是,虽然集群模式可以提高性能,但它也有一些限制。例如,因为每个应用实例都运行在自己的进程中,它们不能直接共享状态。如果你的应用需要维护共享状态,你可能需要使用外部服务(如 Redis)或者使用 IPC 通信。负载均衡使用 PM2 部署 Node.js 应用时,你可以利用其内建的集群模式(cluster mode)来实现负载均衡。在这种模式下,PM2 会创建多个应用实例,并自动将进入的请求分发到各个实例,从而实现负载均衡。以下是使用 PM2 实现负载均衡的步骤:确定应用实例的数量你可以选择让 PM2 自动根据 CPU 的核心数量来创建应用实例。你只需要将 instances 属性设置为 "max",PM2 就会创建与 CPU 核心数量相等的应用实例:module.exports = { apps : [{ name: "app", script: "app.js", instances: "max", exec_mode: "cluster" }] }你也可以手动指定实例的数量。只需要将 instances 属性设置为你想要的数字即可:module.exports = { apps : [{ name: "app", script: "app.js", instances: 4, // 创建4个实例 exec_mode: "cluster" }] }设置执行模式要启用集群模式,你需要将 exec\_mode 属性设置为 "cluster"。启动应用在你的配置文件设置好之后,你可以使用 pm2 start 命令启动你的应用:pm2 start ecosystem.config.js现在,你的 Node.js 应用已经在 PM2 的集群模式下运行了,PM2 会自动实现负载均衡。如果有任何应用实例崩溃,PM2 也会自动重启它。注意,虽然 PM2 的集群模式可以提高多核服务器的性能,但它并不是一个全功能的负载均衡解决方案。例如,它不支持会话粘性(session stickiness),这意味着如果你的应用需要维护用户会话,用户的每个请求可能会被路由到不同的应用实例。在这种情况下,你可能需要使用其他的负载均衡解决方案,例如 Nginx 或 HAProxy。PM2 + Nginx以下是使用 PM2 和 Nginx 部署 Node.js 应用并实现负载均衡的步骤:安装和配置PM2首先,你需要在服务器上安装 Node.js 和 PM2。你可以使用 NPM(Node Package Manager)来安装 PM2:npm install -g pm2然后,你可以使用 PM2 来启动你的 Node.js 应用。假设你的入口文件叫做 **app.js**,你可以使用以下命令来启动你的应用:pm2 start app.js -i max上述命令中的 **-i max** 参数将会告诉 PM2 启动尽可能多的应用实例来利用所有的CPU核心,这将帮助你实现负载均衡。安装和配置Nginx然后,你需要在服务器上安装 Nginx。在 Ubuntu 或 Debian 上,你可以使用以下命令来安装 Nginx:sudo apt-get update sudo apt-get install nginx接下来,你需要配置 Nginx 作为你的 Node.js 应用的反向代理。你需要编辑 Nginx 的配置文件(通常位于 **/etc/nginx/sites-available/default**)并添加以下内容:upstream myapp { server 127.0.0.1:3000; keepalive 64; } server { listen 80; location / { proxy_pass <http://myapp>; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } }在上述配置中,**upstream模块定义了一个服务器组,Nginx将会在该服务器组中进行负载均衡。proxy_pass 指令告诉 Nginx 将所有的请求转发到 myapp** 服务器组。注意:你需要将 **server 127.0.0.1:3000 中的3000**替换为你的 Node.js 应用实际监听的端口。重启Nginx保存配置文件后,你需要重启Nginx以使新的配置生效:sudo systemctl restart nginx现在,你的 Node.js 应用应该可以通过 http://your-server-ip 访问了,同时,你已经成功地实现了负载均衡。记住,你需要为每一个 Node.js 应用的实例配置一个独立的**server条目。你可以将这些server条目添加到upstream模块中以实现负载均衡。如果你的应用在多台服务器上运行,你可以将每个服务器的地址添加到upstream**模块中。Docker为了使用 Docker 来部署一个 Node.js 应用,你需要创建一个 Dockerfile,这是一个用来构建 Docker 镜像的文本文件。然后你可以运行这个 Docker 镜像以启动你的应用。以下是一个基本的 Dockerfile 的示例,它描述了如何创建一个包含 Node.js 应用的 Docker 镜像:# 使用官方 Node.js 作为基础镜像 FROM node:14 # 设置工作目录 WORKDIR /usr/src/app # 复制 package.json 和 package-lock.json COPY package*.json ./ # 安装你的应用的依赖 RUN npm install # 复制你的应用的源码 COPY . . # 暴露你的应用监听的端口 EXPOSE 8080 # 启动你的应用 CMD [ "node", "app.js" ]这个 Dockerfile 假设你的应用在 "app.js" 中定义。你可能需要根据你的应用修改这个文件名。一旦你的 Dockerfile 创建好了,你可以使用以下命令来在 Dockerfile 所在的目录中构建你的 Docker 镜像:docker build -t my-node-app .然后你可以使用以下命令来运行你的 Docker 镜像:docker run -p 8080:8080 -d my-node-app这个命令会启动一个新的 Docker 容器,并将容器的 8080 端口映射到宿主机的 8080 端口。**-d** 选项会让 Docker 在后台运行这个容器。这是使用 Docker 部署一个 Node.js 应用的基本步骤。根据你的应用和环境,你可能需要进行更复杂的 Docker 配置。Docker + Docker Swarm果你希望在 Docker 环境中部署 Node.js 应用并实现负载均衡,但是不想使用 Nginx,你可以考虑使用 Docker 的内置工具如 Docker Swarm 或者第三方工具如 Traefik 和 HAProxy。以下是一个使用 Docker Swarm 实现负载均衡的例子:创建 Dockerfile你需要创建一个 Dockerfile 来定义你的 Node.js 应用的镜像。以下是一个简单的 Dockerfile 例子:FROM node:14 WORKDIR /app COPY package*.json ./ RUN npm install COPY . . EXPOSE 8080 CMD [ "node", "server.js" ]构建 Docker 镜像在你的 Dockerfile 所在的目录中运行以下命令来构建你的 Docker 镜像:bashCopy code docker build -t my-node-app .初始化 Docker Swarm在你的服务器上运行以下命令来初始化 Docker Swarm:docker swarm init部署你的 Node.js 应用你可以使用 Docker Service 来部署你的 Node.js 应用。以下命令将会启动三个你的 Node.js 应用的实例,并在端口8080上公开服务:docker service create --name my-node-service --publish published=8080,target=8080 --replicas=3 my-node-appDocker Swarm 将会自动在这三个实例之间实现负载均衡。以上步骤提供了使用 Docker Swarm 进行负载均衡的基础步骤。如果你希望进行更复杂的负载均衡配置,你可能需要查阅更多的 Docker Swarm 文档或者考虑使用更专业的负载均衡工具如 Traefik 和 HAProxy。Docker + k8s使用 Docker 部署 Node.js 应用,并使用 Kubernetes(k8s)实现负载均衡,你需要创建 Dockerfile、构建和推送 Docker 镜像、然后在 Kubernetes 上创建 Deployment 和 Service。以下是具体的步骤:创建 Dockerfile首先,创建一个 Dockerfile 来定义你的 Node.js 应用的 Docker 镜像:FROM node:14 WORKDIR /app COPY package*.json ./ RUN npm install COPY . . EXPOSE 8080 CMD [ "node", "server.js" ]构建 Docker 镜像在你的 Dockerfile 所在的目录中运行以下命令来构建你的 Docker 镜像:docker build -t my-node-app .推送 Docker 镜像到镜像仓库然后,你需要将你的 Docker 镜像推送到一个可以被你的 Kubernetes 集群访问的镜像仓库中,例如 Docker Hub。首先,登录你的 Docker Hub 帐户:docker login然后,标记并推送你的镜像:docker tag my-node-app:latest yourusername/my-node-app:latest docker push yourusername/my-node-app:latest记住,你需要将 **yourusername** 替换为你的 Docker Hub 用户名。创建 Kubernetes Deployment然后,你需要在 Kubernetes 上创建一个 Deployment 以运行你的 Node.js 应用。创建一个名为**my-node-app-deployment.yaml**的文件,然后添加以下内容:yamlCopy code apiVersion: apps/v1 kind: Deployment metadata: name: my-node-app spec: replicas: 3 selector: matchLabels: app: my-node-app template: metadata: labels: app: my-node-app spec: containers: - name: my-node-app image: yourusername/my-node-app:latest ports: - containerPort: 8080在这个文件中,**replicas: 3**意味着 Kubernetes 将运行三个你的应用的副本。然后,运行以下命令来创建 Deployment:kubectl apply -f my-node-app-deployment.yaml创建 Kubernetes Service最后,你需要创建一个 Kubernetes Service 来公开你的应用。创建一个名为**my-node-app-service.yaml**的文件,然后添加以下内容:apiVersion: v1 kind: Service metadata: name: my-node-app spec: type: LoadBalancer ports: - port: 80 targetPort: 8080 selector: app: my-node-app在这个文件中,**type: LoadBalancer**意味着 Kubernetes 将创建一个云提供商的负载均衡器,并将公网流量导向你的应用。然后,运行以下命令来创建 Service:kubectl apply -f my-node-app-service.yaml现在,你的 Node.js 应用应该已经在 Kubernetes 上运行,并且已经实现了负载均衡。你可以运行以下命令来检查你的 Service 的状态:kubectl get svc my-node-app在输出中,你应该可以看到你的 Service 的外部IP地址,你可以通过这个IP地址访问你的应用。Docker 还有必要使用 pm2 吗PM2 是一个流行的进程管理器,它可以让你保持你的应用始终在线,自动重启在异常情况下崩溃的应用,并且可以方便地管理你的应用的日志。然而,当你的应用运行在 Docker 容器中时,你可能并不总是需要 PM2。在 Docker 的哲学中,通常每个容器只运行一个进程,当这个进程结束时,容器就会停止。因此,如果你的应用崩溃,Docker 可以自动重启容器,这就提供了 PM2 的部分功能。此外,Docker 也提供了日志管理的功能,你可以使用**docker logs**命令来查看你的应用的输出。然而,PM2 还提供了其他一些功能,例如集群模式,它可以启动多个你的应用的实例来充分利用多核CPU。如果你的应用需要这种功能,并且你不打算使用其他的负载均衡解决方案,那么在 Docker 容器中使用 PM2 可能是有价值的。总的来说,是否在 Docker 中使用 PM2 取决于你的具体需求。如果 Docker 的内建功能已经足够满足你的需求,那么你可能不需要 PM2。但是,如果你需要 PM2 的某些特定功能,那么你可能会希望在你的 Docker 容器中使用 PM2。Docker+Nginx 为什么 不适合场景 负载均衡的项目首先,我要澄清一点,Docker+Nginx 是可以实现负载均衡的。当你在 Docker 中启动多个应用实例,然后使用 Nginx 作为反向代理来分发流量时,就实现了负载均衡。我的表述可能有些误导,对此我感到非常抱歉。然而,使用 Docker+Nginx 实现负载均衡有一个局限性,那就是配置的复杂性。**在一个动态环境中,你可能会经常启动和停止 Docker 容器,这就需要你不断地更新 Nginx 的配置以反映这些变化。**这个过程可以手动完成,但在一个大型系统中,这可能会变得非常繁琐和容易出错。此外,Nginx 并不直接支持一些更高级的负载均衡策略,例如基于请求的路由或基于会话的持久性。这些功能通常需要使用更专业的负载均衡器或服务网格来实现。所以,当我说 Docker+Nginx 可能不适合需要复杂负载均衡的项目时,我是指对于那些需要更高级负载均衡功能或者需要在动态环境中自动处理容器变化的项目,可能需要考虑使用如 Kubernetes 或 Docker Swarm 这样的工具,因为它们提供了更强大和灵活的负载均衡能力,并能更好地处理容器的动态性。部署方式比较技术优点缺点出现时间适合场景不适合场景PM2进程管理,自动重启,负载均衡不处理反向代理或容器化2014年单体应用,小规模项目大规模或需要高度可配置的项目PM2+Nginx进程管理,自动重启,负载均衡,反向代理,静态文件服务不处理容器化PM2:2014年,Nginx:2004年单体应用,需要处理HTTP请求的项目需要容器化的项目Docker容器化,跨平台不直接提供进程管理,反向代理,或负载均衡2013年任何大小的项目,需要跨平台的项目无特殊需求的小型项目,因为 Docker 可能引入不必要的复杂性Docker+Nginx容器化,反向代理,静态文件服务,可以配置为负载均衡需要手动维护Nginx配置以反映容器变化,可能不能提供更高级的负载均衡功能Docker:2013年,Nginx:2004年需要处理HTTP请求和基本负载均衡的项目,需要跨平台的项目需要自动处理容器变化或需要高级负载均衡的项目Docker Swarm容器化,服务发现,负载均衡,集群管理功能可能不如 Kubernetes 丰富2016年中等规模的项目,需要集群管理的项目大规模或需要高度可配置的项目Docker+K8s容器化,服务发现,负载均衡,自动扩缩容,滚动更新,自愈,集群管理相对复杂,学习曲线陡峭Docker:2013年,K8s:2015年大规模或复杂的微服务架构的项目小规模项目,因为 Kubernetes 可能引入不必要的复杂性选择哪种部署方式主要取决于你的应用的特性和需求,以及你和你的团队的技能和资源。对于小规模的单体应用,PM2 或者 PM2+Nginx 可能就足够了。如果你的应用需要在多台服务器或云环境中部署,那么你可能需要考虑使用 Docker。如果你的应用是一个大规模或复杂的微服务架构的项目,那么你可能需要考虑使用 Kubernetes。【END】
2023年08月01日
13 阅读
0 评论
0 点赞
1
...
13
14
15
...
39