从零搭建博客之前台开发

从零搭建博客之前台开发

2020年04月10日 阅读:23 字数:1592 阅读时长:4 分钟

主要记录博客前台的nuxtServerInit状态数据管理,前台数据查询以及白天、黑夜主题切换等功能

1. 前言

博客前台也就是现在看到的页面,包括首页、列表页、详情页、单页(关于、友链、各种小工具...)等,需要考虑一定的SEO优化。

一开始是使用Thinkjs + nunjucks 模板引擎进行服务端渲染,比较难集成vue、webpack等技术,前后端不分离。后面改用Nuxt来实现Vue SSR渲染。也方便开发各种单页小工具,如音乐盒必应每日壁纸等。

技术栈:

  • Vue.js 2.0
  • Nuxt.js:SSR渲染
  • Element-UI
  • Axios
  • Live2D:看板娘
  • Prism.js:代码高亮

2. 目录结构

client
├── common 内容管理平台和博客前台功能模块
├── front 博客前台
|   ├── layouts 外部容器
│   |   ├── app.vue 工具页面使用的外部容器,没有菜单
│   |   └── default 默认容器,有菜单
|   ├── pages nuxt页面
|   ├── plugins
│   |   ├── axios.js axios全局拦截器
│   |   ├── element-ui element-ui按需引入
│   |   └── filters 注册全局过滤器
|   ├── store vuex
│   |   └── index.js nuxtServerInit获取菜单及系统配置
|   ├── vendor 第三方库
│   |   ├── aplayer 音乐播放器
│   |   ├── live2d Live2D Cubism SDK v2看板娘
│   └── └── spine-ts spine骨骼动画
├── config 配置
│   ├── nuxt.admin.js 内容管理平台nuxt配置
│   └── nuxt.front.js 博客前台nuxt配置

3. 功能特性

3.1. 状态数据

在nuxtServerInit请求所有页面都需要用到的数据,比如菜单、系统配置

const state = () => ({
  categories: [],
  configs: {}
})

const mutations = {
  SET_CATEGORY: (state, categories) => {
    state.categories = categories
  },
  SET_CONFIGS: (state, configs) => {
    state.configs = configs
  }
}

