前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >使用高德API和MapboxGL实现路径规划并语音播报

使用高德API和MapboxGL实现路径规划并语音播报

原创
作者头像
牛老师讲GIS
发布于 2024-11-11 10:28:02
发布于 2024-11-11 10:28:02
1630
举报

概述

本文使用高德API实现位置查询和路径规划,使用MapboxGL完成地图交互与界面展示,并使用Web Speech API实现行驶中路线的实时语音播报。

效果

Web Speech API简介

Web Speech API使你能够将语音数据合并到 Web 应用程序中。Web Speech API有两个部分:SpeechSynthesis 语音合成(文本到语音 TTS)和 SpeechRecognition 语音识别(异步语音识别)。

  • 语音识别通过 SpeechRecognition接口进行访问,它提供了识别从音频输入(通常是设备默认的语音识别服务)中识别语音情景的能力。一般来说,你将使用该接口的构造函数来构造一个新的 SpeechRecognition对象,该对象包含了一系列有效的对象处理函数来检测识别设备麦克风中的语音输入。SpeechGrammar 接口则表示了你应用中想要识别的特定文法。文法则通过 JSpeech Grammar Format (JSGF.)来定义。
  • 语音合成通过SpeechSynthesis接口进行访问,它提供了文字到语音(TTS)的能力,这使得程序能够读出它们的文字内容(通常使用设备默认的语音合成器)。不同的声音类类型通过SpeechSynthesisVoice对象进行表示,不同部分的文字则由 SpeechSynthesisUtterance 对象来表示。你可以将它们传递给 SpeechSynthesis.speak()方法来产生语音。

SpeechSynthesisUtteranceHTML5中新增的API,用于将指定文字合成为对应的语音。它包含一些配置项,可以指定如何去阅读(如语言、音量、音调等)。

简单使用示例如下代码:

代码语言:js
AI代码解释
复制
// 创建 SpeechSynthesisUtterance 对象
var utterance = new SpeechSynthesisUtterance();
// 可选:设置语言(例如,中文)
utterance.lang = "zh-CN";
// 可选:设置语音
utterance.voice = window.speechSynthesis.getVoices()[0];
// 可选:设置音量(0.0到1.0之间)
utterance.volume = 1.0;
// 可选:设置语速(正常速度为1.0)
utterance.rate = 1.0;
// 可选:设置语调(正常语调为1.0)
utterance.pitch = 1.0;
// 设置要朗读的文本
utterance.text = '设置要朗读的文本';
window.speechSynthesis.speak(utterance);

实现

实现思路

  1. 地图初始化的时候通过H5的geolocation接口获取当前位置;
  2. 调用rgeo接口,根据获取到的位置获取位置所在市;
  3. 调用inputtips接口完成关键词联想查询;
  4. 调用/direction/driving接口完成路径的规划;
  5. 用MapboxGL实现地图交互与路径展示;
  6. 根据当前位置判断是否进入对应的步骤,提示对应的语音。

实现代码

示例使用Vue作为演示,界面与地图初始化代码如下:

代码语言:vue
AI代码解释
复制
<template>
  <div class="container">
    <div class="query">
      <el-form :model="form" label-width="auto" class="query-form">
        <el-form-item label="起点">
          <el-autocomplete
            v-model="form.startPosition"
            :fetch-suggestions="querySearchStart"
            clearable
            placeholder="请输入起点位置"
            @select="handleSelectStart"
          >
            <template #default="{ item }">
              <div class="autocomplete-value">{{ item.value }}</div>
              <div class="autocomplete-address">{{ item.address }}</div>
            </template></el-autocomplete
          >
        </el-form-item>
        <el-form-item label="终点" style="margin-bottom: 0">
          <el-autocomplete
            v-model="form.endPosition"
            :fetch-suggestions="querySearchEnd"
            clearable
            placeholder="请输入终点位置"
            @select="handleSelectEnd"
          >
            <template #default="{ item }">
              <div class="autocomplete-value">{{ item.value }}</div>
              <div class="autocomplete-address">{{ item.address }}</div>
            </template></el-autocomplete
          >
        </el-form-item>
      </el-form>
      <div class="query-button">
        <el-button
          :disabled="!form.startPosition || !form.endPosition"
          class="query-button-inner"
          @click="queryRoute"
          >查询</el-button
        >
      </div>
    </div>
    <div class="map" id="map">
      <div class="map-button">
        <el-button
          v-show="path"
          class="map-button-inner"
          type="primary"
          @click="toggleAnimate"
          >{{ isPlay ? "结束导航" : "开始导航" }}</el-button
        >
      </div>
    </div>
  </div>
