AngularJS & Dancer
for Modern Web Development
Otherwise titled:
How I tried to get my app ready for Dancer, instead of just using Dancer, and how that worked out for me.
Josh Lavin
@jdigory
IRC: digory
Been working with Perl for 13 years.
New to Dancer -- have a lot to learn.
End Point Corporation - web consultancy
since July (formerly of Perusion - merged)
Web Application Developer
both front-end and back-end
Interchange 5 - legacy Perl e-commerce product (still kicking)
It's a fact that a lot of us have to work with legacy software.
Lots of older platforms out there. So unavoidable.
Eventually, old apps are migrated.
Or they die a slow death.
(Or the last developer maintaining it dies.)
Migrate
How?
Legacy App paradigm
Modern Perl
Organize business logic
It'd be great if we could migrate everything to Dancer.
Not always practical, but at least consider ways.
How to prepare for migration?
Paradigm: Doesn't always have to be done a certain way. Better ways.
Also, less dependence on Legacy-App-specifc code.
Modern: OO, tests, CPAN modules (not reinvent)
Logic: trying to avoid logic in the front-end
If I'm going to use modern Perl, I use should tests, right?
Let's write some tests.
Oh, crap.
Explored testing, but soon realized testing requires methods. Haha.
So, _that's_ what Object Oriented means.
Realization my life as a Perl programmer up till now was just doing scripting.
My code looked like a relic from 1990.
It was just a big pile of poo; no methods; not testable.
I couldn't even write tests for the Legacy App - unsupported.
Business logic everywhere.
I set out to change my ways.
Started exploring OO Perl, using Moo, since Dancer2 uses Moo.
Started trying to write unit tests.
Using classes, methods: breaking problems down into smaller problems.
Writing code this way was fun! Felt like I was doing something right.
I quickly realized I wasn't able to run tests in the Legacy App.
It couldn't be called from the command line.
So if my modules depend on legacy app code, I can't run tests from the shell.
(Legacy app can only be called from shell via weird hacks.)
Now I have to abstract away all Legacy App-specific code from my modules (just a couple modules rely on Legacy App).
Which was the right idea anyways, and is the idea of Separation of Concerns. App vs Web App.
Now I can run tests on many modules.
Move already
As I started the process of yak shaving, I soon realized:
I should have moved earlier.
Weird hacks to get Legacy App doing things like Dancer would.
But it was a start. A step in the right direction.
And, my back-end code was all the more ready for working w/ Dancer. Just change a few things, change the Web App side, and presto.
Now for getting the front-end ready.
I had a lot of business logic in there I needed to clean up.
@_TOP_@
[scratch page_title]
[perl]
my $has_course;
for (grep {$_->{mv_ib} eq 'course'} @$Items) {
$has_course++;
}
return $has_course ? 'You have a course!
' : '';
[/perl]
Buy [if cgi items]more[else]now[/else][/if]
@_BOTTOM_@
Separation of Concerns
HTML + placeholders?
I'm going to use modern Perl, I should separate things, right?
How can we apply this principle to the front-end?
[next]
Thought: to abstract away logic from the page, could we use some placeholders in the HTML to later fill in with our data?
Idea isn't new -- late to the party, but better late than never.
@_TOP_@
[my-tag-attr-list
page_title="[scratch page_title]"
has_course="[perl] ... [/perl]"
buy_phrase="Buy [if cgi items]more[else]now[/else][/if]"
]
{PAGE_TITLE}
{HAS_COURSE?}You have a course!
{/HAS_COURSE?}
{BUY_PHRASE}
[/my-tag-attr-list]
@_BOTTOM_@
First attempt.
Used Legacy App's built-in placeholder system.
At least the logic was separated from the HTML a bit now.
It worked OK (?-mark is an "if").
But the logic _was_ still baked into the HTML page.
How could we be more ready for Dancer?
(should have just migrated!)
Template::Toolkit?
But hard to use in Legacy App.
JavaScript framework
Front-end separated from back-end
Eats JSON
What is this AngularJS everyone keeps talking about?
[next]
Back-end can deliver JSON to front-end.
AngularJS displays data.
As if your front-end is consuming an API!
@_TOP_@
You have a course!
Buy more
Buy now
@_BOTTOM_@
After using Angular, now my Legacy App page looked like this.
(not showing JavaScript)
Now all front-end has from Legacy App is basically "includes" to get header/footer.
The rest is HTML code with "ng-" attributes.
These are what Angular uses to do things.
Cleaner.
Still using Legacy back-end, but all it has to do is "routing" to call the right module and deliver JSON (and do authentication).
<html ng-app="MyApp">
...
</html>
AngularJS code will handle how the JSON feeds are then displayed in the page.
Angular pulls in the JSON and can massage for use by "-ng" attributes, etc.
What's the difference? Don't have to use Angular for this...
But I like it, because a dump JS dev can use your feeds to do stuff.
More of treating your site as an API.
Migration of entire app to Dancer is now much easier.
I gave it a whirl with a handful of routes and modules.
It went great.
package MyApp::Feedback;
use MyApp;
my $app = MyApp->new( ... );
sub list {
my $self = shift;
my $code = shift
or return $app->die('Need code');
my $rows = $app->dbh($feedback_table)->...;
return $rows;
}
For my modules that were the "App" (not "Web App"), few changes.
Original module
Using a class to get a custom 'die' and database handle.
Not really doing this properly, but it worked (first attempt).
package MyApp::Feedback;
use Moo;
with MyApp::HasDatabase;
sub list {
my $self = shift;
my $code = shift
or die 'Need code';
my $rows = $self->dbh->...;
return $rows;
}
I had a custom 'die' which I was using to die and log things.
Also was passing a database handle in, but I changed to use a Role.
Before:
sub _route_feedback {
my $self = shift;
my (undef, $sub_action, $code) = split '/', $self->route;
$code ||= $sub_action;
$self->_set_status('400 Bad Request'); # start with 400
my $feedback = MyApp::Feedback->new;
for ($sub_action) {
when ("list") {
my $feedbacks = $feedback->list($code);
$self->_set_tmp( to_json($feedbacks) );
$self->_set_path('special/json');
$self->_set_content_type('application/json; charset=UTF-8');
$self->_set_status('200 OK') if $feedbacks;
}
default { #...
Biggest improvement in things that I "stole" from Dancer (naturally).
This is my Legacy App's route for displaying and accepting feedback entries.
Does not include any authentication checks, nor urldecoding.
This handles feeding back an array of entries for an item ('list'), a single entry (GET), and saving an entry (POST).
Before, continued:
default {
for ($self->method) {
when ('GET') {
my $row = $feedback->get($code)
or return $self->_route_error;
$self->_set_tmp( to_json($row) );
$self->_set_path('special/json');
$self->_set_content_type('application/json; charset=UTF-8');
$self->_set_status('200 OK') if $row;
}
when ('POST') {
my $params = $self->body_parameters
or return $self->_route_error;
$params = from_json($params);
my $result = $feedback->save($params);
$self->_set_status('200 OK') if $result;
$self->_set_path('special/json');
$self->_set_content_type('application/json; charset=UTF-8');
}
}
Biggest improvement in things that I "stole" from Dancer (naturally).
This is my Legacy App's route for displaying and accepting feedback entries.
Does not include any authentication checks, nor urldecoding.
This handles feeding back an array of entries for an item ('list'), a single entry (GET), and saving an entry (POST).
After:
prefix '/feedback' => sub {
my $feedback = MyApp::Feedback->new;
get '/list/:id' => sub {
return $feedback->list( param 'id' );
};
get '/:code' => sub {
return $feedback->get( param 'code' );
};
post '' => sub {
return $feedback->save( scalar params );
};
};
Dancer gives me a lot for free.
A lot simpler.
Still no auth here, but everything else is done (I can use an auth plugin).
TMTOWTDI
Have Dancer deliver HTML files (AngularJS)
Or have web server deliver them
especially if SPA
JSON Web Tokens (JWT)
For the front-end, we have options on how to use Dancer:
Now starring Dancer
In hindsight, probably should have just moved to Dancer right away.
Because Legacy App was a pain:
I built my own Routing module in Legacy App...
I built my own Auth checking module, akin to Dancer::PAE...
Dancer makes it simpler.
Dancer is better?
In the process, I learned something...
I learned you can use tools improperly.
You can do Dancer "wrong".
You can write tag soup in anything, even the best modern tools.
You can stuff logic into TT.
You can do AngularJS "wrong".
I probably do many things the wrong way.
Point is: start somewhere.
Maybe you can't write tests in everything, but you can write smart code.
Dancer is better:
Routes contain code specific to the Web.
Routes call non-Dancer modules (business logic).
Return the data in the appropriate format.
Dancer is better, when...
[next]
(thanks to mst for these)
Makes it easy to test.
Talk to your back-end as if it's an API. Because it is.
Lessons learned
Separate concerns
Keep it testable
Just start somewhere
Start: even breaking out micro-services out of a Legacy App to Dancer.