Git Product home page Git Product logo

weak's Introduction

Build Status

weak

Introduction

This is a step by step guide for creating a simple web app using Webpack, Eslint, Angular and Karma.

Quickstart

If you just want to see the result, you can clone this repo and run the following scripts. After this you can browse to http://localhost:8080/src/ to see the app running.

npm install
npm start

Setup

Start by creating a folder for your project and initalizing a git repo, an npm package and some useful files.

mkdir weak
cd weak
git init
npm init
touch .gitignore
touch README.md

.gitignore

node_modules

Use the README.md file to take notes. You will thank yourself later.

Also try to commit your changes every once in a while.

git add .
git commit -m"my commit message"

Hello from angular

Basic setup is done. We will now create a hello world app using angular. We will use npm to install angular and create an index.html file and a app.js file.

npm install angular --save
mkdir src
touch src/index.html
mkdir src/app
touch src/app/app.js

Copy the following to index.html. We include angular and our app script. Angular is initialized using "ng-app". The ng-model attribute tells angular that we want to connect the variable name with the text inside the input. The ng-show attribute tells angular to only display the h1 element if the name has some content. The value "{{ name }}" tells angular that we want the value of the variable name.

<!doctype html>
<html>

<head>
    <meta charset="utf-8">
    <title>weak</title>
    <script type="text/javascript" src="../node_modules/angular/angular.js" charset="utf-8"></script>
    <script type="text/javascript" src="app/app.js" charset="utf-8"></script>
</head>

<body>
    <div ng-app="app">
        <label>Name:
            <input type="text" ng-model="name" />
        </label>
        <h1 ng-show="name">Hello {{ name }}</h1>
    </div>
</body>

</html>

Our app.js can be very simple for now. It just defines a module named app.

angular.module('app', [])

We can test our hello world app by starting a webserver and browsing to http://localhost:8000/src/. You can fill in your name and should see "Hello your name".

python -m SimpleHTTPServer

Gittermezzo

We now have a functioning app. Maybe it's time to make sure our code gets saved somewhere safe. Create an empty repo on your github account and commit your changes to it.

git remote add origin https://github.com/bartw/weak.git
git push origin master

Webpack

As our app grows we will add more js files and use more libraries. Maintaining the script tags in our index.html will become almost impossible. So let's introduce webpack to save us from all the crap.

npm install webpack --save-dev
touch webpack.config.js

We start by configuring webpack in webpack.config.js. We tell webpack where our entry file is and where we want our bundled output to be saved.

module.exports = {
    entry: './src/app/app.js',
    output: {
        filename: './src/bundle.js'
    }
};

Webpack wants us to require our dependencies. Our only dependency for now is angular.

require('angular');

angular.module('app', [])

Webpack will bundle all our js files in one file "bundle.js". This will make maintaining index.html a breeze.

<!--replace-->
<script type="text/javascript" src="../node_modules/angular/angular.js" charset="utf-8"></script>
<script type="text/javascript" src="app/app.js" charset="utf-8"></script>
<!--with-->
<script type="text/javascript" src="bundle.js" charset="utf-8"></script>

Don't forget to update the .gitignore so we don't check in our bundled file.

bundle.js

Now we can test it out, run webpack, start a server and browse to http://localhost:8000/src/.

webpack
python -m SimpleHTTPServer

Components

Angular 1.5 introduced components. With components you can create your own html elements. Let's create our own component.

First update the app.js. In template we write the html of our component. Each binding is an attribute of our own component. The controller contains the logic of our component.

require('angular');

angular.module('app', [])
    .component('helloWorld', {
        template: '<div ng-show="$ctrl.name"><span>Hey what\'s up {{$ctrl.name}}?</span> <span>{{$ctrl.reverse()}}</div>',
        bindings: { name: '<' },
        controller: function() {
            var ctrl = this;
            
            ctrl.reverse = function() {
                return ctrl.name ? ctrl.name.split('').reverse().join('') : '';
            };
        }
    });

Then we use our component in index.html, every capital is replaced with a dash.

<!--replace-->
<h1 ng-show="name">Hello {{ name }}</h1>
<!--with-->
<hello-world name="name"></hello-world>

We made our own html element! Run webpack, start a server and browse to http://localhost:8000/src/ to check it out.

webpack
python -m SimpleHTTPServer

Testing

How can we be sure that the logic in our component does what we think it does? We write unit tests of course.

We will use karma and jasmine to run and write our tests.