</template>

<script>
const AK = "你申请的key"; // 高德地图key
const BASE_URL = "https://restapi.amap.com/v3";
let map = null,
  animation = null;

import { ElMessage, ElMessageBox } from "element-plus";
import AnimationRoute from "./utils/route";

//封装请求
function request(url, params) {
  let fullUrl = `${BASE_URL}${url}?key=${AK}`;
  for (const key in params) {
    fullUrl += `&${key}=${params[key]}`;
  }
  return new Promise((resolve, reject) => {
    fetch(fullUrl)
      .then((res) => res.json())
      .then((res) => {
        if (res.status === "1") resolve(res);
        else {
          ElMessage.error("接口请求失败");
          reject(res);
        }
      });
  });
}

export default {
  data() {
    return {
      center: [113.94150905808976, 22.523881824251347],
      form: {
        startPosition: "",
        endPosition: "",
        startCoord: [],
        endCoord: [],
      },
      cityInfo: {},
      path: null,
      isPlay: false,
    };
  },
  mounted() {
    const that = this;
    function successFunc(position) {
      const { longitude, latitude } = position.coords;
      that.center = [longitude, latitude];
      that.initMap(true);
    }
    function errFunc() {
      that.initMap(false);
    }
    if (navigator.geolocation) {
      try {
        errFunc();
        navigator.geolocation.getCurrentPosition(successFunc, errFunc);
      } catch (e) {
        errFunc();
      }
    } else {
      errFunc();
    }
  },
  methods: {
    toggleAnimate() {
      if (!animation) return;

      if (this.isPlay) {
        animation.pause();
        ElMessageBox.confirm("确认取消导航吗?", "提示", {
          confirmButtonText: "确认",
          cancelButtonText: "取消",
          type: "warning",
        })
          .then(() => {
            animation.destory();
            animation = null;
            this.path = null;
            this.form = {
              startPosition: "",
              endPosition: "",
              startCoord: [],
              endCoord: [],
            };
            this.isPlay = false;
          })
          .catch(() => {
            animation.play();
            this.isPlay = true;
          });
      } else {
        animation.play();
        this.isPlay = true;
      }
    },
    initMap(isLocate) {
      map = new SFMap.Map({
        container: "map",
        center: this.center,
        zoom: 17.1,
      });
      map.on("load", (e) => {
        new SFMap.SkyLayer({
          map: map,
          type: "atmosphere",
          // 设置天空光源的强度
          atmosphereIntensity: 12,
          // 设置太阳散射到大气中的颜色
          atmosphereColor: "rgba(87, 141, 219, 0.8)",
          // 设置太阳光晕颜色
          atmosphereHaloColor: "rgba(202, 233, 250, 0.1)",
        });
        request("/geocode/regeo", {
          location: this.center.join(","),
        }).then((res) => {
          this.cityInfo = res.regeocode.addressComponent;
        });
      });
    },
    querySearchStart(str, cb) {
      if (str) {
        request("/assistant/inputtips", {
          keywords: str,
          city: this.cityInfo?.city,
        }).then((res) => {
          cb(
            res.tips.map((t) => {
              t.value = t.name;
              return t;
            })
          );
        });
      } else {
        cb([]);
      }
    },
    querySearchEnd(str, cb) {
      if (str) {
        request("/assistant/inputtips", {
          keywords: str,
          city: this.cityInfo?.city,
        }).then((res) => {
          cb(
            res.tips.map((t) => {
              t.value = t.name;
              return t;
            })
          );
        });
      } else {
        cb([]);
      }
    },
    handleSelectStart(item) {
      this.form.startCoord = item.location.split(",").map(Number);
    },
    handleSelectEnd(item) {
      this.form.endCoord = item.location.split(",").map(Number);
    },
    queryRoute() {
      const { startCoord, endCoord } = this.form;
      request("/direction/driving", {
        origin: startCoord.join(","),
        destination: endCoord.join(","),
        extensions: "all",
      }).then((res) => {
        const path = res.route.paths[0];
        let coordinates = [];
        this.path = path;
        path.steps.forEach((step) => {
          const polyline = step.polyline
            .split(";")
            .map((c) => c.split(",").map(Number));
          coordinates = [...coordinates, ...polyline];
        });
        const route = {
          type: "Feature",
          properties: { path },
          geometry: {
            type: "LineString",
            coordinates: coordinates,
          },
        };
        animation = new AnimationRoute(map, route, false);
      });
    },
  },
};
</script>

