commit e772c445e0b3f954ac88f3775fb3c4a2aef6962b
parent ed372648f1283b1fdc685f76f93b4dea2e21da09
Author: Konstantin Tarkus <koistya@gmail.com>
Date: Thu, 17 Dec 2015 15:20:10 +0300
Merge pull request #352 from koistya/fetch
Replace Superagent with WHATWG Fetch
Diffstat:
13 files changed, 92 insertions(+), 53 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
@@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file.
### [Unreleased][unreleased]
+- Replace Superagent with WHATWG Fetch library
+- Rename `app.js` to `client.js` (aka client-side code)
- Integrate [CSS Modules](https://github.com/css-modules/css-modules) and
[isomorphic-style-loader](https://github.com/kriasoft/isomorphic-style-loader)
- Move `DOMUtils.js` to `src/core` folder; remove `src/utils` folder
diff --git a/docs/README.md b/docs/README.md
@@ -4,6 +4,7 @@
* [React Style Guide](./react-style-guide.md)
* [How to configure text editors and IDEs](./how-to-configure-text-editors.md)
+* [Data fetching with WHATWG Fetch](./data-fetching.md)
### Questions
diff --git a/docs/data-fetching.md b/docs/data-fetching.md
@@ -0,0 +1,32 @@
+## Data Fetching with WHATWG Fetch
+
+There is isomorphic `core/fetch` module that can be used the same way in both
+client-side and server-side code as follows:
+
+```jsx
+import fetch from '../core/fetch';
+
+export const path = '/products';
+export const action = async () => {
+ const response = await fetch('/api/products');
+ const data = await response.json();
+ return <Layout><Products {...data}</Layout>;
+};
+```
+
+When this code executes on the client, the Ajax request will be sent via
+GitHub's [fetch](https://github.com/github/fetch) library (`whatwg-fetch`),
+that itself uses XHMLHttpRequest behind the scene unless `fetch` is supported
+natively by the user's browser.
+
+Whenever the same code executes on the server, it uses
+[node-fetch](https://github.com/bitinn/node-fetch) module behind the scene that
+itself sends an HTTP request via Node.js `http` module. It also converts
+relative URLs to absolute (see `./core/fetch/fetch.server.js`).
+
+Both `whatwg-fetch` and `node-fetch` modules have almost identical API. If
+you're new to this API, the following article may give you a good introduction:
+
+https://jakearchibald.com/2015/thats-so-fetch/
+
+
diff --git a/docs/recipes/how-to-implement-routing.md b/docs/recipes/how-to-implement-routing.md
@@ -51,7 +51,7 @@ Just wrap React components inside your routes into asynchronous functions:
```js
import React from 'react';
-import http from './core/http';
+import fetch from './core/fetch';
import Layout from './components/Layout';
import HomePage from './components/HomePage';
import AboutPage from './components/AboutPage';
@@ -60,11 +60,13 @@ import ErrorPage from './components/ErrorPage';
const routes = {
'/': async () => {
- const data = await http.get('/api/data/home');
+ const response = await fetch('/api/data/home');
+ const data = await response.json();
return <Layout><HomePage {...data} /></Layout>
},
'/about': async () => {
- const data = await http.get('/api/data/about');
+ const response = await fetch('/api/data/about');
+ const data = await response.json();
return <Layout><AboutPage {...data} /></Layout>;
}
};
@@ -97,7 +99,7 @@ for matching URL paths to React components.
```js
import React from 'react';
import Router from 'react-routing/src/Router';
-import http from './core/http';
+import fetch from './core/fetch';
import Layout from './components/Layout';
import ProductListing from './components/ProductListing';
import ProductInfo from './components/ProductInfo';
@@ -106,11 +108,13 @@ import ErrorPage from './components/ErrorPage';
const router = new Router(on => {
on('/products', async () => {
- const data = await http.get('/api/products');
+ const response = await fetch('/api/products');
+ const data = await response.json();
return <Layout><ProductListing {...data} /></Layout>
});
on('/products/:id', async (req) => {
- const data = await http.get(`/api/products/${req.params.id}`);
+ const response = await fetch(`/api/products/${req.params.id}`);
+ const data = await response.json();
return <Layout><ProductInfo {...data} /></Layout>;
});
}]);
diff --git a/package.json b/package.json
@@ -16,12 +16,13 @@
"front-matter": "2.0.1",
"history": "1.16.0",
"jade": "1.11.0",
+ "node-fetch": "1.3.3",
"normalize.css": "3.0.3",
"react": "0.14.3",
"react-dom": "0.14.3",
"react-routing": "0.0.6",
"source-map-support": "0.4.0",
- "superagent": "1.6.1"
+ "whatwg-fetch": "0.10.1"
},
"devDependencies": {
"assets-webpack-plugin": "^3.2.0",
diff --git a/src/components/Html/Html.js b/src/components/Html/Html.js
@@ -1,7 +1,7 @@
/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
import React, { Component, PropTypes } from 'react';
-import config from '../../config';
+import { googleAnalyticsId } from '../../config';
class Html extends Component {
@@ -25,7 +25,7 @@ class Html extends Component {
`e=o.createElement(i);r=o.getElementsByTagName(i)[0];` +
`e.src='https://www.google-analytics.com/analytics.js';` +
`r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));` +
- `ga('create','${config.googleAnalyticsId}','auto');ga('send','pageview');`,
+ `ga('create','${googleAnalyticsId}','auto');ga('send','pageview');`,
});
}
diff --git a/src/config.js b/src/config.js
@@ -1,5 +1,5 @@
/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
-export default {
- googleAnalyticsId: 'UA-XXXXX-X',
-};
+export const port = process.env.PORT || 5000;
+export const host = process.env.WEBSITE_HOSTNAME || `localhost:${port}`;
+export const googleAnalyticsId = 'UA-XXXXX-X';
diff --git a/src/core/HttpClient.js b/src/core/HttpClient.js
@@ -1,37 +0,0 @@
-/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
-
-import request from 'superagent';
-import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
-
-function getUrl(path) {
- if (path.startsWith('http') || canUseDOM) {
- return path;
- }
-
- return process.env.WEBSITE_HOSTNAME ?
- `http://${process.env.WEBSITE_HOSTNAME}${path}` :
- `http://127.0.0.1:${global.server.get('port')}${path}`;
-}
-
-const HttpClient = {
-
- get: path => new Promise((resolve, reject) => {
- request
- .get(getUrl(path))
- .accept('application/json')
- .end((err, res) => {
- if (err) {
- if (err.status === 404) {
- resolve(null);
- } else {
- reject(err);
- }
- } else {
- resolve(res.body);
- }
- });
- }),
-
-};
-
-export default HttpClient;
diff --git a/src/core/fetch/fetch.client.js b/src/core/fetch/fetch.client.js
@@ -0,0 +1,8 @@
+/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
+
+import 'whatwg-fetch';
+
+export default self.fetch.bind(self);
+export const Headers = self.Headers;
+export const Request = self.Request;
+export const Response = self.Response;
diff --git a/src/core/fetch/fetch.server.js b/src/core/fetch/fetch.server.js
@@ -0,0 +1,22 @@
+/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
+
+import fetch, { Request, Headers, Response } from 'node-fetch';
+import { host } from '../../config';
+
+function localUrl(url) {
+ if (url.startsWith('//')) {
+ return 'https:' + url;
+ }
+
+ if (url.startsWith('http')) {
+ return url;
+ }
+
+ return `http://${host}${url}`;
+}
+
+function localFetch(url, options) {
+ return fetch(localUrl(url), options);
+}
+
+export { localFetch as default, Request, Headers, Response };
diff --git a/src/core/fetch/package.json b/src/core/fetch/package.json
@@ -0,0 +1,6 @@
+{
+ "private": true,
+ "name": "fetch",
+ "main": "./fetch.server.js",
+ "browser": "./fetch.client.js"
+}
diff --git a/src/routes.js b/src/routes.js
@@ -2,7 +2,7 @@
import React from 'react';
import Router from 'react-routing/src/Router';
-import http from './core/HttpClient';
+import fetch from './core/fetch';
import App from './components/App';
import ContentPage from './components/ContentPage';
import ContactPage from './components/ContactPage';
@@ -24,7 +24,8 @@ const router = new Router(on => {
on('/register', async () => <RegisterPage />);
on('*', async (state) => {
- const content = await http.get(`/api/content?path=${state.path}`);
+ const response = await fetch(`/api/content?path=${state.path}`);
+ const content = await response.json();
return content && <ContentPage {...content} />;
});
diff --git a/src/server.js b/src/server.js
@@ -8,10 +8,9 @@ import ReactDOM from 'react-dom/server';
import Router from './routes';
import Html from './components/Html';
import assets from './assets.json';
+import { port } from './config';
const server = global.server = express();
-const port = process.env.PORT || 5000;
-server.set('port', port);
//
// Register Node.js middleware