Webpack 3 Sass cssnano Autoprefixer Workflow

Basic Webpack 3 Sass preprocessor workflow using cssnano for postprocessing minification and optimization. The Autoprefixer PostCSS plugin is also included in this configuration to automatically add vendor prefixes for only the browsers that need to be supported.

Before getting started, install Node.js and NPM. The default package manager for the Node.js JavaScript runtime environment, NPM is included with the Node.js installation.

Project

Navigate to the project root in your CLI, such as Terminal in OS X, PowersShell in Windows.

Enter npm init to interactively create a package manifest file named package.json. As dependencies are installed using the --save-dev option, this file will be updated so the entire project can be installed elsewhere using npm install or with its shorthand alias npm i.

npm init

Webpack 3

Webpack is a module bundler that generates static assets representing modules and their dependencies from a generated dependency graph. This enables a modular approach to web development that can be extended by using loaders with tasks that are performed when bundling files together.

Webpack version 3 was released earlier this year and as of this writing, version 3.8.1 is the latest. Install the latest version using npm install.

npm install --save-dev webpack

Open the project folder in a code editor, such as VS Code. Inspect the package.json file, webpack with its version number should now be listed under the devDependencies node. For example:

package.json
{
  "name": "myproject",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^3.8.1"
  }
}

Webpack Configuration

Create a new JavaScript file in the project root named webpack.config.js. At the top of this file define these required modules as constants: path, webpack and extract-text-webpack-plugin.

webpack.config.js
const path = require('path')
const webpack = require('webpack')
const ExtractTextPlugin = require('extract-text-webpack-plugin')

We’ve already installed webpack, and path is a core node module. Let’s install the Extract Text Plugin that will be used to extract the css from the bundle into a separate file.

npm install --save-dev extract-text-webpack-plugin

Next, create the webpack configuration object to export to the CLI.

webpack.config.js
...

module.exports = {
  context: path.resolve(__dirname, './src'),
  entry: {
    /* app: './js/index.js',*/
    css: './sass/main.scss',
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    publicPath: '/dist/',
    filename: 'bundle.js'
  },
  module: {
    rules: [
    {
      test: /\.(css|scss)$/,
      use: ExtractTextPlugin.extract({
        // TODO - Add Loaders
      })
    }]
  },
  plugins: [
    new ExtractTextPlugin('style.css'),
  ]
}
  • Throughout this tutorial you will encounter an ellipsis … in the code examples. These are not a part of the code and are there only to denote code that is being skipped and not applicable to the example. To view the entire file, examine the source code.

Install Loaders

Before we define the loaders in the webpack configuration, let’s install them.

SASS Loader compiles Sass to CSS and since it requires node-sass, install both sass-loader and node-sass.

npm install --save-dev sass-loader node-sass

PostCSS Loader processes CSS with PostCSS.

npm install --save-dev postcss-loader

CSS Loader resolves import at-rules and url functions in the CSS.

npm install --save-dev css-loader

Style Loader inlines <style></style> in the DOM.

npm install --save-dev style-loader

Define Loaders

In our configuration, we are extracting the CSS to its own file with the ExtractTextPlugin. Therefore we will define our loaders for processing Sass and CSS in the plugins use option. The fallback option is used when the CSS is not extracted.

webpack.config.js
...

  module: {
    rules: [
    {
      test: /\.(css|scss)$/,
      use: ExtractTextPlugin.extract({
        fallback: 'style-loader',
        use: [
          {
            loader: 'css-loader',
            options: {
              minimize: true || {/* CSSNano Options */}
            }
          },
          {
            loader: 'postcss-loader'
          },
          {
            loader: 'sass-loader'
          }
        ]
      })
    }]
  },
  • Note that the loaders are ordered from bottom to top or right to left. Loaders act like functions, that’s why it’s from right to left. For example, css-loader(postcss-loader(sass-loader(resource)))

The next page covers installing and configuring the Autoprefixer PostCSS plugin, running the Webpack build and creating demo Sass and html to test the output.


Google Maps API with Webpack

Google Map application that uses a draggable location marker to set address, longitude and latitude geocode inputs. This post covers some of the Webpack tools for both a local development environment and production build of the app constructed of a few JavaScript modules and css.

For application details, I encourage you to read my Google Maps API with Browserify post from a year ago. The following overview highlights building the refactored code with Webpack. This refactor brings the application more up to date with ES6 standards. Other changes inlcude the removal of jQuery as a dependency. All of the source code is available for download and browsing at GitHub.

NPM