npm install jasmine-core phantomjs-prebuilt karma karma-cli karma-jasmine karma-phantomjs-launcher karma-webpack karma-sourcemap-loader angular-mocks --save-dev
touch karma.config.js
touch src/tests.js
touch src/app.spec.js

Karma needs some configuration, this is stored in karma.config.js. We will write a file that serves as an entry file for our tests. We configure karma to build this using webpack and add sourcemaps to it.

module.exports = function(config) {
    config.set({
        basePath: '',
        frameworks: ['jasmine'],
        reporters: ['progress'],
        port: 9876,
        colors: true,
        logLevel: config.LOG_INFO,
        autoWatch: true,
        browsers: ['PhantomJS'],
        singleRun: false,
        autoWatchBatchDelay: 300,
        files: ['./src/tests.js'],
        preprocessors: { './src/tests.js': ['webpack', 'sourcemap'] },
        webpack: {
            devtool: 'inline-source-map',
        },
        webpackMiddleware: { noInfo: true }
    });
};

Our test entry file is tests.js. We require angular and angular-mocks to be able to use angular with unit testing. Then we add all test files eg files that end in .spec.js.

require ('angular');
require ('angular-mocks');

var testsContext = require.context('.', true, /.spec$/);
testsContext.keys().forEach(testsContext);

We create a test for our component in app.spec.js.

require('./app.js');

describe('app', function() {
    beforeEach(angular.mock.module('app'));

    describe('with $compile', function() {
        var $compile, $rootScope, element, scope;

        beforeEach(angular.mock.inject(function(_$compile_, _$rootScope_) {
            $compile = _$compile_;
            $rootScope = _$rootScope_;
        }));
        
        beforeEach(function() {
            scope = $rootScope.$new();
            element = angular.element('<hello-world name="name"></hello-world>');
            element = $compile(element)(scope);
            scope.name = 'bart';
            scope.$apply();
        });

        describe('Controller: helloWorld', function() {
            var controller;
            beforeEach(function() {
                controller = element.controller('helloWorld');
            });

            it('should reverse', function() {
                controller.name = 'bart'
                expect(controller.reverse()).toBe('trab');
            });
        });
    });
});

Now we kan run our tests and watch for changes.

karma start karma.config.js

Linting

We always write perfect clean javascript. But what if we make a typo or just forget to use a semicolon? We need something to keep an eye on us. Say hello to Eslint.

npm install eslint eslint-loader eslint-config-angular eslint-plugin-angular --save-dev
touch .eslintrc.json

You can configure eslint yourself or extend some default rules. We will extend the recommended and angular rules in .eslintrc.json.

{
    "env": {
        "browser": true,
        "commonjs": true,
        "jasmine": true
    },
    "extends": ["eslint:recommended", "angular"],
    "rules": {
        "strict": [
            2,
            "global"
        ],
        "indent": [
            "error",
            4
        ],
        "linebreak-style": [
            "error",
            "unix"
        ],
        "quotes": [
            "error",
            "single"
        ],
        "semi": [
            "error",
            "always"
        ],
        "angular/dumb-inject": 2
    }
}

In our karma.config.js we add a module for eslint.

webpack: {
            devtool: 'inline-source-map',
            module: {                
                loaders: [{
                    test: /\.js$/,
                    loader: "eslint-loader",
                    exclude: /node_modules/
                }]
            }
        },

Now run our tests and linting again and start fixing errors. Everytime you save a file the linting and testing will run again.

karma start karma.config.js

Serve with webpack and consolidate

Let's get rid of python's SimpleHTTPServer and also serve using webpack.

npm install webpack-dev-server --save-dev

In package.json create some scripts. The "build" script will simply run webpack and create a bundle.js file. The "serve" script will start a webpack dev server so we can test our app in the browser. The "test" script will lint our code and run the tests. The "start" script will start the "test" and "serve" scripts in parrallel. You can start a script using "npm run script" or "npm script" for default scripts.

"scripts": {
    "build": "webpack",
    "serve": "webpack-dev-server --progress -d --colors",
    "test": "karma start karma.config.js",
    "start": "npm run test & npm run serve"
  }

We can now start our project with the following command. Then we can browse to http://localhost:8080/src/.

npm start

Cleaner angular

As our project grows we will need more components, controllers, services and stuff. We can't just cram all of them in one single app.js file. So let's cleanup our js.

We will start by creating the files and folders that we will use to organise our js files. We also install a raw loader that we will use to load html from an external file.

