17、地理定位博客开发全解析

地理定位博客开发全解析

1. Vue渲染函数基础

1.1 createElement函数

在Vue中, createElement 函数用于创建元素,它可以是DOM元素或Vue组件。该函数最多接受三个参数:
- element(必需) :可以是HTML标签名、已注册组件的ID或直接是组件定义对象,也可以是返回这些内容的函数。
- data(可选) :数据对象,用于指定CSS类、props、事件等信息。
- children(可选) :可以是文本字符串或使用 createElement 构建的子元素数组。

通常,我们会使用 h 作为 createElement 的别名,这是大家常用的名称,也是JSX所要求的。 h 源自 “使用JavaScript编写HTML” 的超文本标记概念。

以下示例等价于 <template><p class="content">{{ message }}</p></template>

render (h) {
  return h('p', { class: 'content' }, this.message)
}

1.2 动态模板

直接编写渲染函数的主要优点是它们更接近编译器,并且可以充分利用JavaScript的能力来操作模板。不过,明显的缺点是代码不再像HTML,不过这可以通过JSX来缓解。

例如,创建一个可以渲染任意级别的标题组件:

Vue.component('my-title', {
  props: ['level'],
  render (h) {
    return h(
      `h${this.level}`,
      this.$slots.default
    )
  }
})

使用示例: <my-title level="2">Hello</my-title>

若使用模板实现则会比较繁琐:

<template>
  <h1 v-if="level === 1">
    <slot></slot>
  </h1>
  <h2 v-else-if="level === 2">
    <slot></slot>
  </h2>
  <h3 v-else-if="level === 3">
    <slot></slot>
  </h3>
  <h4 v-else-if="level === 4">
    <slot></slot>
  </h4>
  <h5 v-else-if="level === 5">
    <slot></slot>
  </h5>
  <h6 v-else-if="level === 6">
    <slot></slot>
  </h6>
</template>

1.3 数据对象

数据对象是 createElement 的第二个可选参数,用于传递关于元素的额外信息。以下是一个涵盖大部分特性的数据对象示例:

{
  'class': {
    foo: true,
    bar: false
  },
  style: {
    color: 'red',
    fontSize: '14px'
  },
  attrs: {
    id: 'foo'
  },
  props: {
    myProp: 'bar'
  },
  domProps: {
    innerHTML: 'baz'
  },
  on: {
    click: this.clickHandler
  },
  nativeOn: {
    click: this.nativeClickHandler
  },
  directives: [
    {
      name: 'my-custom-directive',
      value: '2',
      expression: '1 + 1',
      arg: 'foo',
      modifiers: {
        bar: true
      }
    }
  ],
  slot: 'name-of-slot',
  key: 'myKey',
  ref: 'myRef'
}

例如,根据标题级别应用特殊CSS类:

Vue.component('my-title', {
  props: ['level'],
  render (h) {
    return h(
      `h${this.level}`,
      {
        'class': {
          'important-title': this.level <= 3
        }
      },
      this.$slots.default
    )
  }
})

添加点击事件监听器:

Vue.component('my-title', {
  props: ['level'],
  render (h) {
    return h(
      `h${this.level}`,
      {
        on: {
          click: this.clickHandler
        }
      },
      this.$slots.default
    )
  },
  methods: {
    clickHandler (event) {
      console.log('You clicked')
    }
  }
})

1.4 虚拟DOM

渲染函数的结果是使用 createElement 函数创建的节点树,在Vue中称为VNodes,它代表了组件在Vue维护的虚拟DOM中的视图。Vue不会直接用新的虚拟DOM树替换真实DOM树,因为这可能会导致大量昂贵的DOM操作。为了提高性能,Vue会比较两棵树的差异,只执行必要的DOM操作来更新真实DOM以匹配虚拟DOM。

1.5 JSX

JSX是一种在渲染函数的JavaScript代码中编写更像HTML的代码的语言,它实际上是JavaScript的类XML扩展。使用Babel可以将JSX代码转换为使用 h 函数的真实JavaScript代码。

例如,JSX代码:

export default {
  props: ['message'],
  render (h) {
    return <p class="content">{this.message}</p>
  }
}

会被转换为:

export default {
  props: ['message'],
  render (h) {
    return h('p', { class: 'content' }, this.message)
  }
}

由于 vue-cli 已经启用了JSX支持,我们可以在 .vue 文件中编写JSX代码。

1.6 博客内容结构(使用JSX)

