# 0x00 前言

本文是 Django 全栈开发教程的第四篇

目录在这里,已经更新的文章如下

  • Django 全栈开发教程 - 2018 年不容错过的 Django 全栈项目 – 目录篇
  • Django 全栈开发教程 - YaDjangoBlog 的开发环境配置

本文需要完成三件事情:

  • 第一件事情,介绍为什么选择 VueJS?
  • 第二件事情,介绍 Vue 项目的一些注意点。
  • 第三件事情,蜻蜓点水搬的带大家过一编,YaDjangoBlog 前端的项目结构,静态资源管理,路由以及组件。

# 0x01 为什么是 VueJS

国产框架 + 语法简洁是我入坑 VueJS 初衷。

后来却是 Vue 的丰富的生态和简洁的语法吸引了继续用下去。

这里要感谢为 VueJS 持续贡献代码的人,从 Vue 本身,到 VueCLI, 到 Router, 到 VueX, 如果没有那么多人为之贡献代码,可能今天这一小节就变成了,『为什么是 React 了』逃。

Vue 自称为 Vue 渐进式 JavaScript 框架。

什么是渐进式?

就是你可以逐步按照 Vue 的方式逐渐引入一些 Vue 的组件到项目中。没有必要上来就是 Vue 全家桶,依据场景逐步引入。

参考链接 https://www.zhihu.com/question/51907207

然而,依据我的经验,vue 全家桶用起来还是很舒服的。这里必须要感谢 Vue 社区。

模板语法,数据驱动,双向绑定。写起代码来简直就是一个字,爽。

# 0x02 Vue 项目的一些注意点

从项目角度,我们想想前端项目有哪些地方是需要注意的:

  1. 开发环境和线上环境区分
  2. 前端资源打包
  • Vue 项目资源打包
  • DLL 打包
  • 字体文件打包
  1. CSS/JS 如何管理
  2. 有哪些必要的依赖,如何引入第三方库
  3. 有哪些页面级组件,有哪些小组件?应该安排这些组件?组件与组件应该怎么通讯?
  4. 路由怎么管理
  5. 状态怎么管理
  6. 登录,鉴权怎么做

限于篇幅,我就不一一讲解了。挑在 YaDjangoBlog 中使用到的技术简单介绍一下。

再次感谢 Vue 社区出品的 VueCLI 以及 Webpack 模板。

下面依次介绍:

  1. 项目结构
  2. 静态资源管理
  3. 路由
  4. 组件

# 0x03 项目结构

首先,YaDjangoBlog 文件的前端目录如下:

.
├── README.md
├── build
│   ├── build.js
│   ├── build_iconfont.js # 构建 iconfont 脚本
│   ├── check-versions.js
│   ├── logo.png
│   ├── utils.js
│   ├── vue-loader.conf.js
│   ├── webpack.base.conf.js
│   ├── webpack.dev.conf.js # 增加了 AutoDllPlugin 用于自动打包 DLL
│   └── webpack.prod.conf.js
├── config
│   ├── dev.env.js # 可以在这里添加开发环境的环境变脸
│   ├── index.js
│   ├── prod.env.js
│   └── test.env.js
├── extra
│   └── svg-icon # 这里存放需要生成 iconfont 的字体文件。
├── index.html
├── package.json # 这里添加了一些构建脚本
├── packages
│   └── theme-future # 注意,这里是另一个子项目,使用 Gulp 构建的纯 CSS 子项目。
├── src
│   ├── App.vue
│   ├── api # 对 axios 进行初步封装
│   ├── assets # 从使用 Gulp 生成的 CSS 可以放在这里。
│   ├── components # 跨页面的组件放在这里
│   ├── directives # 指令
│   ├── filters    # 过滤器
│   ├── main.js    # 初始化 Vue 实例
│   ├── pages      # 页面
│   ├── router     # 路由
│   ├── store      # vuex
│   └── utils      # 常用工具类
├── static
│   ├── hightlight # hightlight 脚本
│   ├── iconfont   # 本地构建的 iconfont
│   ├── images
│   └── js
├── test           # 没写测试,大家开源项目不要学我.... 逃
│   ├── e2e
│   └── unit
└── yarn.lock

# 0x04 静态资源管理

静态资源管理,主要涉及到 JS/CSS/ 图片 / 字体

首先,由于使用了 VueCli 的模板,所以大可以按照 VueCli 提供的写法来写。

