React全家桶:react-starter-kit之一

背景

一直想做个项目,以react为前端,node做后端,实现前后端都可以使用javascript开发。所以在github上找手脚架时候,找到了react-starter-kit,8k多的star,于是就拍板使用它了。这就是这系列笔记的来源之所以。

然而最终发现,react-starter-kit实在不是一个平易近人的项目,匮乏的文档,匮乏的demo,匮乏的说明,尤其是,匮乏的starter-guide,实在有些匹配不上react-starter-kit这个名字。

——然而已经入坑了,那就只好慢慢填坑爬出来——一把辛酸泪。。。

内容

在展开所有内容之前,还是有必要讲讲React Starter Kit涵盖了哪些。以下是github项目上介绍:

React Starter Kit — isomorphic web app boilerplate (Node.js, Express, GraphQL, React.js, Babel 6, PostCSS, Webpack, Browsersync)

这是React Starter Kit项目的介绍。

总体上呢,我认为可以分为两个部分:

  1. 工具部分:webpack,babel,browserSync等
  2. 框架部分:Express,GraphQL,redux,React等

除此以外是个人规划部分:

  1. 数据库:Mongodb,使用mongoose做连接池
  2. 模板引擎:干掉Jade,使用artTemplate(不要问我为嘛用这个,只因为习惯了,另外憎恨Jade语法)

加起来,react-starter-kit大概涉及到了:webpack,babel,browserSync,Express,GraphQL,redux,React,mongoose,artTemplate,名副其实的全家桶,需要的储备知识着实不少。

模板引擎

第一步是src/views目录下的两个jade模板。这是首页和报错页面的基础模板。其中首页模板会在各个页面被重用。

下面是关键上下文代码之一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//file:src/server.js
app.get('*', async (req, res, next) => {
try {
let css = [];
let statusCode = 200;
const template = require('./views/index.jade'); // eslint-disable-line global-require
...
res.status(statusCode);
res.send(template(data));
} catch (err) {
next(err);
}
});
app.use((err, req, res, next) => { // eslint-disable-line no-unused-vars
console.log(pe.render(err)); // eslint-disable-line no-console
const template = require('./views/error.jade'); // eslint-disable-line global-require
const statusCode = err.status || 500;
res.status(statusCode);
res.send(template({
message: err.message,
stack: process.env.NODE_ENV === 'production' ? '' : err.stack,
}));
});

其中,可以看到两个地方引用(require)了jade模板,并且代码最终会被webpack打包,所以这里webapck使用了jade-loader,为了可以将index.jade改成index.html,我们需要安装 tmodjs-loader,确保html后缀的artTemplate模板可以被正确识别和编译。
下面是操作步骤

  1. 安装tmodjs-loader: npm i tmodjs-loader –save-dev
  2. 转换jade2html: html2jade,将jade贴到右边左边会有编译的HTML,补充好变量位置即可。
  3. 设置webpack的loader
    tools/webpack.config.js line127左右的位置插入以下代码,为模板设置正确的loader
    1
    2
    3
    4
    {
    test: /\.html$/,
    loader: "tmodjs"
    }

关键上下文代码之一:
除此以外,在src/content目录下有若干的jade文件,这个是Content Component的jade文件,为了去除jade,我们将它转换为markdown文件即可。Content Component逻辑中已经配置好markdown文章的逻辑。不需要操作更多。

至此,jade模板更换artTemplate工序完成。

mongoose

为了链接mongodb,这里使用了mongoose来连接和操作。

安装

安装很容易,在终端下运行:

1
npm i mongoose --save

配置

安装完成之后我们呢需要配置一下才能够使用,在src下新建mongoose目录,用来存放Schema等相关的东西。结构如下:

1
2
3
4
5
6
mongoose
├── Schema
│   ├── News.js
│   ├── Tags.js
│   └── User.js
└── index.js

