Vue專案中最簡單的使用整合UEditor方式,含圖片上傳

鄭昊川發表於2018-08-22

本文永久連結:github.com/HaoChuan942…

前言

封面是UEditor百度指數 折線圖。雖然今天已經是 2018 年,且優秀的富文字編輯器層出不窮(包括移動端),但從圖中可以看出UEditor仍然維持著較高的搜尋熱度。而不少公司和個人也仍然在專案中使用UEditor。目前,UEditor官網的最後一次版本更新是 1.4.3.3,這已經是 2016 年的事情了,而今天的前端開發,很多小夥伴都在使用VueReact 這種元件化的前端框架。這就導致在這些“現代”框架中整合UEditor變得很不平滑。所以才會有下圖這些大量介紹如何在Vue專案中整合UEditor的部落格:

Vue專案中最簡單的使用整合UEditor方式,含圖片上傳

為了提高程式碼的可複用性,也為了儘可能的不在業務程式碼中參雜UEditor的相關操作,我在幾個月前,公司專案的開發中擼了一個元件,可以通過v-model雙向繫結的方式來使用UEditor,簡單到就像使用input框一樣。當我擼完,感覺非常的Vue範兒。而且看了不少部落格和GitHub專案,都沒有類似的實現。於是我決定釋出到 npm 上,幫助一眾還在思考如何把UEditor整合到Vue專案中的小夥伴。幾個月下來,基本已經穩定,所以,今天通過這篇部落格,分享給大家。

先看效果圖:

Vue專案中最簡單的使用整合UEditor方式,含圖片上傳

點選預覽 倉庫地址

Installation

npm i vue-ueditor-wrap
# 或者
yarn add vue-ueditor-wrap
複製程式碼

Quick Start

基於 vue-cli 2.x 的完整 DEMO
基於 Nuxt 的服務端渲染 DEMO

  1. 下載 UEditor

    下載最新編譯的 UEditor。官網目前最新的版本是1.4.3.3,存在諸多 BUG,例如 Issue1,且官方不再積極維護。為了世界的和平,針對一些常見 BUG,我進行了修復,並把編譯好的檔案放在了本倉庫的 assets/downloads 目錄下,你可以放心下載,當然你也可以自己 clone 官方原始碼編譯

    Vue專案中最簡單的使用整合UEditor方式,含圖片上傳

    將下載的壓縮包解壓並重新命名為 UEditor(只需要選擇一個你需要的版本,比如 utf8-php),放入你專案的 static 目錄下。

    Vue專案中最簡單的使用整合UEditor方式,含圖片上傳

    如果你使用的是 vue-cli 3.x,可以把 UEditor 資料夾放入專案的 public 目錄下。

  2. 引入VueUeditorWrap元件

    import VueUeditorWrap from 'vue-ueditor-wrap' // ES6 Module
    // 或者
    const VueUeditorWrap = require('vue-ueditor-wrap') // CommonJS
    複製程式碼

    你也可以通過直接引入 CDN 連結的方式來使用,它會暴露一個全域性的 VueUeditorWrap 變數(具體如何使用你可以閱讀我的這篇部落格或參考這個倉庫)。

    <script src="https://cdn.jsdelivr.net/npm/vue-ueditor-wrap@latest/lib/vue-ueditor-wrap.min.js"></script>
    複製程式碼
  3. 註冊元件

    components: {
      VueUeditorWrap
    }
    // 或者在 main.js 裡將它註冊為全域性元件
    Vue.component('vue-ueditor-wrap', VueUeditorWrap)
    複製程式碼
  4. v-model繫結資料

    <vue-ueditor-wrap v-model="msg"></vue-ueditor-wrap>
    複製程式碼
    data () {
      return {
        msg: '<h2>Vue + UEditor + v-model雙向繫結</h2>'
      }
    }
    複製程式碼

    至此你已經可以在頁面中看到一個初始化之後的 UEditor 了,並且它已經成功和資料繫結了!???

  5. 根據專案需求修改配置,完整配置選項檢視 ueditor.config.js 原始碼或 官方文件

    <vue-ueditor-wrap v-model="msg" :config="myConfig"></vue-ueditor-wrap>
    複製程式碼
    data () {
      return {
        msg: '<h2>Vue + UEditor + v-model雙向繫結</h2>',
        myConfig: {
          // 編輯器不自動被內容撐高
          autoHeightEnabled: false,
          // 初始容器高度
          initialFrameHeight: 240,
          // 初始容器寬度
          initialFrameWidth: '100%',
          // 上傳檔案介面(這個地址是我為了方便各位體驗檔案上傳功能搭建的臨時介面,請勿在生產環境使用!!!)
          serverUrl: 'http://35.201.165.105:8000/controller.php',
          // UEditor 資原始檔的存放路徑,如果你使用的是 vue-cli 生成的專案,通常不需要設定該選項,vue-ueditor-wrap 會自動處理常見的情況,如果需要特殊配置,參考下方的常見問題2
          UEDITOR_HOME_URL: '/static/UEditor/'
        }
      }
    }
    複製程式碼