<template>
  <div id="app">
    <header></header>
    <router-view />
    <footer></footer>
  </div>
</template>

<script>
  export default {
    name: "app",
    components: {
      Header: () => import("./pages/commons/Header.vue"),
      Footer: () => import("./pages/commons/Footer.vue"),
    },
  }
</script>

<style lang="scss">
  $primary-color: #37b24d;
  $dark-color: #2b5732;
  $body-bg: #f9f9f9;
  @import "~spectre.css/src/spectre-icons.scss";
  @import "~spectre.css/src/spectre.scss";
  @import "~spectre.css/src/spectre-exp.scss";
  @import "./assets/theme-future/index.css";
  a {
    &:focus,
    &:hover,
    &:active,
    &.active {
      text-decoration: none;
      box-shadow: 0 0 0 0;
    }
  }
  #app {
  }
</style>

依据我个人经验,做了一部分的微调:

第一 在代码中新建一个主题 CSS, 单独用于处理 SCSS 编译 CSS. 即除了 App.vue, 其他地方的 CSS 直接写在同一个地方。

第二 对于字体文件,不引入 IconFont 在线字体,而是使用 SVG 本地编译字体。这样减少对 iconfont cdn 的依赖,可以以后直接迁移这个字体到其他 CDN 上。

第三 对于依赖库管理,分为 npm 依赖库和外部 JS 依赖库两种

对于 NPM 依赖库,如果有使用过 ECharts3.0 的 SPA 开发者应该对于万恶的 DLL 非常熟悉了。最早的时候,我们是这样做的:

  • 先写一个编译脚本,指定相关依赖包,打包出 dll 和一个 manifest 文件
  • 然后从 index.html 里引入打包好的 dll.
  • 再从 webpack 的配置文件中引入这个文件。

这种恶心的配置随着 autodll-webpack-plugin 的出现从而得到缓解,于是现在的你只需要配置:

    new AutoDllPlugin({
      inject: true, // will inject the DLL bundles to index.html
      debug: true,
      filename: '[name]_[hash].js',
      entry: {
        vendor: [
          '@antv/data-set',
          '@antv/g2',
          '@antv/g6',
          'highlight.js',
          'markdown-it',
          'markdown-it-abbr',
          'markdown-it-deflist',
          'markdown-it-emoji',
          'markdown-it-footnote',
          'markdown-it-ins',
          'markdown-it-katex',
          'markdown-it-mark',
          'markdown-it-sub',
          'markdown-it-sup',
          'markdown-it-task-lists',
          'markdown-it-toc-and-anchor',
          'typed.js',
          'vue',
          'vue-router',
          'vuex'
        ]
      },
      plugins: [new webpack.optimize.UglifyJsPlugin()],
    })

当然,如果你用了 ECharts, 有的时候会出现莫名其妙的

__DEV__ is not defined

解决方法就是在这上面的插件里面加个插件定义一个 Global 的变量

  new webpack.DefinePlugin({
    __DEV__: false
  }),

PS: 去年的版本由于依赖库的一个路径问题导致 autodll-webpack-plugin 不能在 Windows 上使用,今年可以啦。还不快快用起来?

对于外部的 JS/CSS 依赖库:

  1. 直接拷贝到 static 下面,然后从 index.html 引入即可。
  2. 动态创建 script 标签(比如动态引入高德地图)

# 0x04 路由

博客项目,实际上路由比较简单。

export default new Router({
  mode: 'history',
  base: '/',
  // 注释掉这里是因为和引入的 smooth-scroll 冲突
  // scrollBehavior (to, from, savedPosition) {
  //   return { x: 0, y: 0 }
  // },
  routes: [
    {
      path: '/',
      name: 'home',
      component: () => import('../pages/Home.vue')
    },
    // 解决手贱带来的问题
    {
      path: '/index:suffix*',
      name: 'index',
      component: () => import('../pages/Home.vue')
    },
    {
      path: '/blog',
      name: 'blog',
      component: () => import('../pages/Blog.vue')
    },
    {
      path: '/blog/post/:title',
      name: 'post',
      component: () => import('../pages/Blog/ArticlePost.vue')
    },
    {
      path: '/blog/:category(category/\\d+)?/:tags(tags/\\d+)?/:page(page/\\d+)?',
      name: 'blogposts',
      component: () => import('../pages/Blog.vue')
    },
    {
      path: '/archive',
      name: 'archive',
      component: () => import('../pages/Archive.vue')
    },
    {
      path: '/gallery',
      name: 'gallery',
      component: () => import('../pages/Gallery.vue')
    },
    {
      path: '/works',
      name: 'works',
      component: () => import('../pages/Works.vue')
    },
    {
      path: '/about',
      name: 'about',
      component: () => import('../pages/About.vue')
    }
  ]
})

