天天看點

react單頁應用權限控制_我如何建構單頁React應用程式 我如何建構單頁React應用程式 (How I architected a single-page React application)

react單頁應用權限控制

by Gooi Ying Chyi

通過Gooi Ying Chyi

我如何建構單頁React應用程式 (How I architected a single-page React application)

具有資料結構,元件以及與Redux的內建 (With Data Structures, Components and integration with Redux)

I recently built a single-page application that interacts with a backend JSON API server. I chose to use React to deepen my understanding of React fundamentals and how each tool can help in building a scalable frontend.

我最近建構了一個與後端JSON API伺服器進行互動的單頁應用程式。 我選擇使用React來加深我對React基礎知識以及每種工具如何幫助建構可擴充前端的了解。

The stack of this application consists of:

該應用程式的堆棧包括:

  • Frontend with React/Redux

    帶有React / Redux的前端

  • A backend JSON API server with Sinatra, integrated with Postgres for database persistence

    帶有Sinatra的後端JSON API伺服器,與Postgres內建以實作資料庫持久性

  • An API client that fetches data from OMDb API, written in Ruby

    一個API用戶端,該用戶端從Ruby編寫的OMDb API中擷取資料

For this post, we’ll assume that we have the backend completed. So let’s focus on how design decisions are made on the frontend.

對于這篇文章,我們假設我們已經完成了後端。 是以,讓我們集中讨論如何在前端做出設計決策。

Side note: The decisions presented here are for reference only and may vary depending on the needs of your application. An example OMDb Movie Tracker app is used here for demonstration.
旁注:此處提出的決定僅供參考,可能會因您的應用程式需求而異。 這裡使用示例OMDb Movie Tracker應用進行示範。

應用程式 (The App)

The application consists of a search input form. A user can input a movie title to return a movie result from OMDb. The user can also save a movie with a rating and short comment into a favorites list.

該應用程式包含一個搜尋輸入表單。 使用者可以輸入電影标題以從OMDb傳回電影結果。 使用者還可以将帶有評分和簡短評論的電影儲存到收藏夾清單中。

To view the final app, click here. To view the source code, click here.

要檢視最終的應用程式, 請單擊此處 。 要檢視源代碼,請單擊此處 。

When a user searches a movie on the homepage, it looks like this:

當使用者在首頁上搜尋電影時,看起來像這樣:

For the sake of simplicity, we’ll only focus on designing the core features of the application in this article. You can also skip to Part II: Redux of the series.

為了簡單起見,在本文中我們将隻着重于設計應用程式的核心功能。 您也可以跳至第二部分:Redux 系列中的。

資料結構 (Data Structure)

Defining appropriate data structures should be one of the most important aspects of designing an app. This should come as the first step, as it determines not only how the frontend should render the elements, but also how the API server should return the JSON responses.

定義适當的資料結構應該是設計應用程式最重要的方面之一。 這應該作為第一步,因為它不僅确定前端應如何呈現元素,而且還要确定API伺服器應如何傳回JSON響應。

For this app, we’ll need two main pieces of information to properly render our UI: a single movie result and a list of favorited movies.

對于此應用程式,我們需要兩條主要資訊才能正确呈現我們的UI: 單個電影結果和收藏的電影清單 。

電影結果對象 (Movie result object)

A single movie result will contain information such as the title, year, description, and poster image. With this, we need to define an object that can store these attributes:

單個電影結果将包含諸如标題,年份,描述和海報圖像之類的資訊。 這樣,我們需要定義一個可以存儲以下屬性的對象:

{  "title": "Star Wars: Episode IV - A New Hope",  "year": "1977",  "plot": "Luke Skywalker joins forces with a Jedi Knight...",  "poster": "https://m.media-amazon.com/path/to/poster.jpg",  "imdbID": "tt0076759"}
           

The

poster

property is simply a URL to the poster image that will be displayed in the results. If there’s no poster available for that movie, it will be “N/A”, which we will display a placeholder. We will also need an

imdbID

attribute to uniquely identify each movie. This is useful for determining whether or not a movie result already exists in the favorites list. We’ll explore later on how it works.

poster

屬性隻是将在結果中顯示的海報圖像的URL。 如果沒有該電影的海報,它将為“ N / A”,我們将顯示一個占位符。 我們還将需要一個

imdbID

屬性來唯一辨別每部電影。 這對于确定收藏夾清單中是否已經存在電影結果很有用。 稍後我們将探讨其工作原理。

