0
votes

How can values be passed to Vue.js from within a server-rendered template (i.e. integrate Vue.js app with a django template)?

Use Cases

  • Enhancement of specific pages in a server-rendered environment: Embed a simple vue application on one specific page of your server-rendered web application (i.e. django) to enhance the frontend user experience for a specific functionality (i.e. a signup form)
  • Content Management Systems (CMS): In a server-rendered CMS, a vue application can be exposed to the editors as an independent CMS component. The editor can then choose where to place the vue application on specific pages, at specific positions.

Integration Challenges:

  1. Global settings, such as endpoint URLs or other such environment variables
  2. configurations set by the editor: i.e. URL of user profile picture (of course it would be better to fetch such information via a REST API).
  3. translations: If your server-rendered web framework already centrally handles translations (i.e. django's makemessages CLI command) it would be great to include the Vue.js application string translations.
1

1 Answers

0
votes

Here is a first solution approach for django + Vue.js v2. It includes some stuff that is only relevant for django CMS. But the general principle applies to any server-side rendering CMS or web framework.

Enable Vue.js support in your Webpack config, something like the below:

frontend/webpack.config.js:

'use strict';


const path = require('path');
const MiniCssExtractPlugin = require(`mini-css-extract-plugin`);
const VueLoaderPlugin = require('vue-loader/lib/plugin');

const isServerMode = process.env.NODE_ENV === 'production';


const config = {
    mode: 'development',
    entry: {
        // here, any number of entrypoints (bundles) can be defined
        "marketplace-app": './frontend/blocks/marketplace-app/index.js',
    },
    output: {
        filename: '[name].bundle.js',
        path: __dirname + '/dist/',
        publicPath: `http://localhost:8090/assets/`,
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/,
            },
            {
                test: /\.svg$/i,
                exclude: /fonts/,
                loader: 'svg-url-loader',
            },
            {
                test: /\.(sass|scss|css)$/,
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                            sourceMap: true,
                            plugins: () => {
                                return [
                                    require('precss'),
                                    require('autoprefixer'),
                                ];
                            },
                            hmr: true,
                        }
                    },
                    {loader: 'css-loader', options: {sourceMap: true}},
                    {loader: 'sass-loader', options: {sourceMap: true}},
                ]
            },
            {
                // images
                test: /\.(jpe?g|png|gif)$/i,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            query: {
                                hash: 'sha512',
                                digest: 'hex',
                                name: '[name].[ext]'
                            }
                        }
                    },
                    {
                        loader: 'image-webpack-loader',
                        options: {
                            query: {
                                bypassOnDebug: 'true',
                                mozjpeg: {progressive: true},
                                gifsicle: {interlaced: true},
                                optipng: {optimizationLevel: 7},
                            }
                        }
                    }
                ]
            },
            {
                test: /\.(svg)(\?[\s\S]+)?$/,
                // svg fonts cannot be processed the way we do with svg images above
                // therefore they are handled separately here
                include: /fonts/,
                use: [
                    'file-loader'
                ]
            },
            {
                test: /\.woff2?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
                loader: 'file-loader',
            },
            {
                test: /\.(ttf|eot)(\?[\s\S]+)?$/,
                loader: 'file-loader',
            },
            {
                test: /\.modernizrrc.js$/,
                use: ['modernizr-loader'],
            },
            {
                test: /\.modernizrrc(\.json)?$/,
                use: ['modernizr-loader', 'json-loader'],
            },
            {
                test: /\.vue$/,
                use: [{
                    loader: 'vue-loader'
                }, /*{
                    loader: 'eslint-loader' // You can uncomment this if you want compiling to fail if linting fails
                }*/]
            },
        ],
    },
    resolve: {
        extensions: ['.ts', '.tsx', '.js', '.vue'],
        modules: [
            path.resolve('frontend'),
            'node_modules'
        ],
        alias: {
            modernizr$: path.resolve(__dirname, '/frontend/.modernizrrc'),
            vue: process.env.NODE_ENV === 'production' ? 'vue/dist/vue.min.js' : 'vue/dist/vue.js',
        }
    },
    devServer: {
        contentBase: path.resolve(__dirname, `frontend`),
        headers: {'Access-Control-Allow-Origin': '*'},
        host: `localhost`,
        port: 8090,
        hot: true,
        inline: true,
    },
    plugins: [
        new VueLoaderPlugin(),
        new MiniCssExtractPlugin({filename: '[name].css'}),
    ],
    devtool: 'eval-source-map',
    optimization: {
        // the default config from webpack docs, most of it might be useless
        splitChunks: {
            chunks: 'async',
            minSize: 30000,
            maxSize: 0,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    priority: -10,
                },
                default: {
                    minChunks: 2,
                    priority: -20,
                    reuseExistingChunk: true,
                },
            },
        },
    },
};


if (isServerMode) {
    config.mode = 'production';
    config.devtool = 'none';
    config.output.filename = '[name].js';
    config.output.publicPath = '/static/dist/';
}


module.exports = config;

example for the server-side template (i.e. in django this could be something like backend/marketplace/plugins/marketplace_list_plugin/templates/marketplace_list_plugin/marketplace-list-plugin.html):

Notes:

  • This demos how to import one value (an endpoint url) from the django template into the Vue.js application.
  • supports local dev env mode with hot reloading via webpack dev server based on the value of a global setting variable settings.DJANGO_ENV
  • settings.WEBPACK_DEV_URL needs to be set to publicPath from the Webpack dev server setting in webpack.config.js
  • data-is-reload-on-page-edit class needs to be tied to CMS functionality that reloads the js sources upon a frontend editing action, if CMS has such functionality.
{% load i18n staticfiles static thumbnail %}

{% comment %}   this is a vue widget from frontend/blocks/marketplace-app/index.js   {% endcomment %}

<div id="{{ instance.anchor_id }}">
    <div class="marketplace-vue-widget" data-endpoint-url='{% url 'marketplace:article-list' %}'></div>
</div>

{% if settings.DJANGO_ENV.value == 'local' %}
    <script data-is-reload-on-page-edit defer src="{{ settings.WEBPACK_DEV_URL }}/marketplace-app.bundle.js"></script>
    <link rel="stylesheet" href="{{ settings.WEBPACK_DEV_URL }}/marketplace-app.css">
{% else %}
    <script data-is-reload-on-page-edit defer src="{% static 'dist/marketplace-app.js' %}"></script>
    <link rel="stylesheet" href="{% static 'dist/marketplace-app.css' %}">
{% endif %}

Frontend setup (frontend/blocks/marketplace-app/index.js):

import Vue from 'vue';
import App from './app.vue'


// this relates to backend/marketplace/plugins/marketplace_list_plugin/templates/marketplace_list_plugin/marketplace-list-plugin.html
// CMS plugins could (in theory) be added more than once to a page,
// we take care to allow the vue instance to be mounted on any occurrences

// create a constructor for your widget
let Widget = Vue.extend({
    render(h) {
        return h(App, {
            props: { // load stuff from data attributes in the django template
                endpointUrl: this.$el.getAttribute('data-endpoint-url'),
            }
        })
    },
});

// mount the widget to any occurences
let nodes = document.querySelectorAll('.marketplace-vue-widget'); // unique selector!
for (let i = 0; i < nodes.length; ++i) {
    new Widget({el: nodes[i]})
}

app.vue:

<template src='./app.html'></template>
<script src="./app.js"></script>
<style lang="scss" scoped src="./app.scss"></style>

app.js:

// example code

export default {
    props:["endpointUrl"],
    data() {
        return {}
    },
    computed: {},
    mounted() {},
    watch: {},
    methods: {},
    components: {},
}