Ember and Django Get Friendly

Posted by in Code

Like a turducken or cherpumple, we’re going to stick Ember right into the guts of Django.

Let’s get this out of the way: this is not ideal. Doing something like having Django power just the API through the Django REST Framework and then having Ember do all user-facing things would be smarter. Having the separation is easier for maintenance and clearly defines responsibilities.

Unfortunately, real life is rarely ideal. Your website may have some static pages that necessarily need to be served away from the monolithic stature of Ember, but in keeping assets reusable, they’re still tied into the front end builds. Or you may have deployment requirements that necessitate keeping this tightly bundled. Either way, we’re here now, so let’s figure it out.

The Django Part

Getting Django set up is fairly straightforward, as prescribed by the Django docs. The basics on OS X go something like this.

$ brew install python3
$ brew install pyenv # Additional steps at https://github.com/yyuu/pyenv#homebrew-on-mac-os-x
$ pyenv virtualenv --python=$(which python3) myenv
$ pyenv activate myenv
$ pip install Django==1.9.2
$ django-admin startproject myapp
$ cd myapp/
$ python manage.py startapp app1

This will install Python 3.x and pyenv, create a virtualenv called myenv, install Django v1.9.2 to it, and then create a skeleton project in the myapp directory with a single app1 application.

I like to structure my Django projects a particular way since they tend to have multiple apps but a singular source of deployment settings.

myapp
│   manage.py
│   ...

├───apps
│   ├───app1
│   │   │   models.py
│   │   │   views.py
│   │   │   ...
│   │
│   ├───app2
│   │   │   models.py
│   │   │   views.py
│   │   │   ...
│   │
├───deployment
│   │   settings.py
│   │   urls.py
│   │   ...

This does require some changes to fit into this new structure, however.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# manage.py L6
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'deployment.settings')

# /deployment/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'apps.app1',
    'apps.app2',
]

ROOT_URLCONF = 'deployment.urls'

And remember that you’ll now be doing imports like from apps.yourapp import models instead of from yourapp import models.

The Ember Part

The cool part about Ember these days is Ember CLI, which facilitates a great deal of abiding the framework’s directory requirements. We’ll continue with getting that all set up.

$ brew install node
$ npm install -g ember-cli
$ npm install -g bower
$ brew install watchman
$ npm install -g phantomjs
$ ember new frontendapp

If you’re using Git for source control (and you should be, you dummy), this will create a separate Git repo inside /frontendapp/.git, so you might want to remove that.

And if you’re using Submline Text as a text editor, the Ember CLI user guide has a good tip about performance.

Hooking It All Up

The key now is to get Ember to build into a directory that Django will point to as static files. Then they can be served from whatever view you want as a template file.

$ cd frontendapp/
$ ember serve --output-path ../static/ --live-reload=false

By running this during development, you’ll get the constant Ember builds being thrown into static/ without the live reload feature since that’s unnecessary with what we’re doing.

Next you’ll want to edit the deployment/settings.py file to get Django to serve the static files properly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            os.path.join(BASE_DIR, 'templates'),
            os.path.join(BASE_DIR, 'static'),
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

STATIC_URL = '/static/'

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'static'),
)

And now we’ll get Ember to look in the right places. We’ll be using ember-cli-replace to manage environment replacements.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// frontendapp/package.json
{
    ...
    "devDependencies": {
        ...
        "ember-cli-replace": "0.2.1"
    }
}

// frontendapp/config/environment.js
var ENV = {
    ...
    baseURL: '/frontendapp/',
    apiURL: '/api'
};

// frontendapp/ember-cli-build.js
module.exports = function(defaults) {
    ...
    var replacements = {
        assetsURL: '/static/assets',
        imagesURL: '/static/images',
    };
    for (var key in replacements) {
        app.options.replace.patterns.push({
            match: key,
            replacement: replacements[key],
        });
    }

So now you can use @@assetURL and @@imagesURL in place of hardcoding URLs in frontendapp/app/index.html. (And don’t forget to rerun npm install.)

1
2
3
4
5
<link rel="stylesheet" href="@@assetsURL/vendor.css" />
<link rel="stylesheet" href="@@assetsURL/tourf.css" />

<script src="@@assetsURL/vendor.js"></script>
<script src="@@assetsURL/tourf.js"></script>

If you have app1 deal with all the Django-based pages and app2 act as the API, you can set up the various urls.py files to take care of everything.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# deployment/urls.py
urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^', include('apps.app1.urls')),
    url(r'^api/', include('apps.app2.urls')),
]

# apps/app1/urls.py
urlpatterns = [
    url(r'^$', views.index, name='index'),
    url(r'^frontendapp/', views.frontendapp, name='frontendapp'),
]

# apps/app1/views.py
def index(request):
    return render(request, 'main.html')

def frontendapp(request):
    return render(request, 'index.html')

# apps/app2/urls.py
urlpatterns = [
    url(r'^persons/(?P<game_id>[^/]+)?', views.persons, name='persons'),
    url(r'^places/(?P<team_id>[^/]+)?', views.places, name='places'),
    url(r'^things/(?P<bracket_id>[^/]+)?', views.things, name='things'),
]

The resulting directory and file structure should look something like this once you have Ember building and whatnot.

myapp
│   manage.py
│   ...

├───frontendapp
│   │   package.json
│   │   ember-cli-build.js
│   │   ...
│   │
│   ├───config
│   │   │   environment.js
│   │
│   ├───app
│   │   │   index.html
│   │   │   app.js
│   │   │   router.js
│   │   │   ...
│   │   │
│   │   ├───styles
│   │   ├───templates
│   │   └───...
│   │
│   ├───public
│   └───...

├───static
│   │   index.html
│   │   ...
│   │
│   ├───assets
│   ├───images
│   └───...

├───templates
│   │   main.html
│   │   ...

├───apps
│   ├───app1
│   │   │   models.py
│   │   │   views.py
│   │   │   urls.py
│   │   │   ...
│   │
│   ├───app2
│   │   │   models.py
│   │   │   views.py
│   │   │   urls.py
│   │   │   ...
│   │
├───deployment
│   │   settings.py
│   │   urls.py
│   │   ...

Now all you have to do is start the Django development server and access http://localhost:8000/frontendapp/ to get your Ember app.

$ python manage.py runserver

Wham Bam

There you have it! You now have an Ember app running alongside a Django app. Django can continue to serve up views and API endpoints all it wants and Ember will continue to do what it needs to do with all the client-side location routing. Pretty cool, right?

If you have any questions, feel free to leave a comment. I’m pretty good about getting back to people.

And seriously, I know that this is not ideal. But in particular use case, it works pretty great, and from what I’ve heard people ask, it’s more common of a problem than you’d think. I’m definitely open to hearing suggestions about how to improve this, but I’d say this is a good starting point.