Skip to content

从零开始 Mapbox 应用开发

这是本系列开发专栏的第 03 章, 我们采用更灵活和可控的形式来初始化和管理我们的地图渲染。

Demo: Lingr Map 应用模板:lingr-dev/lingr-map-mapbox: A Lingerer map application, using Mapbox GL. (github.com)

1. 工程目录


在之前的两个章节中,我们完成了整个框架的初始化,并初步配置了常见的前端应用开发中必备的一些资源和栏目,这里我们正式进入地图框架的搭建。首先,我们仍然会沿用当前主流的前端应用中类MVC(Model-View-Controller)的一种源代码架构模式来划分我们的整个工程文件夹目录,形成类似如下的工程目录结构:

bash
├─public
  └─fonts
├─src
  ├─assets
  └─style
  ├─components
  ├─map
  └─system
  ├─hooks
  └─map
  ├─layout
  ├─pages
  ├─router
  ├─stores
  └─utils
      ├─log
      └─mapbox
          └─factory
└─types

通过这种类似MVC架构的源代码结构划分,我们可以有效的对中小型项目的源代码进行分层,并较好的适配现代前端工程化中涉及的多种技术框架和工具。

1) 前端路由

代码中引入了 vue-router 来提供基于前端浏览器的路由方案,目的是为了更好的适应地图框架的可扩展性。当然,如果我们仅仅提供单一的地图功能页面,其实也可以不依赖任何路由框架,这一点根据实际的业务场景需求来决定。这里我们考虑引入 vue-router 并使用基于 History API 的路由方案。

bash
pnpm add vue-router

首先通过 pnpm 或其他的包管理工具安装 vue-router,之后我们通过配置式路由的方式来搭建页面的路由。

JavaScript
// src/router/index.js

import { createWebHistory, createRouter } from "vue-router";

function getAbsolutePath() {  
  const path = location.pathname;  
  return path.substring(0, path.lastIndexOf("/") + 1);  
}

const router = createRouter({  
  history: createWebHistory(getAbsolutePath()),  
  routes: [  
    {  
      path: "/",  
      name: "home",  
      redirect: "/home",  
    },  
    {  
      path: "/",  
      component: () => import("../layout/StyleLayout.vue"),  
      children: [  
        {  
          path: "home",  
          name: "home",  
          component: () => import("../pages/home.vue"),  
        },  
      ],  
    },  
  ],  
});  
  
export default router;

通过将 pages/home.vue 文件作为地图入口页面,并将根目录的访问请求重定向到指定的页面,同时,采用统一的 layout 包装组件来管理整个页面的布局。最后,在Vue应用的入口点注册路由即完成应用路由的配置。

JavaScript
// src/main.js
import router from "@/router";

...
const app = createApp(App);

app.use(router);

app.mount("#app");

2) Pinia全局状态

Pinia作为Vue 3x生态中首推的全局状态管理库,其简单的API设计、Composition API 的兼容性以及良好的可扩展性,对大型企业级应用中的组件状态共享都是必不可少的组件包之一。这里我们引入 Pinia 作为全局状态库来管理整个地图应用的全局状态。

bash
pnpm add pinia
JavaScript
// src/main.js
import { createPinia } from "pinia";

...
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);

app.mount("#app");

我们约定通过 src/stores 目录来存储所有的Pinia Stores。至此,我们完成了整个地图应用框架的工程化配置,下面我们开发创建地图组件。

2. 地图组件


不同于其他大多数的业务应用以结构化数据为中心的展示,地图类应用程序具备自身的特点,其大多数的渲染与交互能力来自于特定的地图框架SDK,而这些SDK通常围绕Canvas相关的API构建,并不是常规Vue或者React这一类面向DOM树的视图层框架管理的范畴。在这一类基于VDOM的视图层应用框架中,地图这一类的操作统一被视作“副作用(Side Effect)”。

不同的是,我们在地图应用程序的开发中往往关注的重点正是在于对这些地图的操作,因此,如何对 Vue 开发框架下的地图应用程序中的副作用进行合理的编排管理是开发人员需要考虑的重点。

