sheng00 的所有文章

使用 GitHub Actions 定时自动备份数据库(MySQL / MongoDB)

使用 GitHub Actions 定时自动备份数据库(MySQL / MongoDB)

在日常开发或部署中,数据库备份是保证数据安全的重要环节。但手动备份不仅麻烦,也容易忘记。好在我们可以利用 GitHub Actions 的定时任务(schedule)能力 每天自动备份数据库,并上传到 GitHub、OSS 或服务器。

本文将介绍如何:

  1. 使用 GitHub Actions 定时执行备份
  2. 通过 SSH 登录服务器执行备份脚本
  3. 或直接在 GitHub Actions 中连接远程数据库备份
  4. 将备份文件上传到 GitHub Releases / 阿里云 OSS / AWS S3

一、准备工作

1. 在 GitHub 仓库中配置 Secrets

进入:

Settings → Secrets and variables → Actions → New repository secret

需要设置:

Secret 名称 说明
SSH_PRIVATE_KEY 用于连接服务器(如果你在服务器执行备份)
DB_HOST 数据库地址
DB_USER 数据库用户名
DB_PASSWORD 数据库密码
DB_NAME 需要备份的库名

如果你在 GitHub Actions 内部直连数据库,则只需 DB 相关的 secret。


二、方法一:通过 SSH 登录服务器备份(最佳方案)

这种方式最稳定:
✔ 不暴露数据库端口
✔ 备份在服务器本地执行
✔ 支持 Docker 环境、物理机、云主机

1)服务器备份脚本 example

创建 /root/backup/mysql_backup.sh

#!/bin/bash
set -e

DATE=$(date +"%Y%m%d_%H%M%S")
BACKUP_DIR="/root/backups/mysql"
mkdir -p $BACKUP_DIR

# 备份文件名
FILE="$BACKUP_DIR/${DATE}.sql.gz"

echo "开始备份: $FILE"

mysqldump -u root -p'密码改为你的' --databases gfds | gzip > "$FILE"

echo "备份完成:$FILE"

2)GitHub Actions workflow

.github/workflows/db-backup.yml

name: Database Backup

on:
  schedule:
    - cron: "0 18 * * *" # 每天 02:00(中国时间 UTC+8)
  workflow_dispatch:

jobs:
  backup:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup SSH
        uses: webfactory/ssh-agent@v0.8.0
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: Add server to known hosts
        run: |
          mkdir -p ~/.ssh
          ssh-keyscan -H your-server-ip >> ~/.ssh/known_hosts

      - name: Execute backup script on server
        run: |
          ssh root@your-server-ip "bash /root/backup/mysql_backup.sh"

只要配置好 SSH key,这个 workflow 就会每天自动执行
备份会保存在服务器 /root/backups/mysql/


三、方法二:在 GitHub Actions 中直接备份远程数据库

适用于支持外网访问数据库的场景。

MySQL 备份示例

name: Backup MySQL

on:
  schedule:
    - cron: "0 18 * * *"
  workflow_dispatch:

jobs:
  backup:
    runs-on: ubuntu-latest
    steps:
      - name: Install MySQL client
        run: sudo apt-get update && sudo apt-get install -y mysql-client

      - name: Backup database
        run: |
          FILE="backup_$(date +%Y%m%d_%H%M%S).sql.gz"
          mysqldump -h ${{ secrets.DB_HOST }} \
            -u ${{ secrets.DB_USER }} \
            -p${{ secrets.DB_PASSWORD }} \
            ${{ secrets.DB_NAME }} | gzip > $FILE
          echo "备份文件:$FILE"

      - name: Upload backup as artifact
        uses: actions/upload-artifact@v4
        with:
          name: mysql-backup
          path: "*.sql.gz"

四、方法三:备份 MongoDB

在服务器执行备份

mongo_backup.sh

DATE=$(date +"%Y%m%d_%H%M%S")
DIR="/root/backups/mongo"
mkdir -p $DIR

mongodump --gzip --archive="$DIR/$DATE.gz"

echo "MongoDB 备份完成:$DIR/$DATE.gz"

GitHub Actions 同上,只是换一下脚本名。


五、上传备份到 GitHub / OSS / S3

你可以在 backup job 最后追加:

上传到 GitHub Releases

      - name: Upload to GitHub Releases
        uses: softprops/action-gh-release@v2
        with:
          tag_name: "backup-${{ github.run_id }}"
          files: "*.gz"

上传到阿里云 OSS

      - name: Upload to Aliyun OSS
        uses: manyuanrong/aliyun-oss-website-action@v1.1.9
        with:
          accessKeyId: ${{ secrets.OSS_ID }}
          accessKeySecret: ${{ secrets.OSS_SECRET }}
          bucket: backup-bucket
          endpoint: oss-cn-hangzhou.aliyuncs.com
          folder: db-backups
          localFolder: .

