本篇文章假定读者已经拥有OpenAPI帐号,并且已经获取了API访问的secret key。
本文部分内容由ChatGPT生成
本章节描述了如何从零开发一个私有化版本的 ChatGPT 网站,主要使用技术栈为 NextJS + TailwindCSS + chatgpt ,其中 NextJS 作为 React 的全栈框架,能够快速搭建包含前后端的 React 应用,TailwindCSS 则提供了较为便利的样式变量以及移动端的适配,最后通过 NodeJS 的API库 chatgpt 来调用 OpenAI 进行交互。
完整的项目代码可以在 github 上查看 github.com/helianthusw…
在官方介绍中,NextJS 提供了生产环境所需的所有功能以及最佳的开发体验:包括静态及服务器端融合渲染、 支持 TypeScript、智能化打包、 路由预取等功能,因此它是当前 React 技术栈最为流行的框架。
对我个人而言,一是考虑到项目要通过服务端去代理请求,需要一个全栈的技术方案;另一个也是想学习一下这个框架,所以整个项目的结构以及性能都不是最优,也请读者谅解。
使用以下命令创建一个新的 NextJS 项目:
npx create-next-app@latest --typescript
# or
yarn create next-app --typescript
根据提示语配置完整个项目后,可以得到一个如下目录结构的项目:
├── README.md # 项目的README文件
├── next-env.d.ts # 默认生成的next的ts环境引入文件(不需要关注)
├── next.config.js # next的配置文件
├── package-lock.json # 项目的package-lock.json
├── package.json # 项目的package.json
├── pages # 项目的主要路径目录
│?? ├── _app.tsx # 每个页面的入口文件
│?? ├── _document.tsx # 每个页面的文档结构,相当于index.html
│?? ├── api # 项目的api接口处理(处理服务端接受的请求)
│?? │?? └── hello.ts
│?? └── index.tsx # 单个页面的入口文件
├── public # 静态资源目录
│?? ├── favicon.ico
│?? ├── next.svg
│?? ├── thirteen.svg
│?? └── vercel.svg
├── styles # 样式文件目录
│?? ├── Home.module.css # 使用module css的方式处理scoped样式
│?? └── globals.css # 全局的样式必须在 _app.tsx 中引入
└── tsconfig.json # ts的配置文件
默认生成的项目目录通常为最简单的目录,在实际项目开发中可能并不能满足我们的需求,在稍加改造之后,我们会引入 src 目录作为我们的主要文件目录,其中新增了 components 文件夹存放我们的组件、hooks 文件夹存放自定义的 Hooks、service 文件夹存放一些抽象出来的公共服务、store 文件夹存放状态管理文件、utils 文件夹存放一些公共的方法。
其他的一些配置文件如 Dockerfile、postcss.config.js、tailwind.config.js 等都是在项目开发过程中逐步引入的,在项目初始化阶段我们可以不用关注。
最终完整的项目目录如下所示,也可以在这里快速查看详细内容:
├── Dockerfile # Docker镜像配置文件
├── LICENSE # 项目的LICENSE声明
├── README.md # 项目介绍文件
├── globals.d.ts # 自定义的一些全局的ts变量
├── next-env.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js # postcss配置文件
├── public
│── ├── author.jpg
│── └── favicon.ico
├── src
│── ├── components/ # 组件存放目录
│── ├── hooks/ # 自定义hooks存放目录
│── ├── pages/ # 页面目录
│── ├── service/ # 公共服务目录
│── ├── store/ # 状态管理文件目录
│── ├── styles/ # 样式文件目录
│── └── utils/ # 公共方法目录
├── tailwind.config.js # tailwindcss的配置文件
└── tsconfig.json
由于我们的这个项目复杂度并不高,所以不需要引入 Redux 或者 Mobx 这种完善的状态管理工具,而直接使用了 React 自带的 Context 的方式来进行数据流管理。
Context API是React提供的一种机制,用于在组件之间共享数据,避免了将state从一个组件传递到另一个组件的繁琐过程。
使用Context API时,需要先定义一个Context对象,可以通过React.createContext()函数来创建。然后,在需要共享数据的组件中,使用该Context对象的Provider组件包裹子组件,并将需要共享的数据传递给Provider的value属性。最后,在其他需要访问该数据的组件中,可以通过该Context对象的Consumer组件来获取数据。
例如我们可以通过如下方式来实现 Context 管理数据流:
const MyContext = React.createContext();
function App() {
const [data, setData] = useState({ name: "Alice", age: 20 });
// 定义一个更新数据的函数
const updateData = (newData) => {
setData(newData);
};
return (
<MyContext.Provider value={{ data, updateData }}>
<ChildComponent />
</MyContext.Provider>
);
}
function ChildComponent() {
const { data, updateData } = useContext(MyContext);
const handleClick = () => {
// 更新数据
updateData({ name: "Bob", age: 30 });
};
return (
<>
<div>Name: {data.name}</div>
<div>Age: {data.age}</div>
<button onClick={handleClick}>Update Data</button>
</>
);
}
在我们的项目的 src/store/ 目录下,创建了以下三个 Context,点击可以查看每个的详细内容:
在 src/pages/_app.tsx 中我们引入对应的 ContextProvider 并进行嵌套包裹,这样便可以实现所有的页面和组件都可以使用 useContext 获取到 Context 中管理的状态,引入代码如下所示:
// 省略了样式及其他必要内容的引入代码
import AppStoreProvider from "@/store/App";
import UserStoreProvider from "@/store/User";
import ChatStoreProvider from "@/store/Chat";
export default function App({ Component, pageProps }: AppProps) {
/* 省略了部分代码 */
...
return (
<AppStoreProvider>
<UserStoreProvider>
<ChatStoreProvider>
{/* 省略了部分代码 */}
...
<Component {...pageProps} />
...
{/* 省略了部分代码 */}
</ChatStoreProvider>
</UserStoreProvider>
</AppStoreProvider>
);
}
该项目中前端请求部分并没有使用 axios 这种请求模块,主要还是觉得 axios 略重,并且对我个人来说不够熟悉,因此选择了多年前我开发的并且还一直在使用的前端请求库 web-rest-client。
web-rest-client 是一个用于浏览器中发送RESTful API请求的JavaScript库。相比其他JavaScript库和框架,具有以下优势:
易用性:web-rest-client提供了简单易用的API,使开发者能够轻松地构建和发送HTTP请求。它的API设计直观清晰,易于理解和使用。
轻量级:web-rest-client是一个轻量级的JavaScript库,大小只有几KB,不会增加过多的代码负担。
兼容性:web-rest-client支持所有现代浏览器,包括Chrome、Firefox、Edge等主流浏览器,以及移动端的iOS和Android设备。
配置灵活:web-rest-client支持各种HTTP请求配置选项,如请求头、查询参数、请求体、认证、超时等,可以根据实际需求进行定制。
错误处理:web-rest-client提供了良好的错误处理机制,能够捕获并处理请求失败、超时等异常情况,并向开发者提供详细的错误信息。
可扩展性:web-rest-client的API设计可扩展性强,支持自定义请求拦截器和响应拦截器,可以方便地扩展其功能。
在 src/service/ 目录中新增了 http.ts 文件,用于处理前端的请求发送,这里我们新增了对话请求的API调用,同时通过自定义插件的方式增加了接口报错时的异常提示,完整代码如下所示:
import { message } from "antd";
import { Client, Options, Response } from "web-rest-client";
import { SendResponseOptions } from "@/service/chatgpt";
class HttpService extends Client {
constructor() {
super();
this.responsePlugins.push((res: Response, next: () => void) => {
const { status, data, statusText } = res;
if (status !== 200 && statusText) {
message.error(`${status} ${statusText}`);
}
if ((data as unknown as SendResponseOptions)?.status === "fail") {
res.status = 999;
}
next();
});
}
fetchChatAPIProgress(body: any, options: Omit<Options, "url" | "method">) {
return this.post("/api/chat-progress", body, options);
}
}
const http = new HttpService();
export default http;
为了实现对话过程中的逐字输入效果,我们这里不使用传统的获取到所有对话文本再显示的方式,而是通过服务端设置 application/octet-stream 类型的 Content-type, 使得服务端以下载文件的方式返回二进制数据流给到前端。在前端则是通过给 XMLHttpRequest 对象添加 progress 事件来监听服务端返回的数据流并动态更新数据。当然在 web-rest-client 中已经封装好了对 progress 的处理,我们只需要传入 onDownloadProgress 方法的配置即可。
服务端的 Serverless 函数代码如下:
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { chatReplyProcess, installChatGPT } from "@/service/chatgpt";
import { ConversationRequest } from "@/store/Chat";
import { ChatMessage } from "chatgpt";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
res.setHeader("Content-type", "application/octet-stream");
await installChatGPT();
try {
const { prompt, options = {} } = req.body as {
prompt: string;
options?: ConversationRequest;
};
let firstChunk = true;
await chatReplyProcess(prompt, options, (chat: ChatMessage) => {
res.write(firstChunk ? JSON.stringify(chat) : `\n${JSON.stringify(chat)}`);
firstChunk = false;
});
} catch (error) {
res.write(JSON.stringify(error));
} finally {
res.end();
}
}
在前端为了方便处理,请求调用部分我们可以封装成 useChatProgress hooks,完整的代码可以在这里查看。这里给出 onDownloadProgress 方法的处理代码如下所示:
// useChatProgress中的请求方法
const request = async (index: number, onMessageUpdate?: () => void) => {
// 这里省略了部分代码
...
try {
controller.current = new AbortController();
const { signal } = controller.current;
await http.fetchChatAPIProgress(
{
prompt: message,
options,
},
{
signal,
onDownloadProgress: (
progressEvent: ProgressEvent<XMLHttpRequestEventTarget>
) => {
const xhr = progressEvent.target;
const { responseText } = xhr as XMLHttpRequest;
const lastIndex = responseText.lastIndexOf("\n");
let chunk = responseText;
if (lastIndex !== -1) chunk = responseText.substring(lastIndex);
try {
const data = JSON.parse(chunk);
// 这里省略了更新数据的代码
...
} catch (error) {
console.error(error);
}
},
}
);
} catch (error: any) {
// 这里省略了捕获异常之后的处理代码
...
}
};
这里会简单介绍项目开发过程中几个核心的业务模块及处理方法,详细的具体实现可以查看源码以了解更多。
项目使用了localstorage来做持久化存储。 作为一个轻量级的个人项目,暂时还没有使用 MySQL 或 MongoDB 这种服务端数据库,而是采用前端最熟悉的 localstorage 的方式将用户的所有数据都存在本地。
引入Antd组件库来提升开发效率。 虽然个人感觉在这种小项目中引入 Antd 是有些过重的,而且它还没办法在服务端渲染中使用自定义主题,但是从开发效率出发,个人比较熟悉的 React 的组件库目前也只有它了。
另外 Antd 与 TailwindCSS 共用的时候会出现按钮背景透明的样式冲突问题,只需要在 TailwindCSS 的配置中关闭默认基础样式即可,详细配置可见下个章节。
使用 classnames 库来进行样式类名管理。 classnames 是一个JavaScript库,用于动态生成CSS类名字符串。使用classnames库,开发者可以减少手动编写和拼接CSS类名的工作,使得代码更加清晰简洁、易于维护。
使用dom-to-image库下载图片。 dom-to-image是一个轻量的 JavaScript 库,可以将网页中的任何DOM元素(如div、p、img等)转换为PNG或JPEG格式的图像,并可保存到本地文件系统或直接传输到服务器。
使用原生的 navigator.clipboard.writeText 及 document.execCommand(“copy”) 方法来实现文本的复制。 还是为了保证项目的轻量化,所以直接让 ChatGPT 使用原生的方式实现了一个复制文本的方法,效率也是十分高。
使用 Next 的 404.tsx 和 _error.tsx 来处理异常页面。 当用户输入错误的 URL 或服务端处理请求异常时,我们需要正确的异常页面来进行兜底,这里直接使用 Next 提供的页面加 Antd 的 Result 组件即可。
TailwindCSS 是一个基于原子类的CSS框架,它提供了一组预定义的CSS类,开发者可以通过组合这些类来快速构建出复杂的样式。与其他CSS框架相比,TailwindCSS的特点是更加灵活和可定制。
使用TailwindCSS有以下优势:
提高开发效率:TailwindCSS提供了一组简洁且易于理解的CSS类,使得开发者可以快速构建出需要的UI界面。开发者不需要自己编写大量的CSS代码,只需通过调用预定义的CSS类即可生成所需的样式。
代码复用:由于TailwindCSS的CSS类都是原子类,因此可以轻松地将它们组合成更复杂的样式,从而实现重用。开发者可以利用这种方式避免在多个地方编写重复的样式代码。
可定制性:TailwindCSS提供了大量的配置选项,可以根据实际需求来定制样式。开发者可以通过修改配置文件来改变主题颜色、字体、间距等样式,从而获得更加符合需求的UI界面。
简化UI设计:由于TailwindCSS提供了一组常用的UI组件样式(如按钮、表格、表单等),开发者可以借助这些样式来简化UI设计过程。这些样式遵循一致的设计风格,可以使得UI界面更加统一和美观。
优化性能:TailwindCSS的原子类是高度复用的,因此可以减少CSS文件大小,从而提升页面加载速度和性能。
综合来看,TailwindCSS是一种简单易用、高度定制、以性能为导向的CSS框架,它可以为开发者提供更加高效和灵活的CSS开发体验。
同样的对我个人来说,这也是学习和使用 TailwindCSS 的一个很好的机会。
在项目中通过以下命令来安装 TailwindCSS 所需的依赖:
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
安装完成后通过初始化命令来完成初始化,该步骤主要用于在项目中生成 postcss.config.js 和 tailwind.config.js 这两个配置文件。通常没有特殊的需求,在初始化完成后,我们直接使用默认的配置即可。执行命令如下:
npx tailwindcss init -p
最后我们只需要在 src/styles/global.css 文件中,添加 TailwindCSS 的样式引入并重新启动项目即可愉快的使用了 TailwindCSS 了。
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "antd/dist/antd.variable.min.css";
html,
body,
#__next {
height: 100%;
}
TailwindCSS 是一个响应式的CSS框架,它采用了一种基于断点的响应式设计理念。这种设计理念使得开发者可以在不同的屏幕尺寸上自由地组合和调整CSS类,从而实现对不同设备的适配。
具体来说,TailwindCSS提供了多个预定义的断点(如sm、md、lg、xl等),每个断点对应着一个屏幕宽度区间。开发者可以使用TailwindCSS的响应式前缀(如sm、md、lg等)来指定某个CSS类在特定屏幕尺寸下是否生效。例如,可以使用sm:text-lg类来表示只有在屏幕宽度大于等于sm断点时,该文本应该使用更大的字号。
正是由于 TailwindCSS 优秀的响应式设计,使得我们只需要在移动端访问设备时进行少量的代码修改即可完成响应式。这里我们不用考虑不同移动端设备的尺寸,而是通过 User-Agent 匹配及 设备可视区宽度来判断是否是移动端。完整的 useIsMobile Hooks 判断代码如下:
import { useEffect, useState } from "react";
import debounce from "lodash/debounce";
const useIsMobile = (): boolean => {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
if (
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
)
) {
setIsMobile(true);
return;
}
const updateSize = (): void => {
setIsMobile(window.innerWidth < 768);
};
window.addEventListener("resize", debounce(updateSize, 250));
return (): void => window.removeEventListener("resize", updateSize);
}, []);
return isMobile;
};
export default useIsMobile;
以项目中最外层的容器样式为例,在PC端页面中我们需要增加外边距且容器要有灰色边框和阴影,而在移动端的页面中我们并不需要这些样式,那么我们可以通过以下代码去实现,完整代码可以在这里查看:
// 这里省略其他的引入代码
...
import classNames from "classnames";
import useIsMobile from "@/hooks/useIsMobile";
export default function App({ Component, pageProps }: AppProps) {
const isMobile = useIsMobile();
return (
<div
className={classNames(
// 这里省略其他的样式代码
...
isMobile ? "p-0" : "p-4"
)}
>
<div
className={classNames(
// 这里省略了其他的样式代码
...
isMobile
? ["rounded-none", "shadow-none", "border-none"]
: [
"border",
"rounded-md",
"shadow-md",
"dark:border-neutral-800",
]
)}
>
{/* 这里省略了Component组件的代码 */}
...
</div>
</div>
);
}
最终实现的效果如下图所示:

虽然在我们的项目中目前并没有实现深色模式(因为我懒),但使用 TailwindCSS 还是很方便实现这个功能,所以这里加以介绍。
TailwindCSS 支持深色模式,可以根据不同的主题(如浅色和深色)来自动切换颜色方案。它提供了一组内置的类名来实现深色模式的切换,例如 dark: 和 light: 前缀。我们可以将这些类名与我们定义的颜色类名组合,生成深色模式的CSS类名。例如:
<!-- 按钮 -->
<button class="bg-primary text-white dark:bg-dark-primary dark:text-white">Button</button>
不过 TailwindCSS 默认并没有开启深色模式的功能,若我们期望它根据系统的设置自动变更主题色,则需要在 tailwind.config.js 中增加 darkMode: media 的配置,若使用手动变更主题色,则配置项为 darkMode: class,如下所示:
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
// 浅色模式下的颜色
primary: '#1e88e5',
secondary: '#ffca28',
// 深色模式下的颜色
'dark-primary': '#90caf9',
'dark-secondary': '#ffe082',
},
// 深色模式样式配置
darkMode: 'class', // 可选值:'media'、'class'、false
// 其他TailwindCSS配置...
},
},
};
当我们选择手动配置主题模式,我们可以通过浏览器媒体查询的 prefers-color-scheme 属性来获取系统的主题模式。在模式变更为深色时,手动给 html文档 增加 dark 的类名。这里我们依然将其封装为 src/hooks/useTheme.ts 的 Hooks,其完整代码如下:
import { AppStore } from "@/store/App";
import { useContext, useEffect, useState } from "react";
const useTheme = () => {
const { theme } = useContext(AppStore);
const [type, setType] = useState<"light" | "dark">("light");
useEffect(() => {
if (theme === "auto") {
const type = window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
setType(type);
return;
}
setType(theme);
}, [theme]);
useEffect(() => {
if (type === "dark") {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}, [type]);
return type;
};
export default useTheme;
chatgpt是一个用于在Node.js环境中与ChatGPT模型进行交互的JavaScript库。它提供了一组API,使得开发者可以方便地在Node.js环境中使用ChatGPT模型,从而实现自然语言处理功能。
具体来说,chatgpt库提供了sendMessage函数,可以向ChatGPT模型发送消息,并返回模型生成的响应消息。开发者可以利用这个函数实现多种自然语言处理应用,如聊天机器人、语音助手等。
它提供了以下配置项:
modelName —— 必填项,指定要使用的ChatGPT模型名称。可以从OpenAI或Hugging Face等平台下载或训练自己的模型。api_key —— 必填项,指定使用的API密钥,用于与ChatGPT API服务器进行通信。temperature —— 可选项,设置生成响应的温度值,范围为0到1,默认值为1。较高的温度会导致生成的响应更加随机和多样化。maxTokens —— 可选项,设置生成响应的最大标记数,默认值为50。如果生成的响应超过了这个标记数,则会被截断。stop —— 可选项,设置生成响应的停止词列表,以字符串数组的形式指定。当模型生成的响应中包含任何停止词时,会停止生成响应。在我们的项目中,新增了 src/service/chatgpt.ts 的服务模块,该模块提供了几个不同的方法,分别用于初始化配置、发送消息给 OpenAI以及发送数据给前端。 其中初始化配置部分包含了读取环境变量,并根据不同的变量去初始化不同的 chatgpt实例,另外考虑到国内的情况,也新增了代理的方式来保证能够访问到墙外的API。
这里给出 installChatGPT 方法的实现,具体逻辑在下文注释中,完整代码可以在这里查看。
export const installChatGPT = async () => {
// 在severless函数会重复执行install,所以避免重复初始化实例
if (api) {
return;
}
// 必须得填写一个API的token,否则就报错
if (!process.env.OPENAI_API_KEY && !process.env.OPENAI_ACCESS_TOKEN)
throw new Error("Missing OPENAI_API_KEY or OPENAI_ACCESS_TOKEN environment variable");
// More Info: https://github.com/transitive-bullshit/chatgpt-api
// 如果是调用官方的API接口,则需要API_KEY
if (process.env.OPENAI_API_KEY) {
const OPENAI_API_MODEL = process.env.OPENAI_API_MODEL;
// 模型选择,默认是gpt-3.5-turbo
const model =
typeof OPENAI_API_MODEL === "string" && OPENAI_API_MODEL.length > 0
? OPENAI_API_MODEL
: "gpt-3.5-turbo";
const options: ChatGPTAPIOptions = {
apiKey: process.env.OPENAI_API_KEY,
completionParams: { model },
debug: false,
};
// API请求地址,默认的就是官方提供的,一般不需要修改
if (process.env.OPENAI_API_BASE_URL && process.env.OPENAI_API_BASE_URL.trim().length > 0)
options.apiBaseUrl = process.env.OPENAI_API_BASE_URL;
// 新增的代理模式,使用socks-proxy-agent库结合node-fetch库实现
if (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT) {
const agent = new SocksProxyAgent({
hostname: process.env.SOCKS_PROXY_HOST,
port: process.env.SOCKS_PROXY_PORT,
});
// @ts-ignore
options.fetch = (url, options) => fetch(url, { agent, ...options });
}
api = new ChatGPTAPI({ ...options });
apiModel = "ChatGPTAPI";
} else {
// 使用非官方的代理方式来访问,如Azure云服务等透传请求
const options: ChatGPTUnofficialProxyAPIOptions = {
accessToken: process.env.OPENAI_ACCESS_TOKEN,
debug: false,
};
// 跟上面一样的
if (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT) {
const agent = new SocksProxyAgent({
hostname: process.env.SOCKS_PROXY_HOST,
port: process.env.SOCKS_PROXY_PORT,
});
// @ts-ignore
options.fetch = (url, options) => fetch(url, { agent, ...options });
}
// 第三方代理的接口
if (process.env.API_REVERSE_PROXY)
options.apiReverseProxyUrl = process.env.API_REVERSE_PROXY;
api = new ChatGPTUnofficialProxyAPI({ ...options });
apiModel = "ChatGPTUnofficialProxyAPI";
}
};
对于 NextJS 项目,官方提供了很方便的使用 npm run build && npm run start 的方式来构建及部署项目,但是因为我们没有自己的服务器,所以期望使用一些一键部署的云服务来进行。
在使用 Railway 之前,我还尝试了 Vercel、Deno、Zeabur等一些其他的云服务。Vercel 是 NextJS 官方成员做的项目,所以对于 Next 项目的支持非常好,而且提供了很便利和个性化的功能,但问题在于 Vercel 的免费版限制了 API 的响应时长最大为 10s,就导致我们的接口还没响应完成就直接结束了;Deno 的话我其实有点没太用明白,感觉想要成功部署还需要对项目做一些改造,所以尝试之后就放弃了;Zeabur 看了一下是今年新出的创业团队的项目,是国内的几位同仁做的,界面挺炫酷,不过功能上还是要弱一些,没办法支持自定义 install 命令。
以上这几个都是很不错的云服务平台,非常适合部署一些小的以及创业团队的项目,而且都有免费的额度,有兴趣的同学可以去体验一下。
回到正题。
Railway 是一个基于云平台的开发工具,旨在为开发者提供方便、高效和可靠的应用部署和管理服务。它支持多种编程语言和框架,如Node.js、Python、Ruby等,可以轻松地将应用程序部署到云端并进行自动化管理。
使用Railway,开发者可以省去繁琐的服务器配置、数据库设置和应用部署等步骤,专注于应用程序的开发和迭代。同时,Railway还提供了丰富的功能和工具,如日志监控、性能分析、CI/CD集成等,可帮助开发者更好地管理和优化应用程序。
这里我们不详细介绍 Railway 的过多功能(那样可能要讲好久好久??),只介绍在我们的项目中如何使用 nixpacks 以及 Docker 两种方式来进行部署。
另外最最最重要的一点,使用 Railway 部署时,由于其服务器在国外,所以可以直接访问国外的服务,并且不会被第三方服务认为是的请求,所以不需要?就可以访问 OpenAI。同时 Railway 提供的地址在?内也是可以正常访问的!
进入 railway.app/ ,点击按钮「Start a New Project」,选择从 Github 部署,也可以选择从模板部署或者创建空的项目。之后选择 Ghitub 的项目仓库,确定后 Railway 将自动完成以下几个步骤:创建项目、在项目中新增 Service、编译与部署 Github 项目到 Service 中。此时会进入到项目的 Service 配置界面,如下所示:

当我们的项目中没有 Dockerfile 文件时,Railway 默认会使用 nixpacks 的方式进行项目部署,在 Service 的 Settings 选项卡中可以看到当前项目的部署方式。(由于我的项目是用 Docker 方式部署的,所以另找了一个项目作为示例)

nixpacks 是一个方便、高效和可靠的Railway应用程序构建和部署工具,可以帮助开发者轻松地构建和部署各种类型的应用程序。使用 nixpacks,开发者可以专注于应用程序的开发和迭代,从而提高开发效率和开发体验。我们这里不会深入 nixpacks 的用法和原理,主要介绍如何通过网页交互进行部署。
通常我们开启 nixpacks 方式部署后,nixpacks 会自动分析项目类型,并找到对应的项目构建及启动命令。如在前端及 Node 项目中,nixpacks 会使用 package.json 中的 build去执行项目构建以及使用 start 命令去启动构建完成后的项目。
当我们的项目的构建及启动命令不是 build 和 start 时,我们可以在 Settings 面板中找到 Deploy 和 Build 模块,在这两个模块下,可以通过自定义的方式来修改对应的命令。

正常在项目执行完 build 和 start 之后,便部署成功了,Railway 会为其生成一个专属的域名,我们可以直接通过这个域名访问项目,也可以绑定我们自己的域名。域名的查看及自定义都在 Setting 选项下的 Domains 模块。

在 Railway 上,可以通过以下步骤来绑定自定义域名:
myapp-1234abcd.railway.app,则需要将您的自定义域名 www.example.com 指向 myapp-1234abcd.railway.app。完成上述步骤后,就可以使用自定义域名访问应用程序了。请注意,DNS 记录可能需要一些时间来生效,通常需要几分钟或几个小时的时间。
另外在项目创建时以及在 Service 的 Variables 面板中我们都可以添加项目运行需要的环境变量,这里不再详细介绍。
nixpacks 帮我们做了很多工作,但封装程度越高,就意味着我们可以自定义的内容就越少,因此本项目最终也是采用了 Railway 提供的第二种部署方式 —— Docker部署。
当使用 Docker 方式部署时,我们依然需要先在平台上创建项目和服务(当然也可以用 CLI 工具),之后便只需要专注于书写项目的 Dockerfile 文件即可。
在项目的根目录创建 Dockerfile 文件,并填入内容如下:
# Install dependencies and run build
FROM node:alpine AS deps
WORKDIR /app
COPY package.json package-lock.json /app/
RUN npm install --legacy-peer-deps
COPY . /app
RUN npm run build
# Production image, copy all the files and run next
FROM node:alpine AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
COPY --from=deps /app/public ./public
COPY --from=deps --chown=nextjs:nodejs /app/.next ./.next
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/package.json ./package.json
USER nextjs
EXPOSE 3000
ENV PORT 3000
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry.
# ENV NEXT_TELEMETRY_DISABLED 1
CMD ["node_modules/.bin/next", "start"]
在上述的 Dockerfile 文件中我们基于 node 的基础镜像来制作,在镜像中创建 /app 目录作为我们的项目目录,复制 package.json 文件到镜像中后执行安装依赖的命令,之后将项目中的文件全部拷贝进镜像并执行构建。在下一个阶段中我们新建了单独的用户并从上个阶段复制构建产物到 /app 目录下,最后设置镜像使用 3000 端口并规定启动命令为 node_modules/.bin/next start。
当我们将 Dockerfile 文件提交到项目仓库时,会自动触发 Railway 中的构建及部署,构建部署流程即为镜像的制作和容器启动流程。需要注意的是,镜像容器启动时会将端口映射为 80 端口,所以使用域名访问时不需要再指定端口。
至此,我们已经实现从零开发并自动化部署我们的个人专属 ChatGPT!再加上 ChatGPT 提供的免费的 API Secret Key,终极白嫖的感觉针不戳 ?? ~
彩蛋一:本项目的部分代码是由 ChatGPT 生成的。


彩蛋二:本文的部分内容是由 ChatGPT 生成的。








当我使用Bundler时,是否需要在我的Gemfile中将其列为依赖项?毕竟,我的代码中有些地方需要它。例如,当我进行Bundler设置时:require"bundler/setup" 最佳答案 没有。您可以尝试,但首先您必须用鞋带将自己抬离地面。 关于ruby-我需要将Bundler本身添加到Gemfile中吗?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/4758609/
我注意到像bundler这样的项目在每个specfile中执行requirespec_helper我还注意到rspec使用选项--require,它允许您在引导rspec时要求一个文件。您还可以将其添加到.rspec文件中,因此只要您运行不带参数的rspec就会添加它。使用上述方法有什么缺点可以解释为什么像bundler这样的项目选择在每个规范文件中都需要spec_helper吗? 最佳答案 我不在Bundler上工作,所以我不能直接谈论他们的做法。并非所有项目都checkin.rspec文件。原因是这个文件,通常按照当前的惯例,只
我实际上是在尝试使用RVM在我的OSX10.7.5上更新ruby,并在输入以下命令后:rvminstallruby我得到了以下回复:Searchingforbinaryrubies,thismighttakesometime.Checkingrequirementsforosx.Installingrequirementsforosx.Updatingsystem.......Errorrunning'requirements_osx_brew_update_systemruby-2.0.0-p247',pleaseread/Users/username/.rvm/log/138121
这个问题在这里已经有了答案:关闭10年前。PossibleDuplicate:Rubysyntaxquestion:Rational(a,b)andRational.new!(a,b)我正在阅读ruby镐书,我对创建有理数的语法感到困惑。Rational(3,4)*Rational(1,2)产生=>3/8为什么Rational不需要new方法(我还注意到例如我可以在没有new方法的情况下创建字符串)?
英文版英文链接关注公众号在“亚特兰蒂斯的回声”中踏上一段难忘的冒险之旅,深入未知的海洋深处。足智多谋的考古学家AriaSeaborne偶然发现了一件古代神器,揭示了一张通往失落之城亚特兰蒂斯的隐藏地图。在她神秘的导师内森·兰登教授的指导和勇敢的冒险家亚历克斯·默瑟的帮助下,阿丽亚开始了一段危险的旅程,以揭开这座传说中城市的真相。他们的冒险之旅带领他们穿越险恶的大海、神秘的岛屿和充满陷阱和谜语的致命迷宫。随着Aria潜在的魔法能力的觉醒,她被睿智勇敢的QueenNeria的幻象所指引,她让她为即将到来的挑战做好准备。三人组揭开亚特兰蒂斯令人惊叹的隐藏文明,并了解到邪恶的巫师马拉卡勋爵试图利用其古
我需要用任何语言编写一个算法,根据3个因素对数组进行排序。我以度假村为例(如Hipmunk)。假设我想去度假。我想要最便宜的地方、最好的评论和最多的景点。但是,显然我找不到在所有3个中都排名第一的方法。Example(assumingthereare20importantattractions):ResortA:$150/night...98/100infavorablereviews...18of20attractionsResortB:$99/night...85/100infavorablereviews...12of20attractionsResortC:$120/night
修改(澄清问题)我已经花了几天时间试图弄清楚如何从Facebook游戏中抓取特定信息;但是,我遇到了一堵又一堵砖墙。据我所知,主要问题如下。我可以使用Chrome的检查元素工具手动查找我需要的html-它似乎位于iframe中。但是,当我尝试抓取该iframe时,它是空的(属性除外):如果我使用浏览器的“查看页面源代码”工具,这与我看到的输出相同。我不明白为什么我看不到iframe中的数据。答案不是它是由AJAX之后添加的。(我知道这既是因为“查看页面源代码”可以读取Ajax添加的数据,也是因为我有b/c我一直等到我可以看到数据页面之后才抓取它,但它仍然不存在)。发生这种情况是因为
这个问题在这里已经有了答案:HashsyntaxinRuby[duplicate](1个回答)关闭5年前。我有一个Recipe,其中包含以下未通过lint测试的代码:service'apache'dosupports:status=>true,:restart=>true,:reload=>trueend失败并出现错误:UsethenewRuby1.9hashsyntax.supports:status=>true,:restart=>true,:reload=>true不确定新语法是什么样的...有人可以帮忙吗?
我的问题很简单:我是否必须在使用RubyonRails的类上require'csv'?如果我打开一个railsconsole并尝试使用CSVgem它可以工作,但我必须在文件中这样做吗? 最佳答案 CSVlibrary是ruby标准库的一部分;它不是gem(即第三方库)。与所有标准库(与核心库不同)一样,csv不会由ruby解释器自动加载。所以是的,在您的应用程序中某处您确实需要要求它:irb(main):001:0>CSVNameError:uninitializedconstantCSVfrom(irb):1from/Us
从一开始,我就是一个Windows高手。我从MS-DOS开始。我安装了Windows2.1以及此后的所有Windows。现在,我家里有10台不同的Windows机器在运行,从Windows7Ultimate到各种版本的WindowsServer。我还没有完成Windows8,也不想去那里。我在服务器和各种软件方面都有UNIX经验,但它并不是我的首选环境。但是,我想我正在转换。我试图假装使用Cygwin和MSYS在Windows下运行UNIX。我的目的是搭建一个开发环境。两者都让我失望了。我花了比开发更多的时间来解决一系列技术问题。这是NotAcceptable。到目前为止,我的Ruby