Here is the updated package.json for installing webpack, loaders and other dependencies. This file also defines scripts to start and run commands.

package.json
{
  "name": "gmap-webpack",
  "version": "0.0.1",
  "description": "location inputs populated by google maps api, built with webpack",
  "main": "",
  "scripts": {
    "start": "cross-env NODE_ENV=development webpack-dev-server --progress --inline --open",
    "build": "cross-env NODE_ENV=production webpack"
  },
  "devDependencies": {
    "babel-core": "^6.0.0",
    "babel-loader": "^6.0.0",
    "babel-preset-es2015": "^6.0.0",
    "cross-env": "^3.2.3",
    "css-loader": "^0.25.0",
    "style-loader": "^0.13.2",
    "webpack": "^2.2.1",
    "webpack-dev-server": "^2.4.1"
  }
}

The npm start command is using the cross-env plugin to set the Node environment variable properly for the platform. The webpack-dev-server then bundles the modules and launches a static web server inline for live reloading.

The npm run build command uses cross-env to set the environment flag. Then webpack bundles the modules according to the webpack.config below optimized with a source-map for production.

webpack.config
const path = require('path')
const webpack = require('webpack')

module.exports = {
    context: path.resolve(__dirname, './src'),
    entry: {
        app: './js/index.js',
    },
    output: {
        path: path.resolve(__dirname, './dist'),
        publicPath: '/dist/',
        filename: 'bundle.js'
    },
    devtool: '#eval-source-map',
    module: {
        rules: [{
            test: /\.js$/,
            loader: 'babel-loader',
            exclude: /node_modules/
        },
        {
            test: /\.css$/,
            use: ['style-loader','css-loader']
        }]
    }
}

if (process.env.NODE_ENV === 'production') {
  module.exports.devtool = '#source-map'
  module.exports.plugins = (module.exports.plugins || []).concat([
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"'
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      sourceMap: true,
      compress: {
        warnings: false
      }
    }),
    new webpack.LoaderOptionsPlugin({
      minimize: true
    })
  ])
}

In the webpack.config file, the output.publicPath property is used by the webpack-dev-server to determine where the bundles should be served from.

The devtool property controls if and how source maps are generated. eval-source-map is for faster rebuild when in development. In production mode, devtool is set to use the appropriate source-map style instead.

To test drive the app and experiment with the dev server and live reloading, follow these steps assuming you have Node.js installed.

  1. Download and extract the source code or use git to clone the uiCookbook repository from https://github.com/jimfrenette/uiCookbook
  2. Navigate to the uiCookbook/geocode/gmap-webpack folder in your CLI, such as terminal or Cygwin.
  3. Run npm i or npm install
  4. Run npm start to bring up the dev server, bundle the modules and load the app in the browser.

Source Code


A Vue.js version of the Google Maps applicaton is available in the geocode/gmap-vue directory. The vue-cli webpack-simple scaffold was used to generate the Vue.js 2 project template.

WordPress Post from Front End using REST API and Vue.js

This post is a simple proof of concept for using the new WordPress REST API to submit a new post draft from the front end. The form and user inputs are built using the Vue.js framework and vue-cli to create a simple Webpack build configuration.

Environment

This example was created using a fresh install of WordPress 4.7 served by XAMMP on Windows 10.
Nodejs version 6.9.1
NPM version 3.10.8
vue-cli version 2.6.0
git version 2.7.0

Plugin Development

Change to the plugins directory. For example,

cd c:/xampp/htdocs/wordpress/wp-content/plugins

Create the directory for the new plugin. For example,

mkdir wp-api-vuejs-poc

Entry Point

Create a plugin entry point php file in this new plugin directory. For example,

# change to the new directory
cd wp-api-vuejs-poc

# create a new file
touch wp-api-vuejs-poc.php

Add the following php code to the plugin entry point. At the very least, add the comment block at the top to register the plugin with WordPress.

wp-api-vuejs-poc.php
<?php
/**
 * Plugin Name: WP API Vue.js Proof of Concept
 * Plugin URL: http://example.com
 * Description: WordPress plugin to draft posts from the front end using the new REST API and Vue.js
 * Version: 1.0
 * Author:
 * Author URI: http://example.com
 * Text Domain: wp-api-vuejs-poc
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

if ( ! class_exists( 'WP_API_Vuejs_PoC' ) ) :

/**
 * Main WP_API_Vuejs_PoC Class.
 */
final class WP_API_Vuejs_PoC {

    /**
     * The single instance of the class.
     */
    protected static $_instance = null;

