前端低代码调研与总结

近些年来,低代码的概念逐渐流行了起来,而低代码产品也越来越多的出现在我们的身边。低代码可以叫做可视化搭建,或者叫效能工具等等。像国外的Mendix,国内的宜搭、苍穹、简道云、amis等等。基于这种新型的开发方式,图形化的拖拉拽配置界面,并兼容了自定义的组件、代码扩展,确实在B端后台管理类网站建设中很大程度上的提升了效率。低代码平台能够高效且便捷,成本又低。就应用领域来讲已经很广泛了,例如营销领域,各种页面生产工具,非冰,乐高,宜搭,鲁班。还有电商类的公司都会给商家提供一个类似店铺装修的工具,小程序生产工具

大家好,又见面了,我是你们的朋友全栈君。

近些年来,低代码的概念逐渐流行了起来,而低代码产品也越来越多的出现在我们的身边。低代码可以叫做可视化搭建,或者叫效能工具等等。像国外的Mendix,国内的宜搭苍穹简道云amis等等。基于这种新型的开发方式,图形化的拖拉拽配置界面,并兼容了自定义的组件、代码扩展,确实在B端后台管理类网站建设中很大程度上的提升了效率。

低代码平台能够高效且便捷,成本又低。就应用领域来讲已经很广泛了,例如营销领域,各种页面生产工具,非冰,乐高,宜搭,鲁班。还有电商类的公司都会给商家提供一个类似店铺装修的工具,小程序生产工具等等。在线的所见即所得的文本编辑器不也是低代码应用吗。像是工程控制领域也喜欢低代码的可视化操作平台,可以不用理解艰深的技术就可以完成工作。总体来看,这是非常值得做的事。

本人也基于现有的开源低代码demo做了一些改动,在自己的服务器上部署了一套简单的低代码demo。demo演示地址: http://120.79.203.154:8080/#/ 其界面如下。github地址: https://github.com/llz1990/llz-lowcode-demo

image.png
同类型开源的低代码项目有很多,都值得大家学习:

  • 百度开源的amis:https://gitee.com/baidu/amis

  • 鲁班h5:https://github.com/ly525/luban-h5

  • h5移动端低代码平台:https://github.com/buqiyuan/vite-vue3-lowcode

1. 理解低代码

首先需要了解什么是低代码? 低代码(Low-Code Development,LCD),开发者主要通过图形化用户界面和配置来创建应用软件,而不是像传统模式那样主要依靠手写代码。低代码开发模式的开发者,通常不需要具备非常专业的编码技能,或者不需要某一专门领域的编码技能,而是可以通过平台的功能和约束来实现专业代码的产出。

我们如果需要搭建一个低代码平台。首先,需要明确低代码平台解决的最大问题是复用,复用也是目前前端开发中的一个重要课题,特别是当前的主流前端js框架,例如 vuereact 等,都是组件化的开发方式,又如形形色色ui组件库的出现,像 ant-designelement 等都是来解决重复造轮子的问题。如下图所示vue组件化开发模式:
image.png
我们在前端开发脚手架中,通常会创建一些通用的组件,然后在各个需要这个组件的地方进行引用,来提升开发效率。在脚手架中引用一些ui组件库也是出于这样的目的。为了防止重复的造轮子,我们通常会对一些成熟的ui组件库中组件根据我们的业务需要进行二次封装,形成一个具有更多功能的区块,如下图式演示demo 的区域展示部分:我们可以看见按钮、矩形区域、图片区域等都可以风转成固定组件,通过拖拽到指定大区域中形成我们需要的页面元素展示样式。

image.png
我们可以通过一个json数据来描述这个区块,数据大概是下图所示的样子,包含了这个区块的编码,中文名称,以及可传入到这个区块中的属性及初始属性值,例如这个表格包含的列信息、按钮信息等等。而这个json也是低代码平台搭建中最为核心的部分,它在后续介绍的可视化拖拉拽页面设计中扮演极其重要的角色,它是页面设计和页面渲染间串联的纽带。如下图所示,是我们当前demo 操作在localstorage 中存储的json 数据,其中包含上图中展示的4个组件的所有组件信息(icon、 label、 name、 events、 component等)。

image.png