收藏清單 (Favorites list)

The favorites list will contain all of the movies saved as favorites. The list will look something like this:

收藏夾清單将包含所有儲存為收藏夾的電影。 該清單将如下所示:

[  { title: "Star Wars", year: "1977", ..., rating: 4 },  { title: "Avatar", year: "2009", ..., rating: 5 }]
           

Keep in mind that we’ll need to look up a specific movie from the list, and the time complexity for this approach is O(N). While it works fine for smaller datasets, imagine having to search for a movie in a favorites list that grows indefinitely.

請記住,我們需要從清單中查找特定電影,這種方法的時間複雜度為O(N) 。 盡管它對于較小的資料集可以正常工作,但想象一下必須在無限期增長的收藏夾清單中搜尋電影。

With this in mind, I chose to go with a hash table with keys as

imdbID

and values as favorited movie objects:

考慮到這一點,我選擇使用一個哈希表,其鍵為

imdbID

,值作為收藏的電影對象:

{  tt0076759: {    title: "Star Wars: Episode IV - A New Hope",    year: "1977",    plot: "...",    poster: "...",    rating: "4",    comment: "May the force be with you!",  },  tt0499549: {    title: "Avatar",    year: "2009",    plot: "...",    poster: "...",    rating: "5",    comment: "Favorite movie!",  }}
           

With this, we can look up a movie in the favorites list in O(1) time by its

imdbID

.

這樣,我們可以在O(1)時間通過其

imdbID

在收藏夾清單中查找電影。

Note: the runtime complexity is probably not going to matter in most cases since the datasets are usually small on the client-side. We are also going to perform slicing and copying (also O(N) operations) in Redux anyway. But as an engineer, it’s good to be aware of potential optimizations that we can perform.
注意:在大多數情況下,運作時複雜度可能無關緊要,因為用戶端上的資料集通常很小。 無論如何,我們還将在Redux中執行切片和複制(也是O(N)操作)。 但是,作為一名工程師,最好意識到我們可以執行的潛在優化。

元件 (Components)

Components are at the heart of React. We’ll need to determine which ones that will interact with the Redux store, and which ones that are only for presentation. We can also reuse some of the presentational components too. Our component hierarchy will look something like this:

元件是React的核心。 我們需要确定哪些将與Redux存儲互動,哪些僅用于示範。 我們還可以重用某些示範元件。 我們的元件層次結構如下所示:

首頁 (Main page)

We designate our App component at the top level. When the root path is visited, it needs to render the SearchContainer. It also needs to display flash messages to the user and handle the client-side routing.

我們在頂層指定App元件。 當通路根路徑時,它需要呈現SearchContainer 。 它還需要向使用者顯示Flash消息并處理用戶端路由。

The SearchContainer will retrieve the movie result from our Redux store, providing information as props to MovieItem for rendering. It will also dispatch a search action when a user submits a search in SearchInputForm. More on Redux later.

SearchContainer将從我們的Redux商店中檢索電影結果,并提供相關資訊作為MovieItem進行渲染的道具。 當使用者在SearchInputForm中送出搜尋時,它還将排程搜尋操作。 稍後再介紹Redux。

添加到收藏夾表單 (Add To Favorites Form)

When the user clicks on the “Add To Favorites” button, we will display the AddFavoriteForm, a controlled component.

當使用者單擊“添加到收藏夾”按鈕時,我們将顯示AddFavoriteForm ,這是一個受控元件 。

We are constantly updating its state whenever a user changes the rating or input text in the comment text area. This is useful for validation upon form submission.

每當使用者更改等級或在注釋文本區域中輸入文本時,我們都會不斷更新其狀态。 這對于表單送出時的驗證很有用。

The RatingForm is responsible to render the yellow stars when the user clicks on them. It also informs the current rating value to AddFavoriteForm.

RatingForm負責在使用者單擊黃色星形時對其進行渲染。 它還會将目前評級值通知給AddFavoriteForm 。

收藏夾标簽 (Favorites Tab)

When a user clicks on the “Favorites” tab, the App renders FavoritesContainer.

當使用者點選“收藏夾”頁籤上,在App呈現FavoritesContainer。

The FavoritesContainer is responsible for retrieving the favorites list from the Redux store. It also dispatches actions when a user changes a rating or clicks on the “Remove” button.

收藏夾負責從Redux存儲中檢索收藏夾清單。 當使用者更改評分或單擊“删除”按鈕時,它還會排程操作。

