使用Vue和Django

26

我最近开始使用Django构建社交媒体网站。我正在使用默认的Django模板引擎来填充我的页面。但是,在这个阶段,我想添加JavaScript以使网站更具动态性。这意味着:

  • 页眉和页脚在每个页面上都相同。页眉应该有一个下拉菜单、一个搜索表单,能够在您输入时进行搜索。
  • 我的当前 Django 应用程序有一个基本模板,其中包含页眉和页脚 HTML,因为每个页面都应该有这个。
  • 该网站由多个页面组成,例如首页、个人资料页面、注册页面等。这些页面都有一些共同的但也很多不同的动态组件。例如,注册页面应该具有实时表单验证,而个人资料页面则不需要此功能。个人资料页面应该具有带有无限滚动的状态更新。

我想使用Vue来处理动态组件,但我不知道如何入手。该应用程序不应该是 SPA。

  • 我该如何组织Vue代码?
  • 我该如何打包它?使用Gulp吗?还是django-webpack-loader
  • 我仍然应该能够使用Django模板标签,例如我想在下拉菜单中使用{% url 'index' %}
3个回答

23

这似乎是一个基于观点的问题,没有明确的答案。

你提到你不想让应用成为一个 单页应用程序(SPA)。如果是这样,使用Vue的动机是什么?处理页面内的用户交互吗?

Vue在非SPA环境中也可以完美运行。它将帮助你处理页面内的丰富交互,比如将数据绑定到下拉菜单、表单等。但当你在SPA环境中使用时,Vue的真正力量就会展现出来。

对于你的情况,我建议在 独立 模式下使用Vue.js,在其中你可以快速定义 Vue 组件中的模板,并轻松地在一个 JavaScript 文件中编写所有代码。

这里是你需要的内容:https://vuejs.org/guide/installation.html#Standalone

在“Vue.js独立模式”下,不需要任何webpack构建系统或vue-cli。你可以直接在现有的 Django 开发环境中构建应用程序。gulp 可以选择性地压缩和捆绑你的 JavaScript 文件,就像你在使用 jQuery 的应用程序一样。

Vue.js 使用双括号 {{..}} 作为模板,因此它不会干扰你的 Django 模板字符串。

所有的 Vue.js jsFiddle 示例都运行在 独立 模式下。这正是你此时需要的。你可以查看一些最近带有vue.js标签的问题,找到一个示例 jsFiddle 并了解它是如何实现的。

对于复杂的 SPA 应用程序,你需要将Vue代码与服务端分开构建,在使用虚拟 AJAX 调用进行全面测试后,构建它以进行生产,并将最终的生产构建文件放入服务器进行端到端测试。这是你未来可以做的事情。


2
忘了提一下 - 你的页头和页脚将是Vue组件,模板定义了布局和标记。请记住:所有渲染都在客户端进行,因此页面对于Google搜索机器人来说基本上是不可见的,无法进行索引。 - Mani
2
双大括号如何不影响Django?Django在模板中使用相同的语法来使用变量? - Bono
@Bono Django在服务器端处理模板。因此,如果您希望特定的HTML代码块由客户端的vue.js处理,则需要通过Django进行转义,以便双花括号语法及其变量留给vue.js不受影响。 - Mani

22

我曾经在寻找实现与提问者类似的功能时看过这个问题和其他一些问题。不幸的是,关于Vue的大部分信息都是在单页应用程序(SPA)的上下文中给出的。然而,正如Evan You经常重复的那样,Vue没有偏见,也不需要在SPA内使用。

我想分享一些我的发现,并勾勒出一种可能的方法来使Django和Vue协同工作。虽然不需要SPA,但我认为Vue的真正力量来自于它的组件,这有点推动你朝着Webpack或类似的方法,而不是简单的独立模式在HTML中。

此外,只要非常清楚:90%以上的我的Django视图代码和模板仍然与以前完全相同。 Django并不特别关心它是否使用webpack_loader和render_bundle。甚至更少的是,render_bundle与Vue有关。通过Django的“template blocks”,“verbatim”标签和Vue的“slots”,您可以获得很多灵活性,允许您保留大部分现有内容。

最后,一旦您的应用程序稳定,您可以在端口3000上禁用热重新加载服务器,运行npm run build-production并使用collectstatic将您的JS通过nginx / apache像任何普通静态内容一样提供。所以node作为偶尔的批处理而不是服务运行。需要对Django的配置进行一些调整,但完全可以接受。