综上所述,当我们展示界面时,只需要在页面文件中引用左侧的组件,再把对应的描述json信息传递到组件内就可以了。但是这样似乎还是采用了编码的方式去解决复用问题,距离我们的低代码还有些距离。因此,我们将这些可动态控制的组件属性通过在线表单进行填写,存储到数据库中。前端工程中,我们可以将使用这个区块模板的页面路由所对应的组件都指向我们封装好的列表模板。这样,进入到这个页面,我们只需要将之前存储到数据库的区块描述信息通过api接口获取到,再传递到组件内部,将json数据中我们设置的模板属性值赋值到组成模板的各个组件上就可以完成这个模板的渲染了。这也是我们在实现可视化拖拉拽低代码平台之前所使用的方式。这个界面渲染的流程便是低代码平台的核心逻辑:通过组件元数据拼装成一个页面的描述信息,然后通过渲染器组件将描述信息转化页面dom元素

2. 低代码的核心功能实现

低代码开发平台都是通过拖拉拽可视化的页面设计器进行页面开发的。我们来探索一下页面设计器的实现方式大多数的页面设计器都包含了如下所示的几个区域:

  • 最上方是操作栏,我们可进行页面的保存、预览、查看json信息、查看代码等操作;
  • 左侧是组件列表,当然也可以添加一些切换,让我们的左侧区域支持查看页面树信息、配置数据源等其他操作;
  • 中间是画布区域,我们可以将左侧的组件拖动到画布中,当然也支持画布中组件的赋值、删除等操作;
  • 右侧是属性配置区域,当我们在画布中选中某个组件时,可以在右侧的属性配置区域罗列出当前组件可支持动态配置的属性,修改了属性后可以在画布中看到对应组件的样式变化。
    如下图是钉钉的宜搭低代码平台的页面设计器的展示图和当前使用demo页面的展示图,下文中我们将以此demo 来逐步探讨低代码实现的核心逻辑。

image.png

image.png

2.1 画布编辑器

编辑器的核心其实就是中间的画布。它的作用是:当从左边组件列表拖拽出一个组件放到画布中时,画布要把这个组件渲染出来。这个编辑器的实现思路是:

  1. 用一个数组 componentData 维护编辑器中的数据。
  2. 把组件拖拽到画布中时,使用 push() 方法将新的组件数据添加到 componentData
  3. 编辑器使用 v-for 指令遍历 componentData,将每个组件逐个渲染到画布。
    编辑器渲染的核心代码如下所示:
<component 
  v-for="item in componentData"
  :key="item.id"
  :is="item.component"
  :style="item.style"
  :propValue="item.propValue"
/>

编辑器中的每个组件数据大概是下面这样。在遍历 componentData 组件数据时,主要靠 is 属性来识别出真正要渲染的是哪个组件。例如要渲染的组件数据是 { component: 'v-text' },则 <component :is="item.component" /> 会被转换为 <v-text />。当然,你这个组件也要提前注册到 Vue 中。

{ 
   
    component: 'v-text', // 组件名称,需要提前注册到 Vue
    label: '文字', // 左侧组件列表中显示的名字
    propValue: '文字', // 组件所使用的值
    icon: 'el-icon-edit', // 左侧组件列表中显示的名字
    animations: [], // 动画列表
    events: { 
   }, // 事件列表
    style: { 
    // 组件样式
        width: 200,
        height: 33,
        fontSize: 14,
        fontWeight: 500,
        lineHeight: '',
        letterSpacing: 0,
        textAlign: '',
        color: '',
    },
}

2.2 自定义组件

原则上使用第三方组件也是可以的,但最好封装一下。不管是第三方组件还是自定义组件,每个组件所需的属性可能都不一样,所以每个组件数据可以暴露出一个属性 propValue 用于传递值。在这个 DEMO 组件列表中定义了三个组件。我们以其中的图片组件Picture为例:

// 1.图片组件
<template>
    <div style="overflow: hidden">
        <img :src="propValue">
    </div>
</template>
<script>
export default { 
   
    props: { 
   
        propValue: { 
   
            type: String,
            require: true,
        },
    },
}
</script>

2.3 组件的拖拽

