基於 react, redux 最佳實踐構建的 2048

前段時間 React license 的問題鬧的沸沸揚揚,搞得 React 社區人心惶惶,好在最終 React 團隊聽取了社區意見把 license 換成了 MIT。無論 React license 如何,React 都是一個值得好好學習的優秀視圖庫。javascript

本項目算不上什麼大型項目,但依然按照大型項目的標準採用前端流行的最佳實踐來打造一個有良好代碼質量,高性能,高可維護性,模塊化的應用。本項目是基於 react, redux 構建的 2048,此外也使用了近兩年優秀的開源工具來提升代碼質量,包括 eslintstylelintprettier 等等,以及 traviscodecov 等持續集成,持續部署等服務來保障代碼質量和提升開發效率。css

項目地址,喜歡的話 github 點個 star 支持下吧😘html

預覽

桌面端


screenshot

移動端


screenshot

特性

響應式

自適應桌面和移動平臺不一樣分辨率和尺寸,支持移動平臺瀏覽器觸控操做。下面的動圖模擬了不一樣分辨率下的顯示效果。實現方式主要是把 css 單位從 px 換成了 vw 和 rem ,各元素的尺寸是按照分辨率來進行縮放的。css 媒體查詢到移動瀏覽器的話,調整部分組件的位置,隱藏部分不重要的組件,使頁面更加緊湊。前端


screenshot

數據持久化

網頁應用最怕斷電和離線,第一個問題經過 store.subscribe 訂閱 redux 狀態更新,把狀態序列化到 localStorage 儲存,即便刷新,斷電,程序奔潰再次打開仍然是最新的狀態,第二個問題藉助 chrome 的 PWA 技術,即便斷開網絡仍然能夠訪問緩存的資源文件。java


screenshot

Redux 狀態

redux 是一個可預測的 JS 狀態管理容器,結合 Redux DevTools extension 擴展能夠很方便的進行應用狀態穿梭,對輔助開發和debug大有裨益。不只能夠查看 redux 保存的狀態,還能夠隨時回到到過去某個時刻的狀態就像時間穿梭機同樣,也看獲得 redux 每次 action 的觸發,以及每次觸發形成的狀態改動。node


screenshot

評論系統

藉助 github issue api,使用 github 帳號登陸以後以回覆 issue 的方式留言。留言支持 markdown 格式,和 github issue 體驗相似。react


screenshot

PWA

在支持 PWA 技術的瀏覽器上(好比較新的 chrome)打開頁面會自動詢問你添加到屏幕,添加過程就像原生應用的安裝同樣。應用添加以後就能夠像原生應用同樣離線操做,也能夠卸載應用。下圖演示了 PWA 在 chrome 上面的添加過程,添加完成以後桌面會出現添加的應用,即使關閉全部網絡仍然能夠像原生應用同樣正常操做。webpack


screenshot

i18n

應用支持多語言,且自動適配瀏覽器語言設置。目前檢測到瀏覽器支持中文優先使用中文,不然默認使用英文顯示。須要更多語言支持,編輯 src/utils/i18n.jsdata 對象,添加對應語言文字便可。git


screenshot

