Skip to main content

· 26 min read
Chengzihan

前言

最近在为某公司搭建网站,需要一个内容管理系统,正好之前了解了 Strapi CMS,于是决定用它来搭建这个系统。这里涉及的技术栈有:

  • Next.js:搭建前端页面
  • Tailwind CSS:编写前端样式
  • Strapi CMS:搭建后端内容管理系统,生成 API
  • Ubuntu:服务器
  • Azure:云服务器
  • PM2:Node.js 进程管理
  • Nginx:反向代理
  • ZeroSSL:免费 SSL 证书

什么?你说你不知道这些东西是干什么的?别急,往下看,这是一篇从 0 到 1 的教程。

Next.js 搭建

首先,我们需要安装 Next.js,这里我们使用 create-next-app 来创建一个 Next.js 项目。在这里我把 Next.js 的文档丢给你,你可以看看 Next.js 官方文档

npx create-next-app@latest my-site --typescript

等待安装完成后,我们就可以进入项目目录了。

cd my-site

接着可以运行 npm run dev 来启动项目,然后在浏览器中打开 http://localhost:3000,就可以看到 Next.js 的欢迎页面了。

Tailwind CSS 安装

Why Tailwind? Because it's awesome!

Tailwind 是一个 CSS 框架,它的特点是:

  • 无需编写 CSS,只需添加类名
  • 简单的响应式设计
  • 优化的 CSS 输出

Github 趋势 上我们可以看出 Tailwind CSS 的受欢迎程度:

2

接下来我们需要安装 Tailwind CSS,这里我们使用 create-tailwind-app 来创建一个 Tailwind CSS 项目。在这里我也把 Tailwind CSS 的文档丢给你, Next.js 安装 Tailwind CSS

在我们项目的根目录下运行以下命令:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

这时,脚手架会自动在项目根目录下创建一个 tailwind.config.js 文件,我们可以在这里配置 Tailwind CSS。我们需就要在 tailwind.config.js 文件中添加以下内容,主要是 content 字段中的内容:

/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
// Or if using `src` directory:
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

Strapi CMS 安装

新建一个目录,然后进入这个目录,运行以下命令:

npx create-strapi-app@latest my-project --quickstart

官方文档在此 Strapi 官方文档

待安装完毕之后,我们就可以进入项目目录了。

cd my-project

接着可以运行 npm run develop 来启动项目,然后在浏览器中打开 http://localhost:1337/admin,就可以看到 Strapi CMS 的欢迎页面了。初始界面时,会提示你创建一个管理员账号,这里我们就不多说了。请妥善保存你的管理员账号和密码。

Strapi CMS 配置

以上工作做完之后,我们就可以开始了。

汉化 Strapi CMS 面板

首先你可能想要一个中文面板的 Strapi ,那么我们可以在 src/admin/ 中创建一个 app.js 文件,然后在这个文件中添加以下内容:

export default {
config: {
locales: ["en", "zh-Hans"],
tutorials: false,
},
bootstrap() {},
};

重启项目,然后在浏览器中打开 http://localhost:1337/admin。之后你可以点击左下角头像,然后选择 Settings,在 Language 中选择 简体中文,然后点击 Save,这时你就可以看到中文的 Strapi CMS 面板了。

接口分析

试想我们有如下的需求:即创建一个新闻中心应用,它包含了新闻列表和新闻详情两个页面。新闻列表页面需要展示新闻的标题、发布时间、新闻类别的信息,新闻详情页面需要展示新闻的主要内容。

一条新闻应该包含以下字段:

  • slug:新闻的唯一标识符
  • title:新闻的标题
  • desc:新闻的主要内容
  • date:新闻的发布时间
  • from:新闻的来源(分类)

添加模型构建器

2

点击添加集合类型,然后在面板中输入模型名称,并输入 API ID ,这将决定你的 API 的 URL。

2

完成后,添加上述字段。

2

在高级设置中可以设置其默认值、是否必填、长度等。

2

对于文章内容,这里我们选用富文本编辑器类型,这样我们就可以在编辑器中直接编辑文章内容了。

模型添加完毕后,点击右上角的 保存 按钮。打开内容管理,我们可以看到我们刚刚添加的模型。

2

添加内容

点击右上角的 添加条目 按钮,我们就可以添加新闻了。

2

添加完成后,点击保存并发布,我们就可以在前端页面中看到我们刚刚添加的新闻了。

这时,来到设置页面,进入角色列表,找到 public 角色,然后点击进入。

2

找到刚刚创建的新闻模型的 API ,勾选 findfindone ,然后点击右上角的 保存 按钮。在权限的设置图标上,点击,可以查看对应操作的 API 地址。点击保存。

2

接口测试

接着我们打开 http://localhost:1337/api/news ,可以看到我们刚刚添加的新闻。具体内容是一个 JSON 对象。

2

可以通过 api 测试工具,如 postman 进行测试。这里用的是 apifox

2

列表页面编写

接下来我们就可以开始编写前端页面了。我们首先在 Next.js 中创建一个页面,命名为 NewsList.tsx,用于展示新闻列表。

类型抽象

根据上面的接口测试,查看返回的结果,我们可以抽象出下面的类型:

type propType = {
id: number,
attributes: {
slug: string,
title: string,
desc: string,
date: string,
from: string,
createdAt: string,
updatedAt: string,
publishedAt: string,
}
}

状态管理

创建组件,定义一些状态用来存储新闻列表、分页页码、筛选条件等。

const [newsList, setNewsList] = React.useState<propType[]>([]); // 新闻列表
const [pageNo, setPageNo] = React.useState(1); // 当前页码
const [nowPage, setNowPage] = React.useState<propType[]>([]); // 当前页的新闻列表
const [pageCount, setPageCount] = React.useState(2); // 总页数
const [selectedArr, setSelectedArr] = React.useState<string[]>([]); // 筛选条件

接口"软编码"

接口地址不应该写死,我们应该采用软编码的方式,将接口地址前缀管理在文件中,方便后期维护。可以使用 next.config.js 来管理。

module.exports = {
env: {
API_URL: 'http://localhost:1337/api'
}
}

调用时,可以使用 process.env.API_URL 来获取。

但是我这里使用了一个自定义 Hook 来管理接口地址。创建一个 Hooks 文件夹,创建 useStrapiLink.ts 文件,用于管理接口地址。

import React from "react";
import {useState} from "react";

const useStrapiLink = () => {
const [strapiLink, setStrapiLink] = useState<string>("http://localhost:1337/api");
return strapiLink;
}

export default useStrapiLink;

使用时,可以直接引入 useStrapiLink,然后调用即可。

import useStrapiLink from "../Hooks/useStrapiLink";

const strapiLink = useStrapiLink();

接口请求

useEffect 中,我们可以使用 fetch 来请求接口。

useEffect(() => {
if (selectedArr.length == 0) {
fetch(strapiLink + "/api/news")
.then(res => res.json())
.then(data => {
setNewsList(data.data); // 将请求到的数据存储在 newsList 中
setPageCount(Math.ceil(data.data.length)); // 计算总页数
})
} else {
fetch(strapiLink + "/api/news?filters[from][$in][0]=" + selectedArr[0] + '&filters[from][$in][1]=' + selectedArr[1] + '&filters[from][$in][2]=' + selectedArr[2])
.then(res => res.json())
.then(data => {
setNewsList(data.data);
setPageCount(Math.ceil(data.data.length));
})
}
}, [strapiLink, selectedArr]);

在这里,我为副作用绑定了 strapiLinkselectedArr 两个状态,也就是 strapi 的接口地址和筛选条件。当这两个状态发生变化时,才会触发副作用。

我们可以看到这里使用了 strapi 的 RESTful API,这里我使用了筛选条件,筛选条件的格式为 filters[from][$in][0]=,其中 from 是字段名,$in 是筛选条件,[0] 是数组下标,下标代表筛选条件的位次。这里我有三个条件可供筛选,并且它们之间满足的是或的关系,所以我使用了数组。

筛选使用的是 Acro Design 的 Select 组件。

 <Select style={{width: 270}} mode='multiple' placeholder="全部"
onChange={(value) => {
setSelectedArr(value);
}}
>
{options.map((option, index) => (
<Option key={index} value={option.option}>
{option.option}
</Option>
))}
</Select>

对于更多的 strapi 筛选条件,可以参考 Strapi 文档中的这个条目

可以满足绝大多数的筛选需求。

1

分页

分页的逻辑比较简单,我们只需要将 newsList 中的数据按照页码进行切割即可。

useEffect(() => {
setNowPage(newsList.slice((pageNo - 1) * 10, pageNo * 10));
}, [newsList, pageNo]);

同样是绑定了 newsListpageNo 两个状态,当这两个状态发生变化时,才会触发副作用。每次切割的长度为 10,也就是每页显示 10 条数据。

这里使用了 Acro Design 的 Pagination 组件,它的使用方法很简单,如下:

 <Pagination 
pageSize={10} // 每页显示的条数
total={pageCount} // 总条数
onChange={(pageNumber) => {setPageNo(pageNumber);}} // 页码改变时的回调函数
/>

列表渲染

由于我们动态改变了当前页面的数据,所以我们需要在 nowPage 发生变化时,重新渲染列表。

{nowPage && nowPage.map((item, index) => {
return (
<div
key={index}
className="w-full transition-all ease-in-out duration-300 grid grid-cols-9 border-b-2 border-sky-300/20 py-4 justify-start items-center"
>
<div className={`col-span-3 lg:col-span-2 flex justify-start text-gray-400`}>
{item.attributes.date}
</div>
<Link href={{pathname: '/NewsPage', query: {id: item.id}}}
className="col-span-6 flex items-center hover:underline">
<div className={`truncate text-black font-normal text-md`}>
{item.attributes.title}
</div>
</Link>
<div className={`hidden lg:block lg:col-span-1 flex justify-end text-gray-400`}>
{item.attributes.from}
</div>
</div>
)
})}