<style scoped lang="scss">
.container {
  width: 100%;
  height: 100vh;
  overflow: hidden;
}
.query {
  padding: 0.8rem;
  overflow: hidden;
  background: #ccc;
  .query-form {
    width: calc(100% - 4rem);
    float: left;
  }
  .query-button {
    width: 2rem;
    height: 4.7rem;
    float: right;
    display: flex;
    justify-content: flex-end;
    align-items: center;
    .query-button-inner {
      width: 3.8rem;
      height: 100%;
    }
  }
}
.map {
  height: calc(100% - 6.4rem);
}

.autocomplete-value,
.autocomplete-address {
  white-space: nowrap;
  width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}
.autocomplete-address {
  font-size: 0.8rem;
  color: #999;
  margin-top: -0.8rem;
  border-bottom: 1px solid #efefef;
}
.map-button {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  z-index: 99;
  padding: 0.8rem;
  box-sizing: border-box;
  &-inner {
    width: 100%;
  }
}
:deep .el-form-item {
  margin-bottom: 0.5rem;
}
</style>

示例中使用轨迹播放的方式演示了位置的变化,前文mapboxGL轨迹展示与播放已经有过分享,示例在前文的基础上做了一点改动,改动完代码如下:

代码语言:js
AI代码解释
复制
const icon = "/imgs/car.png";
const arrow = "/imgs/arrow.png";

import * as turf from "@turf/turf";

class AnimationRoute {
  constructor(map, route, play = true, fit = true, speed = 60) {
    this._map = map;
    this._json = route;
    this._play = play;
    this._speed = speed;
    this._path = route.properties.path;
    this.init();

    if (fit) this._map.fitBounds(turf.bbox(route), { padding: 50 });
  }

  init() {
    const that = this;

    // 创建 SpeechSynthesisUtterance 对象
    var utterance = new SpeechSynthesisUtterance();
    // 可选:设置语言(例如,中文)
    utterance.lang = "zh-CN";
    // 可选:设置语音
    utterance.voice = window.speechSynthesis.getVoices()[0];
    // 可选:设置音量(0.0到1.0之间)
    utterance.volume = 1.0;
    // 可选:设置语速(正常速度为1.0)
    utterance.rate = 1.0;
    // 可选:设置语调(正常语调为1.0)
    utterance.pitch = 1.0;
    that.utterance = utterance;

    that._index = 0;
    const length = turf.length(that._json);
    const scale = 60;
    that._count = Math.round((length / that._speed) * 60 * 60) * scale;
    that._step = length / that._count;
    that._stepPlay = -1;
    that._flag = 0;
    that._playId = "play-" + Date.now();
    // 添加路径图层
    that._map.addSource(that._playId, {
      type: "geojson",
      data: that._json,
    });
    that._map.addLayer({
      id: that._playId,
      type: "line",
      source: that._playId,
      layout: {
        "line-cap": "round",
        "line-join": "round",
      },
      paint: {
        "line-color": "#aaaaaa",
        "line-width": 10,
      },
    });
    // 添加已播放路径
    that._map.addSource(that._playId + "-played", {
      type: "geojson",
      data: that._json,
    });
    that._map.addLayer({
      id: that._playId + "-played",
      type: "line",
      source: that._playId + "-played",
      layout: {
        "line-cap": "round",
        "line-join": "round",
      },
      paint: {
        "line-color": "#09801a",
        "line-width": 10,
      },
    });

    // 添加路径上的箭头
    that._map.loadImage(arrow, function (error, image) {
      if (error) throw error;
      that._map.addImage(that._playId + "-arrow", image);
      that._map.addLayer({
        id: that._playId + "-arrow",
        source: that._playId,
        type: "symbol",
        layout: {
          "symbol-placement": "line",
          "symbol-spacing": 50,
          "icon-image": that._playId + "-arrow",
          "icon-size": 0.6,
          "icon-allow-overlap": true,
        },
      });
    });

    // 添加动态图标
    that._map.loadImage(icon, function (error, image) {
      if (error) throw error;
      that._map.addImage(that._playId + "-icon", image);
      that._map.addSource(that._playId + "-point", {
        type: "geojson",
        data: that._getDataByCoords(),
      });
      that._map.addLayer({
        id: that._playId + "-point",
        source: that._playId + "-point",
        type: "symbol",
        layout: {
          "icon-image": that._playId + "-icon",
          "icon-size": 0.75,
          "icon-allow-overlap": true,
          "icon-rotation-alignment": "map",
          "icon-pitch-alignment": "map",
          "icon-rotate": 50,
        },
      });
      that._animatePath();
    });
  }