    /**
     * Ensures only one instance of WP_API_Vuejs_PoC is loaded or can be loaded.
     */
    public static function instance() {
        if ( is_null( self::$_instance ) ) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }

    public function __construct() {
        $this->define_constants();
        $this->includes();
    }

    /**
     * Define Constants.
     */
    private function define_constants() {
        $this->define( 'WAVP_PLUGIN_PATH', plugin_dir_path( __FILE__ ) );
        $this->define( 'WAVP_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
    }

    private function define( $name, $value ) {
        if ( ! defined( $name ) ) {
            define( $name, $value );
        }
    }

    /**
     * What type of request is this?
     *
     * @param  string $type admin, ajax, cron or frontend.
     * @return bool
     */
    private function is_request( $type ) {
        switch ( $type ) {
            case 'admin' :
                return is_admin();
            case 'ajax' :
                return defined( 'DOING_AJAX' );
            case 'cron' :
                return defined( 'DOING_CRON' );
            case 'frontend' :
                return ( ! is_admin() || defined( 'DOING_AJAX' ) ) && ! defined( 'DOING_CRON' );
        }
    }

    public function includes() {
        if ( $this->is_request( 'frontend' ) ) {
            include( 'class-api-vpoc-page.php' );
        }
    }

}

endif;

/**
 * Main instance of WP_API_Vuejs_PoC.
 * Returns the main instance of WAVP to prevent the need to use globals.
 */
function WAVP() {
    return WP_API_Vuejs_PoC::instance();
}

// Global for backwards compatibility.
$GLOBALS['wp-api-vuejs-poc'] = WAVP();

Class for Front End Page Request

In the class of the plugin entry point file above, the includes function contains a request type check. When the request is made from the front end, include the class file for the page. Let’s create that class file now, for example,

touch class-api-vpoc-page.php

Add the following php code to create the class. The constructor method is called on the newly-created class object where we are subscribing to events using hooks.

The action hook is called during the page processing event for script loading, at this point, call our page_scripts function to load the app javascript.

The filter hook is called during data processing, when the content is being loaded, call our page_content function to create the app bootstrap element.

class-api-vpoc-page.php
<?php

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

/**
 * WP_API_Vuejs_PoC_Page class.
 */
class WP_API_Vuejs_PoC_Page {

    /**
     * Access
     */
    private static $user_can = 'edit_posts';
    private static $page_slug = 'api-test';

    /**
     * Constructor.
     */
    public function __construct() {
        add_action( 'wp_enqueue_scripts', array( $this, 'page_scripts' ) );
        add_filter( 'the_content', array( $this, 'page_content' ) );
    }

    public function page_scripts() {
        if ( is_page( self::$page_slug ) ) {
            // load the Vue.js app
            wp_enqueue_script( 'wp-api-vuejs-poc', WAVP_PLUGIN_URL . 'dist/build.js', array(), false, true );

            // localize data for script
            wp_localize_script( 'wp-api-vuejs-poc', 'wp_api_vuejs_poc', array(
                'rest_url' => esc_url_raw( rest_url() ),
                'nonce' => wp_create_nonce( 'wp_rest' ),
                'success' => __( 'Post submitted', 'wp-api-vuejs-poc' ),
                'failure' => __( 'Post could not be processed.', 'wp-api-vuejs-poc' ),
                'current_user_id' => get_current_user_id()
                )
            );
        }
    }

    public function page_content($content) {
        if ( is_page( self::$page_slug ) ) {
            // output only to logged in users who can edit posts
            if ( is_user_logged_in() && current_user_can( self::$user_can ) ) {
                // app bootstrap element
                ob_start();?>
                <div id="app"></div>
                <?php
                $content .= ob_get_clean();
            }else{
                $content .=  sprintf( '<a href="%1s">%2s</a>', esc_url( wp_login_url() ), __( 'Log in', 'wp-api-vuejs-poc' ) );
            }
        }

        return $content;
    }

}

return new WP_API_Vuejs_PoC_Page();

The next page covers the Vue.js front end.


Laravel JWT Auth with Vue.js 2

This post is a refresh of Laravel JWT Auth with Vue.js I posted in September that applies to the 1.x version of Vue.js. Evan You released vue.js version 2 shortly thereafter and this post will cover building the Laravel JSON Web Token based authentication with it.

February 11, 2017 – A lot has changed in the last 4 months with Vue.js and Webpack. Since Vue.js 2.1.x and Webpack 2.2.x were released last month, this post originally published in November has been updated for these new versions.

Included are some notes about refactoring the application from vue.js 1.x to version 2.1. Changes also include using NPM scripts with webpack version 2 for the front end build process instead of Elixir and Browserify.

Environment

For this tutorial, Laravel 5.2 has been installed locally and it is being served by XAMMP on Windows 10.

Command Line Tools
Nodejs version 6.9.4
NPM version 3.10.10
vue-cli version 2.8.1
git version 2.10.2

User Model

After setting the database connection parameters in env.php, run the database migration script to create the users, password_resets and migrations tables. In my Laravel application, I have moved the Users class into a Models folder.

php artisan migrate

mkdir app/Models

mv app/User.php app/Models

Edit app/Models/User.php, change the App namespace to App\Models.

User.php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    ...