Our MovieItem and FavoritesInfo are simply presentational components that receive props from FavoritesContainer.

我們的MovieItem和收藏夾資訊隻是表示性元件,可以從收藏夾 容器中接收道具。

We’ll reuse the RatingForm component here. When a user clicks on a star in the RatingForm, the FavoritesContainer receives the rating value and dispatches an update rating action to the Redux store.

我們将在此處重用RatingForm元件。 當使用者單擊RatingForm中的星形時 , FavoritesContainer會收到評分值,并向Redux存儲分派更新評分操作。

Redux商店 (Redux Store)

Our Redux store will include reducers that handle the search and favorites actions. Additionally, we’ll need to include a status reducer to track state changes when a user initiates an action. We’ll explore more on the status reducer later.

我們的Redux商店将包括處理搜尋和收藏夾操作的減速器。 另外,我們需要包括一個狀态減少器,以在使用者啟動動作時跟蹤狀态變化。 稍後,我們将進一步探讨狀态減少器。

//store.js
           
import { createStore, combineReducers, applyMiddleware } from 'redux';import thunk from "redux-thunk";
           
import search from './reducers/searchReducer';import favorites from './reducers/favoritesReducer';import status from './reducers/statusReducer';
           
export default createStore(  combineReducers({    search,    favorites,    status  }),  {},  applyMiddleware(thunk))
           

We’ll also apply the Redux Thunk middleware right away. We’ll go more into detail on that later. Now, let’s figure out how we manage the state changes when a user submits a search.

我們還将立即應用Redux Thunk中間件。 稍後我們将對此進行詳細介紹。 現在,讓我們弄清楚使用者送出搜尋時如何處理狀态更改。

搜尋減速器 (Search Reducer)

When a user performs a search action, we want to update the store with a new search result via searchReducer. We can then render our components accordingly. The general flow of events looks like this:

當使用者執行搜尋操作時,我們希望通過searchReducer用新的搜尋結果更新商店。 然後,我們可以相應地渲染元件。 事件的一般流程如下所示:

We’ll treat “Get search result” as a black box for now. We’ll explore how that works later with Redux Thunk. Now, let’s implement the reducer function.

現在,我們将“擷取搜尋結果”視為黑匣子。 我們稍後将探讨Redux Thunk的工作原理。 現在,讓我們實作reducer函數。

//searchReducer.js
           
const initialState = {  "title": "",  "year": "",  "plot": "",  "poster": "",  "imdbID": "",}
           
export default (state = initialState, action) => {  if (action.type === 'SEARCH_SUCCESS') {    state = action.result;  }  return state;}
           

The initialState will represent the data structure defined earlier as a single movie result object. In the reducer function, we handle the action where a search is successful. If the action is triggered, we simply reassign the state to the new movie result object.

initialState将表示先前定義為單個電影結果對象的資料結構。 在reducer函數中,我們處理搜尋成功的動作。 如果觸發了動作,我們隻需将狀态重新配置設定給新的電影結果對象。

//searchActions.jsexport const searchSuccess = (result) => ({  type: 'SEARCH_SUCCESS', result});
           

We define an action called searchSuccess that takes in a single argument, the movie result object, and returns an action object of type “SEARCH_SUCCESS”. We will dispatch this action upon a successful search API call.

我們定義了一個名為searchSuccess的動作,該動作采用單個參數(電影結果對象),并傳回類型為“ SEARCH_SUCCESS ”的動作對象。 我們将在成功執行搜尋API調用後排程此操作。

Redux Thunk:搜尋 (Redux Thunk: Search)

Let’s explore how the “Get search result” from earlier works. First, we need to make a remote API call to our backend API server. When the request receives a successful JSON response, we’ll dispatch the searchSuccess action along with the payload to searchReducer.

讓我們探讨一下早期的“擷取搜尋結果”的工作方式。 首先,我們需要對後端API伺服器進行遠端API調用。 當請求接收到成功的JSON響應時,我們會将searchSuccess操作和有效負載一起分派到searchReducer 。

Knowing that we’ll need to dispatch after an asynchronous call completes, we’ll make use of Redux Thunk. Thunk comes into play for making multiple dispatches or delaying a dispatch. With Thunk, our updated flow of events looks like this:

知道我們需要在異步調用完成後分派,是以我們将使用Redux Thunk 。 Thunk可用于進行多個排程或延遲排程。 使用Thunk,我們更新的事件流如下所示:

