실시간 공동 작업 스프레드시트 응용 프로그램 구축 방법(Google Sheets Clone)

55240 단어 pubnubjavascript
지난 10년 동안 사무용 세트는 육중하고 시스템의 귀중한 공간을 차지하는 데스크톱 소프트웨어에서 수요에 따라 가벼운 collaborative applications으로 바뀌었다.이것은 웹 브라우저의 여러 사용자가 지구 어느 곳에서든 협동할 수 있도록 실시간으로 협동하는 새로운 세계를 열었다.
이것이 바로 우리가 오늘 이 강좌에서 구축할 내용이다.우리는 협업 스프레드시트에 특히 관심을 기울일 것이지만, 이러한 개념과 디자인 모델은 증분 업데이트를 할 수 있는 협업 문서 - 텍스트 문서, 스프레드시트 등에 적용될 수 있다.
  • full GitHub repo
  • 제공
  • 보기 live collaborative spreadsheets demo app here.몇 개의 웹 브라우저에서 그것을 열면 실시간 동기화 효과를 볼 수 있습니다!

  • 시작하기 전에 목표를 설정하자.
  • 은 당신만 있는 것이 아닙니다. 누가 온라인에 있는지 표시하고 현재 같은 전자 표를 사용하고 있습니다.
  • 뭐 공부 해요?처리 중인 셀을 강조 표시합니다.
  • 보셨어요?변경 사항을 모니터링하고 여러 스프레드시트 편집 세션에서 동기화합니다.
  • 해냈어요!:누구에 대한 감사 추적을 저장하고 유지하며 이를 사용하여 새로운 합작자를 업데이트합니다.
  • 기술 창고


    주막


  • PubNub: 변경 사항을 전달하고 다른 협업자와 동기화하는 실시간 API 및 인프라입니다.

  • Handsontable: 우리가 선택한 전자 표 모듈.
  • 튜토리얼 때문에


  • Parcel: 빠른 원형 제작에 사용되는 제로 설정 웹 패키지입니다.

  • Faker: 허위 데이터 생성(이 예에서 샘플 정보는 칸을 채웁니다.
  • 개발 도구 체인


    세 가지 절차를 시작할 수 있다.package.json: 초기화
    $ mkdir sync-sheet
    $ cd sync-sheet
    $ npm init -y
    
    설치 도구 체인 종속 항목:
    $ npm i -D parcel-bundler
    
    주의: 지금부터 의존 항목을 수동으로 설치할 필요가 없습니다.우리가 그것을 실행할 때, Parcel은 그것을 처리하지만, 나는 가방과 절차를 알아차릴 것이다.
    브라우저의 일관성을 확보하기 위해 최소 babelbrowserlist 설정을 사용합니다.
    우리는 브라우저의 마지막 두 버전을 지원하기를 희망합니다.browserlistpackage.json.에 추가
    "browserslist": [
      "last 2 version"
    ]
    
    루트에 .babelrc 추가:
     {
      "presets": [
        [
          "@babel/preset-env",
          {
            "useBuiltIns": "entry"
          }
    ] ],
      "plugins": [
        "@babel/plugin-transform-runtime"
    ] }
    
    개발 의존항 경보: @babel/core@babel/plugin-transform-runtime.

    입구점


    우리는 입구점이 필요하다. 우리는 그것을 통해 소포에 도착할 수 있다.소스 폴더를 만듭니다.
    $ mkdir src
    
    소스 폴더에 색인 파일 추가하기
    $ touch src/index.html
    

    파일 Src/IndexHtml


    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
        <title>Sheet Sync using pubnub</title>
    </head>
    <body>
        <!-- our spreadsheet goes here -->
        <script src="./main.js"></script>
      </body>
    </html>
    
    script 태그가 main.js에 연결되어 있기 때문에 우리도 그것을 만듭니다.
    $ touch src/main.js
    
    index.html에 대한 서비스 제공, 개발 시작:
    $ npx parcel src/index.html
    

    지도의


    PubNub 추가 구성 요소


    PubNub를 사용하려면 우선 sign up for a PubNub account.이 필요합니다. 걱정하지 마세요. 저희는 당신의 모든 원형과 개발에 아낌없는 무료 층을 제공합니다.
    등록 후 PubNub Admin Dashboard:에서 다음 로딩 항목 활성화
  • 존재
  • 저장 및 재생
  • 보기


    우리의 관점은 다음과 같다.
  • 스프레드시트
  • 온라인 사용자
  • 스프레드시트 지우기 버튼
  • 의 단추는 우리의 작업표에 허위 데이터를 심는 데 사용됩니다.
  • 몸은 이렇게 보인다.
    <body>
        <div id="online-users"></div>
        <button id="clear-data"> Clear </button>
        <div id="sheet"></div>
        <button id="seed-data"> Seed sample row </button>
        <script src="./main.js"></script>
    </body>
    

    스프레드시트


    다음은 handsontable을 초기화하고 자신을 위한 전자 표를 준비하겠습니다.
    $ touch src/spreadsheet.js
    

    파일 Src/스프레드시트.회사 명


    import Handsontable from 'handsontable';
    import 'handsontable/dist/handsontable.full.min.css';
    const hotSettings = {
      columns: [
        {
          data: 'available',
          type: 'checkbox',
    }, {
    data: 'id',
          type: 'numeric',
        },
        {
          data: 'email',
          type: 'text',
    }, {
          data: 'fullName',
          type: 'text',
        },
      ],
      stretchH: 'all',
      autoWrapRow: true,
      rowHeaders: true,
      colHeaders: [
        '',
        'Id',
        'Email',
        'Name',
    ],
      columnSorting: {
        indicator: true,
      },
      autoColumnSize: {
        samplingRatio: 23,
      },
      contextMenu: true,
      licenseKey: 'non-commercial-and-evaluation',
      customBorders: true,
      colWidths: ['7%', '16%', '38%', '39%'],
      className: 'sheet',
    };
    export default (data, elementId) => new Handsontable(
      document.querySelector(elementId), {
      data,
      ... hotSettings
    });
    
    의존 경고: handsontablesrc/spreadsheet.js을 우리의 src/main.js으로 가져옵니다.전자 표를 저장하는 서버가 있다면, 여기에서 sheetData을 가져와 채워야 합니다.
    import Sheet from '~/spreadsheet'
    const sheetData = [];
    const sheet = Sheet('sheet', sheetData);
    
    이제 샘플 줄의 씨앗을 위한 탐지기를 단추에 추가합니다.
    import faker from 'faker';
    document.getElementById('seed-data').addEventListener('click', () => {
      sheet.populateFromArray(data.length, 0, [[
        faker.random.boolean(),
        faker.random.number(),
        faker.internet.email(),
        faker.name.findName(),
    ]]); });
    
    의존 경고:faker

    사용자 세션


    사용자의 세션은 일반적으로 탭에서 지속되기 때문에 탭 간에 변경 사항을 동기화하기 위해 다른 유형의 세션이 필요합니다.우리는 편집 세션이라고 부른다.
    귀하는 자신의 신분 검증 논리가 있을 수 있지만, 이 강좌에서는 사용자 세션을 간단하게 유지하고 모의합니다.
    $ touch src/user.js
    

    파일 src/user.회사 명


    import faker from 'faker';
    const userFromLocalStorage = window.localStorage.user &&
    JSON.parse(window.localStorage.user);
    const user = userFromLocalStorage || {
      id: faker.random.uuid(),
      color: faker.internet.color(160, 160, 160),
      name: faker.name.findName(),
    };
    // editing session
    user.sessionUUID = faker.random.uuid();
    window.localStorage.user = JSON.stringify(user);
    export default user;
    
    의존 경고:faker
    이렇게 하면 우리의 사용자 세션을 처리할 수 있지만, 보시다시피, 우리는 localStorage에서 편집 세션을 선택하지 않습니다. 왜냐하면 우리는 모든 옵션 카드에 유일한 세션이 필요하기 때문입니다.

    PubNub 커넥터


    편집 세션을 사용하여 PubNub 인스턴스를 초기화합니다.
    $ mkdir src/connectors
    $ touch src/connectors/pubnub.js
    

    파일 Src/Pubnub.회사 명


    import PubNub from 'pubnub';
    import user from '../user';
    const pubnub = new PubNub({
      publishKey: process.env.PUBNUB_PUBLISH_KEY,
      subscribeKey: process.env.PUBNUB_SUBSCRIBE_KEY,
      uuid: user.sessionUUID,
    });
    export default pubnub;
    
    종속성 경고: PubNub

    연결


    우리는 두 가지 유형의 연결이 필요하다.
  • 은 스프레드시트에 대한 변경 사항을 기록합니다.
  • 은 전자 표에 변경 사항을 다시 전송합니다.
  • 파일 Src/Hooks.회사 명


    const hooks = {
      record: {},
      replay: {},
    };
    

    레코드 연결


    우리는 handsontable에서 제공한 다음과 같은 연결을 사용할 것이다

  • AfterChange은 모든 칸이 변경된 후에 터치합니다.

  • AfterCreateRow은 줄을 추가한 후 터치합니다.

  • AfterRemoveRow은 줄을 삭제한 후 터치합니다.

  • 정렬 후 AfterColumnSort을 터치합니다.
  • 트리거는 delta를 제공하여 PubNub에 게시합니다.
    이것도 우리가 전송한 변경 사항을 다시 재생한 후에 촉발될 수 있기 때문에 무한순환에 들어가지 않도록 하는 방법이 필요하다.다행히도, 이전 세 개의 트리거에 대해, 우리는 그것을 다시 PubNub에 발표하지 않도록 변경된 출처를 볼 수 있다.열 정렬에 대해 우리는 대체 해결 방안이 필요하다.
    레코드 연결은 PubNub으로 증가분을 발표하기 위해 PubNub 객체와 워크시트의 이름(이것은 우리의 채널 이름)이 필요합니다. 따라서 레코드 함수는 PubNubsheetName을 매개 변수로 받아들이고 handsontable의 연결 리셋을 되돌려줍니다.

    바꾸다


    hooks.record.afterChange = (pubnub, sheetName) => function recordAfterChange(
      changes,
      source,
    ){
    if (source === 'sync' || !changes) {
    return; }
      // Publish all deltas to pubnub in sequence.
      changes.reduce(async (prev, [row, prop, oldValue, newValue]) => {
        await prev;
        return pubnub.publish({
    message: {
            operation: 'afterChange',
            delta: {
              row, prop, oldValue, newValue,
            },
    },
          channel: sheetName,
        });
    }, true); };
    

    행 만들기


    hooks.record.afterCreateRow = (pubnub, sheetName) => function afterCreateRow(
      index,
    amount,
    source, ){
      if (source === 'sync') {
        return;
    }
      pubnub.publish({
        message: { operation: 'afterCreateRow', delta: { index, amount } },
        channel: sheetName,
    }); };
    

    행 삭제


    hooks.record.afterRemoveRow = (pubnub, sheetName) => function afterRemoveRow(
      index,
    amount,
    source, ){
      if (source === 'sync' || source === 'ObserveChanges.change') {
        return;
      }
      pubnub.publish({
        message: { operation: 'afterRemoveRow', delta: { index, amount } },
        channel: sheetName,
      });
    };
    

    열 정렬


    앞의 갈고리와 달리sort는 source을 제공하지 않습니다.반대로 우리는 변수 lastSortFromSync을 유지하여 마지막으로 PubNub에 동기화된 정렬 설정을 저장할 것입니다.현재 정렬 구성에 변경 사항이 없으면 다른 증가분을 PubNub에 게시하는 것을 건너뜁니다.
    let lastSortFromSync;
    hooks.record.afterColumnSort = (pubnub, sheetName) => function afterColumnSort(
      [currentSortConfig],
      [destinationSortConfig],
    ){
    if (lastSortFromSync === destinationSortConfig) {
    return; }
      if (
        lastSortFromSync && destinationSortConfig
        && lastSortFromSync.column === destinationSortConfig.column
        && lastSortFromSync.sortOrder === destinationSortConfig.sortOrder
    ){ return;
    }
      pubnub.publish({
        message: {
          operation: 'afterColumnSort',
          delta: { currentSortConfig, destinationSortConfig },
        },
        channel: sheetName,
      });
    };
    
    만약 내가 이것들을 나의 서버에 동기화해야 한다면 어떻게 해야 할지 알고 싶을지도 모른다.두 가지 방법이 있습니다.
  • 은 delta를 사용하여 서버에 API 호출을 한 다음 PubNub에 게시합니다.

  • 권장 사항: PubNub에 게시하고 PubNub Functions을 사용하여 서버에 API 호출을 수행합니다.
  • 재방송 연결


    다른 사람이 녹음한 어떤 내용도 반드시 재방송해야 한다.그래서 우리는 통상적인 용의자가 있다.

  • setDataAtCell을 사용하여 afterChange을 재방송합니다.

  • alter을 사용하여 afterCreateRow을 재방송합니다.
  • afterRemoveRowalter으로 재방송됩니다.

  • clearSort을 사용하여 이전의 정렬과 sort 설정 순서를 재설정하여 afterColumnSort을 재방송합니다.
  • 이 갈고리들은 우리의 PubNub 탐지기가 제공할 것이며, 우리는 강좌에서 더욱 토론할 것이다.

    바꾸다


    hooks.replay.afterChange = function replayAfterChange(hot, {
      row, prop, newValue,
    }) {
      hot.setDataAtCell(row, hot.propToCol(prop), newValue, 'sync');
    };
    

    행 만들기


    hooks.replay.afterCreateRow = function replayAfterCreateRow(hot, {
      index, amount,
    }) {
      hot.alter('insert_row', index, amount, 'sync');
    };
    

    행 삭제


    hooks.replay.afterRemoveRow = function replayAfterRemoveRow(hot, {
      index, amount,
    }) {
      hot.alter('remove_row', index, amount, 'sync');
    };
    

    열 정렬


    hooks.replay.afterColumnSort = function replayAfterColumnSort(hot, {
      destinationSortConfig,
    }) {
      if (!destinationSortConfig) {
        hot.getPlugin('columnSorting').clearSort();
    return; }
      hot.getPlugin('columnSorting').sort(destinationSortConfig);
    };
    export default hooks;
    

    상태 연결


    사용자 활동을 추적합니다. 다시 말하면 사용자가 어떤 칸을 처리하고 있거나 사용자가 어떤 칸을 강조 표시하고 있는지 추적합니다.이것은 스프레드시트의 히스토리나 감사 추적의 일부분이 아니므로 PubNub 상태로 설정합니다.
    선택 및 선택 취소 후 연결을 추가합니다.src/main.js에 추가합니다.
    hot.addHook('afterSelectionEnd', (row, col, row2, col2) => {
      pubnub.setState(
    {
    state: {
            selection: {
              row, col, row2, col2,
    },
    user, },
          channels: [sheetName],
        },
    ); });
    hot.addHook('afterDeselect', () => {
      pubnub.setState(
    {
    state: {
            selection: null,
    user, },
          channels: [sheetName],
        },
    ); });
    
    우리는 setBorders을 사용하여 다른 사람들이 어떤 세포를 연구하고 있는지 보여줄 것이다.
    const customBordersPlugin = hot.getPlugin('customBorders');
    function setBorders({
      row, col, row2, col2,
    }, color) {
      customBordersPlugin.setBorders([[row, col, row, col2]], {
        top: { width: 2, color },
      });
      customBordersPlugin.setBorders([[row2, col, row2, col2]], {
        bottom: { width: 2, color },
    });
      customBordersPlugin.setBorders([[row, col, row2, col]], {
        left: { width: 2, color },
    });
      customBordersPlugin.setBorders([[row, col2, row2, col2]], {
        right: { width: 2, color },
    }); }
    

    존재 및 상태


    다음 함수는 PubNub에서 상태를 가져옵니다.상태 현재 스프레드시트를 열고 있는 모든 개인 사용자와 해당 상태를 읽어들입니다.그리고 사용자의 상태를 사용하여 #online-users을 채우고 setBorders을 호출하여 사용자의 상태를 사용하여 칸을 강조 표시합니다.
     function fetchPresense() {
      pubnub.hereNow(
        {
          channels: [sheetName],
          includeUUIDs: true,
          includeState: true,
        },
        (status, { channels: { test_sheet: { occupants } } }) => {
          customBordersPlugin.clearBorders();
          const sessions = new Set();
          const html = occupants.reduce((acc, { state = {} }) => {
            if (!state.user || (state.user.uuid === user.uuid) ||
    sessions.has(state.user.uuid)) {
    return acc; }
            sessions.add(state.user.uuid);
            if (state.selection) {
              setBorders(state.selection, state.user.color);
    }
            return `${acc}
              <span> ${state.user.name} is online </span> <br/>`;
    }, '');
          document.getElementById('online-users').innerHTML = html;
        },
    ); }
    

    테이블 재설정


    테이블 정리에는 다음 두 단계가 포함됩니다.
  • 은 우리의 데이터 집합을 초기화합니다.
  • 다른 사람에게 통지합니다.
  • document.getElementById('clear-data').addEventListener('click', () => {
      data.length = 0;
      pubnub.deleteMessages({
        channel: sheetName,
      }, renderHistory);
      pubnub.publish({
        channel: sheetName,
        message: { operation: 'afterClearHistory' },
      });
    });
    

    PubNub 탐지기


    두 개의 탐지기를 추가해야 합니다.
    한 사람이 이 메시지를 들을 수 있다.수신된 작업이 afterClearHistory이면 데이터를 지웁니다.그렇지 않으면, 우리는 상응하는 재방송 연결을 호출한다.
    두 번째는presence입니다. 앞에서 설명한 fetchPresense 함수를 호출합니다.
    pubnub.addListener({
      message({ publisher, message: { operation, delta }, timetoken }) {
        if (publisher !== pubnub.getUUID()) {
          if (operation === 'afterClearHistory') {
            data.length = 0;
            hot.render();
          } else {
            hooks.replay[operation](hot, delta);
          }
    } },
      presence({ uuid }) {
        if (uuid === pubnub.getUUID()) {
    return; }
        fetchPresense();
      },
    });
    

    감사 추적 재생


    웹 브라우저에 국한되기 때문에 새로운 파트너가 최신 스프레드시트 상태를 실행하고 사용할 수 있는 방법이 필요합니다.
    PubNub의 채널 히스토리는 스프레드시트의 감사 로그로 사용됩니다.그리고 나서 우리는 이 로그들을 재방송하여 사용자에게 최신 상태를 제공할 수 있다.

    Src/Main。회사 명


    pubnub.history({
      channel: [sheetName],
    }, (status, { messages }) => {
      messages.forEach((message) => {
        hooks.replay[message.entry.operation](hot, message.entry.delta);
      });
    });
    

    가입 및 초기화 연결


    우리는 역사가 재연될 때까지 기다리고 싶다.로그를 다시 재생한 후, 우리는 새로운 소식을 얻기 위해 채널을 구독합니다.또한 사용자의 초기 상태를 설정합니다.마지막으로, 우리는 모든 기록 연결을 초기화했다.
    우리는 이 모든 것을 역사 리셋에 추가할 것이다.

    Src/Main에 첨부합니다.회사 명


    pubnub.history({
      channel: [sheetName],
    }, (status, { messages }) => {
      messages.forEach((message) => {
        hooks.replay[message.entry.operation](hot, message.entry.delta);
      });
      pubnub.subscribe({
        channels: [sheetName],
        withPresence: true,
    });
      pubnub.setState(
        {
          state: {
            selection: null,
            user,
    },
          channels: [sheetName],
        },
    );
      Object.keys(hooks.record).forEach((hook) => {
        hot.addHook(hook, hooks.record[hook](pubnub, sheetName));
    });
      fetchPresense();
    });
    

    결과


    그렇습니다!우리 모두 준비가 다 되었으니 이제 시작합시다.
    $ npx parcel src/index.html
    
    브라우저에서 http://localhost:1234을 엽니다.다른 탭에서 다시 엽니다.그리고 익명 브라우저를 열고 다시 한 번 하세요.브라우저를 하나 더 추가하는 게 어때요?되도록 많이 해라.이제 그 중 하나를 변경하고 탭, 창, 브라우저에 전파됩니다.
    또는 live collaborative spreadsheets demo app here을 보십시오.몇 개의 웹 브라우저에서 그것을 열면 실시간 동기화 효과를 볼 수 있습니다!
    마찬가지로, 여기에는 full GitHub repo이 참고할 수 있습니다.

    좋은 웹페이지 즐겨찾기