抱歉,这很草图,不要期望工作代码,因为我正在剥离很多东西。但希望它能给你一个想法。

mysite / __full12_vue.html:

这是我的基本Vue-ify Django模板,它扩展了我的现有Django基本模板__full12.html

  • 假设__full12.html定义了所有一般的Django块,如{% block content %}

    (实际上,有一个非常重要的带有ID bme-vue的div,所以我也将此模板添加到了最后。)

  • 我添加了一个Vue组件来显示用户消息。

  • 并重新定义了菜单模板,以使用Vue + Bootstrap下拉菜单。

{% extends "mysite/__full12.html" %}
<!-- KEY: use this to hook up to https://github.com/ezhome/django-webpack-loader -->
{% load render_bundle from webpack_loader %}


{% block nav %}
    <!-- uses Vue to setup Bootstrap dropdown menus -->
    {% include "mysite/menu_vue.html" %}
{% endblock nav %}


{% block user_msg %}
<div class="row">
    <div class="col-6">
        <!-- uses Vue to display user messages -->
        <bme-user-messages>
            <div>THIS SHOULDNT APPEAR ON SCREEN IF VUE WORKED</div>
        </bme-user-messages>
    </div>
</div>
{% endblock user_msg %}



{%block extra_js_body_end%}
    <!-- KEY  this points Django Webpack loader to appropriate Webpack entry point -->
    {% render_bundle bundle_name %}
{%endblock extra_js_body_end%}

webpack.config.development.js:

这是您告诉Webpack要为视图中指定的bundle_name提供哪些JS文件的地方。

如何配置Webpack超出了我的帖子范围,但我可以向您保证,这确实是一件真正令人头痛的事情。我最初使用pip django-webpack-loader,然后尝试https://github.com/NdagiStanley/vue-django和Bootstrap 4等内容。然而,最终结果比单独使用要强大得多,这是我个人的看法。

/*
webpack config dev for retest
*/

config.entry = {
  "main" : [
    'webpack-dev-server/client?http://localhost:3000','../../static_src/js/index'],

  // ....stuff..
  //KEY: ONE entrypoint for EACH bundlename that you use.  
  "mydjangoappname/some_django_view" : ["../../static_src/js/mydjangoappname/some_django_view"],
  "mydjangoappname/another_django_view" : ["../../static_src/js/mydjangoappname/another_django_view"],
  // ....stuff..

}

  // ....stuff left out...

mydjangoappname/some_django_template.html

最后,我们准备展示一些实际内容: bme-nav-itembme-tab-pane 是我用于 Boostrap 4 标签导航和内容的 2 个自定义 Vue 组件。
Django 使用 var settings= some-json-object 将实例特定数据(而不是页面通用数据)传递给 Vue 和 JS。
{% extends "mysite/__full12_vue.html" %}

<script>
// KEY: settings is provided by json.dumps(some_settings_dictionary) 
// which your views puts into your RequestContext.
// this is how Django tells Vue what changes with different data, on the same view
var settings = {{settings | safe}};
</script>

{% block content %}

    <!-- a button to run a Celery batch via a post command, url should probably come 
    from Django url reverse and be put into a Vue property...
     -->
    <button v-bind:data-url="url_batch" type="button" class="btn btn-block btn-outline-primary" @click.prevent="run_batch">

    <!-- lotsa stuff left out.... -->

    <ul id="tab-contents-nav" class="nav nav-tabs nav-pills">

    <!--  *label* is using a Vue Prop and because there is no {% verbatim %} guard around it, Django will
        inject the contents.  {% urlrev xxx %} could be used to write to an 'url' prop.  Ditto the conditional
        inclusion, by Django, of a template if it's in the RequestContext.
    -->
        {% if templatename_details %}
        <bme-nav-item link="details" 
            label="{{details_label}}" >         
        </bme-nav-item>
        {% endif %}

<!-- lotsa stuff left out.... -->

<bme-tab-pane link="details">
    <div slot="content">

        <!-- KEY: Vue slots are incredibly powerful with Django.  Basically this is saying
                  to Django : inject what you want in the slot, using your include, I'll tidy up afterwards.
                  In my case, this is a Bootstrap NavItem + NavTab 
        -->
        {% if templatename_details %}

            {% include templatename_details %}
        {% else %}
            <span class="text-warning">SHOULDNT APPEAR IF VUE WORKED </span>
        {% endif %}

    </div>
</bme-tab-pane>

{% endblock content %}

