JavaScriptのMementoデザインパターンのパワー



プログラミングのmementoパターンは、オブジェクトの状態を復元する方法が必要な場合に便利です.
JavaScript開発者として、我々は現代的なWebアプリケーションでは、現在、多くの状況でこの概念で動作します.
あなたがしばらくの間ウェブで開発していたならば、あなたは期間のことを聞いたかもしれませんhydration .
あなたが水和が何であるかについて、わからないならば、クライアント側がJSON、JavaScript、HTMLなどのどんなプログラミング言語にでも格納されている静的コンテンツをとって、ブラウザーが実行中に動くことができるコードにそれを変えるウェブ開発のテクニックです.その段階でJavaScriptが実行され、DOMがページ上で実行し始めるときにイベントリスナーをアタッチするようなことができます.
mementoパターンは似ています.このポストでは、実行時にmementoパターンを実装し、静的に何も格納しません.
あなたが働いたならばJSON.parse and JSON.stringify チャンスは偶然あなたが以前にmementoを実装したかもしれません.
通常、mementoパターンの完全な流れを実装する3つのオブジェクトがあります.
  • 創始者
  • メメント
  • 管理人
  • 創始者は、mementoとしてそれ自体の作成および記憶をトリガーするインターフェースを定義する.
    mementoは、受け手から渡され、取得される発信元の内部状態表現です.
    管理人は1つの仕事を持っています:後で使われる記念品を保存するか、保存するために.それは格納されているmementoを取得することができますが、それは何も変異しません.

    mementoデザインパターンの実装


    パターンを説明しましたので、コードの練習をマスターします.
    DOM要素としてインタラクティブな電子メール入力フィールドを作成します.我々は、我々のユーザーがすぐに彼らが@ 送信する前のシンボル.
    入力フィールドがエラー状態になったときには、次のようになります.

    これはHTMLマークアップです.
    <!DOCTYPE html>
    <html>
      <head>
        <title>Memento</title>
        <meta charset="UTF-8" />
      </head>
      <body style="margin:50px;text-align:center;background:linear-gradient(
        76.3deg,
        rgba(44, 62, 78, 1) 12.6%,
        rgba(69, 103, 131, 1) 82.8%
      );height:250px;overflow:hidden;">
        <input type="email" id="emailInput" style="padding:12px;border-radius:4px;font-size:16px;" placeholder="Enter your email"></input>
        <script src="src/index.js"></script>
      </body>
    </html>
    
    このインターフェイスを使用して起動します.

    ここで最初にするのは、エラー状態に定数として2つの定数を定義し、エラーのスタイルに値として割り当てるためにコードを通して使用します.これは、複数回再利用されるので、コードを書くときには任意のタイプミスをしないようにするためです.
    const ERROR_COLOR = 'tomato'
    const ERROR_BORDER_COLOR = 'red'
    const ERROR_SHADOW = `0px 0px 25px rgba(230, 0, 0, 0.35)`
    const CIRCLE_BORDER = '50%'
    const ROUNDED_BORDER = '4px'
    
    これはパターンとは何の関係もありませんが、このポストから余分なヒントを得ることができるように、いくつかのベストプラクティスでランダムにスリップするのは良い習慣だと思います.
    ここで、エラー状態と正常状態とを切り替えるヘルパー関数を作成します.
    const toggleElementStatus = (el, status) => {
      if (status === 'error') {
        return Object.assign(el.style, {
          borderColor: ERROR_BORDER_COLOR,
          color: ERROR_COLOR,
          boxShadow: ERROR_SHADOW,
          outline: 'red',
        })
      }
      return Object.assign(el.style, {
        borderColor: 'black',
        color: 'black',
        boxShadow: '',
        outline: '',
      })
    }
    
    二つのスタイルプリセットをトグルしている間、ボーダー半径をトグルするためにヘルパーを滑らせるだけでよい.これは私たちのコードは“自然”を感じるように我々は、単にこのポストの色と記念品の関係に直接焦点を当てていないので、本物のアプリを感じることです.時には、ランダムコードと実際のコードとの関係を見てみると、もっと良くなると思います.
    const toggleBorderRadius = (el, preset) => {
      el.style.borderRadius =
        preset === 'rounded'
          ? ROUNDED_BORDER
          : preset === 'circle'
          ? CIRCLE_BORDER
          : '0px'
    }
    
    次は、創始者を書くことです.
    覚えておいて、創始者は自分自身の作成と記憶をトリガーとするインターフェイスを定義します.
    function createOriginator({ serialize, deserialize }) {
      return {
        serialize,
        deserialize,
      }
    }
    
    実際に、我々は単に我々のための創始者を生成するだけの工場を作成しました.
    これが本当の創始者です.
    const originator = createOriginator({
      serialize(...nodes) {
        const state = []
    
        nodes.forEach(
          /**
           * @param { HTMLInputElement } node
           */
          (node) => {
            const item = {
              id: node.id || '',
            }
    
            item.tagName = node.tagName.toLowerCase()
    
            if (item.tagName === 'input') {
              item.isError =
                node.style.borderColor === ERROR_BORDER_COLOR &&
                node.style.color === ERROR_COLOR
              item.value = node.value
            }
    
            item.isRounded = node.style.borderRadius === ROUNDED_BORDER
            item.isCircle = node.style.borderRadius === CIRCLE_BORDER
    
            state.push(item)
          },
        )
    
        return state
      },
      deserialize(...state) {
        const providedNode = state[state.length - 1]
    
        if (providedNode) state.pop()
    
        const nodes = []
    
        state.forEach((item) => {
          const node = providedNode || document.createElement(item.tagName)
    
          if (item.tagName === 'input') {
            if (item.isError) {
              toggleElementStatus(node, 'error')
            }
            if (item.isRounded) {
              toggleBorderRadius(node, 'rounded')
            } else if (item.isCircle) {
              toggleBorderRadius(node, 'circle')
            }
            node.value = item.value || ''
            if (item.placeholder) node.placeholder = item.placeholder
            if (item.id) node.id = item.id
          }
    
          nodes.push(node)
        })
    
        return nodes
      },
    })
    
    創始者でserialize メソッドはDOMノードを受け取り、DOMノードの状態表現を返し、ローカルストレージ内に文字列として格納することができます.ローカルストレージは文字列を受け入れるので、これが必要です.
    たった今、我々はJavaScriptでこのパターンのピークです.シリアル化はこのパターンが私たちにとって重要である唯一の理由です.さもなければ、私たちは直接DOMノードをローカルストレージに保管し、それを一日呼び出すことができます.
    インサイドserialize 方法は、暗黙のうちに我々が表現を決定するのを助ける規則のカップルを定義しました.
    以下に行を示します.
    if (item.tagName === 'input') {
      item.isError =
        node.style.borderColor === ERROR_BORDER_COLOR &&
        node.style.color === ERROR_COLOR
      item.value = node.value
    }
    
    item.isRounded = node.style.borderRadius === ROUNDED_BORDER
    item.isCircle = node.style.borderRadius === CIRCLE_BORDER
    
    入力要素のmementosを保存するときには、この方法を実装するかどうか選択します.
    if (item.tagName === 'input') {
      item.style.borderColor = node.style.borderColor
      item.style.color = node.style.color
      item.value = node.value
    }
    
    item.style.borderRadius = node.style.borderRadius
    
    私のアドバイスをしてください:あなたのデザインパターンの実装では、特にあなたのコードから有用な意味を作成することが良い練習です.コードの意味を開始すると、コードの他の領域で有用なレベルの抽象化を考えることができます.
    使用item.isError エラースタイルのプリセットを表すために、我々のプロジェクトが任意のスタイルを直接割り当てるのとは対照的に時間をかけてより複雑になるので、再利用できる興味深い再利用可能なmementosを作るためにより広い機会を開きます.
    たとえば、重要なフィールドが空白のままにされたときにフォームが送信されないようにするのが一般的です.フォームは、それが送信から自分自身を停止する必要があるいくつかの状態に移行する必要があります.
    私たちがフォームのmementoを保存することになっているならば、我々はこの状態を回復するとき、ユーザーが「無効にされた」状態に戻されることを確実とする必要があります:
    const originator = createOriginator({
      serialize(...nodes) {
        const state = []
    
        nodes.forEach(
          /**
           * @param { HTMLInputElement } node
           */
          (node) => {
            const item = {
              id: node.id || '',
            }
    
            item.tagName = node.tagName.toLowerCase()
    
            if (item.tagName === 'input') {
              item.isError =
                node.style.borderColor === ERROR_BORDER_COLOR &&
                node.style.color === ERROR_COLOR
              item.value = node.value
            }
    
            item.isRounded = node.style.borderRadius === ROUNDED_BORDER
            item.isCircle = node.style.borderRadius === CIRCLE_BORDER
    
            if (node.textContent) item.textContent = node.textContent
    
            state.push(item)
          },
        )
    
        return state
      },
      deserialize(state) {
        const nodes = []
    
        if (!Array.isArray(state)) state = [state]
    
        state.forEach((item) => {
          const node = document.createElement(item.tagName)
    
          if (item.style) {
            Object.entries(item.style).forEach(([key, value]) => {
              node.style[key] = value
            })
          }
    
          if (item.isRounded) {
            toggleBorderRadius(node, 'rounded')
          } else if (item.isCircle) {
            toggleBorderRadius(node, 'circle')
          }
    
          if (item.spacing) {
            node.style.padding = item.spacing
          }
    
          if (item.id) node.id = item.id
    
          if (item.tagName === 'input') {
            if (item.isError) {
              toggleElementStatus(node, 'error')
            }
            node.value = item.value || ''
            if (item.placeholder) node.placeholder = item.placeholder
          } else if (item.tagName === 'label') {
            if (item.isError) {
              node.style.color = ERROR_COLOR
            }
          } else if (item.tagName === 'select') {
            if (item.options) {
              item.options.forEach((obj) => {
                node.appendChild(...originator.deserialize(obj, node))
              })
            }
          }
    
          if (item.textContent) node.textContent = item.textContent
    
          nodes.push(node)
        })
    
        return nodes
      },
    })
    
    const caretaker = createCaretaker()
    
    function restore(state, container, { onRendered } = {}) {
      let statusSubscribers = []
      let status = ''
    
      const setStatus = (value, options) => {
        status = value
        statusSubscribers.forEach((fn) => fn(status, options))
      }
    
      const renderMemento = (memento, container) => {
        return originator.deserialize(memento).map((el) => {
          container.appendChild(el)
    
          if (memento.isError && status !== 'error') {
            setStatus('error')
          }
    
          if (memento.children) {
            memento.children.forEach((mem) => {
              renderMemento(mem, el).forEach((childEl) => el.appendChild(childEl))
            })
          }
    
          return el
        })
      }
    
      const render = (props, container) => {
        const withStatusObserver = (fn) => {
          statusSubscribers.push((updatedStatus) => {
            if (updatedStatus === 'error') {
              // Do something
            }
          })
    
          return (...args) => {
            const elements = fn(...args)
            return elements
          }
        }
    
        const renderWithObserver = withStatusObserver(renderMemento)
    
        const elements = renderWithObserver(props, container)
        statusSubscribers.length = 0
        return elements
      }
    
      const elements = render(state, container)
    
      if (onRendered) onRendered(status, elements)
    
      return {
        status,
        elements,
      }
    }
    
    const container = document.getElementById('root')
    
    const { status, elements: renderedElements } = restore(mementoJson, container, {
      onRendered: (status, elements) => {
        if (status === 'error') {
          const submitBtn = container.querySelector('#submit-btn')
          submitBtn.disabled = true
          submitBtn.textContent = 'You have errors'
          toggleElementStatus(submitBtn, 'error')
        }
      },
    })
    
    要素を直接返す代わりに、返されるものがmementoをレンダリングする現在の状態であることを確認します.
    より高いレベルの視点でこれを見て、我々はその事実を利用しますisError フォームのようなものを表現し、概観することができます.少し必要なフィールドが欠落しているか、値が正しく入力されていない場合は、フォームを送信しないでください.
    その場合、フォームをユーザーに表示する前に送信ボタンを無効にすることで、フォームを対話的ではならないことを確認します.

    あなたが気づいていないならばrestore オリジナルラップdeserialize 創始者からの方法
    私たちが今持っているのは、深い子供とレンダリング状態をサポートする、より高いレベルの抽象化されたmementoですisError ) 我々の全部のmementoの.

    結論


    そして、それはこのポストの終わりを終わります!私はあなたがこれが価値があることを発見し、将来的にもっと見てほしい!
    私を見つけるmedium