일반 웹 구성 요소를 어떻게 만들었는지

나는 최근에 shiki-element을 발표했는데 이것은 간단한 웹 구성 요소로 shiki 라이브러리를 통해 텍스트 응용 문법에 대한 하이라이트 디스플레이에 사용된다.
현대 해결 방안으로만 일반적인 웹 구성 요소를 작성하는 것은 흥미로운 경험이기 때문에 다른 사람들이 같은 방법을 시도하려고 하지 않도록 간단명료한 요약이 있다.항상 틀이 필요한 것은 아니라는 것을 보여준다.
주의: 저는 많은 샘플을 따라야 한다는 것을 알고 있습니다. 현실 세계에서 저는 보통 중점 라이브러리를 선택하여 공백을 메우고 렌더링/전파층은 lit-element을 선택하는 것을 권장합니다.이것은 단지 일반적인 구성 요소와 나의 특수한 경험을 어떻게 만드는지 보여주기 위해서이다.

목표
내 목표는 shiki 라이브러리를 포장하고 다음과 같은 인터페이스/기능을 가진 웹 구성 요소를 만드는 것이다.
<shiki-highlight language="javascript">
function hello() {
  return 'hello world';
}
</shiki-highlight>
나는 어떤 프레임워크나 라이브러리도 사용하고 싶지 않다. 가능하면 shiki 의존항을 제외하고는 다른 의존항이 없다.
나는 또한 ESM만 사용하고 싶다. 즉, CommonJS도 지원하지 않고, CommonJS에도 의존하지 않는다.