react 最佳實踐

  • 一個文件一個組件。
  • 儘可能使用無狀態(Stateless)組件,也就是若是隻是寫一個單純展現的組件,不須要組件保存本身的狀態,不須要生命週期方法或者 refs 來操做 DOM 的組件則優先使用無狀態組件,採用函數的形式。以項目 Tips 組件示例:es6

    import React from "react";
      import PropTypes from "prop-types";
      import styles from "./tips.scss";
    
      export default function Tips({ title, content }) {
        return (
          <div className={styles.tips}> <p className={styles.title}>{title}</p> <p className={styles.content}>{content}</p> </div>
        );
      }
    
      Tips.propTypes = {
        title: PropTypes.string.isRequired,
        content: PropTypes.string.isRequired
      };複製代碼
  • 和上面相反,若是你須要組件生命週期方法優化組件性能(典型應用,重寫 shouldComponentUpdate 方法),須要組件保存本身的狀態,或者用 refs 操做 DOM,你就須要一個有狀態組件,採用 es6 class 繼承 React.Component 的寫法。組件示例:

    import React from "react";
      import PropTypes from "prop-types";
      import classnames from "classnames";
      import styles from "./cell.scss";
      import { isObjEqual } from "../../utils/helpers";
    
      export default class Cell extends React.Component {
        static propTypes = {
          value: PropTypes.number.isRequired
        };
    
        shouldComponentUpdate(nextProps, nextState) {
          return (
            !isObjEqual(nextProps, this.props) || !isObjEqual(nextState, this.state)
          );
        }
    
        render() {
          const { props: { value } } = this;
    
          const color = `color-${value}`;
          return (
            <td> <div className={classnames([styles.cell, { [styles[color]]: !!value }])} > <div className={styles.number}>{value || null}</div> </div> </td>
          );
        }
      }複製代碼
  • 事件綁定 this 方法。在構造函數裏面綁定一次 this 以後後面就能夠正常使用。以 ControlPanel 組件部分代碼示例:

    constructor(...args) {
      super(...args);
    
      this.handleMoveUp = this.handleMoveUp.bind(this);
      this.handleMoveDown = this.handleMoveDown.bind(this);
      this.handleMoveLeft = this.handleMoveLeft.bind(this);
      this.handleMoveRight = this.handleMoveRight.bind(this);
      this.handleKeyUp = this.handleKeyUp.bind(this);
      this.handleSpeakerClick = this.handleSpeakerClick.bind(this);
      this.handleUndo = this.handleUndo.bind(this);
    }複製代碼
  • 使用 propTypes 屬性進行傳入 prop 的校驗。能夠校驗 prop 的類型和是否必需,非必需的 prop 還必需填寫 defaultProps 默認值。以無狀態組件 Button 的部分代碼示例:

    Button.propTypes = {
        children: PropTypes.oneOfType([PropTypes.node]),
        onClick: PropTypes.func,
        size: PropTypes.oneOf(["lg", "md", "sm", "xs"]),
        type: PropTypes.oneOf([
          "default",
          "primary",
          "warn",
          "danger",
          "success",
          "royal"
        ]).isRequired
      };
    
      Button.defaultProps = {
        children: "",
        onClick() {},
        size: "md",
      };複製代碼
  • 使用 HOC(Higher-Order Components) 代替 mixin。mixin 官方已經不推薦使用了,redux 的 connect 方法就是 HOC 的應用。
  • 爲了提升應用性能,避免沒必要要的視圖重繪,在須要的組件使用 shouldComponentUpdate 方法;以組件 Row 示例:
    // 若是該行沒有格子須要刷新也沒有組件本身的狀態刷新,
    // 則該組件不執行 render 方法,
    // 避免每次別的行數據刷新也跟着從新渲染。
    shouldComponentUpdate(nextProps, nextState) {
      return (
        !isObjEqual(nextProps, this.props) || !isObjEqual(nextState, this.state)
      );
    }複製代碼

項目結構

本項目是基於 Facebook 官方出品的 create-react-app 腳手架搭建的,reject 後作了適當修改以適配項目需求。

調整以下

  • webpack 添加 scss 支持。之因此沒有用 CssInJS 的方案是由於這些方案廣泛不完美,也考慮到要遵循樣式和結構分離的原則,scss 是目前比較成熟的 css 預處理器,社區輪子也比較多,開發起來很方便。推薦學習 scss/sass 教程。添加 sass-loader 到 scss 規則下面最下面便可。配置代碼
  • 開啓 css module 支持。在大型項目裏面組件之間須要儘可能解耦,可是 css 類名的全局特性很容易致使意料以外的錯誤。開啓 css module 以後,全部的類名最終都會被一小段 hash 值填充,因此類名也就有必定的惟一性,不容易污染全局的代碼。配置代碼
  • 添加 stylelint 支持。js 代碼已經有 eslint (但採用了更流行,校驗更嚴格的 airbnb 規則) 來檢查代碼,可是樣式代碼也須要保持代碼風格統一,同時校驗規則通常有社區的最佳實踐。配置代碼
  • 添加靜態資源 cdn 支持。因爲項目部署在 github page 在國內訪問速度不是很理想,因此在可能的狀況下儘可能減少 js 包的大小對頁面加載速度相當重要。像 ReactDOM 這類較大的 npm 包從打包文件剝離出去採用 CDN 來加載,可顯著減少打包文件的大小。(PS:之因此 CDN 加載比較快,是由於 CDN 提供商在全國各地都創建了緩存服務器,資源就近獲取比本身從 github 獲取快得多,並且通常 CDN 的帶寬也比較充裕)把 React 和 ReactDOM 剝離出去只須要在 html 文件添加 CDN 的 script 標籤,同時在 webpack 添加 externals 屬性,該屬性指定代碼 import 該包時直接從全局變量獲取。剝離後打包的 js 文件大小從 278kb 減少到 164 kb。
  • 添加 webpack 代碼壓縮插件。默認的 webpack 配置直接輸出原始的 js,css 代碼,但添加壓縮事後,文件顯著減少(js 文件從 164kb 到 49kb),對於移動瀏覽器來講打開速度獲得明顯提高。配置代碼
  • 添加 webpack-bundle-analyzer 插件,經過各模塊包所佔打包文件後的比重來分析項目代碼,藉此優化代碼。好比,React 和 ReactDOM 的剝離就是由於分析後發現這兩個包所佔比重較大。

