玩一下低代碼框架amis,並動手封裝一個表情輸入框組件

一:amis是什麼東西,怎麼使用

1.1 關於概念的介紹

amis: 前端低代碼框架,經過 JSON 配置就能生成各類後臺頁面,極大減小開發成本,甚至能夠不須要了解前端。 css

這句是來自它git上的一句描述,字面意思說:」不須要了解前端僅經過一些json配置就能搞出各類mis後臺頁面「,嗯...看起來挺吊的。可是它真的有這麼吊嘛,咱們一塊兒去看看。html

正式介紹amis以前,可能有小夥伴不明白什麼是低代碼平臺。這裏小白藉助知乎的一篇回答簡單介紹一下 前端

能夠把低代碼框架理解成普通框架的進化版本,咱們都知道向react、vue這些框架的出現主要目的是爲了簡便咱們的開發。可是使用他們仍是須要必定的前端知識的,那麼框架進化到最後可不能夠達到這樣的效果。咱們無需任何的知識儲備,(像一些快速搭建網站的可視化的東西)框架自動就幫助咱們生成一個頁面vue

就像這種:mysql

嗯嗯...,未來框架要是都往這個方向發展是否是有一批前端工程師就要被優化了呢。。。react

之後的事情我不知道,可是起碼如今這種低代碼框架火候還達不到。它們定製性仍是蠻差的,可是對於一些mis項目使用它真的是超級簡便開發git

做爲一個專業的前端開發,我們是確定不會使用什麼可視化的東西來搞的。咱們下面主要來看一下amis是怎樣使用json配置來寫頁面sql

1.2 怎樣來使用amis

amis的使用有兩種方式,1是經過sdk 2是經過react 。這裏咱們是以react的形式爲例。數據庫

這裏我就直接拉一下它的一個demo,直接看一下app.tsxjson

爲了能夠看的清晰一點我先把不那麼重要東西都摺疊起來

看來這個renderAmis是負責把傳到它肚子裏的東西轉成一個jsx的,即(schema, props, env) => JSX.Element;

它須要三個參數

  • schema
  • props
  • env

這裏最重要就是schema,這個schema也即咱們要寫的json配置。仍是先簡單介紹一下另外兩個是幹啥使得

props:相似於一個全局變量的東西,props裏面的數據會傳給內部的全部組件

env :就是一些傳給組件的工具函數,比較請求了、系統消息了...

接下來看一下怎麼使用amis寫組件了,好比寫一個表單吧

render() {
    return renderAmis(

      {
        "type": "page",
        "body": {
          "type": "form",
          "api": "https://houtai.baidu.com/api/mock2/form/saveForm",
          "controls": [
            {
              "type": "text",
              "name": "name",
              "label": "姓名:"
            },
            {
              "name": "email",
              "type": "email",
              "label": "郵箱:"
            }
          ]
        }

      },
      {
        // props...
      },
      env
    );
  }
複製代碼

簡單介紹一下屬性的含義

  • type:指定amis的組件類型
  • body:能夠理解爲容器屬性
"type": "form",       指定爲表單組件
          "api": "https://houtai.baidu.com/api/mock2/form/saveForm",   表單組件提交的地址
          "controls": [		用來裝表單項的
            {
              "type": "text",   指定爲input的text組件
              "name": "name",		至關於這個input的name值,下同
              "label": "姓名:"
            },
            {
              "name": "email",
              "type": "email",
              "label": "郵箱:"
            }
          ]
複製代碼

再往下關於amis的具體使用就不介紹了仍是比較簡單的這裏咱們主要是本身寫一個amis組件,詳細看它的文檔就能夠了

二:先簡單瞭解amis的基本原理

咱們的主要目的,是封裝一個amis組件。在封裝以前咱們確定要先了解它的基本原理是什麼。怎麼咱們寫一個json配置最後就出來一個對應的頁面呢?

它的工做原理仍是蠻簡單的,普通的ui框架會怎麼寫呢?寫完模板組件就直接導出了。這裏僅是又增長了一個步驟。