npm install raw-loader --save-dev
mkdir src/app/components
touch src/app/components/components.js
mkdir src/app/components/helloworld
touch src/app/components/helloworld/helloWorld.js
touch src/app/components/helloworld/helloWorld.component.js
touch src/app/components/helloworld/helloWorld.controller.js
touch src/app/components/helloworld/helloWorld.html

Now let's extract our component from app.js and cut it up into small files.

We will put our template in helloWorld.html.

<div ng-show="$ctrl.name">
    <span>Hey what's up {{$ctrl.name}}?</span> <span>{{$ctrl.reverse()}}</span>
</div>

Our controller function belongs in helloWorld.controller.js

'use strict';

module.exports = function HelloWorldController() {
    var helloWorldController = this;

    helloWorldController.reverse = function() {
        return helloWorldController.name ? helloWorldController.name.split('').reverse().join('') : '';
    };
};

In the helloWorld.component.js file we require the controller, the template and configure the rest of the component.

'use strict';

var controller = require('./helloWorld.controller');

module.exports = {
    template: require('./helloWorld.html'),
    bindings: { name: '<' },
    controller: controller
};

We create a helloWorld module and add the helloWorldComponent to it in helloWorld.js

'use strict';

require('angular');

var helloWorldComponent = require('./helloWorld.component');

module.exports = angular.module('helloWorld', [])
    .component('helloWorld', helloWorldComponent);

In components.js we require the helloworld module and add it to the app.components module.

'use strict';

require('angular');

var HelloWorld = require('./helloworld/helloWorld');

angular.module('app.components', [
    HelloWorld.name
]);

module.exports = angular.module('app.components');

Now we can cleanup the app.js and require our components module.

'use strict';

require('angular');

var Components = require('./components/components');

angular.module('app', [ Components.name ]);

We still have to add the raw loader to our webpack config in webpack.config.js

module.exports = {
    entry: './src/app/app.js',
    output: {
        filename: './src/bundle.js'
    },
    module: {
        loaders: [
            { test: /\.html$/, loader: 'raw' }
        ]
    }
};

and karma.config.js.

webpack: {
            devtool: 'inline-source-map',
            module: {
                loaders: [
                    { test: /\.js$/, loader: "eslint-loader", exclude: /node_modules/ },
                    { test: /\.html$/, loader: 'raw' }
                ]
            }
        },

There seems to be a little duplication going on there. Maybe we can fix that later.

We can now start our project with the following command. Then we can browse to http://localhost:8080/src/. Everything should work as before and our tests should still be green.

npm start

Automating tests with Travis CI

We can automate our tests on github using Travis CI.

First we create a config file and specify that we have a nodejs project and what version we use.

touch .travis.yml
language: node_js
node_js:
  - "4.4.1"

We create a new karma configuration file for the travis tests.

touch karma.travis.config.js
module.exports = function(config) {
    config.set({
        basePath: '',
        frameworks: ['jasmine'],
        reporters: ['progress'],
        port: 9876,
        colors: true,
        logLevel: config.LOG_INFO,
        browsers: ['PhantomJS'],
        singleRun: true,
        files: ['./src/tests.js'],
        preprocessors: { './src/tests.js': ['webpack', 'sourcemap'] },
        webpack: {
            devtool: 'inline-source-map',
            module: {
                loaders: [
                    { test: /\.js$/, loader: "eslint-loader", exclude: /node_modules/ },
                    { test: /\.html$/, loader: 'raw' }
                ]
            }
        },
        webpackMiddleware: { noInfo: true }
    });
};

Then we have to update our test script in package.json so that it tests only once.

"scripts": {
    "build": "webpack",
    "serve": "webpack-dev-server --progress -d --colors",
    "testwatch": "karma start karma.config.js",
    "test": "karma start karma.travis.config.js",
    "start": "npm run testwatch & npm run serve"
  },

After we commit and push to github we can go to https://travis-ci.org/ and login with github. Now we can enable travis for our repo. And it will start a build for our repo!

Check https://docs.travis-ci.com/user/status-images/ to learn how we can show that cool badge.

Less

We start by installing a style, css and less loader. And we need a less file four our hello world component.

npm install style-loader css-loader less-loader less --save-dev
touch src/app/components/helloworld/helloWorld.less

In all our webpack configs, that's karma.config.js, karma.travis.config.js and webpack.config.js, we need to add a loader for the less files.

{ test: /\.less$/, loader: "style!css!less" }

In helloWorld.js we need to require our less file.

require('./helloWorld.less');

We'll just make some text green so helloWorld.less can be very short.

.helloWorld {
    .greenText {
        color: green;
    }   
}