  • Throughout this tutorial you will encounter an ellipsis … in the code examples. These are not a part of the code and are there only to denote code that is being skipped and not applicable to the example. To view the entire file, examine the source code.

Edit, config/auth.php. Update the authentication drivers user provider for the App\Models\User namespace change as follows.

auth.php
'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\Models\User::class,
    ],

    ...

Webpack Vue.js CLI

A simple webpack vue-loader setup is being used for the front end build process. First step is to install vue-cli globally.

npm install -g vue-cli
  • If you have an older version of vue-cli, you will need to update it to version 2.8.
npm update -g vue-cli

Using vue-cli from the laravel root directory, install the webpack-simple template into the existing laravel resources folder.

vue init webpack-simple resources

After the webpack-simple template has been installed. Change to the resources directory and run the npm install. This will install vue 2.1.x, vue-loader, vue-template-compiler, webpack 2.2.x and other dependencies. For details, inspect the install manifest resources/package.json.

cd resources

npm i
  • For example, the vue-cli 2.8.0 release from January, 2017 installs vue 2.1.10, vue-loader 10.0.2, vue-template-compiler 2.1.10 and webpack 2.2.0.

Vue Router and Components

Install the router for vue.js 2.x.

npm i vue-router --save-dev

Create a resources/assets/components folder and add an empty App.vue, Dashboard.vue, Home.vue, Register.vue and Signin.vue component.

mkdir assets/components

touch assets/components/App.vue

touch assets/components/Dashboard.vue

touch assets/components/Home.vue

touch assets/components/Register.vue

touch assets/components/Signin.vue

Add the following code to the App.vue base layout component. The new <router-link> component replaces the the deprecated v-link directive from vue.js 1.x.

App.vue
<template>
    <div class="panel panel-default">
        <div class="panel-heading">
            <nav>
                <ul class="list-inline">
                    <li>
                        <router-link :to="{ name: 'home' }">Home</router-link>
                    </li>
                    <li class="pull-right">
                        <router-link :to="{ name: 'register' }">Register</router-link>
                    </li>
                </ul>
            </nav>
        </div>
        <div class="panel-body">
            <router-view></router-view>
        </div>
    </div>
</template>

Add the following code to the Home.vue component.

Home.vue
<template>
    <h1>Laravel 5</h1>
</template>

Add the following code to the Register.vue component. Note the change from version 1.0 of this component. A div has been added at the root of the template since components now must have exactly one root element.

Register.vue
<template>
    <div>
        <div class="alert alert-danger" v-if="error && !success">
            <p>There was an error, unable to complete registration.</p>
        </div>
        <div class="alert alert-success" v-if="success">
            <p>Registration completed. You can now sign in.</p>
        </div>
        <form autocomplete="off" v-on:submit="register" v-if="!success">
            <div class="form-group" v-bind:class="{ 'has-error': error && response.username }">
                <label for="name">Name</label>
                <input type="text" id="name" class="form-control" v-model="name" required>
                <span class="help-block" v-if="error && response.name">{{ response.name }}</span>
            </div>
            <div class="form-group" v-bind:class="{ 'has-error': error && response.email }">
                <label for="email">E-mail</label>
                <input type="email" id="email" class="form-control" placeholder="gavin.belson@hooli.com" v-model="email" required>
                <span class="help-block" v-if="error && response.email">{{ response.email }}</span>
            </div>
            <div class="form-group" v-bind:class="{ 'has-error': error && response.password }">
                <label for="password">Password</label>
                <input type="password" id="password" class="form-control" v-model="password" required>
                <span class="help-block" v-if="error && response.password">{{ response.password }}</span>
            </div>
            <button type="submit" class="btn btn-default">Submit</button>
        </form>
    </div>
</template>

JavaScript

Create a resources/assets/js folder for the JavaScript.

mkdir assets/js

