改造百度ueditor

背景

富文本編輯是管理後臺(cms)系統中的重要功能,編輯器的選擇也很是多,現在大多編輯器都是走的簡約路線,趕上挑剔的客戶就沒法知足他們的需求。百度的ueditor做爲一款重量級的編輯器,提供了強大的功能,而且從word中直接copy到編輯器中的還原效果也很是好,可是因爲官方已經好久沒有維護了,因此對接已有的系統靈活度不夠。 基於vue封裝的ueditor組件挺多的,而且封裝和改造的效果都還不錯,好比vue-ueditor-wrap,在封裝react-ueditor-component過程當中也借鑑了開源社區中的優秀代碼。php

功能需求

ueditor其餘功能沒什麼須要改動的,可是上傳文件的功能與後端耦合過高,不符合如今的先後端分離的系統設計,也很差對接第三方存儲(如七牛OSS),因此要改造實現基本的兩個功能:html

  1. 後端配置前移,上傳文件的配置參數直接寫在前端,不須要請求一次後端接口才能初始化上傳文件的功能
  2. 自定義上傳文件的請求頭和請求,雖然官方提供瞭解決方案,可是不夠靈活

另外一塊就是基礎的編輯功能,封裝後的組件應該像input使用同樣簡單,value控制編輯器內容,onChange監聽編輯器內容變化事件前端

下面解析一些核心功能的實現思路vue

初始化編輯器

ueditor的初始化是異步的,因此須要在編輯器準備就緒後才能進行後續的操做,這裏使用Promise進行流程控制node

componentDidMount () {
  // 編輯器ready後再進行後續操做
  this.setState(state => ({
    editorReady: new Promise((resolve, reject) => {
      let ueditor = window.UE.getEditor(this.editorId, {
        ...this.ueditorOptions, // 一些默認參數
        ...this.props.ueditorOptions // props傳入的參數
      });

      ueditor.ready(() => {
        resolve(ueditor);

        this.observerChangeListener(ueditor); // 初始化監聽編輯器變化的方法,後面會具體說明

        ueditor.setContent(this.props.value || '');
      });
    })
  }));
}
複製代碼

value

value改變觸發react-ueditor-component中的編輯器的變化是個很簡單的父組件向子組件傳參, 使用static getDerivedStateFromProps就能夠實現react

static getDerivedStateFromProps (nextProps, prevState) {
  let editorReady = prevState.editorReady;
  let value = nextProps.value;

  if (Object.prototype.hasOwnProperty.call(nextProps, 'value')) {
    editorReady && editorReady.then((ueditor) => {
      (value === prevState.content || value === ueditor.getContent()) || ueditor.setContent(value || '');
    });
  }

  return {
    ...prevState,
    content: value
  };
}
複製代碼

上面的代碼比想象中複雜一點,在組件內的state中會建立一個屬性content用於存儲上次傳過來的valueprops.value會和content和編輯器中實際的內容比較 由於在一些特殊狀況下,編輯器中的內容會發生變化,而同時getDerivedStateFromProps會被觸發可是value並無發生變化,若是不進行比較編輯器中的內容會被回退爲舊值。git

onChange

編輯器內容變化可使用ueditor提供的contentChange,可是會有bug,好比按下多個按鍵時並不會觸發該事件github

react-ueditor-component採用MutationObserver監聽DOM變化web

observerChangeListener (ueditor) {
  const changeHandle = () => {
    let onChange = this.props.onChange;

    if (ueditor.document.getElementById('baidu_pastebin')) {
      return;
    }

    onChange && onChange(ueditor.getContent());
  };

  this.observer = new MutationObserver(changeHandle); // FIXME: 這裏可使用debounce節流

  this.observer.observe(ueditor.body, {
      attributes: true, // 是否監聽 DOM 元素的屬性變化
      attributeFilter: ['src', 'style', 'type', 'name'], // 只有在該數組中的屬性值的變化纔會監聽
      characterData: true, // 是否監聽文本節點
      childList: true, // 是否監聽子節點
      subtree: true // 是否監聽後代元素
    });
}
複製代碼

後端配置前移

此功能的實現須要修改ueditor源碼了,筆者從fex-team/ueditorfork了一份,基於dev-1.4.3.3分支建立了dev-3.0.0分支,github,全部代碼的修改都用MARK:標記出來了,能夠全局搜索查看全部源碼改動json

只須要找到獲取配置的方法並修改就能夠了,在_src/core

UE.Editor.prototype.loadServerConfig = function(){
  this._serverConfigLoaded = false;

  try {
    utils.extend(this.options, this.options.serverOptions);
    utils.extend(this.options, this.options.serverExtra);
    this.fireEvent('serverConfigLoaded');
    this._serverConfigLoaded = true;
  } catch (e) {
    console.error(this.getLang('loadconfigFormatError'));
  }
}
複製代碼

相應的,封裝的react-ueditor-component增長了字段配置

window.UE.getEditor(this.editorId, {
  serverUrl: this.props.ueditorOptions.serverUrl,
  serverOptions: {
    imageActionName: 'uploadimage',
    imageFieldName: 'file',
    ...others
  },
  serverExtra: this.props.ueditorOptions.serverUrl
});
複製代碼

beforeUpload鉤子

beforeUpload鉤子是自定義請求數據實現的關鍵,但實現的功能又不止於增長自定義請求數據

beforeUpload方法由參數傳入ueditor

上傳前須要進行的操做不少狀況下多是一個異步過程,這裏使用Promise進行流程控制,以autoupload.js爲例