Advanced

  1. 如何獲取 UEditor 例項?

    <vue-ueditor-wrap @ready="ready"></vue-ueditor-wrap>
    複製程式碼
    methods: {
      ready (editorInstance) {
        console.log(`編輯器例項${editorInstance.key}: `, editorInstance)
      }
    }
    複製程式碼
  2. 設定是否在元件的 beforeDestroy 鉤子裡銷燬 UEditor 例項

    <vue-ueditor-wrap :destroy="true"></vue-ueditor-wrap>
    複製程式碼
  3. 選取 v-model 的實現方式。雙向繫結的實現依賴對編輯器內容變化的監聽,由於監聽方式的不同,會帶來監聽效果的差異性,你可以自行選擇,但建議使用開箱即用的預設值。

    <vue-ueditor-wrap mode="listener"></vue-ueditor-wrap>
    複製程式碼

    可選值:observerlistener

    預設值:observer

    引數說明:

    1. observer 模式藉助 MutationObserver API。優點在於監聽的準確性,缺點在於它會帶來一點額外的效能開銷。你可以通過 observerDebounceTime 屬性設定觸發間隔,還可以通過 observerOptions 屬性有選擇的設定 MutationObserver 的監聽行為。該 API 只相容到 IE11+,但 vue-ueditor-wrap 會在不支援的瀏覽器中自動啟用 listener 模式。

      <vue-ueditor-wrap
        mode="observer"
        :observerDebounceTime="100"
        :observerOptions="{ attributes: true, characterData: true, childList: true, subtree: true }"
        >
      </vue-ueditor-wrap>
      複製程式碼
    2. listener 模式藉助 UEditor 的 contentChange 事件,優點在於依賴官方提供的事件 API,無需額外的效能消耗,相容性更好,但缺點在於監聽的準確性並不高,存在如下方 [常見問題 5] 中的提到的 BUG。

  4. 是否支援 Vue SSR

    2.4.0 版本開始支援服務端渲染!本元件提供對 Nuxt 專案開箱即用的支援。但如果你是自己搭建的 Vue SSR 專案,你可能需要自行區分服務端和客戶端環境並結合 forceInit 屬性強制初始化編輯器,但大概率你用不到該屬性,即使是自己搭建的 SSR 專案,更多問題歡迎提交 ISSUE。

  5. 如何進行二次開發(新增自定義按鈕、彈窗等)?

    本元件提供了 beforeInit 鉤子,它會在 UEditor 的 scripts 載入完畢之後、編輯器初始化之前觸發,你可以在此時機,通過操作 window.UE 物件,來進行諸如新增自定義按鈕、彈窗等的二次開發。beforeInit 的觸發函式以 編輯器 id 和 配置引數 作為入參。下面提供了一個簡單的自定義按鈕和自定義彈窗的示例,DEMO 倉庫中也提供了自定義“表格居中”按鈕的示例,如果有更多二次開發的需求,你可以參考官方 API 或者 UEditor 原始碼 中的示例。

    自定義按鈕 Demo
    <vue-ueditor-wrap v-model="msg" @beforeInit="addCustomButtom"></vue-ueditor-wrap>
    複製程式碼
    addCustomButtom (editorId) {
      window.UE.registerUI('test-button', function (editor, uiName) {
        // 註冊按鈕執行時的 command 命令,使用命令預設就會帶有回退操作
        editor.registerCommand(uiName, {
          execCommand: function () {
            editor.execCommand('inserthtml', `<span>這是一段由自定義按鈕新增的文字</span>`)
          }
        })
    
        // 建立一個 button
        var btn = new window.UE.ui.Button({
          // 按鈕的名字
          name: uiName,
          // 提示
          title: '滑鼠懸停時的提示文字',
          // 需要新增的額外樣式,可指定 icon 圖示,圖示路徑參考常見問題 2
          cssRules: "background-image: url('/test-button.png') !important;background-size: cover;",
          // 點選時執行的命令
          onclick: function () {
            // 這裡可以不用執行命令,做你自己的操作也可
            editor.execCommand(uiName)
          }
        })
    
        // 當點到編輯內容上時,按鈕要做的狀態反射
        editor.addListener('selectionchange', function () {
          var state = editor.queryCommandState(uiName)
          if (state === -1) {
            btn.setDisabled(true)
            btn.setChecked(false)
          } else {
            btn.setDisabled(false)
            btn.setChecked(state)
          }
        })
    
        // 因為你是新增 button,所以需要返回這個 button
        return btn
      }, 0 /* 指定新增到工具欄上的哪個位置,預設時追加到最後 */, editorId /* 指定這個 UI 是哪個編輯器例項上的,預設是頁面上所有的編輯器都會新增這個按鈕 */)
    }
    複製程式碼
    自定義彈窗 Demo
    <vue-ueditor-wrap v-model="msg" @beforeInit="addCustomDialog"></vue-ueditor-wrap>
    複製程式碼
    addCustomDialog (editorId) {
      window.UE.registerUI('test-dialog', function (editor, uiName) {
        // 建立 dialog
        var dialog = new window.UE.ui.Dialog({
          // 指定彈出層中頁面的路徑,這裡只能支援頁面,路徑參考常見問題 2
          iframeUrl: '/customizeDialogPage.html',
          // 需要指定當前的編輯器例項
          editor: editor,
          // 指定 dialog 的名字
          name: uiName,
          // dialog 的標題
          title: '這是一個自定義的 Dialog 浮層',
          // 指定 dialog 的外圍樣式
          cssRules: 'width:600px;height:300px;',
          // 如果給出了 buttons 就代表 dialog 有確定和取消
          buttons: [
            {
              className: 'edui-okbutton',
              label: '確定',
              onclick: function () {
                dialog.close(true)
              }
            },
            {
              className: 'edui-cancelbutton',
              label: '取消',
              onclick: function () {
                dialog.close(false)
              }
            }
          ]
        })
    
        // 參考上面的自定義按鈕
        var btn = new window.UE.ui.Button({
          name: 'dialog-button',
          title: '滑鼠懸停時的提示文字',
          cssRules: `background-image: url('/test-dialog.png') !important;background-size: cover;`,
          onclick: function () {
            // 渲染dialog
            dialog.render()
            dialog.open()
          }
        })
    
        return btn
      }, 0 /* 指定新增到工具欄上的那個位置,預設時追加到最後 */, editorId /* 指定這個UI是哪個編輯器例項上的,預設是頁面上所有的編輯器都會新增這個按鈕 */)
    }
    複製程式碼

    彈出層中的 HTML 頁面 customizeDialogPage.html

    <!DOCTYPE html>
    <html>
    
    <head>
      <meta charset="UTF-8">
      <title>Title</title>
      <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
      <meta name="renderer" content="webkit">
      <!--頁面中一定要引入internal.js為了能直接使用當前開啟dialog的例項變數-->
      <!--internal.js預設是放到 UEditor/dialogs 目錄下的-->
      <script type="text/javascript" src="./UEditor/dialogs/internal.js"></script>
    </head>
    
    <body>
      <h1>hello vue-ueditor-wrap</h1>
      <script>
        //可以直接使用以下全域性變數
        //當前開啟dialog的例項變數
        console.log('editor: ' + editor);
        //一些常用工具
        console.log('domUtils: ' + domUtils);
        console.log('utils: ' + utils);
        console.log('browser: ' + browser);
        dialog.onok = function() {
          editor.execCommand('inserthtml', '<span>我點選了確定</span>');
        };
        dialog.oncancel = function() {
          editor.execCommand('inserthtml', '<span>我點選了取消</span>');
        };
      </script>
    </body>
    
    </html>
    複製程式碼