文件結構

  • src, 項目源代碼大部分都在這裏,主要是 react 組件 js 代碼 和 scss 樣式代碼。次級目錄包含了 jest 單元測試代碼,測試代碼儘可能和源代碼挨着,以方便編寫。
    • assets,主要存放一些全局樣式代碼,icon svg 文件,遊戲音效 mp3 文件,圖片等等;
    • components,存放 react dumb 組件, 每一個組件包含在採用首字母大寫的目錄的 index.js 裏面,同時該目錄包含該組件用到樣式的 scss 文件,儘可能一個目錄包含該組件所需的全部代碼避免污染其餘代碼,提升組件複用性。
    • containers,存放 react smart 組件,該目錄結構和 components 相似,但由於是 smart 組件,因此這裏的組件能夠操做 redux 的數據,不用太考慮複用性。
    • reducers,這是 redux 包含的是無反作用的純函數式計算狀態操做的函數。
    • utils,包括評論組件初始化,i18n 多語言文件,移動瀏覽器滑動檢測和註冊 ServiceWorker 等等。
    • index.js,項目入口文件,主要把 react 根組件 渲染到指定 DOM 節點,而且註冊 ServiceWorker
    • store.js,redux store 初始化,同時 store.subscribe 訂閱應用狀態更新,序列化狀態存到 localStorage
  • public,包括項目的 html 文件,網站 icon favicon 和 PWA manifest 文件。
  • config,主要包括 webpack 的各類配置文件。
  • scripts,npm 的啓動腳本,啓動開發模式,項目打包,運行 jest 單元測試等等。
  • build,項目打包後的輸出目錄。
  • screenshots,README 各類圖片的原圖,爲了國內用戶訪問方便實際上 README 的圖片來自新浪微博的圖牀。
  • .editorconfig,通用的編輯器配置,統一不一樣編輯器 / IDE 的代碼格式。
  • .eslintignore,須要 eslint 忽略的文件或者目錄,規則相似 .gitignore
  • .travis.yml, 持續集成腳本,每次提交代碼到 github 以後,測試服務器都會自動運行該腳本執行測試用例,並輸出代碼覆蓋率,最後自動部署到 github page。全部狀態都在項目中 README 的徽章中可見。
  • package.json,項目基本信息和部分配置都存在這裏。常見的內容包括項目的各種依賴包,各類啓動腳本,項目 homepage 等等;爲了減小根項目的文件數目,jest,babel,eslint,stylelint 的配置也寫在這裏。值得注意的是,項目中引入 husky,在每次代碼 commit 以前都會執行 lint-staged,以自動執行 prettier 來美化代碼格式。每次代碼推送 到 github 以前也會執行全部單元測試用例,所有經過才能夠繼續推送。
  • yarn.locl,yarn 首次安裝依賴包以後生成的 lock 文件。經過 yarn 來安裝依賴包時,yarn 自動把項目的依賴包(包括依賴包依賴的父級包)固定在指定的版本(包括依賴包安裝的 url 和 hash 值),這樣全部開發環境都使用 yarn 來管理項目,不一樣的機器不一樣的系統安裝出來包都是同樣的,這樣就避免了以前 npm 的缺陷(版本要求太鬆或者父級包版本更新等等致使每次安裝出來的依賴版本不同)。

技術棧

  • react,組件式構建 UI
  • redux,管理應用狀態
  • babel,把 es2017+ 語法轉成 es5 兼容語法
  • webpack,代碼熱加載,scss 樣式文件處理,組件編譯打包等等
  • scss,成熟的 css 預處理器(之因此沒有用 CssInJS 的方案是由於這些方案廣泛不完美,也考慮到要遵循樣式和結構分離的原則)
  • eslint,使用流行的 airbnb 的代碼規範嚴格約束代碼風格
  • stylelint,scss 代碼風格檢查
  • jest,fb 出品的代碼測試框架,snapshot 功能對測試 react 組件 UI 十分方便
  • Prettier,js 和 scss 代碼格式美化工具
  • PWA(Progressive Web Apps),藉助瀏覽器 service worker 能力,使 web 應用在移動平臺有接近原生應用的能力,可離線使用,接收通知消息等等

運行 & 測試 & 打包

由於配置文件用了 es6+ 語法因此要求 node 的版本大於 6.10,同時建議使用 yarn 來管理依賴包。fork 項目以後能夠按以下命令操做。

npm i -g yarn # 安裝 yarn
  git clone git@github.com:<你的名字>/React-2048-game.git
  cd React-2048-game
  yarn # 安裝依賴包
  yarn start # 開啓調試模式,啓動後自動打開瀏覽器 http://localhost:3000 
  yarn test # 自動測試
  yarn build # 打包代碼複製代碼

踩坑記錄

  • 在調煙花動畫的時候發現沒效果,仔細對比了下 webpack 編譯後的 css 文件發現全部的 @keyframes 的名字都加了 hash 值(也就是當成普通的局部 css 類名),解決辦法就是在 @keyframes 的名字前面和整個 scss 文件添加僞類 :global,能夠參考煙花的 scss 文件,這不是完美的解決辦法(css 類名再也不有局部特性),後續再深挖一下。
  • css module 用到的 :global 這個不是標準的僞類,因此 stylelint 須要添加配置以忽略這個錯誤。參見 package.jsonstylelint.rules

項目地址,喜歡的話 github 點個 star 支持下吧😘

相關文章
相關標籤/搜索