这里仅仅展示一下News.js和index.js,已经足够展示整体结构了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//file:News.js
import mongoose from 'mongoose';
import {mongoDbUrl} from '../../config';
let conn = mongoose.connect(mongoDbUrl);
let schema = new mongoose.Schema({
title: 'string',
link: 'string',
author:'string',
publishedDate: 'string',
contentSnippet:'string',
});
let News = mongoose.model('news', schema);
export default News;
//file:index.js
import News from './Schema/News';
import Tags from './Schema/Tags';
import User from './Schema/User';
export {News,Tags,User};

可以看到,虽然目录名字虽然叫做Schema,但是导出的是model,这是为了方便进行数据操作。至于index.js,只是为了方便导入各个Model而做的归集。

另外,这里有个变量mongoDbUrl,从外部引入了,这里也贴一下相关的配置:

1
2
3
4
5
6
7
8
9
10
let mongodbInfo = {
url:"xxx.mlab.com",
port:19839,
user:"xxx",
password:'xxx',
dbName:'learn'
};
//fromat:'mongodb://username:password@ds019839.mlab.com:19839/learn'
export const mongoDbUrl = 'mongodb://'+mongodbInfo.user+':'+mongodbInfo.password+'@'+mongodbInfo.url+':'+mongodbInfo.port+'/'+mongodbInfo.dbName

实际上就是一个带了认证信息的mongodb链接。
这部分内容只是很简单的讲解了如何进行安装,配置,使用上还是需要对mongoose有基础的认识,需要一些知识储备。之前写过的mongoDB笔记(二)可以做一些浅显的参考,更具体的概念性东西,大概需要读者另外爬文了。

API

有了数据库连接池,那么现在往下走,基于monogoose来做一些API。API主要考虑到两个方面,一个数据读写,一个是路由设置。

路由

我们先从路由开始开工。
在src下新建api文件夹,建立api/index.js文件。
下面弄个简单的index.js例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Router } from 'express';
import {News,User,Tags} from '../mongoose';
const router = new Router();
// 获取新闻数据
router.all('/news', (req, res) => {
News.find().exec(function(err,news){
res.json({
code:200,
news:news
});
});
});
export default router;

完毕之后在src/server.js中添加路由相应的路由代码:

1
2
3
import api from './api';
...
app.use('/api', api);

数据读写

数据读写的代码其实在上面已经贴出来了。

1
2
3
News.find().exec(function(){
//读取完毕之后的回调
})

不过这里仅仅是非常简单的全部查询,并没有限制性的条件,这在大多数时候是不存在的,不过这里还算合用(马上讲到GraphQL)。如果需要了解更多的相关mongoose的查询语句,可以参考我之前写的笔记mongoDB笔记(三),这篇笔记对响应的查询都有比较详细的记录。

小总结

到这里,就可以使用浏览器访问 /api/news 来获取json数据了。

GraphQL

GraphQL是个很赞的东西。怎么形容它呢?当前前端有个词,叫做响应式,页面会根据屏幕宽度来展现不同的外观。而GraphQL的最大优势和这个词汇有点像。它可以从前端接受查询语句,然后按照要求返回指定的字段。

优势

也许指定的字段这个不太能突出GraphQL的优势,这里举些例子吧。

例一:
当前如果有个接口 /api/list,返回的数据结构是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
code:200,
message:"success",
data:[
{
name:"",
title:"",
sum:"",
color:"",
height:"",
width:"",
logo:"",
brand:""
}
]
}

而实际上,页面逻辑需要这样的就够了:

1
2
3
4
5
6
7
8
9
{
code:200,
message:"success",
data:[
{
name:"",
}
]
}

如果前端队这个接口高度复用,不同页面只是过去了name,title,brand等数据进行了单独的调用,可以复用的时候实现最优json结构保持简洁和节省流量带宽吗?

例二:
数据结构和例一相同,但是当初接口设计时候只需要做查询就够了,又或者设计时候只接受name进行过滤,到那时现在我们需要队brand进行过滤,此时应该怎么办?