将组件列表中的组件拖拽到画布中,必须给它添加一个 draggable 属性。另外,在将组件列表中的组件拖拽到画布中,还有两个事件是起到关键作用的:

  1. dragstart 事件,在拖拽刚开始时触发。它主要用于将拖拽的组件信息传递给画布。
  2. drop 事件,在拖拽结束时触发。主要用于接收拖拽的组件信息。
    可以看到给列表中的每一个组件都设置了 draggable 属性。另外,在触发 dragstart 事件时,拖拽开始,同时使用 dataTransfer.setData() 传输数据:
<div @dragstart="handleDragStart" class="component-list">
    <div v-for="(item, index) in componentList" :key="index" class="list" draggable :data-index="index">
        <i :class="item.icon"></i>
        <span>{ 
   { 
    item.label }}</span>
    </div>
</div>
......
handleDragStart(e) { 
   
    e.dataTransfer.setData('index', e.target.dataset.index)
}

触发 drop 事件时,拖拽的组件放置在画布中,使用 dataTransfer.getData() 接收传输过来的索引数据,然后根据索引找到对应的组件数据,再添加到画布,从而渲染组件。

<div class="content" @drop="handleDrop" @dragover="handleDragOver" @click="deselectCurComponent">
    <Editor />
</div>
......
handleDrop(e) { 
   
    e.preventDefault()
    e.stopPropagation()
    const component = deepCopy(componentList[e.dataTransfer.getData('index')])
    this.$store.commit('addComponent', component)
}

2.4 组件在画布中移动和层级调整