创建一个新的 src/components/content 文件夹,并在其中创建 BlogContent.vue 文件。该组件代表右侧面板,负责显示相应的组件:
- LocationInfo.vue 组件:如果在地图上选择了位置,可能会显示位置地址和名称。
- 下方根据不同情况显示:
- NoContent.vue 组件:如果未选择位置,显示点击地图的提示。
- CreatePost.vue 组件:如果有草稿帖子,显示表单。
- PostContent.vue 组件:如果选择了真实帖子,显示内容和评论列表。

创建这些组件时,模板初始为空:

<template></template>

BlogContent.vue 组件的实现步骤如下:
1. 创建命名空间助手:

import { createNamespacedHelpers } from 'vuex'
const {
  mapGetters: postsGetters,
  mapActions: postsActions
} = createNamespacedHelpers('posts')
  1. 编写组件定义:
export default {
  computed: {
    ...postsGetters([
      'draft',
      'currentPost'
    ]),
    cssClass () {
      return [
        'blog-content',
        {
          'has-content': this.currentPost
        }
      ]
    }
  }
}
  1. 编写渲染函数:
render (h) {
  let Content
  if (!this.currentPost) {
    Content = NoContent
  } else if (this.draft) {
    Content = CreatePost
  } else {
    Content = PostContent
  }
  return <div class={this.cssClass}>
    <LocationInfo />
    <Content />
  </div>
}

注意,在JSX中,标签首字母的大小写很重要。小写字母开头的标签会被视为 createElement 函数的字符串参数,解析为HTML元素或已注册组件;大写字母开头的标签会被视为变量。

重写 GeoBlog.vue 组件并添加 BlogContent 组件:

import AppMenu from './AppMenu.vue'
import BlogMap from './BlogMap.vue'
import BlogContent from './content/BlogContent.vue'
export default {
  render (h) {
    return <div class="geo-blog">
      <AppMenu />
      <div class="panes">
        <BlogMap />
        <BlogContent />
      </div>
    </div>
  }
}

NoContent.vue 组件模板:

<template>
  <div class="no-content">
    <i class="material-icons">explore</i>
    <div class="hint">Click on the map to add a post</div>
  </div>
</template>

1.7 创建帖子

当用户在没有标记的地图位置上点击时,会创建一个草稿帖子,右侧面板的表单将编辑其内容。用户点击 “Create” 按钮时,将草稿发送到服务器,并将结果(新帖子数据)添加到帖子列表中。

1.7.1 草稿存储操作

posts 命名空间存储模块中,需要添加一些新的操作来创建、更新和清除草稿帖子:

actions: {
  clearDraft ({ commit }) {
    commit('draft', null)
  },
  createDraft ({ commit }) {
    commit('draft', {
      title: '',
      content: '',
      position: null,
      placeId: null
    })
  },
  setDraftLocation ({ dispatch, getters }, { position, placeId }) {
    if (!getters.draft) {
      dispatch('createDraft')
    }
    dispatch('updateDraft', {
      position,
      placeId
    })
  },
  updateDraft ({ dispatch, commit, getters }, draft) {
    commit('updateDraft', draft)
  }
}

用户点击地图时调用 setDraftLocation 操作,会自动创建新草稿(如果没有)并更新其位置。

1.7.2 Blog Map组件更改

为了集成Vuex存储,需要对 BlogMap 组件进行一些更改:
1. 添加 posts 命名空间模块的Vuex助手,并对 maps 模块的助手进行重命名:

const {
  mapGetters: mapsGetters,
  mapActions: mapsActions
} = createNamespacedHelpers('maps')
const {
  mapGetters: postsGetters,
  mapActions: postsActions
} = createNamespacedHelpers('posts')
  1. 添加 draft getter:
computed: {
  ...mapsGetters([
    'center',
    'zoom'
  ]),
  ...postsGetters([
    'draft'
  ])
}
  1. 添加 setDraftLocation 操作:
methods: {
  ...mapsActions([
    'setCenter',
    'setUserPosition',
    'setZoom'
  ]),
  ...postsActions([
    'setDraftLocation'
  ])
}
1.7.3 点击处理

为地图添加点击处理程序:
1. 在地图组件中添加点击事件:

<googlemaps-map
  :center="center"
  :zoom="zoom"
  :options="mapOptions"
  @update:center="setCenter"
  @update:zoom="setZoom"
  @click="onMapClick"
>
  1. 添加相应的方法:
onMapClick (event) {
  this.setDraftLocation({
    position: event.latLng,
    placeId: event.placeId
  })
}
1.7.4 幽灵标记

在草稿位置显示透明标记:

<googlemaps-marker
  v-if="draft"
  :clickable="false"
  :label="{
    color: 'white',
    fontFamily: 'Material Icons',
    text: 'add_circle'
  }"
  :opacity=".75"
  :position="draft.position"
  :z-index="6"
/>

1.8 帖子表单

CreatePost.vue 组件用于显示输入新帖子详细信息(如标题和内容)的表单。

  1. 创建模板:
<template>
  <form
    class="create-post"
    @submit.prevent="handleSubmit">
    <input
      name="title"
      v-model="title"
      placeholder="Title"
      required />
    <textarea
      name="content"
      v-model="content"
      placeholder="Content"
      required />
    <div class="actions">
      <button
        type="button"
        class="secondary"
        @click="clearDraft">
        <i class="material-icons">delete</i>
        Discard
      </button>
      <button
        type="submit"
        :disabled="!formValid">
        <i class="material-icons">save</i>
        Post
      </button>
    </div>
  </form>
</template>
  1. 映射 posts 模块的Vuex助手:
import { createNamespacedHelpers } from 'vuex'
const {
  mapGetters: postsGetters,
  mapActions: postsActions
} = createNamespacedHelpers('posts')
  1. 添加必要的getters和方法:
export default {
  computed: {
    ...postsGetters([
      'draft'
    ])
  },
  methods: {
    ...postsActions([
      'clearDraft',
      'createPost',
      'updateDraft'
    ])
  }
}
  1. 添加与表单输入元素绑定的计算属性:
title: {
  get () {
    return this.draft.title
  },
  set (value) {
    this.updateDraft({
      ...this.draft,
      title: value
    })
  }
},
content: {
  get () {
    return this.draft.content
  },
  set (value) {
    this.updateDraft({
      ...this.draft,
      content: value
    })
  }
},
formValid () {
  return this.title && this.content
}
  1. 添加 handleSubmit 方法:
handleSubmit () {
  if (this.formValid) {
    this.createPost(this.draft)
  }
}

1.9 发送请求

1.9.1 创建 createPost 操作

posts Vuex模块中创建 createPost 操作:

async createPost ({ commit, dispatch }, draft) {
  const data = {
    ...draft,
    position: draft.position.toJSON()
  }
  const result = await $fetch('posts/new', {
    method: 'POST',
    body: JSON.stringify(data)
  })
  dispatch('clearDraft')
  commit('addPost', result)
  dispatch('selectPost', result._id)
}

该操作准备数据,发送POST请求到服务器,清除草稿,将新帖子添加到存储并选择该帖子。

1.9.2 创建 selectPost 操作
async selectPost ({ commit }, id) {
  commit('selectedPostId', id)
  // TODO fetch the post details (comments, etc.)
}

1.10 获取帖子

1.10.1 存储操作

每次用户平移或缩放地图导致地图边界改变时,需要获取帖子。为了解决请求顺序问题,使用唯一标识符来中止先前的请求。

  1. posts.js 文件顶部声明唯一标识符:
let fetchPostsUid = 0
  1. 添加 fetchPosts 操作:
async fetchPosts ({ commit, state }, { mapBounds, force }) {
  let oldBounds = state.mapBounds
  if (force || !oldBounds || !oldBounds.equals(mapBounds)) {
    const requestId = ++fetchPostsUid
    const ne = mapBounds.getNorthEast()
    const sw = mapBounds.getSouthWest()
    const query = `posts?ne=${encodeURIComponent(ne.toUrlValue())}&sw=${encodeURIComponent(sw.toUrlValue())}`
    const posts = await $fetch(query)
    if (requestId === fetchPostsUid) {
      commit('posts', {
        posts,
        mapBounds
      })
    }
  }
}
1.10.2 操作调度

maps 存储中创建 setBounds 操作,当地图平移或缩放后空闲时调度该操作,它会调度 posts 模块的 fetchPosts 操作:

setBounds ({ dispatch }, value) {
  dispatch('posts/fetchPosts', {
    mapBounds: value
  }, {
    root: true
  })
}

总结

通过以上步骤,我们完成了地理定位博客的主要功能开发,包括渲染函数、JSX的使用、虚拟DOM的处理、帖子的创建和获取等。整个开发过程涉及到Vuex存储、组件通信、地图交互等多个方面,希望这些内容能帮助你更好地理解和开发类似的应用。

流程图