除了 import 语法之外,需要注意的就是 ‘/blog/:category(category/\d+)?/:tags(tags/\d+)?/:page(page/\d+)?’ 这个奇怪的表达式。

这个表达式可以用于匹配下面的路由

/blog/category/1/tags/2/page/3
/blog/category/1/page/3
/blog/tags/3/page/3
/blog/page/3

匹配完毕之后,就可以拿到 categroy tags page 的值然后提交数据库拿数据咯。

# 0x05 组件

博客里面需要注意的就三个组件

  • ArticlePost 组件
  • 分页组件
  • 打字终端组件

第一个,ArticlePost 组件

<template>
  <div class="p-article-post">
    <div class="columns">
      <div class="col-1 hide-xl"></div>
      <div class="col-2 col-xl-3">
        <div class="g-sidebar">
          <h4>本文目录</h4>
          <div v-html="articleToc"></div>
        </div>
      </div>
      <div class="col-6 col-xl-8">
        <ArticleCard
          :article="article"
          @articleTocReady="initArticleToc"
        ></ArticleCard>
      </div>
      <div class="col-2 col-xl-3">
        <div class="g-sidebar">
          <h4>公告</h4>
          <div>
            <p>MG 的编程小屋,其实就是我整理笔记,写写文章的地方。</p>
            <p>
              专注 Python / JavaScript , 爱折腾的全干工程师 (Full Stuff
              Engineer)
            </p>
            <p>如果我的文章给您的日常开发带来很大帮助的话</p>
            <p>您可以关注我的公众号</p>
            <div>
              <img
                src="/static/images/mp_wechat.jpg"
                alt=""
                style="width: 200px"
              />
            </div>
            <p>也可以扫描二维码进行投喂</p>
            <div>
              <img
                src="/static/images/tips_wechat.jpeg"
                alt=""
                style="width: 200px"
              />
            </div>
            <p>听说关注或者进行投喂的人,技术都越来越牛咯。</p>
          </div>
        </div>
      </div>
      <div class="col-1 hide-xl"></div>
    </div>
  </div>
</template>

<script>
  import { fetchBlogPost } from "../../api/blog"
  export default {
    name: "BlogPage",
    components: {
      ArticleCard: () => import("../../components/Common/ArticleCard.vue"),
    },
    data() {
      return {
        article: "",
        articleToc: undefined,
      }
    },
    watch: {
      "$route.params": function () {
        this.initArticle()
      },
    },
    created() {
      this.initArticle()
    },
    mounted() {},
    methods: {
      initArticleToc: function (v) {
        this.articleToc = v
      },
      initArticle: function () {
        let title = this.$route.params.title
        fetchBlogPost(title).then(res => {
          this.article = res
        })
      },
    },
  }
</script>

嗯,其实就是监听 url, 如果匹配上 url 的话,从 url 中取 title, 然后发送请求,接着取回响应的内容交给子组件处理。子组件处理完毕会 emit 出一个 toc 的值,将这个值赋值给左侧 toc 即可。

  • 分页组件

见地址吧 https://github.com/twocucao/YaVueBlog/blob/master/src/components/Common/Pagination.vue

  • 打字终端组件

终端的样式,当然是抄别人的 CSS, 打字效果,来源于 typed.js 依赖库

# 0x06. 扩展

对于其他的实现,自然是要多多看代码咯。

其实前端工程化是一个很广的概念,本文没有提到代码风格、团队开发工作流、CSS 编写规范、组件优化、Webpack 详细配置等等。这都需要在日常开发中多多练习的。

笔者最近换了份工作,以 React 为技术栈。 加上篇幅和精力有限,也就是不在以 Vue 为前端这一块详细展开了。

下面的文章还是聚焦在后端上面。

# 0xEE. 参考链接

还犹豫啥,Django 前后端分离最佳实践,点赞后,快上车吧


ChangeLog:

  • 2018-03-18 重修文字