{"id":13672956,"url":"https://github.com/Somainer/Roselia-Blog","last_synced_at":"2025-04-28T04:30:42.061Z","repository":{"id":25548594,"uuid":"104496317","full_name":"Somainer/Roselia-Blog","owner":"Somainer","description":"A blog engine. Code for roselia.moe/blog","archived":false,"fork":false,"pushed_at":"2023-02-11T18:14:21.000Z","size":50680,"stargazers_count":10,"open_issues_count":33,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-11-11T11:45:26.531Z","etag":null,"topics":["blog-engine"],"latest_commit_sha":null,"homepage":"https://roselia.moe/blog","language":"Vue","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Somainer.png","metadata":{"files":{"readme":"README-CN.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2017-09-22T16:35:03.000Z","updated_at":"2024-04-18T12:44:27.000Z","dependencies_parsed_at":"2024-01-17T04:19:01.042Z","dependency_job_id":null,"html_url":"https://github.com/Somainer/Roselia-Blog","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Somainer%2FRoselia-Blog","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Somainer%2FRoselia-Blog/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Somainer%2FRoselia-Blog/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Somainer%2FRoselia-Blog/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Somainer","download_url":"https://codeload.github.com/Somainer/Roselia-Blog/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251251833,"owners_count":21559668,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["blog-engine"],"created_at":"2024-08-02T09:02:00.725Z","updated_at":"2025-04-28T04:30:41.011Z","avatar_url":"https://github.com/Somainer.png","language":"Vue","readme":"# Roselia-Blog\n\nRoselia-Blog 是一个博客引擎，前端大部分基于 `TypeScript` 编写，后端使用`Python`编写。\n\nA single page app with slight SEO optimization, can also be deployed as an anti-SEO blog.\n\n虽然是一个单页APP，但是也能够做到搜索引擎优化，同时正因为是SPA，可以进行反搜索引擎优化部署，阻止搜索引擎爬虫爬取你的文章。\n\n\u003e Demo: [Roselia-Blog](https://roselia.moe/blog/)\n\n[English](./README.md) | 简体中文\n\n### Usage（最简版）:\n\u003e * 安装python3.6+ \u0026 node \u0026 yarn\n\u003e * 设置 api_server/config.py \u0026\u0026 frontend/src/common/config.js\n\u003e * **!重要!** 在`secret.py`里面填写`APP_KEY`\u0026`APP_SALT`或者改为`gen_key()`每次在启动时改变。\n\u003e * 将你需要的图片放在 static_assets/img 里面\n\u003e * api_server/roselia.py build\n\u003e * RUN api_server/roselia.py run-prod (For production)\n\u003e * Access localhost:5000\n\n## 使用方法\n\n### 依赖项\n* `Python 3.6+` （因为使用了字符串插值）\n* `NodeJS` （为了编译前端）\n* `Yarn pkg`（NodeJS的包管理器）\n\n接下来可以通过`pip install -r requirements.txt`来安装python的依赖包。\n接下来切换到`./frontend`，执行`yarn`.\n\n### 设置\n\n在`api_server/config.py`，有如下内容供设置：\n* `BLOG_LINK`: 博客链接\n* `BLOG_INFO`: 自定义标题等 (以供服务器端渲染)\n* `DEBUG`: 如果你想在生产环境中使用他，请确保该选项为 `False`.\n* `ANTI_SEO`: 如果为`True`，关闭SEO优化，这样搜索引擎爬虫将无法获取信息。注意某些支持执行JavaScript脚本的爬虫（比如Google）仍然有可能获取内容。\n* `HOST`: 监听地址，默认 `0.0.0.0`.\n* `PORT`: 监听端口，默认 5000.\n* `DB_PATH`: 服务器数据库地址。\n* `UPLOAD_DIR`: 图片上传文件夹，留空以关闭上传到服务器的功能。\n\n在`api_server/secret.py`中，有如下内容以供设置：\n* `APP_KEY` \u0026 `APP_SALT`: 生成token所需的app key 和 secret。 将其改为 `gen_key()`，每次启动生成不同的值，或者设定一个自定义的字符串值。\n* `GITHUB_CLIENT_ID` \u0026 `GITHUB_CLIENT_SECRET`: GitHub OAuth 所需的id和secret，留空则关闭。\n* `MICROSOFT_CLIENT_ID` \u0026 `MICROSOFT_CLIENT_SECRET`: Microsoft Accout OAuth 所需的id和secret，留空则关闭。\n* `CHEVERETO_API_KEY` \u0026 `CHEVERETO_API_ENDPOINT`: 上传到chevereto服务器所需的API和API key，留空以关闭。\n* `SM_MS_API_TOKEN`: 上传到 `sm.ms` 图片服务的API token。留空关闭。\n\n在 `frontend/src/common/config.js`，有如下内容供设置 (在底下的 `export default` 语句中):\n* `title`: 博客标题\n* `motto`: 博客座右铭\n* `apiBase`: 网站API网址，默认 `/api`.\n* `theme`: 博客主题\n* `enableRoseliaScript`: 控制是否启用文章内嵌脚本。\n* `enableAskYukina`: 控制是否启动助手，请将其设为false，该功能仍然在开发中。\n* `footName`: 网页页脚的内容。\n* `urlPrefix`: 网址前缀，默认空。\n* `images.indexBannerImage`: 主页图片\n* `images.lazyloadImage`: 图片懒加载时的占位图片\n* `images.timelineBannerImage`: 时间轴页的图片\n\n### 关于`api_server/roselia.py`\n\n`roselia.py` 提供了一些指令便于启动或者构件环境。\n用法：`roselia.py [command]`\n\nCommands: \n\u003e * serve: 根据 `DEBUG` 与否启动开发或者生产环境。\n\u003e * run-dev: 强制启动开发环境\n\u003e * run-prod: 强制启动生产环境\n\u003e * run-gunicorn: 使用`gunicorn`启动环境。\n\u003e * compress-assets: **删除原有的static文件夹**，将`static_assets`复制到`static`，压缩其中的图片\n\u003e * copy-assets 将**已经build完成的**前端文件复制到static，并且替换template中的CSS/JS文件名\n\u003e * build-frontend 编译前端\n\u003e * build = compress-assets + build-frontend + copy-assets\n\u003e * assets = compress-assets + copy-assets\n\n### 关于启动开发环境\n启动开发环境，你需要先启动`yarn serve`，再启动`roselia.py run-dev`\n\n目前正在逐步迁移到`TypeScript`\n\n## 特点\n\n如果你经常在文章中写代码或者公式，那么该博客十分适合你，因为Roselia-Blog\n对理工科用户特别友好，原生支持：\n\n* 代码高亮\n* 内嵌公式\n* 侧边栏导航\n* 站内文章链接预览\n* 文章内引用文献预览\u0026跳转\n* 黑幕支持（~黑幕内容~，格式为用`~`围绕的文字）\n* 文章内迷你脚本\u0026字符串插值 r{{ 表达式 }}（roselia-script）\n* 通过粘贴或者拖拽来上传图片\n* 直接粘贴来自VSCode中的代码片段（自动识别语言）\n* 隐藏文章：只能通过链接访问。\n* 私密文章：只有相应等级的用户才能访问\n* 第三方账号登陆：支持GitHub和微软账户\n* 二步验证\n\n## roselia-script(~~其实只是普通的字符串插值~~)\n\n语法： (Roselia|roselia|r|R){{(expression)}}\n\n然后这个表达式的返回值将被插入到对应位置中，如果该表达式以分号为结尾，就将被视为语句，其结果将被丢弃。\n\n因为Roselia-Blog不开放注册，因此我们将完全信任所有的用户，该功能对所有用户的文章和评论开放。\n该功能可被用来做字符串插值，开关某些功能或者动态修改文章或评论的元信息。\n\n存在内置api\n\n`def (name, func)` 定义函数，或者为api结果命名\n\n`music (meta, autoplay = false, onPlayerReady = null)` 插入一首歌（基于APlayer）\n\n`onceLoad (fn)`: 定义文章加载完成后的回调\n\n`onceUnload (fn)`: 文章销毁时的回调\n\n`getElement (el)`: 获取对应的元素\n\n`btn (text, onClick, externalClasses = '', externalAttributes?: object)`：插入一个按钮\n\n`toast (text, color)`: 显示一个toast通知\n\n`then (fn)`: 在DOM完成后执行（对DOM的操作请务必在then中进行，除非你确信此时已经有这个元素了）\n\n`audio (src)` 插入一段原生audio Element\n\n`importJS (url, onComplete)`: 插入外置JS代码（会影响到之后的文章，慎用）\n\n`element (el: RSElementSelector): Promise\u003cHTMLElement\u003e`\n\n```typescript\ncreateElement\u003cK extends keyof HTMLElementTagNameMap\u003e(\n    type: K,\n    extend?: RecursivePartial\u003cHTMLElementTagNameMap[K]\u003e,\n    children?: (Node | string)[]\n): HTMLElementTagNameMap[K]\n```\n创建类型为`type`的元素，将其属性根据`extend`赋值，最后加入`childen`作为子元素。\n\n`createTextNode(text: string): Text`: 创建文本节点\n\n`Y`: 大名鼎鼎的Y-组合子[Y-Combinator](https://roselia.moe/blog/post?p=30)\n\n`currentTheme()` 获取当前主题\n\n`changeThemeOnce(theme: Partial\u003ctypeof config.theme\u003e): void` 改变主题，在切换文章时还原\n\n`changeTheme(theme: Partial\u003ctypeof config.theme\u003e): void` 直到刷新之前，改变主题（需要征求`theme`权限）\n\n`resetTheme(): void` 重置主题（需要征求`theme`权限）\n\n`saveCurrentTheme(): void` 保存当前主题 （需要征求`theme`权限）\n\n`switchToColorMode(isLight: boolean)`: 改变颜色模式，如果`isLight`为真则切换为浅色模式，否则切换为深色模式。\n\n`async forceSwitchToColorMode(light: boolean)`: 切换颜色模式直到刷新。\n\n```typescript\nchangeExtraDisplaySettings(settings: Partial\u003c{\n    metaBelowImage: boolean,\n    blurMainImage: boolean,\n    disableSideNavigation: boolean\n  }\u003e)\n```\n改变额外的显示设置：\n* `metaBelowImage` 控制文章的信息是否应该处于图片的下方\n* `blurMainImage` 控制文章主图片是否应该模糊\n* `disableSideNavigation` 控制是否关闭侧边导航\n\n`sendNotification(notification: INotification)`: 向用户发送通知 (通过通知总线)\n\n`hyperScript`是createElement的简便操作，该函数使得我们不需要将children以数组的形式传入，直接使用参数就行，此外，如果props为空，则可以直接不传该prop。访问其属性可以获得一个函数，可以直接构造出那个HTML元素。在大多数情况下，我们直接将其命名为`h`，可以精简代码。一般我们这样可以精简：`def('h', hyperScript);`下面看一个例子：\n```JavaScript\ndefState('userName', ''),\nhyperScript.div(\n    hyperScript.h1('Hello', ' ', 'World!'),\n    hyperScript.span({\n        className: 'heimu'\n    }, 'This content is hidden.'),\n    'Who are you?',\n    hyperScript('input', {\n        value: userName,\n        onInput() {\n            userName = this.value;\n        }\n    }),\n    hyperScript('button', {\n        onClick() {\n            userName = '';\n            toast('Submitted!', 'success')\n        }\n    }, 'Submit')\n)\n```\n\n### Hooks\nRoselia-Script 支持类似于`React`的hook。这个功能收到了Vue和React的启发（~~读书人的事，怎么能说是抄呢~~），该功能可以帮助用户撰写出响应式的文章。\n我们有如下约定：如果某API能在当前上下文中添加变量，则该API以`def`开头，hook API以`use`开头。\n因此，我们有如下API：\n\n#### defState\n```typescript\ndeclare function defState\u003cS\u003e(name: string, state: S | (() =\u003e S)): void\n```\n`defState` 接受一个名字和一个状态，该状态可以是一个函数从而减少渲染时重复计算的开销，在这之后，你可以在文章上下人中使用和修改该变量。\n基本上，你可以使用该方法来写出一个最简单的计数器：\n```\nr{{\n    defState('count', 0), btn(count, () =\u003e ++count)\n}}\n```\n这样，会在文章中显示一个按钮，上面显示当前的数字，每按一次，计数+1。\n\n#### useState\n有人可能会十分想念`React`中的`useState` hook，不过没有关系，这里也有。\n\n该API与[React State Hook](https://zh-hans.reactjs.org/docs/hooks-state.html)的功能一致。\n\n```typescript\ndeclare function useState\u003cS\u003e(state: S | (() =\u003e S)): [S, (value: (S | ((oldValue: S) =\u003e S))) =\u003e void]\n```\n\n一个计数器还能这么写：\n```javascript\n(() =\u003e {\n    const [count, setCount] = useState(0)\n    return btn(count, () =\u003e setCount(c =\u003e c + 1))\n})()\n```\n\n或者：\n\n```javascript\n    def(['count', 'setCount'], useState(0)), btn(count, () =\u003e setCount(c =\u003e c + 1))\n```\n\n`defState` 更加响应式但是 `useState` 更加函数式。 如果你经常使用Vue，你应该会喜欢 `defState`，如果你经常使用React，或者你是函数式编程的拥趸，你应该会喜欢`useState`。\n如果你都不是，那你爱用啥用啥吧，这里没有特别的偏好。\n\n#### useEffect\n这个hook和[React Effect Hook](https://zh-hans.reactjs.org/docs/hooks-effect.html)功能一致。\n```typescript\ndeclare function useEffect(effect: () =\u003e void, deps: any[]): void\n```\n\n#### useInterval / useTimeout\n```typescript\ndeclare function useInterval(callback: () =\u003e void, interval: number | null): void\n```\n这两个的hook函数签名一致，定时器的间隔随着参数的变化而作出响应式变化，如果参数是`null`，则该定时器会被终止。\n\n下面这个实现能说明原理（~~但是是用ts写的，文章里也没有`setInterval`给你用，看个热闹就行~~）\n```typescript\nfunction useInterval(callback: () =\u003e void, interval: number | null): void {\n    useEffect(() =\u003e {\n        if (typeof interval === 'number') {\n            const timer = setInterval(() =\u003e fn(), interval)\n            return () =\u003e clearInterval(timer)\n        }\n    }, [interval])\n}\n```\n\n#### useReactiveState\n```typescript\ndeclare function useReactiveState\u003cS extends object\u003e(init: S | (() =\u003e S)): S;\n```\n\n这个hook接受一个初始值，返回一个Proxy（该Proxy不能递归监听其子属性的变化）。对该Proxy的属性的修改都会引发一次更新。因为这个不是递归监听的，所以要保证每次对元素的修改都是对第一层元素的修改。\n\n#### useMemo / useCallback\n```typescript\ndeclare function useMemo\u003cS\u003e(compute: () =\u003e S, deps: any[] = []): S;\nfunction useCallback(callback: () =\u003e void, deps: any[]) {\n    return useMemo(() =\u003e callback, deps)\n}\n```\n\n`useMemo` 可以防止耗时任务的重复计算。该hook接受一个计算函数，该函数不接受任何参数，返回计算后的值，该值将会被缓存，每次调用都会返回缓存的值，直到deps数组的元素发生了更改。\n\n#### 上下文\n```typescript\ninterface IRoseliaScriptContext\u003cT\u003e {\n    Provider: (props: { value: T }) =\u003e RoseliaVNode\n}\ndeclare function createContext\u003cT\u003e(defaultValue: T): IRoseliaScriptContext\u003cT\u003e;\n\ndeclare function useContext\u003cT\u003e(context: IRoseliaScriptContext\u003cT\u003e): T;\n```\n\n在使用Context前，你需要先用`createContext`创建一个。然后渲染`context.Provider`组件，这个行为和`React`一致。\n不一样的是，我们不提供consumer组件，我们用来`useContext` hook替代，这样就足够了。\n`useContext` hook 接受一个context（**不是其Provider**），返回其值，如果找不到这样的上下文，则返回默认值。\n\n```typescript\nconst ThemeContext = createContext(null);\nconst ThemedButton = ({text}) =\u003e {\n    const theme = useContext(ThemeContext)\n    return hyperScript.button({\n        style: {\n            background: theme.primary\n        },\n    }, text)\n}\nconst App = () =\u003e {\n    return hyperScript(\n        ThemeContext.Provider, \n        {\n            value: {\n                primary: '#6670ed'\n            }\n        },\n        hyperScript(ThemedButton, {\n            text: 'Themed Button'\n        })\n    )\n}\n```\n\n### Roselia-Dom\n\nRoselia-Blog 可以采用两种不同的方法来处理文章中的内嵌脚本。他们分别是：\n\n1. 渲染\n\n    这种方法将会将脚本中的内容替换为执行的结果，最终生成一个`HTML`字符串，这种字符串\n    会直接替换为文章内容。在遇到状态变量改变的时候， 将会重新全部渲染生成一个新的字符串，\n    并替换文章的内容。这种方法在遇到大量的状态更新的时候，性能不是十分优秀。这是默认的处理文章和评论中脚本的方法。\n\n2. 挂载\n\n    这种方法将会把文章编译成一个生成虚拟节点（virtual dom, vdom）的函数，即函数组件。\n    这个组件会生成浏览器的文档组件，并挂载到文章对应的容器里，和大多数MVVM框架一样。\n    在状态改变的时候，会重新生成一个vdom，再通过调度算法，对新内容和旧内容进行对比，\n    生成的补丁操作才会真正改变dom的内容。这个操作是异步的，且只会在浏览器空闲的时候执行。\n    在没有大量状态改变或者根本没有状态改变的时候，这个方法就会多了很多多余的步骤。这个处理方式是实验功能，并且正在测试中，且只在处理文章时可用。为了开启这个功能，你需要在文章第一行\n    加入如下内容：\n        \n        ---feature:roselia-dom---\n\n例如：插入一首歌（会在切换文章时自动销毁）：\n```\nr{{\n    music({\n       title: '陽だまりロードナイト',\n        author: 'Roselia',\n        url: 'https://cdn.roselia.moe/static/img/roselia/hidamari.mp3',\n        pic: 'https://p4.music.126.net/gT4F8nlV2Io58GTVAEWyLw==/18636722092789001.jpg'\n    })\n}}\n```\n\n如果我想要获取这个播放器，并且希望利用APlayer的API的话：\n\n```\nroselia{{\n    def('player', music({...}, false, player =\u003e {\n        console.log(player, 'is ready!')\n        toast('Player is ready!')\n        player.play()\n    }))\n}}\n```\n\n在此之后，就可以用player代指播放器实例了。\n\n```\nr{{\n\tdef('revue', audio('https://static.roselia.moe/static/audio/revue.mp3'))\n    \n}}\nR{{\n    def('playRevue', () =\u003e getElement(revue).play()),\n    def('playbtn', btn('play', playRevue))\n}}\nr{{\n\tthen(() =\u003e getElement(playbtn).style.color = '#66ccff')\n}}\n\n//Or\n\nr{{\n\telement(playbtn).then(e =\u003e e.style.color = '#66ccff')\n}}\n```\n遗憾的是，为插入一个元素，就必须有一个r{{}}，因此可能会出现不少的R{{}}影响美观。\n\n但是，迷你脚本的出现确实大大增加了文章以及评论的灵活性，比起script标签，代码是运行在沙箱环境中的，因此可以受保护地访问某些对象，并且用一些受控的API进行访问，保障了安全性，\n在插件相对匮乏（其实根本没有）的Roselia-Blog里，迷你脚本可以在一定程度上实现部分插件的功能。但是，`Roselia-Script`仍然能进行沙箱逃逸，并且仍然能执行恶意代码，因此目前只对登陆用户开放。\n\n比如，对于登陆用户，在其迷你脚本执行的上下文里面就会有`comment`作为其评论的信息对象，用户可以修改除了评论ID等重要信息以外的信息，可以做到神奇的效果，同时执行的脚本运行在沙箱中可以确保一定程度上的安全，而且基于Roselia-Blog的邀请制注册，评论区中脚本的使用是可控的，因此可以放心使用。如果仍然不放心，可以在`config.js`里面把`enableRoseliaScript`设置为`false`从而彻底关闭这个功能。\n","funding_links":[],"categories":["Vue"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FSomainer%2FRoselia-Blog","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FSomainer%2FRoselia-Blog","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FSomainer%2FRoselia-Blog/lists"}