thumbnail
nvPress的诞生给广大前端开发者提供了一个优秀的博客平台,它可以完全基于你所熟悉的任何框架进行博客主题开发。本文将介绍,如何使用官方提供的主题示例,开发一个自己的nvPress主题。
SCOTT-STUDIO

开发注意事项

环境搭建

本地开发需要nodejs环境,同时需要安装本地开发版nvPress

nodejs的安装请自行百度

本地开发介绍

nvPress的主题完全属于前端开发,如果你是一位前端开发工作者,你可以使用任何你喜欢的框架进行开发,官方示例是通过vue.js进行开发的,所以本文将以vue.js为例展开介绍。

创建项目

你可以使用命令yarn create vite创建一个全新的项目,或者从nvPress官方下载主题开发示例

项目目录结构

  • front-end-web:主题打包后的代码,也可以是其他的目录名称
  • front-end-source:主题源代码
  • function.js:主题驱动文件,类似于vue项目中的main.js
  • theme.json:主题信息文件,包含主题的版本以及作者信息等
  • 其他的目录及文件

源码结构说明

主题分为:首页、分类页、内容页 三个部分。各部分代码分别在以下文件夹:

  • 首页:front-end-source\src\pages\home\home.vue
  • 分类页:front-end-source\src\pages\term\term.vue
  • 文章或页面的内容页:front-end-source\pages\post\post.vue

这是主题的基础内容,其他的内容比如封装的组件,自定义的编辑器模块等,都可以根据自己的代码风格进行定义,当然如果你不喜欢默认的三个部分的目录结构,也可以进行修改。

只需要修改 front-end-source\src\router.js 中的组件引入定义即可

项目运行与部署

  • 安装依赖:npm install
  • 本地运行:npm run dev
  • 项目打包:npm run build

打包说明

打包后需要将除了源代码(此处为/front-end-source)之外的所有目录都放进主题目录中

示例结构:

theme.json
front-end-web
functions.js
...

运行nvPress进行本地开发

1、将打包后的文件夹剪切到nvPress本地开发版的nv-themes文件夹中

2、nvpress启动方法

Windows 系统启动 nvPress 方法

  1. 在资源管理器中打开nvPress本地开发文件夹
  2. 路径定位到nvPress本地开发文件夹:在空白处按住shift点击鼠标右键,选择:在此处打开命令行
  3. 输入nvpress.exe然后回车

macOS 系统启动 nvPress 方法

  1. 打开终端app
  2. 路径定位到nvPress本地开发文件夹,输入./nvpress 回车

3、此时,终端中会显示nvPress启动成功并告诉你后台地址。

4、访问nvPress后台地址http://localhost:8081/nv-admin/

5、完成简单的注册流程后,登录后台打开外观-主题

6、启动你开发的主题,然后重启 nvPress

使用functions开发主题

nvPress官方为我们提供了restApi和functions api,在此基础上可以完成与数据的交互和主题的配置

nvpress REST API 文档

nvpress Functions API 文档

配置渲染模式

set_frontstage_rendering_mode(renderType, dirPath);

renderType:SSR服务器端渲染模式或者CSR客户端渲染模式

dirPath:配置主题打包文件目录,默认为/web

注册导航菜单

register_nav_menus(options);

options:导航菜单

register_nav_menus({
    topNav: '顶部菜单',
    catNav: '分类菜单',
})

增加后台配置菜单

add_submenu_page 官方文档

示例

add_submenu_page({
  parent_slug: 'appearance',
  page_title: '主题设置',
  menu_title: '主题设置',
  menu_slug: 'theme-settings',
  power: 10,
  position: 9,
  component_url: '/srcs/page-settings/index.vue',
});

注册静态资源根路径

register_static_url(url_path, folder_path)

url_path:URL地址,如:"/srcs"

folder_path:指向的本地文件夹地址,如:path.join(__dirname,"./srcs/")

比如我们需要开发一些在编辑器中使用的小组件,并且打算将其放在根目录下的/srcs目录中,就可以在function.js中使用这个钩子注册

注册REST API

register_rest_route(namespace, name, args)

官方文档

示例

// 查询最近5条已发布公告列表,按照默认排序倒序查询
register_rest_route('salary', 'bulletin-post', {
  methods: 'post',
  callback(data, req) {
    var posts = query_posts({
      post_type: 'bulletin',
      status: 'publish',
      order: 'DESC',
      posts_per_page: 5,
    });
    return posts.data;
  },
});

注册自定义编辑器模块

register_editor_block(block_id, supports, script_url)

官方文档

示例:

register_editor_block('scottstudio/alert', ['page', 'article'], '/srcs/block-alert/index.js');

nvPress使用的编辑器是editor.js,一般的自定义模块包含三个文件

  • index.js 注册文件
  • settings_ui.vue 自定义模块的工具配置文件(比如可以修改模块的文字排版和颜色)
  • ui.vue 自定义模块的ui渲染文件