Features

  1. v-model 雙向資料繫結!你不需要考慮例項化,也不需要考慮何時 getContent,何時setContent,簡單到像使用 input 框一樣!

  2. 完全遵從官方 API,所有的配置引數和例項方法與官方完全一致。通過給 vue-ueditor-wrap 元件的 config 屬性傳遞一個物件,你就可以得到一個完全獨立配置的 UEditor 編輯器。通過監聽 ready 事件你就可以得到初始化後的 UEditor 例項並執行例項上的各種方法。

  3. 自動新增依賴檔案。你不需要自己在 index.htmlmain.js 裡引入 UEditor 的 JS 檔案。更重要的是即使你在一個頁面裡同時使用多個 vue-ueditor-wrap 元件,它所依賴的 JS 檔案也只會載入一次。這麼做的原因在於你不需要當使用者一開啟專案就先載入大量 UEditor 相關的資源,所有的資原始檔只會在 vue-ueditor-wrap 元件第一次被啟用時才載入。當然,如果你在 index.htmlmain.js 裡引入了相關資源,vue-ueditor-wrap 也會準確判斷,你不用擔心它會重複載入。

  4. 每個 vue-ueditor-wrap 元件是完全獨立的。你甚至可以在上面使用 v-for 指令一次渲染 99個 兔斯基(不要忘記新增 key 值)。 Vue專案中最簡單的使用整合UEditor方式,含圖片上傳