초기 프로젝트 설정
나의 생각은 내가 원하는 기본 도구를 한데 조합하는 것이다.
  • 타본
  • 모카
  • web-test-runner
  • prettier
  • eslint
  • typescript-eslint
  • eslint-config-google
  • 나의 모든 자료는 src/ 목록에 있고, 나의 테스트는 src/test/ 목록에 있다.

    타자 원고
    ESM을 작성하여 출력하고 싶다는 점을 감안하면 tsconfig.json은 매우 단도직입적입니다.
    {
      "compilerOptions": {
        "target": "es2017",
        "module": "esnext",
        "moduleResolution": "node",
        "declaration": true,
        "outDir": "./lib",
        "strict": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "noImplicitReturns": true,
        "noFallthroughCasesInSwitch": true,
        "noUncheckedIndexedAccess": true,
        "forceConsistentCasingInFileNames": true
      },
      "include": [
        "src/**/*.ts"
      ]
    }
    

    에스린트
    간단하게 보기 위해 저는 구글의 lint 설정을 사용하고 .eslintrc.json에서 자신의 취향에 따라 몇 가지 규칙을 조정했습니다.
    {
      "extends": [
        "eslint:recommended",
        "google",
        "plugin:@typescript-eslint/eslint-recommended",
        "plugin:@typescript-eslint/recommended"
      ],
      "plugins": ["@typescript-eslint"],
      "rules": {
        "indent": "off",
        "comma-dangle": ["error", "never"],
        "spaced-comment": "off",
        "@typescript-eslint/no-unused-vars": "off",
        "@typescript-eslint/no-inferrable-types": "off"
      }
    }
    
    TypeScript 컴파일러가 이런 검사를 했고 ESLint(no-unused-varsnoUnusedLocals)보다 더 잘했기 때문에 noUnusedParameters 규칙을 사용하지 않았습니다.
    나는 또한 no-inferrable-types을 금지했다. 왜냐하면 일치성을 위해 나는 추리에 의존하지 않고 내 유형을 설명하는 것을 더 좋아하기 때문이다.

    더욱 아름답다
    나는 또한 나의 .prettierrc.json을 추가해서 나의 첫 번째 옵션에 더 예쁜 옵션을 설정하도록 선택했지만, 기본 설정은 대부분의 사람들에게 문제가 없을 수도 있다.

    네트워크 테스트 실행자
    나는 web-test-runner.config.mjs에서 puppeteer을 통해 웹 테스트 runner를 전송을 사용하는 테스트로 설정했다.
    import {puppeteerLauncher} from '@web/test-runner-puppeteer';
    
    export default {
      nodeResolve: true,
      files: 'lib/test/**/*_test.js',
      testFramework: {
        config: {
          ui: 'bdd'
        }
      },
      coverage: true,
      coverageConfig: {
        include: ['lib/**/*.js'],
        exclude: ['lib/test/**/*.js']
      },
      browsers: [
        puppeteerLauncher()
      ]
    };
    
    마찬가지로 매우 간단합니다. 저는 모차의 BDD 인터페이스를 사용하여 Puppeter를 통해 테스트 커버율을 시작하고 싶습니다.
    내 전송 소스가 거의 실제 소스와 같기 때문에 WTR을 실행하는 것을 기억하십시오.그러나 WTR은 esbuild plugin을 사용하여 유형 스크립트 소스에 대해 실행할 수 있습니다.

    단언
    내가 설정한 마지막 부족한 부분은 내가 테스트에서 단언할 내용입니다.
    나는 보통 나무를 선택하지만, 그것은 점점 유행이 지났다. (또는 솔직히 말하면, 그것은 이미 유행이 지났다.)공식 ESM 입구점을 제공하지 않으므로 CommonJS를 사용하려면 스택에서 지원해야 합니다.이것은 내가 구축하는 과정에서 패키지를 도입하는 것을 의미한다. 이것은 받아들일 수 없는 것이다!
    그래서 나는 기쁘게 장작을 버렸고, ESM을 지지하는 대안을 찾는 사람들에게 매달렸다.여기가 내가 uvu을 만난 곳이다.uvu은 매우 작고 TypeScript를 지원하며 ESM으로 발표합니다!위대했어
    그것은 자신의 모카 대체품이 있지만, 내가 이런 디자인을 좋아하는지 확실하지 않기 때문에, 나는 그것에 포함된 uvu/assert 모듈만 사용하고, 꾸준히 모카를 사용한다.

    마지막으로 코드가 있어요.👀
    나는 코드를 한 줄 쓰기 전에 전체 항목을 함께 설정하는 것이 심상치 않다고 의심하기 때문에 위의 대부분의 내용을 뛰어넘을 수 있다😬

    간단한 구성 요소
    먼저 HTML 사용법을 기억하십시오.
    <shiki-highlight language="javascript">
    console.log(12345);
    </shiki-highlight>
    
    따라서 구성 요소는 다음과 같이 대체적으로 필요합니다.
    class ShikiHighlight extends HTMLElement {
      public language?: string;
    }
    
    customElements.define('shiki-highlight', ShikiHighlight);
    
    현재, 이것은 어떤 내용도 과장하지 않지만, 정확한 인터페이스가 있습니다.

    속성이랑 속성이 달라요.language 속성은 있지만 HTML 속성과는 다릅니다.따라서 현재 language 속성은 아무 작업도 수행되지 않으며 속성과 동기화되지 않습니다.
    // These are not equivalent
    node.setAttribute('language', 'javascript');
    node.language = 'javascript';
    
    attributeChanged 콜백 및 observedAttributes 을 통해 다음과 같은 이점을 얻을 수 있습니다.
    class ShikiHighlight extends HTMLElement {
      public language?: string;
    
      public static get observedAttributes(): string[] {
        return ['language'];
      }
    
      public attributeChangedCallback(
        name: string,
        oldValue: string,
        newValue: string
      ): void {
        if (name === 'language') {
          this.language = newValue;
        }
      }
    }
    
    브라우저는 observedAttributes static을 사용하여 변경 리셋을 트리거하는 속성을 확인합니다.속성 변경 값이 관찰될 때마다 변경 리셋(attributeChangedCallback)이 터치됩니다.
    이것은 원소가 language으로 변경될 때마다 우리의 속성도 같은 값으로 설정된다는 것을 의미한다.
    참고: 현재 동기화는 다른 방식으로 발생하지 않습니다. 즉, 설정 중인 속성은 속성을 설정하지 않습니다.

    섀도우 루트 생성하기
    최종적으로 우리는 그림자 뿌리에 문법적으로 밝게 보이는 노드를 보여 사용자의 DOM 트리('light DOM')에 영향을 주지 않기를 바란다.
    그래서 우리는 뿌리가 필요하다.
    public constructor() {
      super();
      this.attachShadow({mode: 'open'});
    }
    
    이로 인해 DOM은 다음과 같습니다.
    <shiki-highlight>
      #shadow-root (open)
        <!-- syntax highlight result will live here -->
      function hello() {
        return 'hello world';
      }
    </shiki-highlight>
    

    관찰광DOM 함량
    우리는 튀어나온 것이 필요해...요소 컨텐트 앞의 예를 기억하는 경우:
    <shiki-highlight>
    console.log(12345); // This is text content of the element
    </shiki-highlight>
    
    우리는 텍스트 내용의 변화를 관찰해야 한다. 매번 새로운 문법 하이라이트를 터치해서 생성된 HTML을 우리가 만든 그림자 뿌리로 출력해야 한다.
    이 작업은 MutationObserver을 통해 수행할 수 있습니다.
    public constructor() {
      super();
      this.attachShadow({mode: 'open'});
    
      this._observer = new MutationObserver(() =>
        this._domChanged());
    }
    
    public connectedCallback(): void {
      this._observer.observe(this, {
        characterData: true,
        subtree: true,
        childList: true
      });
    }
    
    public disconnectedCallback(): void {
      this._observer.disconnect();
    }
    
    protected _domChanged(): void {
      // Fired any time the dom changes
    }
    
    요소를 DOM 트리에 추가하면 브라우저는 connectedCallback을, DOM 트리에서 요소를 제거하면 브라우저는 disconnectedCallback을 호출합니다.
    우리의 예에서 우리는 연결할 때 광DOM(this)을 관찰하고 연결을 끊을 때 관찰을 멈추기를 희망한다.
    우리는 텍스트(characterData)와 서브노드(childList)의 변화를 관찰하고 있다.
    주의: 약간 TIL과 같습니다. textContent을 설정하면 characterData이 바뀌지 않습니다. 사실상 childList은 새로운 텍스트 노드를 하위 노드로 설정하기 때문입니다._domChanged은 다음과 같은 이점을 제공합니다.
    protected _domChanged(): void {
      this._render();
    }
    
    protected _render(): void {
      // do some syntax highlighting here
    }
    

    재산의 변화를 관찰하다
    저희 language호텔 기억나세요?각 언어의 구문 강조 표시가 다르기 때문에 변경할 때마다 다시 렌더링해야 합니다.
    Getter와 setter를 통해 다음과 같은 관찰기를 구현할 수 있습니다.
    // Change our language property to be protected
    protected _language?: string;
    
    // Replace the original property with a getter
    public get language(): string|undefined {
      return this._language;
    }
    
    // and a setter which triggers a re-render
    public set language(v: string) {
      this._language = v;
      this._render();
    }
    
    이제 language 속성을 설정할 때마다 다시 렌더링합니다.
    앞의 속성이 language으로 변경되었는지 확인해야 합니다. 따라서 다시 렌더링을 촉발할 것입니다.

    렌더링 방법 구현
    마지막으로 우리는 문법을 돋보이게 하는 작업을 해야 한다.
    protected _render(): void {
      const highlightedHTML = highlightText(this.textContent ?? '');
      this.shadowRoot.innerHTML = highlightedHTML;
    }
    
    기본적으로 light DOM 텍스트 내용을 강조 표시 라이브러리에 전달한 다음 섀도우 루트에 첨부된 HTML로 돌아갑니다.
    DOM은 다음과 같습니다.
    <shiki-highlight language="javascript">
      # shadow-root (open)
        <pre class="shiki"> ... </pre>
      console.log(12345);
    </shiki-highlight>
    

    테스트 같은 거.
    그 후에 저는 uvu와 모카를 사용하여 일련의 단원 테스트를 작성했습니다.
    import {assert} from 'uvu/assert';
    
    describe('shiki-highlight', () => {
      it('should work', () => {
        assert.is(actual, expected);
      });
    });
    
    WTR을 통해 실행 중:
    npx web-test-runner
    
    나는github 작업 흐름과 패키지 스크립트(lint,format 등)도 설정했다.

    마무리
    내가 이 글을 쓴 것은 정말 내가 vanilla JS와 현대 도구를 이용하여 구성 요소를 만드는 것을 좋아하기 때문이다.
    나는 오래된 브라우저를 고려하지 않고 최신 지원에만 의존한다.
    번들에도 아무런 문제가 없으며, 나는 단지 이런 상황에서 그것을 피하고 싶을 뿐이다. 왜냐하면 원래대로 ESM을 발표하고 작성하는 것이 매우 좋기 때문이다.
    여기서 완료된 구성 요소를 볼 수 있습니다.
    https://github.com/43081j/shiki-element
    여기에서 프레젠테이션을 볼 수 있습니다. (처음에는 로드가 좀 느릴 수도 있습니다.shiki가 뚱뚱하기 때문입니다.)
    https://webcomponents.dev/preview/cQHUW8WJDlFAKqWpKibc
    모든 것이 프레임이나 기본 라이브러리를 필요로 하는 것은 아니다.상태 또는 더 복잡한 데이터 전파/관찰이 필요할 때 문제를 고려해야 할 수도 있습니다.

    좋은 웹페이지 즐겨찾기