diff --git a/conf/artalk.example.simple.yml b/conf/artalk.example.simple.yml index 43e77420..56bac286 100644 --- a/conf/artalk.example.simple.yml +++ b/conf/artalk.example.simple.yml @@ -171,6 +171,7 @@ frontend: content: 300 children: 400 scrollable: false + imgLazyLoad: false reqTimeout: 15000 versionCheck: true pluginURLs: [] diff --git a/conf/artalk.example.yml b/conf/artalk.example.yml index 903fa73a..86cdfb02 100644 --- a/conf/artalk.example.yml +++ b/conf/artalk.example.yml @@ -338,6 +338,8 @@ frontend: children: 400 # Scrollable (scrollable height limit area) scrollable: false + # Image lazy load [false, "native", "data-src"] + imgLazyLoad: false # Request timeout (unit: ms) reqTimeout: 15000 # Version check diff --git a/conf/artalk.example.zh-CN.yml b/conf/artalk.example.zh-CN.yml index 5ef287e2..5ebf44a0 100644 --- a/conf/artalk.example.zh-CN.yml +++ b/conf/artalk.example.zh-CN.yml @@ -343,6 +343,8 @@ frontend: children: 400 # 滚动限高 (允许限高区域滚动) scrollable: false + # 图片懒加载 [false, "native", "data-src"] + imgLazyLoad: false # 请求超时 (单位:毫秒) reqTimeout: 15000 # 版本检测 diff --git a/docs/docs/.vitepress/config.ts b/docs/docs/.vitepress/config.ts index 329372e7..d283eba0 100644 --- a/docs/docs/.vitepress/config.ts +++ b/docs/docs/.vitepress/config.ts @@ -117,6 +117,7 @@ export default defineConfig({ { text: '浏览量统计', link: '/guide/frontend/pv.md' }, { text: 'Latex', link: '/guide/frontend/latex.md' }, { text: '图片灯箱', link: '/guide/frontend/lightbox.md' }, + { text: '图片懒加载', link: '/guide/frontend/img-lazy-load.md' }, { text: 'IP 属地', link: '/guide/frontend/ip-region.md' }, { text: '精简版本', link: '/guide/frontend/artalk-lite.md' }, { text: '置入博客', link: '/guide/frontend/import-blog.md' }, diff --git a/docs/docs/guide/backend/config.md b/docs/docs/guide/backend/config.md index 0fdb0f20..f1000f77 100644 --- a/docs/docs/guide/backend/config.md +++ b/docs/docs/guide/backend/config.md @@ -177,7 +177,7 @@ Artalk.init({ site: 'Artalk 官网' }) 这样,你就无需在侧边栏的[控制中心](../frontend/sidebar.md#控制中心)手动创建站点。 -## 前端配置 `frontend` +## 界面配置 `frontend` 增加 `frontend` 字段内容可以在后端控制前端的配置,详情可参考:[在后端控制前端](/guide/backend/fe-control)。 diff --git a/docs/docs/guide/frontend/config.md b/docs/docs/guide/frontend/config.md index 8708e74a..44d419b3 100644 --- a/docs/docs/guide/frontend/config.md +++ b/docs/docs/guide/frontend/config.md @@ -314,6 +314,17 @@ Artalk.init({ }) ``` +### imgLazyLoad + +**图片懒加载** + +- 类型:`"native"|"data-src"|Boolean` +- 默认值:`false` + +为评论中的图片启用懒加载功能。 + +详情参考:[图片懒加载](./img-lazy-load.md) + ### preview **编辑器实时预览功能** diff --git a/docs/docs/guide/frontend/img-lazy-load.md b/docs/docs/guide/frontend/img-lazy-load.md new file mode 100644 index 00000000..c6164673 --- /dev/null +++ b/docs/docs/guide/frontend/img-lazy-load.md @@ -0,0 +1,53 @@ +# 图片懒加载 + +Artalk 支持为评论中的图片添加懒加载功能,以减少页面加载时间。该功能默认禁用,可在控制中心「设置 - 界面配置」找到「图片懒加载」配置项 (frontend.imgLazyLoad): + +| 配置值 | 说明 | +| --- | --- | +| `native` | 使用浏览器原生的图片懒加载 | +| `data-src` | 使用懒加载库实现图片懒加载 | +| `false` | 禁用图片懒加载 | + +## 浏览器原生 + +当图片懒加载设置为 `native` 时,Artalk 将使用浏览器原生的图片懒加载功能,即为图片标签添加 `loading="lazy"` 属性。例如: + +```html + +``` + +这不需要额外引入库和编写代码,是最简单的方式,但部分浏览器可能不支持该属性,或者实现的懒加载策略不同,详情参考:[MDN - 懒加载](https://developer.mozilla.org/zh-CN/docs/Web/Performance/Lazy_loading)。 + +## 懒加载库 + +使用该方式能确保在不同的浏览器下有一致的表现,并且能定制更多功能,如图片加载动画等。一个博客主题可能自带懒加载库,借助其附带的库,让 Artalk 也同时支持懒加载。 + +当图片懒加载设置为 `data-src` 时,Artalk 将为图片标签添加 `class="lazyload"` 和 `data-src` 属性。例如: + +```html + +``` + +这时,你需要引入一个额外的图片懒加载库:[vanilla-lazyload](https://github.com/verlok/vanilla-lazyload);在页面的 `` 中添加以下代码: + +```html + +``` + +编写代码,在 Artalk 初始化之前,添加事件监听:当评论列表发生更新后,调用图片懒加载库的 `update` 方法: + +```js +// 初始化图片懒加载库 +const lazyLoadInstance = new LazyLoad({ + elements_selector: '.lazyload', + threshold: 0, +}) + +// 监听 Artalk 事件 +Artalk.use((ctx) => { + ctx.on('list-loaded', () => lazyLoadInstance.update()) +}) + +// 初始化 Artalk +const artalk = Artalk.init({ /* ... */ }) +``` diff --git a/ui/artalk/src/comment/height-limit.ts b/ui/artalk/src/comment/height-limit.ts index 1ccdea14..22fb0178 100644 --- a/ui/artalk/src/comment/height-limit.ts +++ b/ui/artalk/src/comment/height-limit.ts @@ -2,8 +2,8 @@ import * as Utils from '../lib/utils' import $t from '../i18n' export interface IHeightLimitConf { - /** Post expand btn click */ - postExpandBtnClick?: (e: MouseEvent) => void + /** After expand btn click */ + afterExpandBtnClick?: () => void /** Allow Scroll */ scrollable?: boolean } @@ -27,23 +27,27 @@ export function check(conf: IHeightLimitConf, rules: THeightLimitRuleSet) { if (!el) return // set max height to limit the height - if (imgCheck) el.style.maxHeight = `${max + 1}px` // allow 2px more for next detecting + if (imgCheck) el.style.maxHeight = `${max + 1}px` // allow 1px more for next detecting + + let lock = false + const _check = () => { + if (lock) return + if (Utils.getHeight(el) <= max) return // if not exceed the limit, do nothing + + const afterExpandBtnClick = () => { + lock = true // add lock to prevent collapse again after expand when image lazy loaded + conf.afterExpandBtnClick?.() + } - const _apply = () => { - const postBtnClick = conf.postExpandBtnClick !conf.scrollable - ? applyHeightLimit({ el, max, postBtnClick }) + ? applyHeightLimit({ el, max, afterExpandBtnClick }) : applyScrollableHeightLimit({ el, max }) } - // checking - const _check = () => { - if (Utils.getHeight(el) > max) _apply() // 是否超过高度 - } - - _check() // check immediately + // check immediately + _check() - // image check + // check images after loaded if (imgCheck) { // check again when image loaded el.querySelectorAll('.atk-content img').forEach((img) => { @@ -60,7 +64,7 @@ const HEIGHT_LIMIT_CSS = 'atk-height-limit' export function applyHeightLimit(obj: { el: HTMLElement max: number - postBtnClick?: (e: MouseEvent) => void + afterExpandBtnClick?: (e: MouseEvent) => void }) { if (!obj.el) return if (!obj.max) return @@ -78,7 +82,7 @@ export function applyHeightLimit(obj: { e.stopPropagation() disposeHeightLimit(obj.el) - if (obj.postBtnClick) obj.postBtnClick(e) + if (obj.afterExpandBtnClick) obj.afterExpandBtnClick(e) } obj.el.append($expandBtn) } diff --git a/ui/artalk/src/comment/render.ts b/ui/artalk/src/comment/render.ts index cff75ae7..80b839be 100644 --- a/ui/artalk/src/comment/render.ts +++ b/ui/artalk/src/comment/render.ts @@ -78,7 +78,7 @@ export default class Render { HeightLimit.check( { - postExpandBtnClick: () => { + afterExpandBtnClick: () => { // 子评论数仅有 1,直接取消限高 const children = this.comment.getChildren() if (children.length === 1) diff --git a/ui/artalk/src/lib/marked-renderer.ts b/ui/artalk/src/lib/marked-renderer.ts index f00bcfe5..f6d3d973 100644 --- a/ui/artalk/src/lib/marked-renderer.ts +++ b/ui/artalk/src/lib/marked-renderer.ts @@ -1,10 +1,16 @@ import { marked as libMarked } from 'marked' +import type { ArtalkConfig } from '@/types' import { renderCode } from './highlight' -export function getRenderer() { +export interface RendererOptions { + imgLazyLoad: ArtalkConfig['imgLazyLoad'] +} + +export function getRenderer(options: RendererOptions) { const renderer = new libMarked.Renderer() renderer.link = markedLinkRenderer(renderer, renderer.link) renderer.code = markedCodeRenderer() + renderer.image = markedImageRenderer(renderer, renderer.image, options) return renderer } @@ -46,3 +52,15 @@ const markedCodeRenderer = `` ) } + +const markedImageRenderer = + (renderer: any, orgImageRenderer: Function, { imgLazyLoad }: RendererOptions) => + (href: string, title: string | null, text: string): string => { + const html = orgImageRenderer.call(renderer, href, title, text) + if (!imgLazyLoad) return html + if (imgLazyLoad === 'native' || (imgLazyLoad as any) === true) + return html.replace(/^ { - // allow hljs style + // class whitelist const allowed = [ ['code', /^hljs\W+language-(.*)$/], ['span', /^(hljs-.*)$/], + ['img', /^lazyload$/], ] allowed.forEach(([tag, reg]) => { if (node.tag === tag && !!node.attrs.class && !(reg as RegExp).test(node.attrs.class)) { diff --git a/ui/artalk/src/plugins/markdown.ts b/ui/artalk/src/plugins/markdown.ts index f37de6c5..d864ef41 100644 --- a/ui/artalk/src/plugins/markdown.ts +++ b/ui/artalk/src/plugins/markdown.ts @@ -2,9 +2,12 @@ import type { ArtalkPlugin } from '@/types' import * as marked from '@/lib/marked' export const Markdown: ArtalkPlugin = (ctx) => { - marked.initMarked() + ctx.watchConf(['markedReplacers', 'imgLazyLoad', 'markedOptions'], (conf) => { + marked.initMarked({ + markedOptions: ctx.getConf().markedOptions, + imgLazyLoad: ctx.getConf().imgLazyLoad, + }) - ctx.on('updated', (conf) => { if (conf.markedReplacers) marked.setReplacers(conf.markedReplacers) }) } diff --git a/ui/artalk/src/types/config.ts b/ui/artalk/src/types/config.ts index 243adc7f..50ff58ea 100644 --- a/ui/artalk/src/types/config.ts +++ b/ui/artalk/src/types/config.ts @@ -1,3 +1,4 @@ +import type { MarkedOptions } from 'marked' import type { I18n } from '@/i18n' import type { CommentData } from './data' import type { EditorApi } from './editor' @@ -108,6 +109,9 @@ export interface ArtalkConfig { /** 图片上传器 */ imgUploader?: (file: File) => Promise + /** Image lazy load */ + imgLazyLoad?: 'native' | 'data-src' + /** 版本检测 */ versionCheck: boolean @@ -126,6 +130,9 @@ export interface ArtalkConfig { /** Replacer for marked */ markedReplacers?: ((raw: string) => string)[] + /** Marked options */ + markedOptions?: MarkedOptions + /** 列表请求参数修改器 */ listFetchParamsModifier?: (params: any) => void