  pause() {
    this._play = false;
    window.cancelAnimationFrame(this._flag);
  }

  start() {
    this._index = 0;
    this.play();
  }

  play() {
    this._play = true;
    this._animatePath();
  }

  _animatePath() {
    if (this._index > this._count) {
      window.cancelAnimationFrame(this._flag);
    } else {
      const coords = turf.along(this._json, this._step * this._index).geometry
        .coordinates;
      // 已播放的线
      const start = turf.along(this._json, 0).geometry.coordinates;
      this._map
        .getSource(this._playId + "-played")
        .setData(turf.lineSlice(start, coords, this._json));

      // 车的图标位置
      this._map
        .getSource(this._playId + "-point")
        .setData(this._getDataByCoords(coords));
      // 计算旋转角度
      const nextIndex =
        this._index === this._count ? this._count - 1 : this._index + 1;
      const coordsNext = turf.along(this._json, this._step * nextIndex).geometry
        .coordinates;
      let angle = turf.bearing(turf.point(coords), turf.point(coordsNext)) - 90;
      if (this._index === this._count) angle += 180;
      this._map.setLayoutProperty(
        this._playId + "-point",
        "icon-rotate",
        angle
      );
      const camera = this._map.getFreeCameraOptions();
      camera.position = mapboxgl.MercatorCoordinate.fromLngLat(coords, 100);
      camera.lookAtPoint(coordsNext);
      this._map.setFreeCameraOptions(camera);
      this._map.setPitch(80);
      this._index++;
      if (this._play) {
        this._playInstruction(coords);
        this._flag = requestAnimationFrame(() => {
          this._animatePath();
        });
      }
    }
  }

  _playInstruction(coords) {
    const { steps } = this._path;
    const stepPlay = this._stepPlay;
    const start = this._stepPlay !== -1 ? this._stepPlay : 0
    for (let i = start; i < steps.length; i++) {
      const step = steps[i];
      const polyline = step.polyline
        .split(";")
        .map((v) => v.split(",").map(Number));
      const pt = turf.point(coords);
      const line = turf.lineString(polyline);
      const dis = turf.pointToLineDistance(pt, line) * 1000;
      if (i > this._stepPlay && dis < 10) {
        this._stepPlay = i;
        break;
      }
    }
    if (stepPlay !== this._stepPlay) {
      this.utterance.text = steps[this._stepPlay].instruction;
      window.speechSynthesis.speak(this.utterance);
    }
  }

  _getDataByCoords(coords) {
    if (!coords || coords.length !== 2) return null;
    return turf.point(coords, {
      label: this._formatDistance(this._step * this._index),
    });
  }

  _formatDistance(dis) {
    if (dis < 1) {
      dis = dis * 1000;
      return dis.toFixed(0) + "米";
    } else {
      return dis.toFixed(2) + "千米";
    }
  }

  destory() {
    window.cancelAnimationFrame(this._flag);
    if (this._map.getSource(this._playId + "-point")) {
      this._map.removeLayer(this._playId + "-point");
      this._map.removeSource(this._playId + "-point");
    }
    if (this._map.getSource(this._playId + "-played")) {
      this._map.removeLayer(this._playId + "-played");
      this._map.removeSource(this._playId + "-played");
    }
    if (this._map.getSource(this._playId)) {
      this._map.removeLayer(this._playId);
      this._map.removeLayer(this._playId + "-arrow");
      this._map.removeSource(this._playId);
    }
  }
}

