改造百度ueditor

背景

富文本編輯是管理后臺(cms)系統(tǒng)中的重要功能唆鸡,編輯器的選擇也非常多涝影,如今大多編輯器都是走的簡約路線,遇上挑剔的客戶就無法滿足他們的需求喇闸。百度的ueditor作為一款重量級的編輯器袄琳,提供了強大的功能,并且從word中直接copy到編輯器中的還原效果也非常好燃乍,但是由于官方已經(jīng)很久沒有維護了宛琅,所以對接已有的系統(tǒng)靈活度不夠。
基于vue封裝的ueditor組件挺多的嘿辟,并且封裝和改造的效果都還不錯,比如vue-ueditor-wrap红伦,在封裝react-ueditor-component過程中也借鑒了開源社區(qū)中的優(yōu)秀代碼。

功能需求

ueditor其他功能沒什么需要改動的昙读,但是上傳文件的功能與后端耦合太高,不符合現(xiàn)在的前后端分離的系統(tǒng)設計,也不好對接第三方存儲(如七牛OSS)只嚣,所以要改造實現(xiàn)基本的兩個功能:

  1. 后端配置前移,上傳文件的配置參數(shù)直接寫在前端册舞,不需要請求一次后端接口才能初始化上傳文件的功能
  2. 自定義上傳文件的請求頭和請求,雖然官方提供了解決方案调鲸,但是不夠靈活

另一塊就是基礎的編輯功能,封裝后的組件應該像input使用一樣簡單线得,value控制編輯器內(nèi)容,onChange監(jiān)聽編輯器內(nèi)容變化事件

下面解析一些核心功能的實現(xiàn)思路

初始化編輯器

ueditor的初始化是異步的贯钩,所以需要在編輯器準備就緒后才能進行后續(xù)的操作,這里使用Promise進行流程控制

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

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

        this.observerChangeListener(ueditor); // 初始化監(jiān)聽編輯器變化的方法办素,后面會具體說明

        ueditor.setContent(this.props.value || '');
      });
    })
  }));
}

value

value改變觸發(fā)react-ueditor-component中的編輯器的變化是個很簡單的父組件向子組件傳參角雷,
使用static getDerivedStateFromProps就可以實現(xiàn)

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
  };
}

上面的代碼比想象中復雜一點,在組件內(nèi)的state中會創(chuàng)建一個屬性content用于存儲上次傳過來的value性穿,props.value會和content和編輯器中實際的內(nèi)容比較
因為在一些特殊情況下勺三,編輯器中的內(nèi)容會發(fā)生變化,而同時getDerivedStateFromProps會被觸發(fā)但是value并沒有發(fā)生變化需曾,如果不進行比較編輯器中的內(nèi)容會被回退為舊值吗坚。

onChange

編輯器內(nèi)容變化可以使用ueditor提供的contentChange,但是會有bug呆万,比如按下多個按鍵時并不會觸發(fā)該事件

react-ueditor-component采用MutationObserver監(jiān)聽DOM變化

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節(jié)流

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

后端配置前移

此功能的實現(xiàn)需要修改ueditor源碼了商源,筆者從fex-team/ueditorfork了一份,基于dev-1.4.3.3分支創(chuàng)建了dev-3.0.0分支谋减,github牡彻,所有代碼的修改都用MARK:標記出來了,可以全局搜索查看所有源碼改動

只需要找到獲取配置的方法并修改就可以了出爹,在_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鉤子是自定義請求數(shù)據(jù)實現(xiàn)的關鍵,但實現(xiàn)的功能又不止于增加自定義請求數(shù)據(jù)

beforeUpload方法由參數(shù)傳入ueditor

上傳前需要進行的操作很多情況下可能是一個異步過程严就,這里使用Promise進行流程控制总寻,以autoupload.js為例

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

    // 設置請求頭和請求內(nèi)容,開始上傳
  })
} else {
  // 設置請求頭和請求內(nèi)容梢为,開始上傳
}

自定義請求數(shù)據(jù)

自定義請求數(shù)據(jù)用serverExtra實現(xiàn)渐行,需要這部分內(nèi)容是隨時可變的轰坊,所以需要新增一個方法,可以隨時設置serverExtra

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

上面的代碼不難看出來殊轴,實際上setExtraData方法可以設置任何配置衰倦,但是后續(xù)封裝組件并使用時,我只建議用于修改serverExtra旁理,因為修改ueditor的其他參數(shù)并不一定有效樊零,并且可能會出現(xiàn)無法預期的bug。

在每次執(zhí)行上傳之前應該讀取配置孽文、設置上傳內(nèi)容驻襟,以autoupload.js為例

