create-react-app PWA에 "새 버전 사용 가능"알림 표시

상사/영업팀으로부터 앱이 마술처럼 아름다운 빈 화면으로 바뀌었다고 불평하는 메시지와 인쇄 화면을 이미 받은 경우(내부 패닉 발작 후 갑자기 프로덕션 앱에 액세스하여 무슨 일이 일어나고 있는지 확인) 솔루션은 페이지를 다시 여는 것이라고 설명합니다. 아마도 해당 게시물이 귀하를 위해 만들어졌을 것입니다!



— 하지만 내 컴퓨터에서는 앱이 정상적으로 작동합니다!! 무슨 일이야?!

시작하기 전에 메시지를 남기고 싶습니다.create-react-app 패키지를 사용하여 만든 시스템에서 새로운 프런트 엔드 배포 문제를 해결하기 위해 내가 찾고 만든 것을 공유하는 방법으로 이 기사를 작성했습니다. 나는 누군가를 돕고 싶고 더 나은 해결책이나 제안이 있으면 아래에 자유롭게 의견을 말하십시오. 커뮤니티와 함께 ​​더 많은 것을 배울 수 있다면 정말 좋을 것입니다.

목차


  • What happened to my app?
  • Important things to notice before starting to code

  • Finally, the implementation
  • Creating the functions
  • Placing snackbar provider

  • Testing the feature

  • 1. 내 앱은 어떻게 되었나요?

    To understand what happened, first of all, it is necessary to know some basic concepts as, what is service worker and how it is used on PWAs.

    According to Google Developers page

    A service worker is a script that your browser runs in the background, separate from a web page [...] The reason this is such an exciting API is that it allows you to support offline experiences, giving developers complete control over the experience.
    The intent of the service worker lifecycle is to:

    • Make offline-first possible.
    • Allow a new service worker to get itself ready without disrupting the current one.
    • Ensure an in-scope page is controlled by the same service worker (or no service worker) throughout.
    • Ensure there's only one version of your site running at once.


    모바일 앱에서와 유사한 PWA의 이 오프라인 경험은 추가 방문을 위해 모든 정적 자산을 캐싱하여 이루어집니다. 그러나 이는 create-react-app documentation에 설명된 대로 기본 서비스 작업자 수명 주기 동작으로 인해 일부 결과가 발생할 수 있습니다.

    After the initial caching is done, the service worker lifecycle controls when updated content ends up being shown to users. In order to guard against race conditions with lazy-loaded content, the default behavior is to conservatively keep the updated service worker in the "waiting" state. This means that users will end up seeing older content until they close (reloading is not enough) their existing, open tabs.



    따라서 앱의 새 버전이 배포되고 고객이 액세스를 시도하면 브라우저는 새 버전을 식별하지만 고객은 다음 방문 시에만 액세스하고 사용하는 코드의 변경 사항에 따라 달라집니다. 이전 캐시된 앱의 경우 빈 페이지가 발생할 수 있습니다.

    2. 코딩을 시작하기 전에 알아두어야 할 중요한 사항

    If you take a look at the index.js of a create-react-app (CRA) project that uses service worker lifecycle you'll find something like this

    ...
    // If you want your app to work offline and load faster, you can change
    // unregister() to register() below. Note this comes with some pitfalls.
    // Learn more about service workers: https://bit.ly/CRA-PWA
    serviceWorker.register();
    

    or even something like this, if you're using an old version of CRA

    ...
    if (process.env.NODE_ENV === 'production') {
     serviceWorker.register()
    }
    ... 
    

    and it's this function, serviceWorker.register() , that makes all the magic behind the scenes

    Now, if you open src/serviceWorker.js file, what you're gonna see is a bunch of code verifying if the environment is the production one and checking if there is new content waiting to be load on the next visit or if the content is already updated and cached for offline use.

    If the content on serviceWorker.js file seems to be complicated, don't worry! the icing on the cake is the discreet callback named onUpdate(registration) called when there is a new content waiting to be used. It's that callback we're going to use.

    function registerValidSW(swUrl, config) {
    ...
                if (navigator.serviceWorker.controller) {
                  // At this point, the updated precached content has been fetched,
                  // but the previous service worker will still serve the older
                  // content until all client tabs are closed.
                  console.log(
                    'New content is available and will be used when all ' +
                      'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
                  );
    
                  // Execute callback
                  if (config && config.onUpdate) {
                    config.onUpdate(registration);
                  }
                } else {
                  ...
                  }
    
        })
        ...
    }
    
    The last thing that's important to show could be viewed on the production version of your app, where a file named service-worker.js will be generated by workbox . 이 파일은 아래와 같이 브라우저 검사 옵션의 source 탭에서 볼 수 있습니다.

    이전에 말했듯이 발견된 새 업데이트는 다음 액세스 시 로드될 때까지 대기하지만 이 대기 시간을 건너뛸 수 있는 옵션이 있으며 인쇄 화면의 빨간색 화살표는 해당 이벤트를 위해 생성된 이벤트를 가리킵니다.



    3. 마지막으로 구현

    Once we found such things, we need to implement a feature that sends a SKIP_WAITING message for the service-worker on the browser when there is a new version available and the user clicks on an update button on a snackbar notification shown on the interface.



    — What?!
    Calm down! I tried to drawn out for you


    이를 가능하게 하기 위해 두 가지 상태, 두 가지 기능 및 notistack 패키지를 사용하지만 다른 패키지를 사용할 수도 있습니다.

    3.1. 함수 만들기

    Assuming that a new version could be detected in any route of the app, any of these routes may be able to trigger the skip message to the service-worker event listener when the app loads, so we're going to make the functions and the states on the main application component, the App.js .

    Notice that the approaching will depend on your project. The project I was working on has the App.js component as a class but feel free to use React Hooks if you're using functional components.

    The first needed states are a boolean to manage the opening of the snackbar and an object for saving the waiting service worker. I named them newVersionAvailable and waitingWorker respectively and these states are going to be changed through the onUpdate callback called when the browser finds out another version of the app. I named this callback onServiceWorkerUpdate() as can be seen on the code block below.

    onServiceWorkerUpdate = registration => {
            this.setState({
                waitingWorker: registration && registration.waiting,
                newVersionAvailable: true
            })
        }
    

    The next function declared and showed below was the updateServiceWorker() , that posts the SKIP_WAITING message and will be used on the snackbar refresh button.

    updateServiceWorker = () => {
            const { waitingWorker } = this.state
            waitingWorker && waitingWorker.postMessage({ type: 'SKIP_WAITING' })
            this.setState({ newVersionAvailable: false })
            window.location.reload()
        }
    

    Besides the addition of these functions, the serviceWorker.register() should be cut from index.js and pasted on the App.js . This register function needs to be executed on the first load of the application and we also need to pass onServiceWorkerUpdate() function, made previously, as an argument to it as well as use updateServiceWorker() as the snackbar onClick function as you can see on the next code block.

    componentDidMount = () => {
        const { enqueueSnackbar } = this.props;
        const { newVersionAvailable } = this.state;
    
    if (process.env.NODE_ENV === 'production') {
        serviceWorker.register({ onUpdate: this.onServiceWorkerUpdate });
    }
    
        if (newVersionAvailable) //show snackbar with refresh button
          enqueueSnackbar("A new version was released", {
            persist: true,
            variant: "success",
            action: this.refreshAction(),
          });
      };
    
    refreshAction = (key) => { //render the snackbar button
        return (
          <Fragment>
            <Button
              className="snackbar-button"
              size="small"
              onClick={this.updateServiceWorker}
            >
              {"refresh"}
            </Button>
          </Fragment>
        );
      };
    

    With these modifications made, the App.js should look like this

    import React, { Component, Fragment } from "react";
    //default imports...
    import { withSnackbar } from "notistack";
    import * as serviceWorker from "./serviceWorker";
    import { Button } from "@material-ui/core";
    
    class App extends Component {
      constructor(props) {
        super(props);
        this.state = {
          newVersionAvailable: false,
          waitingWorker: {},
        };
      }
    
    onServiceWorkerUpdate = (registration) => {
        this.setState({
          waitingWorker: registration && registration.waiting,
          newVersionAvailable: true,
        });
      };
    
      updateServiceWorker = () => {
        const { waitingWorker } = this.state;
        waitingWorker && waitingWorker.postMessage({ type: "SKIP_WAITING" });
        this.setState({ newVersionAvailable: false });
        window.location.reload();
      };
    
      refreshAction = (key) => { //render the snackbar button
        return (
          <Fragment>
            <Button
              className="snackbar-button"
              size="small"
              onClick={this.updateServiceWorker}
            >
              {"refresh"}
            </Button>
          </Fragment>
        );
      };
    
    
      componentDidMount = () => {
        const { enqueueSnackbar } = this.props;
        const { newVersionAvailable } = this.state;
    if (process.env.NODE_ENV === 'production') {
        serviceWorker.register({ onUpdate: this.onServiceWorkerUpdate });
    }
    
        if (newVersionAvailable) //show snackbar with refresh button
          enqueueSnackbar("A new version was released", {
            persist: true,
            variant: "success",
            action: this.refreshAction(),
          });
      };
    
      render() {
           //render components
       }
    }
    export default withSnackbar(App); //uses the snackbar context
    
    

    3.2. 스낵바 공급자 배치

    Once the main App component is ready, the file index.js is the next one to be changed.

    On this file, it is necessary to wrap the main App component with the Snackbar provider.

    //default imports
    import { SnackbarProvider } from "notistack";
    
    ReactDOM.render(
      <React.StrictMode>
          <SnackbarProvider>
              <App/>
          </SnackbarProvider>
      </React.StrictMode>,
      document.getElementById("root")
    );
    
    If you have questions about notistack package I recommend accessing this site .

    기능 테스트

    The last thing to do is testing the feature and to do so it's necessary to build the application . 이를 위해 아래 명령을 사용할 수 있습니다.

    npm run build
    

    정적 서버를 보다 쉽게 ​​처리하려면 다음 명령을 사용하여 설치 및 실행할 수 있는 serve 패키지를 사용하는 것이 좋습니다.

    npm install -g serve
    serve -s build
    

    이 명령을 실행하면 애플리케이션이 실행되고(아마도 5000 포트에서) 콘솔을 열면 다음과 같은 로그가 표시됩니다.



    이제 코드로 돌아가서 예를 들어 버튼 레이블을 변경하는 등 몇 가지 수정 작업을 수행합니다. 이제 npm run build를 다시 실행하고 제공된 페이지를 새로 고칩니다. 결과는 아래 GIF와 같아야 합니다.

    좋은 웹페이지 즐겨찾기