HTML 템플릿: 기능적 방법

머리말



이 기사에서는 Javascript를 사용하여 DOM 노드를 생성하기 위해 직접 만든 작지만 강력한 도우미에 대한 개발 프로세스를 재구성합니다. 😁

처음에 이것을 구축한 이유는 HTML을 작성하는 것을 좋아하지 않기 때문입니다. 그리고 대부분의 템플릿 엔진은 본질적으로 단순히 문자열 보간을 미화하기 때문입니다. 😖

이것은 또한 실제로 새로운 코드가 아닙니다. 대부분의 경우 정확히 동일한 메커니즘을 구현하는 Lua library의 재작성입니다. 😂

라는 말과 함께...

시작하자!



시작점으로 HTML 요소를 생성하는 간단한 함수를 정의해 보겠습니다.

export const node = (name) => {
   const element = document.createElement(name)
   return element
}


좋아요, 간단합니다. 그러나 이것은 document.createElement() 의 별칭에 불과합니다. 우리는 더 많은 인수를 전달하기를 원할 것입니다. 함수는 (대부분) 완성된 DOM 노드를 반환해야 합니다.

추가 인수 설정



중첩 구조를 허용하기 위해 재귀 함수를 사용하여 인수를 처리해 보겠습니다. 먼저 node 함수에 추가합니다.

export const node = (name, args) => {
   const element = document.createElement(name)
   parseArgs(element, args)
   return element
}


Note: some readers might already realize that we could collect many args into an array with ...args; don't worry, I'll get to that in another step 😉



속성



그리고 이제 우리는 그것을 구현합니다. 간단한 경우부터 시작하겠습니다. 일부 속성이 있는 빈 요소: node("div", [{class:"box"}])<div class="box"> 와 같은 요소를 반환해야 합니다.

+ const parseArgs = (element, args) => {
+    for (arg of args)
+       for (key in arg)
+          element.setAttribute(key, arg[key])
+ }


텍스트 내용



좋은! 진행 상황입니다! 그러나 HTML 요소가 비어 있을 수만 있다면 그 의미가 무엇입니까? node("p", ["Hello, World!"]) 를 호출하면 "Hello, World!"라는 텍스트가 포함된 <p> 요소를 반환해야 합니다. 해당 논리도 추가해 보겠습니다.

  const parseArgs = (element, args) => {
     for (arg of args)
+       if (typeof(arg) == "string")
+          element.appendChild(document.createTextNode(arg))
+       else
            for (key in arg)
               element.setAttribute(key, arg[key])
  }



그리고 그것으로 대부분의 힘든 작업은 이미 완료되었습니다. 구조적으로 우리는 끝났습니다. 남은 것은 케이스를 더 추가하는 것뿐입니다.

중첩 배열



먼저 재귀를 해보자. 입력을 전달하기 전에 평면화할 필요가 없는 경우 유용하므로 배열 인수에 대한 재귀 사례를 추가해 보겠습니다. node("p", ["foo", ["bar", "baz"]])<p>foobarbaz</p>로 바꾸자.

  const parseArgs = (element, args) => {
     for (arg of args)
        if (typeof(arg) == "string")
           element.appendChild(document.createTextNode(arg))
+       else if ("length" in arg)
+          parseArgs(element, arg)
        else
           for (key in arg)
              element.setAttribute(key, arg[key])
  }



자식 요소



마지막으로 가장 좋은 부분은 다른 HTML 요소를 자식으로 추가하는 것입니다. 이것은 문자열의 경우와 매우 유사합니다.

  const parseArgs = (element, args) => {
     for (arg of args)
        if (typeof(arg) == "string")
           element.appendChild(document.createTextNode(arg))
+       else if ("nodeName" in arg)
+          element.appendChild(arg)
        else if ("length" in arg)
           parseArgs(element, arg)
        else
           for (key in arg)
              element.setAttribute(key, arg[key])
  }



그것을 밖으로 시도



이것으로 기본 HTML 렌더링 메커니즘이 완료되었습니다. 이제 다음과 같은 코드를 작성할 수 있습니다.

let navlink = (name) =>
   node("a", [{href: "/"+name}, name])

let menu = (items) => node("nav", [
   node("ul", [
      items
      .map(navlink)
      .map(link => node("li", [link])
   ])
])

body.appendChild(menu(["home", "about", "contact"]))


작동하며 이미 해당 설정으로 많은 코드 재사용을 수행할 수 있습니다. 그러나 사용하기가 매우 번거롭게 느껴집니다.
  • 노드 유형을 문자열로 전달 💢
  • 배열에서 인수 래핑하기 😩
  • 래퍼 기능을 사용해야 하는 경우 map 🤔

  • 편리함의 또 다른 층



    이러한 문제를 해결하기 위해 일부 메타 프로그래밍을 수행하고 이에 대한 멋진 래퍼를 구축해 보겠습니다.

    export const html = new Proxy({}, {
       get: (_, prop) => (...args) => node(prop, args)
    })
    


    그리고 그게 다야! html 와 같은 문자열로 새로운 html.div Proxy 객체를 인덱싱할 때 ; 노드 유형을 추가하고 모든 인수를 배열로 수집하는 node 함수에 대한 래퍼를 자동으로 반환합니다.

    이것은 map 문제만 남습니다. 이 배열 메서드는 배열 항목을 콜백 함수의 첫 번째 인수로 전달하지만 인덱스와 배열도 추가합니다. 즉, 해당 값이 node 함수에 전달되어 문제가 발생합니다.

    이 문제를 해결하기 위해 프록시를 다음과 같이 변경할 수 있습니다.

    export const html = new Proxy(Window, {
       get: (target, prop, receiver) => {
          if (prop.search(/^[A-Z]/)+1)
             return (arg) => node(prop, [arg])
          else
             return (...args) => node(prop, args)
       }
    })
    


    이제 대문자로 시작하는 문자열로 html 프록시를 인덱싱하면 첫 번째 인수만 전달하는 다른 클로저를 반환합니다.

    변경 시도



    이제 위의 예를 다음과 같이 다시 작성할 수 있습니다.

    let navlink (name) => html.a({href: "/"+name}, name)
    
    let menu = items => html.nav(
       html.ul(
          items
          .map(navlink)
          .map(html.Li)
       )
    )
    
    body.appendChild(menu(["home", "about", "contact"]))
    



    그리고 그게 다였어 😁

    질문이나 피드백이 있으면 댓글을 남겨주세요. 코드에 명확히 해야 할 부분이 있으면 알려주세요. 그러면 게시물을 연장하거나 새 게시물을 작성하겠습니다. 💖

    좋은 웹페이지 즐겨찾기