export default AnimationRoute;

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
Mac上的软件包管理工具
Linux系统有个让人蛋疼的通病,软件包依赖,好在当前主流的两大发行版本都自带了解决方案,Red hat/CentOS有yum,Ubuntu有apt-get
码客说
2019/10/22
3.1K0
MAMP PRO for Mac(专业Web开发环境)
MAMP PRO for Mac是一款专业Web开发环境,可以打开本地邮件服务器,以便通过PHP脚本调度邮件。
Mac软件分享
2022/08/09
1.6K0
MAMP PRO for Mac(专业Web开发环境)
MAMP Pro for Mac(PHP/MySQL开发环境)
MAMP Pro for Mac是一款基于macOS平台的本地服务器软件,可以让用户在本地计算机上搭建Web服务器环境,方便用户进行网站开发和测试。它包括了Apache服务器、MySQL数据库和PHP脚本语言,用户可以使用它来搭建和管理本地的网站、应用和数据库等,同时还支持多个PHP版本和虚拟主机等高级功能。
快乐的小丸子
2023/04/24
2.2K0
转:全新安装Mac OSX 开发者环境 同时使用homebrew搭建 (LNMP开发环境)
Brew 是 Mac 下面的包管理工具,通过 Github 托管适合 Mac 的编译配置以及 Patch,可以方便的安装开发工具。 Mac 自带ruby 所以安装起来很方便,同时它也会自动把git也给你装上。官方网站:http://brew.sh 。
全栈程序员站长
2021/05/19
1.4K0
MAMP Pro for mac/win(PHP/MySQL开发环境)
MAMP PRO一款专业的本地Web服务器开发环境软件。MAMP这几个首字母代表的是Macintosh、Apache、MySQL和PHP。MAMP内含Apache 服务器、PHP安装套件以及MySQL安装套件。只要轻松点选就能安装架站/讨论区/论坛必备的元件。透过Web界面稍作设定,在本地电脑上架设自己专属的网站,都是Apache+Mysql+PHP的集成环境。
捧着风的少女
2022/11/11
1.5K0
MAMP Pro for mac/win(PHP/MySQL开发环境)
Fedora 11 的安装以及 LAMP环境的搭建(二)
        Windows下的同事,很多都使用winrar来压缩和传输文件,所以这个也是不可或缺的,提供对于rar压缩格式的支持
大江小浪
2018/07/25
3730
mamp环境下禁止页面缓存
2018-06-0312:52:23 发表评论 1℃热度 MAMP Pro是一款适用于Mac操作系统的软件。MAMP PRO是专业级版本的经典本地服务器环境的os x软件。MAMP这几个首字母代表苹果的OSX系统上的Macintosh、Apache、MySQL和PHP,顾名思义,你应该知道MAMP的强大功能 啦!MAMP 内含 Apache 服务器、PHP 安装套件以及MySQL安装套件。只要轻松点选就能安装架站/讨论区/论坛必备的元件。透过Web界面稍作设定,在苹果电脑上架设自己专属的网站,就是这 么简
timhbw
2018/06/06
2.3K0
Mac OS X安装php工作环境
1.安装Apche Sudo apachectl start 2.开启php支持 sudo vi /etc/apache2/httpd.conf 找到LoadModule php5_module libexec/apache2/libphp5.so 去掉前面的# 3.编辑PHP.ini sudo cp /etc/php.ini.default /etc/php.ini 4.重启Aache测试环境 sudo apachectl restart 编写文件 sudo vi /Library/WebServer/D
苦咖啡
2018/05/07
1K0
mac php开发集成环境,MAC OS X下php集成开发环境mamp
之前苦于mac上搭建本地服务器之艰辛,找寻好久都没找到一款类似windows上集成的本地服务器环境,诸如phpstudy,xampp,appserv,虽说xampp也有mac版,但不知为何不是Apache启动不了,这里小编为大家分享了MAC OS X 下php集成开发环境mamp教程,下面大家跟着学习啦小编一起来了解一下吧。
全栈程序员站长
2022/09/13
3.7K0
mac php开发集成环境,MAC OS X下php集成开发环境mamp
PHP开发环境搭建工具有哪些?
因为要做php开发,搭建一个能够运行php网站的服务器环境是第一步,传统的php环境软件非常复杂,好在很多公司开发了一键搭建php安装环境,一键进行php环境配置,大大节省了搭建php mysql环境的时间!对老手来说安装配置php环境也不再是一件繁琐的事。
大脸猫
2020/06/27
5.2K1
PHP开发环境搭建工具有哪些?
Mac搭建PHP环境[通俗易懂]
原因:因为Mac下默认php.ini配置的default_socket在/var/mysql/mysql.socket,而后安装的mysql的socket文件大多在/tmp/mysql.socket。
全栈程序员站长
2022/11/08
2.4K0
Mac搭建PHP环境[通俗易懂]
探索7个MAMP本地开发环境的高效替代软件
本地开发环境是Web开发环境中的一种类型,它是指开发者自己的计算机上配置的一套用于开发和测试网站或应用程序的软件集合。这套环境使得开发者可以在本地计算机上构建和测试网站,而无需实时部署到服务器。
侧风
2024/04/08
8480
探索7个MAMP本地开发环境的高效替代软件
Mamp pro 5.1破解版
Mamp 是Mac系统上强大的PHP集成开发环境 mamp代表Macintosh、Apache、MySQL和PHP,即包含Macintosh、Apache、MySQL和PHP四大开发环境。 截图
Inkedus
2020/04/16
1.2K0
Mamp pro 5.1破解版
环境变量详解
 在终端输入的命令行对应着应用程序,如果不是系统自带的命令,那么系统需要环境变量来定位应用程序所在的文件路径。