For this, we define a function that takes in a single argument

title

and serves as the initial search action. This function is responsible for fetching the search result and dispatching a searchSuccess action:

為此,我們定義了一個函數,該函數采用單個參數

title

并用作初始搜尋動作。 這個 函數負責擷取搜尋結果并排程searchSuccess操作:

//searchActions.jsimport apiClient from '../apiClient';
           
...
           
export function search(title) {  return (dispatch) => {    apiClient.query(title)      .then(response => {        dispatch(searchSuccess(response.data))      });  }}
           

We’ve set up our API client beforehand, and you can read more about how I set up the API client here. The

apiClient.query

method simply performs an AJAX GET request to our backend server and returns a Promise with the response data.

我們已經預先設定了API用戶端,您可以在此處閱讀有關我如何設定API用戶端的更多資訊 。

apiClient.query

方法僅向後端伺服器執行AJAX GET請求,并傳回帶有響應資料的Promise。

We can then connect this function as an action dispatch to our SearchContainer component:

然後,我們可以将此功能作為動作分派連接配接到我們的SearchContainer元件:

//SearchContainer.js
           
import React from 'react';import { connect } from 'react-redux';import { search } from '../actions/searchActions';
           
...
           
const mapStateToProps = (state) => (  {    result: state.search,  });
           
const mapDispatchToProps = (dispatch) => (  {    search(title) {      dispatch(search(title))    },  });
           
export default connect(mapStateToProps, mapDispatchToProps)(SearchContainer);
           

When a search request succeeds, our SearchContainer component will render the movie result:

搜尋請求成功後,我們的SearchContainer元件将呈現電影結果:

處理其他搜尋狀态 (Handling Other Search Statuses)

Now we have our search action working properly and connected to our SearchContainer component, we’d like to handle other cases other than a successful search.

現在,我們的搜尋操作可以正常運作并連接配接到SearchContainer元件,我們希望處理成功搜尋以外的其他情況。

搜尋請求待處理 (Search request pending)

When a user submits a search, we’ll display a loading animation to indicate that the search request is pending:

當使用者送出搜尋時,我們将顯示加載動畫,以訓示搜尋請求正在等待處理:

搜尋請求成功 (Search request succeeds)

If the search fails, we’ll display an appropriate error message to the user. This is useful to provide some context. A search failure could happen in cases where a movie title is not available, or our server is experiencing issues communicating with the OMDb API.

如果搜尋失敗,我們将向使用者顯示相應的錯誤消息。 這對于提供一些上下文很有用。 如果電影标題不可用,或者我們的伺服器遇到與OMDb API通信的問題,則可能會導緻搜尋失敗。

To handle different search statuses, we’ll need a way to store and update the current status along with any error messages.

為了處理不同的搜尋狀态,我們需要一種方法來存儲和更新目前狀态以及所有錯誤消息。

狀态減少器 (Status Reducer)

The statusReducer is responsible for tracking state changes whenever a user performs an action. The current state of an action can be represented by one of the three “statuses”:

statusReducer負責在使用者執行操作時跟蹤狀态更改。 動作的目前狀态可以用以下三種“狀态”之一表示:

  • Pending (when a user first initiates the action)

    待定(使用者首次啟動操作時)

  • Success (when a request returns a successful response)

    成功(請求傳回成功響應時)

  • Error (when a request returns an error response)

    錯誤(請求傳回錯誤響應時)

With these statuses in place, we can render different UIs based on the current status of a given action type. In this case, we’ll focus on tracking the status of the search action.

有了這些狀态後,我們可以根據給定操作類型的目前狀态來呈現不同的UI。 在這種情況下,我們将專注于跟蹤搜尋操作的狀态。

We’ll start by implementing the statusReducer. For the initial state, we need to track the current search status and any errors:

我們将從實作statusReducer開始。 對于初始狀态,我們需要跟蹤目前的搜尋狀态和任何錯誤:

// statusReducer.jsconst initialState = {  search: '',      // status of the current search  searchError: '', // error message when a search fails}
           

Next, we need to define the reducer function. Whenever our SearchContainer dispatches a “SEARCH_[STATUS]” action, we will update the store by replacing the

search

and

searchError

properties.

接下來,我們需要定義化簡函數。 每當我們的SearchContainer排程“ SEARCH_ [STATUS]”操作時,我們将通過替換

search