六、总结

GitHub Actions + 定时任务(cron)可以轻松实现自动化数据库备份
根据你的系统结构,你可以选择:

场景 推荐方案
服务器上有数据库 SSH 登录服务器执行备份(最安全)
数据库允许外网访问 在 GitHub Actions 中直接备份
需要长期保存备份 上传 GitHub Releases / OSS / S3

前端使用浏览器实现asr语音识别


// 创建一个新的SpeechRecognition对象 var recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition || window.mozSpeechRecognition || window.msSpeechRecognition)(); recognition.lang = 'en-US'; // 设置语言 recognition.interimResults = false; // 设置是否返回临时结果 recognition.maxAlternatives = 1; // 设置返回的最大替代词数量 // 当识别到语音结束时,返回结果 recognition.onresult = function(event) { console.log('You said: ', event.results[0][0].transcript); }; // 开始语音识别 recognition.start();

demo

 

react-native仿微信通讯录右侧边栏快速定位功能

代码地址 -> ReactNativeCountrySelect

1. 界面

SectionList把数据渲染出来

右边的A-ZText组件即可,这里为了做滑动定位,没有选择Touchable组件

import * as React from 'react';
import {
  Text,
  View,
  StyleSheet,
  SectionList,
  SafeAreaView,
} from 'react-native';

import countries from './countryCode.json';
const sectionMapArr = [
  ['A', 0],
  ['B', 1],
  ['C', 2],
  ['D', 3],
  ['E', 4],
  ['F', 5],
  ['G', 6],
  ['H', 7],
  ['I', 8],
  ['J', 9],
  ['K', 10],
  ['L', 11],
  ['M', 12],
  ['N', 13],
  ['O', 14],
  ['P', 15],
  ['Q', 16],
  ['R', 17],
  ['S', 18],
  ['T', 19],
  ['U', 20],
  ['V', 21],
  ['W', 22],
  ['X', 23],
  ['Y', 24],
  ['Z', 25],
];
export default class App extends React.Component {
  render() {
    return (
      <SafeAreaView style={styles.container}>
        <SectionList
          containerStyle={{ flex: 1, justifyContent: 'center' }}
          ItemSeparatorComponent={() => (<View style={{ borderBottomColor: '#F8F8F8', borderBottomWidth: 1, }} />)}
          renderItem={({ item, index, section }) => (
            <View style={styles.itemContainer}>
              <Text style={styles.itemText} key={index}>
                {item.countryName}
              </Text>
            </View>
          )}
          renderSectionHeader={({ section: { key } }) => (
            <View style={styles.headerContainer}>
              <Text style={styles.headerText}>{key}</Text>
            </View>
          )}
          sections={countries}
          keyExtractor={(item, index) => item + index}
        />
        <View
          style={{ width: 16, justifyContent: 'center' }}
        >
          {sectionMapArr.map((item, index) => {
            return (
              <Text
                key={index}
              >
                {item[0]}
              </Text>
            );
          })}
        </View>
      </SafeAreaView>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#FFFFFF',
    flexDirection: 'row',
  },
  headerContainer: {
    padding: 5,
    backgroundColor: '#F8F8F8',
  },
  headerText: {
    fontWeight: 'bold',
  },
  itemContainer: {
    paddingHorizontal: 5,
    paddingVertical: 10,
  },
  itemText: {
    fontSize: 16,
  },
});

这时候界面已经完成了,然后就是增加触摸滑动定位的功能了。

2. 使用Gesture Responder System监听触摸事件

给右侧的View上启用手势


<View style={{ width: 16, justifyContent: 'center' }} onStartShouldSetResponder={() => true} onMoveShouldSetResponder={() => true} > {sectionMapArr.map((item, index) => { return ( <Text key={index} > {item[0]} </Text> ); })} </View>

这样我们就可以在触摸滑动的时候,获得滑动到的位置

3. 使用onLayout找到每个字母对应的X、Y

首先在constructor里声明一个实例属性,用来记录每个字母的信息:

this.ps = [];

然后在Text组件上利用onLayout获得每个字母的位置,并且存到this.ps里:

<Text
  key={index}
  onLayout={({
    nativeEvent: {
      layout: { x, y, width, height },
    },
  }) => {
    this.ps = this.ps.filter(i => i.key !== item[0]); 
    this.ps.push({
      key: item[0],     // 对应的字母 A-Z
      min: y,           // 字母顶部Y坐标
      max: y + height,  // 字母底部Y坐标
      index: item[1],   // 字母对应SectionList的index
    });
  }}
>
  {item[0]}