FAQ(常見問題)

  1. 是否支援 IE 等低版本瀏覽器?

    Vue 相同,整體支援到 IE9+???

    Vue專案中最簡單的使用整合UEditor方式,含圖片上傳
  2. 為什麼我會看到這個報錯?

    Vue專案中最簡單的使用整合UEditor方式,含圖片上傳

    這是 UEDITOR_HOME_URL 引數配置錯誤導致的。在 vue cli 2.x 生成的專案中使用本元件,預設值是 '/static/UEditor/',在 vue cli 3.x 生成的專案中,預設值是 process.env.BASE_URL + 'UEditor/' 。但這並不能滿足所有情況。例如你的專案不是部署在網站根目錄下,如"http://www.example.com/my-app/",你可能需要設定為"/my-app/static/UEditor/"。是否使用了相對路徑、路由是否使用 history 模式、伺服器配置是否正確等等都有可能會產生影響。總而言之:無論本地開發和部署到伺服器,你所指定的 UEditor 資原始檔是需要真實存在的,vue-ueditor-wrap 也會在 JS 載入失敗時通過 console 輸出它試圖去載入的資原始檔的完整路徑,你可以藉此分析如何填寫。當需要區分環境時,你可以通過判斷 process.env.NODE_ENV 來分別設定。

  3. 我該如何上傳圖片和檔案?為什麼我會看到後臺配置項返回格式出錯

    Vue專案中最簡單的使用整合UEditor方式,含圖片上傳

    上傳圖片、檔案等功能是需要與後臺配合的,而你沒有給 config 屬性傳遞正確的 serverUrl ,我提供了http://35.201.165.105:8000/controller.php 的臨時介面,你可以用於測試,但切忌在生產環境使用!!! 關於如何搭建上傳介面,可以參考官方文件

  4. 單圖片跨域上傳失敗!

    UEditor 的單圖上傳是通過 Form 表單 + iframe 的方式實現的,但由於同源策略的限制,父頁面無法訪問跨域 iframe 的文件內容,所以會出現單圖片跨域上傳失敗的問題。我通過 XHR 重構了單圖上傳的方式,下載最新編譯的 UEditor 資原始檔即可在 IE10+ 的瀏覽器中實現單圖跨域上傳了。具體細節,點此檢視。當然你也可以通過配置 toolbars 引數來隱藏單圖片上傳按鈕,並結合上面介紹的“自定義按鈕”,曲線救國,以下程式碼僅供參考。

    var input = document.createElement('input')
    input.type = "file"
    input.style.display = 'none'
    document.body.appendChild(input)
    input.click()
    input.addEventListener('change',(e)=>{
        // 利用 AJAX 上傳,上傳成功之後銷燬 DOM
        console.log(e.target.files)
    })
    複製程式碼
  5. 為什麼我輸入的"? ! $ #" 這些特殊字元,沒有成功繫結?

    當你使用 listener 模式時,由於 v-model 的實現是基於對 UEditor 例項上 contentChange 事件的監聽,而你輸入這些特殊字元時通常是按住 shift 鍵的,UEditor 本身的 contentChangeshift 鍵按住時不會觸發,你也可以嘗試同時按下多個鍵,你會發現 contentChange 只觸發一次。你可以使用 observer 模式或移步 UEditor

  6. 單圖片上傳後 v-model 繫結的是 loading 小圖示。

    這個也是 UEditorBUG。我最新編輯的版本,修復了官方的這個 BUG,如果你使用的是官網下載的資原始檔,請替換資原始檔或參考 Issue1