幸运的是,Vue 3x开始提供的Composition API风格语法提供了一种函数式的方式可以让我们抽取和隔离 Vue 体系下的DOM相关内容和地图应用SDK中的地图操作内容。我们通过自定义的Composable Function 作为桥梁来连接这两者。React生态同样提供了Custom Hooks的方式,这两者可谓殊途同归。

JavaScript
// src/hooks/map/useMapboxView.js

const useMapboxView = (container) => {  
  const mapStore = useMapStore();  
  
  onMounted(() => {  
    if (!container.value) return;  
  
    configMapboxGL();  
      
    const map = mapCreator.createMap(container.value);  
  
    map.on("load", () => {  
      mapStore.onViewReady(map);  
    });  
  });  
};

这里,我们采用了一种称之为"Thin Composable"风格的设计模式来规划我们的代码结构。顾名思义,通过薄薄的一层Composable Function,我们将基于JavaScript/TypeScript语言的地图框架SDK操作函数(通常为纯函数实现,为了便于进行单元测试),与Vue体系中的响应式系统链接起来。

pAVlq2T.png

1)通过工厂函数创建地图实例

我们通过将Mapbox相关SDK的操作代码与Vue响应式体系进行解耦来增强整个开发框架的健壮性和可维护性,这里我们通过统一的工厂函数向Vue暴露地图实例创建的接口。

JavaScript
// src/utils/map-creator.js

export default {

  createMap(viewDiv) {
    const map = new mapboxgl.Map({
      container: viewDiv,
      style: "mapbox://styles/mapbox/streets-v12",
    });

    createMapControls(map);

    return map;
  },
};

2)通过Pinia Store全局存储地图

为了让Vue框架对地图实例的创建和内部状态变更等行为具备感知,我们借助于Pinia Store向组件提供全局的响应式状态,并同时把地图实例存储在Store中。Pinia提供两种形式来定义一个Store,为了提高地图应用的性能,我们采用Vue提供的shallowRef来包裹地图实例对象,以避免响应式系统深度监听地图实例的全部属性,因此,我们采用函数式的风格来定义全局的Map Store。

JavaScript
// src/stores/map.js

const useMapStore = defineStore("map", () => {  
  const map = shallowRef(null);  
  const ready = ref(false);  
  
  function onViewReady(inst) {  
    map.value = inst;  
    ready.value = true;  
  }  
  
  return {  
    map,  
    ready,  
  
    onViewReady,  
  };  
});  
  
export { useMapStore };

3. 其他配置


现代的Vite框架提供了一种插件式的架构允许开发者基于约定的可扩展点来对Vite的运行时行为进行自定义。因此,围绕Vite生态出现了许多关注开发提效的插件,并得到广泛的使用和验证。其中,有些插件仅满足特定场景下的使用,另一些则具备通用性,我们可以通过引入一些常用的插件来进一步完备我们的框架。

1)Auto-Import

unplugin-auto-import 是一个广为使用的自动补全import语句的插件,通过引入该插件,可以避免我们在各个Vue组件中重复引用一些频繁使用的组件、工具或是函数。

bash
pnpm add unplugin-auto-import -D
JavaScript
// vite.config.js
import AutoImport from "unplugin-auto-import/vite";

export default defineConfig({

  plugins: [
	  ...
	  AutoImport({  
	    imports: ["vue", "vue-router", "@vueuse/core", "pinia"],  
	    dts: "types/auto-import.d.ts",  
	    dirs: ["src/stores", "src/hooks"],  
	  }),
	  ...
  ],
});

通过AutoImport特性,我们为所有的Vue组件以及dirs指定目录下的文件自动引用了imports指定的库范围内暴露的API,从而无需反复多次的编写诸如 import { ref } from 'vue' 这样高频的代码,一定程度上提高了代码编写的效率。

至此,我们完成了地图应用框架的第一个里程碑,通过初步的代码结构划分,我们搭建了一个层次结构较为合理的企业级地图应用程序开发框架。后续我们还将陆续为当前的地图框架增加更多的功能特性,并在迭代过程中持续的重构和优化。

如果你觉得本文对你有些许启发,请持续关注我的公众号“戈伊星球”吧!

Released under the MIT License.