const actions = {
  async nuxtServerInit({ commit }, { req, $axios }) {
    // 获取菜单以及系统配置
    const { categories, configs } = await $axios.$get(`/general`)
    commit('SET_CATEGORY', categories)
    commit('SET_CONFIGS', configs)
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

3.2. 列表页

列表页使用watchQuery属性监听分页、排序、标签参数改变,重新请求列表数据

// mixins.js
export const listPage = {
  data () {
    return {
      total: 0,
      listPage: {
        page: 1,
        pageSize: 20
      },
      filters: {},
      dynamicTags: []
    }
  },
  methods: {
    /**
     * 列表分页切换
     * @param {Object} { page: 当前页, limit: 每页个数 }
     */
    changeListPage ({ page }) {
      const { name, params, query } = this.$route
      const [id] = params.id ? params.id.split('-') : ['list']
      this.$router.push({ name, params: { id: `${id}-${page}` }, query })
    },
    handleSearch () {
      const { name, params } = this.$route
      const [id] = params.id ? params.id.split('-') : ['list']
      const serachParams = this.filterParams(this.filters)
      this.$router.push({ name, params: { id: `${id}-1` }, query: serachParams })
    },
    /**
     * 删除筛选标签
     * @param {String} tag 列表标签
     */
    handleDeleteTag (tag) {
      this.dynamicTags.splice(this.dynamicTags.indexOf(tag), 1)
      this.filters.tags = this.dynamicTags.join()
      this.handleSearch()
    },
    /**
     * 添加筛选标签
     * @param {String} tag 列表标签
     */
    handleAddTag (tag) {
      if (this.dynamicTags.includes(tag)) { return }
      this.dynamicTags.push(tag)
      this.filters.tags = this.dynamicTags.join()
      this.handleSearch()
    },
    /**
     * 剔除对象中值为空的属性
     * @param {Object} data 源对象
     * @returns {Object}
     */
    filterParams (data) {
      const target = {}
      for (const key in data) {
        if (data[key] !== '') {
          target[key] = data[key]
        }
      }
      return target
    }
  }
}

文章列表页

export default {
  name: 'ArticleList',
  mixins: [listPage],
  asyncData ({ params, query, $axios }) {
    // 请求数据
  },
  watchQuery: ['sortBy', 'orderBy', 'tags']
})

3.3. Dark Light主题

使用 @nuxtjs/color-mode 模块,实现白天、黑夜主题切换功能

styles/variables.scss

//theme
:root {
  --color-primary: #1890ff;
  --color-heading: #1f1f1f;
  --color-text: #434343;
  --color-secondary: #8c8c8c;
  --bg: #f5f5f5;
  --bg-normal: #fff;
  --border-color: #EBEEF5;
  --box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}

.dark-mode {
  --color-primary: #1890ff;
  --color-heading: #f5f5f5;
  --color-text: #d9d9d9;
  --color-secondary: #8c8c8c;
  --bg: #000;
  --bg-normal: #1f1f1f;
  --border-color: rgba(255, 255, 255, .2);
}

nuxt.config.js

module.exports = {
  buildModules: [
    '@nuxtjs/color-mode'
  ],
  css: [
    '@/styles/variables.scss'
  ]
}

样式规则,使用var(变量)就可以获取当前主题下的变量值

body{
  background-color: var(--bg);
  color: var(--color-text);
  transition: background-color .3s;
}

3.4. 代码高亮

使用prismjs来实现文章中的代码高亮功能

另一篇文章也有详细讲到:https://www.timelessq.com/article/detail/8

import Prism from 'prismjs'
import ClipboardJS from 'clipboard'

export default {
  name: 'ArticleDetail',
  mounted () {
    this.initPrism()
  },
  methods: {
    initPrism() {
      Prism.plugins.toolbar.registerButton('copy-to-clipboard', (env) => {
        const buttonElement = document.createElement('button')
        const iconElement = document.createElement('i')
        const tooltipElement = document.createElement('em')
        buttonElement.className = 'toolbar-item-button'
        iconElement.className = 'icon-fuzhi'
        tooltipElement.className = 'toolbar-item-button__tips'
        tooltipElement.innerText = '复制'
        buttonElement.appendChild(iconElement)
        buttonElement.appendChild(tooltipElement)

        const clipboard = new ClipboardJS(buttonElement, {
          text: () => {
            return env.code
          }
        })
        clipboard.on('success', () => {
          iconElement.className = 'icon-wancheng'
          clearTimeout(this.timer)
          this.timer = setTimeout(() => {
            iconElement.className = 'icon-fuzhi'
          }, 3000)

          // 复制成功,转载最好带上出处哟
        })
        clipboard.on('error', () => {
          // 复制失败了呢
        })
        return buttonElement
      })

      Prism.highlightAll()
    }
  }
})

3.5. xml渲染

博客网站需要一个sitemap网站地图,用于搜索引擎收录,再弄一个RSS订阅,这些都需要生成XML。

方案一:使用nuxt的sitemap插件,生成静态sitemap.xml文件

https://www.npmjs.com/package/@nuxtjs/sitemap

方案二:

使用thinkjs的View视图功能,通过nunjucks动态渲染xml

https://thinkjs.org/zh-cn/doc/3.0/view.html

server 控制器 controller/front/xml.js

