Er zijn miljoenen apps, maar de app die de informatie zoals jij dat wil, is niet altijd beschikbaar. De enige oplossing is om er zelf één te maken.
Ik heb een snelle en gemakkelijke manier bedacht om PWA ℹ️ apps met PHP en React te ontwikkelen. Het algemene idee is om HTML van een website te halen, te converteren in JSON en de data weer te geven met een op React-gebaseerde PWA.
In deze blog loop ik alle stappen door. Als je vindt dat het leest als een recept - veel stappen met weinig uitleg hoe het werkt - dan heb je helemaal gelijk. Dat is de bedoeling. Als je meer achtergrondinformatie wil, dan is deze link op github.com 🔗 een goed begin.
Aan het einde van deze tutorial heb je een simpele PWA die de huidige temperatuur voor verschillende steden laat zien. Ik heb de tabel op de website van het KNMI 🔗 gebruikt als input voor de app.
0. Wat heb je nodig?
- PHP (ik gebruik valet+)
- Git
- node.js v8.15.1 of hoger
- npm
- PHP Simple HTML DOM Parser 🔗
- React Boilerplate 🔗
- Een website met informatie die je wil scrapen
Aan de slag!
1. Zet HTML om in JSON
Maak een projectmap, bijvoorbeeld "scraper". Zorg ervoor dat uw PHP-scripts hier kunnen worden uitgevoerd.
Download PHP Simple HTML DOM Parser 🔗 en plaats de (uitgepakte) simplehtmldom_2_0-RC2-map in de root van uw project. U kunt in plaats daarvan natuurlijk composer gebruiken.
composer require simplehtmldom/simplehtmldom:2.0-RC2
Maak in de root index.php aan.
Voeg Simple HTML DOM als volgt toe. Zorg er vervolgens voor dat de CORS-instellingen juist zijn en dat de pagina als JSON-inhoud wordt weergegeven.
<?php
# With Composer
# include 'vendor/simplehtmldom/simplehtmldom/simple_html_dom.php';
# Or manual download
include 'simplehtmldom_2_0-RC2/simple_html_dom.php';
//$cors = 'https://yourdomainhere.ninja/';
$cors = 'http://localhost:3000';
header('Content-type: application/json; charset=UTF-8;');
header('Access-Control-Allow-Origin: ' . $cors, FALSE);
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Origin");
Nu kan de KNMI-data uit de tabel worden opgehaald. Dat kan met Simple HTML DOM.
Find tags on an HTML page with selectors just like jQuery.
Simple HTML DOM
We gebruiken het om door de HTML-structuur te lopen en een array op te bouwen.
function raw_scrape($url) {
$doc = file_get_html($url);
$headers = [];
$rows = [];
foreach ($doc->find('table tr') as $tr) {
foreach ($tr->find('th') as $element) {
array_push($headers, strip_tags($element->plaintext));
}
$row = [];
foreach ($tr->find('td') as $element) {
array_push($row, $element->plaintext);
}
if (!empty($row)) {
array_push($rows, $row);
}
}
$table = $rows;
array_unshift($table, $headers);
return $table;
}
Het resultaat wordt in de stap hieronder verfijnd.
De array moet nog worden geconverteerd naar een JSON. De volgende code doet dit en start vervolgens de functies om de JSON-gecodeerde array te tonen.
function scrape ($url) {
$tempArr = [];
$table = raw_scrape($url);
$headers = array_shift($table);
foreach ($table as $indexR => $row) {
$data = [];
foreach ($row as $indexT => $tuple) {
$data[$headers[$indexT]] = $tuple;
}
array_push($tempArr, $data);
}
return json_encode($tempArr);
}
echo scrape('https://www.knmi.nl/nederland-nu/weer/waarnemingen');
Controleer de uitvoer door de lokale URL te bezoeken. Voor valet+ is het standaard scraper.test. In Firefox ziet het er als volgt uit:

