Skip to content

Commit

Permalink
feat(ui/img_lazyload): add support for lazy loading images (#850)
Browse files Browse the repository at this point in the history
  • Loading branch information
qwqcode committed May 2, 2024
1 parent c3f730b commit 170db0f
Show file tree
Hide file tree
Showing 14 changed files with 136 additions and 24 deletions.
1 change: 1 addition & 0 deletions conf/artalk.example.simple.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ frontend:
content: 300
children: 400
scrollable: false
imgLazyLoad: false
reqTimeout: 15000
versionCheck: true
pluginURLs: []
2 changes: 2 additions & 0 deletions conf/artalk.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions conf/artalk.example.zh-CN.yml
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@ frontend:
children: 400
# 滚动限高 (允许限高区域滚动)
scrollable: false
# 图片懒加载 [false, "native", "data-src"]
imgLazyLoad: false
# 请求超时 (单位:毫秒)
reqTimeout: 15000
# 版本检测
Expand Down
1 change: 1 addition & 0 deletions docs/docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/guide/backend/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ Artalk.init({ site: 'Artalk 官网' })

这样,你就无需在侧边栏的[控制中心](../frontend/sidebar.md#控制中心)手动创建站点。

## 前端配置 `frontend`
## 界面配置 `frontend`

增加 `frontend` 字段内容可以在后端控制前端的配置,详情可参考:[在后端控制前端](/guide/backend/fe-control)。

Expand Down
11 changes: 11 additions & 0 deletions docs/docs/guide/frontend/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,17 @@ Artalk.init({
})
```

### imgLazyLoad

**图片懒加载**

- 类型:`"native"|"data-src"|Boolean`
- 默认值:`false`

为评论中的图片启用懒加载功能。

详情参考:[图片懒加载](./img-lazy-load.md)

### preview

**编辑器实时预览功能**
Expand Down
53 changes: 53 additions & 0 deletions docs/docs/guide/frontend/img-lazy-load.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 图片懒加载

Artalk 支持为评论中的图片添加懒加载功能,以减少页面加载时间。该功能默认禁用,可在控制中心「设置 - 界面配置」找到「图片懒加载」配置项 (frontend.imgLazyLoad):

| 配置值 | 说明 |
| --- | --- |
| `native` | 使用浏览器原生的图片懒加载 |
| `data-src` | 使用懒加载库实现图片懒加载 |
| `false` | 禁用图片懒加载 |

## 浏览器原生

当图片懒加载设置为 `native` 时,Artalk 将使用浏览器原生的图片懒加载功能,即为图片标签添加 `loading="lazy"` 属性。例如:

```html
<img src="1.png" loading="lazy" />
```

这不需要额外引入库和编写代码,是最简单的方式,但部分浏览器可能不支持该属性,或者实现的懒加载策略不同,详情参考:[MDN - 懒加载](https://developer.mozilla.org/zh-CN/docs/Web/Performance/Lazy_loading)

## 懒加载库

使用该方式能确保在不同的浏览器下有一致的表现,并且能定制更多功能,如图片加载动画等。一个博客主题可能自带懒加载库,借助其附带的库,让 Artalk 也同时支持懒加载。

当图片懒加载设置为 `data-src` 时,Artalk 将为图片标签添加 `class="lazyload"``data-src` 属性。例如:

```html
<img data-src="1.png" class="lazyload" />
```

这时,你需要引入一个额外的图片懒加载库:[vanilla-lazyload](https://github.com/verlok/vanilla-lazyload);在页面的 `<head>` 中添加以下代码:

```html
<script src="https://unpkg.com/vanilla-lazyload/dist/lazyload.iife.min.js"></script>
```

编写代码,在 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({ /* ... */ })
```
34 changes: 19 additions & 15 deletions ui/artalk/src/comment/height-limit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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<HTMLImageElement>('.atk-content img').forEach((img) => {
Expand All @@ -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
Expand All @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion ui/artalk/src/comment/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export default class Render {

HeightLimit.check(
{
postExpandBtnClick: () => {
afterExpandBtnClick: () => {
// 子评论数仅有 1,直接取消限高
const children = this.comment.getChildren()
if (children.length === 1)
Expand Down
20 changes: 19 additions & 1 deletion ui/artalk/src/lib/marked-renderer.ts
Original file line number Diff line number Diff line change
@@ -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
}

Expand Down Expand Up @@ -46,3 +52,15 @@ const markedCodeRenderer =
`</pre>`
)
}

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(/^<img /, '<img class="lazyload" loading="lazy" ')
if (imgLazyLoad === 'data-src')
return html.replace(/^<img /, '<img class="lazyload" ').replace('src=', 'data-src=')
return html
}
13 changes: 11 additions & 2 deletions ui/artalk/src/lib/marked.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { marked as libMarked, MarkedOptions } from 'marked'

import type { ArtalkConfig } from '@/types'
import { sanitize } from './sanitizer'
import { renderCode } from './highlight'
import { getRenderer } from './marked-renderer'
Expand All @@ -25,8 +26,13 @@ export function setReplacers(arr: Replacer[]) {
replacers = arr
}

export interface MarkedInitOptions {
markedOptions: ArtalkConfig['markedOptions']
imgLazyLoad: ArtalkConfig['imgLazyLoad']
}

/** 初始化 marked */
export function initMarked() {
export function initMarked(options: MarkedInitOptions) {
try {
if (!libMarked.name) return
} catch {
Expand All @@ -35,8 +41,11 @@ export function initMarked() {

// @see https://github.com/markedjs/marked/blob/4afb228d956a415624c4e5554bb8f25d047676fe/src/Tokenizer.js#L329
libMarked.setOptions({
renderer: getRenderer(),
renderer: getRenderer({
imgLazyLoad: options.imgLazyLoad,
}),
...markedOptions,
...options.markedOptions,
})

instance = libMarked
Expand Down
5 changes: 3 additions & 2 deletions ui/artalk/src/lib/sanitizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,17 @@ const insaneOptions = {
allowedAttributes: {
'*': ['title', 'accesskey'],
a: ['href', 'name', 'target', 'aria-label', 'rel'],
img: ['src', 'alt', 'title', 'atk-emoticon', 'aria-label'],
img: ['src', 'alt', 'title', 'atk-emoticon', 'aria-label', 'data-src', 'class', 'loading'],
// for code highlight
code: ['class'],
span: ['class', 'style'],
},
filter: (node) => {
// 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)) {
Expand Down
7 changes: 5 additions & 2 deletions ui/artalk/src/plugins/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
7 changes: 7 additions & 0 deletions ui/artalk/src/types/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { MarkedOptions } from 'marked'
import type { I18n } from '@/i18n'
import type { CommentData } from './data'
import type { EditorApi } from './editor'
Expand Down Expand Up @@ -108,6 +109,9 @@ export interface ArtalkConfig {
/** 图片上传器 */
imgUploader?: (file: File) => Promise<string>

/** Image lazy load */
imgLazyLoad?: 'native' | 'data-src'

/** 版本检测 */
versionCheck: boolean

Expand All @@ -126,6 +130,9 @@ export interface ArtalkConfig {
/** Replacer for marked */
markedReplacers?: ((raw: string) => string)[]

/** Marked options */
markedOptions?: MarkedOptions

/** 列表请求参数修改器 */
listFetchParamsModifier?: (params: any) => void

Expand Down

0 comments on commit 170db0f

Please sign in to comment.