Memos官网:memos
前言
一般来讲,Memos通过Docker来部署,所以想要部署使用Memos的话,最好有服务器。
如果你没有服务器的话,可以尝试使用小N同学和杜老师维护的公益项目:memos纯公益代部署服务。
简介
Memos是一个轻量级的自托管备忘录中心。开源和永久免费。可以说是支持 Docker 自部署的 flomo ,而且有 API 调取数据和发 Memos 。在短短两年的时间内,Memos就在GitHub上获得了20k的星星。
效果展示
说说页面展示:空间动态
清单页面展示:清单列表
本教程参考了Leonus和木木木木木关于Memos的文章,感兴趣的同学可以去看一看。
部署
下面是在服务器上使用docker进行的Memos部署方法。
第一步
新建一个文件夹memos,作为项目的根文件夹。
第二步
在根文件夹下新建文件docker-compose.yml
。 推荐使用docker-compose.yml
方式进行部署,方便制定数据储存位置及更新版本,其中使用${PWD}
指定路径为当前文件夹。
version: "3.0"
services:
memos:
image: neosmemo/memos:latest
container_name: memos
volumes:
- ${PWD}/.memos/:/var/opt/memos
ports:
- 5230:5230
第三步
以宝塔面板为例:
- 安装docker
宝塔面板好像已经自带了docker环境(不确定忘记了)
- 安装docker compose
pip install docker compose
验证docker compose是否安装成功
docker compose version
- 构建并启动容器
docker compose up -d
- 由于Memos是一个新的项目,还处于快速迭代的时期,所以我们可能会经常更新版本
docker compose down && docker image rm neosmemo/memos:latest && docker compose up -d
我们的数据都会存放在.memos
文件夹中,所以建议定时备份目录下的.memos
文件夹。
- 附
docker compose
的用法\命令:
用法:docker compose [OPTIONS] COMMAND
使用Docker定义并运行多容器应用程序。
OPTIONS:
命令 | 注释 |
---|---|
–ansi string | 控制何时打印ANSI控制字符(”never”|”always”|”auto”)(默认:”auto”) |
–compatibility | 在向后兼容模式下运行compose |
–dry-run | 在dry run 模式下执行命令 |
–env-file stringArray | 指定备用环境文件 |
-f, –file stringArray | 编写配置文件 |
–parallel int | 控制最大并行度,-1表示无限制(默认值-1) |
–profile stringArray | 指定要启用的配置文件 |
–progress string | 设置进度输出类型(auto、tty、plain、quiet)(默认为“auto”) |
–project-directory string | 指定备用工作目录(默认值:首次指定的Compose文件的路径) |
-p, –project-name string | 项目名称 |
COMMAND:
命令 | 注释 |
---|---|
build | 构建或重建服务 |
config | 分析、解析并呈现规范格式的compose文件 |
cp | 在服务容器和本地文件系统之间复制文件/文件夹 |
create | 为服务创建容器 |
down | 停止并移除容器、网络 |
events | 从容器接收实时事件 |
exec | 在运行的容器中执行命令 |
images | 列出创建的容器使用的图像 |
kill | 强制停止服务容器 |
logs | 查看容器的输出 |
ls | 列出正在运行的撰写项目 |
pause | 暂停服务 |
port | 打印端口绑定的公共端口 |
ps | 列出容器 |
pull | 拉取服务图像 |
push | 推送服务图片 |
restart | 重新启动服务容器 |
rm | 删除已停止的服务容器 |
run | 对服务运行一次性命令 |
start | 启动服务 |
stop | 停止服务 |
top | 显示正在运行的进程 |
unpause | 无偿服务 |
up | 创建和启动容器 |
version | 显示Docker Compose版本信息 |
wait | 阻塞,直到第一个服务容器停止 |
有关命令的详细信息,请运行docker compose COMMAND--help
第四步
添加反向代理:
操作如下:网站—>添加站点(之后的操作建立在自己创建的站点上面)—>设置—>反向代理—>添加反向代理
其中目标url填写的是http://自己的服务器ip:5230
Memos api
Memos api的格式如下:https://memos地址/api/v1/memo?creatorId=用户ID&tag=标签名&limit=限制数量
memos地址:
就是自己memos服务的域名地址,比如我的就是:memos.zhaozeyu.top
。
用户ID:
在自己的memos主页点击头像,点击RSS,可以获取到RSS地址如:https://memos.zhaozeyu.top/u/1/rss.xml,其中1就是自己的用户ID。
标签名:
自己在memos服务中创建的标签名,用来区分不同类型的备忘录,你可以自定义为“说说”、“清单”、“相册”等。
限制数量:
一个数字,用来限制要显示的数量。
完整memos api示例:https://memos.zhaozeyu.top/api/v1/memo?creatorId=1&tag=说说&limit=30
说说功能
在子比主题的网站中新建页面,选择自定义HTML
区块,然后嵌入一下代码(记得将Memos api改成自己的):
<style>
#memos-box {
width: 100%;
position: relative;
}
.memos-talk {
padding: 10px;
border: 1px solid lightgray;
border-radius: 20px;
position: absolute;
transition: all 0.2s linear;
}
.memos-talk:hover {
box-shadow: 0 0 12px rgba(0, 0, 0, 0.24);
}
.memos-meta {
display: flex;
align-items: center;
height: 60px;
}
.memos-avatar {
width: 60px!important;
height: 60px!important;
margin: 0 !important;
border-radius: 15px;
}
.memos-info {
margin-left: 10px;
display: flex;
flex-direction: column;
}
span.memos-nick {
color: var(--theme-color);
}
span.memos-date {
opacity: 0.6;
font-size: 1.4rem;
}
/* 中间内容 */
.memos-content {
margin-top: 10px;
}
.memos-images {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
.memos-img {
margin: 2px !important;
object-fit: cover;
aspect-ratio: 1 / 1;
}
.memos-files {
display: flex;
flex-wrap: wrap;
}
.memos-file {
margin: 5px;
padding: 2px 5px;
background-color: lightgray;
border: 1px lightgrey solid;
border-radius: 8px;
font-size: 1.2rem;
}
.memos-file:hover {
background-color: transparent;
transition: all 1s linear;
}
/* 底部 */
.memos-tag {
margin: 0 5px;
opacity: 0.6;
font-size: 1.4rem;
}
</style>
<div id="memos-box"></div>
<script>
const url = "https://memos.zhaozeyu.top";
fetch(`${url}/api/v1/memo?creatorId=1&tag=说说&limit=30`)
.then((res) => res.json())
.then((res) => {
let items = [];
let html = "";
res.forEach((item) => {
items.push(format(item));
});
items.forEach((item) => {
let imagesHtml = "";
let tagsHtml = "";
let resourcesHtml = "";
item.images.forEach((t, index) => {
imagesHtml += `<img class="memos-img" src="${t}" alt="${index}"/>`;
});
item.resources.forEach((t) => {
resourcesHtml += `<div class="memos-file"><a href="${t.url}" target="_blank">${t.filename}</a></div>`;
});
item.tags.forEach((t) => {
tagsHtml += `<span class="memos-tag">#${t}</span>`;
});
html += `<div class="memos-talk">
<div class="memos-meta">
<img
class="memos-avatar"
src="https://imagecloud.zhaozeyu.top/2023/10/07/652166bad3c71.webp"
alt="avatar"
/>
<div class="memos-info">
<span class="memos-nick">是zzy呀</span>
<span class="memos-date">${item.createDate}</span>
</div>
</div>
<div class="memos-content">
<div style="white-space: pre-wrap">${item.content}</div>
<div class="memos-images">${imagesHtml}</div>
<div class="memos-files">${resourcesHtml}</div>
</div>
<div class="memos-bottom">
<div>${tagsHtml}</div>
</div>
</div>`;
});
document.getElementById("memos-box").innerHTML = html;
setPositions();
setPositions();
});
/**
* 格式化
* @param {{createdTs: string, content: string, resourceList: [{filename: string, externalLink: string}]}} item
* @returns {{images: (string[]|*[]), resources: *[], content: string, createDate: string, tags: (string[]|*[])}}
*/
function format(item) {
// 创建日期
let createDate = getTime(new Date(item.createdTs * 1000).toString());
// 标签数组
let tagMatches = item.content.match(/\{(.*?)}/g);
let tags = tagMatches
? tagMatches.map((t) => t.replace(/^\{(.*)}$/, "$1"))
: [];
// 图片数组
let imageMatches = item.content.match(/!\[.*?]\(.*?\)/g);
let images = imageMatches
? imageMatches.map((t) => t.replace(/!\[.*?]\((.*?)\)/, "$1"))
: [];
// 去除 标签、图片、自定义tag
let content = item.content
.replace(/#(.*?)\s/g, "")
.replace(/!\[(.*?)]\((.*?)\)/g, "")
.replace(/\{(.*?)}/g, "")
.replace(/^\s+|\s+$/g, "")
.replace(
/\[(.*?)]\((.*?)\)/g,
(match, alt, url) => `<a href=${url}>${alt}</a>`
);
// 资源数组
let resources = [];
if (item.resourceList.length > 0) {
item.resourceList.forEach((t) => {
resources.push({
filename: t.filename,
url: t.externalLink
? t.externalLink
: `${url}/o/r/${t.id}/${t.publicId}/${t.filename}`,
});
});
}
return {
createDate,
content,
tags,
images,
resources,
};
}
// 时间转化
function getTime(time) {
let d = new Date(time);
let ls = [
d.getFullYear(),
d.getMonth() + 1,
d.getDate(),
d.getHours(),
d.getMinutes(),
d.getSeconds(),
];
for (let i = 0; i < ls.length; i++) {
ls[i] = ls[i] <= 9 ? "0" + ls[i] : ls[i] + "";
}
if (Number(ls[0]) === new Date().getFullYear()) {
return ls[1] + "月" + ls[2] + "日 " + ls[3] + ":" + ls[4];
} else {
return (
ls[0] + "年" + ls[1] + "月" + ls[2] + "日 " + ls[3] + ":" + ls[4]
);
}
}
// 瀑布流布局
// 计算一共有多少列,以及每一列之间的间隙
function cal() {
let boxElement = document.getElementById("memos-box");
let boxWidth = boxElement.clientWidth; // box容器的宽度
// 计算列的数量
let columns;
if (boxWidth > 1536) {
columns = 5;
} else if (boxWidth > 1280) {
columns = 4;
} else if (boxWidth > 1024) {
columns = 3;
} else if (boxWidth > 768) {
columns = 2;
} else if (boxWidth > 512) {
columns = 2;
} else if (boxWidth > 375) {
columns = 1;
} else {
columns = 1;
}
// 计算间隙个数,两边不设置间隙
let spaceNum = columns - 1; // 间隙数量
// 定义每一个间隙的宽度
let spaceWidth = 10;
// 计算 talk 的宽度
let talkWidth = Math.floor((boxWidth - spaceNum * spaceWidth) / columns);
return {columns, talkWidth};
}
// 设置每个 talk 的位置
function setPositions() {
let boxElement = document.getElementById("memos-box");
let talksElement = document.querySelectorAll(".memos-talk");
// 得到列数和间隙空间
let info = cal();
// 该数组的长度位列数,每一项表述该列的下一个图片的纵坐标
let nextTops = new Array(info.columns);
nextTops.fill(10); // 初始化,每一项都为0
for (let i = 0; i < talksElement.length; i++) {
let talk = talksElement[i];
// 找到 nextTops最小值作为当前图片的纵坐标
let minTop = Math.min.apply(null, nextTops);
talk.style.width = `${info.talkWidth}px`;
talk.style.top = `${minTop}px`;
// 检测 talk 中是否有 images 元素
let imagesElement = talk
.querySelector(".memos-content")
.querySelector(".memos-images");
let imgWidth = (imagesElement.clientWidth - 12) / 3;
if (imagesElement.children.length === 1) {
imagesElement.children[0].style.aspectRatio = "auto";
imagesElement.children[0].style.height = `${imgWidth}px`;
imagesElement.children[0].style.width = "auto";
} else {
for (let j = 0; j < imagesElement.children.length; j++) {
imagesElement.children[j].style.width = `${imgWidth}px`;
}
}
// 更新数组这一项的的下一个top值
// 得到使用的是第几列的top值
let index = nextTops.indexOf(minTop);
nextTops[index] += talk.clientHeight + 10;
// 横坐标
let left = index * (info.talkWidth + 10);
talk.style.left = `${left}px`;
}
// 求最大值
let maxTops = Math.max.apply(null, nextTops);
// 这只容器的高度
boxElement.style.height = `${maxTops}px`;
}
// 检测窗口尺寸变化
let timeId = null;
window.onresize = function () {
if (timeId) {
clearTimeout(timeId);
}
timeId = setTimeout(setPositions, 250);
};
</script>
发表说说的格式如下:
#说说 {说说}{测试}
memos说说功能测试
超链接测试
图片九宫格测试
文件上传测试
[Memos](https://memos.zhaozeyu.top)
![夜晚雨天街道女孩.jpg](https://imagecloud.zhaozeyu.top/2023/09/03/64f48c61901a8.jpg)
![蓝眼睛的猫女孩.jpg](https://imagecloud.zhaozeyu.top/2023/09/03/64f48b1f10e7a.jpg)
![微信图片_20230919221351.webp](https://imagecloud.zhaozeyu.top/2023/09/19/6509adfe72008.webp)
![微信图片_20230919221354.webp](https://imagecloud.zhaozeyu.top/2023/09/19/6509adff97992.webp)
![微信图片_20230919221352.webp](https://imagecloud.zhaozeyu.top/2023/09/19/6509ae0004d60.webp)
![avatar.webp](https://imagecloud.zhaozeyu.top/2023/10/07/652166bb173ae.webp)
![git.webp](https://imagecloud.zhaozeyu.top/2023/10/07/652166bad3c71.webp)
![小N同学.webp](https://imagecloud.zhaozeyu.top/2023/10/07/652166badb01c.webp)
![森鹿语.webp](https://imagecloud.zhaozeyu.top/2023/10/07/652166bae2cc7.webp)
选择标签 —> 大括号包裹自定义标签 —> 回车后开始写正文,链接和图片都是用markdown书写格式。
目前实现了文字、超链、图片、文件的展示。
清单功能
操作同上面的说说功能,嵌入代码如下:
发表清单的格式如下:
#清单 {想去的地方}
- [x] 甘坑客家小镇
- [x] 大芬油画村
- [x] 文博宫
- [x] 华为坂田研发总部
- [ ] 大小梅沙海滨公园
- [ ] 深圳湾公园
- [ ] 深圳人才公园
- [ ] 深圳天文台栈道
- [ ] 深圳博物馆
- [ ] 南山博物馆
- [ ] 梧桐山
- [ ] 长白山
- [ ] 黑龙江冰雪世界
选择标签 —> 大括号写清单的标题 —> 回车开始写清单,一样使用markdown书写格式。
暂无评论内容