mydjangoappname/some_django_view.js:

  import Vue from 'vue';
  import Vuex from 'vuex';
  //now Vue is using Vuex, which injects $store centralized state variables as needed
  Vue.use(Vuex);



  //KEY: re-using components defined once.
  import {base_messages, base_components} from '../mysite/commonbase.js';

  var local_components = {
    //nothing, but I could have imported some other components to mix-n-match
    //in any case, bme-nav-item, bme-tab-pane and bme-user-messages need to 
    //coming from somewhere for this page! 
  };

  const components = Object.assign({}, base_components, local_components);

  //we're going to put together a Vue on the fly...

  export function dovue(config) {

      //KEY:  The store is a Vuex object - don't be fooled, it's not SPA-only
      // it's the easiest way to coherently share data across Vue Components, so use it.
      store.commit('initialize', config);

      //here I am telling the store which html IDs need hiding
      var li_tohide = settings.li_tohide || [];
      li_tohide.forEach(function(hidden) {
          store.commit('add_hidden', hidden);
      });

      /* eslint-disable no-new */
      var vm = new Vue({

        //KEY:  This tells the Vue instance what parts of your html are in its scope.
        el: '#bme-vue'

        //KEY: each bme-xxx and bme-yyy special tag needs to be found in components below
        //otherwise you'll see my SHOULDNT APPEAR IF VUE WORKED text in your page
        ,components: components

        ,data: {
          li_rowcount: window.settings.li_rowcount || []
          ,csrf_token: window.csrf_token
          ,url_batch: "some url"
        }
        ,mounted: function () {
           // a Vue lifecycle hook.  You could use to set up Vue Event listening for example
           console.log("data.js.lifecycle.mounted");
        }
        ,methods : {
          ,run_batch: function(e) {
              //hook this up to a button 
              console.assert(this.$data, COMPONENTNAME + ".run_batch.this.$data missing. check object types");
              var url = e.target.dataset.url

              //this is defined elsewhere 
              post_url_message(this, url, this.csrf_token);
          }
        }
        //the Vuex instance to use for this Vue.
        ,store: store
      });

      //did Django provide any user messages that need displaying?
      var li_user_message = config.li_user_message || [];

      li_user_message.forEach(function(user_message, i) {
        //the bme-user-messages Component?  It's listening for this event 
        //and will display Bootstrap Alerts for them.
        vm.$emit(base_messages.EV_USER_MESSAGE, user_message);
      });
      return vm;
  }

  //various app and page specific settings...
  import {app_config, LOCALNAV_LINK, TOPNAV_LINK_OTHERS} from "./constants";
  var page_config = {
    //used to show which navigation items are .active
    localnav_link : LOCALNAV_LINK.data
    , topnav_link: TOPNAV_LINK_OTHERS.system_edit_currentdb
  };

  //this is coming from Django's RequestContext.
  var generated_config = window.settings;

  //ok, now we are merging together a Django app level config, the page config and finally
  //what the Django view has put into settings.  This will be passed to the Vuex store
  //individual Vue Components will then grab what they need from their $store attribute
  //which will point to that Vuex instance.
  var local_config = Object.assign({}, app_config, page_config, generated_config);
  var vm = dovue(local_config);

vuex/generic.js:

这是一个天真的、大多数情况下只读的存储实现:

//you can add your page's extra state, but this is a shared baseline
//for the site
const state = {
  active_tab: ""
  ,topnav_link: ""
  ,localnav_link: ""
  ,li_user_message: []
  ,s_visible_tabid: new Set()
  ,urls: {} 
};
const mutations = {
    //this is what your page-specific JS is putting into the state.
    initialize(state, config){
      //initialize the store to a given configuration
      //KEY: attributes that did not exist on the state in the first place wont be reactive.
        // console.log("store.initialize");
        Object.assign(state, config);
    },
    //a sample mutation
    set_active_tab(state, tabid){
        //which bme-tab-nav is active?
        if (! state.s_visible_tab.has(tabid)){
          return;
        }
        state.active_tab = tabid;
    },
};

export {state as generic_state, mutations};

为了让您了解一般的文件层次结构:

.
./manage.py
./package.json  //keep this under version control
./