</Text>

4. 根据滑动找到滑到哪个字母上

<View
  style={{ width: 16, justifyContent: 'center' }}
  onStartShouldSetResponder={() => true}
  onMoveShouldSetResponder={() => true}
  onResponderMove={({ nativeEvent: { pageY } }) => {
    const offsetY = pageY - this.offsetY;
    const find = this.ps.find(
      i => i.min < offsetY && i.max > offsetY
    );
    if (find) {
      console.log(find) // 滑动到的字母
    }
  }}
>

5. 根据触摸的字母,SectionList跳到对应的位置

  1. 先在constructor里创建ref
this.sectionlist = React.createRef();
  1. 在SectionList上绑定ref
ref={this.sectionlist}
  1. 调用SectionList的scrollToLocation
onResponderMove={({ nativeEvent: { pageY } }) => {
  const offsetY = pageY - this.offsetY;
  const find = this.ps.find(
    i => i.min < offsetY && i.max > offsetY
  );
  if (find) {
    this.sectionlist.current.scrollToLocation({
      sectionIndex: find.index,
      itemIndex: 0,
      animated: false,
    });
  }
}}

完工

react native改变WebView背景颜色

react-native的WebView背景颜色是无法改变的,不过我们可以用另一个View盖住WebView,达到改变颜色的效果。

import React from 'react';
import {View, StyleSheet, WebView, ActivityIndicator} from 'react-native';

export default class YourComponent extends React.PureComponent{

    state = {
        loading: true,
    };

    render() {
        return (
            <View>
                <WebView
                    source={html}
                    onLoadEnd={() => {
                        this.setState({loading: false});
                    }}
                />
                {
                    this.state.loading && (
                        <View style={{
                            ...StyleSheet.absoluteFillObject,
                            backgroundColor: '#000000',  // your color 
                            alignItems: 'center',
                            justifyContent: 'center',
                        }}>
                            <ActivityIndicator />
                        </View>
                    )
                }
            </View>
        )

    }
}

Chrome中from memory cache和from disk cache

Chrome中文件缓存有Memory Cache和Disk Cache两种

顾名思义

  • Memory Cache:放在内存中的缓存

  • Disk Cache:放在硬盘上的缓存

一些规律:

  1. 第一次打开页面的时候,是没有缓存的,直接请求资源

  2. 刷新页面(⌘+R)的时候,会发现有些文件是from memory cache,有些是from disk cache

  3. 关掉浏览器,再打开页面,没有memory cache了

  4. 无痕窗口下,资源都会放在memory cache,关掉窗口缓存就没了,不会留下痕迹

  5. 图片会优先放进memory cache

  6. 小文件会优先放进memory cache

  7. 大文件几乎是disk cache

最后送上一张http缓存的图片

iOS开发:在Swift中使用Alamofire发送HTTP请求

创建项目

  1. 打开Xcode,点击Create a new Xcode project

  2. 选择Single View App,点击Next

  3. 输入Product Name:demo,Language选择Swift,点击Next

  4. 选择一个目录存放你的项目

使用Pod安装Alamofire

  1. 关掉Xcode

  2. 在你的项目目录里,创建一个文件Podfile

  3. 输入

    source 'https://github.com/CocoaPods/Specs.git'
    platform :ios, '10.0'
    use_frameworks!
    
    target 'demo' do
        pod 'Alamofire', '~> 4.7'
    end
    
  4. 在终端中输入:pod install

  5. 打开demo目录,双击demo.xcworkspace打开项目

使用Alamofire发送HTTP请求

  1. 打开文件ViewController.swift

  2. 在viewDidLoad函数内调用Alamofire

    Alamofire.request("https://api.github.com/gists").responseJSON { response in
        print("Request: \(String(describing: response.request))")   // original url request
        print("Response: \(String(describing: response.response))") // http url response
        print("Result: \(response.result)")                         // response serialization result
    
        if let json = response.result.value {
            print("JSON: \(json)") // serialized json response
        }
    
        if let data = response.data, let utf8Text = String(data: data, encoding: .utf8) {
            print("Data: \(utf8Text)") // original server data as UTF8 string
        }
    }
    
  3. ⌘+R启动APP,可以看到输出

使用Gatsby生成静态网站并部署在GitHub上

什么是Gatsby

Blazing-fast static site generator for React

Gatsby是一个基于React极其快的静态网站生成工具

支持各种数据源,markdown、Wordpress等

创建网站

  1. 安装Gatsby命令行工具:
    npm install --global gatsby-cli
    
  2. 创建一个新的网站
    gatsby new sheng00.cn
    
  3. 运行刚才创建的网站
    cd sheng00.cn
    gatsby develop
    

    打开localhost:8000即可看到