例三:
有两个接口 /api/list1/api/list2,结构和例一一致,但是两种不同品类商品
假如我们有这样一个需求:

  1. 从list1中过滤出brand==”que01”
  2. 从list2中过滤出color== “yellow”
  3. 将结果分别赋值给{data1:[],data2:[])

此时常规的办法的是怎样的?先请求一次list1,过滤,然后请求list2,过滤。拼接。两次请求,3次数据处理,如果服务端承担了这个过滤,也必须请求两次,然后进行合并操作。我们可以更简单一些嘛?

是的。在GraphQL中,这些可以毫不费力的实现。这实在是一个超级令人激动的东西。
React-starter-kit本身已经集成了GraphQL。但是要使用起来,还是有一些注意事项。不过本文主要立足于怎么将GraphQL在react-starter-kit中用来了,所以就不做太多的理论说明了,有时间单独总结一下它的使用,上面的例子,只是为了让大家明白他的优势。

使用

在react-starter-kit中使用GraphQL不是一件太困难的事情。

在server.js中有以下代码:

1
2
3
4
5
6
app.use('/graphql', expressGraphQL(req => ({
schema,
graphiql: true,
rootValue: { request: req },
pretty: process.env.NODE_ENV !== 'production',
})));

GraphQL已经作为中间件载入了。这里仅对初步使用作出讲解。下面是src/data目录的结构

1
2
3
4
5
6
7
8
9
10
11
├── models
├── queries
│   ├── content.js
│   ├── news.js
│   └── tags.js
├── schema.js
├── sequelize.js
└── types
├── ContentType.js
├── NewsItemType.js
└── TagsType.js

下面是我个人初步的schema.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import {
GraphQLSchema as Schema,
GraphQLObjectType as ObjectType,
} from 'graphql';
import tags from './queries/tags';
import content from './queries/content';
import news from './queries/news';
const schema = new Schema({
query: new ObjectType({
name: 'Query',
fields: {
tags,
content,
news,
},
}),
});
export default schema;

这里是最外层的接口了。其中tags,news,content都是最外层的api,也是我们需要自行定制的地方。
做好这些以后我们查询时候发送请求到/graphql接口。
下面这是发送过去的类似json的字符串

1
2
3
4
5
6
7
8
9
{
news {
title
link
author
publishedDate
contentSnippet
}
}

然后返回的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"data": {
"news": [
{
"title": "测试标题1",
"link": "http://www.baidu.com",
"author": "que01",
"publishedDate": "2014-11-11",
"contentSnippet": "测试摘要测试摘要"
},
{
"title": "测试标题2",
"link": "http://www.baidu.com",
"author": "que01",
"publishedDate": "2014-11-11",
"contentSnippet": "测试摘要测试摘要"
}
]
}
}

这里值得一提的是,title,link,author,publishedDate,contentSnippet这些字段可以任意顺序组合,删减。

简单的用法的我们暂时就这样简单的一笔跳过,详细的如果有时间另外写一篇讲解这个。我们现在来看看如何处理数据源。我们配合之前做好的api作为数据源来支挽GraphQL。

为了方便,这里使用tags来讲解。下面是tags.js的详细代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { GraphQLList as List,GraphQLString } from 'graphql';
import _ from 'lodash';
import fetch from '../../core/fetch';
import TagsType from '../types/TagsType';
const url = '/api/tags';
let items = [];
let lastFetchTask;
const tags = {
type: new List(TagsType),
args: {
id: { type: GraphQLString },
name: { type: GraphQLString }
},
async resolve(reqObj,{id,name}) {
lastFetchTask = await fetch(url)
.then(response => response.json())
.then(data => {
if (data.code === 200) {
items = data.tags;
}
if(!!id){
items = _.filter(items,{id:+id})
}else if(!!name){
items = _.filter(items,{name:name})
}
return items;
})
.finally(() => {
lastFetchTask = null;
});
if (items.length) {
return items;
}
return lastFetchTask;
},
};
export default tags;

