全栈项目开发实践:yiwei-fullstack-web
# 全栈项目开发实践:yiwei-fullstack-web
# 概述
这是一个包含前端H5、小程序、后台管理系统、服务端的全栈项目,包含以下子项目:
- taro+react小程序/5跨端项目:
mini-taro-app - koa2服务端项目:
node-backend - vue3后端admin项目:
vue-admin-web
# 技术选型
这里C端选择用Taro这一个跨端框架来开发,因为我打算C端是一套代码,多端输出,降低开发成本,所以优先考虑跨端框架;而Taro框架是目前比较知名的框架,兼容React和Vue两种语法,可以输出H5,支持微信小程序、支付宝小程序、百度小程序、头条小程序、QQ小程序等;我用的是
Taro+React来进行开发,因为刚开始Taro就是一个用React来进行开发的跨端框架,所以相较Vue,React可能支持度上会更成熟些;B端是用
Vue2+Vite来搭建的后台管理系统;主要是现在的公司都是用的React,而我之前的公司经常用的却是Vue,而我不想让自己的Vue技术生疏了,这里就选择了Vue;用Vite也是为了保持对现在前端技术发展的关注吧后端服务用的是
koa搭建的node.js服务,这个目前也没有其他更好的选择吧,毕竟我也不会python或java...
# taro-mini-app
跨端小程序/h5:taro/react
node v20+
# 小程序初始化
登录微信公众平台 (opens new window),注册,创建小程序,获取到
AppID;之后可以配置小程序的名称、图标等信息;Taro (opens new window)初始化项目,项目根目录
project.config.json下填入AppID;下载微信开发者工具 (opens new window),打开项目,编译预览,开始进行开发;
配置接口访问域名
- 的轮毂微信公众平台,进入"开发管理" → "开发设置" → "服务器域名"
- 在"request合法域名"中添加你的接口域名
https://xxx.com;域名必须是 HTTPS,域名不能使用 IP 地址或 localhost,域名必须经过 ICP 备案,一个月内最多可申请修改 5 次 project.config.json:
{
"setting": {
"urlCheck": true, // 必须设为 true,用于检查安全域名
// ... 其他配置
}
}
在开发工具中测试时,可以临时关闭 urlCheck;真机调试时必须使用已配置的合法域名;上传代码前必须将
urlCheck设为true
- 开发过程中,微信账号需要在公众平台添加
开发者权限,才能扫码登录开发者工具;
# 微信小程序登录
button:
open-type,微信开发能力 (opens new window)getUserInfo: 获取用户信息,可以从 bindgetuserinfo 回调中获取到用户匿名数据:encryptedData、iv、signature、userInfo(不包含昵称头像)getPhoneNumber: getPhoneNumber 手机号快速验证,向用户申请,并在用户同意后,快速填写和验证手机;但需完成企业认证才能用
wx.getUserProfile(Object object): 若开发者需要获取用户的个人信息(头像、昵称、性别与地区),可以通过wx.getUserProfile接口进行获取,该接口从基础库2.10.4版本开始支持,该接口只返回用户个人信息,不包含用户身份标识符; 开发者每次通过该接口获取用户个人信息均需用户确认。参考 (opens new window)- 小程序登录、用户信息相关接口调整说明官方说明 (opens new window)
- 2021 年 4 月 28 日 24 时后发布的新版本小程序,开发者调用
wx.getUserInfo或<button open-type="getUserInfo"/>将不再弹出弹窗,直接返回匿名的用户个人信息(不会返回用户昵称头像) - 自 2022 年 10 月 25 日 24 时起,小程序
wx.getUserProfile接口将被收回:生效期后发布的小程序新版本,通过 wx.getUserProfile 接口获取用户头像将统一返回默认灰色头像,昵称将统一返回 “微信用户”。参考 (opens new window) - chooseAvatar (opens new window)
名词解释
服务端调用 auth.code2Session 接口会返回:
openid: 服务端调用auth.code2Session接口,换取 用户唯一标识OpenIDunionId: 用户在微信开放平台账号下的唯一标识 UnionID,想获取unionId需要在微信开放平台进行绑定~ https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/union-id.htmlsession_key: 标识当前用户在微信上使用你的小程序的 session 信息;access_token: 小程序全局唯一后台接口调用凭据,后端调用绝大多数后台接口时都需使用。开发者可以通过getAccessToken接口获取并进行妥善保存。;https://developers.weixin.qq.com/miniprogram/dev/framework/server-ability/backend-api.html#access_token
两种登录方式
- openid 登录
- 静默登录:
wx.login(前端) + jscode2session(后端)获取openid、session_key、unionid - 后端:
jwt + openid生成 token 返回给前端,以 openid 为 key,新增用户; - 前端:本地缓存
token,以后需要鉴权的接口请求带上 token 信息; - 前端:调微信提供的
头像昵称填写能力获取头像昵称(需要弹窗授权),更新用户信息;
- 手机号快捷登录(需企业认证)
- 通过 button 按钮的
bindgetphonenumber事件,弹出手机号授权,获取到加密数据后,向后端换取 token
# 其他登录
- 账号+密码+图形验证码登录
登录时,先获取图形验证码,再通过
图形验证码+账户+密码获取token,最后通过token获取账号密码登录;
参考:koa2+svg-captcha实现登录验证码以及存储验证功能(非常详细版) (opens new window)
- 手机号+短信验证码登录
短信验证服务需要选择短信服务商(如阿里云、腾讯云等),然后通过手机号+短信验证码获取token,在node-backend项目的cms.js我已经添加了部分代码,以后有时间再完善,目前账号密码登录已经能满足需求~
# h5/小程序适配
项目开发中我们直接使用
px,h5包会编译成rem,小程序会编译成rpx,需要做一些适配~
config/index.ts
designWidth(input: any) {
let file = input?.file?.replace(/\+/g, '/');
if (file?.indexOf('@antmjs/vantui') > -1) { // vantui 库
return 750
}
return 375
},
deviceRatio: { // 设备比例配置
640: 2.34 / 2,
750: 1,
375: 2,
828: 1.81 / 2
},
plugins: [
[
"@tarojs/plugin-html", // https://docs.taro.zone/docs/use-h5
{pxtransformBlackList: [/demo-/, /^body/, /^van-/]} // 包含 `demo-` 的类名选择器中的 px 单位不会被解析
],
],
具体配置看源码~
postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
// tailwindcss 里面工具类的长度单位,默认都是 rem, 在 h5 环境下自适应良好。但小程序里面,我们大部分情况都是使用 rpx 这个单位来进行自适应,所以就需要把默认的 rem 单位转化成 rpx。
"postcss-rem-to-responsive-pixel": {
// H5环境下1rem = 16px,小程序环境下1rem = 32rpx
rootValue: process.env.TARO_ENV === "h5" ? 16 : 32,
// 默认所有属性都转化
propList: ["*"],
// 转化的单位,可以变成 px / rpx
transformUnit: "rpx",
},
},
};
# 代理配置
- 接口调用:
export const _aiChat = (params: any) => {
return postJSON('/api/ai/chat', params);
};
- axios配置:
// src/services/axios.js
const service = axios.create({
timeout: 20000,
baseURL: ENV === 'h5' ? '' : process.env.TARO_APP_SERVE_URL
// h5的baseURL为空,小程序开发走配置的合法域名
});
- env配置:
# .env.development
TARO_APP_SERVE_URL = "https://www.verneyzhou-code.cn" # 线上环境, 这个域名需要和微信公众平台设置的合法域名一致(主要给小程序开发时使用)
TARO_APP_PUBLIC_PATH = "" # h5静态资源公共路径,开发环境默认为空
# .env.production
TARO_APP_SERVE_URL = "https://www.verneyzhou-code.cn" # 线上环境, 这个域名需要和微信公众平台设置的合法域名一致
TARO_APP_PUBLIC_PATH = "/yiwei/taro-mini-app/" # 打包后h5静态资源路径,或路由base
- config中配置H5代理:
// config/index.js
// 上面axios配置中h5的baseURL为空,h5本地接口请求会走到这里:
h5: {
// 因为taro打包之后的h5项目是个多页面项目,这里静态资源就不按照SPA应用那样配置相对路径了,不然刷新页面会找不到静态资源
publicPath: process.env.TARO_APP_PUBLIC_PATH || '/',
devServer: {
port: 10087,
proxy: [
{
context: ['/api'],
target: 'http://localhost:9527', // 代理到本地开发服务器地址
changeOrigin: true,
},
],
},
router: {
mode: process.env.TARO_ENV === 'harmony-hybrid' ? 'hash' : 'browser', // 路由模式,支持 hash、browser
basename: process.env.TARO_APP_PUBLIC_PATH || '', // 路由基地址,默认为 '/'
},
}
/**
* 这样配置完成后,开发环境下h5:
* 页面访问url为:http://localhost:10087/pages/welcome/index
* 接口请求url为:http://localhost:10087/api/ai/chat,代理到本地开发服务器地址 http://localhost:9527/api/ai/chat
* 页面静态资源访问url为:http://localhost:10087/js/xxx.js
* 生产环境下h5:
* 页面访问url为:https://www.verneyzhou-code.cn/yiwei/taro-mini-app/pages/welcome/index
* 接口请求url为:https://www.verneyzhou-code.cn/api/ai/chat
* 页面静态资源访问url为:https://www.verneyzhou-code.cn/yiwei/taro-mini-app/js/xxx.js
*
* /
- whistle代理配置: 之后浏览器中通过 whistle 代理转发到本地开发服务器:
127.0.0.1:9527 www.verneyzhou-code.cn
- 小程序开发环境代理配置:
微信开发者工具 =》 设置 =》 代理设置 =》 手动设置代理:127.0.0.1:8899,这个地址是刚才浏览器中whistle本地启动的地址;
现在就可以在小程序中使用代理请求本地后端服务了。
packageMain/home中router配置:
const TARO_ENV = process.env.TARO_ENV;
const prefix = process.env.TARO_APP_PUBLIC_PATH;
const basename =
TARO_ENV === "h5" ? `${prefix}/packageMain/home` : "packageMain/home";
// 首页入口
export default function App() {
return (
<BrowserRouter basename={basename}>
<HomePage />
</BrowserRouter>
);
}
# mock
# 备注
taro 里面普通的
div绑定addEventListener事件无效,需要用ScrollView组件进行绑定使用 tailwindcss 的样式有时不生效,微信开发者工具
styles里有,但预览没效果,暂时无解~taro 中使用 scrollIntoView 无效,换成
ScrollView组件的scrollIntoView小程序本地调接口:
- 添加安全域名:参考 (opens new window)
- https 域名如果证书过期了,需要
重新购买证书 =》下载证书 =》登录服务器 =》替换服务器原证书 =》重启nginx =》替换node服务的证书 =》重启node服务
- 配置 whistle 代理:
- whistle 开启,配置:
127.0.0.1:9527 www.verneyzhou-code.cn - 开发者工具 =》设置 =》代理设置 =》手动设置代理 =》输入
127.0.0.1:8899 开发者工具 =》详情 =》本地设置 =》勾选不校验TLS版本
- 重新编译
# 报错记录
- npm run dev:h5 报错:
Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0.
https://blog.csdn.net/weixin_68340504/article/details/144019029
- 小程序真机调试报错:
Error during evaluating file "pages/home/index.js":
ReferenceError: TextEncoder is not defined
TextDecoder 是浏览器自带的 API,在微信开发者工具中可以使用,在真机小程序环境没有这个方法,所以会报错的。
下载https://github.com/anonyco/FastestSmallestTextEncoderDecoder/blob/master/EncoderDecoderTogether.min.js放到目录里,在app.jsx文件直接引入;参考 (opens new window)
- 小程序开发报错:
Error: Minified React error #321; visit https://reactjs.org/docs/error-decoder.html?invariant=321 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
at Object.N (._node_modules_react-reconciler_cjs_react-reconciler.production.min.js:80)
react-dom.development.js:15408 Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
把
useLocation这些 hooks 放在函数组件顶部,不要放在条件语句类
提示:
[taro warn] 元素 img 的 src 属性值数据量过大,可能会影响渲染性能。考虑降低图片转为 base64 的阈值或在 CSS 中使用 base64。小程序开发报错:
TypeError: (0 , _packageMain_home_index__WEBPACK_IMPORTED_MODULE_5__.useHomePageContext) is not a functionTypeError: Cannot read property '__wxWebviewId__' of undefined
Taro.createSelectorQuery().in(this)报错
- 报错:
找不到模块“@/types/common”或其相应的类型声明。ts(2307), 参考 (opens new window)
tsconfig.json中配置:
"paths": {
"@/*": [
"src/*"
]
},
- 报错:
TypeError: Cannot read property 'length' of undefined at clearContainer (.._src_reconciler.ts:179)
toast 组件初始化需要在页面初始化完成后调用
引入@tarojs/plugin-mock 报错:
TypeError: helper.createBabelRegister is not a function微信开发者工具接口请求报错:
响应异常:>> TypeError: Cannot read property 'match' of undefined
本地调接口,需要配置代理~
Error: MiniProgramError {"errMsg":"request:fail 小程序要求的 TLS 版本必须大于等于 1.2"}
# vue-admin-web
就是一个常规的后台管理系统,具体开发不多赘述~
# 代理配置
- 接口调用:
export function getLoginCodeApi() {
return request<Login.LoginCodeResponseData>({
url: "/auth/code",
method: "get"
})
}
- axios配置:
// aixos.js
const service = axios.create({
baseURL: import.meta.env.VITE_BASE_URL, // api 的 base_url
})
- env配置:
# .env.development(开发环境)
## 后端接口地址base
VITE_BASE_URL = /api/admin
## 开发环境域名和静态资源公共路径(一般 / 或 ./ 都可以)
VITE_PUBLIC_PATH = /
# .env.production(生产环境)
VITE_BASE_URL = https://www.verneyzhou-code.cn/api/admin
VITE_PUBLIC_PATH = /yiwei/vue-admin-web/ # 打包后静态资源路径,或路由base
- vite配置:
// vite.config.js
defineConfig({
return {
base: process.env.VITE_PUBLIC_PATH // 打包后静态资源路径(保险起见,还是配绝对路径稳妥些)
// 打包之后index.html中js引入为:<script type="module" crossorigin src="/yiwei/vue-admin-web/assets/index-BoAc0P1M.js"></script>
// 线上访问地址为:https://www.verneyzhou-code.cn/yiwei/vue-admin-web/assets/index-BoAc0P1M.js
// base: './' // 如果不想单独配置静态资源路径,可以这样写,这样打包后静态资源就跟项目入口模板 index.html 平级
/////// 打包之后index.html中js引入为:<script type="module" crossorigin src="./assets/index-CUNvPOdi.js"></script>
// 线上访问地址为:https://www.verneyzhou-code.cn/[项目入口模板所在目录]/assets/index-CUNvPOdi.js
// 开发环境服务器配置
server: {
proxy: {
"/api/admin": {
target: "http://localhost:9527", // 代理到本地开发服务器地址
}
},
}
}
})
- router配置:
// router.js
export const routerConfig = {
history: createWebHistory(VITE_PUBLIC_PATH), // 路由模式
}
// 开发环境路由:http://localhost:1234/login
// 生产环境路由:https://www.verneyzhou-code.cn/yiwei/vue-admin-web/login
项目上线后需要在ngnix中配置路由~
# node-backend
- 技术栈:
koa+nodejs - ai接口库:
openai - token加密:
jsonwebtoken + crypto - 接口请求:
axios + cookie - 验证码生成:
svg-captcha - 服务管理:
pm2
# MySql
- 安装 本地 mysql, 同时安装数据库客户端 MySQL Workbench,参考 (opens new window)
- MySQL Workbench 使用教程 (opens new window)、MySQL-Workbench 数据库基本操作 (opens new window)
- mysql 教程 (opens new window)
关于Mysql本地安装的流程可参考另一篇博文:msql的安装与使用
# 报错记录
- 运行 mysql 时报错:
Error: getaddrinfo ENOTFOUND http://localhost
连接 mysql 时
http://localhost改成localhost即可~
- 云服务器上
nvm use v20.18.3报错:
[root@iz2zef9ue9eyhqrvjxs3aqz node-backend]# nvm use v20.18.3
node: /lib64/libm.so.6: version `GLIBC_2.27' not found (required by node)
node: /lib64/libstdc++.so.6: version `GLIBCXX_3.4.20' not found (required by node)
node: /lib64/libstdc++.so.6: version `CXXABI_1.3.9' not found (required by node)
node: /lib64/libstdc++.so.6: version `GLIBCXX_3.4.21' not found (required by node)
node: /lib64/libc.so.6: version `GLIBC_2.28' not found (required by node)
node: /lib64/libc.so.6: version `GLIBC_2.25' not found (required by node)
# 报错
[root@iz2zef9ue9eyhqrvjxs3aqz build]# make && make install
make: \*\*\* 没有指明目标并且找不到 makefile。 停止。
这个错误是因为服务器上的
GLIBC和GLIBCXX版本过低,无法支持 Node.js v20 版本。我们需要先升级系统库,然后再安装 Node.js:
# 检查当前 GLIBC 版本
ldd --version
# 下载并安装新版本 GLIBC
cd /usr/local/src
wget http://ftp.gnu.org/gnu/glibc/glibc-2.28.tar.gz
tar xvf glibc-2.28.tar.gz
cd glibc-2.28
mkdir build
cd build
../configure --prefix=/usr --disable-profile --enable-add-ons --with-headers=/usr/include --with-binutils=/usr/bin
make
make install
node: /lib64/libm.so.6: version `GLIBC_2.27‘ not found 问题解决方案 (opens new window)
- 报错:
# ../configure --prefix=/usr --disable-profile --enable-add-ons --with-headers=/usr/include --with-binutils=/usr/bin 时报错:
*** These critical programs are missing or too old: make compiler
*** Check the INSTALL file for required versions.
##### 解决办法:
# 首先安装 CentOS 的 SCL 源
yum install -y centos-release-scl-rh centos-release-scl
# 创建新的 repo 文件
cat > /etc/yum.repos.d/centos-sclo-rh.repo << 'EOF'
[centos-sclo-rh]
name=CentOS-7 - SCLo rh
baseurl=https://mirrors.aliyun.com/centos/7/sclo/x86_64/rh/
gpgcheck=0
enabled=1
EOF
# 清理缓存
yum clean all
# 更新缓存
yum makecache
# 重新尝试安装
yum install -y devtoolset-7-gcc devtoolset-7-gcc-c++ devtoolset-7-binutils
# 启用新版本
scl enable devtoolset-7 bash
- 服务器执行
yum install -y bison报错:bison-3.0.4-2.el7.x86_64: [Errno 256] No more mirrors to try.
这个错误表明 yum 无法找到可用的镜像源来安装 bison 包。我们可以通过以下步骤解决:
# 备份原有源
mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup
# 下载阿里云源
wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
# 清除缓存
yum clean all
# 生成缓存
yum makecache
# 安装 bison
yum install -y bison
- 我服务器上的mysql版本是
v5.6,我往表里插入中文字符时报错:Incorrect string value: '\\xE6\\x99\\xBA\\xE8\\xB0\\xB1...' for column 'label' at row 1
使用 utf8mb4 而不是 utf8,因为 utf8mb4 支持完整的 Unicode 字符集(包括 emoji);在
MySQL Workbench中修改label字段的Charset/Collation为utf8mb4 / utf8mb4_unicode_ci
- 往服务器上数据库插入数据时报错:
ERROR 1064: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'JSON NULL DEFAULT NULL
MySQL 5.7.8 及以上版本才支持 JSON 数据类型,而我的服务器mysql版本只有
v5.6,所以报错了。 可以考虑使用 TEXT 或LONGTEXT类型来存储 JSON 数据
- 调后台接口报错:
Incorrect integer value: 'obYIF5eMaS5MRNpGsgzk6We36fLM' for column 'openid' at row 1
修改
openid类型为VARCHAR(45)即可
# 开发流程
首先,
git clone下来代码库yiwei-fullstack-web;然后切换node到v20+,然后分别进入taro-mini-app、node-backend和vue-admin-web目录下,分别执行npm install安装依赖,等待node包安装完毕;h5开发的话,执行
npm run dev:h5即可,本地启动成功会在浏览器中自动打开一个地址;微信小程序开发的话,执行
npm run dev:weapp即可,之后使用微信开发者工具打开taro-mini-app项目所在目录即可;开发使用的微信号需要在小程序添加为开发者成员;后台管理系统开发的话,执行
npm run dev即可,本地启动成功会在浏览器中自动打开一个地址;后端服务开发的话稍微要复杂些:
系统偏好配置=> 打开MySQL, 启动服务;需提前安装好MySql,安装流程参考上方连接~- 打开
MySQL Workbench,连接本地数据库;看下对应数据table是否正常; - 打开
node-backend项目,npm run dev启动项目即可~
如果涉及到新的数据表添加,就需要再在
MySQL Workbench中创建新的数据表,并设置好字段类型;
# 部署
# 小程序上线
- 开发完成,上传代码;
- 小程序管理后台进行基础信息配置;
- 进入微信公众平台,版本管理,提交审核,发布版本;
小程序第一次上线前需要先去上传个人信息进行
备案,设置主营类目;之后微信小程序运营会给你打电话确认信息;然后工信部发给你发链接,进行ICP审核备案;审核通过后即可提交审核,发布版本;
发版,审核,发布
绑定多端应用:小程序绑定多端应用 (opens new window)
# h5、后台管理系统上线
这里taro打包后的h5项目和vue-admin-web打包后的后台管理系统都需要部署到nginx服务器上,流程差不多~
准备阿里云ECS服务器,安装Node.js环境,安装nginx,安装mysql数据库,安装pm2,具体可参考这里阿里云centOS服务器搭建
服务器环境配置完成,这里以 taro-mini-app 为例,根目录下创建自动化部署脚本
deploy.sh:
#!/bin/bash
# 定义变量
HOST="123.57.172.182"
USER="root"
PORT="22"
REMOTE_DIR="/root/nginx/upload/yiwei/taro-mini-app"
# 输出部署开始的信息
echo "开始部署H5项目到服务器..."
# 构建H5项目
echo "开始构建H5项目..."
npm run build:h5
if [ $? -ne 0 ]; then
echo "构建失败,退出部署"
exit 1
fi
# 检查dist/h5目录是否存在
if [ ! -d "dist/h5" ]; then
echo "dist/h5目录不存在,请确认构建是否成功"
exit 1
fi
# 使用rsync上传文件到服务器
echo "开始上传文件到服务器..."
# 使用rsync命令同步文件
# -a: 归档模式,保留所有文件属性
# -v: 显示详细信息
# -z: 传输时进行压缩
# --delete: 删除目标目录中有而源目录中没有的文件
# -e: 指定使用ssh作为远程shell,并设置端口
rsync -avz --delete -e "ssh -p ${PORT}" dist/h5/ ${USER}@${HOST}:${REMOTE_DIR}
if [ $? -ne 0 ]; then
echo "文件上传失败"
exit 1
fi
echo "部署完成!"
package.json中添加部署脚本命令:"deploy:h5": "chmod +x deploy.sh && ./deploy.sh"服务器下的
nginx.conf更新配置:
# nginx.conf
server {
# taro-mini-app 应用配置
location /yiwei/taro-mini-app/ {
# 使用alias指令将/yiwei/taro-mini-app/路径指向正确的静态资源目录
alias /root/nginx/upload/yiwei/taro-mini-app/;
index index.html;
# 添加了try_files指令支持前端路由,防止刷新页面时出现404错误
try_files $uri /yiwei/taro-mini-app/index.html;
# 添加一些基本的安全头
add_header X-Frame-Options "SAMEORIGIN"; # 防止点击劫持
add_header X-XSS-Protection "1; mode=block"; # 防止XSS攻击
add_header X-Content-Type-Options "nosniff"; # 防止MIME类型猜测
}
}
之后重启nginx服务:
nginx -s reload
- 最后通过taro-mini-app项目根目录下执行
npm run deploy:h5命令,即可完成部署~
部署过程中会提示让手动输入服务器密码,输入即可~
- 以上如果第一次部署的时候配置好,之后上线直接本地执行
npm run build,再执行npm run deploy就可以部署了~
deploy.sh中添加了npm run build命令,但如果直接执行npm run deploy打出来的报包不是最新的,很奇怪...
# 后端部署上线
准备阿里云ECS服务器,确定服务器ngnix和数据库已配置完成;
打开
MySQL Workbench,连接服务器数据库(需要输入账号+密码),按照本地开发时数据库新建对应的数据表;服务器根目录下创建自动化部署脚本
deploy.sh:
#!/bin/bash
# 定义服务器相关信息
HOST="123.57.172.182"
USER="root"
PORT="22"
REMOTE_DIR="/root/nginx/upload/yiwei/node-backend"
# 打印部署开始信息
echo "开始部署node-backend项目到生产环境..."
# 构建项目
echo "正在打包项目文件..."
# 创建临时部署目录
DEPLOY_DIR="deploy_tmp"
rm -rf $DEPLOY_DIR
mkdir $DEPLOY_DIR
# 复制必要的文件到部署目录
cp -r .env .env.config.js app.js config.js controller error main.js package.json pm2.config.js router service sql utils $DEPLOY_DIR/
# 连接服务器并部署
echo "正在部署到服务器..."
# 创建远程目录(如果不存在)
# ssh -p ${PORT} ${USER}@${HOST} "mkdir -p ${REMOTE_DIR}"
# 上传文件到服务器
echo "正在上传文件..."
# 使用rsync命令进行文件同步
# -a: 归档模式,保留所有文件属性
# -v: 显示详细信息
# -z: 传输时进行压缩
# --checksum: 基于校验和而不是时间戳来决定文件是否需要传输
# --delete: 删除目标目录中有而源目录中没有的文件
# --exclude: 排除node_modules目录
# -e: 指定使用ssh协议并设置端口
rsync -avz --checksum --delete --exclude='node_modules' -e "ssh -p ${PORT}" $DEPLOY_DIR/ ${USER}@${HOST}:${REMOTE_DIR}
# 清理临时部署目录
rm -rf $DEPLOY_DIR
echo "正在启动服务..."
# 在服务器上安装依赖并启动服务(首次或需要重新下载node包)
# ssh -p ${PORT} ${USER}@${HOST} "cd ${REMOTE_DIR} && \
# npm install && \
# pm2 update && pm2 start pm2.config.js && pm2 list"
ssh -p ${PORT} ${USER}@${HOST} "cd ${REMOTE_DIR} && pm2 update && pm2 reload YiweiNodeServer && pm2 list"
# 清理临时部署目录
rm -rf $DEPLOY_DIR
echo "部署完成!"
- 服务器
nginx.conf配置更新:
# https 配置
server {
# Node.js API 反向代理配置
location /api/ {
proxy_pass http://localhost:9527/; # 代理转发到本地node服务端口
proxy_http_version 1.1; # 使用HTTP 1.1协议
proxy_set_header Upgrade $http_upgrade; # WebSocket支持
proxy_set_header Connection 'upgrade'; # WebSocket连接升级
proxy_set_header Host $host; # 设置主机头
proxy_cache_bypass $http_upgrade; # 绕过缓存
proxy_set_header X-Real-IP $remote_addr; # 传递真实IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 传递代理链路信息
# 允许跨域请求
add_header Access-Control-Allow-Origin *; # 允许所有来源
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS, PUT, DELETE'; # 允许的HTTP方法
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; # 允许的请求头
# 处理 OPTIONS 请求
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin *; # 预检请求跨域支持
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS, PUT, DELETE';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
add_header Access-Control-Max-Age 1728000; # 预检请求缓存时间
add_header Content-Type 'text/plain charset=UTF-8';
add_header Content-Length 0;
return 204; # 返回无内容状态码
}
}
}
- 服务器根目录下添加
pm2.config.js文件:
module.exports = {
apps: [
{
name: 'YiweiNodeServer', // 应用名称
script: './main.js', // 入口文件
},
],
};
package.json中添加命令:"deploy": "chmod +x deploy.sh && ./deploy.sh"在服务器根目录下执行
npm run deploy命令,即可完成部署~
部署过程中会提示让手动输入服务器密码,输入即可~
如果在
deploy.sh执行了重新安装包部署,有时服务会启动错误,接口报502;这时需要通过ssh登录服务器nvm use v16切换node版本,然后npm install手动再安装一遍,然后再执行pm2 start pm2.config.js启动服务才可以~
有时node切到
v16之后执行pm2会报sh: pm2: 未找到命令,但重新退出登录服务器就可以了,很奇怪...
# 备注
# mysql中如何保存数组?
使用 JSON 类型:
- sql:
ALTER TABLE admin.chat_table MODIFY COLUMN messages JSON - js:
// 存储时无需 JSON.stringify,直接存储数组 const statement = ` INSERT INTO admin.chat_table (chat_id, model, messages, creator, create_time) VALUES (?, ?, ?, ?, ?) `; await connection.execute(statement, [chat_id, model, messages, creator, time]);- sql:
使用字符串分隔(简单场景)
-- 定义类型
`ALTER TABLE admin.chat_table MODIFY COLUMN messages TEXT`
-- 存储时用分隔符连接
INSERT INTO chat_table (messages) VALUES ('item1,item2,item3');
-- 查询时用 SUBSTRING_INDEX 分割
SELECT SUBSTRING_INDEX(messages, ',', 1) as first_item FROM chat_table;
- 使用独立的关联表(标准范式):
-- 主表
CREATE TABLE chats (
chat_id VARCHAR(50) PRIMARY KEY,
model VARCHAR(50)
);
-- 消息表
CREATE TABLE chat_messages (
id INT AUTO_INCREMENT PRIMARY KEY,
chat_id VARCHAR(50),
message_content TEXT,
message_type VARCHAR(20),
created_at TIMESTAMP,
FOREIGN KEY (chat_id) REFERENCES chats(chat_id)
);
# 高并发情况下怎么保证生成id的唯一性?
- Redis 自增 + 前缀
- 数据库自增 + 分段锁
- 雪花算法(Snowflake)
高并发场景下的最佳实践:
- 使用
分布式 ID 生成器:雪花算法(推荐)、UidGenerator(百度开源)、Leaf(美团开源) - 预分配机制:批量获取 ID 范围、本地缓存部分 ID
- 多重保障:数据库唯一索引、业务层去重检查、分布式锁
- 监控告警:ID 生成速率监控、时钟回拨检测、容量预警
- 容灾方案:多机房部署、快速故障转移、降级策略
选择建议:
- 并发量不大:
UUID 或时间戳+随机数 - 中等并发:
Redis 自增方案 - 高并发:
雪花算法或专业的分布式 ID 生成服务
# 查看ECS服务器上数据库所占内存大小
top -p `pidof mysqld` # 使用top命令
ps aux | grep mysql # 使用ps命令
du -sh /var/lib/mysql/ # 查看数据库文件大小
# koajwt怎么解析用户token
- 工作原理:
koa-jwt中间件会自动检查请求头中的Authorization字段或cookie中的token- 验证
token是否有效(使用配置的secret密钥) - 如果验证通过,会将解码后的
payload信息挂载到ctx.state.user上
- 基本使用方式:
// 配置 jwt 中间件
app.use(
koajwt({
secret: config.JWT_PRIVATE_KEY, // 用于验证token的密钥
cookie: 'token', // 从cookie中获取token
// cookie: 'yiwei-admin-web-token-key', // 自定义的cookie名称
key: 'user', // 解析后的用户信息存储在ctx.state.user中
tokenKey: 'token' // header中的token键名
}).unless({
// 不需要验证token的路由
path: ['/api/auth/login', '/api/public']
})
);
- 前端传递token方式:
// 方式1:通过 Authorization 请求头(推荐)
fetch('/api/data', {
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...' // Bearer + 空格 + token
}
});
// 方式2:通过 cookie(需要配置 cookie: 'token')
import Cookies from "js-cookie"
Cookies.set('token', token)
// token会自动从cookie中读取,无需特殊处理
- 在路由中使用解析后的用户信息:
router.get('/profile', async (ctx) => {
// token验证通过后,可以从ctx.state.user中获取用户信息
const userInfo = ctx.state.user;
// userInfo中包含了token的payload部分,例如:
// {
// id: 123,
// username: 'test',
// iat: 1516239022,
// exp: 1516242622
// }
ctx.body = {
code: 0,
data: userInfo,
message: 'success'
};
});
- token 生成示例:
const jwt = require('jsonwebtoken');
// 登录接口生成token
async function login(ctx) {
// 验证用户名密码...
const token = jwt.sign(
{
id: user.id,
username: user.username
},
config.JWT_PRIVATE_KEY, // 加密密钥
{ expiresIn: '24h' } // token有效期
);
// 后端设置 Cookie(推荐) :在登录接口中通过设置响应头来添加 cookie:
ctx.cookies.set('token', token, {
maxAge: 24 * 60 * 60 * 1000, // // cookie过期时间,这里设置24小时
httpOnly: true // 防止XSS攻击, 前端 JavaScript 无法读取或修改该 cookie,这是一个安全特性
path: '/', // cookie生效的路径,'/' 表示这个 cookie 在整个域名下都可以被访问
secure: process.env.NODE_ENV === 'production', // 生产环境下只在https中传输
sameSite: 'strict' // 防止CSRF攻击, cookie 只会在用户直接访问原站点时发送;
// - 从其他站点链接过来时不会发送 cookie
// - 例如:从邮件链接或其他网站跳转过来时,不会带上这个 cookie
});
ctx.body = {
code: 0,
data: { token },
message: '登录成功'
};
}
// 当后端通过 ctx.cookies.set() 设置 cookie 后,浏览器会自动将这个 cookie 保存在当前域名下,前端不需要做任何额外操作,这是因为:
// - 当服务器返回响应时,会在响应头中包含 Set-Cookie 字段
// - 浏览器识别到这个响应头后,会自动将 cookie 存储在当前域名下
// - 之后该域名下的所有请求都会自动带上这个 cookie
// 也可以前端拿到token后,手动设置cookie:
// 使用js-cookie库
import Cookies from 'js-cookie';
function login() {
fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify(loginData)
})
.then(res => res.json())
.then(data => {
// document.cookie = `token=${data.token}; path=/; max-age=86400`;
Cookies.set('token', data.token, {
expires: 1, // 1天后过期
path: '/'
});
});
}
# 前后端的credentials跨域设置
// 后端
app.use(cors({
credentials: true, // 允许跨域请求携带凭证(cookie)
// - 表示允许跨域请求携带凭证信息(如 cookies、HTTP 认证及客户端 SSL 证明等)
// - 如果设置为 false ,即使前端设置了 credentials: 'include' ,浏览器也不会发送 cookie
origin: 'http://www.example.com' // 如果是跨域请求,必须指定具体域名,不能用 *
// ... 其他配置
}));
// 这个设置只影响跨域请求,对同源请求没有任何影响。同源请求会始终携带 cookie,这是浏览器的默认行为。
// 前端
fetch('http://api.example.com/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(loginData)
credentials: 'include', // 告诉浏览器发送凭证
// - include' : 总是发送凭证(即使是跨域请求)
// - 'same-origin' : 只有当请求同源时才发送凭证(默认值)
// - 'omit' : 从不发送凭证
// ... 其他配置
});
// 在使用 axios 时:
// - axios.withCredentials = true 等价于 fetch credentials: 'include'
// - axios.withCredentials = false 等价于 fetch credentials: 'same-origin'
# 对象存储
对象存储,Object-Based Storage System,云存储服务,国内有阿里云的OSS,腾讯云的对象存储COS,百度的BOS等。
文件上传,在实际生产环境下,需要将文件上传到云存储(如阿里云OSS、腾讯云COS等),而不是直接存储在服务器本地。
根据oss bucket (opens new window) 的地域选择对应的
region, OSS地域和访问域名 (opens new window)权限管理 (opens new window)中添加OSS权限;
代码中安装阿里云OSS的SDK,并编写接口,Node.js快速入门 (opens new window)
接口编写完后,可在前端调接口上传文件验证
上传之后的文件可以在这里查看: Bucket列表 (opens new window),
Bucket配置 => 域名管理下可以配置自定义域名,可以通过CNAME关联域名;如果配置了secure: true, 启动https, 需添加证书- 跨域配置:
数据安全 > 跨域设置:
添加以下CORS规则:
允许的来源:* 或指定的域名(例如:https://static.example.com)
允许的方法:GET
允许的头部:Content-Type
暴露的头部:ETag
# 其他
nvm alias default 16:切换默认node版本在前端和后端中的数据存储?
- 比如前端页面中声明一个变量,用户访问,这个变量只在当前页面中存在;
- node后端中声明一个变量,用户访问,这个变量在当前服务器中存在,其他用户访问,这个变量也存在;需要考虑高并发问题?