更多問題,歡迎提交 ISSUE 或者去 聊天室 提問。但由於這是一個個人維護的專案,我平時也有自己的工作,所以並不能保證及時解決你們的所有問題,如果小夥伴們有好的建議或更炫酷的操作,也歡迎 PR,如果你覺得這個元件給你的開發帶來了實實在在的方便,也非常感謝你的Star,當然還有咖啡:

程式碼修改請遵循指定的 ESLint 規則,PR 之前請先執行 npm run lint 進行程式碼風格檢測,大部分語法細節可以通過 npm run fix 修正,構建之後,記得修改 package.json 裡的版本號,方便我 Review 通過後麻溜溜的釋出到 npm

Vue專案中最簡單的使用整合UEditor方式,含圖片上傳Vue專案中最簡單的使用整合UEditor方式,含圖片上傳

總結

雖然這是一次很小的創新,UEditor也可能是一個過氣的富文字編輯器。但是在維護這個專案以及幫助一眾小夥伴解決ISSUE的過程中,我成長了很多。最令我感動的是不少小夥伴還給我郵箱發了感謝信,而且我還發現確實已經有一些人開始在專案中用了。這種被他人認可,以及幫助別人的快樂真的只有體會過的人才知道。也就在前不久,我決定開始在掘金寫部落格,雖然一些東西寫的不那麼好,或者自己認知有錯誤,但總有一群熱心且優秀的小夥伴,會在評論區指正以及給出寶貴的意見。分享是快樂的!所以,我的這篇文章也權當拋磚引玉,如果小夥伴們有好的建議或更炫酷的操作,也歡迎PR,不過PR之前請先執行npm run lint進行程式碼風格檢測,大部分語法細節也可以通過npm run fix修正,也要記得修改package.json的版本號version,方便我直接釋出到npm。當然如果你有好用的富文字編輯器,也可以在評論區推薦。

相關文章