前言

在我们前端的实际开发当中我们肯定会遇到相关的一下功能需要我们去使用编辑器对齐编辑,我们总不能将其写死成静态页吧,一般的情况下我们都是动态渲染出来的页面。

总所周知,Vue对SEO的是及其不友好的,解决SEO的最好的办法就是SSR(服务端渲染),使用Nuxt脚手架去开发。本篇的文章是使用富文本编辑器。接下来我们就直接开始去操作吧

代码操作

首先我们需要安装包依赖

npm install vue-quill-editor

接下来我们新建一个页面叫做Editor.vue,在里面导入vue-quill-editor包和相关的css样式。

import { quillEditor } from "vue-quill-editor";
import "quill/dist/quill.core.css";
import "quill/dist/quill.snow.css";
import "quill/dist/quill.bubble.css";

然后就是整个页面的初始化渲染,下面我就直接放出来全部的代码,代码中都注释响应各位应该能看懂什么意思

<template>
  <div>
    <div class="center">
      <el-form :model="form_data">
        <el-form-item label="文章题目:">
          <el-input
            type="text"
            v-model="form_data.title"
            placeholder="请输入文章标题"
            style="width: 150px"
          ></el-input>
        </el-form-item>
        <el-form-item label="文章分类:">
          <el-select
            v-model="form_data.type"
            placeholder="请选择文章分类"
            style="width: 150px"
          >
            <el-option
              v-for="item in options"
              :key="item.id"
              :label="item.type"
              :value="item.id.toString()"
            ></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="文章简介:">
          <el-input
            type="textarea"
            v-model="form_data.desc"
            style="width: 400px"
            placeholder="请输入文章简介,可填写也可不填写,非必填项"
            maxlength="80"
            show-word-limit
          ></el-input>
        </el-form-item>
      </el-form>
    </div>
    <!-- 图片上传组件辅助-->
    <el-upload
      class="avatar-uploader"
      :action="serverUrl"
      name="file"
      :headers="header"
      :show-file-list="false"
      :on-success="uploadSuccess"
      :on-error="uploadError"
      :before-upload="beforeUpload"
    >
    </el-upload>

    <quill-editor
      class="editor"
      v-model="content"
      ref="myQuillEditor"
      :options="editorOption"
      @blur="onEditorBlur($event)"
      @focus="onEditorFocus($event)"
      @change="onEditorChange($event)"
    >
    </quill-editor>
    <div class="btn-onsubmit">
      <template>
        <el-button type="primary" :plain="true" @click="onsubmit">提交</el-button>
      </template>
    </div>
  </div>
