JS改造计划实践
发表于更新于
字数总计4.7k阅读时长23分钟阅读量
该教程不适合纯小白食用,建议有一定js基础后再阅读!!!
注意:阅读本篇教程前请先阅读前置内容!!!
如果你的主题不是Butterfly
,教程中提到的pug文件目录和内容可能会和你的不一样,还请自行调整。(如果能在评论区发出来是最好的了。)
有一部分原本由配置文件控制的内容(如主页卡片数量等)我迁移到了JS中,不再受配置文件控制。
将pjax.loadUrl
改为pjax.refresh
思路
方案的总体思路就是把HTML代码复制到JS中,然后JS运行时对其中需要修改的数据进行修改,然后输出到DOM中。最后再修改源代码,将原本内置在HTML中的内容删除即可(不删也能正常工作,删了可以节省空间)。
因为主题、配置等因素的影响,我们的HTML结构可能不太一样,除非你能保证一定兼容,否则建议读者直接去CV自己的HTML代码。
实现起来起始也十分简单,我们给出几个示例:
侧边栏
公告栏
引入JS:
1 2 3 4 5 6 7 8 | document.addEventListener('kms:loaded', () => kms.readPostsJson().then(json => { const content = json.config.doc const div = document.querySelector('.aside-content>.card-widget.card-announcement>.announcement_content') if (div) div.innerHTML = content pjax?.refresh(div) }))
|
修改 [theme]\layout\includes\widget\card_announcement.pug
为:
1 2 3 4 5 6 | if theme.aside.card_announcement.enable .card-widget.card-announcement .item-headline i.fas.fa-bullhorn.fa-shake span= _p('aside.card_announcement') .announcement_content
|
文章页相关推荐
引入JS:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | document.addEventListener('kms:loaded', () => kms.readPostsJson().then(json => { const div = document.getElementById('related-list') if (!div) return div.innerHTML = '' function buildHTML(abbrlink, title, img, date) { const url = `/posts/${abbrlink}/` return `<div class="aside-list-item"><a class="thumbnail" href="${url}" title="${title}"><img alt="${title}" src="${img}"></a><div class="content"><a class="title" href="${url}" title="${title}">${title}</a><time title="发表于 ${date.toLocaleDateString()}">${date.toLocaleDateString()}</time></div></div>` } const href = location.pathname.substring(0, location.pathname.length - 1) const abbrlink = href.substring(href.lastIndexOf('/') + 1) const list = kms.readPostInfo(json, abbrlink).related const date = json.config.related for (let post of list) { const info = kms.readPostInfo(json, post) const html = buildHTML(post, info['title'], info['img'], new Date(info[date])) div.insertAdjacentHTML("beforeend", html) } pjax?.refresh(div) }))
|
创建 [theme]\layout\includes\widget\card-related-post.pug
:
1 2 3 4 5 | .card-widget.card-recommend-post .item-headline i.fa.fa-book span= _p('post.recommend') .aside-list(id='related-list')
|
修改 [theme]\layout\includes\widget\index.pug
:(注意缩进)
1 2 3 4 5 6 | .sticky_layout if showToc include ./card_post_toc.pug if theme.related_post && theme.related_post.enable + !=partial('includes/widget/card-related-post', {}, {cache: true}) - != related_posts(page,site.posts)
|
文章卡片
接下来我们来修改主页,这里要特别注意,就算是同一个主题,配置不一样主页博文卡片的HTML代码也会不同,一定要根据自己想要的样式生成HTML代码。
因为这个JS代码只需要且只能在主页运行,所以我选择把JS代码内嵌在HTML中。
修改[theme]\layout\index.pug
为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | extends includes/layout.pug
block content #recent-posts.recent-posts include includes/pagination.pug
script. document.addEventListener('kms:loaded', function fun() { document.removeEventListener('kms:loaded', fun) function homePage(index, json, back = true, edit = true) { if (back) btf.scrollToDest(btf.getEleTop(document.getElementById('content-inner'))) const isDateSort = true const postsList = isDateSort ? json.dateSort : json.updatedSort const maxPage = Math.ceil(postsList.length / 10) const buildHTML = (abbrlink, direct) => { const info = kms.readPostInfo(json, abbrlink) const getTime = () => isDateSort ? `发表于 ${new Date(info.date).toLocaleDateString()}` : `更新于 ${new Date(info.updated).toLocaleDateString()}` const url = `/posts/${abbrlink}` let result = `<div class="recent-post-item ${direct}"><div class="post_cover ${direct}"><a href="${url}"><img class="post_bg" src="${info.img}"></a></div><div class="recent-post-info"><a class="article-title" href="${url}">${info.title}</a><div class="article-meta-wrap"><span class="post-meta-date"><i class="far fa-calendar-alt"></i><span class="article-meta-label">${getTime()}</span></span>` const divPoint = '<span class="article-meta-separator">|</span>' // 不同项目之间的分隔符 const spanPoint = '<span class="article-meta-link">•</span>' // 项目内部的分隔符 result += `<span class="article-meta">${divPoint}<i class="fas fa-inbox"></i>` for (let i = 0; i < info.categories.length; ++i) { const category = info.categories[i] if (i !== 0) result += spanPoint result += `<a class="article-meta__categories" onclick="pjax.loadUrl('/categories/${category}/')" data-pjax-state="">${category}</a>` } result += `</span><span class="article-meta tags">${divPoint}<i class="fas fa-tag"></i>` for (let i = 0; i < info.tags.length; ++i) { const tag = info.tags[i] if (i !== 0) result += spanPoint result += `<a class="article-meta__tags" onclick="pjax.loadUrl('/tags/${tag}/')">${tag}</a>` } result += `</span></div><div class="content">${info.des}</div></div></div>` return result } if (index < 1) index = 1 else if (index > maxPage) index = maxPage const recentPosts = document.getElementById('recent-posts') recentPosts.innerHTML = '' for (let i = (index - 1) * 10, k = 0; k !== 10 && i !== postsList.length; ++i, ++k) recentPosts.innerHTML += buildHTML(postsList[i], (k & 1) === 0 ? 'left' : 'right') recentPosts.style.cssText = 'overflow-x:hidden;' recentPosts.innerHTML += `<nav id="pagination"><div class="pagination"><a class="first"><i class="fa fa-fast-backward"></i></a><a class="prev"><i class="fas fa-chevron-left fa-fw"></i></a><input id="pagination-input" type="number" min="1" max="${maxPage}" value="${index}"><button class="jump">跳转</button><a class="next"><i class="fas fa-chevron-right fa-fw"></i></a><a class="end"><i class="fa fa-fast-forward"></i></a></div></nav>` pjax?.refresh(recentPosts) recentPosts.addEventListener('animationend', function fun() { recentPosts.removeEventListener('animationend', fun) recentPosts.style.cssText = '' }) const initDiv = () => { if (edit && back) location.hash = index const pagination = document.getElementById('pagination') pagination.onclick = event => { const element = event.target.childNodes.length !== 0 ? event.target : event.target.parentNode if (element.classList.contains('disable')) return const classList = element.classList if (classList.contains('prev')) { homePage(index - 1, json) } else if (classList.contains('next')) { homePage(index + 1, json) } else if (classList.contains('jump')) { const newIndex = Number.parseInt(document.getElementById('pagination-input').value) if (index !== newIndex) homePage(newIndex, json) } else if (classList.contains('first')) { homePage(1, json) } else if (classList.contains('end')) { homePage(maxPage, json) } } const setDisable = name => pagination.getElementsByClassName(name)[0].classList.add('disable') const removeDisable = name => pagination.getElementsByClassName(name)[0].classList.remove('disable') if (index === 1) { setDisable('prev') setDisable('first') removeDisable('next') removeDisable('end') } else if (index === maxPage) { removeDisable('prev') removeDisable('first') setDisable('next') setDisable('end') } } initDiv() }
kms.readPostsJson().then(json => { const parseUrlIndex = (hash = location.hash) => hash.length === 0 ? 1 : Number.parseInt(hash.substring(1)) homePage(parseUrlIndex(), json, false) window.onhashchange = event => { const oldUrl = event.oldURL const splitIndex = oldUrl.indexOf('#') const index = Number.parseInt(splitIndex < 0 ? 1 : oldUrl.substring(splitIndex)) const newIndex = parseUrlIndex() if (index !== newIndex) homePage(newIndex, json, false, false) } }) })
|
如果你想要修改卡片样式,修改builtHTML
这个函数即可。
为了编码方便,主页下方的翻页按钮我改成了我自己设计的样式,没有照搬原版的,不喜欢的小伙伴自行修改代码即可。
同时,这段代码中并没有添加懒加载的支持,想要添加懒加载的小伙伴自行修改代码即可。
编写JS:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | document.addEventListener('kms:loaded', function fun() { document.removeEventListener('kms:loaded', fun)
function homePage(index, json, back = true, edit = true) { if (back) btf.scrollToDest(btf.getEleTop(document.getElementById('content-inner'))) const isDateSort = true const postsList = isDateSort ? json.dateSort : json.updatedSort const maxPage = Math.ceil(postsList.length / 10) const buildHTML = (abbrlink, direct) => { const info = kms.readPostInfo(json, abbrlink) const getTime = () => isDateSort ? `发表于 ${new Date(info.date).toLocaleDateString()}` : `更新于 ${new Date(info.updated).toLocaleDateString()}` const url = `/posts/${abbrlink}` let result = `<div class="recent-post-item ${direct}"><div class="post_cover ${direct}"><a href="${url}"><img class="post_bg" src="${info.img}"></a></div><div class="recent-post-info"><a class="article-title" href="${url}">${info.title}</a><div class="article-meta-wrap"><span class="post-meta-date"><i class="far fa-calendar-alt"></i><span class="article-meta-label">${getTime()}</span></span>` const divPoint = '<span class="article-meta-separator">|</span>' const spanPoint = '<span class="article-meta-link">•</span>' result += `<span class="article-meta">${divPoint}<i class="fas fa-inbox"></i>` for (let i = 0; i < info.categories.length; ++i) { const category = info.categories[i] if (i !== 0) result += spanPoint result += `<a class="article-meta__categories" onclick="pjax.loadUrl('/categories/${category}/')" data-pjax-state="">${category}</a>` } result += `</span><span class="article-meta tags">${divPoint}<i class="fas fa-tag"></i>` for (let i = 0; i < info.tags.length; ++i) { const tag = info.tags[i] if (i !== 0) result += spanPoint result += `<a class="article-meta__tags" onclick="pjax.loadUrl('/tags/${tag}/')">${tag}</a>` } result += `</span></div><div class="content">${info.des}</div></div></div>` return result } if (index < 1) index = 1 else if (index > maxPage) index = maxPage const recentPosts = document.getElementById('recent-posts') recentPosts.innerHTML = '' for (let i = (index - 1) * 10, k = 0; k !== 10 && i !== postsList.length; ++i, ++k) recentPosts.innerHTML += buildHTML(postsList[i], (k & 1) === 0 ? 'left' : 'right') recentPosts.style.cssText = 'overflow-x:hidden;' recentPosts.innerHTML += `<nav id="pagination"><div class="pagination"><a class="first"><i class="fa fa-fast-backward"></i></a><a class="prev"><i class="fas fa-chevron-left fa-fw"></i></a><input id="pagination-input" type="number" min="1" max="${maxPage}" value="${index}"><button class="jump">跳转</button><a class="next"><i class="fas fa-chevron-right fa-fw"></i></a><a class="end"><i class="fa fa-fast-forward"></i></a></div></nav>` pjax?.refresh(recentPosts) recentPosts.addEventListener('animationend', function fun() { recentPosts.removeEventListener('animationend', fun) recentPosts.style.cssText = '' }) const initDiv = () => { if (edit && back) location.hash = index const pagination = document.getElementById('pagination') pagination.onclick = event => { const element = event.target.childNodes.length !== 0 ? event.target : event.target.parentNode if (element.classList.contains('disable')) return const classList = element.classList if (classList.contains('prev')) { homePage(index - 1, json) } else if (classList.contains('next')) { homePage(index + 1, json) } else if (classList.contains('jump')) { const newIndex = Number.parseInt(document.getElementById('pagination-input').value) if (index !== newIndex) homePage(newIndex, json) } else if (classList.contains('first')) { homePage(1, json) } else if (classList.contains('end')) { homePage(maxPage, json) } } const setDisable = name => pagination.getElementsByClassName(name)[0].classList.add('disable') const removeDisable = name => pagination.getElementsByClassName(name)[0].classList.remove('disable') if (index === 1) { setDisable('prev') setDisable('first') removeDisable('next') removeDisable('end') } else if (index === maxPage) { removeDisable('prev') removeDisable('first') setDisable('next') setDisable('end') } } initDiv() }
kms.readPostsJson().then(json => { const parseUrlIndex = (hash = location.hash) => hash.length === 0 ? 1 : Number.parseInt(hash.substring(1)) homePage(parseUrlIndex(), json, false) window.onhashchange = event => { const root = location.protocol + '//' + location.host if (Math.abs(root.length - event.newURL.length) > 1) return const oldUrl = event.oldURL const splitIndex = oldUrl.indexOf('#') const index = Number.parseInt(splitIndex < 0 ? 1 : oldUrl.substring(splitIndex)) const newIndex = parseUrlIndex() if (index !== newIndex) homePage(newIndex, json, false, false) } }) })
|
注意这段代码中,使用document.removeEventListener
移除了事件,如果启用了pjax的话这行代码是必须写的。因为启用pjax后切换页面时并不会移除document
中注册的事件,如果不手动移除事件,这段代码就有可能在其它页面触发,这是我们所不期望的。
修改[theme]\layout\index.pug
(注意修改js的URL地址):
1 2 3 4 5 6 7 8 9 | extends includes/layout.pug block content - include ./includes/mixins/post-ui.pug #recent-posts.recent-posts - +postUI include includes/pagination.pug + + script(defer src='[这里写上面那个JS的URL]')
|
然后再引入CSS(这个CSS是给卡片添加的进入的动画,可以不添加):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @keyframes SlipIn2Right from transform translateX(-100%) rotateX(90deg) to transform translateX(0) rotateX(0)
@keyframes SlipIn2Left from transform translateX(100%) rotateX(90deg) to transform translateX(0) rotateX(0)
.recent-post-item &.left animation SlipIn2Right 1.5s ease-in-out
&.right animation SlipIn2Left 1.5s ease-in-out
|
删除多余页面
此时主页就编写好了,但是有一个隐藏的问题,就是Hexo会生成好几个主页(/
, /page/[n]
),有强迫症的小伙伴可以通过hexo script
或gulp
删除这个目录。
这里仅给出gulp
的方案:
如果没有安装gulp
的话先安装gulp
,在博客根目录运行下面的指令:
1 2 | npm install --global gulp-cli npm install gulp --save
|
然后在根目录创建gulpfile.js
文件,在其中键入下面的内容:
1 2 3 4 5 6 7 | const remove = require('gulp-clean')
gulp.task('clean', () => gulp.src('./public/page', {read: false}) .pipe(remove()) )
|
然后安装所需插件,在博客根目录运行下面的指令:
1 | npm install gulp-clean --save-dev
|
这时候执行完hexo g
后再执行gulp clean
就可以删除掉/page/[n]
的页面了。
标签页 & 分类页
编写并引入JS:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | const kms = { buildTagOrCatsPage: (pageName) => kms.collectJInfo(pageName).then(json => { const list = json.cache[pageName] const categories = document.getElementById(pageName) const cardList = categories.getElementsByClassName('card') const h2 = document.getElementById(`${pageName}-name`) const getDate = date => new Date(date).toLocaleDateString() let sortIndex = 'updated' let sortDirect = 1 let preName = null
const buildList = () => { let html = '' for (let name in list) { html += `<a class="card" index="${name}"><div class="img"><img src="${kms.readPostInfo(json, list[name][0]).img}!/fw/230"></div><p>${name}</p></a>` } categories.innerHTML = html } const selectFlag = (name, scroll = true) => { if (name == null) { if (location.hash.length !== 0) { name = decodeURIComponent(location.hash.substring(1)) } else return } const flagList = list[name] if (!flagList) return preName = name flagList.sort((a, b) => kms.readPostInfo(json, a)[sortIndex] < kms.readPostInfo(json, b)[sortIndex] ? sortDirect : -sortDirect) document.getElementById('details').classList.remove('close') kms.setText(h2, name) if (scroll) btf.scrollToDest(btf.getEleTop(h2)) let html = '' for (let abbrlink of flagList) { const info = kms.readPostInfo(json, abbrlink) const href = `/posts/${abbrlink}` html += `<div class="card"><a class="img" href="${href}"><img src="${info.img}!/fw/128"></a><div class="descr"><div class="time"><span class="date"><i class="far fa-calendar-alt"></i><p>${getDate(info.date)}</p></span><span class="updated"><i class="fa fa-pencil"></i><p>${getDate(info.updated)}</p></span></div><a class="title" href="${href}">${info.title}</a></div></div>` } const div = document.getElementById(`${pageName}-list`) div.innerHTML = html pjax?.refresh(div) } const registryEvent = () => { const select = document.getElementById('list-sort') select.onchange = () => { sortIndex = select.value selectFlag(preName, false) } const button = document.getElementById('sort-direct') button.onclick = () => { sortDirect = -sortDirect const className = sortDirect === 1 ? 'desc' : 'asc' button.innerHTML = `<i class="fa fa-sort-amount-${className}"></i>` selectFlag(preName, false) } } registryEvent() buildList() selectFlag(null)
for (let element of cardList) { const name = element.getAttribute('index') element.onclick = () => selectFlag(name) } }) }
|
修改[theme]\layout\includes\page\tags.pug
为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #tags
.close#details .name h2#tags-name .block button#sort-direct i.fa.fa-sort-amount-desc select#list-sort option(value='updated') 更新时间 option(value='date') 发布时间 option(value='title') 标题 #tags-list
script. document.addEventListener('kms:loaded', function fun() { document.removeEventListener('kms:loaded', fun) kms.buildTagOrCatsPage('tags') })
|
修改[theme]\layout\includes\page\categories.pug
为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #categories
.close#details .name h2#categories-name .block button#sort-direct i.fa.fa-sort-amount-desc select#list-sort option(value='updated') 更新时间 option(value='date') 发布时间 option(value='title') 标题 #categories-list
script. document.addEventListener('kms:loaded', function fun() { document.removeEventListener('kms:loaded', fun) kms.buildTagOrCatsPage('categories') })
|
引入CSS:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | #page #categories, #tags text-align center border 2px dashed var(--km-button-dark-bg) border-radius 10px
.img height 120px max-width 100% overflow hidden border-radius 10px transition all .3s
img width 100% height 100% object-fit cover
.card width calc(100% / 4 - 16px) display inline-block margin 8px text-align center
p display block font-weight bold font-size large margin-top 12px
&:hover border-radius 5px background var(--km-card-bg)
.img transform scale(.9)
#tags .card width unset margin 0 padding 8px margin-top 5px margin-bottom 5px
p margin 0
.img display none
@media screen and (max-width: 700px) #categories .card width calc(100% / 3 - 16px)
@media screen and (max-width: 500px) #categories .card width calc(100% / 2 - 16px)
@media screen and (max-width: 300px) #categories .card width calc(100% - 16px)
#details &.close display none
.name h2 display inline-block
.block display inline-block float right margin-top 27px
select margin-left 10px padding 5px background var(--btn-bg) color var(--btn-color) border-radius 5px border none
button background var(--btn-bg) color var(--btn-color) border-radius 5px padding 6px
&:hover background var(--btn-hover-color)
.card margin-bottom 15px
.img display inline-block height 80px width 80px vertical-align middle border-radius 8px overflow hidden margin-right 15px
img width 100% height 100% object-fit cover transition all .2s
&:hover transform scale(1.1)
.descr display inline-block vertical-align middle max-width calc(100% - 110px)
.time display inline-block margin 0 color #858585
span white-space nowrap
p display inline-block width 80px margin 0 margin-left 6px
.date margin-right 12px
.title display block margin 0 font-weight bold font-size large overflow hidden text-overflow ellipsis white-space nowrap
|
不喜欢我写的页面样式的可以自行修改。
修改页面链接
这时候分类页和标签页已经写好了,但是页面内除了顶部导航栏内的链接,其它地方仍然会跳转到Hexo生成的分类页/标签页中,我们需要把它们改正过来(不想修改的可以跳过)。
找到并修改下列文件,将其中的链接/tags/[name]/
和/categories/[name]/
分别改为/tags/#name
和/categories/#name
即可:
[theme]\layout\index.pug
[theme]\layout\post.pug
- 自己编写的JS和HTML中的链接
此时,Hexo自己生成的分类页和标签页其实还在,但是我们没有添加对它们的引用,读者可以自行决断是否删除这些页面。
个人建议保留这些页面,然后在归档页中添加对这些页面的引用,方便搜索引擎爬取,因为我们的方案本身就不利于搜索引擎爬取页面了,留一个接口给搜索引擎也不错。
到这里,读者应该差不多能够明白整个过程是什么样子了,试一下自己把一个元素改为JS生成吧。