module.exports = class extends think.Controller {
  async __before() {
    this.baseurl = 'https:' + this.siteurl;
    this.archives = await this.model('front/xml').selectArchives();
  }

  // sitemap地图
  async sitemapAction() {
    this.assign('siteurl', this.baseurl);

    const categories = await this.model('category').getCacheCategory();
    const sitemapList = await this.getSitemapList(categories);
    this.assign('list', sitemapList);

    const lastmod = sitemapList.length ? sitemapList[0].updatetime : think.datetime(new Date(), 'YYYY-MM-DD');
    this.assign('lastmod', lastmod);

    const filterCategory = categories.filter(item => !item.link);
    const targetCategory = filterCategory.map(item => {
      const { id, url, level } = item;
      const rows = sitemapList.filter(element => element.category_id === id);
      let lastmod;
      if (rows.length) {
        rows.sort((a, b) => new Date(b.updatetime) - new Date(a.updatetime));
        lastmod = rows[0].updatetime;
      }
      return {
        url: `${this.baseurl}${url}`,
        lastmod: lastmod || think.datetime(new Date(), 'YYYY-MM-DD'),
        priority: level === 1 ? 0.9 : (level === 2 ? 0.8 : 0.7)
      };
    });
    this.assign('categoryList', targetCategory);

    this.ctx.type = 'text/xml';
    return super.display('home/sitemap.xml');
  }

  /**
   * 获取sitemap列表
   * @returns {Array} 文章列表 {...url,...priority}
   */
  async getSitemapList(categories) {
    const archives = JSON.parse(JSON.stringify(this.archives));
    const sitemapList = archives.sort((a, b) => {
      return new Date(b.updatetime) - new Date(a.updatetime);
    });
    sitemapList.forEach(item => {
      const { id, category_id: categoryId } = item;
      const findCategory = categories.find(element => element.id === categoryId);
      item.url = findCategory ? `${this.baseurl}/${findCategory.type}/detail/${id}` : '';
      item.priority = 0.6;
    });
    return sitemapList;
  }
};

view/home/sitemap.xml

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>{$ siteurl $}</loc>
    <lastmod>{$ lastmod $}</lastmod>
    <priority>1.0</priority>
    <changefreq>weekly</changefreq>
  </url>
  {% for item in categoryList %}
  <url>
    <loc>{$ item.url $}</loc>
    <lastmod>{$ item.lastmod $}</lastmod>
    <priority>{$ item.priority $}</priority>
    <changefreq>weekly</changefreq>
  </url>
  {% endfor %}
  {% for item in list %}
  <url>
    <loc>{$ item.url $}</loc>
    <lastmod>{$ item.updatetime $}</lastmod>
    <priority>{$ item.priority $}</priority>
    <changefreq>weekly</changefreq>
  </url>
  {% endfor %}
</urlset>

RSS也是类似,获取最新的几篇文章

module.exports = class extends think.Controller {
  async __before() {
    this.baseurl = 'https:' + this.siteurl;
    this.archives = await this.model('front/xml').selectArchives();
  }

  // rss订阅
  async rssAction() {
    this.assign('siteurl', this.baseurl);

    const configs = await this.model('config').getCacheConfig();
    this.assign('options', configs);

    this.assign('currentTime', (new Date()).toUTCString());

    const list = await this.getRssList();
    this.assign('list', list);

    this.ctx.type = 'text/xml';
    return super.display('home/rss.xml');
  }

  /**
   * 获取最新6条动态
   * @returns {Array} 文章列表 {...url}
   */
  async getRssList() {
    const categories = await this.model('category').getCacheCategory();

    const archives = JSON.parse(JSON.stringify(this.archives));
    const rssList = archives.sort((a, b) => {
      return new Date(b.updatetime) - new Date(a.updatetime);
    }).slice(0, 6);
    rssList.forEach(item => {
      const { id, category_id: categoryId } = item;
      const findCategory = categories.find(element => element.id === categoryId);
      item.url = findCategory ? `${this.baseurl}/${findCategory.type}/detail/${id}` : '';
    });
    return rssList;
  }
};

view/home/rss.xml

<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title>{$ options.title $}</title>
    <link>{$ siteurl $}</link>
    <description>{$ options.description $}</description>
    <atom:link href="{$ siteurl $}/rss.html" rel="self" />
    <language>zh-cn</language>
    <lastBuildDate>{$ currentTime $}</lastBuildDate>
    {% for item in list %}
      <item>
        <title>{$ item.title $}</title>
        <link>{$ item.url $}</link>
        <description><![CDATA[
          {$ item.description | xml | safe $}
        ]]></description>
        <pubDate>{$ item.updatetime $}</pubDate>
        <guid>{$ item.url $}</guid>
      </item>
    {%- endfor %}
  </channel>
</rss>

推荐阅读

恰饭区

评论区 (0)

0/500

还没有评论,快来抢第一吧