├── mydjangoappname
│   ├── migrations
│   └── static
│       └── mydjangoappname
├── node_modules
├        //lots of JavaScript packages here, deposited/managed via npm && package.json
├── static
│   └── js
├── static_src
│   ├── assets
│   ├── bundles
│   │   // this is where django-webpack-loader's default config deposits generated bundles...
│   │   // probably belonged somewhere else than under static_src ...
│   │   ├── mydjangoappname
│   ├── components
│   │   ├── mydjangoappname
│   ├── css
│   ├── js
│   │   ├── mydjangoappname
│   │   └── mysite
│   └── vuex
│       ├── mydjangoappname
├── staticfiles
│   //  for Production, collectstatic should grab django-webpack-loader's bundles, I think...
├── templates
│   ├── admin
│   │   └── pssystem
│   ├── mydjangoappname
│   └── mysite
└── mysite
    ├── config
    ├       // where you configure webpack and the entry points.
    ├       webpack.config.development.js 
    ├── sql
    │   └── sysdata
    ├── static
    │   └── mysite
    └── templatetags

好的,我确实需要修改网站的基本模板,以确保div#bme-vue始终可用。

可能需要在这个模板和mysite/__full12_vue.html之间进行一些重构。

mysite/__full12.html:

<!-- lots of stuff left out -->
<body>

    <!--     KEY: the #bme-vue wrapper/css selector tells Vue what's in scope.  
    it needs to span as much of the <body> as possible, but 
    also end right BEFORE the render_bundle tag.  I set that css
    selector in mydjangoappname/some_django_view.js and I'd want to
    use the same selector everywhere throughout my app.
    -->

    <div id="bme-vue">
        <!-- anything that ends up here
, including through multiple nested/overridden Django content blocks
, can be processed by Vue
, but only when have Vue-relevant markup 
such as custom component tags or v-for directives.
-->

    ...more blocks...
    {% block search %}
    {% endblock search %}

    <div id="main" role="main">
        <div> <!-- class="container"> -->
            {% block content %}
            {% endblock %}
        </div>
    </div>
    ...more blocks...


    </div>    <!-- bme-vue -->
    {%block extra_js_body_end%}
    {%endblock extra_js_body_end%}
</body>
</html>

非常棒的细节,谢谢。非常感谢您花时间整理这些内容。 - chuckjones242

4
这是我将Vue与Django项目集成的方法:
第一种方法是构建独立的Django和Vue应用程序。 Django将负责提供使用Django REST框架构建的API,而Vue将使用Axios客户端或浏览器的fetch API消耗这些API。您需要两个服务器,在开发和生产中都需要一个用于Django(REST API),另一个用于Vue(提供静态文件)。
第二种方法是不同的前端和后端应用程序将被耦合。 基本上,您将使用Django来同时提供Vue前端并公开REST API。因此,您需要将Vue和Webpack与Django集成,以下是您可以遵循的步骤:
首先生成您的Django项目,然后在此项目目录中使用Vue CLI生成您的Vue应用程序。
对于Django项目,请使用pip安装django-webpack-loader:
pip install django-webpack-loader

接下来将应用程序添加到已安装的应用程序中,并通过添加以下对象在settings.py中进行配置

WEBPACK_LOADER = {
    'DEFAULT': {
            'BUNDLE_DIR_NAME': '',
            'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats.json'),
        }
}

然后添加一个Django模板,用于挂载Vue应用程序,并由Django提供服务。
{ % load render_bundle from webpack_loader % }

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Django + Vue</title>
  </head>
  <body>
    <div id="root">
     This is where Vue will be mounted
    </div>
    { % render_bundle 'app' % }
  </body>
</html>

然后在urls.py中添加一个URL来服务于这个模板。

from django.conf.urls import url
from django.contrib import admin
from django.views.generic import TemplateView

urlpatterns = [

    url(r'^', TemplateView.as_view(template_name="main.html")),

]

如果您此时启动Django和Vue服务器,您将收到一个Django错误,指出webpack-stats.json文件不存在。因此,接下来您需要使Vue应用程序能够生成统计文件。
请继续进入Vue应用程序中,然后安装webpack-bundle-tracker。
npm install webpack-bundle-tracker --save

前往 build/webpack.base.conf.js,然后添加以下内容:

let BundleTracker = require('webpack-bundle-tracker')
module.exports = {
  // ...
  plugins: [
    new BundleTracker({filename: '../webpack-stats.json'}),
  ],
}

这将向Webpack添加BundleTracker插件,并告诉它在Django文件所在的父文件夹中生成webpack-stats.json文件。
现在,如果重新运行Vue服务器,则将生成webpack-stats.json文件,并且Django将能够使用它来查找Vue dev服务器生成的Webpack捆绑包的信息。
您可以从这个教程中获取更多信息。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接