React + webpack + WordPress
Me llaman Ignacio
Me llaman Ignacio
Me llaman Ignacio
- Especializado en WordPress Multisitio
- Empecé con WordPress de resaca
- Especializado en empezar cosas que nunca acabo
- Mago frustrado
Agenda del día
ECMAScript 6
let y const
for (let i = 0; i < a.length; i++) {
let x = a[i]
…
}
for (let i = 0; i < b.length; i++) {
let y = b[i]
…
}
const myConst = 10;
Arrow functions
// ES5
function( param1, param2 ) {
return param1 + param2;
}
// ES6
( param1, param2 ) => {
return param1 + param2;
}
( param1, param2 ) => param1 + param2;
myList.map( ( element ) => {
return element + 1;
});
$el.click( ( e ) => {
// this no es el elemento.
});
Rest parameter
function foo(x, y, ...a) {
return (x + y) * a.length
}
foo(1, 2, "hello", true, 7);
Property shorthand
// ES5
var x = 'value1';
var y = 'value2';
obj = { x: x, y: y }
// ES6
obj = { x, y }
Classes
class Shape {
constructor (id, x, y) {
this.id = id
this.move(x, y)
}
move (x, y) {
this.x = x
this.y = y
}
}
Babel
¿Qué es Babel?
- Transpilador de ES6 (que no entienden los navegadores) a ES5 (lo que sí entienden)
- Demo: https://babeljs.io/
webpack
¿Qué es webpack ?
- Bundler modular
- Automatiza los procesos de transpilación (a través de Babel) y preprocesado de código (parecido a Grunt o Gulp)
- Sólo entiende JavaScript con su instalación base
- Se le indica un archivo de entrada JS y otro de salida. El primero puede llamar a otros archivos.
- Durante el procesado, genera un gráfico de dependencias
Webpack: conceptos
- Entry: Punto de entrada a la aplicación. Desde donde webpack generará el gráfico. Es el primer archivo que lanzará tu aplicación
- Output: Fichero de salida. Donde irá el resultado del procesado.
- Loaders: Capaces de entender archivos no-JS (CSS, Sass...).
- Plugins: Pues plugins :)
webpack: Instalación
En la carpeta de nuestro proyecto:
npm run install --save-dev webpack
webpack: Configuración
- Fichero webpack.config.js
const path = require('path');
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js'
}
};
webpack: Workflow
- Durante el desarrollo:
- En producción:
webpack --watch
webpack -p
Una guía completa
React
¿Qué es React?
- Librería para UIs
- No es un Framework. Sólo se encarga de la vista.
- Extremadamente declarativo.
- Basado en componentes reutilizables
- Los componentes reaccionan al estado de la aplicación.
- JavaScript + JSX (XML para construir los componentes)
- Genera un DOM Virtual
- El DOM cambiará el DOM real sólo las partes que son necesarias
Componente sencillo con props
class Button extends React.Component {
render() {
return <button>Hello {this.props.name}</button>;
}
}
// Dentro de mi aplicación
...
<Button name="ignacio" />
...
Componente sencillo con estado
class Button extends React.Component {
constructor( props ) {
super( props );
this.state = {
clickCounts: 0
}
this.increment = this.increment.bind( this );
}
increment() {
this.setState( { clickCounts : this.state.clickCounts + 1 } );
}
render() {
return <button onClick={ this.increment }>Hello {this.props.name}</button>;
}
}
Aplicación práctica
Repositorio
Estado inicial : Un sencillo plugin
- Sólo una página de administración debajo de Ajustes con un div#app
- wp_enqueue_scripts para encolar js/app.js (no existe todavía)
- _src/app.js: Crea una lista a partir de un array de todos
- _src/helpers.js: Funciones de utilidad.
- _src/app.js: Entry point para webpack.
Configuración inicial
npm init
npm install --save-dev webpack babel-core babel-loader babel-preset-es2015
Configuración inicial:webpack.config.js
webpack.config.js
let path = require('path');
module.exports = {
entry: './_src/app.js',
output: {
path: path.resolve( __dirname, 'js' ),
filename: 'app.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['es2015']
}
}
}
]
},
};
Configuración inicial: ejecución de webpack
./node_modules/.bin/webpack
Hash: e4b05c7393919d7eba0d
Version: webpack 2.6.1
Time: 2677ms
Asset Size Chunks Chunk Names
app.js 3.32 kB 0 [emitted] main
[0] ./_src/helpers.js 267 bytes {0} [built]
[1] ./_src/app.js 314 bytes {0} [built]
Pasar lista de todos desde PHP. meetup-plugin.php
$init_state = array(
'todos' => array(
array(
'title' => 'Hacer la compra',
'id' => 1,
),
array(
'title' => 'Médico a las 11',
'id' => 2
)
)
);
wp_localize_script( 'meetup', 'Meetup_Init_State', $init_state );
Pasar lista de todos desde PHP. _src/helpers.js
export const getTodos = () => {
return Meetup_Init_State.todos;
};
Configuración inicial: npm scripts. package.json
...
"scripts": {
"build": "webpack -p",
"watch": "webpack --watch"
},
...
Configuración inicial: Ejecución npm
npm run build
npm run watch
Source Maps. webpack.config.js
- Muchos tipos distintos para webpack: https://webpack.js.org/guides/development/#source-maps
- Nosotros usaremos cheap-module-source-map
...
devtool: 'cheap-module-source-map'
}
Sass: Instalación de loaders
npm install sass-loader style-loader node-sass css-loader --save-dev
Sass: _src/scss/style.scss
ul.todos {
li.todo-item {
padding:20px;
border:1px solid #CFCFCF;
width:100%;
}
}
Sass: _src/app.js
import { getTodos } from './helpers';
import styles from './scss/style.scss';
...
Un alto en el camino
Movemos _src/*.js a _src/js/ por tener más separada la cosa
Sass. webpack.config.js
...
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['es2015']
}
}
},
{
test: /\.scss$/,
use: [ 'style-loader', 'css-loader', 'sass-loader' ]
}
]
},
...
Sass
- Un problema: Los estilos se han embebido en un tag <style>
- Reto: Extraer los estilos a un .css
Extraer estilos a .css
npm install --save-dev extract-text-webpack-plugin
Extraer estilos a un .css. webpack.config.js
let path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractSass = new ExtractTextPlugin( '../css/style.css' );
module.exports = {
...
module: {
rules: [
...
{
test: /\.scss$/,
loader: extractSass.extract(['css-loader', 'sass-loader'])
}
]
},
devtool: 'cheap-module-source-map',
plugins: [
extractSass
]
};
Extraer estilos a un .css. meetup-plugin.php
...
$plugin_url = plugin_dir_url( __FILE__ );
wp_enqueue_script( 'meetup', $plugin_url . 'js/app.js', array(), '', true );
wp_enqueue_style( 'meetup', $plugin_url . 'css/style.css');
...
Custom Post Type: Todo. meetup-plugin.php
...
...
add_action( 'init', function() {
register_post_type( 'todo', array(
'description' => 'Todos',
'labels' => array(
'name' => _x( 'Todos', 'post type general name', 'todo' ),
'singular_name' => _x( 'Todo', 'post type singular name', 'todo' ),
'menu_name' => _x( 'Todos', 'admin menu', 'todo' ),
'name_admin_bar' => _x( 'Todo', 'add new todo on admin bar', 'todo' ),
'add_new' => _x( 'Add New', 'post_type', 'todo' ),
'add_new_item' => __( 'Add New Todo', 'todo' ),
'edit_item' => __( 'Edit Todo', 'todo' ),
'new_item' => __( 'New Todo', 'todo' ),
'view_item' => __( 'View Todo', 'todo' ),
'search_items' => __( 'Search Todos', 'todo' ),
'not_found' => __( 'No todos found.', 'todo' ),
'not_found_in_trash' => __( 'No todos found in Trash.', 'todo' ),
'parent_item_colon' => __( 'Parent Todo:', 'todo' ),
'all_items' => __( 'All Todos', 'todo' ),
),
'public' => false,
'hierarchical' => false,
'exclude_from_search' => true,
'publicly_queryable' => false,
'show_ui' => true,
'show_in_menu' => true,
'show_in_nav_menus' => false,
'show_in_admin_bar' => false,
'menu_position' => null,
'menu_icon' => null,
'capability_type' => 'post',
'capabilities' => array(),
'map_meta_cap' => null,
'supports' => array( 'title' ),
'has_archive' => false,
'show_in_rest' => true
) );
});
...
REST API: Todo.
npm install --save-dev whatwg-fetch es6-promise
Instalamos una librería JS para hacer llamadas a la REST API fácilmente
REST API: Todo. _src/js/Fetcher.js
require( 'es6-promise' ).polyfill();
import { getRESTAPIUrl, getRESTAPINonce } from './helpers';
import 'whatwg-fetch';
function MeetupFetcher() {
const url = getRESTAPIUrl();
const nonce = getRESTAPINonce();
const request = function( url, params ) {
return fetch( url, params )
.then( ( response ) => {
return response.json();
});
};
return {
getTodos: () => {
let params = {
credentials: 'same-origin',
headers: {
'X-WP-Nonce': nonce
}
};
return request( `${url}/todo`, params );
}
};
}
const Fetcher = new MeetupFetcher();
export default Fetcher;
REST API: Todo. _src/js/app.js
import { getTodos } from './helpers';
import styles from '../scss/style.scss';
import Fetcher from './Fetcher';
let ul = document.createElement( 'ul' );
ul.classList.add( 'todos' );
let app = document.getElementById( 'app' );
Fetcher.getTodos()
.then( ( response ) => {
response.map( ( todo ) => {
let li = document.createElement( 'li' );
li.classList.add( 'todo-item' );
li.innerHTML = todo.title.rendered;
ul.append( li );
});
app.innerHTML = '';
app.append( ul );
});
REST API: Todo. _src/js/helpers.js
export const getRESTAPIUrl = () => {
return Meetup_Init_State.apiUrl;
};
export const getRESTAPINonce = () => {
return Meetup_Init_State.apiNonce;
};
REST API: Todo. meetup-plugin.php
...
$init_state = array(
'apiUrl' => esc_url_raw( rest_url( 'wp/v2' ) ),
'apiNonce' => wp_create_nonce( 'wp_rest' )
);
...
<div class="wrap">
<div id="app">Loading...</div>
</div>
...
Configuración para React: Dependencias
npm install --save-dev babel-preset-react react react-dom
Configuración para React: webpack.config.js
...
presets: ['es2015', 'react']
...
Primer componente en React: _src/js/app.js
import { getTodos } from './helpers';
import styles from '../scss/style.scss';
import Fetcher from './Fetcher';
import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component {
render() {
return <ul className="todos">
<li className="todo-item">One todo</li>
<li className="todo-item">Another todo</li>
</ul>;
}
}
ReactDOM.render( <App />, document.getElementById( 'app' ) );
Nos olvidamos por un momento de la REST API
Añadiendo estado a la App: _src/js/app.js
...
class App extends React.Component {
constructor() {
super();
this.state = {
todos: [
{
title: 'One todo',
id: 1
},
{
title: 'Another todo',
id: 2
}
]
}
}
render() {
const todos = this.state.todos.map( ( todo ) => {
return <li key={ todo.id } className="todo-item">{ todo.title }</li>
});
return <ul className="todos">
{ todos }
</ul>;
}
}
...
Eliminar Todos: _src/js/app.js
...
deleteTodo( todoId ) {
const todos = this.state.todos.filter( ( todo ) => {
return ! ( todo.id === todoId );
});
this.setState( { todos } );
}
...
const todos = this.state.todos.map( ( todo ) => {
return <li key={ todo.id } className="todo-item">
{ todo.title }
<span
className="todo-delete"
onClick={ this.deleteTodo.bind( this, todo.id ) }>
Delete
</span>
</li>
});
Estilos para el componente: _src/scss/style.scss
ul.todos {
li.todo-item {
padding:20px;
border:1px solid #CFCFCF;
width:80%;
span.todo-delete {
color:red;
cursor:pointer;
float:right;
}
}
}
De vuelta a la REST API: _src/js/app.js
...
constructor() {
super();
this.state = {
todos: []
};
}
loadTodos() {
Fetcher.getTodos()
.then( ( todos ) => {
todos = todos.map( ( todo ) => {
return {
id: todo.id,
title: todo.title.rendered
}
});
this.setState( { todos } );
});
}
...
Iniciamos los todos como una lista vacía y añadimos nuevo método para cargarlos
¿Cuándo los cargamos? Ciclo de vida de componentes
- Un componente pasa por diferentes estados durante un ciclo de vida
Ciclo de vida de componentes: Mounting
- constructor()
- componentWillMount()
- render()
- componentDidMount()
Ciclo de vida de componentes: Updating
- componentWillReceiveProps()
- shouldComponentUpdate()
- componentWillUpdate()
- render()
- componentDidUpdate()
Ciclo de vida de componentes: Umounting
- componentWillUnmount()
- shouldComponentUpdate()
- componentWillUpdate()
- render()
- componentDidUpdate()
Cargando todos: _src/js/app.js
...
componentDidMount() {
this.loadTodos();
}
...
Cargando todos:
meetup-plugin.php
...
function render_meetup_plugin_menu() {
?>
<div class="wrap">
<div id="app">Loading...</div>
</div>
<?php
}
Borrando todos con la REST API: _src/js/app.js
...
deleteTodo( todoId ) {
const todos = this.state.todos.filter( ( todo ) => {
if ( todo.id === todoId ) {
Fetcher.deleteTodo( todo.id );
return false;
}
return true;
});
this.setState( { todos } );
}
...
Borrando todos con la REST API: _src/js/Fetcher.js
...
deleteTodo: ( id ) => {
let params = {
credentials: 'same-origin',
headers: {
'X-WP-Nonce': nonce,
'Content-type': 'application/json'
},
method: 'delete'
};
return request( `${url}/todo/${id}`, params );
}
...
Separación de componentes: _src/js/components/TodoItem.js
import React, {PropTypes} from 'react';
export default class TodoItem extends React.Component {
render() {
return <li className="todo-item">
{ this.props.title }
<span
className="todo-delete"
onClick={ this.props.onDelete }
>
Delete
</span>
</li>;
}
}
Separación de componentes: _src/js/app.js
import TodoItem from './components/TodoItem';
...
render() {
const todos = this.state.todos.map( ( todo ) => {
return <TodoItem
onDelete={ this.deleteTodo.bind( this, todo.id ) }
key={ todo.id }
title={ todo.title }
/>
});
...
This is
The End
This is
The alt End
React-webpack-WordPress
By Ignacio Cruz Moreno
React-webpack-WordPress
- 2,713