示例代码:

// index.js
export default ({ register_block_type }) => {
  register_block_type('scottstudio/title', {
    // 编辑器内显示的标题
    name: '小标题',
    // 编辑器内显示的图标
    icon: ``,
    // 模块的属性
    attributes: {
      tag: 'h2',
      style: 1,
      text: '',
    },
    sanitize(editor) {
      return {};
    },
    // 模块主要界面
    editor: {
      url: '/srcs/block-title/ui.vue',
    },
    // 模块的配置项
    settings: {
      url: '/srcs/block-title/settings_ui.vue',
    },
  });
};
// ui.vue
<template>
  <div class="salary-title" :data-style="style" :data-tag="tag">
    <div class="flex items-center justify-start relative overflow-hidden">
      <div class="icon"><i class="fa-solid fa-hashtag"></i></div>
      <richText class="title" :tag="tag" v-model:value="text" nowrap />
    </div>
  </div>
</template>
<script>
export default {
  name: 'scottstudio-title-block',
  components: {
    richText: nv.components.richText,
  },
  data() {
    // 这里是index.js中设置的attributes
    return {
      tag: 'h2',
      style: 1,
      text: '',
    };
  },
  mounted() {
    // 加载默认数据
    nv.block.loadDefaultData.bind(this)();
  },
};
</script>
<style scoped>
</style>
// settings_ui.vue
<template>
  <div>
    <div class="tune-buttons">
      <button class="cdx-settings-button" :class="{ 'cdx-settings-button--active': tag == 'h2' }" @click="tag = 'h2'"><strong>H2</strong></button>
      <button class="cdx-settings-button" :class="{ 'cdx-settings-button--active': tag == 'h3' }" @click="tag = 'h3'"><strong>H3</strong></button>
      <button class="cdx-settings-button" :class="{ 'cdx-settings-button--active': tag == 'p' }" @click="tag = 'p'"><strong>P</strong></button>
    </div>
    <div class="tune-buttons border-bottom">
      <button class="cdx-settings-button" :class="{ 'cdx-settings-button--active': style == 1 }" @click="style = 1">样式1</button>
      <button class="cdx-settings-button" :class="{ 'cdx-settings-button--active': style == 2 }" @click="style = 2">样式2</button>
    </div>
  </div>
</template>
<script>
export default {
  name: 'scottstudio-title-settings',
  components: {},
  data() {
    return {
      tag: 'h2',
      style: 1,
      text: '',
    };
  },
  mounted() {
    // 加载默认数据
    nv.block.loadDefaultData.bind(this)();
  },
  methods: {
    handleTypeSelect(type) {
      this.type = type;
    },
  },
};
</script>
<style scoped>
</style>

同时我们的源代码中也需要对模块进行渲染

// /src/components/block-parser/scott-studio-title.vue

<template>
  <div class="salary-category">
    <div class="flex items-center">
      <div class="icon"><i class="fa-solid fa-hashtag"></i></div>
      <component class="title" :is="data.tag" v-html="data.text" />
    </div>
  </div>
</template>
<script setup lang="ts">
interface ITitleProps {
  style: number;
  tag: string;
  text: string;
}
defineProps<{ data: ITitleProps }>();
</script>
<style lang="less" scoped>
</style>

这里我们定义一个blockParser组件,统一对所有的编辑器模块进行渲染


// /src/components/block-parser/parser.vue
<template>
  <component :is="is" class="nv-blocks">
    <p v-if="blocks.length == 0">暂无数据</p>
    <template v-for="block in blocks">
      <component :is="`block-${block.type.replace(/\//g, '-')}`" :data="block.data" :data-block-id="block.id" />
    </template>
  </component>
</template>
<script>
import { defineComponent, defineAsyncComponent, computed } from 'vue';

const files = import.meta.glob('./block-*.vue');
const modules = {};
for (const key in files) {
  modules[key.replace(/(\.\/|\.vue)/g, '')] = defineAsyncComponent(files[key]);
}

export default defineComponent({
  name: 'block-parser',
  components: {
    ...modules,
  },
  props: {
    is: {
      type: String,
      default: 'div',
    },
    blocks: {
      type: Array,
    },
  },
});
</script>

<style>
.nv-blocks a {
  color: var(--primary-color);
  text-decoration: none;
  text-shadow: 2px 2px 2px var(--primary-opacity-3);
}
.nv-blocks mark {
  background: rgb(var(--orangered-1));
  border-radius: 5px;
  color: rgb(var(--orangered-6));
  padding: 3px 5px;
  margin: 0 3px;
}
</style>

在页面中使用:

import blockParser from '@/components/block-parser/parser.vue';
<blockParser  :blocks="data.content.blocks" />

增加后台配置菜单

以主题设置为例:

获取主题设置的REST API /nv/get-options

更新主题设置的REST API /nv/set-options

就像写普通的vue页面一样,先获取到已有的配置,并且塞入到表单内,同时保存表单时提交更新设置

<template>
  <div class="nv-admin-page">
    <div class="page-title">
      <span>{{ $route.meta.title }}</span>
    </div>
    <div class="page-content flex-grow">
      <pd-form :config="formConfig" :data="formData" @submit="handleSubmit">
        <template v-slot:rewardLinks>
          <nvSettingTable add-label="添加图片" :columns="rewardLinksColumns" v-model:data="formData.scott_reward_links">
            <template v-slot:column-image="row">
              <nv-thumbnail-selector :height="40" v-model:value="formData.scott_reward_links[row.$index].image" />
            </template>
            <template v-slot:column-text="row">
              <n-input v-model:value="formData.scott_reward_links[row.$index].text" />
            </template>
            <template v-slot:column-url="row">
              <n-input placeholder="http(s)://" v-model:value="formData.scott_reward_links[row.$index].url" />
            </template>
          </nvSettingTable>
        </template>
        <template v-slot:personalLinks>
          <nvSettingTable add-label="添加链接" :columns="personalLinksColumns" v-model:data="formData.scott_personal_links">
            <template v-slot:column-image="row">
              <nv-thumbnail-selector :height="40" v-model:value="formData.scott_personal_links[row.$index].image" />
            </template>
            <template v-slot:column-text="row">
              <n-input v-model:value="formData.scott_personal_links[row.$index].text" />
            </template>
            <template v-slot:column-url="row">
              <n-input placeholder="http(s)://" v-model:value="formData.scott_personal_links[row.$index].url" />
            </template>
          </nvSettingTable>
        </template>
      </pd-form>
    </div>
  </div>
</template>

<script>
export default {
  name: 'niRvana-theme-settings',
  data() {
    return {
      formConfig: {
        form: {
          labelWidth: '7em',
          size: 'large',
          submitText: '保存设置',
        },
        items: [
          {
            label: '打赏图片设置',
            type: 'FormSubtitle',
          },
          {
            custom_type: 'rewardLinks',
            prop: 'scott_reward_links',
          },
          {
            label: '个人外链设置',
            type: 'FormSubtitle',
          },
          {
            custom_type: 'personalLinks',
            prop: 'scott_personal_links',
          },
          {
            label: '卡片样式设置',
            type: 'Select',
            prop: 'scott_card_theme',
            config: {
              clearable: true,
              style: 'width: 200px',
              defaultValue: 1,
              options: [
                { value: 1, label: '样式一' },
                { value: 2, label: '样式二' },
              ],
            },
          },
        ],
      },
      formData: {
        scott_reward_links: [],
        scott_personal_links: [],
      },
      rewardLinksColumns: [
        { title: '图片', key: 'image' },
        { title: '文本', key: 'text' },
        { title: '链接地址', key: 'url' },
      ],
      personalLinksColumns: [
        { title: '文本', key: 'text' },
        { title: '链接地址', key: 'url' },
      ],
    };
  },
  mounted() {
    this.requestData();
  },
  methods: {
    requestData() {
      //从formConfig里面读出需要从后端得到的options数据
      var names = [];
      this.formConfig.items.forEach((item) => {
        var prop = item.prop;
        if (prop) {
          names.push(prop);
        }
      });
      $fullLoading.start();
      this.$axios({
        method: 'post',
        url: this.$API + '/nv/get-options',
        data: {
          names,
        },
      })
        .then(({ data }) => {
          if (!this.$isSuccess(data)) {
            return;
          }
          this.formData = data;
          this.$nextTick(() => {
            this.formDataChanged = false;
          });
        })
        .catch((error) => {
          $message.warning('读取设置请求失败');
          console.log(error);
        })
        .finally(() => {
          $fullLoading.end();
        });
    },
    handleSubmit() {
      $fullLoading.start();
      this.$axios({
        method: 'post',
        url: this.$API + '/nv/set-options',
        data: this.formData,
      })
        .then(({ data }) => {
          if (!this.$isSuccess(data)) {
            return;
          }
          $message.success('保存成功');
          this.formDataChanged = false;
        })
        .catch((error) => {
          $message.warning('保存设置请求失败');
          console.log(error);
        })
        .finally(() => {
          $fullLoading.end();
        });
    },
  },
};
</script>

<style scoped></style>

如果想添加其他的配置页面也是一样的方法。

示例主题源码获取

示例主题由nvPress官方提供,传送门

当然,如果你觉得示例主题太过于简单,你也可以下载官方免费提供的主题niRvana进行二次开发

也可以使用我二次开发的主题,SCOTT-THEME

如果你觉得本文对你有所帮助,可以帮我GitHub点个star或者请我喝杯奶茶~万分感谢🎉🎉🎉