</template>
<script>
// 工具栏配置
const toolbarOptions = [
  ["bold", "italic", "underline", "strike"], // 加粗 斜体 下划线 删除线
  ["blockquote", "code-block"], // 引用  代码块
  [{ header: 1 }, { header: 2 }], // 1、2 级标题
  [{ list: "ordered" }, { list: "bullet" }], // 有序、无序列表
  [{ script: "sub" }, { script: "super" }], // 上标/下标
  [{ indent: "-1" }, { indent: "+1" }], // 缩进
  [{ direction: "rtl" }], // 文本方向
  [
    {
      size: [
        "10px",
        "12px",
        "14px",
        "16px",
        "18px",
        "20px",
        "22px",
        "24px",
        "26px",
        "28px",
        "30px",
        "32px",
        "36px",
        "38px",
        "40px",
        "45px",
        "50px",
        false,
      ],
    },
  ], // 字体大小
  [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
  [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
  [{ font: [] }], // 字体种类
  [{ align: [] }], // 对齐方式
  ["clean"], // 清除文本格式
  //   ["link", "image","video"] // 链接、图片、视频
  ["image"], // 链接、图片
];

import { quillEditor } from "vue-quill-editor";
import "quill/dist/quill.core.css";
import "quill/dist/quill.snow.css";
import "quill/dist/quill.bubble.css";
import { Base64 } from "js-base64";

export default {
  props: {
    /*编辑器的内容*/
    value: {
      type: String,
    },
    /*图片大小*/
    maxSize: {
      type: Number,
      default: 4000, //kb
    },
  },

  components: {
    quillEditor,
  },

  data() {
    return {
      options: [],
      content: this.value,
      form_data: {
        title: "",
        desc: "",
        type: "",
      },
      quillUpdateImg: false, // 根据图片上传状态来确定是否显示loading动画,刚开始是false,不显示
      editorOption: {
        placeholder: "",
        theme: "snow", // or 'bubble'
        placeholder: "请输入文章内容",
        modules: {
          toolbar: {
            container: toolbarOptions,
            // container: "#toolbar",
            handlers: {
              image: function (value) {
                if (value) {
                  // 触发input框选择图片文件
                  document.querySelector(".avatar-uploader input").click();
                } else {
                  this.quill.format("image", false);
                }
              },
            },
          },
        },
      },
      //   serverUrl: this.baseServerUrl+"/fileUpload/uploadPic?filepath=artwork", // 这里写你要上传的图片服务器地址
      serverUrl: "http://127.0.0.1:3000/api/file_upload",
      header: {
        // token: sessionStorage.token
      }, // 有的图片服务器要求请求头需要有token
    };
  },
  created() {
    this.getData();
  },
  inject: ["reload"], //刷新组件,
  methods: {
    //初始化数据
    getData() {
      this.$get("/management/type").then((res) => {
        this.options = res.data;
      });
    },
    //提交事件
    onsubmit() {
      console.log("提交事件");
      console.log(this.content.toString());
      console.log(this.form_data);
      if (!this.content) {
        this.$message({
          showClose: true,
          message: "请输入文章内容",
          type: "error",
        });
        return;
      }
      if (!this.form_data.title) {
        this.$message({
          showClose: true,
          message: "请输入文章标题",
          type: "error",
        });
        return;
      }
      if (!this.form_data.type) {
        this.$message({
          showClose: true,
          message: "请选择文章分类",
          type: "error",
        });
        return;
      }
      this.$post("/article/add", {
        title: Base64.encode(this.form_data.title),
        type_id: Base64.encode(this.form_data.type),
        article_desc: Base64.encode(this.form_data.desc),
        connect: Base64.encode(this.content),
      }).then((res) => {
        if (res.code == 200) {
          this.$message({
            showClose: true,
            message: res.msg,
            type: "success",
          });
          this.reload();
        } else {
          this.$message({
            showClose: true,
            message: res.msg,
            type: "error",
          });
        }
      });
    },
    onEditorBlur() {
      //失去焦点事件
    },
    onEditorFocus() {
      //获得焦点事件
    },
    onEditorChange() {
      //内容改变事件
      this.$emit("input", this.content);
    },

    // 富文本图片上传前
    beforeUpload() {
      // 显示loading动画
      this.quillUpdateImg = true;
    },
    uploadSuccess(res, file) {
      console.log(res);
      console.log(file);
      // res为图片服务器返回的数据
      // 获取富文本组件实例
      let quill = this.$refs.myQuillEditor.quill;
      // 如果上传成功
      if (res.code == 200) {
        // 获取光标所在位置
        let length = quill.getSelection().index;
        // 插入图片  res.url为服务器返回的图片地址
        quill.insertEmbed(length, "image", res.data[0]);
        // 调整光标到最后
        quill.setSelection(length + 1);
      } else {
        this.$message.error("图片插入失败");
      }
      // loading动画消失
      this.quillUpdateImg = false;
    },
    // 富文本图片上传失败
    uploadError() {
      // loading动画消失
      this.quillUpdateImg = false;
      this.$message.error("图片插入失败");
    },
  },
};
</script>

<style>
.center {
  margin-top: 30px;
  width: 930px;
  margin: 0 auto;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="10px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="10px"]::before {
  content: "10px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="12px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="12px"]::before {
  content: "12px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="14px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="14px"]::before {
  content: "14px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="16px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="16px"]::before {
  content: "16px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="18px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="18px"]::before {
  content: "18px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="20px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="20px"]::before {
  content: "20px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="22px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="22px"]::before {
  content: "22px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="24px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="24px"]::before {
  content: "24px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="26px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="26px"]::before {
  content: "26px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="28px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="28px"]::before {
  content: "28px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="30px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="30px"]::before {
  content: "30px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="32px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="32px"]::before {
  content: "32px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="36px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="36px"]::before {
  content: "36px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="38px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="38px"]::before {
  content: "38px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="40px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="40px"]::before {
  content: "40px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="45px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="45px"]::before {
  content: "45px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="50px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="50px"]::before {
  content: "50px";
}
/**
设置默认字体显示
设置默认字体颜色
**/
.ql-container {
  font-size: 16px;
  color: black;
}
.btn-onsubmit {
  margin-top: 50px;
  text-align: center;
}
.btn-onsubmit .el-button {
  width: 30%;
}
.editor {
  line-height: normal !important;
  width: 930px;
  margin: 0 auto;
}
.ql-container {
  height: 400px !important;
}
.ql-snow .ql-tooltip[data-mode="link"]::before {
  content: "请输入链接地址:";
}
.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
  border-right: 0px;
  content: "保存";
  padding-right: 0px;
}

.ql-snow .ql-tooltip[data-mode="video"]::before {
  content: "请输入视频地址:";
}

.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
  content: "14px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before {
  content: "10px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before {
  content: "18px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before {
  content: "32px";
}

.ql-snow .ql-picker.ql-header .ql-picker-label::before,
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
  content: "文本";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
  content: "标题1";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
  content: "标题2";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
  content: "标题3";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
  content: "标题4";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
  content: "标题5";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
  content: "标题6";
}

.ql-snow .ql-picker.ql-font .ql-picker-label::before,
.ql-snow .ql-picker.ql-font .ql-picker-item::before {
  content: "标准字体";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before {
  content: "衬线字体";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before {
  content: "等宽字体";
}
</style>

这里我使用的传送方式是Base64加密,可以减少传送中的数据异常修改,虽然Base64可以被解密出来,但是加密和不加密是两种情况

里面的图片的地址可以替换成自己的项目上传地址,我这里我上传是没有做任何权限限制的,所以存在的问题就是任意用户都可以上传文件,从而导致的服务器资源滥用。

这是咱们后台的使用方式,那么我们前端怎么进行页面的渲染呢?

接下来怎么看具体的代码实现步骤

在前端的Vue项目中,我们同样需要安装相关的编辑器依赖

npm install vue-quill-editor

然后咱们的页面中引入相关的依赖

import { quillEditor } from "vue-quill-editor";
import "quill/dist/quill.core.css";
import "quill/dist/quill.snow.css";
import "quill/dist/quill.bubble.css";

接下来就是接口数据的绑定

<div class="ql-container ql-snow">
	<div class="content ql-editor" v-html="article_list.connect" />
</div>

在v-html中就是咱们的数据。

*注意:在使用富文本编辑器的使用我们要格外的注意我们的当中的XSS漏洞,此XSS漏洞是输入存储型的,危害很高!

我这里建议搭建使用xss组件对齐进行白名单过滤,转义,禁止一切的js代码执行,可以很有效防范XSS攻击