Now all that's left is helloWorld.html

<div ng-show="$ctrl.name" class="helloWorld">
    <span>Hey what's up {{$ctrl.name}}?</span> <span class="greenText">{{$ctrl.reverse()}}</span>
</div>

If we start the app, our reverse name should be in green.

Clean and release

Time to clean our config files and making a release build.

npm install html-webpack-plugin --save-dev
mv src/index.html src/index.ejs
rm karma.travis.config.js
touch karma.shared.config.js
touch karma.watch.config.js
touch webpack.debug.config.js
touch webpack.release.config.js

In webpack.config.js we will put the shared webpack config.

module.exports = {
    entry: './src/app/app.js',
    module: {
        loaders: [
            { test: /\.html$/, loader: 'raw' },
            { test: /\.less$/, loader: "style!css!less" }
        ]
    },
    plugins: []
};

In webpack.debug.config.js we will load the shared config and add some extras. We also added the HtmlWebpackPlugin to build our own index.html from the index.ejs template.

var HtmlWebpackPlugin = require('html-webpack-plugin');
var webpackConfig = require('./webpack.config');

webpackConfig.output = {
    filename: './debug/bundle.js'
};
webpackConfig.devtool = 'inline-source-map';
webpackConfig.plugins.push(new HtmlWebpackPlugin({
    template: './src/index.ejs',
    filename: './debug/index.html'
}));

module.exports = webpackConfig;

In webpack.release.config.js we will load the shared config and add some extras. We also added the HtmlWebpackPlugin to build our own index.html from the index.ejs template.

var HtmlWebpackPlugin = require('html-webpack-plugin');
var webpackConfig = require('./webpack.config');

webpackConfig.output = {
    filename: './release/bundle.js'
};
webpackConfig.plugins.push(new HtmlWebpackPlugin({
    template: './src/index.ejs',
    filename: './release/index.html'
}));

module.exports = webpackConfig;

In karma.shared.config.js we'll load the webpack debug config and put the shared karma config.

var webpackConfig = require('./webpack.debug.config');
webpackConfig.entry = {};
webpackConfig.module.loaders.push({ test: /\.js$/, loader: "eslint-loader", exclude: /node_modules/ });

module.exports = function(config) {
    config.set({
        basePath: '',
        frameworks: ['jasmine'],
        reporters: ['progress'],
        port: 9876,
        colors: true,
        logLevel: config.LOG_INFO,
        browsers: ['PhantomJS'],
        files: ['./src/tests.js'],
        preprocessors: { './src/tests.js': ['webpack', 'sourcemap'] },
        webpack: webpackConfig,
        webpackMiddleware: { noInfo: true }
    });
};

In karma.config.js we will load the shared config and add some extras.

var sharedConfig = require('./karma.shared.config');

module.exports = function(config) {
    sharedConfig(config);
    config.set({
        singleRun: true
    });
};

In karma.watch.config.js we will load the shared config and add some extras.

var sharedConfig = require('./karma.shared.config');

module.exports = function(config) {
    sharedConfig(config);
    config.set({
        singleRun: false,
        autoWatch: true,
        autoWatchBatchDelay: 300
    });
};

We'll reconfigure the scripts in package.json.

"scripts": {
    "builddebug": "webpack --config webpack.debug.config.js",
    "buildrelease": "webpack -p --config webpack.release.config.js",
    "serve": "npm run builddebug && webpack-dev-server --progress -d --colors --config webpack.debug.config.js --content-base debug/",
    "testwatch": "karma start karma.watch.config.js",
    "test": "karma start karma.config.js",
    "start": "npm run testwatch & npm run serve"
  }

Now let's make the template for index.html in index.ejs

<!doctype html>
<html>

<head>
    <meta charset="utf-8">
    <title>weak</title>
</head>

<body>
    <div ng-app="app">
        <label>Name:
            <input type="text" ng-model="name" />
        </label>
        <hello-world name="name"></hello-world>
    </div>
</body>

</html>

For now I won't be saving my debug or release builds so let's add the folder to our .gitignore.

node_modules
bundle.js
npm-debug.log
debug
release

We can now start our project in debug with the following command. Then we can browse to http://localhost:8080/. Everything should work as before and our tests should still be green.

npm start

To start in release mode we'll use the python server. The we can browse to http://localhost:8000/release/.

python -m SimpleHTTPServer

weak's People

Contributors

bartw avatar

Stargazers

 avatar  avatar Patrick Mitchell avatar

Watchers

 avatar James Cloos avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.