amis的核心工做就是你給一個schema它幫你映射到對應的模板組件而後由react執行渲染

因此關鍵是它如何產生的這個映射關係

好比上面的例子,爲何咱們只寫了type:form amis就幫我映射到了它內部已經寫好的form模板組件呢

要達到這個目的確定是須要兩個步驟的

  1. 寫模板組件的時候要有一個"惟一標識",即標上個記號方便咱們可以快遞定位到它
  2. 對於schema的解析,好比解析到schema的type值爲"form",就能夠立刻定位到模板組件

核心代碼在這裏,感興趣的能夠自行查閱(這篇文章先不詳細介紹)

既然最主要的就是這兩個,且開始的時候咱們就發現了。在app.tsx中的renderAmis函數就是乾的解析json的活

故咱們要本身寫一個自定義的amis組件就差一步了,那就是寫模板組件的時候搞一個」標識「

不過值得高興的是,amis也爲咱們提供了一個這樣的工具函數Renderer。它是一個HOC

好比這樣操做

@Renderer({
  test: /(^|\/)my-amis$/
})
export class FormRenderer extends React.Component { 
  render() {
    return (
      <div>你好啊</div>
    );
  }
}
複製代碼

那麼如今一個簡單的映射關係就搞好了

這時你在schema中指定type爲my-amis,上面這個react組件就被找到並渲染出來。schema中的其餘值就做爲這個組件的props傳進去了

其實就是作了一個字典,簡單帖一下它的實現(先去掉不重要的)核心代碼都在factory.tsx感興趣直接翻翻就ok了

三:表情輸入框要作的事情及要注意的坑

好了開始正題!

先來看一下咱們接下要實現的最終效果(不管是由於前端展現仍是數據庫存儲咱們其實都很差直接使用emoji,因此這裏我仍是選擇的表情圖片)

這就是一個簡單的表情輸入框的東西,之因此選擇這個爲例子編寫,是由於我發現這個小東西看起來簡單可是仍是存在很多坑的

首先咱們先來肯定一下怎樣劃分組件

  • 編輯區
  • 表情區

若是以前沒有作過這類的需求,你會怎麼作呢。編輯區直接搞一個textarea就完事了?

我以前也是這樣想的,先提早透露一下這個輸入框是這個需求中坑最多的

3.1.1 字典映射及基礎組件編寫

字典映射

下面的這個是csdn的私信它作的比較簡單直接用的一個textarea

再來看掘金的,(從技術上看就知道誰更專業了吧,而且直接把emoji格式的傳到後臺了想必它也升級了數據庫。由於雖然emoji也是Unicode編碼,可是它以utf8編碼時好像是4個字節來着原來的mysql的utf8是3個字節來着。偷個懶不去查了)

爲了避免動數據庫,這裏咱們在把信息發出去的時候再轉會[xx]這種形式

那麼首先要作的事情就是要搞一個映射關係了,即進如輸入框中應該是有[微笑]=>🙂,發送時在由🙂轉爲[微笑](這裏的小表情用的是emoji格式的操做中咱們放進去的是圖片)。這比較簡單搞一個字典就能夠了

像是這樣:(對應的值就是每個小表情圖片的地址)

const emojiDictionary = {
    "[微笑]": imgMap.img01,
    "[撇嘴]": imgMap.img02,
    "[色]": imgMap.img03,
    "[發呆]": imgMap.img04,
    "[得意]": imgMap.img05,
    "[流淚]": imgMap.img06,
    "[害羞]": imgMap.img07,
    "[閉嘴]": imgMap.img08,
    "[睡]": imgMap.img09,
    "[大哭]": imgMap.img10,
   ...
}

複製代碼

後面再是怎麼個邏輯呢?

  1. 選中表情彈框點擊拿到選中的小表情信息
  2. 建立一個img的dom結構塞進輸入框裏
  3. 發送時在將對應的img的dom替換成相應的[xx]字符串

組件編寫

咱們接下來先把基本的組件寫好吧

輸入框組件

import * as React from 'react';
import { useState } from "react";
import { Renderer } from 'amis';
import classnames from 'classnames'