部署在GitHub上

  1. 在GitHub上创建一个repository

  2. 在刚才生成的网站运行下面的命令

    git add -A
    git commit -m "first commit"
    git remote add origin git@github.com:shengoo/sheng00.cn.git
    git push -u origin master
    
  3. 安装gh-pages
    yarn add gh-pages
    
  4. 在package.json里增加一个脚本
    "deploy": "gatsby build --prefix-paths && gh-pages -d public"
    
  5. 部署到GitHub
    yarn deploy
    

设置自定义域名

  1. 从域名注册商那里,把域名指向yourusername.github.io

  2. 在GitHub的repository的设置里,设置Custom Domain:www.sheng00.cn

  3. 在Gatsby生成的网站中,新建一个目录static,创建一个文件CNAME

    www.sheng00.cn
    
  4. 再次部署
    yarn deploy
    

源代码网址:https://github.com/shengoo/sheng00.cn

在react应用中使用模块化css

什么是模块化CSS?

模块化管理CSS,避免全局污染,实现模块化、可服用。

在react中应用

启用CSS Modules

css-loader中增加一个选项:

{
    loader: require.resolve('css-loader'),
    options: {
        importLoaders: 1,
        minimize: true,
        sourceMap: shouldUseSourceMap,
        modules: true, // 启用CSS Modules
    },
},

CSS文件

app.css:

.title{
    color: red;
}

最后会编译成:

._2L1SLeGPg5sisdRmqO9mCH {
  color: red;
}

JavaScript文件

app.js:

import styles from './app.css';

class App extends Component {
  render() {
    return <div className={styles.title}>Hello World.</div>;
  }
}

最后会编译成:

<div class="_2L1SLeGPg5sisdRmqO9mCH">Hello World.</div>

fetch 用法简介

Fetch API提供了一个获取资源的接口(包括跨域)。任何使用过 XMLHttpRequest 的人都能轻松上手,但新的API提供了更强大和灵活的功能集。

参数

第一个参数是URL地址。

第二个参数(可选)是fetch使用的选项(method、headers等选项)

返回结果

fetch会返回一个promise对象,resolve对应请求的Response。

response属性:

  1. body
  2. bodyUsed

    一个布尔值来标示该Response是否读取过Body

  3. headers

    此Response所关联的Headers 对象.

  4. ok

    一个布尔值来标示该Response成功(状态码200-299) 还是失败.

  5. redirected

    该Response是否来自一个重定向,如果是的话,它的URL列表将会有多个

  6. status

    Response的状态码 (例如, 200 成功).

  7. statusText

    与该Response状态码一致的状态信息 (例如, OK对应200).

  8. type

    Response的类型 (例如, basic, cors).

  9. url

    Response的URL.

response方法

  1. arrayBuffer()

    读取 Response对象并且将它设置为已读(因为Responses对象被设置为了 stream 的方式,所以它们只能被读取一次) ,并返回一个被解析为ArrayBuffer格式的promise对象

  2. blob()

    读取 Response对象并且将它设置为已读(因为Responses对象被设置为了 stream 的方式,所以它们只能被读取一次) ,并返回一个被解析为Blob格式的promise对象

  3. formData()

    读取Response对象并且将它设置为已读(因为Responses对象被设置为了 stream 的方式,所以它们只能被读取一次) ,并返回一个被解析为FormData格式的promise对象

  4. json()

    读取 Response对象并且将它设置为已读(因为Responses对象被设置为了 stream 的方式,所以它们只能被读取一次) ,并返回一个被解析为JSON格式的promise对象

  5. text()

    读取 Response对象并且将它设置为已读(因为Responses对象被设置为了 stream 的方式,所以它们只能被读取一次) ,并返回一个被解析为USVString格式的promise对象

示例:读取一个图片并生成URL

var myImage = document.querySelector('.my-image');
fetch('flowers.jpg').then(function(response) {
  return response.blob();
}).then(function(response) {
  var objectURL = URL.createObjectURL(response);
  myImage.src = objectURL;
});

get请求

fetch('http://example.com/movies.json')
  .then(function(response) {
    return response.json();
  })
  .then(function(myJson) {
    console.log(myJson);
  });

post请求

fetch(url, {
  method: 'POST',
  body: JSON.stringify(data),
  headers: new Headers({
    'Content-Type': 'application/json'
  })
})

上传文件&提交表单

var formData = new FormData();
var fileField = document.querySelector("input[type='file']");

formData.append('username', 'abc123');
formData.append('avatar', fileField.files[0]);

fetch('https://example.com/upload', {
  method: 'PUT',
  body: formData
})
.then(response => response.json())
.catch(error => console.error('Error:', error))
.then(response => console.log('Success:', response));