searchError

屬性來更新商店。

// statusReducer.js
           
...
           
export default (state = initialState, action) => {  const actionHandlers = {    'SEARCH_REQUEST': {      search: 'PENDING',      searchError: '',    },    'SEARCH_SUCCESS': {      search: 'SUCCESS',       searchError: '',          },    'SEARCH_FAILURE': {      search: 'ERROR',      searchError: action.error,     },  }  const propsToUpdate = actionHandlers[action.type];  state = Object.assign({}, state, propsToUpdate);  return state;}
           

We use an

actionHandlers

hash table here since we are only replacing the state’s properties. Furthermore, it improves readability more than using

if/else

or

case

statements.

我們在這裡使用一個

actionHandlers

哈希表,因為我們僅替換狀态的屬性。 此外,與使用

if/else

case

語句相比,它提高了可讀性。

With our statusReducer in place, we can render the UI based on different search statuses. We will update our flow of events to this:

有了我們的statusReducer ,我們可以根據不同的搜尋狀态呈現UI。 我們将事件流程更新為:

We now have additional searchRequest and searchFailure actions available to dispatch to the store:

現在,我們還有其他searchRequest和searchFailure操作可用于排程到商店:

//searchActions.js
           
export const searchRequest = () => ({  type: 'SEARCH_REQUEST'});
           
export const searchFailure = (error) => ({  type: 'SEARCH_FAILURE', error});
           

To update our search action, we will dispatch searchRequest immediately and will dispatch searchSuccess or searchFailure based on the eventual success or failure of the Promise returned by Axios:

為了更新我們的搜尋操作,我們将立即排程searchRequest ,并将根據Axios傳回的Promise的最終成功或失敗來排程searchSuccess或searchFailure :

//searchActions.js
           
...
           
export function search(title) {  return (dispatch) => {    dispatch(searchRequest());
           
apiClient.query(title)      .then(response => {        dispatch(searchSuccess(response.data))      })      .catch(error => {        dispatch(searchFailure(error.response.data))      });  }}
           

We can now connect the search status state to our SearchContainer, passing it as a prop. Whenever our store receives the state changes, our SearchContainer renders a loading animation, an error message, or the search result:

現在,我們可以将搜尋狀态狀态連接配接到我們的SearchContainer ,并将其作為道具傳遞。 每當我們的商店收到狀态更改時,我們的SearchContainer就會呈現加載動畫,錯誤消息或搜尋結果:

//SearchContainer.js
           
...(imports omitted)
           
const SearchContainer = (props) => (  <main id='search-container'>    <SearchInputForm       placeholder='Search movie title...'      onSubmit={ (title) => props.search(title) }    />    {      (props.searchStatus === 'SUCCESS')      ? <MovieItem          movie={ props.result }          ...(other props)        />      : null    }    {      (props.searchStatus === 'PENDING')      ? <section className='loading'>          <img src='../../images/loading.gif' />        </section>      : null    }    {      (props.searchStatus === 'ERROR')      ? <section className='error'>           <p className='error'>            <i className="red exclamation triangle icon"></i>            { props.searchError }          </p>        </section>      : null    }  </main>);
           
const mapStateToProps = (state) => (  {    searchStatus: state.status.search,    searchError: state.status.searchError,    result: state.search,  });
           
...
           

收藏夾減速器 (Favorites Reducer)

We’ll need to handle CRUD actions performed by a user on the favorites list. Recalling from our API endpoints earlier, we’d like to allow users to perform the following actions and update our store accordingly:

我們需要處理使用者在“收藏夾”清單上執行的CRUD操作。 從我們的API端點回憶起,我們希望允許使用者執行以下操作并相應地更新我們的商店:

  • Save a movie into the favorites list

    将電影儲存到收藏夾清單中

  • Retrieve all favorited movies

    檢索所有喜歡的電影

  • Update a favorite’s rating

    更新收藏的評分

  • Delete a movie from the favorites list

    從收藏夾清單中删除電影

To ensure that the reducer function is pure, we simply copy the old state into a new object together with any new properties using

Object.assign

. Note that we only handle actions with types of _SUCCESS:

為了確定reducer函數是純函數,我們隻需使用

Object.assign

将舊狀态與任何新屬性一起複制到新對象中。 請注意,我們隻處理_SUCCESS類型的動作 :

//favoritesReducer.js
           