import Emoji from './emojinew'
import './blog.css'

const emojiInput = (props) => {

    const [show, setShow] = useState(true)

    const showEmoji = () => {
        setShow(!show)
    }
    return (
        <div className="emoji-input"> <div className="input-textarea"> 請輸入... </div> <div className="input-col"> <i className="iconfont icon-biaoqing" onClick={showEmoji}></i> <i className="iconfont icon-icon-"></i> </div> <div className={classnames('input-emoji', { show: show })}> <Emoji /> </div> </div>
    );
}


const emojiInputRender = Renderer({
    test: /(^|\/)emoji-input$/,
    name: "emoji-input"
})(emojiInput)
export default emojiInputRender;
複製代碼

表情框組件

import * as React from 'react';
import { useEffect } from 'react'
import { Scrollbars } from 'react-custom-scrollbars';


import emojiDictionary from '../lib/emojiDictionaries'
const newEmojiDictionary = Object.entries(emojiDictionary)

const EmojiItem = (props) => {
    const { msg, pic } = props
    return (
        <span className="emoji-item-img"> <img src={pic} data-msg={msg} /> </span>
    )
}


const EmojiNew = (props) => {
    const { } = props
    useEffect(() => {
        console.log();
    })
    return (
        <Scrollbars style={{ height: '100px' }} autoHide > <div className="emoji-new"> { newEmojiDictionary.map( item => { return <EmojiItem key={item[0]} msg={item[0]} pic={item[1]} /> } ) } </div> </Scrollbars > ); } export default EmojiNew; 複製代碼

3.1.2 添加事件,解析替換

下面咱們先簡單點,每次點擊表情的時候就將這個小表情放到最後。

給表情組件傳一個處理事件下去,input組件中添加

const clickEmoji = (pic, msg) => {
        const img = document.createElement('img')
        img.src = pic
        img.setAttribute("data-msg", msg)
        inputRef.current.appendChild(img)

    }
複製代碼

透傳到EmojiItem,做爲它的點擊事件的處理函數並拿去pic與msg

const EmojiItem = (props) => {
    const { msg, pic, clickEmoji } = props
    return (
        <span className="emoji-item-img" onClick={() => { clickEmoji(pic, msg) }}> <img src={pic} data-msg={msg} /> </span>
    )
}
複製代碼

而後怎麼在div中編輯文字呢?很簡單給這個div加一個屬性<div className="input-textarea" ref={inputRef} contentEditable={true}>

可是react中設置它在控制檯會有很噁心的warning故還須要再設置一個屬性去幹掉它。

到這最終形式

<div className="input-textarea" ref={inputRef} contentEditable={true} suppressContentEditableWarning={true} >
                請輸入...
 </div>
複製代碼

好了如今基本的效果已經出來了

再來處理髮送消息時把對應的img替換成對應的描述[xx]

提醒一點:dom的innerHTML取得的東西徹底是能夠看成字符串來處理的。那就好辦了接下來咱們就直接搞個正則字符串替換一下

給小飛機圖標一個發送的點擊事件