2. Van JSON naar website
2.1 Opzetten React Boilerplate
React Boilerplate is een geweldig startpunt met redux-, redux-saga- en PWA-implementaties. Het is eenvoudig genoeg voor kleine projecten zoals deze, maar geschikt voor complexe apps.
Gebruik Git om React Boilerplate 🔗 te downloaden naar een subfolder van het project, bijvoorbeeld in "js".
git clone https://github.com/react-boilerplate/react-boilerplate.git js
Ga naar de nieuwe map en voer het volgende uit:
npm run setup && npm run clean
Antwoord "no" als je geen repo wil aanmaken.
Voer npm run generate uit om een nieuwe container te maken.
npm run generate
Kies container en beantwoord de volgende vragen:
- What should it be called? weather
- Do you want to wrap your component in React.memo? Yes
- Do you want headers? No
- Do you want an actions/constants/selectors/reducer tuple for this container? Yes
- Do you want sagas for asynchronous flows? (e.g. fetching data) Yes
- Do you want i18n messages (i.e. will this component use text)? No
- Do you want to load resources asynchronously? Yes
Er is nu een nieuwe map aangemaakt met de naam "Weather" in js/app/containers. Zoiets:

Voer npm start uit en bezoek http://localhost:3000. Je krijgt nu een witte pagina.
npm start
2.2 Fetch the JSON
Vanaf hier moet u veel code kopiëren en plakken.
Waarschijnlijk heb je js/app/utils/request.js nog niet. Download het van github.com 🔗.
Nu gaan we het Weather component instellen als homepage.
Ga naar js/app/containers/App/index.js en verander "HomePage" in "Weather". Pas de import aan, en de Route. Dan krijg je dit:
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import Weather from 'containers/Weather/Loadable';
import NotFoundPage from 'containers/NotFoundPage/Loadable';
import GlobalStyle from '../../global-styles';
export default function App() {
return (
<div>
<Switch>
<Route exact path="/" component={Weather} />
<Route component={NotFoundPage} />
</Switch>
<GlobalStyle />
</div>
);
}
Je browser opent nu het Weather component als je http://localhost:3000 bezoekt.
De volgende stap is het ophalen van de JSON. Dit doe je in js/app/containers/Weather/saga.js. Pas het als volgt aan:
import { call, put, takeLatest } from 'redux-saga/effects';
import { LOAD_WEATHER } from './constants';
import { weatherLoaded } from './actions';
import request from 'utils/request';
export function* getWeather() {
let requestURL = 'http://scraper.test';
if (process.env.NODE_ENV == 'production') {
requestURL = 'https://yoursite.ninja/whateversubfolder';
}
try {
const weather = yield call(request, requestURL);
yield put(weatherLoaded(weather));
} catch(e) {
console.error(e);
}
}
export default function* weatherData() {
yield takeLatest(LOAD_WEATHER, getWeather);
}
Zie hier de eerder toegevoegde request.js. Knip en plak de volgende bestanden:
actions.js
import { LOAD_WEATHER, LOAD_WEATHER_SUCCESS } from './constants';
export function loadWeather(weather) {
return {
type: LOAD_WEATHER,
weather
};
}
export function weatherLoaded(weather) {
return {
type: LOAD_WEATHER_SUCCESS,
weather
};
}
reducer.js
import produce from 'immer';
import { LOAD_WEATHER_SUCCESS } from './constants';
export const initialState = {};
const weatherReducer = (state = initialState, action) =>
produce(state, ( draft ) => {
switch (action.type) {
case LOAD_WEATHER_SUCCESS:
draft.weather = action.weather;
break;
}
});
export default weatherReducer;
selectors.js
import { createSelector } from 'reselect';
import { initialState } from './reducer';
const selectWeatherDomain = state => state.weather || initialState;
const makeSelectWeather = () =>
createSelector(
selectWeatherDomain,
substate => substate.weather,
);
export default makeSelectWeather;
export { selectWeatherDomain };
constants.js
export const LOAD_WEATHER = 'app/Weather/LOAD_WEATHER';
export const LOAD_WEATHER_SUCCESS = 'app/Weather/LOAD_WEATHER_SUCCESS';
Als laatste en grootste, index.js
import React, { memo, useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { compose } from 'redux';
import { useInjectSaga } from 'utils/injectSaga';
import { useInjectReducer } from 'utils/injectReducer';
import { loadWeather } from './actions';
import makeSelectWeather from './selectors';
import reducer from './reducer';
import saga from './saga';
export function Weather(props) {
useInjectReducer({ key: 'weather', reducer });
useInjectSaga({ key: 'weather', saga });
useEffect(() => {
props.initWeather();
}, []);
if (props.weather) {
return (
<div>
{props.weather.map(weather => (
<div key={weather.Station}>
{weather.Station} {weather['Temp (°C)']}
</div>
))}
</div>
);
}
return 'No weather';
}
Weather.propTypes = {
dispatch: PropTypes.func.isRequired,
initWeather: PropTypes.func,
weather: PropTypes.array,
};
const mapStateToProps = createStructuredSelector({
weather: makeSelectWeather(),
});
function mapDispatchToProps(dispatch) {
return {
initWeather: () => {
dispatch(loadWeather());
},
dispatch,
};
}
const withConnect = connect(
mapStateToProps,
mapDispatchToProps,
);
export default compose(
withConnect,
memo,
)(Weather);
Kort door de bocht is dit wat er gebeurt: index.js wordt geladen door de router en start initWeather(). Dit vuurt loadWeather() uit actions.js af, wat op zijn beurt de saga activeert om de JSON op te halen. Zodra de gegevens er zijn, gaan deze terug via weatherLoaded() en komen ze langs de reducer om de state aan te passen. Tenslotte gaat er via de selector een bericht uit naar index.js om de gegevens op het scherm te zetten. 🤯
Laten we localhost:3000 bekijken. Data!

3. De PWA app maken
Om alles aan de praat te krijgen, moet de index.php en simplehtmldom_2_0-RC2 naar een server worden geüpload.
Bekijk index.php en voeg de servernaam toe aan de $cors variabele.
$cors = 'https://yoursite.ninja/whateversubfolder';
Pas ook in js/app/containers/Weather/saga.js de servernaam aan. Een relatief pad kan natuurlijk ook.
if (process.env.NODE_ENV == 'production') {
requestURL = 'https://yoursite.ninja/whateversubfolder';
}
Als je de site uitrolt naar een subfolder, pas dan js/internals/webpack/webpack.base.babel.js aan.
publicPath: '/whateversubfolder/',
En de router in js/app/containers/App/index.js to
<Route exact path="/whateversubfolder" component={Weather} />
Ook lokaal wordt de route anders. De site is nu beschikbaar op http://localhost:3000/whateversubfolder.
Bouw het project en kopieer vervolges de bestanden uit js/build, inclusief .htaccess, naar de subfolder van je server.
npm run build
4. Tweaks
Ga naar js/internals/webpack/webpack.prod.babel.js en pas de gegevens in WebpackPwaManifest aan.
name: 'Weather',
short_name: 'Weather app tutorial',
description: 'Best app ever!',
background_color: '#00ffff',
theme_color: '#ffff00',
In js/app/images kun je de afbeeldingen vinden voor eigen (fav)icons. Bij een nieuwe build worden deze verwerkt.
5. Gebruik de PWA
Ga naar de site op je mobiel of tablet. In mijn geval ga ik naar emmanuelweethetwel.nl/weather in Chrome voor Android.






6. Nabranders
Dit is een eenvoudige tutorial. Het is een startpunt om kennis te maken met React, redux-saga en asynchroniciteit. Om het leuker te maken, heb ik het gecombineerd met scrapen. Scrapen is leuk, maar onbetrouwbaar als je een stabiele bron en een voorspelbare data-structuur nodig heb. Zeker in een professionele omgeving is scrapen geen geadviseerde techniek.
Al het bovenstaande uitzoekwerk heeft meerdere web-apps als resultaat gehad. Deze weer-app toont alle info van De Bilt. emmanuelweethetwel.nl/knmi/
Bekijk de tutorial-code op github.com 🔗.
Ik hoop dat dit je heeft geïnspireerd om je eigen app te maken!