文章里很多都是配置,可以直接先直接复制过去使用,或者直接下载源代码运行,先跑通项目。
- package version
| package | version |
|---|---|
| koa | ^2.13.4 |
| sequelize | ^6.14.1 |
| vite | ^2.7.13 |
| vue | ^3.2.25 |
| pinia | ^2.0.6 |
- 提前准备
- 1 台线上服务器,百度云、阿里云等等,并安装好 docker(教程自行搜索,很简单)。没有的用本地虚拟机代替,自行研究
- 注册一个 dockerhub 账户,有阿里云服务器的会自带容器镜像服务可以用,可以不用注册 dockerhub
- 注册一个 github 账户,并创建一个仓库。文章以 github 为例,gitee 的自行研究
项目目录结构简单,前端应用放在
web目录内,数据库放在mysql目录内,其它的是后台koa应用
├── docker-compose.yml # docker-compose配置文件
├── Dockerfile # 后台koa的容器镜像配置文件
├── log # 日志存储
| ├── access.log-2022-01-28.log
| ├── application.log-2022-01-28.log
| └── mysql.log-2022-01-28.log
├── middleware # koa中间件
| ├── handleGlobalError.js
| ├── index.js
| └── logClientDevices.js
├── mysql # mysql配置文件
| ├── create_db.sql # 创建数据库sql
| ├── Dockerfile # mysql的容器镜像配置文件
| ├── initial_data.sql # 初始化数据表sql
| ├── privileges.sql # 修改mysql账号权限密码
| └── setup.sh # 容器启动执行的shell
├── package-lock.json
├── package.json
├── public # koa-static静态文件配置目录
| ├── favicon.ico
| └── index2.html
├── README.md
├── routes # @koa/router的接口路由
| ├── todoList.js
| └── uuid.js
├── server.js # koa的启动文件
├── util
| ├── db.js # mysql连接配置
| └── logger.js # koa-log4的日志配置
└── web # vue前端应用
├── Dockerfile
├── index.html
├── nginx.conf
├── package.json
├── public
├── README.md
├── src
| ├── App.vue
| ├── assets
| ├── components
| ├── main.js
| ├── services # 接口
| └── store # Pinia状态管理
├── vite.config.js
└── yarn.lock
当然要用最近炒的火热的 Pinia 状态管理 + vue3 来写 todolist 应用了
3.1 Pinia 状态管理快速上手,对着文档开撸
// src/store/index.jsimport{defineStore}from"pinia";import*astypesfrom"./types";exportconstuseTodosStore=defineStore("todos",{state: ()=>({/** @type {{ msg: string, id: string, is_finished: boolean, create_time: date }[]} */todos: [],filter: types.ALL,nextId: 0,}),getters: {finishedTodos(state){returnstate.todos.filter((todo)=>todo.is_finished);},unfinishedTodos(state){returnstate.todos.filter((todo)=>!todo.is_finished);},filterTodos(state){if(this.filter===types.FINISHED){returnthis.finishedTodos;}elseif(this.filter===types.UNFINISHED){returnthis.unfinishedTodos;}returnthis.todos;},},actions: {addTodos({ id, msg, create_time }){this.todos.unshift({ id, msg, create_time,is_finished: false});},finishedOneTodo(obj){constindex=this.todos.findIndex((item)=>item.id===obj.id);this.todos.splice(index,1,{
...obj,is_finished: true,});},deleteOne(id){this.todos=this.todos.filter((item)=>item.id!==id);},setInitialData(arr){this.todos=[...arr];},},});报错部分:
上午11:02:22 [vite] new dependencies found: element-plus/es, element-plus/es/components/option/style/css, updating...
上午11:02:22 [vite] Failed to load source map for /node_modules/.vite/chunk-TPOPRDHF.js?v=e12284c2.
> error: Failed to write to output file: open D:\my\resource\Vue.js\vue3+Pinia\node_modules\.vite\element-plus.js: Access is denied.
上午11:02:37 [vite] error while updating dependencies:
Error: Build failed with 1 error:
error: Failed to write to output file: open D:\my\resource\Vue.js\vue3+Pinia\node_modules\.vite\element-plus.js: Access is denied.
at failureErrorWithLog (D:\my\resource\Vue.js\vue3+Pinia\node_modules\esbuild\lib\main.js:1493:15)
at D:\my\resource\Vue.js\vue3+Pinia\node_modules\esbuild\lib\main.js:1151:28
at runOnEndCallbacks (D:\my\resource\Vue.js\vue3+Pinia\node_modules\esbuild\lib\main.js:941:63)
at buildResponseToResult (D:\my\resource\Vue.js\vue3+Pinia\node_modules\esbuild\lib\main.js:1149:7)
at D:\my\resource\Vue.js\vue3+Pinia\node_modules\esbuild\lib\main.js:1258:14
at D:\my\resource\Vue.js\vue3+Pinia\node_modules\esbuild\lib\main.js:629:9
at handleIncomingPacket (D:\my\resource\Vue.js\vue3+Pinia\node_modules\esbuild\lib\main.js:726:9)
at Socket.readFromStdout (D:\my\resource\Vue.js\vue3+Pinia\node_modules\esbuild\lib\main.js:596:7)
at Socket.emit (events.js:400:28)
at addChunk (internal/streams/readable.js:290:12)
一番折腾(又是删除 node_modules 重新 install 安装,又是升级依赖到最新版本,又是检查文件夹路径有无中文...)过后,找到了解决方法:
升级 Node.js 到最新版本(当前最新版本为 16.13.2),删掉 node_modules 文件夹,重新安装依赖,启动正常
如果你使用 nvm,可以这样快捷升级 nodejs
nvm install 16.13.2
nvm use 16.13.2server {
listen 80; #配置监听端口
charset utf-8;#charset koi8-r;#access_log /var/log/nginx/host.access.log main;
location ~ /api/ { # 配置跨域转发# 注意这里的配置代理名称为docker-compose中koa容器的名称
proxy_pass http://koa_server:4000;# rewrite ^/api/(.*)$ /$1 break;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 100m;
}
location / {
root /usr/share/nginx/dist; #服务默认启动目录
index index.html index.htm; #默认访问文件
try_files $uri /index.html; # 防止浏览器刷新后,页面404
client_max_body_size 100m;
}
location =/admin {
deny all; #admin目录禁止任何人访问
}
location ~\.java$ {
deny all; #匹配所有java文件禁止访问
}
error_page 500 502 503 504 /50x.html; #错误状态的显示页面,配置后需重启
error_page 404 /404.html; #404页面location = /50x.html {
root /usr/share/nginx/html;
}
gzip on; #开启gzip
gzip_buffers 32 4k; #设置压缩所需要的缓冲区大小,以4k为单位,如果文件为32k则申请32*4k的缓冲区
gzip_comp_level 6; #gzip 压缩级别,1-9,数字越大压缩的越好,也越占用CPU时间
gzip_min_length 4000; #gizp压缩起点,文件大于4k才进行压缩
gzip_vary on; # 是否在http header中添加Vary: Accept-Encoding,建议开启
gzip_static on; #nginx对于静态文件的处理模块,开启后会寻找以.gz结尾的文件,直接返回,不会占用cpu进行压缩,如果找不到则不进行压缩
gzip_types text/xml text/javascript application/javascript text/css text/plain application/json application/x-javascript; # 进行压缩的文件类型
}使用多阶段构建,减少容器体积
FROM alpine:3.15 AS base
ENV NODE_ENV=production \
APP_PATH=/app
WORKDIR $APP_PATH
# 使用apk命令安装 nodejs 和 yarnRUN apk add --no-cache --update nodejs=16.13.2-r0 yarn
FROM base AS install
COPY package.json yarn.lock $APP_PATH/
RUN yarn install
FROM base AS build
# 拷贝上面生成的 node_modules 文件夹复制到最终的工作目录下COPY --from=install $APP_PATH/node_modules $APP_PATH/node_modules
# 拷贝当前目录的文件到工作目录(除了.dockerignore中忽略的)COPY . $APP_PATH/
RUN yarn run build
FROM nginx:alpine
WORKDIR /usr/share/nginx/dist
# 添加自己的配置 default.conf 在下面ADD nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist .
EXPOSE 80// server.jsconstpath=require("path");constKoa=require("koa");constRouter=require("@koa/router");constROOT=path.resolve(process.cwd(),"./");const{ connectMySQL }=require(path.resolve(ROOT,"./util/db"));consttodoList=require(path.resolve(ROOT,"./routes/todoList"));constapp=newKoa();constrouter=newRouter();// 加载所有子路由router.use("/api",todoList.routes(),todoList.allowedMethods());// 加载路由中间件app.use(router.routes()).use(router.allowedMethods());app.listen(4000,async()=>{awaitconnectMySQL();});// util/db.jsconstpath=require("path");const{ Sequelize }=require("sequelize");constdb=newSequelize("todolist","root","123456",{dialect: "mysql",dialectOptions: {charset: "utf8mb4",collate: "utf8mb4_unicode_ci",supportBigNumbers: true,bigNumberStrings: true,},// 这里的host,线上指向docker-compose中mysql的容器名host:
process.env.NODE_ENV==="development"
? "localhost"
: "todolist_mysql_server",timezone: "+08:00",// 东8区port: "3306",});constconnectMySQL=async()=>{try{awaitdb.authenticate();console.log("mysql连接成功");}catch(e){console.log(e);console.log("连接失败,3秒后重试");setTimeout(connectMySQL,3000);}};exports.connectMySQL=connectMySQL;exports.db=db;// routes/todolist.jsconstRouter=require("@koa/router");constdayjs=require("dayjs");constutc=require("dayjs/plugin/utc");// dependent on utc pluginconsttimezone=require("dayjs/plugin/timezone");const{ QueryTypes }=require("sequelize");constROOT=path.resolve(process.cwd(),"./");const{ db }=require(path.resolve(ROOT,"./util/db"));consttodoList=newRouter();// https://dayjs.gitee.io/docs/zh-CN/plugin/timezonedayjs.extend(utc);dayjs.extend(timezone);dayjs.tz.setDefault("Asia/Shanghai");// 列表查询todoList.get("/todoList/list",async(ctx,next)=>{constreqParams=ctx.query;// https://github.com/demopark/sequelize-docs-Zh-CN/blob/master/core-concepts/getting-started.md#promises-%E5%92%8C-asyncawaitconstselects={0: "SELECT * FROM todolist WHERE is_finished='0' ORDER BY create_time DESC;",1: "SELECT * FROM todolist WHERE is_finished='1' ORDER BY create_time DESC;",2: "SELECT * FROM todolist ORDER BY create_time DESC;",};constfilterType=reqParams.filterType||"2";try{letlist=awaitdb.query(selects[filterType],{type: QueryTypes.SELECT,});list=list.map((item)=>({
...item,create_time: dayjs(item.create_time).tz("Asia/Shanghai").format("YYYY-MM-DD HH:mm:ss"),is_finished: item.is_finished==="0" ? false : true,}));ctx.body={code: 200,data: list||[],msg: "ok",};}catch(e){console.log(e);}awaitnext();});module.exports=todoList;FROM alpine:3.15 AS base
ENV NODE_ENV=production \
APP_PATH=/www/node-server
WORKDIR $APP_PATH
# 使用apk命令安装 nodejsRUN apk add --no-cache --update nodejs=16.13.2-r0 npm
# 基于基础镜像安装项目依赖FROM base AS install
# 将当前目录的package.json 拷贝到工作目录下COPY package.json package-lock.json $APP_PATH/
RUN npm install
# 基于基础镜像进行最终构建FROM base
# 拷贝 上面生成的 node_modules 文件夹复制到最终的工作目录下# COPY命令复制文件夹的时候,不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径COPY --from=install $APP_PATH/node_modules $APP_PATH/node_modules
# 拷贝当前目录的文件到工作目录(除了.dockerignore中忽略的)COPY . $APP_PATH/
EXPOSE 4000
CMD ["npm", "run", "server"]- 数据库连接配置时区
timezone,设置为东 8 区 - 设置数据库字符集为
utf8mb4,方便后面存储表情符 dayjs库调整显示为东 8 区时间sequelize查询出来有重复项(非数据库里的数据重复),需要配置type为QueryTypes.SELECT
这里选择的版本为5.7.30版本,线上 8.0 版本未部署成功
FROM mysql:5.7.30
LABEL version="1.0.0" description="todolist MySQL 服务器"WORKDIR /mysql
ENV MYSQL_ROOT_PASSWORD=123456
# MYSQL_DATABASE=todolist# MYSQL_ALLOW_EMPTY_PASSWORD=yes# 启动脚本COPY setup.sh /mysql/setup.sh
# 创建数据库COPY create_db.sql /mysql/create_db.sql
# 初始化数据COPY initial_data.sql /mysql/initial_data.sql
# 设置密码和权限COPY privileges.sql /mysql/privileges.sql
EXPOSE 3306
CMD ["sh", "/mysql/setup.sh"]#!/bin/bashset -e
# https://xie.infoq.cn/article/a3c8ffbd34d818de010f2b0f6# 打印mysql服务的状态echo$(service mysql status)echo'1.启动mysql...'#启动mysql# service mysql stop# service mysql restart
service mysql start
# sleep 3echo'2.创建数据库...'
mysql </mysql/create_db.sql
sleep 3
echo'3.开始导入数据...'
mysql </mysql/initial_data.sql
sleep 3
echo$(service mysql status)echo'4.修改mysql权限...'
mysql </mysql/privileges.sql
sleep 3
echo'4.权限修改完毕...'# 防止container启动后退出# http://www.mayanpeng.cn/archives/121.html
tail -f /dev/nullCREATEDATABASEIF NOT EXISTS todolist;-- 使用todolist库
USE todolist;
-- 创建todolist表CREATETABLEIF NOT EXISTS todolist(id VARCHAR(50) PRIMARY KEY, create_time DATETIME UNIQUE, is_finished ENUM('0', '1') DEFAULT '0', msg VARCHAR(100) DEFAULT '--') COMMENT='todolist表' ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 打印数据库
SHOW TABLES;
-- 打印表结构DESC todolist;
-- 插入1条默认数据INSERT INTO todolist(id, create_time, is_finished, msg) VALUES(1, NOW(), '0', 'hello world');use mysql;
SELECT host, user FROM user;
-- 将数据库的权限授权给root用户,密码为123456GRANT ALL PRIVILEGES ON*.* TO 'root'@'%' IDENTIFIED BY '123456';
-- 刷新权限这一条命令一定要有:
flush privileges;在
dockerhub账号或者阿里云镜像容器服务创建 3 个仓库,分别放 mysql、koa、vue 应用,我这里创建的是todolist_mysql、koa和vite_vue3_pinia,在下面的 docker-compose.yml 配置文件中一一对应
在你的
github仓库里里配置好上面 dockerhub 的账号和密码secrets,方便于后面使用 github 仓库的 actions 推送。我这里创建的是DOCKERHUB_TOKEN放密码,DOCKERHUB_USERNAME放账户
version: "3"services:
mysql: # mysqlbuild: ./mysql# image名称为在dockerhub里创建好的名称image: 你的docker账户名/todolist_mysql:latestcontainer_name: todolist_mysql_serverrestart: always# MYSQL_ROOT_PASSWORD: "123456"node: # nodejs服务depends_on:
- "mysql"build: . # Dockerfile所在目录构建image: 你的docker账户名/koa:latestcontainer_name: koa_server# ports:# - "8001:4000"restart: always # 自动重启environment:
- NODE_ENV=productioncommand: npm run server # 覆盖容器启动后默认执行的命令vue:
depends_on: # vue 容器会保证在 node容器之后启动
- "node"build: ./webimage: 你的docker账户名/vite_vue3_pinia:latestcontainer_name: vite_vue3_piniarestart: alwaysenvironment:
- NODE_ENV=productionports:
- "8001:80"networks:
default:
external:
name: vue3-koa# .github/workflows/ci.ymlname: MySQL + Koa2 Server + Vue3 todoliston:
push:
branches: [main] # 监听main分支jobs:
build:
runs-on: ubuntu-lateststeps:
- name: Checkoutuses: actions/checkout@v2# 制作docker镜像并推送到dockerhub
- name: build and push docker imagerun: | # 这里用到上面创建的secrets变量 # 登录你的docker账号 docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_TOKEN }} # 进行docker镜像制作 docker-compose build # 推送到你的docker账户里 docker-compose push # 退出登录 docker logout等不及了,是时候进行提交代码了
git add .
git commit -m "feat: init"
git push -u origin main不出意外,在仓库的Actions里看到构建成功。
费了这么大劲,todolist 还没看到,是我的错,最后一步了,别放弃。
这就如同 git,使用 git GUI 工具和 git bash 命令行其他一样,方便容器管理操作。如果你喜欢命令行,就直接跳过这一节,直接部署容器吧。
把 git 仓库代码下载到服务器,docker compose up即可解决战斗!
7.2 登录你的服务器安装portainer,我这里之前安装的 1.24.2 版本
前提是在你的服务器上安装好 docker
- 下载
docker pull portainer/portainer:latest- 运行
注意: 这里你的服务器的安全组要开放 9000 端口
docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock --restart=always --name prtainer portainer/portainer
浏览器里打开http://你的服务器公网ip:9000,设置好账户和密码,进行登录,并且设置本地节点
刚才那 3 个镜像都要拉取
如果是推到阿里云镜像容器的,点击左侧菜单 Registry,然后进去点击 Add registry 添加你的镜像容器 url,同时可以配置Authentication,你的账号和密码,然后就可以和上面一样 pull 镜像了
我受不了,到底还有多少步,你骗我。真的马上结束了
实质是使用了 docker network,然后再把自己的容器添加到这个 network 中来
docker network create 自定义bridge名称
docker network connect 自定义bridge名称 容器名称这里我们用图形化界面操作
我真的受不了了,还没结束啊,这次真的结束了
点击 container 添加容器。这里必须和 docker-compose 中的顺序一样,先部署 MySQL,再部署 Koa,最后部署 vue 应用
进行部署配置,按顺序部署完 3 个容器,容器名称和端口必须都得和配置文件中的一样
xdm,真的结束了,是我折磨你了。一切顺利的话,3 个容器都部署成功
打开浏览器访问你的 todolist, http://你的ip:8001
因为前面创建表的时候,msg 列没有使用
utf8mb4字符集。那为什么不在建表的时候就设置好,我错了,xdm,揍我吧
觉得复杂难受的,直接跳过
docker 进入容器实战
前提是登录你的服务器
- 查看运行的容器
docker container ls- 进入容器
docker exec -it 上面mysql容器的CONTAINER ID sh- 连接 mysql
mysql -h localhost -u root -p
# 然后输入设置的密码不出意外,我们成功进入容器内,并连接上了 mysql
[root@iZbp19ftqv2b85av0b2d4qZ /]# docker exec -it a206f021c205 sh
# mysql -h localhost -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 53
Server version: 5.7.30 MySQL Community Server (GPL)
Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
- 修改 todolist 表的 msg 列属性
- 使用 todolist 库
USE todolist;- 进行修改
mysql> ALTER TABLE todolist MODIFY msg VARCHAR(100) character set utf8mb4 collate utf8mb4_unicode_ci default '--';
Query OK, 6 rows affected (0.04 sec)
Records: 6 Duplicates: 0 Warnings: 0- 验证是否修改成功
可以看到,msg 列已经修改成功了
mysql> SHOW CREATE TABLE todolist\G
***************************1. row ***************************
Table: todolist
Create Table: CREATE TABLE `todolist` (
`id`varchar(50) NOT NULL,
`create_time` datetime DEFAULT NULL,
`is_finished` enum('0','1') DEFAULT '0',
`msg`varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '--',
PRIMARY KEY (`id`),
UNIQUE KEY `create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='todolist表'1 row inset (0.00 sec)- 浏览器里输入 emoji 表情符添加测试,不出意外,已经 ok。结束了!!!