var fd = new FormData()
// 請求體中增加額外數(shù)據(jù)
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]);
  }
}
// 請求頭中增加額外數(shù)據(jù)
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中實現(xiàn)響應式更新

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改造和封裝中最核心的內(nèi)容芋哭,下面簡單介紹一下應該如何使用react-ueditor-component沉衣,詳細的使用教程請看readme.md,項目源碼中也提供了完整的demo减牺,App.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吸占,解壓文件矾屯,放在網(wǎng)站的根目錄菌湃,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: {
      // 上傳文件額外的數(shù)據(jù)
      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) => {
        // 設置七牛直傳額外數(shù)據(jù)
        this.setState({
          serverExtra: {
            extraData: {
              token: data.token,
              key
            }
          },
          // 設置額外數(shù)據(jù)完成會觸發(fā)`setExtraDataComplete`
          setExtraDataComplete: () => {
            resolve(file);
          }
        });
      });
  })

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

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

希望以上輪子有朝一日對你有所幫助势似,歡迎提供技術支持履因,或者加入我們 yemao@talkmoney.cn

作者簡介:葉茂栅迄,蘆葦科技web前端開發(fā)工程師,代表作品:口紅挑戰(zhàn)網(wǎng)紅小游戲西篓、服務端渲染官網(wǎng)憋活、微信小程序粒子系統(tǒng)。擅長網(wǎng)站建設吮成、公眾號開發(fā)粱甫、微信小程序開發(fā)冗美、小游戲、公眾號開發(fā)节预,專注于前端領域框架属韧、交互設計、圖像繪制糠赦、數(shù)據(jù)分析等研究拙泽。 一起并肩作戰(zhàn): yemao@talkmoney.cn 訪問 www.talkmoney.cn 了解更多

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末顾瞻,一起剝皮案震驚了整個濱河市德绿,隨后出現(xiàn)的幾起案子退渗,更是在濱河造成了極大的恐慌会油,老刑警劉巖古毛,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喇潘,死亡現(xiàn)場離奇詭異颖低,居然都是意外死亡,警方通過查閱死者的電腦和手機蹬敲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門伴嗡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來从铲,“玉大人,你說我怎么就攤上這事阱扬∩毂伲” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵窃蹋,是天一觀的道長警没。 經(jīng)常有香客問我惠奸,道長恰梢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任嗅回,我火速辦了婚禮绵载,結果婚禮上苛白,老公的妹妹穿的比我還像新娘。我一直安慰自己懂版,他們只是感情好躏率,可當我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布薇芝。 她就那樣靜靜地躺著,像睡著了一般嚷缭。 火紅的嫁衣襯著肌膚如雪耍贾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天优床,我揣著相機與錄音胆敞,去河邊找鬼杂伟。 笑死,一個胖子當著我的面吹牛观话,可吹牛的內(nèi)容都是我干的频蛔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼瀑粥,長吁一口氣:“原來是場噩夢啊……” “哼三圆!你這毒婦竟也來了?” 一聲冷哼從身側響起修噪,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤黄琼,失蹤者是張志新(化名)和其女友劉穎磷籍,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體弛矛,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡丈氓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年万俗,在試婚紗的時候發(fā)現(xiàn)自己被綠了饮怯。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡库倘,死狀恐怖论矾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情饱亿,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布钻注,位于F島的核電站队寇,受9級特大地震影響章姓,放射性物質(zhì)發(fā)生泄漏凡伊。R本人自食惡果不足惜窒舟,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一惠豺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧洁墙,春花似錦蛹疯、人聲如沸捺弦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至理郑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間香浩,已是汗流浹背臼勉。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工宴霸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留膏蚓,地道東北人。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓驮瞧,卻偏偏與公主長得像论笔,于是被迫代替她去往敵國和親千所。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內(nèi)容

  • HTML模版 之后出現(xiàn)的React代碼嵌套入模版中最楷。 1. Hello world 這段代碼將一個一級標題插入到指...
    ryanho84閱讀 6,243評論 0 9
  • 【紅旗飄飄】Win7專業(yè)業(yè)版64位適度精簡版 版本號7601.24260 基于官方原版適度精簡籽孙,保留微軟拼音輸入法...
    紅旗飄飄588閱讀 2,898評論 0 1
  • 記住大學時,我們都被班長同學的各項優(yōu)異成績所信服忆家。 有一次,一位同學逮到時機調(diào)侃了班長揭芍,問她是不是天分異稟卸例? 出人...
    wuyexuan閱讀 202評論 0 0
  • 今天語文課學的三個生字,東西可筷转。數(shù)學練習了10以內(nèi)的減法。圍棋今天我們考試了锭汛。老師還教我們做了手指操。體育課我們練...
    王哲浩閱讀 105評論 0 0