if (me.options.beforeUpload) {
  Promise.resolve(me.options.beforeUpload(file)).then(function (file) {
    if (!file) {
      return
    }

    // 設置請求頭和請求內容,開始上傳
  })
} else {
  // 設置請求頭和請求內容,開始上傳
}
複製代碼

自定義請求數據

自定義請求數據用serverExtra實現,須要這部份內容是隨時可變的,因此須要新增一個方法,能夠隨時設置serverExtra

UE.Editor.prototype.setExtraData = function (options) {
  try {
    utils.extend(this.options, options);
  } catch (e) {
    console.error(this.getLang('setExtraconfigFormatError'));
  }
}
複製代碼

上面的代碼不難看出來,實際上setExtraData方法能夠設置任何配置,可是後續封裝組件並使用時,我只建議用於修改serverExtra,由於修改ueditor的其餘參數並不必定有效,而且可能會出現沒法預期的bug。

在每次執行上傳以前應該讀取配置、設置上傳內容,以autoupload.js爲例

var fd = new FormData()
// 請求體中增長額外數據
if (me.options.extraData && Object.prototype.toString.apply(me.options.extraData) === "[object Object]") {
  for (var key in me.options.extraData) {
      fd.append(key, me.options.extraData[key]);
  }
}
// 請求頭中增長額外數據
if (me.options.headers && Object.prototype.toString.apply(me.options.headers) === "[object Object]") {
  for (var key in me.options.headers) {
    xhr.setRequestHeader(key, me.options.headers[key]);
  }
}
複製代碼

封裝在組件中,須要在static getDerivedStateFromProps中實現響應式更新

if (Object.prototype.hasOwnProperty.call(nextProps.ueditorOptions, 'serverExtra')) {
  let serverExtraStr = JSON.stringify(nextProps.ueditorOptions.serverExtra);

  if (serverExtraStr === prevState.serverExtraStr) {
    return {
      ...prevState,
      content: value
    };
  }
  editorReady && editorReady.then((ueditor) => {
    ueditor.setExtraData && ueditor.setExtraData(nextProps.ueditorOptions.serverExtra);
  });
  return {
    ...prevState,
    serverExtraStr,
    content: value
  };
}
複製代碼

以上即是ueditor改造和封裝中最核心的內容,下面簡單介紹一下應該如何使用react-ueditor-component,詳細的使用教程請看readme.md,項目源碼中也提供了完整的demoApp.js(不使用react-ueditor-component)、OwnServer.js(使用react-ueditor-component上傳到本身的服務器)、QiniuServer.js(使用react-ueditor-component對接七牛OSS)。

安裝和引入

安裝組件

yarn add react-ueditor-component --save
複製代碼

下載修改後打包的ueditor.zip,或者找到node_modules/react-ueditor-component/assets/utf8-php.zip,解壓文件,放在網站的根目錄,react項目通常放在public文件夾下, index.htmlscript標籤引入ueditor代碼

<script src="/utf8-php/ueditor.config.js"></script>
<script src="/utf8-php/ueditor.all.js"></script>
複製代碼

若是你只須要編輯功能

import ReactUEditorComponent from 'react-ueditor-component';

export default class App extends React.Component {
  state = {
    value: ''
  }

  onChange = (value) => this.setState(value);

  render () {
    <div>
    <ReactUEditorComponent value={this.state.value} onChange={this.onChange} /> {/* 配合antd的form */} { this.props.form.getFieldDecorator('content')( <ReactUEditorComponent /> ) } </div>
  }
}
複製代碼

如何使用beforeUpload鉤子

一般對接第三方OSS須要獲取上傳憑證,這就須要用到beforeUpload鉤子

export default class App extends React.Component {
  state = {
    value: '',
    serverExtra: {
      // 上傳文件額外的數據
      extraData: {}
    }
  }

  beforeUpload = file => new Promise((resolve, reject) => {
    let key = 't' + Math.random().toString().slice(5, 16);

    // 請求服務器,獲取七牛上傳憑證
    fetch('getuploadtoken.com', {
      headers
    })
      .then(response => response.json())
      .then((data) => {
        // 設置七牛直傳額外數據
        this.setState({
          serverExtra: {
            extraData: {
              token: data.token,
              key
            }
          },
          // 設置額外數據完成會觸發`setExtraDataComplete`
          setExtraDataComplete: () => {
            resolve(file);
          }
        });
      });
  })

  onChange = (value) => this.setState(value);

  render () {
    return (
      <ReactUEditorComponent value={this.state.value} onChange={this.onChange} // 必須在statesetExtraDataComplete={this.state.setExtraDataComplete} ueditorOptions={{ beforeUpload: this.beforeUpload, // 上傳文件時的額外信息 serverExtra: this.state.serverExtra, serverUrl: 'http://qiniuupload.com' // 上傳文件的接口 }} /> ) } } 複製代碼

但願以上輪子有朝一日對你有所幫助,歡迎提供技術支持,或者加入咱們 yemao@talkmoney.cn

做者簡介:葉茂,蘆葦科技web前端開發工程師,表明做品:口紅挑戰網紅小遊戲、服務端渲染官網、微信小程序粒子系統。擅長網站建設、公衆號開發、微信小程序開發、小遊戲、公衆號開發,專一於前端領域框架、交互設計、圖像繪製、數據分析等研究。 一塊兒並肩做戰: yemao@talkmoney.cn 訪問 www.talkmoney.cn 瞭解更多

相關文章
相關標籤/搜索