基于 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 支持下吧😘

相关文章
相关标签/搜索