export default (state = {}, action) => {  switch (action.type) {    case 'SAVE_FAVORITE_SUCCESS':      state = Object.assign({}, state, action.favorite);      break;
           
case 'GET_FAVORITES_SUCCESS':      state = action.favorites;      break;
           
case 'UPDATE_RATING_SUCCESS':      state = Object.assign({}, state, action.favorite);      break;
           
case 'DELETE_FAVORITE_SUCCESS':      state = Object.assign({}, state);      delete state[action.imdbID];      break;
           
default: return state;  }  return state;}
           

We’ll leave the initialState as an empty object. The reason is that if our initialState contains placeholder movie items, our app will render them immediately before waiting for the actual favorites list response from our backend API server.

我們将initialState保留為空對象。 原因是,如果我們的initialState包含占位符電影項目,那麼我們的應用将在等待來自後端API伺服器的實際收藏夾清單響應之前立即渲染它們。

From now on, each of the favorites action will follow a general flow of events illustrated below. The pattern is similar to the search action in the previous section, except right now we’ll skip handling any “PENDING” status.

從現在開始,每個收藏夾操作都将遵循以下所示的一般事件流。 該模式與上一節中的搜尋操作相似,除了現在我們将跳過處理任何“ PENDING”狀态。

儲存收藏夾動作 (Save Favorites Action)

Take the save favorites action for example. The function makes an API call to with our apiClient and dispatches either a saveFavoriteSuccess or a saveFavoriteFailure action, depending on whether or not we receive a successful response:

以儲存收藏夾操作為例。 該函數使用我們的apiClient進行API調用,并根據我們是否收到成功的響應,排程saveFavoriteSuccess或saveFavoriteFailure操作:

//favoritesActions.jsimport apiClient from '../apiClient';
           
export const saveFavoriteSuccess = (favorite) => ({  type: 'SAVE_FAVORITE_SUCCESS', favorite});
           
export const saveFavoriteFailure = (error) => ({  type: 'SAVE_FAVORITE_FAILURE', error});
           
export function save(movie) {  return (dispatch) => {    apiClient.saveFavorite(movie)      .then(res => {        dispatch(saveFavoriteSuccess(res.data))      })      .catch(err => {        dispatch(saveFavoriteFailure(err.response.data))      });  }}
           

We can now connect the save favorite action to AddFavoriteForm through React Redux.

現在,我們可以節省通過陣營終極版最喜歡的動作AddFavoriteForm連接配接。

To read more about how I handled the flow to display flash messages, click here.

要了解有關如何處理顯示Flash消息的更多資訊, 請單擊此處 。

結論 (Conclusion)

Designing the frontend of an application requires some forethought, even when using a popular JavaScript library such as React. By thinking about how the data structures, components, APIs, and state management work as a whole, we can better anticipate edge cases and effectively fix errors when they arise. By using certain design patterns such as controlled components, Redux, and handling AJAX workflow using Thunk, we can streamline managing the flow of providing UI feedback to user actions. Ultimately, how we approach the design will have an impact on usability, clarity, and future scalability.

即使使用流行JavaScript庫(例如React),設計應用程式的前端也需要一些周全的考慮。 通過考慮資料結構,元件,API和狀态管理作為一個整體的工作方式,我們可以更好地預測邊緣情況并在出現錯誤時進行有效修複。 通過使用某些設計模式,例如受控元件,Redux,以及使用Thunk處理AJAX工作流程,我們可以簡化對向使用者操作提供UI回報的流程的管理。 最終,我們如何進行設計将對可用性,清晰度和未來可擴充性産生影響。

參考文獻 (References)

Fullstack React: The Complete Guide to ReactJS and Friends

Fullstack React:ReactJS和Friends完整指南

關于我 (About me)

I am a software engineer located in NYC and co-creator of SpaceCraft. I have experience in designing single-page applications, synchronizing state between multiple clients, and deploying scalable applications with Docker.

我是位于紐約市的軟體工程師,也是SpaceCraft的共同建立者。 我在設計單頁應用程式,在多個用戶端之間同步狀态以及使用Docker部署可伸縮應用程式方面有豐富的經驗。

I am currently looking for my next full-time opportunity! Please get in touch if you think that I will be a good fit for your team.

我目前正在尋找下一個全職機會! 請取得聯系 ,如果你認為我會是一個很好的适合你的團隊。

翻譯自: https://www.freecodecamp.org/news/how-i-architected-a-single-page-react-application-3ebd90f59087/

react單頁應用權限控制