这里args里面定义了接口接受的参数,resolve则定义了处理数据的方式并返回数据(第二个参数是传递的请求参数组成的数组),type则定义了数据类型。做好相应的逻辑配置之后,我们不仅可以想news那样直接返回所有数据,还可以传递参数id和name之一过去过滤数据.like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//request:
{
tags(name:"css") {
id,
name
}
}
//response:
{
"data": {
"tags": [
{
"id": "2",
"name": "css"
}
]
}
}

到这里,最简单的使用就可以跑起来了。这已经可以满足最基础的场景应用。这部分我们到此为止。下一步我们讲讲自定义组件和页面。

自定义组件和页面

react-starter-kit定义了一些页面,但是很显然,我们不能满足修改已有进行改造使用,更多时候我们需要更多的页面…以及…更多的组件构成的页面…

这里仍旧以/tags页面为说明,这是一个新建的页面,使用了自定义的组件和数据源来构建的新的页面。
我们第一步要做的是在/src/routes下新建一个页面,然后在内部建立 Tags.css、Tags.js、index.js三个文件,保持同其他route组件的一致性。

下面是具体code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
//file:Tags.js
import React, { PropTypes } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './Tags.css';
const title = '热门标签';
function Tags(props, context) {
context.setTitle(title);
return (
<div className={s.root}>
<div className={s.container}>
<h1>{title}</h1>
<div>
{props.tags.map((item, index) => (
<span className={s.tags} key={index}>{item.name}</span>
))}
</div>
</div>
</div>
);
}
Tags.contextTypes = { setTitle: PropTypes.func.isRequired };
export default withStyles(s)(Tags);
//file:index.js
import React from 'react';
import Tags from './Tags';
import fetch from '../../core/fetch';
export default {
path: '/tags',
async action() {
const resp = await fetch('/graphql', {
method: 'post',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: '{tags{name}}',
}),
credentials: 'include',
});
const { data } = await resp.json();
return <Tags tags={data.tags} />;
},
};

css代码就不贴了,虽然它是组件的重要组成部分,但是就逻辑来说,它无关紧要,不是吗?这里简单说下这两个文件,如果说Tags.js是组件的结构逻辑,那么index.js就是数据获取并渲染Tags.js组件的文件。

做好以上步骤以后,我们需要做最后一步,将其添加到路由内部,是我们访问 /tags时候可以访问到。
一下是关键代码,文件位置是 /src/routes/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import tags from './tags';
...
export default {
path: '/',
children: [
home,
contact,
login,
registerSuccess,
tags,
register,
content,
error,
],
async action({ next, render, context }) {
const component = await next();
if (component === undefined) return component;
return render(
<App context={context}>{component}</App>
);
},
};

关键点在于import和children中的tags。这里有个非常重要的事情是,自定义组件,请放在content上面(重要的事情说三遍:请放在content上面!请放在content上面!请放在content上面!),如果你不这样做,就无法保证自己路由优先基本,实质上代码逻辑中,经个人调试,会有一个『*』的路由,如果你不放到上面,它就被『*』匹配了,这样就没你的页面什么事情了,因为根本不会往下走了!

总结

本文虽然不是深度性的文章,但是确实是一个大纲性质和入门的文章。不管是mongoose、API、GraphQL还是React,他们几乎每个都能写本书。
所以说,文章虽然浅显,但是真想用好react-starter-kit,还是需要相对巨量的知识储备。

react-starter-kit,个人认为,虽然是for starter,但是是相对于react-starter-kit的受众starter来说,它实在不是相对于前端初学者。

虽然是个starter-kit,但是:

—— 如果你没有学过express,请不必细看下去;
—— 如果你没有学过mongoose,请不必细看下去;
—— 如果你没有学过React,请不必细看下去;
—— 如果你没有打算学GraphQL,请不必细看下去;

就这样吧,感叹下,前端实在太快,如果没有足够的知识储备,真的无法走在前沿。
——向小伙伴们祝好!