graph TD;
    A[用户点击地图] --> B{是否有草稿};
    B -- 否 --> C[创建草稿];
    B -- 是 --> D[更新草稿位置];
    C --> D;
    D --> E[显示CreatePost组件];
    E --> F{表单是否有效};
    F -- 是 --> G[发送请求创建帖子];
    G --> H[清除草稿];
    H --> I[添加新帖子到存储];
    I --> J[选择新帖子];
    F -- 否 --> K[提示表单无效];

表格

操作 描述
clearDraft 清除草稿帖子
createDraft 创建草稿帖子
setDraftLocation 设置草稿帖子的位置
updateDraft 更新草稿帖子的内容
createPost 发送请求创建新帖子
selectPost 选择帖子
fetchPosts 获取地图边界内的帖子
setBounds 调度 fetchPosts 操作

2. 功能总结与拓展思路

2.1 已实现功能回顾

我们已经完成了地理定位博客的多个核心功能,下面对这些功能进行简要回顾:
- 渲染函数与JSX :掌握了 createElement 函数(别名 h )的使用,通过渲染函数实现动态模板,同时借助JSX让代码更接近HTML语法,提高了开发效率和代码可读性。
- 虚拟DOM :了解了Vue通过虚拟DOM和差异比较来优化DOM操作,减少性能开销的原理。
- 帖子管理 :实现了草稿帖子的创建、更新和清除,以及真实帖子的创建、选择和获取功能,涉及到Vuex存储模块的多个操作。
- 地图交互 :实现了地图点击创建帖子、显示幽灵标记等交互功能,增强了用户体验。

2.2 拓展思路

基于已实现的功能,我们可以进一步拓展地理定位博客的功能,以下是一些拓展思路:
- 帖子详情页 :目前 selectPost 操作中只是简单设置了选中帖子的ID,后续可以实现帖子详情页,展示帖子的详细信息,如评论列表、点赞数等。
- 用户认证 :添加用户认证功能,让用户可以注册、登录,实现用户专属的帖子管理和互动功能。
- 地图标记优化 :可以对地图上的标记进行优化,如根据帖子的热度、点赞数等显示不同颜色或大小的标记。
- 搜索功能 :添加搜索功能,让用户可以根据关键词搜索帖子,提高信息查找的效率。

2.3 代码优化建议

在开发过程中,我们可以对代码进行一些优化,提高代码的可维护性和性能:
- 代码复用 :提取一些重复的代码逻辑,封装成组件或函数,提高代码的复用性。
- 错误处理 :在发送请求时,添加错误处理机制,提高系统的健壮性。例如,在 createPost 操作中,可以捕获请求异常并提示用户。

async createPost ({ commit, dispatch }, draft) {
  try {
    const data = {
      ...draft,
      position: draft.position.toJSON()
    }
    const result = await $fetch('posts/new', {
      method: 'POST',
      body: JSON.stringify(data)
    })
    dispatch('clearDraft')
    commit('addPost', result)
    dispatch('selectPost', result._id)
  } catch (error) {
    console.error('创建帖子失败:', error)
    // 可以在这里添加提示用户的逻辑
  }
}
  • 性能优化 :在获取帖子时,可以考虑添加缓存机制,减少不必要的请求。

2.4 总结与展望

通过本文的介绍,我们详细了解了地理定位博客的开发过程,从渲染函数、JSX的使用到帖子的创建和获取,再到地图交互功能的实现。整个开发过程涉及到Vue、Vuex、地图API等多个技术点,希望这些内容能帮助你更好地理解和开发类似的应用。

未来,我们可以根据拓展思路进一步完善地理定位博客的功能,为用户提供更好的体验。同时,不断优化代码,提高系统的性能和可维护性。

流程图

graph TD;
    A[用户进入博客] --> B{是否登录};
    B -- 否 --> C[显示登录/注册页面];
    C --> D[用户登录/注册];
    D --> E[显示博客主页];
    B -- 是 --> E;
    E --> F{是否点击地图};
    F -- 是 --> G[创建/编辑帖子];
    F -- 否 --> H[浏览帖子列表];
    G --> I{是否提交帖子};
    I -- 是 --> J[发送请求创建帖子];
    J --> K[更新帖子列表];
    I -- 否 --> G;
    H --> L{是否点击帖子};
    L -- 是 --> M[显示帖子详情页];
    L -- 否 --> H;

表格

拓展功能 描述
帖子详情页 展示帖子的详细信息,如评论列表、点赞数等
用户认证 实现用户注册、登录功能,管理用户专属帖子
地图标记优化 根据帖子热度、点赞数等显示不同标记
搜索功能 让用户可以根据关键词搜索帖子
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值