跳转到详情页

值得注意的是,这里使用了 Link 组件,它的作用是跳转到指定的页面,我们通过 query 属性传递了 id 参数,这个参数将会在详情页中使用,用来渲染指定数据的详情页的数据。

<Link href={{pathname: '/NewsPage', query: {id: item.id}}}>
// ...
</Link>

列表页的效果如下:

2

下面我们会讲到详情页的实现。

详情页的创建

详情页我们要获得指定文章的 id ,然后通过 id 去请求数据,最后渲染出来。这里创建一个 NewsPage.tsx 文件。

跳转参数 id 的获取

我们需要获取到 query 中的 id 参数,然后将其赋值给 id 状态。获取 id 参数的方法很简单,就是使用 useRouter 钩子函数,然后获取 query 中的 id 参数。

const {id} = useRouter().query;

新闻请求

我们需要通过 id 去请求数据,这里我们使用 useEffect 副作用,当 id 发生变化时,才会触发副作用。

useEffect(() => {
fetch(strapiLink + "/api/news/"+ id ).then(
res => res.json()
).then(
data => {
setArticle(data.data);
}
)
}, [strapiLink]);

渲染 Markdown

Strapi 的富文本编辑器是使用 Markdown 格式的,所以我们需要将 Markdown 转换为 HTML 格式,然后渲染出来。这里使用一个插件 react-markdown

首先安装插件:

npm install react-markdown

接着只需要传入参数即可:

<ReactMarkdown
children={article.attributes.desc
// 将 ](/uploads 替换为 ](strapiLink+/uploads
.replace(/\]\(\//g, `](${strapiLink}/`)}
remarkPlugins={[remarkGfm]}
className={`${styles.markdown} lg:pr-12`}
/>

这里也有个要点,就是 Strapi 富文本中图片的地址是 /uploads 开头的,并不会携带 URL 头,所以我们需要将其替换为 strapiLink+/uploads,这样才能正确的请求到图片。这里使用了正则表达式

remarkPlugins 插件是用来解析 Markdown 的,这里我们使用了 remark-gfm 插件,能够更好的解析 Markdown 一些高级语法。更多关于 react-markdown 可以参考官方文档

配置服务器

配置和连接

这里肯定需要一台服务器来部署 strapi,当然,也有一些一键部署的服务,如 Render,但是这里我们还是使用自己的服务器来部署。

笔者这里使用的是 Github Student Pack 提供的 Azure 服务器,这里就不多说了,建议是学生的话,可以去申请一下。除了 Azure 服务器以外,学生包中还赠送了三个域名及 SSL 证书,还有一台 DigitalOcean 的服务器,嗯,很香😋。

在 Azure 上创建实例,建议选择近一点的服务器如日本或者新加坡等。我这里使用的是 Ubuntu 镜像。

设置 SSH 访问密码。

2

然后使用 SSH 连接到服务器。这里使用 FinalShell 连接。在这里可以下载 FinalShell

点击文件夹,然后点击 新建,选择 SSH,然后输入服务器的 IP 地址,用户名,密码,端口号。

2

密码是刚才修改的密码。

2

名称任意取。这里的主机一项需要填写 IP 地址,可以在服务器控制台获取。

2

注意要在 Azure 面板放行端口号22. 否则无法连接。

2

安装 Node.js

首先安装 Node.js,注意需要安装 14.x 以上的版本。在 FinalShell 面板中输入以下命令:

curl -sL https://deb.nodesource.com/setup_18.x | sudo -E bash  -
sudo apt-get install -y nodejs

安装完成后,输入 node -v 查看版本号,如果是 14.x 以上的版本,说明安装成功。

node -v
npm -v # 查看 npm 版本

安装 Nginx

Nginx 是一个高性能的 HTTP 和反向代理服务器,这里我们使用 Nginx 来做反向代理,将请求转发到 Strapi 服务器。

apt update && apt upgrade -y &&  apt install nginx -y

安装完成后,输入 nginx -v 查看版本号,出现版本号说明安装成功。

nginx -v

如果上述过程有失败的情况,可能需要提权,在指令前加上 sudo

安装 PM2

PM2 是一个 Node.js 进程管理器,可以帮助我们管理 Node.js 进程。在进程发生异常崩溃时,PM2 可以自动重启进程。

sudo npm install -g pm2

检查是否安装成功,输入 pm2 -help,出现帮助信息说明安装成功。

pm2 -help

上传 Strapi 工程文件

在 FinalShell 中的文件管理器中,将你的 Strapi 工程,除了 node_modules 文件夹外,上传到服务器,最好先打包好(npm run build),在上传,否则小型服务器可能无法支持打包进程。一般上传到 /www/wwwroot/ 文件夹下。

2

进入到 Strapi 工程文件夹,安装依赖。

cd /www/wwwroot/backend
sudo npm install

安装完毕后,尝试运行 Strapi 服务器(npm run start),如果出现错误,可能是端口号被占用。

PM2 启动 Strapi 服务器

在 backend 文件夹下创建文件 server.js 作为入口文件。

const strapi = require('strapi');
strapi().start();

在该目录下启动 PM2 进程。

pm2 start server.js

可以使用指令查看进程是否启动成功。

pm2 list
  • 如何在没有证书和域名时测试 Strapi ?

使用 PM2 启动 Strapi 后,你就可以在 Azure 安全组中放行你的 Strapi 服务的端口,然后使用 IP 加端口的方式来访问你的 Strapi 服务。比如我这里是 1337 端口,所以我可以通过 http://<IP>:1337/admin 来访问 Strapi 的后台管理页面。当然,如果你不嫌麻烦,可以用 Nginx 配置反向代理,然后访问。

Next.js 部署

步骤类似,这里不再赘述。上传打包后的工程,安装依赖,启动 PM2 进程即可。

Nginx 配置

编辑 Nginx 配置文件。

sudo vim /etc/nginx/nginx.conf

进入后,按 i 进入编辑模式,将 http 下修改为以下内容。

server {
listen 80;
server_name xxx.com; # 你的域名
return 301 https://$server_name$request_uri; # 重定向到 https
}

server {
listen 80;
server_name admin.xxx.com; # 你的域名,用于后台管理
return 301 https://$server_name$request_uri;
}

server{
listen 443 ssl;
server_name xxx.com;
ssl_certificate /etc/nginx/cert/ssl.crt; # 你的证书路径
ssl_certificate_key /etc/nginx/cert/ssl.key; # 你的证书密钥路径
ssl_session_timeout 5m; # 会话超时时间
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; # 加密套件
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # 协议
ssl_prefer_server_ciphers on; # 优先使用服务器端加密套件

location /{
proxy_pass http://localhost:3000; # 你的 Next.js 服务器端口
proxy_http_version 1.1; # http 版本
proxy_set_header Upgrade $http_upgrade; # 设置升级头
proxy_set_header Connection 'upgrade'; # 设置连接头
proxy_set_header Host $host; # 设置主机头
proxy_cache_bypass $http_upgrade; # 设置缓存头
}
}

server{
listen 443 ssl;
server_name admin.xxx.com;
ssl_certificate /etc/nginx/cert/adminssl.crt; # 你的证书路径,另一个证书
ssl_certificate_key /etc/nginx/cert/adminssl.key; # 你的证书密钥路径,另一个证书密钥
ssl_session_timeout 5m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;

location /{
proxy_pass http://localhost:1337; # 你的 Strapi 服务器端口
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;
}
}

添加完后,保存退出。

:wq

这里监听了 80 和 443 端口,分别用于 http 和 https。我们将 80 端口的请求重定向到 443 端口,这样就可以使用 https 访问所有服务。

这里,你还没有申请域名和证书呢,所以我们暂时先别管这些,去申请证书和域名。

域名申请

域名我使用的是 GitHub Student Developer Pack 提供的域名,是 Name.com 的域名,可以免费申请一个 .live 的域名。点击下面的连接,使用 GitHub 账号登录,然后申请一个域名。

Name.com Student

输入心仪的域名,按回车提交,在下面找到想要的域名,可以发现价格都是 0 元,加入购物车。

1

接着在 My Domains 下找到你刚刚申请的域名,点击域名进入管理页面。

点击管理 DNS 记录。

2

添加两条 A 记录,一台的主机名为 @,另一台的主机名为 admin回答都指向你的服务器 IP。TTL 设置为 300 秒。

证书申请

这里我们使用 ZeroSSL 来申请免费的证书。SSL 证书分为多种,有 DV(Domain Validation)、OV(Organization Validation)、EV(Extended Validation)等,DV 证书是最简单的,只需要验证域名的所有权,OV 和 EV 证书需要验证组织的所有权,需要更多的信息。DV 证书可以申请到免费的证书,OV 和 EV 证书需要付费。

打开 ZeroSSL,点击 Get Free SSL。

2

使用邮箱注册账号。点击 New Certificate。

2

输入你的域名,点击 Next。

2

选择一张 90 天内有效的证书,点击 Next。(一年的证书需要付费)

2

接着一直点击 Next,直到新的页面出现。在验证页面,选择使用 DNS 验证。将 Name 和 Points To 的值复制下来。

2

在 Name.com 的管理页面,添加一条 CNAME 记录,主机名为 Name,值为 Points To。

2

由于有两个域名,所以申请两张证书。

添加好后,回到 ZeroSSL 的验证页面,点击 Next。接着点击验证。验证成功后,点击下载 Nginx 证书。

2

每份证书都有两个文件,一个是证书文件,一个是私钥文件。将两个文件下载下来。下载好两张证书后,将份证书(共四张)上传到服务器的 /etc/nginx/cert 目录下。没有这个目录的话,可以自己创建。

2

接着,在 Nginx 的配置文件中,修改证书的路径。

sudo vim /etc/nginx/nginx.conf
/etc/nginx/nginx.conf
ssl_certificate /etc/nginx/cert/ssl.crt;    # 你的证书路径
ssl_certificate_key /etc/nginx/cert/ssl.key; # 你的证书密钥路径

检查 Nginx 配置

待一切配置完成后,可以使用 nginx -t 来检查 Nginx 的配置是否正确。

sudo nginx -t

如果配置正确,会显示 nginx: the configuration file /etc/nginx/nginx.conf syntax is ok

接着重启 Nginx。

sudo nginx -s reload

放行端口

你需要在 Azure 的网络安全组中,放行 80 和 443 端口。这个上面放行 22 端口的时候已经讲过了,这里不再赘述。

常见问题

  • 我的 Strapi 不会返回媒体数据(如图片)?

这是因为 Strapi 默认不返回媒体数据,可以在接口后面加上 ?populate=* 来返回媒体数据。详见 文档

2

  • 在操作 Ubuntu 时,经常出现无权限的问题,这是因为你的账号不是 root 账号,所以需要使用 sudo 命令来执行命令。

  • 对于国内的服务器,可能在安装 npm 依赖时,会出现网络问题,这里可以使用 cnpm 来安装依赖。

sudo npm install -g cnpm --registry=https://registry.npm.taobao.org
  • 明明都已经配置好了,但是访问网站时,还是会出现 ERR_CONNECTION_REFUSED。这是因为你的服务器没有放行 80 和 443 等相应的端口,所以需要在 Azure 的网络安全组中,放行 80 和 443 端口。

  • Next 和 Strapi 服务启动失败。请现在本地启动 Next 和 Strapi 服务,进行测试,待没有问题后,再将服务部署到服务器上。

  • 如何在没有证书和域名时测试 Strapi ?

使用 PM2 启动 Strapi 后,你就可以在 Azure 安全组中放行你的 Strapi 服务的端口,然后使用 IP 加端口的方式来访问你的 Strapi 服务。比如我这里是 1337 端口,所以我可以通过 http://<IP>:1337/admin 来访问 Strapi 的后台管理页面。

  • 服务器部署完后 Next.js 无法请求接口?

那应该是你的软编码没改,需要将 http://localhost:1337 改为你的服务器的 IP 地址,或者你的域名。

  • 阿里云等服务器访问资源慢?

因为国内服务器限制带宽(为1-5M),而 Azure 不会,所以尽量压缩你的资源文件,比如图片、视频等。或者使用 Cloudflare 的 CDN 来加速你的网站。

  • Nginx 检查配置文件时,出现各种报错?

这是因为你的配置文件语法或者格式有问题,需要检查你的配置文件。可以在下方留言,我会尽快回复你。

如果对你有帮助,欢迎点赞和分享。

· 2 min read
Chengzihan

1

考虑的问题

  • 代码结构
  • 样式解决方案
  • 组件需求分析和编码
  • 组件测试用例分析和编码
  • 代码打包输出和发布
  • CI/CD,文档生成等

项目创建

创建 React 项目:

npx create-react-app triangle-design typescript

添加 ESLint

yarn add eslint -D
yarn eslint --init

样式系统

inline css

举例:

<div style={{ color: 'red' }}>Hello</div>

css in js

styled-components

styled-components 是一个用于 React 的 CSS-in-JS 库,它可以让你在组件中使用样式,而不用担心样式污染。

import styled from 'styled-components'

const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`

ReactDOM.render(
<Title>Hello World!</Title>,
document.getElementById('root')
)

emotion

Emotion 是一个用于 CSS-in-JS 的库,它可以让你在组件中使用样式,而不用担心样式污染。

import styled from '@emotion/styled'

const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`

ReactDOM.render(
<Title>Hello World!</Title>,
document.getElementById('root')
)

Sass/Less

Sass

Sass 是一种 CSS 预处理器,它可以让你使用变量、嵌套规则、混合(mixin)等高级功能来简化 CSS 的编写。

// style.scss
$color: red;

body {
color: $color;
}
// index.js

import './style.scss'

Less

Less 是一种 CSS 预处理器,它可以让你使用变量、嵌套规则、混合(mixin)等高级功能来简化 CSS 的编写。

// style.less

@color: red;

body {
color: @color;
}
// index.js

import './style.less'

tailwindcss

Tailwind 是一个用于 CSS 的工具类库。

// index.js

import './style.css'
// style.css

@tailwind base;
@tailwind components;
@tailwind utilities;

同时也可以直接在组件中使用 tailwindcss 的工具类。

<div className="bg-red-500">Hello</div>

· 15 min read
Chengzihan

关键帧提取

实现视频(.avi)镜头突变检测并提取出关键帧,然后对其做帧内图像分割,显示分割结果(关键帧提取及图像分割方法自选,各完成一种即可,如完成多种方法,可加分)。

分水岭算法

void watershed(Mat& _img)
{
Mat image = _img;
Mat huidu;
// 转换成灰度图像
cvtColor(image, huidu, CV_RGB2GRAY);
// 高斯滤波和边缘检测
GaussianBlur(huidu, huidu, Size(3, 3), 0, 0);
Canny(huidu, huidu, 50, 150, 3);
// 轮廓
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
// 找到轮廓
findContours(huidu, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point());
// 生成分水岭图像
Mat imageContours = Mat::zeros(image.size(), CV_8UC1);
Mat biaoji(image.size(), CV_32S);
biaoji = Scalar::all(0);
int index = 0, n1 = 0;
// 绘制轮廓
for (; index >= 0; index = hierarchy[index][0], n1++)
{
drawContours(biaoji, contours, index, Scalar::all(n1 + 1), 1, 8, hierarchy); // 绘制轮廓
drawContours(imageContours, contours, index, Scalar(255), 1, 8, hierarchy); // 绘制轮廓
}

Mat biaojiShows;
convertScaleAbs(biaoji, biaojiShows);
watershed(image, biaoji);

Mat w1; // 分水岭后的图像
convertScaleAbs(biaoji, w1); // 转换成8位图像
imshow("分水岭算法", w1);
waitKey(10);
}

最大熵算法

/**
* @brief 最大熵法
* @param currentframe 输入图像
* @param threshImage 输出图像
*/

void threshEntroy(Mat currentframe)
{
int hist_t[256] = { 0 }; //每个像素值个数
int index = 0;//最大熵对应的灰度
double Property = 0.0;// 像素占有比率
double maxEntropy = -1.0;//最大熵
double frontEntropy = 0.0;//前景熵
double backEntropy = 0.0;//背景熵
int sum_t = 0; //像素总数
int nCol = currentframe.cols;//每行的像素数
// 计算每个像素值的个数
for (int i = 0; i < currentframe.rows; i++)
{
uchar* pData = currentframe.ptr<uchar>(i);
for (int j = 0; j < nCol; ++j)
{
++sum_t;
hist_t[pData[j]] += 1;
}
}
// 计算最大熵
for (int i = 0; i < 256; i++)
{
// 背景像素数
double sum_back = 0;
for (int j = 0; j < i; j++)
{
sum_back += hist_t[j];
}

//背景熵
for (int j = 0; j < i; j++)
{
if (hist_t[j] != 0)
{
Property = hist_t[j] / sum_back;
backEntropy += -Property * logf((float)Property);
}
}
//前景熵
for (int k = i; k < 256; k++)
{
if (hist_t[k] != 0)
{
Property = hist_t[k] / (sum_t - sum_back);
frontEntropy += -Property * logf((float)Property);
}
}

if (frontEntropy + backEntropy > maxEntropy)// 求最大熵
{
maxEntropy = frontEntropy + backEntropy;
index = i;
}
frontEntropy = 0.0; // 前景熵清零
backEntropy = 0.0; // 背景熵清零
}
Mat threshImage;
threshold(currentframe, threshImage, index, 255, 0); //进行阈值分割
imshow("最大熵法", threshImage);
}

直方图双峰法

/**
* @brief 计算直方图,两个峰值,然后波谷阈值法
* @param currentframe 输入图像
*/

void threshBasic(const Mat& currentframe)
{
// 计算灰度直方图,zero函数用于创建一个大小为256的Mat对象,类型为CV_32F,全部初始化为0
Mat histogram = Mat::zeros(Size(256, 1), CV_32SC1);
// 获取行高
int rows = currentframe.rows;
int cols = currentframe.cols;
// 遍历图像,计算灰度直方图
for (int i = 0; i < rows; i++){
for (int j = 0; j < cols; j++){
int index = int(currentframe.at<uchar>(i, j));
histogram.at<int>(0, index) += 1;
}
}
// 找到灰度直方图最大峰值对应的灰度值
Point peak1;
// 在数组中找到全局最小和最大值
minMaxLoc(histogram, NULL, NULL, NULL, &peak1);
int p1 = peak1.x;
Mat gray_2 = Mat::zeros(Size(256, 1), CV_32FC1);
for (int k = 0; k < 256; k++)
{
int hist_k = histogram.at<int>(0, k);
gray_2.at<float>(0, k) = pow(float(k - p1), 2) * hist_k;
}
Point peak2;
minMaxLoc(gray_2, NULL, NULL, NULL, &peak2);
int p2 = peak2.x;
//找到两个峰值之间的最小值对应的灰度值,作为阈值
Point threshLoc;
int thresh = 0;
if (p1 < p2) {
minMaxLoc(histogram.colRange(p1, p2), NULL, NULL, &threshLoc);
thresh = p1 + threshLoc.x + 1;
}
else {
minMaxLoc(histogram.colRange(p2, p1), NULL, NULL, &threshLoc);
thresh = p2 + threshLoc.x + 1;
}
/**
* CV_EXPORTS_W double threshold( InputArray src, OutputArray dst,
double thresh, double maxval, int type );
* src:输入图像
* dst:输出图像
* thresh:阈值
* maxval:输出图像中最大值
* type:阈值类型,THRESH_BINARY 指二值化
*/
Mat threshImage;
threshold(currentframe, threshImage, thresh, 255, THRESH_BINARY);
imshow("直方图阈值分割结果", threshImage);
}

视频增强和滤波

编程实现具有视频增强及滤波功能的视频播放器,包括以下功能:

  1. 控制视频的播放与暂停。
  2. 控制视频播放进度。
  3. 可调节当前视频的对比度与亮度。
  4. 可对视频进行增强(如直方图均衡化)。
  5. 可对视频进行滤波(任意实现一种滤波器,多做加分)

改变视频亮度

void changeLight(int x) {
x = x - 255;
for (int r = 0; r < nowRate.rows; r++) {
for (int c = 0; c < nowRate.cols; java) {
int B = nowRate.at<Vec3b>(r, c)[0];
int G = nowRate.at<Vec3b>(r, c)[1];
int R = nowRate.at<Vec3b>(r, c)[2];
B += x;
G += x;
R += x;
if (B > 255)B = 255;
if (G > 255)G = 255;
if (R > 255)R = 255;
if (B < 0)B = 0;
if (G < 0)G = 0;
if (R < 0)R = 0;
nowRate.at<Vec3b>(r, c)[0] = B;
nowRate.at<Vec3b>(r, c)[1] = G;
nowRate.at<Vec3b>(r, c)[2] = R;
}
}
}

改变视频对比度

void Contrast(int x) {
float k = 0;
if (x - 50 == 0)
return;
else if (x - 50 > 0) {
k = 1 + (x - 50) / 100.0;
}
else
k = 1 - (50 - x) / 100.0;

for (int r = 0; r < nowRate.rows; r++) {
for (int c = 0; c < nowRate.cols; java) {
int B = nowRate.at<Vec3b>(r, c)[0];
int G = nowRate.at<Vec3b>(r, c)[1];
int R = nowRate.at<Vec3b>(r, c)[2];
B *= k;
G *= k;
R *= k;
if (B > 255)B = 255;
if (G > 255)G = 255;
if (R > 255)R = 255;
nowRate.at<Vec3b>(r, c)[0] = B;
nowRate.at<Vec3b>(r, c)[1] = G;
nowRate.at<Vec3b>(r, c)[2] = R;
}
}
}

直方图均衡化

// 直方图均衡化
void histVideo() {
// 三个通道分别进行均衡化处理
float sum_p = nowRate.rows * nowRate.cols;
int hist[3][256] = { 0 };//BGR
// 构造 3 通道的直方图
for (int r = 0; r < nowRate.rows; r++) {
for (int c = 0; c < nowRate.cols; java) {
hist[0][nowRate.at<Vec3b>(r, c)[0]]++;
hist[1][nowRate.at<Vec3b>(r, c)[1]]++;
hist[2][nowRate.at<Vec3b>(r, c)[2]]++;
}
}
float CDF[3][256] = { 0 };//3 个通道的累计分布
// 计算 3 通道的所有累计分布函数的值
for (int i = 0; i < 256; i++) {
float sum[3] = { 0 };
for (int k = 0; k <= i; k++) {
sum[0] += hist[0][k];
sum[1] += hist[1][k];
sum[2] += hist[2][k];
}
CDF[0][i] = sum[0] / sum_p;
CDF[1][i] = sum[1] / sum_p;
CDF[2][i] = sum[2] / sum_p;
}
for (int r = 0; r < nowRate.rows; r++) {
for (int c = 0; c < nowRate.cols; java) {
for (int i = 0; i < 3; i++)
nowRate.at<Vec3b>(r, c)[i] = 255 * CDF[i][nowRate.at<Vec3b>(r, c)[i]];
}
}
}

均值滤波

void Filter(int x)  
{
Mat temp = nowRate.clone();
int num = x * x; // 滤波器的大小
// 遍历图像
for (int r = x / 2; r < nowRate.rows - x / 2; r++) {
for (int c = x / 2; c < nowRate.cols - x / 2; java) {
// 遍历滤波器
for (int i = 0; i < 3; i++) {
// 3 个通道
int sum = 0;
// 遍历滤波器
for (int dr = - x / 2; dr <= x / 2; dr++) {
for (int dc = - x / 2; dc <= x / 2; djava) {
sum += temp.at<Vec3b>(r + dr, c + dc)[i];
}
}
// 滤波器的值
nowRate.at<Vec3b>(r, c)[i] = (float)sum / num;
}
}
}
}

其他滤波

void Filter2(int x)
{
Mat temp = nowRate.clone();
boxFilter(temp, nowRate, -1, Size(x, x)); // 方框滤波
}

void Filter3(int x)
{
Mat temp = nowRate.clone();
GaussianBlur(temp, nowRate, Size(x, x), 0, 0); // 高斯滤波
}

void Filter4(int x)
{
Mat temp = nowRate.clone();
// 中值滤波
medianBlur(temp, nowRate, x);
}

运动目标检测

给定一段视频,编程实现帧差法或背景减法检测视频中的运动目标,并分析实验结果。

帧差法

/*
@ temp 临时帧
@ frame 当前帧
return result 帧差法运动目标检测结果,为每个运动目标加上绿色的矩形框
*/

Mat frameDiff(Mat temp, Mat frame)
{
Mat result;
// 将当前帧转换为灰度图像
cvtColor(frame, frame, COLOR_BGR2GRAY);
// 将临时帧转换为灰度图像
cvtColor(temp, temp, COLOR_BGR2GRAY);
// 将当前帧与临时帧进行帧差运算
absdiff(frame, temp, result);
// 将帧差运算结果二值化
threshold(result, result, 50, 255, THRESH_BINARY);
// 对二值化结果进行腐蚀膨胀运算
//erode(result, result, Mat());
//dilate(result, result, Mat());

// 对二值化结果进行形态学开运算
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
morphologyEx(result, result, MORPH_OPEN, kernel);
// 对二值化结果进行形态学闭运算
morphologyEx(result, result, MORPH_CLOSE, kernel);
imshow("biyunsuan", result);
// 查找二值化结果中的轮廓
vector<vector<Point>> contours;
findContours(result, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
// 遍历轮廓
for (size_t t = 0; t < contours.size(); t++)
{
// 绘制轮廓
drawContours(frame, contours, t, Scalar(0, 0, 255), 2);


// 获取当前轮廓的矩形边界框
Rect rect = boundingRect(contours[t]);
// 如果在图形的上半部分,忽略小于 的框
//if (rect.y < frame.rows / 4 && rect.area() < 500)
// continue;
//// 如果在图形的下半部分,忽略小于 的框
//if (rect.y > frame.rows / 4 && rect.area() < 900)
// continue;
if (rect.area() < 400)
continue;
// 在当前帧中绘制矩形边界框
rectangle(frame, rect, Scalar(0, 255, 0), 2, 8, 0);
}
return frame;
}

背景减法

/*
@ temp 临时帧
@ frame 当前帧
return result 检测结果,为每个运动目标加上绿色的矩形框,使用 BackgroundSubtractorMOG 函数
*/

Mat detectMotion(Mat frame) {
Mat result;
frame.copyTo(result);
//创建背景减法器
Ptr<BackgroundSubtractorMOG2> bgModel = createBackgroundSubtractorMOG2();
//背景减法
Mat fgMask;
bgModel->apply(frame, fgMask);
//膨胀
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
dilate(fgMask, fgMask, kernel);
imshow("fgmask", fgMask);
//查找轮廓
vector<vector<Point>> contours;
findContours(fgMask, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
//绘制边缘
for (size_t t = 0; t < contours.size(); t++)
{
// 绘制轮廓-红褐色
drawContours(result, contours, t, Scalar(0, 0, 255), 2);
//如果在图形的上1/3部分,忽略面积小于 400 的框
Rect rect = boundingRect(contours[t]);
if (rect.y < frame.rows / 4 && rect.area() < 200)
continue;
// 如果在图形的下2/3部分,忽略面积小于 1000 的框
if (rect.y > frame.rows * 1 / 4 && rect.area() < 900)
continue;

rectangle(result, rect, Scalar(0, 255, 0), 2, 8, 0);

}
return result;
}

边缘检测

/*
@ temp 临时帧
@ frame 当前帧
return result 检测结果,为每个运动目标加上绿色的矩形框,使用 canny 算法
*/

Mat MotionDetectByCanny(Mat temp, Mat frame)
{
Mat result;
frame.copyTo(result);
// 1. 将当前帧和临时帧转换为灰度图
Mat gray1, gray2;
cvtColor(temp, gray1, COLOR_BGR2GRAY);
cvtColor(frame, gray2, COLOR_BGR2GRAY);
// 2. 对当前帧和临时帧进行高斯滤波
GaussianBlur(gray1, gray1, Size(3, 3), 0, 0);
GaussianBlur(gray2, gray2, Size(3, 3), 0, 0);
// 3. 对当前帧和临时帧进行运动目标检测
Mat motion = gray1 - gray2;
// 4. 对运动目标进行二值化处理
threshold(motion, motion, 50, 255, THRESH_BINARY);
// 5. 对二值化图像进行形态学操作
Mat element = getStructuringElement(MORPH_RECT, Size(3, 3));
morphologyEx(motion, motion, MORPH_OPEN, element);
// 6. 对二值化图像进行边缘检测
// 后面两个参数分别代表 threshold1 和 threshold2 的值以及 apertureSize
Canny(motion, motion, 3, 9, 3);
imshow("motion", motion);
// 7. 查找轮廓
vector<vector<Point>> contours;
findContours(motion, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);
// 8. 绘制轮廓
for (size_t t = 0; t < contours.size(); t++)
{
// 绘制轮廓-红褐色
drawContours(result, contours, t, Scalar(0, 0, 255), 2);
//如果在图形的上1/3部分,忽略面积小于 400 的框
Rect rect = boundingRect(contours[t]);
if (rect.y < frame.rows / 4 && rect.area() < 200)
continue;
// 如果在图形的下2/3部分,忽略面积小于 1000 的框
if (rect.y > frame.rows * 1 / 4 && rect.area() < 900)
continue;

rectangle(result, rect, Scalar(0, 255, 0), 2, 8, 0);

}
return result;
}

三帧差法

Mat frameDiff3(Mat temp, Mat frame)
{
Mat result;
Mat gray1, gray2, gray3;
Mat diff1, diff2, diff3;
Mat diff;
Mat threshold_output;
vector<vector<Point> > contours;
vector<Vec4i> hierarchy;
int thresh = 100;
int max_thresh = 255;
RNG rng(12345);
// 将当前帧转换为灰度图像
cvtColor(temp, gray1, CV_BGR2GRAY);
cvtColor(frame, gray2, CV_BGR2GRAY);
absdiff(gray1, gray2, diff1); // 第一帧与第二帧的差
cvtColor(frame, gray3, CV_BGR2GRAY);
absdiff(gray2, gray3, diff2); // 第二帧与第三帧的差
absdiff(diff1, diff2, diff3); // 两帧差的差
threshold(diff3, threshold_output, thresh, 255, THRESH_BINARY);
findContours(threshold_output, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE, Point(0, 0));
imshow("threshold_output", threshold_output);
vector<vector<Point> > contours_poly(contours.size());
vector<Rect> boundRect(contours.size());
// 遍历轮廓
for (int i = 0; i < contours.size(); i++)
{
approxPolyDP(Mat(contours[i]), contours_poly[i], 3, true); // 多边形逼近
boundRect[i] = boundingRect(Mat(contours_poly[i])); // 获取当前轮廓的矩形边界框
}
result = frame.clone();
for (int i = 0; i < contours.size(); i++)
{
// 绘制轮廓-红褐色
drawContours(result, contours_poly, i, Scalar(0, 0, 255), 1, 8, vector<Vec4i>(), 0, Point());

//如果在图形的上1/3部分,忽略面积小于 400 的框
if (boundRect[i].y < frame.rows / 4 && boundRect[i].area() < 200)
continue;
//如果在图形的下2/3部分,忽略面积小于 900 的框
if (boundRect[i].y > frame.rows / 4 && boundRect[i].area() < 600)
continue;
//if ( boundRect[i].area() < 200)
// continue;

rectangle(result, boundRect[i].tl(), boundRect[i].br(), Scalar(0, 255, 0), 2, 8, 0);
}
return result;
}

· 3 min read
Chengzihan

介绍

chatGPT 是 openAI 发布的一个开源项目,可以用来生成聊天内容,这里我们将其接入到微信中,实现微信聊天机器人。

获取 token

  1. 前往 https://chat.openai.com/chat 并登陆。
  2. 按下 F12 打开开发者工具.
  3. 点击 Application 选项卡 > Cookies. image
  4. 复制 Secure-next-auth.session-token 的值,保留,在下面会使用到。
无法登录的解决方法

如果无法登录,可参考这个方法

使用 Railway 部署

Railway 是一个部署平台,您可以在其上配置基础架构,在本地使用该基础架构进行开发,然后将其部署到云端。本部分将描述如何快速使用 Railway 部署一个 wechat-chatgpt 项目。

首先,您需要注册一个 Railway 帐户,并使用 GitHub 验证登录。

然后点击下面的一键部署按钮进行部署。

Deploy on Railway

完成一些验证操作后,就可以开始部署了。您将看到以下界面:

railway-deployment

您需要配置一些环境变量:

  • CHAT_GPT_EMAIL :您的 OpenAI 帐户电子邮件,如果您有 session_token,则可不填。

  • CHAT_GPT_PASSWORD :您的 OpenAI 帐户密码,如果您有 session_token,则可不填。

  • CHAT_GPT_SESSION_TOKEN :您的 OpenAI 帐户 session_token,如果您有电子邮件和密码,则可选。

  • CHAT_GPT_RETRY_TIMES :当 OpenAI API 返回 429 或 503 时重试的次数。

  • CHAT_PRIVATE_TRIGGER_KEYWORD :如果您希望只有一些关键字才能在私人聊天中触发 chatgpt,则可以设置它。

点击“部署”按钮,您的服务将立即开始部署。以下界面出现表示部署已经开始:

railway-deploying

当部署过程显示为成功后,点击查看日志,在部署日志中找到微信登录链接:

railway-deployed

点击链接,使用准备好的微信扫码登录。

成功登录并开始发送和接收消息(此过程可能需要几分钟):

railway-success

· 6 min read
Chengzihan

为什么要使用脚本

游戏对象的行为由附加的组件控制。虽然 Unity 的内置组件可能用途很广泛,但是您很快就会发现,必须超越组件可提供的功能来实现自己的游戏功能。Unity 允许使用脚本来自行创建组件。使用脚本可以触发游戏事件,随时修改组件属性,并以所需的任何方式响应用户的输入。
Unity 本身支持 C# 编程语言。C#(发音为 C-sharp)是一种类似于 Java 或 C++ 的行业标准语言。

创建脚本

在 Unity 的项目视图中,右键单击Assets文件夹,然后选择Create>C# Script。然后将脚本拖拽到模型上或者模型的检查器中,即可完成脚本的创建。这个脚本便绑定了这个模型。

脚本模板概览

使用 JetBrains Rider 或 Visual Studio Code 编辑器打开脚本。您将看到一个类似于下面的模板:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 类名和脚本名是一样的
public class Move : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{

}

// Update is called once per frame
void Update()
{

}
}

脚本的生命周期

Awake:只会被调用一次,在Start方法之前被调用。主要用于字段值的初始化工作,禁用脚本,创建游戏对象,或者 Resources.Load(Prefab) 对象。

Start:只执行一次,在Awake方法执行结束后执行,但在 Update 方法执行前执行,主要用于程序 UI 的初始化操作,比如获取游戏对象或者组件。

FixedUpdate:以固定频率调用的函数(默认0.02s)。

Update:以非固定的频率调用的函数,该频率与设备的性能和程序自身有关(每帧)。

LateUpdate:每当Update调用完之后立马调用。

OnDisable :游戏对象消失(销毁/隐藏)的那一刻调用。

OnDestroy :游戏对象销毁的那一刻调用。

Unity 输出

在 Unity 中我们使用 debug.log() 来输出信息,调试错误。

下面是一个完整生命周期的例子:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 类名和脚本名是一样的
public class Move : MonoBehaviour
{
// 最先执行的 awake 函数
private void Awake()
{
// 控制台输出
Debug.Log("Awake");
}

private void OnEnable()
{
// 激活组件调用,可能调用多次
Debug.Log("激活组件");
}

// 在第一次 onEnable 之后,只调用一次
void Start()
{
Debug.Log("开始");
}

// 每一帧调用一次
void Update()
{
// 每帧调用
}

// 在 Update 之后调用
private void LateUpdate()
{
// 在刷新后调用
}

// 默认调用,与帧间隔无关
private void FixedUpdate()
{
// 固定调用
}

// 对象消失时调用
private void OnDisable()
{
// 消失时调用
}

// 销毁时调用
private void OnDestroy()
{
// 销毁时才会调用
}
}

1

多个脚本运行顺序

当某个对象有多个脚本时,与检查器中的脚本顺序无关。但是我们可以利用脚本的生命周期来控制脚本的执行顺序。

注意

在脚本执行时,先把所有脚本的 Awake 函数执行完,再执行所有脚本的 Start 函数,然后执行所有脚本的 Update 函数,以此类推。

Move1.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Move1 : MonoBehaviour
{
// 最先执行的 awake 函数
private void Awake()
{
// 控制台输出
Debug.Log("这是要先执行的");
}
void Start()
{
Debug.Log("开始");
}
}
Move2.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Move2 : MonoBehaviour
{
// 最先执行的 awake 函数
private void Awake()
{
// 空
}
void Start()
{
Debug.Log("这是后执行的");
}
}

当然,也可以在 Project 设置中直接修改脚本执行顺序。

常用脚本

响应键盘

// 键盘按下 W A S D 时,相机沿着对应的方向移动
if (Input.GetKey(KeyCode.W))
{
character.transform.Translate(Vector3.forward * Time.deltaTime * 10);
}
if (Input.GetKey(KeyCode.A))
{
character.transform.Translate(Vector3.left * Time.deltaTime * 10);
}
if (Input.GetKey(KeyCode.S))
{
character.transform.Translate(Vector3.back * Time.deltaTime * 10);
}
if (Input.GetKey(KeyCode.D))
{
character.transform.Translate(Vector3.right * Time.deltaTime * 10);
}

游戏物体在有重力时不会掉下去

在物体组件中添加 Box Collider 组件,然后在 Box Collider 组件中勾选 Is Trigger。在下面修改包围盒参数,使其与物体大小一致。

· 15 min read
Chengzihan

介绍

Flask 是一个基于 Python 的 Web 后端框架。通过 Flask 可以快速写出一个 API 接口。MongoDB 是一个基于分布式文件存储的数据库。本文将介绍如何使用 Flask 和 MongoDB 实现简单的 Web 后端。

前端部分

由于涉及插值以及条件渲染,这里简单地使用到了 Vue.js 框架。首先进行框架引入:

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

接着,创建基本的输入框和按钮:

<body>
<div id="app" class="main">
<h1>输入姓名查询学号</h1>
<!-- 值绑定,监听输入框的内容变化,如果 Input 内内容改变,data.id就改变为这个值 -->
<input v-model="id" type="text" placeholder="学号" />
<!-- @onclick 触发事件名,后面的函数在 method 中 -->
<button class="button" id="signIn" @click="Query">查询</button>
</div>
</body>

在这里,我们将数据用 v-model 方法绑定到 input 之上,当 input 的值发生变化时,数据值也会发生变化。当我们点击 button 时,我们将调用 Query 函数。

下面,创造 Vue 实例:

注意,上面的 div 被绑定了实例名 app

<div id="app" class="main">
<!-- ... -->
</div>

那么,现在我们就可以在 script 中创建 Vue 实例了:

<script>
// 创建vue实例
const app = new Vue({
// 交代宿主是谁,id用 # 查询
el: "#app",
// 数据容器
data: {
id:"",
result:"",
},
// 函数方法
methods: {
Query() {
// ... 查询方法
}
},
})
</script>

在这里可以看到,我们在 data 中初始化了两个变量,分别是 idresultid 用于存储输入框的值,result 用于存储查询结果。当 input 的值发生变化时,id 的值也会跟踪发生变化。

用 VScode 插件 Live Server 启动一个本地服务器,打开 index.html(或直接打开),可以看到如下界面:

2

Flask API接口

接下来,我们需要实现一个 API 接口,用于接收前端传来的数据,查询数据库,返回查询结果。

在 Python 中,import 用于导入模块,from 用于导入模块中的某个函数。这里需要 Flask 的 Flaskrequest 函数,用于实现http服务器。

from flask import Flask, request

为了服务器能被直接访问,我们需要设置跨域访问。

from flask_cors import CORS

CORS(app, resources=r'/*') # 注册 CORS, "/*" 允许访问所有api

接着,可以创建一个 Flask 实例:

app = Flask(__name__)

然后,我们需要创建一个路由,用于接收前端传来的数据,接受使用 GET 进行访问,访问路径为 /getname

@app.route('/getname', methods=['GET'])

接着,我们需要获取前端传来的数据,这里我们需要用到 request 函数,它可以获取前端传来的数据:

def check():
# 默认返回内容,result是根据查询结果要返回的
return_dict = {'return_code': '200', 'return_info': '处理成功', 'result': False}
# 判断入参是否为空
if request.args is None:
return_dict['return_code'] = '5004'
return_dict['return_info'] = '请求参数为空'
# 告知前端,查询失败
return json.dumps(return_dict, ensure_ascii=False)
# 获取传入的参数
get_data = request.args.to_dict()
# 获取参数中名为 ID 的参数值
ID = get_data.get('ID')
# 对参数进行操作,用 ID 去查询数据库
return_dict['result'] = getMongo(ID)
# 返回查询结果
return json.dumps(return_dict, ensure_ascii=False)

在这里,还没有写查询数据库的方法,我们先把这个方法写出来一个简单的应答,然后一会再去实现查询数据库的方法。

def getMongo(ID):
# 这里是查询数据库的方法
# 这里先返回一个简单的应答
return True

最后,在主函数中,启动 Flask 服务器:

if __name__ == "__main__":
app.run(debug=True)

这里的 debug=True 是为了方便调试,可以在控制台看到错误信息。并在本地进行运行。

接口测试

这里,我们先不管数据库,对接口进行简单的测试。在这里,我使用 Apifox,进行接口测试。下载后打开。

点击新建团队:

1

然后,新建项目:

2

打开项目后,选择快捷请求

3

在 Pycharm 中,运行 getname.py

3

可以看到下方控制台提示,服务器已经启动。访问地址为 http://127.0.0.0:5000。我们复制这个地址,然后在 Apifox 中,粘贴到地址栏中,在地址后添加访问的接口名:/getname,填写 Params 参数 ID 的值,随便填一个,因为上面写了无论什么时候都返回 True,所以预期结果是 True,点击发送:

2

可以看到,下面的控制台提示,接口调用成功(200,OK),返回结果为 True。

现在,可以关闭 Apifox 了。

在前端中请求接口

上面 Vue 代码中,我们空了一个方法 Query(),这个方法就是用来请求接口的。现在我们使用 XMLHttpRequest 对象来请求接口。

首先初始化一个 XMLHttpRequest 对象:

const request = new XMLHttpRequest();

接着,去发起请求:

request.open("GET", "http://127.0.0.1:5000/getname" +"?ID="+ this.id);

这里的 GET 是请求方式,http://127.0.0.1:5000 是服务器地址,/getname 是接口名,?ID= 是参数名,this.id 是参数值。this.id 是在 Vue 中调用一个变量的方法。接着,监听请求状态:

request.onreadystatechange = () => {
// readyState 为 4 时,请求已完成,request.status 为 200 时,请求成功
if (request.readyState === 4 && request.status === 200) {
// 格式化返回值为json
const obj = JSON.parse(request.responseText);
// 打印来看看
console.log(obj);
// 让 data 里面的 result 变成这个值
this.result = obj.result;
// 打印来看看
console.log(this.result);
}
};

最后,发送请求:

request.send();

完整的 Query() 方法如下:

Query() {
console.log(this.id);
// 开始 http 请求
const request = new XMLHttpRequest(); // Ajax 初始化异步请求
// 发起 http 请求
request.open("GET", "http://127.0.0.1:5000/getname" +"?ID="+ this.id);
request.onreadystatechange = () => {
if (request.readyState === 4 && request.status === 200) {
// 格式化返回值为json
const obj = JSON.parse(request.responseText);
// 让 data 里面的 result 变成这个值
this.result = obj.result;
console.log(this.result);
}
};
request.send();
// end
}

在保持 Pycharm 运行的情况下,我们在浏览器中打开 index.html,输入 ID,点击查询,按F12查看控制台打印,可以看到服务器向前端返回了 一条应答,状态码为 200,OK,返回结果为 True。

2

MongoDB 数据库

倘若你不需要配置数据库,或者用 csv 本地文件代替数据库,可以跳过这一节。上面就够用了,你只需要将 MongoDB 函数改写成查询自己的 csv 文件即可。

注册 MongoDB 账号

MongoDB 数据库为每个用户提供了 512MB 的免费存储空间,你可以在 MongoDB Atlas 上注册一个免费的数据库。(用来存储你的任务数据完全足够了)
Mongo Atlas 提供云数据库服务,这意味着你可以在远程访问你的数据库,而不需要在本地搭建数据库。

账号注册

你首先需要注册一个 MongoDB 账号,点击 这里 注册。这可能需要你提供邮箱等个人信息。

创建数据库

登陆后,你将看到下面的界面:

1

点击右上角 Create 。

选择 shared 类型的集群,接着选择一家服务提供商,这里是亚马逊,当然,可以试一下微软的 Azure ,说不定国内访问更快。然后选择一个离你最近的地区,比如中国香港,点击 Create Cluster

3

点击右下角的 Create Cluster

2

在出现的窗口中,添加连接 IP 地址。我们需要允许从任何地方访问。因此,单击“允许从任何地方访问”按钮,然后单击“添加 IP 地址”进行设置。选择云环境,即Cloud Environment,IP 地址填写为 0.0.0.0

接下来,我们需要创建一个用户来连接到此数据库。在“创建数据库用户”窗体上,输入自定义用户名、密码,然后单击“创建数据库用户”。请记住这个密码。

连接数据库

点击 Connect按钮,连接数据库。
2

选择 Connect your application ,然后选择 Python ,接着选择 3.12 or later

2

接着,出现了一个连接地址,不勾选 "Include full example" 这个地址就是你的数据库连接地址,你需要将它复制下来,稍后会用到。(请一定妥善保存)

链接格式如下:

mongodb+srv://YourName:<password>@cluster0.xxxxxx.mongodb.net/?retryWrites=true&w=majority

将其中的 <password> 替换为刚刚你输入的自定义密码。这个才是需要用到的代码,请妥善保存。

举例:

mongodb+srv://inannan:12345678@cluster0.xxxxxx.mongodb.net/?retryWrites=true&w=majority

创建数据集

点击集群名 Cluster0 进入集群。点击添加数据库:

2

图中有其他数据库,不用管。

2

创建成功:

2

插入几条示例数据:

3

重复几次,示例如下:

3

OK了。

现在我们来看看如何在 Python 中使用 MongoDB。

首先引入需要的依赖:

import json
from flask_cors import CORS
import pymongo
from bson import json_util

查询过程:

def getMongo(ID):
# 链接 MongoDB atlas
myclient = pymongo.MongoClient('mongodb+srv://inannan:12345678@cluster0.xxxxxx.mongodb.net/?retryWrites=true&w=majority') # 数据库的连接地址,改成你自己的
mydb = myclient['mydb'] # 数据库名称
mycol = mydb['db1'] # 集合名称
myquery = {"id": ID} # 查询条件
mydoc = mycol.find(myquery) # 查询结果
# 取出其中的数据格式为json
for x in mydoc:
# 将x转换为对象
x = json.loads(json_util.dumps(x))
print(x)
return x

这里,返回的是一个对象。

重新打开 Apifox,创建一个新的接口,接口地址为:/getname,请求方式为 GET。携带参数 ID,值为 201000000,因为上面在 Mongodb 中插入的数据中,有一个 id201000000 的数据。所以这里就用这个值来测试。

由此可见,我们已经成功的从 MongoDB 中查询到了数据。

2

返回的结果是一个 json 数据,例如:

{
"return_code": "200",
"return_info": "处理成功",
"result": {
"_id": {"$oid": "635a918c1be53239a161f777"},
"id": "201000000",
"name": "JZ",
"class": "数媒20"
}
}

可以这样访问 name 的值:

result.name

让我们修改前端页面,将数据展示出来。首先创建结果显示框,使用条件渲染,当 result 有值时,才显示结果。

<div v-if="result">
姓名:{{result.name}}
<br/>
班级:{{result.class}}
</div>

Flask 多个接口

我们可以创建多个接口,例如:

@app.route('/getname', methods=['GET'])
def getname():
ID = request.args.get('ID')
result = getMongo(ID)
return jsonify(result)

@app.route('/getclass', methods=['GET'])
def getclass():
ID = request.args.get('ID')
result = getMongo(ID)
return jsonify(result)

在访问时,只需要在接口地址后面加上 /getname 或者 /getclass 即可。

扩展应用

这只是最简单的一个例子,实际上,我们可以在这个基础上做很多事情。例如:

  • 实现 MongoDB 的增删改查操作,实现注册、登录等功能。
  • 将多个数据存储到一个集合中,实现多个数据的查询。如爬取结果。

部署

腾讯云服务器部署 Flask 接口
腾讯云 Serverless 部署Flask接口
Ubuntu Nginx 部署 Flask 接口
Windows Nginx 部署 Flask 接口
Vercel 部署 Flask 接口

开源源代码

GitHub

· 9 min read
Chengzihan

three.js 简介

three.js 是一个基于 WebGLJavaScript 3D 库,它可以让你在浏览器中使用 JavaScript 创建 3D 图形。three.js 的目标是让 3D 在 Web 上变得简单。

引入 three.js

three.js 可以直接在原生 html 语法中引用。

<!-- 开发环境版本,包含了有帮助的命令行警告 -->
<script src="https://cdn.jsdelivr.net/npm/three/build/three.js"></script>
<!-- 生产环境版本,优化了尺寸和速度 -->
<script src="https://cdn.jsdelivr.net/npm/three/build/three.min.js"></script>

或者可以使用 npm 安装。

npm install three

程序结构

three.js 的程序结构如下图所示。

1

基本使用

下面是一个简单的 three.js 程序。

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>第一个three.js文件_WebGL三维场景</title>
<style>
body {
margin: 0;
overflow: hidden;
/* 隐藏body窗口区域滚动条 */
}
</style>
<!--引入three.js三维引擎-->
<script src="http://www.yanhuangxueyuan.com/versions/threejsR92/build/three.js"></script>
<!-- <script src="./three.js"></script> -->
<!-- <script src="http://www.yanhuangxueyuan.com/threejs/build/three.js"></script> -->
</head>

<body>
<script>
/**
* 创建场景对象Scene
*/
var scene = new THREE.Scene();
/**
* 创建网格模型
*/
// var geometry = new THREE.SphereGeometry(60, 40, 40); //创建一个球体几何对象
var geometry = new THREE.BoxGeometry(100, 100, 100); //创建一个立方体几何对象Geometry
var material = new THREE.MeshLambertMaterial({
color: 0x0000ff
}); //材质对象Material
var mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh
scene.add(mesh); //网格模型添加到场景中
/**
* 光源设置
*/
//点光源
var point = new THREE.PointLight(0xffffff);
point.position.set(400, 200, 300); //点光源位置
scene.add(point); //点光源添加到场景中
//环境光
var ambient = new THREE.AmbientLight(0x444444);
scene.add(ambient);
// console.log(scene)
// console.log(scene.children)
/**
* 相机设置
*/
var width = window.innerWidth; //窗口宽度
var height = window.innerHeight; //窗口高度
var k = width / height; //窗口宽高比
var s = 200; //三维场景显示范围控制系数,系数越大,显示的范围越大
//创建相机对象
var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
camera.position.set(200, 300, 200); //设置相机位置
camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
/**
* 创建渲染器对象
*/
var renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);//设置渲染区域尺寸
renderer.setClearColor(0xb9d3ff, 1); //设置背景颜色
document.body.appendChild(renderer.domElement); //body元素中插入canvas对象
//执行渲染操作 指定场景、相机作为参数
renderer.render(scene, camera);
</script>
</body>
</html>

几何体 Geometry

//创建一个立方体几何对象Geometry
var geometry = new THREE.BoxGeometry(100, 100, 100);
// 创建一个球体几何对象
var geometry = new THREE.SphereGeometry(60, 40, 40);

材质 Material

//材质对象Material
var material = new THREE.MeshLambertMaterial({
color: 0x0000ff
});

光源 Light

//点光源
var point = new THREE.PointLight(0xffffff);
point.position.set(400, 200, 300); //点光源位置
scene.add(point); //点光源添加到场景中
//环境光
var ambient = new THREE.AmbientLight(0x444444);
scene.add(ambient);

参数0xffffff定义的是光照强度。如果设置为0x000000,那么就是没有光照,如果设置为0xffffff,那么就是最大光照。three.js引擎对WebGL光照模型算法都进行了封装,不需要你了解计算机图形学, 可以直接使用调用three.js光源相关API直接创建一个光源对象。

相机 Camera

//创建相机对象
var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
camera.position.set(200, 300, 200); //设置相机位置
camera.lookAt(scene.position); //设置相机方向(指向的场景对象)

把该构造函数参数中用到的参数s,也就是代码var s = 200;中定义的一个系数,可以把 200 更改为 300 ,你会发现立方体显示效果变小,这很好理解,相机构造函数的的前四个参数定义的是拍照窗口大小, 就像平时拍照一样,取景范围为大,被拍的人相对背景自然变小了。camera.position.set(200, 300, 200);camera.lookAt( scene.position);定义的是相机的位置和拍照方向,可以更改camera.position.set(200,300,200) ;参数重新定义的相机位置,把第一个参数也就是x坐标从200更改为250, 你会发现立方的在屏幕上呈现的角度变了,这就像你生活中拍照人是同一个人,但是你拍照的位置角度不同,显示的效果肯定不同。

渲染器 Renderer

//创建渲染器对象
var renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);//设置渲染区域尺寸
renderer.setClearColor(0xb9d3ff, 1); //设置背景颜色
document.body.appendChild(renderer.domElement); //body元素中插入canvas对象
//执行渲染操作 指定场景、相机作为参数
renderer.render(scene, camera);

场景——相机——渲染器

2

周期渲染

// 渲染函数
function render() {
renderer.render(scene, camera);//执行渲染操作
mesh.rotateY(0.01);//每次绕y轴旋转0.01弧度
}

//间隔20ms周期性调用函数fun,20ms也就是刷新频率是50FPS(1s/20ms),每秒渲染50次
setInterval("render()", 20);

上面代码定义了一个渲染函数render(),函数中定义了三个语句,通过setInterval("render()",20); 可以实现m每间隔20毫秒调用一次函数render(),每次调用渲染函数的时候,执行renderer.render(scene,camera); 渲染出一帧图像,执行mesh.rotateY(0.01);语句使立方体网格模型绕y轴旋转0.01弧度。

实际开发中,为了更好的利用浏览器渲染,可以使用函数requestAnimationFrame()代替setInterval() 函数,requestAnimationFrame()setInterval()一样都是浏览器window对象的方法。

// 渲染函数
function render() {
renderer.render(scene, camera);//执行渲染操作
mesh.rotateY(0.01);//每次绕y轴旋转0.01弧度
requestAnimationFrame(render);//请求再次执行渲染函数render
}

render();//执行渲染函数render

鼠标动作

为了使用鼠标操作三维场景,可以借助three.js众多控件之一OrbitControls.js

可以通过npm install three-orbitcontrols安装OrbitControls.js,也可以直接下载OrbitControls.js 文件,然后在html文件中引入OrbitControls.js文件。

// 引入OrbitControls.js
<script src="js/OrbitControls.js"></script>

npm install three-orbitcontrols

首先可以使用它响应鼠标的上下左右:

function render() {
renderer.render(scene, camera);//执行渲染操作
}

render();
var controls = new THREE.OrbitControls(camera, renderer.domElement);//创建控件对象
controls.addEventListener('change', render);//监听鼠标、键盘事件

OrbitControls.js控件提供了一个构造函数THREE.OrbitControls() ,把一个相机对象作为参数的时候,执行代码new THREE.OrbitControls(camera,renderer.domElement),浏览器会自动检测鼠标键盘的变化, 并根据鼠标和键盘的变化更新相机对象的参数,比如你拖动鼠标左键,浏览器会检测到鼠标事件,把鼠标平移的距离按照一定算法转化为相机的的旋转角度,你可以联系生活中相机拍照,即使景物没有变化,你的相机拍摄角度发生了变化,自然渲染器渲染出的结果就变化了,通过定义监听事件controls.addEventListener('change', render) ,如果你连续操作鼠标,相机的参数不停的变化,同时会不停的调用渲染函数render()进行渲染,这样threejs就会使用相机新的位置或角度数据进行渲染。

执行构造函数THREE.OrbitControls()浏览器会同时干两件事,一是给浏览器定义了一个鼠标、键盘事件,自动检测鼠标键盘的变化,如果变化了就会自动更新相机的数据, 执行该构造函数同时会返回一个对象,可以给该对象添加一个监听事件,只要鼠标或键盘发生了变化,就会触发渲染函数。

· 4 min read
Chengzihan

前言

对于一名合格的前端工程师来说,UI 图转代码的能力是不可或缺的能力,在人工智能大力发展的今天,UI 转代码的 AI 工具也在便捷着前端工程师的工作,这里就借助即时设计工具和 codefun 插件来介绍一下基本流程。

即时设计使用

介绍

即时设计是可云端编辑的专业级 UI 设计工具,为中国设计师量身打造。它和Adobe XD、Figma、Sketch等设计工具有着一样的功能。

下载和安装

即时设计官网下载即时设计,安装即可使用。

导出切图

有时候我们需要导出设计师设计的图片,我们只需要在项目中选中图片,在右边的边栏中选择导出,即可导出图片。

1

注意

为了图片在设备上能够清晰地显示,请选择合适的尺寸,这里选择的是 4x。

Codefun 使用

介绍

CodeFun 是一款 UI 设计稿智能生成源代码的工具,可以将 Sketch、Photoshop 的设计稿智能转换为前端源代码。它最大的特色是:

  • 精准还原设计稿,不再需要反复 UI 走查
  • 可以生成如工程师手写一般的代码

在日常工作中,诸如像扣像素、调布局这些繁琐、枯燥的体力活都将得到极大的削减,原来 8 小时的工作量只需要 10 来分钟即可完成。

下载和安装

打开需要使用的 UI 稿件。

1

找到左侧的 UI 按钮,点击后会弹出一个弹窗,选择左上角的省略号,选择获取更多插件。在搜索中搜索 CODE.FUN,点击安装即可。

1

2

回到 UI 稿件,还是在左侧插件栏中,找到 CodeFun。

2

使用微信登录。点击 + 号,创建项目,点击页面的标题,选中页面,点击上传,当然,也可以选择上传全部。

3

待处理完成。完成后点击打开,在项目的页面中选择需要转换的页面。

3

点击查看代码,即可查看转换后的代码。

5

5

提示

建议不要完全使用 CodeFun 生成的代码,因为它生成的代码不是很规范,不利于维护。使用这个工具是用来参考数据比例以及布局的,然后自己手动写代码。

· 10 min read
Chengzihan

调用报平安接口并使用 GitHub Actions 自动完成北林每日报平安。本脚本 最大的特点是不需要你有一台一直开着的主机或者服务器,让 GitHub 完成这件事即可。

主逻辑来自 @Memory

食用指南

Fork 仓库

仓库地址https://github.com/inannan423/reportPeace

进入本仓库,点击右上角的 Fork 按钮,将本仓库 Fork 到自己的账号下。 顺便点击 star按钮收藏(为我点个赞)。

1

注意,此时这个仓库就是你的了,你在你的仓库密匙中填写的任何内容我均无法获取,请放心使用。

添加个人信息

在报平安代码中,有许多涉及隐私的东西。比如学号、密码、用户ID等。我们不希望这些东西在源代码被公开,所以我们需要把它们添加到 GitHub Secrets 中。

下面举一个例子:

在上面我们需要把学号存储为密匙,在代码中,它的变量名为 STUDENT_ID 。 打开你 Fork 后你的仓库。点击 Settings,然后点击 Secrets,点击 New repository secret,在 Name 中输入 STUDENT_ID,在 Value 中输入你的学号,点击 Add secret

2

3

4

5

然后点击添加。

按照上述步骤,你还需要添加下面内容,这些内容打开你平时报平安的界面,获取相同的即可:

STUDENT_ID # 学号,示例:201002401
PASSWORD # 密码,示例:abc123123123,也就是你的校园网密码
NAME # 姓名,示例:张三
SCHOOL # 学院,示例:信息学院
MAJOR # 专业,示例:计算机科学与技术
TYPE # 类型,示例:本科生 | 研究生
PHONE # 手机号,示例:12345678901
MASTER # 审核人,示例:李四
BUILDING # 宿舍楼栋,示例:11号楼
ROOM # 宿舍号,示例:901

以上添加完成后如图所示:

2

修改定时【可选】

yml 脚本中,我已经设置了每天 0:11 执行,为保证准确性,还添加了二次执行时间为早上 8:10. 不选择整点执行而选择在11分执行是为了避开高峰期。 你可以对其修改。在 .github/workflows 文件夹中,打开 main.yml 文件,修改 cron 的值即可。

name: autoReportPeace

on:
repository_dispatch:
types: [morning]
schedule:
- cron: '11 16,0 * * *'
# UTC 时间 16:11和 0:11对北京时间 0:11和 8:11
workflow_dispatch:
jobs:
#......

GitHub 采用世界标准时间,也就是 UTC 时间。北京时间是 UTC+8,因此北京时间比 UTC 时间早8小时。所以 UTC 0:00 对应的 北京 时间是 8:00。
也就是说,我们想要的时间减去8小时才是 UTC 时间。

corn的语法如下:

┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of the month (1 - 31)
│ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
│ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
* * * * *

例如,我们希望早上九点半执行,那么我们需要设置为 1:30:

corn: '30 1 * * *'

或者我们希望每天8:00和20:00执行:

corn: '0 0,12 * * *'

或者我们只希望每周三周五执行:

corn: '30 1 * * 3,5'

手动测试

除了定时测试外,我们还支持手动进行测试。你只需要打开 GitHub Action 界面。选中左侧的 autoReportPeace,点击右侧的下拉栏中的 Run workflow,即可手动执行。

2

3

接着,会有黄色的运行进程开始,直到进程变成蓝色的勾,表示运行成功。

3

延迟一到两分钟,你的微信会收到学校企业微信的报平安通知。表示部署成功,如果 不如预期,请检查上述步骤。

腾讯云加强定时【选做】

提高定时的准确性,可以不做。

但是有一个缺陷,就是 GitHub Action 的定时是不准确的,有时候会延迟十分钟甚至一两个小时。 不过我们通过两个时间点的定时,可以很大程度保证准确性。

2

Note: The event can be delayed during periods of high loads of GitHub Actions workflow runs. High load times include the start of every hour. To decrease the chance of delay, schedule your workflow to run at a different time of the hour.schedule

注意:在 GitHub Actions 工作流高负载运行期间,事件可能会延迟。高负载时间包括每小时的开始时等。为了减少延迟的可能性,请安排工作流在每小时的不同时间运行。

实测使用 GitHub Pages 的定时服务,延迟在 10 分钟到 1 小时不等,极端条件下定时不会执行

而 GitHub Actions 支持 workflow_dispatch 触发器(请参阅 GitHub Docs 上的触发工作流的事件,因此如果您手动触发工作流,它将立刻执行。这意味着您可以使用第三方 cron 调度服务,如 腾讯云、IFTTT、Google Cloud Scheduler 等,向 GithubAPI 发出请求以触发工作流。

那么这里将讲授一种使用腾讯云云函数定时触发的方法,增强其可靠性。如果你认为 GitHub定时两个时间点已经足够可靠,可以不再进行此操作

获取 GitHub Token

想要从远端操作 GitHub,我们需要一个 GitHub Token。

在你的 Github 主页,选择 setting
1
选择左下角的Developer settings
2
然后选择 Personal access tokens
3
填写相关信息,选择 无限期 , 勾选 repoworkflow 。点击创建。
4
然后生成完成,之后它会生成一串密码:
5
在你的 Github 主页,选择 setting
1
选择左下角的Developer settings
2
然后选择 Personal access tokens
3
填写相关信息,选择 无限期 , 勾选 repoworkflow 。点击创建。
4
然后生成完成,之后它会生成一串密码:
5

注意
请记住这一串密码,你将不再能看到它。

创建云函数

创建腾讯云函数

进入 腾讯云 并点击注册。可以使用微信登录。点击右上角进入控制台,搜索 云函数

2

点击新建云函数:

3

不选择模板,从头开始。

3

填写相关参数,选择环境 python

3

提高超时时间。

3

清空原有的代码,填入新的代码:

2

代码结构如下,你只需要修改 TOKEN (上面一步获取的) 这几参数以及 https://api.github.com/repos/你的用户名/reportPeace/dispatches 中的 GitHub 用户名即可。

# 腾讯云执行脚本,如不需要可忽略
import requests


def run():
headers = {
'Accept': 'application/vnd.github+json',
'Authorization': 'token 你的TOKEN',
# 注:'token' 前缀必不可少
}

data = '{"event_type": "autoReportPeace"}'

response = requests.post(f'https://api.github.com/repos/你的用户名/reportPeace/dispatches',
headers=headers, data=data)


# 云函数入口
def main_handler(event, context):
return run()

点击启用日志。

2

然后点击完成。进入详细界面的代码界面,点击测试。

3

查看下面的日志:

3

如果出现 204,则表示成功。其他状态码均为失败。

然后到 GitHub 仓库查看 Actions,可以看到已经成功触发了工作流。

2

勾变为蓝色,表示成功。

设置云函数定时触发

在侧边中创建定时:

2

点击创建触发器。

3

其中填写 Cron 表达式。

0 0 8 * * * *   # 每天 8 点
0 0 18 * * * * # 每天 18 点
0 0 7 * * 1-5 * # 周一到周五 7 点
0 30 6 * * * * # 每天 6 点半

点击提交后,会在每天指定时间触发执行。注意,腾讯云与 GitHub 不同,腾讯云 使用的是北京时间,这意味着你并不需要考虑时区问题。

一个函数而已,并不会消耗多少资源,所以不用担心。

· One min read
Chengzihan

:app:installDebug

问题描述

* What went wrong:
Execution failed for task ':app:installDebug'.
> java.util.concurrent.ExecutionException: com.android.builder.testing.api.DeviceException: com.android.ddmlib.InstallException: Unknown failure: Exception occurred while executing 'install':
android.os.ParcelableException: java.io.IOException: Requested internal only, but not enough space

最关键的一句:

Requested internal only, but not enough space

即:

申请的内部空间不足

解决方案

扩充模拟器空间。

1

在模拟器的设置中,将内部存储空间扩充到 4096M 即可。

访问错误

问题描述

error Failed to install the app. Make sure you have the Android development environment set up: https://reactnative.dev/docs/environment-setup.
Error: Command failed: gradlew.bat app:installDebug -PreactNativeDevServerPort=8081

无法正确访问仓库安装依赖。

解决方案

android 目录下的 build.gradle 文件中,将 android/build.gradle 中的 jcenter() 和 google() 分别替换为 maven { url 'https://maven.aliyun.com/repository/jcenter' }maven { url 'https://maven.aliyun.com/repository/google' }(注意有多处需要替换)。