要想编辑器(画布)中的各个组件可以移动,则需要设置画布div的position属性为relative ,同时各个组件的position属性为 absolute。再通过监听三个事件来进行移动:

  1. mousedown 事件,在组件上按下鼠标时,记录组件当前的位置,即 x、y 坐标(为了方便讲解,这里使用的坐标轴,实际上 x、y 对应的是 css 中的 lefttop
  2. mousemove 事件,每次鼠标移动时,都用当前最新的 x、y 坐标减去最开始的 x、y 坐标,从而计算出移动距离,再改变组件位置。
  3. mouseup 事件,鼠标抬起时结束移动。
    组件移动的核心代码实现:
handleMouseDown(e) { 
   
    e.stopPropagation()
    this.$store.commit('setCurComponent', { 
    component: this.element, zIndex: this.zIndex })

    const pos = { 
    ...this.defaultStyle }
    const startY = e.clientY
    const startX = e.clientX
    // 如果直接修改属性,值的类型会变为字符串,所以要转为数值型
    const startTop = Number(pos.top)
    const startLeft = Number(pos.left)

    const move = (moveEvent) => { 
   
        const currX = moveEvent.clientX
        const currY = moveEvent.clientY
        pos.top = currY - startY + startTop
        pos.left = currX - startX + startLeft
        // 修改当前组件样式
        this.$store.commit('setShapeStyle', pos)
    }

    const up = () => { 
   
        document.removeEventListener('mousemove', move)
        document.removeEventListener('mouseup', up)
    }

    document.addEventListener('mousemove', move)
    document.addEventListener('mouseup', up)
}

由于拖拽组件到画布中是有先后顺序的,所以可以按照数据顺序来分配图层层级。例如画布新增了五个组件 a、b、c、d、e,那它们在画布数据中的顺序为 [a, b, c, d, e],图层层级和索引一一对应,即它们的 z-index 属性值是 0、1、2、3、4(后来居上)。用代码表示如下:

<div v-for="(item, index) in componentData" :zIndex="index"></div>

理解了这一点之后,改变图层层级就很容易做到了。即改变组件数据在 componentData 数组中的顺序。例如有 [a, b, c] 三个组件,它们的图层层级从低到高顺序为 abc(索引越大,层级越高),如果要将 b 组件上移,只需将它和 c 调换顺序即可:

const temp = componentData[1]
componentData[1] = componentData[2]
componentData[2] = temp

2.5 属性配置

image.png

如上图所示,每个组件都有一些通用属性和独有的属性,我们需要提供一个能显示和修改属性的地方。上文中我们已经知晓每个组件数据的大概结构:

{ 
   
    component: 'v-text', // 组件名称,需要提前注册到 Vue
    label: '文字', // 左侧组件列表中显示的名字
    propValue: '文字', // 组件所使用的值
    icon: 'el-icon-edit', // 左侧组件列表中显示的名字
    animations: [], // 动画列表
    events: { 
   }, // 事件列表
    style: { 
    // 组件样式
        width: 200,
        height: 33,
        fontSize: 14,
        fontWeight: 500,
        lineHeight: '',
        letterSpacing: 0,
        textAlign: '',
        color: '',
    },
}

我定义了一个 AttrList 组件,用来显示每个组件的属性。代码逻辑实际上就是遍历组件的 style 对象,将每一个属性遍历出来。并且需要根据具体的属性用不同的组件显示出来,例如颜色属性,需要用颜色选择器显示等等。为了方便用户修改属性值,我使用 v-model 将组件和值绑定在一起。

<template>
    <div class="attr-list">
        <el-form>
            <el-form-item v-for="(key, index) in styleKeys" :key="index" :label="map[key]">
                <el-color-picker v-if="key == 'borderColor'" v-model="curComponent.style[key]"></el-color-picker>
                <el-color-picker v-else-if="key == 'color'" v-model="curComponent.style[key]"></el-color-picker>
                <el-color-picker v-else-if="key == 'backgroundColor'" v-model="curComponent.style[key]"></el-color-picker>
                <el-select v-else-if="key == 'textAlign'" v-model="curComponent.style[key]">
                    <el-option
                        v-for="item in options"
                        :key="item.value"
                        :label="item.label"
                        :value="item.value"
                    ></el-option>
                </el-select>
                <el-input type="number" v-else v-model="curComponent.style[key]" />
            </el-form-item>
            <el-form-item label="内容" v-if="curComponent && curComponent.propValue && !excludes.includes(curComponent.component)">
                <el-input type="textarea" v-model="curComponent.propValue" />
            </el-form-item>
        </el-form>
    </div>
</template>

2.6 事件绑定

虽然看起来实现了通过组件拖拉拽完成页面的开发,但是目前的页面还是无法进行使用的,这是因为页面中的组件都是相互独立而又没有关联的。实际页面中的绝大多数组件都需要进行相互通讯。例如当我们点击某个按钮时,需要获取到表单组件的表单值进行提交;又比如我们点击某个按钮会弹出一个弹窗。因此还需要分析组件间的通讯、交互方式。

每个组件有一个 events 对象,用于存储绑定的事件:

// 编辑器自定义事件
const events = { 
   
    redirect(url) { 
   
        if (url) { 
   
            window.location.href = url
        }
    },
    alert(msg) { 
   
        if (msg) { 
   
            alert(msg)
        }
    },
}
const mixins = { 
   
    methods: events,
}
const eventList = [
    { 
   
        key: 'redirect',
        label: '跳转事件',
        event: events.redirect,
        param: '',
    },
    { 
   
        key: 'alert',
        label: 'alert 事件',
        event: events.alert,
        param: '',
    },
]
export { 
   
    mixins,
    events,
    eventList,
}

通过 v-for 指令将事件列表渲染出来,选中事件时将事件添加到组件的 events 对象。

<el-tabs v-model="eventActiveName">
    <el-tab-pane v-for="item in eventList" :key="item.key" :label="item.label" :name="item.key" style="padding: 0 20px">
        <el-input v-if="item.key == 'redirect'" v-model="item.param" type="textarea" placeholder="请输入完整的 URL" />
        <el-input v-if="item.key == 'alert'" v-model="item.param" type="textarea" placeholder="请输入要 alert 的内容" />
        <el-button style="margin-top: 20px;" @click="addEvent(item.key, item.param)">确定</el-button>
    </el-tab-pane>
</el-tabs>

预览或真正渲染页面时,也需要在每个组件外面套一层 DIV,这样就可以在 DIV 上绑定一个点击事件,点击时触发我们刚才添加的事件。

<template>
    <div @click="handleClick">
        <component
            class="conponent"
            :is="config.component"
            :style="getStyle(config.style)"
            :propValue="config.propValue"
        />
    </div>
</template>
......
handleClick() { 
   
    const events = this.config.events
    // 循环触发绑定的事件
    Object.keys(events).forEach(event => { 
   
        this[event](events[event])
    })
}

3. 低代码的特点

3.1 低代码的优势

简单来说,低代码为企业提供了降本、增效、提质的价值。降本、增效、提质,就是为企业降低研发成本、人力成本,提升研发效率,缩短产品交付周期,加快企业试错的速度,降低试错成本。使得企业的产品和服务以更快的速度进行迭代和优化,在激烈的市场竞争中胜出。在接受 Creatio 调研的 1000 位开发高管中,95% 的人认为低代码开发速度相对于传统方式有提高,其中 61% 的高管认为提高速度在 40% 以上

低代码为什么能够降本、增效、提质?低代码平台所具备的能力有哪些?

1、开发过程可视化。可视化交互是低代码平台所具备的一种必备能力,不再面对冷冰冰的传统文本IDE编辑器,转而和可视化的编辑器进行交互,不管是UI界面,交互事件、后端接口、数据库/Redis调用,都能通过优雅而简单的可视化交互完成配置和编辑。

2、代码开发组件化。这个能力和中台化、SDK的概念有相似之处,就是将重复的公共的能力沉淀出来,封装起来,让开发人员可以在低代码平台上,直接拿出来作为工具嵌到产品中,这样开发者就不用再关心这个功能/组件的内部实现。

3、一次开发,多端发布。对于前端研发人员来说,经常需要多端发布同一个项目/页面,H5/小程序/IOS/Android的开发工作,经常需要不同技术栈的研发人员。而对于低代码,就屏蔽了具体的代码选型,内部编辑都用一种低代码语言,最后发布上线,可以发布到小程序/安卓/IOS等多个端,而且能尽量保证UI、交互、功能的一致性。

3.2 典型低代码的特征

微信截图_20211125121435.png
如上图所示,一个基本的低代码平台必须具备如下几个方面特征:

  • 一、拖拽式开发 —— 拖拽式开发是“低代码”开发平台给大家最直观的印象,所以也是“低代码”开发平台最基本的特征。世面上的许多“低代码”平台都能够做得到,对于一个低代码平台来说也是必须且基本的操作。

  • 二、对象封装与数据模型 —— 这部分指的是低代码平台要操作的对象、数据模型、表达式等等,它可以是高度抽象和封装的对象,可以省略掉“类”、“接口”、“函数”这些编程语言的高级特性,以更简化的方式提供出来,供程序调用。

  • 三、模型驱动 —— “模型驱动”是相对于“表单驱动”的,指的是对于数据进行建模和处理,比如国外的低代码平台OutSystems、Mendix,就有很强大的模型驱动的能力,包括了定义实体、实体关联、主键、索引、数据查询等等。

  • 四、脚本语言 —— 脚本语言实际上就是编程语言了,是低代码平台实现复杂业务逻辑的扩展,可以使用 JavaScripts、Python、Java等语言进行编程。低代码平台会把语言的编译过程做好封装,做到一键发布,即时运行,方便代码调试。

  • 五、软件测试与部署 —— 低代码开发平台,本质上是软件开发工具。所以整体开发过程也要遵守软件工程的流程规范。只是把许多环节都做了简化、内部封装,降低了学习成本、开发成本、测试成本、部署成本。

  • 六、API与集成 —— 主要是解决低代码平台开发出来的系统,跟其它外部系统的数据互联互通,否则又是造了一堆大烟囱,一些数据孤岛。

4. 低代码的发展

对企业来说,低代码是对传统专业企业应用软件的一种扩展和升级,符合企业业务多样性和快速发展、按需采购、数字化战略升级的需求,也符合软件大厂的宣传和技术演进路线,所以企业接受度非常高。另一方面,软件厂商从提供专业软件/软件定制化开发服务切换到提供低代码平台,剥离了专业业务知识,转而通过平台提供一种让企业自己积累和分享专业知识/业务经验的标准和能力,对软件厂商来说降低了实施的成本、对企业来说提升自己的掌控力和业务响应能力,这是巨大的一个进步。国内外大量软件厂商和创业公司进入这一领域并开始服务越来越多的客户充分证明了这种趋势。目前市场主流软件供应商都加大了低代码平台发展力度,一方面是希望通过平台来简化和规范应用生产的难度和流程,提升生产力;另一方面减少对昂贵的专业开发人员依赖来降低成本。随着越来越多的企业开始接受低代码平台,资金正逐步流向低代码供应商,更加坚定了供应商投入研发低代码平台的信心。

从技术层次来看,一方面,新技术层出不穷,技术栈越来越长,细分领域也越来越多;一方面,参与IT系统设计开发人员的认知能力和技术水平参差不齐。两者相交的结果,面对同样的需求,不同的开发人员的设计和使用技术往往相差十万八千里,差异性往往带来后续高维护成本;同时伴随着国内IT领域人才的高流动率,往往导致一个企业内部各种不同技术栈和架构并存,最终不堪重负;不少中型的互联网公司在培养了不少专业方向的技术人才的情况下,尚且不能打通任督二脉,为上层业务开发者提供友好业务开发环境,何况急需数字化转型的大量传统企业。因此技术的分层很重要,通用技术层实现当前主流技术架构,低代码应用开发层实现企业应用开发的最佳实践,通过低代码平台真正让企业应用开发者关注业务,才能真正提升应用开发和对业务响应的效率。

参考资料及文档

  • https://github.com/woai3c/Front-end-articles
  • https://github.com/ly525/luban-h5
  • https://github.com/buqiyuan/vite-vue3-lowcode
  • https://developer.mozilla.org/zh-CN/docs/Web/API/Document/drag_event
  • https://zhuanlan.zhihu.com/p/432140729

本文掘金链接

  • https://juejin.cn/post/7034052451573432351
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/129401.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • java实战——图书管理系统

    因为这个写的比较完整,所以简单说明一下过程中使用的EJB和RMI两个东西。EJB实现原理:就是把原来放到客户端实现的代码放到服务器端,并依靠RMI进行通信。RMI实现原理:就是通过Java对象可序列化机制实现分布计算。好了,没了,就这么简单…想稍微深入了解一下的看一下这个好了,我就不再赘述。https://blog.csdn.net/lovechuanyu/article/…

  • ubuntu20.04 美化_ubuntu19美化

    ubuntu20.04 美化_ubuntu19美化本文目录效果终端文件管理器步骤Ubuntu上的准备工作tweaktool安装火狐浏览器/谷歌浏览器安装插件安装插件下载主题、图标等配置终端的标题栏太大???方案1方案2效果终端文件管理器步骤Ubuntu上的准备工作tweaktool安装sudoaptinstallgnome-tweak-tool火狐浏览器/谷歌浏览器安装插件在浏览器的插件管理界面搜索gnomeshellintegration插件,然后安装。安装插件上面的东西都准备好之后,去gnome插件官网

  • Oracle数据库ORA-12154: TNS: 无法解析指定的连接标识符解决方法[通俗易懂]

    Oracle数据库ORA-12154: TNS: 无法解析指定的连接标识符解决方法[通俗易懂]对于这个问题,对于我这种初学者来说是经常遇到的,今天就把可靠的解决发法记于此,希望能帮助到大家。ORA-12154:TNS:无法解析指定的连接标识符第一步:查看自己的Oracle服务是否打开。OracleDBConsoleORCL是Oracle网页端管理工具的服务,访问地址一般为“http://127.0.0.1:1158/em/console/logon/logon”,如果不习惯用…

  • 网管必备工具_ps功能介绍与工具使用视频

    网管必备工具_ps功能介绍与工具使用视频http://book.51cto.com/art/200903/116214.htm 转载于:https://blog.51cto.com/netsecing/163178

  • 【渗透测试】密码暴力破解工具——九头蛇(hydra)使用详解及实战

    【渗透测试】密码暴力破解工具——九头蛇(hydra)使用详解及实战【渗透测试】密码暴力破解工具——九头蛇(hydra)使用详解及实战

  • Apache和PHP结合

    Apache和PHP结合Apache和PHP结合配置httpd支持PHPServerNameRequirealldeniedAddTypeapplication/x-httpd-php.php//解析PHPDirectoryIndexindex.htmlindex.php[root@shuai-01~]#vim/usr/local/apache2.4/conf/httpd.conf修

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号