前言
最近正在奋力研究React中,被它的魅力深深的吸引了。 关于如何使用React和外部API搭建大型应用,看到一遍非常好的文章,于是乎翻译了一份,方便大家一起学习。
本文会一步一步演示如何搭建一个完整的React的项目,将要建立的项目名为:Clone Yelp 完整的代码在这里 试试效果吧
在本博文中,我们会涉及到这些内容:
- 如何从零开始搭建一个React项目
- 如何创建一个基础React组件
- 如何使用
postcss
编写模块化CSS - 如何React编写测试
- 如何利用
React-Route
做页面跳转 - 如何和Google地图集成
- 如何创建基于Google地图的React组件
- 如何创建五星React组件
在本文中,会运用React的很多属性和模块,手把手教大家搭建完整的React应用,不论你的前端知识了解多深! 好了,让我们系好安全带,准备起飞啦!!!
目录
环境配置
千里之行,始于足下。搭建开发环境以及创建项目的骨架子往往是完成整个项目最痛苦的一部分。我们将借助以下的一些工具帮助我们~
最终版本的配置文件:
package.json
和webpack.config.js
可以在这里找到哦!
真正的一步到位
虽然有一打的模板可以供我们使用,但是直接运用模板往往会让我们更加的困惑,得不偿失。在本文中,让我们亲自尝试构建所有的东西吧。我相信,做完这些以后,我们对如何从零开始创建React项目会有一个更加清楚明确的认识。
如果你想跳过这些复杂配置,你也可以使用yeoman generator工具获得完整的配置,并且直接跳到路由章节。
载入工具
1 $ npm install -g yo generator-react-gen
运行
1 yo react-gen
在整个开发过程中,我们将使用例如ES6,inline css modules, async module loading, test等相关知识。并且,我们使用Webpack作为我们的构建工具。
为了你能够顺利的起飞,请确保你的机器以及安装了Node.js,并且已经配置好了npm。 如果你对ES6不是很熟悉,我推荐阮一峰老师的入门教程。 同样的,Webpack的教程可以阅读这个系列:Webpack傻瓜式指南。
以下是我们的项目目录结构:
我们首先创建一个新的node
项目,打开我们的终端,创建主要目录结构。
1 | $ mkdir yelp && cd $_ |
在这个文件夹下面,我们使用npm init
来初始化我们的node
项目。初始化中会要求回答一些问题,填好答案完成初始化以后,会自动生成一个package.json
的配置文件,我们可以直接修改此文件更改项目配置。
如何回答初始化中的这些问题其实并不重要啦,我们可以随时更改
package.json
文件滴。
此外,可以使用
npm init -y
命令代替npm init
, 这样的话,所有的问题都会按照默认的答案完成初始化。
1 | npm init |
在我们项目运行起来之前,需要添加一些必要的依赖。
Babel
Babel 是一个 JavaScript 编译器,让我们可以使用下一代 JavaScript 语法编写代码,目前来说,也就是ES6啦。
同时,也需要安装一些babel的其他插件,例如es-2015deng:
1 | npm install --save-dev babel-core babel-preset-es2015 babel-preset-react babel-preset-react-hmre babel-preset-stage-0 |
我们需要添加一些配置,辅助babel更好的解析的我们的代码。非常简单,只要在根目录下新建一个.babelrc
文件并添加一些代码就可以啦。
1 | { |
Babel给我们提供了一个env
的属性,以便于我们可以针对不同的运行环境添加不同的配置。 在此,我们将只在开发环境下,引入babel-hmre
插件(我们在生产环境不需要热加载功能)。
1 | { |
让我们也给生产和测试环境也添加上预留上一些配置吧,方便以后添加:
1 | { |
webpack
原生的Webpack的配置和使用是相当的复杂的。不过不用担心,在这儿,给大家介绍一款配置webpack的神器hjs-webpack,它可以帮助我们省去很多的工作。
hjs-webpack 会帮我们把常用的生产和开发环境下需要的配置(loaders)工作做好,包括hot reloading, minification, ES6 templates 等。
让我们安装 webpack 和 hjs-webpack 吧。
1 | npm install --save-dev hjs-webpack webpack |
如果不添加一些插件和设置,webpack用处就比较小啦。让我们开始添加我们项目需要的插件吧,诸如babel-loader, css/styles等。 当然,也包括URL以及file loader(font-loading 以及内置在hjs-webpack了)。
1 | $ npm install --save-dev babel-loader css-loader style-loader postcss-loader url-loader file-loader |
React
既然是React应用,当然要引入React和它的一些依赖啦。和前面的那些不同,React以及相关的一些依赖仅仅是为了开发才需要的,而是我们的APP的组成部分。
1 | npm install --save react react-dom |
我们也需要使用React-Router来处理我们项目的路由和跳转。
1 | npm install --save react-router |
下载和保存依赖包的缩写命令:
1 | npm i -S [dependencies] |
那么,前面的命令可以写成这样:
1 | npm i -S react react-dom |
下载和保存用于开发的依赖包,只需要把-S
替换成-D
,比如:
1 | npm i -D [dependencies] |
创建 app.js
好了,我们可以开始建立我们的项目入口文件啦,就如前面webpack中配置的一样,并且建立一个顶层的容器,我们项目所有的流程以及和服务器的交互都会建立在此基础上。
首先,我们在 app.js
中简创建一个简单的React App。 新建 src/app.js
1 | touch src/app.js |
在这个文件里,让我们建立一个简单的React容器,在这个容器中仅仅包含一个输出一些文字的组件。首先,利用webpack引入一些必要的库:
1 | import React from 'react' |
演示: Basic app.js
Demo
Text Text Text
<App />
组件已经添加好了,为了让它能够显示在页面上,我们需要让它和页面的某一个Dom节点关联起来,那么,在哪里呢?
hjs-webpack
内置了一个简单的index.html
文件,如果不做特别的配置的话(在配置文件中修改html
属性, 这个文件会被自动加载。hjs-webpack
内置的这个文件完全满足我们的需要,所以我们不需要自己额外的配置一个HTML 文件。为了满足单页面应用的需求(简称SPA),在这个HTML 中,只包含了一个id为root
的div
节点。
这样的话,我们可以把<App />
组件和这个HTML中的Div关联起来啦,修改src/app.js
文件:
1 | import React from 'react' |
src/app.js
已经写好了,让我们准备跑起来试试吧。 hjs-webpack
自带了一个用于开发的node服务器,我们只需要运行一下命令就可以启动啦!
1 | NODE_ENV=development ./node_modules/.bin/hjs-dev-server |
设置node的环境变量NODE_ENV
是个非常好的习惯,就像我们这边做的一样。
服务器启动成功以后,会告诉我们它所监听的地址,我们只要通过这个URL就能访问我们的APP啦。打开我们的浏览器(我们会使用Google Chrome)并且访问http://localhost:3000
虽然看起来不怎么样,至少在我们的努力下,我们的项目算是初步跑起来了啦!
使用
Ctrl+C
来停止服务器
要启动我们的服务器,需要记住这一长串的命令,实在是不容易。我们可以在package.json
中添加一些配置,让启动过程变得简单好记!
package.json
允许我们在scripts
属性中,添加一些自定义的脚步,我们把启动服务器的命令添加到这里去吧。
1 | { |
做了以上的一些配置,现在我们只需要用npm run start
就可以启动我们的服务器啦。
针对于start
和test
脚步,package.json
做了一些特殊的定义,所以,我们可以直接省略run
关键字。
比如:
1 | npm start |
所有的其他脚步命令都不能省略run
哦!
postcss
这么难看的页面,肯定没人喜欢的,为了让我们的页面变得更加的漂亮,我们添加postcss和CSS modules 来帮助我们增加和管理样式!
PostCSS是CSS变成JavaScript的数据,使它变成可操作。PostCSS是基于JavaScript插件,然后执行代码操作。PostCSS自身并不会改变CSS,它只是一种插件,为执行任何的转变铺平道路。PostCSS可以作为预处理器或者后处理器,类似于lesscss和sass一样,当然,它的功能更加强大,详情的用法有机会我会另起博文介绍。
只要我们安装了autoprefixer,hjs-webpack
就会加载需要的loader,不需要我们在webpack中做任何额外的配置。
1 | npm install --save-dev autoprefixer |
我们也会添加两个PostCSS的插件,来帮助我们Postcss管理和维护项目的CSS。一个是precss package, 用来帮助做一些css的预处理。另外一个是cssnano,帮助处理用于生产环境的css的压缩等。
1 | npm i -D precss cssnano |
hjs-webpack 会自动给 autoprefixer 添加需要的配置,但是对于这两个插件并没有做任何的配置支持。所以,我们需要手动的添加一些配置来支持这个插件的运行。
很关键的,hjs-webpack帮我们创建好了一个webpack的配置对象。我们只需要扩展这个对象就能添加我们多需要的配置。
在webpack的配置中,postcss-loader
对应于一个叫做postcss
的属性。所以,我们只需要在属性内容的前面和后面增加配置,就如以下代码所显示的一样:
1 | // ... |
每一个postcss的插件都会输出一个返回postcss processor的方法,因此我们有机会改变这些配置
虽然在本项目中,我们没有在方法中添加任何的配置。
以下是这些插件的文档:
CSS modules
CSS modules可以使我们为不同的页面部分定义不同的生效范围CSS,从而避免了全局CSS所带来的混淆和难以维护。
在最终生成的CSS中,css module会为每一条css添加于其作用域和生命周期相关的名字,让我们来看看实际的代码。
对于某一个实例,假设我们有一个css文件,并且包含一个container
的类:
1 | .container { |
如果没有CSS modules,类名.container
将会是一个非常普通的名字,它将会被应用到所有带有container
类的Dom对象上。这样的话,可能会带来一些不必要的冲突,以及在项目后期会变得非常难以维护和扩展。CSS modules帮我们解决了这个问题!
为了在<App />
组件中使用.container
类,我们需要import文件和内容。
1 | import React from 'react' |
以上的样式会产生一个以css类名作为Key,以插件生成的一个特殊的css的类名作为Value的对象: 比如:
1 | "container" = "src-App-module__container__2vYsV" |
把生成好的特殊类名加入到组建的className
里:
1 | // ... |
演示: CSS Modules
Demo
Text Text Text
为了让CSS module工作起来,我们需要给webpack增加一些配置,配置有一点点复杂,慢慢来。
css modules的文档写的非常不错,可以从中学到很多css modules 的最佳实践。
我们可以使用postcss-loader
提供的一些选项来配置我们的css modules。我们需要告诉webpack我们准备给我们的css modules取什么样的名字。在开发环境下,我们需要让我们的css模块更加的通俗易懂,然而在生产环境中,并不需要。
在配置文件webpack.config.js
中,我们给css模块定义一个命名规则,如下:
1 | // ... |
hjs-webpack
帮助我们简化了webpack的配置,内置了css的loader。但是对于css modules来说,我们不仅仅需要添加一个用于load css modules的loader,而且也需要维护已经存在的那个。
让我们使用简单的正则表达式,在config.module.loaders
里找出已经配置好的那个loader:
1 | const matchCssLoaders = /(^|!)(css-loader)($|!)/; |
好在,在已经存在的loaders中找出了css loader,接下来,我们为css modules新建一个loader。
添加和维护已经存在的css loader,可以让我们很方便的应用全局的css样式。
回到webpack.config.js
, 让我们创建一个新的css loader:
1 | // ... |
在我们新的loader中, 仅仅去load在src文件夹中的css文件。 其他的css文件,比如font-awsome, 将会使用另外的一个不做css modules处理的css loader来加载。
1 | config.module.loaders.push({ |
Css loader配置好了以后,让我们创建一个全局的css文件 src/app.css
:
1 | echo "body{ border: 1px solid red;}" > src/app.cs |
可以在src/app.js
中载入这些样式了:
1 | // ... |
使用npm start
启动服务器,并且刷新浏览器,我们将看到预期中的页面:
为了验证我们的css modules已经可以很好地工作了,让我们在 src/
文件夹下面创建一个 styles.module.css
文件,并且添加一个 .wrapper
类:
1 | // In src/styles.module.css |
在 src/app.js
中引入刚刚创建的css modules 文件,并且把 .wrapper
类加入到 <div />
中:
1 | // ... |
启动服务器,并且刷新浏览器页面,让我们看看效果吧:
Configuring Multiple Environments
在我们的App中,我们需要和Google API 做集成。 把keys直接写在一个发布了的项目中是不值得推荐的,我们需要让我们能够配置不同的方式在不同的环境中,读取到这些些关键的keys。
处理这些keys的一个有效的方式就是使用系统环境变量和我们的key绑定在一起。借助于 webpack.DefinePlugin()
和 dotenv
, 我们可以建立一个支持多环境的机制:
首先, 我们需要加载 dotenv
依赖包:
1 | npm i -D dotenv && touch .env |
我们在项目的根目录下创建 .env
文件,在此文件中配置和我们项目有关的环境变量。借助于dotenv,我们可以配置一些脚本并且使得项目有权限读取这些变量。
使用 dotenv
读取环境变量是一件非常简单的事,让我们在 webpack.config.js
中引用 .env
文件:
1 | // ... |
一般来说,我们可以将全局环境变量保存在 .env
文件中。 为了区分不同的运行环境,我们将创建一个文件夹 config/
, 并且增加不同环境的配置文件 [env].config.js
。
为了加载这些配置文件,我们将会使用同一个方法。在 webpack.config.js
文件中,我们需要添加以下的一些方法:
1 | // ... |
我们可以将这两个对象合并到一起:
1 | // ... |
envVariables
变量现在包含了所有环境变量和全局的环境变量,为了将它们和我们的的App联系起来,我们需要授予项目访问这个变量的权限。
Webpack包含一部分通过的插件,包括 DefinePlugin()
, DefinePlugin()
可以在项目渲染到到浏览器之前,利用正则表达式替换某些对象的键-值。
按照惯例,我们给将要被替换的变量前后加上(--
),例如,我们通过__NODE_ENV__
来访问NODE_ENV
变量。
在我们的webpack.config.js
文件中,我们通过 reduce()
方法创建一个包含传统变量的对象:
1 | // ... |
然后,将 defines
对面作为配置参数传给 DefinePlugin()
, 再将配置告诉webpack。
1 | // ... |
好了,我们可以验证我们的配置是否成功啦,我们可以使用 <App />
组件来展示我们配置的变量。比如: 我们可以修改 render()
方法,来展示 __NODE_ENV__
变量的值。
1 | //... |
同样的,重启服务器,刷新页面,就可以看到效果啦。(我们是使用NODE_ENV=development
来启动我们的项目的)。
Font Awesome
我们将会使用Font awsome显示评分星星。引入font-awsome的大部分配置工作都在前面的章节中做好了,我们只需要安装一下就可以啦。
使用npm
安装:
1 | npm i -S font-awesome |
使用Font Awesome提供的图标,我们只需要在相应的节点上添加相应的类就行了,详细请参考官方文档。
将font awesome的css引入到我们的项目中非常简单。因为我们需要全局使用,所以我们可以在src/app.js
中引入:
1 | import React from 'react' |
验证一下,修改 render()
方法,给我们的环境变量添加一个小图标:
1 | // ... |
刷新浏览器,看看效果吧:
Webpack Tip: Relative requires
我们使用了webpack来管理我们的app,它也可以帮助我们更简单的管理相关联的其他需求。比如,我们可以设置一个alias,来替代那些需要根据相对路径引入一些其他依赖文件的方式。
让我们将目录 node_modules/
和 src/
命名为 root
,在添加一些其他的alias 对于我们已经创建的一些文件夹:
1 | var config = getConfig({ |
在我们的源代码里,我们可以直接使用 require(‘containers/some/app’)
来替代相对路径的引用了。
配置测试环境
React提供很多的测试方式,让我们用测试来保证我们的功能能够正常工作。我们甚至可以打开浏览器并且刷新页面(我们配置了热加载,刷新都可能不需要)来验证。
虽然,在开发中可以及时从浏览器得到反馈是非常不错的。但是为了我们能够方便以及覆盖面更加全面,写一些程序化测试时非常必要的,可以快速有效并且可靠的告诉我们项目是否如期工作。
在这个环节中,我们写的大部分代码都是测试驱动的,就是先写测试,然后在补全功能,这样可以确保我们的能够测试我们的代码。
React team 使用的是 jest测试框架,我们将会使用一些列工具的组合:
首先,让我们把这些都安装起来吧,并且安装一个ES6的依赖
1 | npm i -D mocha chai enzyme chai-enzyme expect sinon babel-register babel-polyfill react-addons-test-utils |
我们将会使用一个叫做 enzyme
的库,来帮助我们编写react测试变得更加简单和有趣。为了正确的使用它,我们需要添加一些webpack的配置。我们需要安装 json-loader
来load json格式的文件。(hjs-webpack以及帮助我们配置好了json-load 所需的修改,我们只要安装就行啦)。
1 | npm i -D json-loader |
我们将会使用karma来运行我们的测试,所以我们需要安装karma的一些依赖。使用karma是因为karma和react有很好的集成,当然咯,还是需要一些配置的。
让我们安装这些依赖吧:
1 | npm i -D karma karma-chai karma-mocha karma-webpack karma-phantomjs-launcher phantomjs-prebuilt phantomjs-polyfill |
我们将会使用 [phantomJS] 来测试我们的文件。这样就不需要打开浏览器新窗口了。 PhantomJS是一个轻量级的,Webkit驱动的,使用JS接口的,可以使用我们在后台运行的模拟浏览器。
当然咯,如果你更喜欢使用Google Chrome来运行测试的话,可以将 ·、
karma-phantomjs-launcher
替换成karma-chrome-launcher
.
可以去喝一杯咖啡等待安装完成了,这需要几分钟的。 一旦安装完成,我们需要创建两个配置文件,一个用来配置karma,一个用来配置我们的测试。
让我们通过karma来建立我们的webpack测试环境吧。最简单的方式就是使用karma提供的 karma init
命令。首先,我们需要安装karma-cli,以及一些相关依赖。
1 | npm install -g karma-cli |
安装完成以后,可以运行karma init 了。
1 | $ karma init |
回答一些问题以后,命令会帮忙生成一个 karma.conf.js
的配置文件。文件中的大部分内容我们都会去修改,所以,在这里,可以一路按确定键了。
我们也可以自己创建这个配置文件
1 $ touch karma.conf.js
配置文件创建好了,我们需要去添加一些配置选项,大部分的配置都自动添加好了。
1 | module.exports = function(config) { |
我们需要告诉karma,我们将会使用mocha和chai作为我们的测试框架,所以我们需要修改frameworks: []
属性,也需要添加支持这两个框架的插件:
1 | module.exports = function(config) { |
我们使用了webpack打包我们的所有文件,所以,我们也需要告诉karma我们的webpack配置。
1 | var webpackConfig = require('./webpack.config'); |
也需要告诉karma如何运用webpack的配置,我们可以给plugins添加一个webpack的插件:
1 | module.exports = function(config) { |
让我们修改一些可以让测试变得更加友好的东西吧。使用spec-reporter
替换默认的 progress
,将浏览器从chrome
改为PhantomJS
.
首先, 让我们安装spec reporter:
1 | npm i -D karma-spec-reporter |
回到配置文件,将插件添加并且更改浏览器:
1 | module.exports = function(config) { |
最后,我们需要告诉karma去哪儿找到测试的源文件。我们不直接告诉karma,而是采用一个中间件,告诉webpack我们的测试文件在哪里,然后再又webpack的配置告诉karma去哪儿找到打包好的测试文件。
1 | module.exports = function(config) { |
在这儿暂停一下,我们需要让kamar知道,需要从webpack中寻找测试文件tests.webpack.js
。并且告诉它,需要使用sourcemap 预处理器来运行测试,这样的话可以便于我们调试。
1 | module.exports = function(config) { |
让我们创建 tests.webpack.js
文件,这个文件将会作为中间件联系webpack和karma。Karma将会更加这个文件加载所有被webpack打包好的测试文件。
这个文件内容很简单:
1 | require('babel-polyfill'); |
当karma运行这个文件的时候,它将会在src/
文件加下面寻找所有以 .spec.js
结尾的文件,并且将他们当做测试文件执行。在这里,我们可以创建任何我们想用到全局的帮助方法。
既然我们想使用全局使用一个叫做 chai enzyme
的帮助库,我们可以在这里配置他们:
1 | require('babel-polyfill'); |
好了,到目前为止,我们的karma配置文件就是这个样子了:
1 | var path = require('path'); |
我们差不多做好了所有的Karma测试的环境配置了,然是我们少了最后两步。在完成karma的配置之前,让我们创建一个简单的测试样例,来验证我们的配置是否完成吧。
我们将使用我们前面所创建的 <App />
组件作为我们项目的根组件吧。我们需要创建一个 containers/App/App.spec.js
测试文件。
1 | mkdir src/containers/App && touch src/containers/App/App.spec.js |
在这个测试文件中,我们将添加一个简单的测试,来测试我们的节点上是不是存在一个叫做 wrapper的样式。
这个测试仅仅用于验证我们的配置,所以我们只需要借助mocha和chai做简单的测试:
1 | import React from 'react' |
在后面的内容中,我们会详细讲解如何测试我们的项目。这儿,你可以直接将测试代码复制到你的文件中去。
为了让测试跑起来,我们需要新建两个文件。 src/containers/App.js
和css modules 的文件 src/containers/styles.module.css
. 我们不需要使得我们的测试通过,仅仅让他们跑起来。
让我们创建一个 App.js
文件,并且将 scr/styles.modul.css
移动到container文件夹下面:
1 | $ touch src/containers/App/App.js |
并且,把src/app.js
中的 <App />
的定义移动到新建的文件中去 src/containers/App/App.js
:
1 | import React from 'react' |
最后我们需要在src/app.js
中 引入 <App />
组件:
1 | // ... |
使用以下命令运行我们的测试:
1 | NODE_ENV=test \ |
噢,天哪!什么东西出了错!别担心,这在预料之中的。。。
这个错误告诉我两件事。第一,webpack试图找到我们的测试框架并且和我们的测试绑定在一起。Webpack希望通过分析一个静态文件找到我们所有的依赖以及我们的源代码,然而,enzyme使用了一些动态的文件,所以就是失败咯。
显然,我们不希望这么做,因为我们并不需要给生产环境添加任何的测试框架。我们可以主动的告诉webpack让它忽略我们的测试框架,并且假设我们可以将它设置为一个_外部_依赖!
在我们 webpack.config.js
文件中,让我们添加一些enzyme需要的外部依赖:
1 | // ./webpack.config.js |
第二个错误是,有一些生产环境的插件和我们的测试混淆在一起没法区分。这样的话,我们需要在测试环境下,不加载那些插件。所以,让我们给测试环境做一些特殊的设置。
首先,我们可以通过 NODE_ENV
或者 是否运行在 karma
中来判断是否为测试环境。 让我们在 webpack.config.js
最上面添加一个 isTest
的标记吧:
1 | require('babel-register'); |
这样的话,我们就可以在我们的配置文件中,添加一些只在测试环境需要的配置啦。
将我们刚刚添加的那些_外部_依赖移动到测试里去吧,更新后的 webpack.config.js
文件如下:
1 | // ./webpack.config.js |
现在,我们再次运行我们的测试,我们只是会得到没有通过的提示啦:
1 | NODE_ENV=test \ |
下面,让我们致力于让测试通过吧。
首先,让我们把在
package.json
中添加运行测试的脚步吧,省的我们每次都要输入一串长长的命令:1 | { |
这样的话,我们只需要运行 npm test
就可以启动我们的测试啦:
1 | npm test |
我们前面的测试没有通过是因为,虽然我们定义了一个 .wrapper{}
类,但是这个类确实空的,所以webpack会忽略它。那么通过 styles.wrapper
引用到的就是 undefined了。所以,我们只需要给类添加一些内容:
1 | .wrapper { |
同时,我们也把 src/styles.module.css
中的内容清楚吧。
在我们的App中,我们将会使用 flexbox 布局,所以我们可以添加 display: flex;
到我们css 类的描述中。
这次,我们使用 npm test
再次运行我们的测试,所有测试都通过啦!
来回在我们的编辑器和终端之间切换,是一个很烦的事情。如何当我们修改了代码,测试能自动更新并且报告错误的话,那就好了!非常幸运,karma提供了非常简答的方式。
只需要在我们的测试启动脚本后面,加上 --watch
就行啦,让我们同样把他加到 package.json
中去吧:
1 | { |
如此,我们需要使用 npm run test:watch
来代替 npm test
启动我们的测试。我们也需要告诉karma(通过配置 karma.conf.js
文件),让它帮忙监听文件的修改。
Karma 使用配置对象中的 singleRun
属性来处理这个。我们将借助与Node的 yargs
来实现:
1 | npm i -D yargs |
在 karma.conf.js
文件中,我们可以使用 singleRun
以及 yargs
来检查 --watch
标记。
1 | var argv = require('yargs').argv; |
好了,当我们运行 npm run test:watch
命令。修改和保存了一个文件以后,我们的测试就会被触发。让我们的测试驱动开发,非常方便!
建立测试骨架
让我们先把我们app的地基建立起来吧。我们的 <App />
容器将会包含我们的所以页面,并且负责负责页面间的跳转和路由。
作为一名优秀的开发者,让我们首先创建测试,来确保我们的实现了我们的设想。首先,我们要确保我们有一个 <Router />
组件。
我们也可以顺便构建我们的测试结构。
在 src/containers/App/App.spec.js
中,让我们开始创建我们的测试样例。首先,我们要引用我们的库以及要测试的源文件:
1 | import { expect } from 'chai' |
我们将会是用 expect()
来确保我们的测试期望。使用 shallow()
(来自 enzyme) 来渲染被测试的元素到我们的测试浏览器中。
测试模式
无论使用是什么语言,测试什么样的代码,我们的原则都是相同的。我们希望:
- 定义核心功能
- 设置一个期待结果
- 对比运行结果和期待结果
在Jasmine中,以上的步骤都非常容易实现:
- 使用
describe()/it()
来定义核心功能 - 使用
expect()
来设定期待输出结果 - 使用
beforeEach()
以及 matchers 来验证输出
在我们的测试中,需要假装 <App />
已经在浏览器中渲染好了。Enzyme 可以轻松的做到这一点,无论是深度渲染还是浅渲染(后面将会涉及这两点的区别)。
1 | // ... |
我们可以开始测试 <App />
里是否有一个 <Router />
组件啦,通过使用 find()
方法在我们的wrapper实例中查找router。
1 | // ... |
然后,我们就可以跑起来试试啦:
因为我们还没有实现 <App />
组件,所以,我们的测试挂啦!
路由
在实现routes之前,让我们快速的谈谈如何建立我们的路由吧!
我们的React App,可以通过 children
组件的特性来控制我们的路由。不同的子组件对于不同的路由。 我们有一个标题栏,我们希望它可以一直在页面上显示,每次更换路由,仅仅改变标题栏以下的页面内容。
我们将会在App中创建一个 <Router />
组件,根据不同的路由渲染这个组件不同的内容,该如何显示如何切换都又路由内容决定。<App />
将会变成一个非常的容器,仅仅用来安放 路由组件,不在显示和处理任何的页面元素。
在 src/containers/App/App.js
中,我们首先确实我们引入了 react-router
库:
1 | import React,{ PropTypes } from 'react' |
然后,按照惯例,让我们来创建React组件吧(无论是使用createClass({})
,还是ES6 提供的继承。
1 | import React, { PropTypes } from 'react'; |
我喜欢使用 getter/setter 方法来创建class类容,这仅仅是个人习惯
1 | import React, { PropTypes } from 'react' |
我们将会使用app 容器返回 <Router />
组件的一个实例。<Router />
组件需要我们传入一个记录历史记录的对象,以此来告诉我们浏览器如何监听浏览历史和地址变化。历史记录会告诉我们的react如何跳转。
目前,有许多种不同的记录历史记录的方式可以供我们使用,最流行是两种, browserHistory
和 hashHistory
。 browserHistory
使用的是原生的HTML5 react路由,表现形式和服务器路由一样。
另外一种,是使用 #
来标记不同的浏览记录。基于浏览器哈希的方式,一个支持所有浏览器的客户端路由的老把戏。
我们将会使用 browserHistory
的方式。我们将 browserHistory
作为一个属性传给<Router />
组件以此来告诉它我们的决定。
1 | import React, { PropTypes } from 'react'; |
路由的基础差不多搭好了,我们只需要传入我们自定义的路由就行啦。
1 | import React, { PropTypes } from 'react'; |
为了真正的使用上我们的路由,当我们渲染 <App />
组件的时候,我们需要告诉它路由需要的两个信息:
- history: 我们将会在
react-router
中引入browserHistory
对象,并且直接传给App。 - routers: 我们会传入一个 JSX。
回到 src/app.js
文件,我们直接传入引入的 history:
1 | import React from 'react' |
然后,我们需要建立一些路由规则。首先,我们需要在浏览器中得到一些信息。让我们根据我们要显示的页面,建立一个独立的路由吧(我们很快就会改变它)。
我们需要访问这些组件:
<Router />
组件<Route />
组件- 自定义的路由组件
Router
以及 Route
组件可以直接从 react-router
中引入:
1 | import React from 'react' |
我们可以创建这两个组件的JSX实例来实现自定义的路由:
1 | // ... |
因为我们没有创建 Home
组件,所以前面的例子是错误的,让我们来创建一个简单的组件,让上面的代码能够工作:
1 | // ... |
最后,我们可以将 routes
对象传给 <App />
组件啦,然后刷新浏览器,如果一切都没用搞错的话,我们将会在页面上看到 “Hello World”啦。
同样的,我们会发现,测试也通过了!
建立真实路由表
到目前为止,我们仅仅给我们的项目创建了一个用于演示的单独的路由。让我们把路由分割到单独的文件吧,这样可以使得我们的项目结构更加清晰。
1 | touch src/routes.js |
在 src/routes.js
文件中,让我们创建并且输出一个用于创建不同路由的方法。将 src/app.js
中的一部分代表搬移过来,并且做一些修改:
1 | import React from 'react' |
我们可以在 src/app.js
文件中用输出的 makeRoutes
方法替代 routes
的声明!
1 | mport React from 'react' |
我们可以看看测试结果,来确保我们的修改是否可以正常的工作。
主页以及其嵌套路由
路由建立好了,我们开始创建我们的主页吧。这个页面用来展示我们的主要地图,并且列出一些餐厅。这就是我们的主页。
因为我们将会搭建一个复杂的项目,我们会将路由切分开来,让需要使用到的组件自己去维护自己的路由。换句话说,我们的主页的视图仅仅包含它的子路由,而不是一个巨大的路由文件,嵌套的组件将会创建他们自己的视图。
让我们在根目录下创建一个新的文件夹 views
, 并且按照路由的名字来命名子文件夹,目前,我们要创建一个 Main/
路由的视图:
1 | mkdir -p src/views/Main |
在这个 Main/
文件夹里,创建两个文件:
- routes.js : 用于定义
Main/
视图下的路由规则 - Container.js : 用于定于路由本身容器中的文件
1 | touch src/views/Main/{Container,routes}.js |
让我们在 src/views/Main/routes.js
中为视图容器创建一个简单的路由。
1 | import React from 'react' |
当我们在主路由文件中,引入刚刚创建的这个路由时,我们需要创建一个子元素。但是,我们现在仅仅需要验证我们的机制是否正确,我们可以创建一个非常简单的内容,代码如下:
1 | // in src/views/Main/Container.js |
定义好了内容,让我们回到 src/routes.js
文件载入这个新的子路由。 因为我们期待的是一个方法而不是一个对象,所以我们要确保我们展示的是这个方法的返回值而不是方法本身。
修改最初的 src/routes.js
文件,移除 Home
组件,并且引入我们的新的路由,我们的文件变成了这样:
1 | import React from 'react' |
定义了子路由以后,我们应该很少会去修改主路由了。我们可以根据相同的步骤,创建其他的顶级路由。
演示: Routing to Container Content
Demo
Hello from the container
Maps 的路由
让我们把 <Map />
组件添加到我们的页面上来,在前面的文章中,我们讲解了如何创建一个google API 的地图组件。我们可以将这个地图模块用到我们的项目中。
让我们安装这个 npm
吧:
1 | npm install --save google-maps-react |
在我们的 <Container />
组件中,我们将这页面上放一个隐藏的地图组件。这个地图组件将会加载Google APIs, 并且创建一个Google 地图的实例。我们会将这个实例交给所有的子组件使用,但是本身不会再页面上显示。因为我们将需要显示一系列的地址信息但是不要地图在页面显示出来。
在我们可以使用 <Map />
组件之前,我们需要申请一个Google API key。在本文的第一章节中,我们使用 WebpackDefinePlugin()
来处理存放这些key的相关程序。 所以我们可以将我们的Google Api key 放在 .env
中:
1 | GAPI_KEY=abc123 |
在项目中,我们通过访问 __GAPI_KEY__
来读取这个key。
下面一步,我们需要告诉google Api 插件,我们的key:
1 | import React from 'react' |
现在,当我们在页面上载入 <Container />
组件,我们可以看到我们的google api 实例被加载了:
既然,Google API 已经加载了,我们可以将 <Map />
组件加入到 <Container />
组件中了。
1 | import React from 'react' |
获取位置清单
经过我们的努力,终于把地图加载到我们页面上了,让我们使用google api 显示一个很多位置的列表吧。当 <Map />
组件在页面上加载好了以后,回调函数 onReady()
会被触发。我们将会利用这个函数来向google api 做一些请求。
让我们修改 src/views/Main/Container.js
文件, 定义一个 onReady()
的实现:
1 | export class Container extends React.Component { |
在这儿,我们可以直接使用Google Api 而不用做任何其他的设置。我们先创建一个帮助方法来运行google api 的命令。让我们在 src/utils
文件夹下面创建一个新的文件 googleApiHelpers.js
. 我们可以把所有的和google api 有关的方法和命令都放在这儿,便于管理,并且返回一个promise。
1 | export function searchNearby(google, map, request) { |
现在,我们可以在 onReady
方法里调用google api的这个帮助方法了:
1 | import {searchNearby} from 'utils/googleApiHelpers' |
因为我们要改变 <Container />
组件的状态,便于我们将api的结果更新给组件,所以:
1 | export class Container extends React.Component { |
现在,当请求到google api的结果以后,我们可以更新在组件的state里面了,让我们更新 onReady()
方法吧:
1 | export class Container extends React.Component { |
有了这些state了,我们可以再次更新我们的 render()
方法:
1 | export class Container extends React.Component { |
演示结果如下:
在本章节中,我们将为我们的项目添加一个侧边栏,让他更加的潮流一些。
为了实现这一部分,我们将增加一些内联样式并且使用react 原生的数据流。
首先,让我们安装一个叫做 classnames 的npm 模块。它的说明文档 非常详细的阐述了这个模块的用法和主要功能,这里就不详述了:
1 | $ npm install --save classnames |
现在,让我们跳出App,开始进入到组件的内容中来吧。首先,我们要创建一个 <Header />
组件。
因为我们创建的是一个共享的组件(并不是那种只用在特定页面的),通常比较推荐的存放地点是 src/components/Header
文件夹,让我们来创建这个文件夹以及相关的测试文件吧:
1 | $ mkdir src/components/Header |
我们的 <Header />
非常简单。就是用来显示我们项目的名字和一些导航菜单。按照惯例,我们首先创建测试。
在 src/components/Header.spec.js
文件中,让我们来创建测试:
1 | import React from 'react' |
测试也比较简单,我们仅仅期待这个组件包含一些文本:
1 | import React from 'react' |
这时候运行测试,肯定是挂的,因为我们根本还没有实现任何可以使得测试通过的功能。
所以,让我给组件添加一些代码吧:
1 | import React from 'react' |
再次运行测试,就能通过啦:
然而,刷新浏览器,并没有看到我们所期待的东西。让我们确保,我们在我们的顶级容器中包含了这个新建的组件:
在 src/views/Main/Container.js
中:
1 | // our webpack alias allows us to reference `components` |
再次刷新浏览器:
内联样式
我们可以直接给 <Header />
添加样式。不使用CSS Modules的话,我们可以创建一个特殊css标识,引入一个全局css 文件,并且将它应用到 <Header />
组件中去。既然我们使用了css modules,那么让我们创建一个css modules 文件来处理 header
相关的样式吧。
让我们在 header
文件夹下面创建一个后缀为 .module.css
的css文件吧:
1 | $ touch src/components/Header/styles.module.css |
让我们创建一个名为 topbar
的简单类,并且添加一些描述:
1 | /* In `src/components/Header/styles.module.css `*/ |
我们可以引入这个文件来使用这个样式:
1 | import React from 'react' |
将 styles.topbar
类添加到 <Header />
组件以后,我们可以看到,我们的标题栏被边框框起来啦、
让我们继续添加一些样式,让标题栏变得更加漂亮:
1 | /* In `src/components/Header/styles.module.css `*/ |
并且在 app.css
中添加一些全局样式(一些全局起作用的样式)。
1 | *, |
现在,我们的标题栏终于说的上漂亮啦。但是,给标题栏添加了一些样式以后,我们的主要内容被遮掉了一些。我让我们给主页添加一些样式来修复吧。
同样的,我们可以给 <Container />
组件创建一个css modules 样式文件。
1 | $ touch src/views/Main/styles.module.css |
再添加一些css:
1 | .wrapper { |
给内容设置
flex: 2
是为了让它去两个元素的最大者(sidebar vs content),关于这个后面会有更详细的描述
同样的,在组件中加载这些样式:
1 |
|
CSS 变量
我们注意到,上面的css中有很多值是写死的,比如说 topbar
中的高度属性。
1 | /* In `src/components/Header/styles.module.css `*/ |
PostCSS 中,非常有价值的一部分功能就是可以在我们的css中使用变量。 在CSS中使用变量有好几种的解决方案,我们将会使用postcss 所提供的方法。
我们可以在变量前面加上两个 -
作为前缀定义变量。比如:
1 | .topbar { |
注意: 当我添加一个自定义的属性(或者是变量)的时候,我们也要遵循css的语法规则。
这样的话,我们就可以在后续的css中使用这个变量了。用 var()
方法调用变量,比如说:
1 | /* In `src/components/Header/styles.module.css `*/ |
虽然这样已经很好了,但是我们仍然只能在 .topbar
类中使用这些变量。为了可以在其他的样式里面也是用这个变量,比方说,在 content
类中使用,我们需要把变量放在更高层次的DOM结构中。
比如说,我们可以把变量定义在 wrapper
类中。但是,postcss 有一个更加聪明的办法,我们使用 :root
选择器将变量放在一个根节点中。
1 | /* In `src/components/Header/styles.module.css `*/ |
我们可以创建一个通用的css文件,把css变量都放在里面,是的所有的css都可以使用到我们定义的变量,比如,创建一个文件 src/styles/base.css
:
1 | $ touch src/styles/base.css |
然后,我们将 :root
定义搬到 base.css
中:
1 | :root { |
在我们其他的css modules中使用这些变量,首先要引入这个文件。
回到我们 src/components/Header/styles.module.css
, 我们可以将 :root
定义替换为引用文件:
1 | @import url("../../styles/base.css"); |
现在,我们有一个可以统一修改高度的地方啦。
组件的分割和设计
标题栏创建好了,让我们继续我们的主页面。我们的项目包含一个侧边栏和一个根据内容变化的主要页面。就是说,我们的页面首先显示的是一张地图,当用户点击地图上某一个点的时候,我们将会更新页面内容,显示用户点击的地点的详细信息。
让我们先从显示位置列表的侧边栏开始吧,其中最艰难的工作我们已经完成了。 React 推荐的方式,我们定义一个组件来包含多个相同的组件。简单地说,就是我们将会建立的 <Listing />
组件用来列举一个个单独的一项。这样的话,我们可以集中关注在每一列的内容上,而不用考虑更加高层次的东西。
让我们从我们简单的列表扩展到一个 <Sidebar>
组件。因为我们创建的这个组件是可以共享的,所以将它放在 src/components/
,同时创建CSS,
1 | $ mkdir src/components/Sidebar |
在我们的 src/views/Main/Container.js
中:
1 | import Sidebar from 'components/Sidebar/Sidebar' |
当然,我们也需要传一些属性给 <Sidebar />
组件。它的职责就是列举我们的位置,所以组件需要知道我们从google API 拿到了哪些位置。这些信息现在存放在 <Container />
组件的state中。回到这个文件:
1 | import Sidebar from 'components/Sidebar/Sidebar' |
现在,让我们开始添加侧边栏组件的内容吧:
1 | import React, { PropTypes as T } from 'react' |
添加一些样式:
1 | @import url("../../styles/base.css"); |
到目前为止,我们的 <Sidebar />
组件基本建立好了,但是仅仅是渲染了标题信息。我们暂时就不再花更多时间在这个组件上,让我们继续进入到一个新的 <Listing />
组件中去。
我们可以直接在
<Sidebar>
里面列举位置清单,但是这不是 React 推荐的方式。
让我们创建一个 <Listing />
组件来列举每个地点。我们将利用另一个附加的组件 <Item />
来显示每一个单独的地点。
1 | $ mkdir src/components/Listing |
既然我们是遵从测试驱动开发的模式,那么让我们首先关注 Listing.spec.js
文件吧。我们对这个组件的期望是,有预期中的样式,并且包含一些列的 <Item />
组件。所以,测试应该是这样:
1 | import React from 'react' |
让我们继续,并且完善我们的测试。
1 | import React from 'react' |
Business Listing 组件
<Listing />
组件的测试已经写好了,让我们来补充我们的组件,让测试通过。和 <Sidebar />
类似,有一个样式文件需要载入,并且需要包装一个 <Item />
组件。
也非常简单:
1 | import React, { PropTypes as T } from 'react' |
并且创建样式:
1 | .container { |
组件中使用到了 <Item />
, 所以我们也需要创建 <Item />
组件。
Item 组件
对于 <Item />
组件,我们期待它能够显示地点的名称,并且能够显示一个评分。当然,样式也是必不可少的。所以,在我们的 src/components/Listing/Item.spec.js
测试文件中:
1 | import React from 'react' |
并且将测试内容填好:
1 | // ... |
显而易见,我们还会创建一个处理从google api 得到的关于地点评分信息的组件 <Rating />
。但是,我们现在先让测试通过吧。
在 src/components/Listing/Item.js
中,我们的 <Item />
组件不用关心如何去得到一个地点的信息,这些信息都又上一个组件传递下来了。组件需要显示一个标题,显示一个评分。
1 | import React, { PropTypes as T } from 'react' |
为了能够对 <Item />
组件添加样式,让我们增加一个 .item
类吧。
1 | /* ... */ |
好了,回到我们的浏览器,你可以看到:
Rating 组件
让我们使评分更加的漂亮一些,使用星星来代替数值的显示。我们准备把这个组件放在 src/components/
目录下面,让我们来创建必要文件吧:
1 | $ mkdir src/components/Rating |
添加测试:
1 | // ... |
根据测试,创建我们的组件,在文件夹 src/components/Rating/Rating.js
中:
1 | import React, { PropTypes as T } from 'react' |
<RatingIcon>
组件不需要依赖任何数据的注入,仅仅是显示一个*
用样式来控制显示 *
的个数。
1 | export class Rating extends React.Component { |
如果没有添加样式,看起来就不像一个评分了。
让我们来添加样式,分离底层和顶层的显示
1 | .sprite { |
在继续前进之前,让我们确保我们将颜色都变成都变量。这样的话,我们可以保证,在我们的整个项目中,所有的颜色都能够保持一致。创建一个 src/styles/colors.css
文件来保存所有的这些颜色变量。
在 src/styles/colors.css
,
1 | :root { |
更改 <Rating />
组件的内容:
1 | @import "../../styles/colors.css"; |
现在,我们的 <Sidebar />
总算完成了!
创建地图主页
我们的 <Sidebar />
组件实现了,现在让我们来实现我们的主要元素吧,就是我们的地图和地图的详细信息。因为,我们希望这些页面都是可以被点击的,也就是说,我们希望用户可以复制和粘贴URL地址。所以,我们会根据URL来建设我们的主页。
我们需要修改我们的路由,使得它使用这些地图元素的跳转。目前,我们的路由只是设置了一个单个路由,显示一个 <Container />
组件。让我们修改我们的路由,让它也可以对 Map
组件做一些处理。
作为对照,我们目前的路由文件是这样的:
1 | import React from 'react' |
让我们修改 makeMatinRoutes
方法,让它可以包含一个子路由。
1 | export const makeMainRoutes = () => { |
再把 <Map />
组件作为容器加进来:
1 | import Map from './Map/Map' |
在浏览器中加载新的路由,我们看不到任何东西,除非我们进入 /map
中,并且实现我们的组件。为了简便起见,我们先创建一个简单的组件来确保我们的路由是能够正常工作的。
就像我们做过很多次的一样,创建js文件,测试文件,模块演示文件。
1 | $ mkdir src/views/Main/Map |
创建一个带有静态文字的简单组件:
1 | import React, { PropTypes as T } from 'react' |
回到浏览器,我们可以看到。。。等等,还是空白的,为什么呢?我们忘记告诉 <Container />
组件,什么时候,如何去渲染子路由。那么,我们需要告诉这个组件我们的期望!
React 推荐的方式来达到这个目的是采用 children
属性。我们将会使用这个属性,来告诉容器,我们想显示什么样的子路由。
为了使用children属性,我们需要对这个组件做一些修改:
1 | export class Container extends React.Component { |
这样的话,我们再次刷新浏览器,可以看到效果啦!
在外层组件中,已经成功的加载了 <Map />
组件,我们就可以处理从google得到的关联关系转变成一个地图的实例。我们可以将这个关联关系作为一个children属性,传递下去,克隆一份children属性,并且创建一个新的。
使用 React.cloneElement()
方法,可以使得上面所说的方式变得非常简单。
1 | export class Container extends React.Component { |
加上google信息,可能children:
1 | export class Container extends React.Component { |
现在,<Container />
组件的孩子们可以接受到 google
的属性了。当然,我们也需要传递 places
.
1 | export class Container extends React.Component { |
现在,让我们更新我们的 <MapComponent />
组件。
1 | import React, { PropTypes as T } from 'react' |
<MapComponent>
组件能够和我们预期的一样工作了。
创建地图标记
让我们在我们的地图上做一些标记吧,我们可以将 <Marker />
作为地图组件的子组件渲染,并且可以采取和前面一样的策略。
1 | export class MapComponent extends React.Component { |
让我们从google-maps-react
模块中引用 <Marker />
组件吧。
1 | import Map, { Maker } from 'google-maps-react' |
现在,让我们完善 renderMarkers()
方法吧。
1 | export class MapComponent extends React.Component { |
刷新浏览器,看看效果:
现在我们为每一个地点在地图上做好了标记,下面,我们开始致力于添加更加详细的信息吧。
更新所有 子组件
因为我们不仅仅需要更新地图组件中的标记,而是要更新所有的子组件,所以,是时候对这些子组件的渲染方式做一个改进了。
让我们在 <MpConponect />
组件中创建一个 renderChildren()
方法, 来负责我们所有的子组件的更新和渲染。
1 | export class MapComponent extends React.Component { |
点击标记
我们可以监听每一个标记的点击事件,来处理当用户点击了标记时更新我们的页面,跳转到一个显示地点详细信息的路由中去。
我们将我们的大部分逻辑都放在了 <Container />
组件中,而将 <MapComponent />
组件当做是一个无状态的组件使用。 所以,我们将点击事件的逻辑放到 <Comtainer />
组件中,并且以此来触发地图页面的更新。
就像我们可以将数据利用 props
属性传递一样,我们也能将方法的引用传递下去。让我们使用 React.cloneElement()
借口,并且将点击处理事件取名叫做 onMarkerClick()
.
回到 <Container />
组件:
1 | export class Container extends React.Component { |
我们将会将这个方法绑定在 <Marker />
组件的 onClick()
事件上。
1 | export class MapComponent extends React.Component { |
现在,每当一个标记被点击了,都会触发 <Comtainer />
组件中的 onMarkerClick()
方法。同样的,可以改变要传下来的地点信息。
1 | export class Container extends React.Component { |
当我们的用户点击标记,我们希望可以引导他们进入一个新的路由,详细信息路由。
1 | export class Container extends React.Component { |
这样的话,点击一个标记,会把用户跳转到一个新的路由 /map/detail/${place.place_id}
为标记增加详细内容
让我们来创建 /detail/:placeId
路由。当用户访问类似于 /detail/abc123
的地址是,我们希望给用户显示id是abc123的地点的详细信息。这种规则,可以让用户复制和粘贴这个地址,随时可以直接访问单独的详细地址页面。
先让我们创建新路由吧:
1 | export const makeMainRoutes = () => { |
继续创建 Detail
组件。
1 | $ mkdir src/views/Main/Detail |
让我们创建一个API处理器,来获取地点的详细信息,在文件 src/utils/googleApiHelpers.js
中:
1 | export function getDetails(google, map, placeId) { |
让我们开始建立详细信息组件吧,并且确保引用了刚刚建立的新的帮助方法:
1 | import React, { PropTypes as T } from 'react' |
我们添加了 componentDidMount()
以及 componentDidUpdate()
方法, 并且了增加了一个自己的属性,来帮助更新组件内容。
在继续前进之前,让我们给我们的 <h2>
放在一个叫做 header
的类里面,以便于维护页面的样式。
1 | export class Detail extends React.Component { |
回到 src/views/Main/Detail/styles.module.css
文件,添加 header 类:
1 | .header { |
别忘了,我们可以使用相同的padding数值,所以,相比于直接写死在代码中,然我们引入我们的css变量文件,并且使用 --padding
变量。
1 | @import url("../../../styles/base.css"); |
虽然看起来没有太多的提升,但是,至少,我们证明了我们的css模块在这边是工作的。
Google API 的返回类容中,包含了很多有趣的东西,比如说图片。让我们将他们显示在我们的页面中吧。
我们可以把 photos
加入到 render()
方法中。Google Api 并没有给我们返回图片的连接,所以,我们需要向google 请求这些图片的连接地址。非常幸运的是,Google JS SDK
给我们提供了一个方法,可是使我们得到图片的最大宽度和最大高度,我们将使用这个来生成URL。
我们建立一个图片的渲染方法来代替直接在render方法中添加,并且要考虑到有一些地点并没有图片。
1 | export class Detail extends React.Component { |
每一个地点,得到的都是一个图片数组,我们需要将他们都render在页面上:
1 | export class Detail extends React.Component { |
刷新浏览器,可以看到:
看起来不是很好,添加一些样式,让图片不转行吧:
1 | .details { |
让我们隐藏起那个下面的滚动条吧,添加 ::-webkit-scrollbar
:
1 | .details { |
最后,让每个图片之间,添加一个写间距:
1 | .details { |
响应式布局
在中屏以及大屏幕上,我们的引用看起来很不错,但是,如果在小屏幕上呢?
看起来就不好了,让我们着手添加一些响应式的布局吧。我们仍然希望能够在页面上显示地点列表以及附近的一些推荐。但是,当窗口比较小的时候,地点列表显示在下面,图片显示在上面会比较合理的。
我们将会是一个一些 media queries 属性来更改我们的样式:
1 | body { font-size: 18px; } |
在我们前面配置postcss的时候,我们使用了 precss 插件,可以帮助我们很轻松的添加 postcss-custom-media插件到我们的代码中。这个插件允许我们定义一些 media queries 变量。
在移动端优先的设计中,我们应该首先让我们的样式满足移动端的渲染,再更具一些media queries 适应大屏幕。
- 设计和实现对移动端友好的样式。
- 再添加适应大屏幕的样式。
在这种思想下,我们将我们的主要页面作为页面内容的第一块,列表作为第二块来设计。
为了让我们的应用使用 Flexbox 的布局,让我们先了解一下Flexbox的其中三个知识点。想要更加详细的了解什么是flex,并且如何使用flex,可以参考Chris Coyier
所写的一遍非常棒的指导,我们这边仅仅是为了能让我们的项目够用!
display: flex
想要让浏览器知道,我们想要使用Flex 布局,我们需要给我们的父节点设置 display: flex
属性。
对于本项目来说,我们将要在入口页面中的 wrapper
类添加这个属性,在前面的文章中,我们已经设置好了。
flex-direction
flex-direction
属性将会告诉浏览器,我们想要如何显示我们的布局,可以使水平的也可以是垂直的。设置 flex-direction: column
属性,浏览器会将元素从上往下一个个渲染。因为我们希望我们的页面在移动端的时候,是垂直显示的,所以:
1 | /* In src/views/Main/styles.module.css */ |
刷新浏览器,我们可以看到,我们的布局从水平变成了垂直。 添加 flex: 1
可以均衡移动端页面的显示。
现在,如果我们回到桌面屏幕大小,我们会发现,垂直是不合时宜的,有点不好看。让我们添加第一个 media query 来修复这个问题。
首先,我们比较喜欢让他们自己定义自己的media变量。虽然可以直接写在 base.css 文件中,但是比较好的做法是,新建一个文件:
1 | $ touch src/styles/queries.css |
在这个文件中,让我们使用 @custom-media
来定义不同屏幕的样式:
1 | @custom-media --screen-phone (width <= 35.5em); |
在文件 src/views/Main/styles.module.css
1 | /* In src/views/Main/styles.module.css */ |
好了,现在不管是大屏幕还是小屏幕,都能比较友好的显示啦!
order
Flexbox 也允许我们设置区块的排序方式,比如现在, content
和 sidebar
区块都被设置为了顺序是1
的优先级,所以,先显示了content区块,因为它先被定义。为了手动修改显示的顺序,我们可以给样式添加 order
属性。
1 | /* In src/views/Main/styles.module.css */ |
对于 sidebar
区块,让我们采取相同的方式设置吧:
1 | @import url("../../styles/base.css"); |
现在再回到浏览器,刷新页面,我们可以看到,不管是大屏幕还是小屏幕,都看起来非常好看了。
总结
我们创建了一个完整的React 应用,
添加了大量的路由,复杂的项目关联,甚至引入了响应式布局,这是一段漫长的旅程。
我们将代码放在的github上面,可以在这里获得 fullstackreact/react-yelp-clone.
小光头总结
经历了长达大半个月的时间,终于完成了这篇博客的翻译,从这次经历中体会到:
- 并没有涵盖所有知识,Redux 等知识没有涉及。
- 自己理解与表达给他人,其中差了N行代码的距离,还需要非常多的练习
- 时间,时间,时间!
- 学习,学习,学习,感觉不知道的东西太多。
第一次翻译,肯定有非常非常多的不足,任何建议请留言或者联系我哦,不过,至少迈出了一步,加油,小光头!!!
英文原文地址: https://www.fullstackreact.com/articles/react-tutorial-cloning-yelp/