Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Refactoring Javascript Applications

Refactoring Javascript Applications

Slides for my talk at Coding Serbia Conference

Jovan Vidic

October 08, 2014
Tweet

More Decks by Jovan Vidic

Other Decks in Programming

Transcript

  1. codecentric d.o.o J v V ć, IT C @ r

    REFACTORING JAVASCRIPT APPLICATIONS
  2. ~ 6 years of development Java + DWR 732 JavaScript

    files ~ 20 000 lines of javascript code Motivation - Legacy project
  3. What was wrong? - JavaScript in HTML - Global functions

    - Inline styles - No coding style guidelines - No tests MOTIVATION - Legacy project
  4. What was OK? - OOP - Throwing own errors -

    Wrapping third party libraries - Prototype.js MOTIVATION - Legacy project
  5. What is Refactoring? a change made to the internal structure

    of software to make it easier to understand and cheaper to modify without changing its observable behavior M. F w r
  6. Refactoring Sample Application – v1 – index.html </head>! <body>! "<ul

    class="nav nav-pills">! " "<li class="active"><a href="../../v1/client/index.html">V1</a></ li>! " "<li><a href="../../v2/client/index.html">V2</a></li>! " "<li><a href="../../v3/client/index.html">V3</a></li>! " "<li><a href="../../v4/client/index.html">V4</a></li>! " "<li><a href="../../v5/client/index.html">V5</a></li>! "</ul>! "<div class="page-header">! " "<h1>Coding Serbia <small> Refactoring JavaScript Applications - V1</small></h1>! "</div>! "<!-- Button trigger modal -->! "<button class="btn btn-primary btn-lg" data-toggle="modal" onclick="createSpeaker()">! " "Add Speaker! "</button>!
  7. Refactoring Sample Application – v1 – speakers.js function add() {!

    var speakerId = $('#speakerId').val();! ! $.ajax({! type: speakerId == '' ? "POST" : "PUT",! url: speakerId == '' ? "http://localhost:4730/speakers" : "http:/ data: JSON.stringify({id: speakerId, firstName: $('#firstName').v contentType: "application/json; charset=utf-8",! dataType: "json",! success: function(data){ $('#speakerId').val(''); $('#firstName’) failure: function(errMsg) {alert(errMsg);}! ! function load() {! $.getJSON( "http://localhost:4730/speakers", function( data ) {! var newRows = "";! $.each(data, function(i,item) {! var sessionTime = new Date(Date.parse(item.sessionTime));! ! newRows += "<tr><td>" + sessionTime.toLocaleString() +"</td>< });! $(".table-striped tbody").html("");! $(".table-striped tbody").append(newRows);! });! };!
  8. Refactoring Sample Application – v2 - REQUIREJS requirejs.config({! paths: {!

    "jquery": "lib/jquery.min",! "bootstrap": "lib/bootstrap.min"! },! shim: {! "bootstrap": {! deps: ["jquery"]! }! }! });! ! require(["app/speakers","bootstrap"], function (speakers) {! ! "$("#btnSaveSpeaker").click(function() {! " "speakers.save();! "});! ! speakers.loadAll();! });! <script data-main="scripts/main" src="scripts/lib/require.js"></script>!
  9. Refactoring Sample Application – V2 – SPEAKERS.JS & THEME.jS define(["jquery","app/theme"],

    function ($, theme) {! "use strict";! ! var SpeakerControler = {! loadAll : function () {! $.getJSON("http://localhost:4730/speakers", function (data) { var speakersTable = $("." + theme.SPEAKERS_TABLE +" tbody define([], function () {! "use strict";! ! return {! DELETE_BUTTON : "btn btn-danger glyphicon glyphicon-remove",! EDIT_BUTTON : "btn btn-default glyphicon glyphicon-pencil",! SPEAKERS_TABLE : "table-striped"! };! });!
  10. Refactoring Sample Application – SPEAKERS.JS define(["jquery","app/theme"], function ($, theme) {!

    "use strict";! ! var SpeakerControler = {! loadAll : function () {! cell.append($("<button class='"+theme.DELETE_BUTTON+"'></button>”) SpeakerControler.remove(item.id);! })); remove : function(id) {! $.ajax({! type: "DELETE",! url: "http://localhost:4730/speaker/" + id,! success: function() {! SpeakerControler.loadAll();! }! });! }! return SpeakerControler;! });
  11. Refactoring Sample Application – v3 – bower.json {! "name": "javascript-refactoring",!

    "version": "1.0",! "authors": [! "Jovan Vidic <[email protected]>"! ],! "private": true,! "ignore": [! ""node_modules",! "bower_components",! "test",! "tests"! ],! "dependencies": {! "jquery": "1.11.1",! "bootstrap" : "3.2.0",! "requirejs" : "2.1.15"! },! ! "devDependencies": {! ! }! }!
  12. Refactoring Sample Application – v4 – MODEL.JS function Speaker(id, firstName,

    lastName, topic, sessionTime, track) {! this.id = id;! this.firstName = firstName;! this.lastName = lastName;! this.topic = topic;! this.sessionTime = sessionTime;! this.track = track;! ! this.hasId = function () {! return (this.id !== undefined) && (this.id !== null) ! && (this.id !== "");! };! ! this.getFullName = function () {! return this.firstName + " " + this.lastName;! };! }! ! return {! "Agenda" : {! "Speaker" : Speaker! }! };!
  13. Refactoring Sample Application – v4 – Speakers.JS define(["jquery", "app/model", "app/theme"],

    function ($, model, theme) { "use strict";! var SpeakerControler = {! editSpeaker : function(id) {! $.getJSON( "http://localhost:4730/speaker/" + id, ! " " " "function(speakerData) {! if(speakerData) {! var speaker = convertToModel(speakerData);! showEditSpeakerPopup(speaker);! }! });! }! function showEditSpeakerPopup(speaker) {! $('#myModalLabel').html('Edit speaker <strong>' ! " " " "+ speaker.getFullName() + "</strong>");! }! function convertToModel(speakerData) {! return new model.Agenda.Speaker(speakerData.id, " " " "speakerData.firstName, speakerData.lastName, speakerData.topic, "speakerData.sessionTime, speakerData.track);! }
  14. Refactoring Sample Application – v4 – jasmine describe("Test model objects",

    function() {! "use strict";! ! var Model;! ! beforeEach(function(done) {! require(["app/model"], function (model) {! Model = model;! done();! });! }); it("should return Jovan Vidic when firstName is Jovan and lastName is "Vidic", function() {! ! var speaker = new Model.Agenda.Speaker(1, "Jovan", "Vidic");! ! expect(speaker.getFullName()).toBe("Jovan Vidic");! });!
  15. Refactoring Sample Application – v5 – SPEAKERS AJAX CLIENT define(["jquery",

    "app/model"], function ($, model) {! "use strict";! ! var baseURL = requirejs.s.contexts._.config.cs["api-url"],! speakersURL = baseURL + "speakers/",! speakerURL = baseURL + "speaker/";! ! function convertToModel(speakerData) {! return new model.Agenda.Speaker(speakerData.id, speakerData.first }! return {! loadAllSpeakers : function (callbackHandle) {! $.getJSON(speakersURL, function (speakersData) {! var speakers = [];! $.each(speakersData, function (index, speakerData) {! var speaker = convertToModel(speakerData);! speakers.push(speaker);! });! callbackHandle.success(speakers);! });! },
  16. Refactoring Sample Application – v5 – Backend FACADE define(["app/client/speakers_ajax_client"], function

    (speakersClient) {! "use strict";! ! return {! loadAllSpeakers : function (callbackHandle) {! speakersClient.loadAllSpeakers(callbackHandle);! },! ! saveSpeaker : function (speaker, callbackHandle) {! speakersClient.saveSpeaker(speaker, callbackHandle);! }!
  17. Refactoring Sample Application – v5 - TesTS define(["squire"], function (Squire)

    {! "use strict";! ! var injector = new Squire(),! client = jasmine.createSpyObj("client", ["loadAllSpeakers", " " " " ""saveSpeaker", "removeSpeaker"]),! callbackHandle = jasmine.createSpy("callback"),! builder = injector.mock("app/client/speakers_ajax_client”, client ! describe("Test backend facade", function() {! ! var backend;! ! beforeEach(function(done) {! builder.require(["app/backend_facade"], function(backendFacad backend = backendFacade;! done();! });! });! it("should loadAllSpeakers", function() {! backend.loadAllSpeakers(callbackHandle);! expect(client.loadAllSpeakers)! .toHaveBeenCalledWith(callbackHandle);! });! ! !
  18. Refactoring Sample Application – v5 – speakers PRESENTER define(["app/backend_facade", "app/speakers/speakers_view",

    ! "app/events"], function (backend, SpeakersView, events) {! ! "use strict";! ! var EventManager = events.EventManager,! Actions = events.Actions;! ! function SpeakerPresenter() { ! var view = new SpeakersView(),! self;! ! return {! init : function () {! self = this;! EventManager.register(Actions.LOAD_ALL_SPEAKERS, this.loadAll);! },! loadAll : function () {! backend.loadAllSpeakers({! "success" : function (speakers) {! view.showAll(speakers);! }! });! },!
  19. Refactoring Sample Application – v5 – Speakers view define(["app/events", "app/components",

    "app/speakers/speakers_popup"], ! "use strict”;! var EventManager = events.EventManager;! ! function SpeakersView() {! var speakersTable = new components.SpeakersTable(),! createButton = new components.Button("btnAddSpeaker"),! popup = new SpeakersPopup();! ! function showCreateSpeakerDialog() {! EventManager.fire(events.Actions.SHOW_CREATE_SPEAKER);! }! function init() {! createButton.addClickListener(showCreateSpeakerDialog);! }! " " init();! return {! showAll : function (speakers) {! var i, len;! speakersTable.clear();! for (i = 0, len = speakers.length; i < len; i += 1) {! speakersTable.addSpeaker(speakers[i]);! }! }!
  20. Refactoring Sample Application – v5 – SPEAKERS POPUP define(["app/model", "app/events",

    "app/components"], function (model, ev ! function SpeakersPopup() {! var speaker,! popup = new components.Popup("myModal"),! firstNameInput = new TextField("firstName"),! ! function saveSpeaker() {! speaker.firstName = firstNameInput.val();! speaker.lastName = lastNameInput.val();! speaker.topic = topicInput.val();! ! if (speaker.hasId()) {! EventManager.fire(events.Actions.EDIT_SPEAKER, speaker);! } else {! EventManager.fire(events.Actions.SAVE_SPEAKER, speaker);! }! }! return {! openAndSet : function (speakerToUpdate) {! speaker = speakerToUpdate;! firstNameInput.val(speaker.firstName);! lastNameInput.val(speaker.lastName);! !
  21. Refactoring Sample Application – v5 – components http://warren.chinalle.com/2010/12/18/model-view-presenter/ define(["jquery", "app/events",

    "app/theme"], function ($, events, theme ! function TextField(id) {! var textField = $("#" + id);! return {! val : function (value) {! if (value !== undefined) {! textField.val(value);! } else {! return textField.val();! }! }! };! }! ! function SimpleButton(id) {! var button = $("#" + id);! return {! addClickListener : function (listener) {! button.click(listener);! }! };! }! !
  22. Refactoring Sample Application – v5 - GRUNT grunt.initConfig({! ! jslint:

    {! src: [! "scripts/**/*.js"! ],! ! },! ! karma: {! unit: {! configFile: "karma.conf.js"! }! },! ! copy: {! main: {! files: [! {expand: true, src: ["lib/bootstrap/dist/css/**"], dest: "dist/"} {expand: true, src: ["lib/bootstrap/dist/fonts/**"], dest: "dist/ {expand: true, src: ["lib/requirejs/require.js"], dest: "dist/"}! ]! }! }!
  23. Refactoring Sample Application – v5 - GRUNT requirejs: {! compile:

    {! options: {! baseUrl: "scripts/",! name: "main",! out: "dist/app-built.js”,! paths: {! app: "app/",! "jquery": "../lib/jquery/dist/jquery.min",! "bootstrap": "../lib/bootstrap/dist/js/bootstrap"! }! }! }! },! processhtml: {! options: {! data: { ! }! },! dist: {! files: {! "dist/index.html": ["index.html"]! }! }!
  24. Coffeescript define [], () ->! ! class Speaker! constructor: (@id,

    @firstName, @lastName, @topic, @sessionTime, @track) ->! ! hasId: ->! @id?.length != 0! ! fullName: ->! @firstName + " " + @lastName! ! return {"Agenda": {"Speaker" : Speaker}} !