nodejs/브라우저와 호환되는 라이브러리 작성

33761 단어

질문:


플랫폼별 기능을 사용하면 호환성 문제가 발생할 수 있으며 다음과 같은 상황이 발생할 수 있다
  • 서로 다른 모듈화 규격: 언제 묶을지 지정
  • 플랫폼 특정 코드: 예를 들어 서로 다른 플랫폼의 자체 적응 코드를 포함한다
  • 플랫폼에 특정한 의존항: 예를 들어 nodejs는 작성해야 한다fetch/FormData
  • 플랫폼에 대한 특정한 유형 정의: 예를 들어 브라우저의Blob와nodejs의Buffer
  • 다른 모듈 사양


    이것은 매우 평범한 일이다.현재 cjs/amd/ife/umd/esm를 포함한 여러 가지 규범이 있기 때문에 이들을 지원하는 것(또는 적어도 주류 cjs/esm을 지원하는 것)도 필수적이다.다행히도, 패키지 도구 롤러는 서로 다른 형식의 출력 파일을 지원하기 위해 상응하는 설정을 제공합니다.

    GitHub sample project


    형상상
    // rollup.config.js
    export default defineConfig({
      input: 'src/index.ts',
      output: [
        { format: 'cjs', file: 'dist/index.js', sourcemap: true },
        { format: 'esm', file: 'dist/index.esm.js', sourcemap: true },
      ],
      plugins: [typescript()],
    })
    
    그런 다음 패키지에 지정합니다.json
    {
      "main": "dist/index.js",
      "module": "dist/index.esm.js",
      "types": "dist/index.d.ts"
    }
    

    Many libraries support cjs/esm, such as rollup, but there are also libraries that only support esm, such as unified.js series


    플랫폼 유한 코드


    - 서로 다른 포털 파일을 통해 다른 내보내기 파일을 패키지화하고 browser 환경과 관련된 코드를 지정합니다(예: dist/browser.js/dist/node.js: 패키지 도구를 사용할 때 주의해야 합니다(비용은 사용자에게 이전).
    - 코드를 사용하여 운영 환경의 동적 로드 확인
    비교
    다른 출구
    코드 판단
    우세하다
    더욱 철저한 코드 격리
    패키지 도구에 의존하지 않는 행위
    최종 코드는 현재 환경의 코드만 포함합니다
    결점
    패키지 도구의 사용자 행동에 따라 다름
    환경을 판단하는 코드가 정확하지 않을 수 있습니다
    최종 코드는 모든 코드를 포함하지만 선택적으로 불러옵니다

    axios combines the above two methods to achieve browser and nodejs support, but at the same time it leads to the shortcomings of the two methods and a little confusing behavior. Refer to getDefaultAdapter. For example, in the jsdom environment, it will be considered as a browser environment, please refer to detect jest and use http adapter instead of XMLHTTPRequest


    다른 입력 파일을 통해 다른 내보내기 파일을 포장합니다


    GitHub sample project


    // rollup.config.js
    export default defineConfig({
      input: ['src/index.ts', 'src/browser.ts'],
      output: [
        { dir: 'dist/cjs', format: 'cjs', sourcemap: true },
        { dir: 'dist/esm', format: 'esm', sourcemap: true },
      ],
      plugins: [typescript()],
    })
    
    {
      "main": "dist/cjs/index.js",
      "module": "dist/esm/index.js",
      "types": "dist/index.d.ts",
      "browser": {
        "dist/cjs/index.js": "dist/cjs/browser.js",
        "dist/esm/index.js": "dist/esm/browser.js"
      }
    }
    

    코드를 사용하여 실행 중인 환경의 동적 불러오기를 확인합니다


    GitHub sample project


    기본적으로 코드에서 판단한 다음await import
    import { BaseAdapter } from './adapters/BaseAdapter'
    import { Class } from 'type-fest'
    
    export class Adapter implements BaseAdapter {
      private adapter?: BaseAdapter
      private async init() {
        if (this.adapter) {
          return
        }
        let Adapter: Class<BaseAdapter>
        if (typeof fetch === 'undefined') {
          Adapter = (await import('./adapters/NodeAdapter')).NodeAdapter
        } else {
          Adapter = (await import('./adapters/BrowserAdapter')).BrowserAdapter
        }
        this.adapter = new Adapter()
      }
      async get<T>(url: string): Promise<T> {
        await this.init()
        return this.adapter!.get(url)
      }
    }
    
    // rollup.config.js
    export default defineConfig({
      input: 'src/index.ts',
      output: { dir: 'dist', format: 'cjs', sourcemap: true },
      plugins: [typescript()],
    })
    

    Note: vitejs cannot bundle this kind of package, because the nodejs native package does not exist in the browser environment, this is a known error, refer to: Cannot use amplify-js in browser environment (breaking vite/snowpack/esbuild).


    플랫폼별 종속성

  • 직접 사용import을 의존항으로 한다. 이것은 서로 다른 환경에서 폭발할 것이다(예를 들어 node-fetch는 브라우저에서 폭발할 것이다)
  • 917이 실행할 때 동적으로 도입되면 (674) 동적으로 불러옵니다 (914) 914
  • 실행 시 코드에서 판단할 때 require를 통해 동적 의존항을 도입한다. 이로써 코드 세그먼트를 나누고 의존항은 선택적으로 단독 파일로 불러온다
  • 서로 다른 입구 파일을 통해 다른 내보내기 파일을 포장한다. 예를 들어import()/dist/browser.js: 사용 시 주의해야 한다
  • 선언dist/node.js 옵션 의존항을 사용자가 직접 작성할 수 있도록 함: 사용 시 주의(비용을 사용자에게 전달)
  • 대비도
    요구 사항
    수입하다
    가슴에 넣을까요?
    예, 그렇습니다.
    아니오.
    개발자가 주의해야 합니까
    아니오.
    아니오.
    여러 번 불러올까요?
    아니오.
    예, 그렇습니다.
    동기화
    예, 그렇습니다.
    아니오.
    요약 지원
    맞다
    맞다

    실행 중인 코드를 확인하는 중peerDependencies를 통해 의존항을 동적으로 도입합니다


    GitHub project example


    // src/adapters/BaseAdapter.ts
    import { BaseAdapter } from './BaseAdapter'
    
    export class BrowserAdapter implements BaseAdapter {
      private static init() {
        if (typeof fetch === 'undefined') {
          const globalVar: any =
            (typeof globalThis !== 'undefined' && globalThis) ||
            (typeof self !== 'undefined' && self) ||
            (typeof global !== 'undefined' && global) ||
            {}
          // The key is the dynamic require here
          Reflect.set(globalVar, 'fetch', require('node-fetch').default)
        }
      }
    
      async get<T>(url: string): Promise<T> {
        BrowserAdapter.init()
        return (await fetch(url)).json()
      }
    }
    

    실행 중인 코드에서 Require를 통해 의존항을 동적으로 도입합니다


    GitHub project example


    // src/adapters/BaseAdapter.ts
    import { BaseAdapter } from './BaseAdapter'
    
    export class BrowserAdapter implements BaseAdapter {
      // Note that this has become an asynchronous function
      private static async init() {
        if (typeof fetch === 'undefined') {
          const globalVar: any =
            (typeof globalThis !== 'undefined' && globalThis) ||
            (typeof self !== 'undefined' && self) ||
            (typeof global !== 'undefined' && global) ||
            {}
          Reflect.set(globalVar, 'fetch', (await import('node-fetch')).default)
        }
      }
    
      async get<T>(url: string): Promise<T> {
        await BrowserAdapter.init()
        return (await fetch(url)).json()
      }
    }
    
    패키지 결과

    몇몇 하위 문제에 부딪히다

  • 전역 변수의 존재 여부를 어떻게 판단합니까
  • typeof fetch === 'undefined'
    
  • 다양한 환경의 글로벌 변수에 대한 PLYFILL 작성 방법
  • const globalVar: any =
      (typeof globalThis !== 'undefined' && globalThis) ||
      (typeof self !== 'undefined' && self) ||
      (typeof global !== 'undefined' && global) ||
      {}
    
  • import():axios는 주로 판단TypeError: Right-hand side of'instanceof' is not callable하고 FormData는 기본적으로 내보내기 때문에 사용해야 한다form-data(나 세대는 항상 내가 구멍을 파고 있다고 느낀다)

  • 요약 패키지를 사용할 때 호환성 문제가 발생할 수 있습니다.사실상, 그들은 내연 코드인지, 아니면 단독으로 파일로 포장하는지 선택해야 한다.참조: https://rollupjs.org/guide/en/#inlinedynamicimports
    내연 = > 개요
    // inline
    export default {
      output: {
        file: 'dist/extension.js',
        format: 'cjs',
        sourcemap: true,
      },
    }
    
    // Outreach
    export default {
      output: {
        dir: 'dist',
        format: 'cjs',
        sourcemap: true,
      },
    }
    

    플랫폼 제한 유형 정의


    다음 해결 방안은 본질적으로 여러 개의 패키지이다
  • 혼합 유형 정의.예: axios
  • 서로 다른 수출 서류와 유형 정의를 포장하고 사용자가 필요로 하는 서류를 스스로 지정하도록 요구한다.예를 들어 (await import('form-data' )).default/module/node를 통해 서로 다른 기능을 탑재한다(사실 이것은 플러그인 시스템에 매우 가깝고 여러 모듈을 분리하는지 여부에 불과하다)
  • 플러그인 시스템을 사용하여 서로 다른 환경의 적응 코드를 여러 개의 서브 모듈로 분리한다.예: 설명.js 커뮤니티
  • 비교
    다중 유형 정의 파일
    블렌드 유형 정의
    다중 모듈
    우세하다
    더욱 뚜렷한 환경 표지
    통합 입구
    더욱 뚜렷한 환경 표지
    결점
    사용자가 선택해야 함
    유형 정의 이중화
    사용자가 선택해야 함
    불필요한 의존
    유지 보수가 비교적 번거롭다(특히 유지 보수가 외롭지 않을 때)

    다른 내보내기 파일 및 유형 정의를 패키지화하고 원하는 파일을 직접 지정해야 합니다.


    GitHub project example


    그것은 주로 핵심 코드에서 추상적인 것을 한 층 추출한 다음에 플랫폼에 특정된 코드를 추출하여 단독으로 포장하는 것이다.
    // src/index.ts
    import { BaseAdapter } from './adapters/BaseAdapter'
    
    export class Adapter<T> implements BaseAdapter<T> {
      upload: BaseAdapter<T>['upload']
    
      constructor(private base: BaseAdapter<T>) {
        this.upload = this.base.upload
      }
    }
    
    // rollup.config.js
    
    export default defineConfig([
      {
        input: 'src/index.ts',
        output: [
          { dir: 'dist/cjs', format: 'cjs', sourcemap: true },
          { dir: 'dist/esm', format: 'esm', sourcemap: true },
        ],
        plugins: [typescript()],
      },
      {
        input: ['src/adapters/BrowserAdapter.ts', 'src/adapters/NodeAdapter.ts'],
        output: [
          { dir: 'dist/cjs/adapters', format: 'cjs', sourcemap: true },
          { dir: 'dist/esm/adapters', format: 'esm', sourcemap: true },
        ],
        plugins: [typescript()],
      },
    ])
    
    사용자 예
    import { Adapter } from 'platform-specific-type-definition-multiple-bundle'
    
    import { BrowserAdapter } from 'platform-specific-type-definition-multiple-bundle/dist/esm/adapters/BrowserAdapter'
    export async function browser() {
      const adapter = new Adapter(new BrowserAdapter())
      console.log('browser:', await adapter.upload(new Blob()))
    }
    
    // import {NodeAdapter} from 'platform-specific-type-definition-multiple-bundle/dist/esm/adapters/NodeAdapter'
    // export async function node() {
    // const adapter = new Adapter(new NodeAdapter())
    // console.log('node:', await adapter.upload(new Buffer(10)))
    //}
    

    플러그인 시스템을 사용하여 서로 다른 환경의 적응 코드를 여러 개의 서브 모듈로 분리하다


    간단하게 말하자면, 실행할 때 의존 관계를 서로 다른 하위 모듈 (예: 위 module/browser 으로 확장하거나, 플러그인 API가 매우 강하다면, 플러그인 하위 모듈에서 분리할 수 있는 공식 적응 코드를 사용할 수 있습니다.

    좋은 웹페이지 즐겨찾기