Create a resources/assets/js/app.js JavaScript file.

touch assets/js/app.js

Add the following code to the app.js file to import vue modules, components and define the router. Exporting will allow other modules in this project to import them.

Note the changes between version 1.0 of app.js.

  1. router.map has been replaced with an array on the new routes option.
  2. router is passed to the new Vue instance as option since starting an app with routing no longer requires a special method.
app.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from '../components/App.vue';
import Dashboard from '../components/Dashboard.vue';
import Home from '../components/Home.vue';
import Register from '../components/Register.vue';
import Signin from '../components/Signin.vue';

Vue.use(VueRouter);

export default Vue;

export var router = new VueRouter({
    routes: [
        {
            path: '/',
            name: 'home',
            component: Home
        },
        {
            path: '/register',
            name: 'register',
            component: Register
        }
    ]
});

new Vue({
    el: '#app',
    router: router,
    render: app => app(App)
});

Web Page

Edit the resources/views/welcome.blade.php template.

Replace the entire contents of the file with this markup.

welcome.blade.php
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="csrf-token" content="{!! csrf_token() !!}">
    <title>Laravel</title>

    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

</head>
<body>
    <div class="container">
        <div id="app"></div>
    </div>
    <script src="/js/app.js"></script>
</body>
</html>

Webpack

Edit the resources/webpack.config.js entry and output values.

REPLACE

module.exports = {
  entry: './src/main.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    publicPath: '/dist/',
    filename: 'build.js'
  },
  ...
}

WITH

module.exports = {
  entry: './assets/js/app.js',
  output: {
    path: path.resolve(__dirname, '../public/js'),
    publicPath: '/js/',
    filename: 'app.js'
  },
  ...
}

Test drive the updated webpack configuration with the npm build script.

npm run build

Your build output should be similar to this. The laravel public folder should now contain js/app.js and a respective js/app.js.map source map.

> resources@ build C:\xampp\htdocs\laravel\resources
> cross-env NODE_ENV=production webpack --progress --hide-modules

Hash: 260fd0b01225d3f55a16
Version: webpack 2.2.1
Time: 6031ms
     Asset    Size  Chunks             Chunk Names
    app.js  118 kB       0  [emitted]  main
app.js.map  884 kB       0  [emitted]  main
Laravel JWT Auth Vue.js 2.0 Home Page

The next page covers Vue Resource for request handling, Form Request Validation, API authorization with JWT Auth, User endpoint and Sign in.


Vue.js CLI Webpack Laravel Proof of Concept

This post is a simple proof of concept for using vue-cli to scaffold a Webpack Laravel project. The production build modifies the default Laravel blade view and outputs the built assets into the public/static directory. The development workflow demonstrates hot reloading and css style extraction.

Environment

This example was created using a fresh install of Laravel 5.2 served by XAMMP on Windows 10.
Nodejs version 6.9.1
NPM version 3.10.8
vue-cli version 2.4.0
git version 2.7.0

Development

Change to the laravel directory. For example,

cd c:/xampp/htdocs/laravel

Use vue-cli to download vuejs, wepbpack and template dependencies and create the project in the laravel/resources folder.

vue init webpack resources

Change to the resources directory.

cd resources

Install node modules with npm install or npm i.

npm i

Run development server.

  • Check the dev.port in resources/config/index.js to make sure it is not the same as the port used by the XAMPP Apache server.
npm run dev

Update the /resources/src/components/Hello.vue vue commponent to see the hot reloading take place. In the script portion of the component, change the msg value. For example, ‘Welcome to Your Vue.js Laravel App’.

Hello.vue
<script>
export default {
  name: 'hello',
  data () {
    return {
      msg: 'Welcome to Your Vue.js Laravel App'
    }
  }
}
</script>
  • For development on Laravel sever generated pages, another solution is to update the package.json scripts property so the npm run dev command runs webpack development compilation with watch mode enabled. Watch mode recompiles after every change is saved.
package.json
"dev": "cross-env NODE_ENV=development webpack --watch --progress --colors"

Production

Edit /resources/config/index.js. Update the build paths so the welcome.blade.php view gets the index markup and the assets are built to the public/static folder.

index.js
build: {
    ...
    index: path.resolve(__dirname, '../views/welcome.blade.php'),
    assetsRoot: path.resolve(__dirname, '../../public'),
  • The ellipsis … in the snippet above is not a part of the code and is there only to denote lines that are skipped and not applicable to the example.

Run production build.

npm run build

Manually refresh the laravel.dev page served by XAMPP to verify the production build.

Resources