Fisherman渔夫
2020/02/18
1.3K0
三步将Mac系统默认PHP版本切换为MAMP等扩展环境中的PHP版本
平时做开发的时候大多都是在Mac系统下,开发环境用的是MAMP集成的,但是Mac系统原本就带有Apache的。这种情况下回默认使用系统自带的PHP版本,最近由于项目需要用到PHP7.1的版本,在不升级系统版本的情况下实现切换到MAMP环境的PHP版本!免去系统版本升级麻烦 1.先查出MAMP下面集成的PHP版本 cd /Applications/MAMP/bin/php ls -ls 2.编辑修改 .bash_profile 文件(没有.bash_profile 文件的情况下回自动创建) sudo vim
企鹅号小编
2018/02/06
3.9K0
三步将Mac系统默认PHP版本切换为MAMP等扩展环境中的PHP版本
mac 初次配置apache,及mac下安装mysql
先打开apache,在浏览器上输入    localhost     回车后会如果屏幕上显示:It works!  如下图:
lin_zone
2018/08/15
2K0
mac 初次配置apache,及mac下安装mysql
iPhone手机越狱-逆向砸壳-代码注入
目前13以上系统还没有完美越狱的方案,可以临时使用checkra1n方案对手机进行越狱:
周希
2020/10/19
2.1K0
iPhone手机越狱-逆向砸壳-代码注入
mac搭建lamp开发环境
前段时间,由于一个在公司使用一个开源项目,发现该开源项目不支持PHP集成开发环境,但是使用mac自带的php版本又太低,于是想能不能安装两个版本进行切换,百度了很多方法发现不行。通过百度的多篇文章总结出来的,希望对大家有所帮助。
兔云小新LM
2019/07/22
3K0
如何使用 MAMP 快速搭建 php 环境
有时候网站、项目需要在本地搭建 php 环境,如果还像以前手工配置一个个环境就太复杂了,而且也不是每个人都能掌握这个技能的。后来就出现了很多本地 php 环境包,可以一键搭建本地 php 环境。前面魏
魏艾斯博客www.vpsss.net
2018/06/01
1.9K0
MAC上PHP集成开发环境搭建
用惯了在Windows上配置php开发环境,要在MAC上捣腾一个PHP开发环境还不大习惯,那mac上php开发环境怎么搭建配置呢?有哪些集成软件呢? 本文为你推荐几款常用的mac php环境软件,并介绍这些软件的安装与配置教程,下面一起动手搭建一个macbook php开发环境吧!
骤雨重山
2022/01/17
3.7K0
推荐阅读
相关推荐
Mac上的软件包管理工具
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档