const sendData = () => {
        let str = inputRef.current.innerHTML
        let imgReg = /<img.*?(?:>|\/>)/gi
        let nameReg = /<img[^>]+data-msg[=\'\"\s]+([^\'\"]*)[\'\"]?[\s\S]*/i
        let arr = str.match(imgReg)

        if (arr) {
            for (let i = 0; i < arr.length; i++) {
                let names = arr[i].match(nameReg)
                if (names && names[1]) {
                    str = str.replace(arr[i], names[1])
                }
            }
        }
        console.log(str)
    }
複製代碼

好了,搞到這看起來就基本上是大功告成了吧

3.1.3 處理換行與空格

別高興得太早,當你在輸入框中換個行、空個格的時候

好傢伙,原來在可編輯的div中它的換行是經過搞個div實現的

不過雖然有些煩人,可是處理也是蠻簡單。仍是用正則唄,幹掉div換成\n(有時也會出現br的狀況,因此br的狀況也要處理一下,也要考慮只換行沒寫內容的狀況)

str = str.replace(/<div><\/div>/g, "")
   str = str.replace(/<div>/g, "\n")
   str = str.replace(/<br>/g, "\n")
   str = str.replace(/<\/div>/g, "")
複製代碼

再來處理一個空格的狀況

str = str.replace(/&nbsp;/g, " ")
複製代碼

還得考慮一種狀況,即把原來的第一行刪掉了,那麼下面的就成了這個<div>我是小白&nbsp; 啊</div><div>你好認識我嘛</div><div>很久不見了</div>。咱們前面的處理僅是把<div>換成了\n,處理完了豈不是在開頭就給人家搞了一個換行,這不太合適吧

作一下判斷吧,若是\n在首部位置就將它幹掉好了

if (str.indexOf('\n') === 0) {
            str = str.replace(/\n/, "")
   }
複製代碼

好了,這一階段咱們的活基本已經處理完畢了

3.1.4 處理copy進去文字的樣式問題

這裏來試試效果吧,咱們去隨便找個網址粘貼一段文字放進去

臥槽,這下真的是我直接就好傢伙了,咱們預想的僅是把一個純文本帖過來,沒想到把人家樣式都整來了

這要咋辦,仍是正則去拿數據?這看着就腦殼疼,怎麼寫這個正則呢?

我以前搞這個玩意時,主要的思路也是放在我應該怎麼寫這個正則上面了。可是我在尋找相關資料的時候卻發現本身好像一個傻蛋...

還記得咱們是怎麼讓一個div能夠編輯文字的嘛,咱們是給他加了一個contentEditable屬性。當時咱們給它的值是一個true。可是這個contentEditable還有其餘的屬性值。plaintext-only來看一下這個屬性值字面意思好像就咱們須要的啊。來試一試吧

額...牛皮。(樣式問題是由於我給了編輯盒子一個固定的高度。改爲由內容撐高就行了)

3.1.5 處理焦點問題

走到這,基本的問題咱們都處理完了,可是別忘了。開始的時候咱們只是讓小表情圖片默認加到了編輯盒子的最後,可是誰家的東西是這樣弱智的...它應該是跟在光標的後面的呀

再來處理一下這個最後的問題吧

解決思路也不復雜 。像上圖中,咱們想在失敗後面加一個表情。首先是先把光標定到那裏,而後點擊表情框選中相應表情

咱們要怎麼作呢,要知道當咱們在表情框上選表情而且點擊時原本在輸入框中的焦點便沒有了,因此咱們要記錄一下這個最後位置

給編輯框一個失焦事件來記錄最後光標位置(對getSelection不熟悉的話看一下mdn就能夠了)

再搞一個變量存一下

//失去焦點要作的事情
const record = () => {
        let selection = getSelection()
        let range = selection.getRangeAt(0)
        lastRange = range
    }
複製代碼

再從新改造一下點擊小表情的處理函數

const clickEmoji = (pic, msg) => {
        const img = document.createElement('img')
        img.src = pic
        img.setAttribute("data-msg", msg)
        let selection = window.getSelection()
        // 看一下有沒有最後的光標對象
        if (lastRange) {
            selection.removeAllRanges()
            selection.addRange(lastRange)
        }
        let range = selection.getRangeAt(0)
        range.insertNode(img)
        range.collapse(false)
        lastRange = range

    }
複製代碼

大功告成!

四:寫道最後

這幾個月來一直很忙,最近靜了心來想了想。我也不知道我究竟忙了些什麼。真的是有些浮躁。接下來本身不能再悶着頭玩了,每個階段都要有每個階段的目標,人終歸是有惰性的,仍是得適量得逼本身一把

2021打工人一塊兒加油

藉助一句歌詞

用力撕掉那些亂標籤早就聽膩了風涼話

說過的都在慢慢實現沒想過作個空想家

老是太理想化 創做難道不須要靈感嗎

用心的 走好每一個階段不須要他們任何評價

本身帶隊伍 不作個廢物 從開始就沒有想過留退路

不會妥協 不論是誰 編造的套路別想將我束縛

相關文章
相關標籤/搜索