全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2022-11-14_CSS 架构模式之 BEM 在组件库中的实践

您的位置:首页 >> 新闻 >> 行业资讯

CSS 架构模式之 BEM 在组件库中的实践 本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究 前言在软件领域有很多的架构思想,通过不同的架构模式,可以让你的软件工程易拓展、易维护、易复用。同样在 CSS 工程当中,我们也需要使用架构思想,如果 CSS 没有使用架构思想的话,就会存在 CSS 代码极度混乱,难复用、难拓展、难维护等问题。特别是如果一个系统页面极度复杂的情况下,没有对 CSS 代码进行一个规划的话,那么后期维护 CSS 代码则堪称灾难。 所以我们需要像其他编程语言那样通过一些架构模式进行提高 CSS 代码的健壮性和可维护性。本文将探讨 Element Plus 组件库中的 CSS 架构思想。 CSS 设计模式之 OOCSS我们如果对 Element Plus 的 CSS 架构稍微有些了解的话,就知道 Element Plus 的 CSS 架构使用了 BEM 设计模式,而 BEM 是 OOCSS 的一种实现模式,可以说是进阶版的 OOCSS。那么什么是 OOCSS 呢? OOCSS 的全称为 Object Oriented CSS (面向对象的 CSS),它让我们可以使用向对象编程思想进行编写 CSS。 面向对象有三大特征:封装、继承、多态,在 OOCSS 中,主要应用到了面向对象的封装和继承的思想。我们以掘金的下图这个部分来进行说明: item.png图中画红色的部分,可以看成是有四个容器组成的,每个容器里面的内容又不一样。那么每个容器都有相同的样式,那么我们就可以进行封装。 把每一个容器封装成一个叫item的class,因为它们都有一些共同的样式。 .item{ position:relative; margin-right:20px; font-size:13px; line-height:20px; color:#4e5969; flex-shrink:0; } 然后如果我们需要对它们每一项进行拓展的话,那么我们只需要在原来的样式基础上进行新增一个 class,再针对这个 class 写不同的样式即可,这样达到继承原来基础部分的样式进行拓展自己独有的样式。 li.png通过上图可以得知我们相当于继承基础类型 item 后,然后分别拓展出浏览 view、点赞 like、评论 comment、更多 more的 CSS 内容。 .item.view{ //浏览 } .item.like{ //点赞 } .item.comment{ //评论 } .item.more{ //更多 } 通过这种模式就大大增加了 CSS 代码的可维护性,可以在没有修改源代码的基础上进行修正和拓展。同时通过上面的例子,我们可以引出 OOCSS 的两大原则: 容器(container)与内容(content)分离结构(structure)与皮肤(skin)分离例如在 Element Plus 组件库中就有两个经典的布局组件Container 布局容器和Layout 布局,这是 OOCSS 的典型应用: container.png实质上我们在写 Vue 组件的时候,就是在对 CSS 进行封装,这也是 OOCSS 的实践方式之一。 el-buttonclass="self-button"默认按钮/el-button stylelang="stylus"rel="stylesheet/stylus"scoped .self-button{ color:white; margin-top:10px; width:100px; } /style 例如上述代码,我们的 Element Plus 组件库已经对 el-button 组件的样式进行了封装,但我们还可以基于 el-button 组件的样式进行拓展我们符合我们项目 UI 的样式,这就是典型的封装与继承。 OOCSS 强调重复使用类选择器,避免使用 id 选择器,最重要的是从项目的页面中分析抽象出“对象”组件,并给这些对象组件创建 CSS 规则,最后完善出一套基础组件库。这样业务组件就可以通过组合多个 CSS 组件实现综合的样式效果,这体现了 OOCSS 的显著优点:可组合性高。 OOCSS 为我们提供了一种编写 CSS 代码的思维模型或者说方法论,后续则演化出更加具体的一种实现模式,也就是 BEM。 CSS 设计模式之 BEMBEM 是由 Yandex 团队提出的一种 CSS 命名方法论,即 Block(块)、Element(元素)、和 Modifier(修改器)的简称,是 OOCSS 方法论的一种实现模式,底层仍然是面向对象的思想。下面我们从 Element Plus 的 Tabs 组件进行讲解 BEM 的核心思想。 tab.png那么整一个组件模块就是一个 Block(块),classname 取名为:el-tabs。Block 代表一个逻辑或功能独立的组件,是一系列结构、表现和行为的封装。 其中每个一个切换的标签就是一个 Element(元素),classname 取名为:el-tabs__item。Element(元素)可以理解为块里的元素。 Modifier(修改器)用于描述一个 Block 或者 Element 的表现或者行为。例如我们需要对两个 Block(块) 或者两个 Element(元素)进行样式微调,那么我们就需要通过 Modifier(修改器),Modifier(修改器)只能作用于 Block(块)或者 Element(元素),Modifier(修改器)是不能单独存在的。 例如按钮组件的 classname 取名为 el-button,但它有不通过状态譬如:primary、success、info,那么就通过 Modifier(修改器)进行区分,classname 分别为: el-button--primary、el-button--success、el-button--info。从这里也可以看出 BEM 本质上就是 OOCSS,基础样式都封装为 el-button,然后通过继承 el-button 的样式,可以拓展不同的类,例如:el-button--primary、el-button--success、el-button--info。 BEM 规范下 classname 的命名格式为: block-name__element-namemodifier-namemodifier_value 所有实体的命名均使用小写字母,复合词使用连字符 “-” 连接。Block 与 Element 之间使用双下画线 “__” 连接。Mofifier 与 Block/Element 使用双连接符 “--” 连接。modifier-name 和 modifier_value 之间使用单下画线 “_” 连接。当然这些规则并不一定需要严格遵守的,也可以根据你的团队风格进行修改。 在 OOCSS 中,我们通过声明一个选择器对一个基础样式进行封装的时候,这个选择器是全局的,当项目庞大的时候,这样就容易造成影响到其他元素。通过 CSS 命名方法论BEM,则在很大程度上解决了这个问题。因为 BEM 同时规定 CSS 需要遵循只使用一个 classname 作为选择器,选择器规则中既不能使用标签类型、通配符、ID 以及其他属性,classname 也不能嵌套,此外通过 BEM 可以更加语义化我们的选择器名称。BEM 规范非常适用于公共组件,通过 BEM 命名规范可让组件的样式定制具有很高的灵活性。 此外通过 BEM 的命名规范可以让页面结构更清晰。 formclass="el-form" divclass="el-form-item" labelclass="el-form-item__label"名称:/label divclass="el-form-item__content" divclass="el-input" divclass="el-input__wrapper" inputclass="el-input__inner"/ /div /div /div /div /form 我们以 Element Plus 的 Form 表单的 HTML 结构进行分析,我们可以看到整个 classname 的命名是非常规范的,整个 HTML 的结构是非常清晰明了的。 通过 JS 生成 BEM 规范名称在编写组件的时候如果通过手写 classname 的名称,那么需要经常写el、-、__、--,那么就会变得非常繁琐,通过上文我们可以知道 BEM 命名规范是具有一定规律性的,所以我们可以通过 JavaScript 按照 BEM 命名规范进行动态生成。 命名空间函数是一个 hooks 函数,类似这样的 hooks 函数在 Element Plus 中有非常多,所以我们可以在 packages 目录下创建一个 hooks 模块(具体创建项目过程可参考本专栏的第二篇《2. 组件库工程化实战之 Monorepo 架构搭建》),进入到 hooks 目录底下初始化一个 package.json 文件,更改包名:@cobyte-ui/hooks。文件内容如下: { "name":"@cobyte-ui/hooks", "version":"1.0.0", "description":"ElementPluscomposables", "license":"MIT", "main":"index.ts", "module":"index.ts", "unpkg":"index.js", "jsdelivr":"index.js", "types":"index.d.ts", "peerDependencies":{ "vue":"^3.2.0" }, "gitHead":"" } 接着在 hooks 目录下再创建一个 use-namespace 目录用于创建 BEM 命名空间函数,再在 hooks 目录下创建一个 index.ts 文件用于模块入口文件。 index.ts 文件内容: export*from'./use-namespace' 本项目的 GitHub 地址:https://github.com/amebyte/element-plus-guide 首先引入一个命名空间的概念,所谓命名空间就是加多一个命名前缀。 import{computed,unref}from'vue' //默认命名前缀 exportconstdefaultNamespace='el' exportconstuseNamespace=(block:string)={ //命名前缀也就是命名空间 constnamespace=computed(()=defaultNamespace) return{ namespace, } } 通过加多一个命名前缀,再加上 BEM 的命名规范就可以大大降低我们组件的 classname 与项目中的其他 classname 发生名称冲突的可能性。 通过前文我们知道 BEM 的命名规范就是通过一定的规则去书写我们的 classname,在 JavaScript 中则表现为按照一定规则去拼接字符串。 BEM 命名字符拼接函数: //BEM命名字符拼接函数 const_bem=( namespace:string, block:string, blockSuffix:string, element:string, modifier:string )={ //默认是Block letcls=`${namespace}-${block}` //如果存在Block前缀,也就是Block里面还有Block,例如:el-form下面还有一个el-form-item if(blockSuffix){ cls+=`-${blockSuffix}` } //如果存在元素 if(element){ cls+=`__${element}` } //如果存在修改器 if(modifier){ cls+=`--${modifier}` } returncls } 这里值得注意的是 Block 也有可能有前缀,也就是 Block 里面还有 Block,例如:el-form下面还有一个el-form-item。 通过 BEM 命名字符拼接函数,我们就可以自由组合生成各种符合 BEM 规则的 classname 了。 exportconstuseNamespace=(block:string)={ //命名前缀也就是命名空间 constnamespace=computed(()=defaultNamespace) //创建块例如:el-form constb=(blockSuffix='')= _bem(unref(namespace),block,blockSuffix,'','') //创建元素例如:el-input__inner conste=(element?:string)= element?_bem(unref(namespace),block,'',element,''):'' //创建块修改器例如:el-form--default constm=(modifier?:string)= modifier?_bem(unref(namespace),block,'','',modifier):'' //创建前缀块元素例如:el-form-item constbe=(blockSuffix?:string,element?:string)= blockSuffixelement ?_bem(unref(namespace),block,blockSuffix,element,'') :'' //创建元素修改器例如:el-scrollbar__wrap--hidden-default constem=(element?:string,modifier?:string)= elementmodifier ?_bem(unref(namespace),block,'',element,modifier) :'' //创建块前缀修改器例如:el-form-item--default constbm=(blockSuffix?:string,modifier?:string)= blockSuffixmodifier ?_bem(unref(namespace),block,blockSuffix,'',modifier) :'' //创建块元素修改器例如:el-form-item__content--xxx constbem=(blockSuffix?:string,element?:string,modifier?:string)= blockSuffixelementmodifier ?_bem(unref(namespace),block,blockSuffix,element,modifier) :'' //创建动作状态例如:is-successis-required constis:{ (name:string,state:boolean|undefined):string (name:string):string }=(name:string,...args:[boolean|undefined]|[])={ conststate=args.length=1?args[0]!:true returnnamestate?`${statePrefix}${name}`:'' } return{ namespace, b, e, m, be, em, bm, bem, is, } } 最后我们就可以在组件中引入 BEM 命名空间函数进行创建各种符合 BEM 命名规范的 classname 了,例如: 创建块 el-form、创建元素 el-input__inner、创建块修改器 el-form--default、创建前缀块元素 el-form-item、创建元素修改器 el-scrollbar__wrap--hidden-default、创建动作状态 例如:is-success is-required具体创建代码使用代码如下: import{ useNamespace, }from'@cobyte-ui/hooks' //创建classname命名空间实例 constns=useNamespace('button') 然后就可以在 template 中进行使用了: template button ref="_ref" :class="[ ns.b() ]" 按钮/button template 通过 SCSS 生成 BEM 规范样式我们在本专栏的第二篇《2. 组件库工程化实战之 Monorepo 架构搭建》中已经创建一个样式主题的目录theme-chalk。现在我们接着在这个目录下创建组件样式代码,我们在theme-chalk目录下创建一个src目录,在src目录下创建一个mixins目录。 Element Plus 的样式采用 SCSS 编写的,那么就可以通过 SCSS 的 @mixin 指令定义 BEM 规范样式。在mixins目录下新建三个文件:config.scss、function.scss、mixins.scss。 其中 config.scss 文件编写 BEM 的基础配置比如样式名前缀、元素、修饰符、状态前缀: $namespace:'el'!default;//所有的组件以el开头,如el-input $common-separator:'-'!default;//公共的连接符 $element-separator:'__'!default;//元素以__分割,如el-input__inner $modifier-separator:'--'!default;//修饰符以--分割,如el-input--mini $state-prefix:'is-'!default;//状态以is-开头,如is-disabled 在 SCSS 中,我们使用$+ 变量名:变量来定义一个变量。在变量后加入!default表示默认值。给一个未通过!default声明赋值的变量赋值,此时,如果变量已经被赋值,不会再被重新赋值;但是如果变量还没有被赋值,则会被赋予新的值。 mixins.scss 文件编写 SCSS 的 @mixin 指令定义的 BEM 代码规范。 定义 Block: @mixinb($block){ $B:$namespace+'-'+$block!global; .#{$B}{ @content; } } $B表示定义一个一个变量,$namespace 是来自 config.scss 文件中定义的变量,!global表示其是一个全局变量,这样就可以在整个文件的任意地方使用。#{}字符串插值,类似模板语法。通过@content可以将include{}中传递过来的内容导入到指定位置。 定义 Element: @mixine($element){ $E:$element!global; $selector: $currentSelector:''; @each$unitin$element{ $currentSelector:#{$currentSelector+ '.'+ $B+ $element-separator+ $unit+ ','}; } @ifhitAllSpecialNestRule($selector){ @at-root{ #{$selector}{ #{$currentSelector}{ @content; } } } }@else{ @at-root{ #{$currentSelector}{ @content; } } } } 首先定义一个全局变量$E,接着定义父选择器$selector,再定义当前的选择器$currentSelector,再通过循环得到当前的选择器。接着通过函数 hitAllSpecialNestRule(hitAllSpecialNestRule 函数在 mixins 目录的 function.scss 文件中) 判断父选择器是否含有 Modifier、表示状态的.is-和 伪类,如果有则表示需要嵌套。@at-root的作用就是将处于其内部的代码提升至文档的根部,即不对其内部代码使用嵌套。 定义修改器: @mixinm($modifier){ $selector: $currentSelector:''; @each$unitin$modifier{ $currentSelector:#{$currentSelector+ $selector+ $modifier-separator+ $unit+ ','}; } @at-root{ #{$currentSelector}{ @content; } } } 这个非常好理解,就是定义了父选择器变量$selector和 当前选择器变量$currentSelector,并且当前选择器变量初始值为空,再通过循环传递进来的参数$modifier,获得当前选择器变量$currentSelector的值,再定义样式内容,而样式内容是通过@content将include{}中传递过来的内容。 定义动作状态: @mixinwhen($state){ @at-root{ &.#{$state-prefix+$state}{ @content; } } } 选择器就是 config.scss 文件中的变量$state-prefix加传进来的状态变量,而样式内容是通过@content将include{}中传递过来的内容。 接着我们再看下上面定义 Element 的时候说到的 hitAllSpecialNestRule 函数,这个函数是定义在 mixins 目录下的 function.scss 文件中。function.scss 文件内容如下: @use'config'; //该函数将选择器转化为字符串,并截取指定位置的字符 @functionselectorToString($selector){ $selector:inspect( $selector //inspect(...)表达式中的内容如果是正常会返回对应的内容,如果发生错误则会弹出一个错误提示。 $selector:str-slice($selector,2,-2);//str-slice截取指定字符 @return$selector; } //判断父级选择器是否包含'--' @functioncontainsModifier($selector){ $selector:selectorToString($selector); @ifstr-index($selector,config.$modifier-separator){ //str-index返回字符串的第一个索引 @returntrue; }@else{ @returnfalse; } } //判断父级选择器是否包含'.is-' @functioncontainWhenFlag($selector){ $selector:selectorToString($selector); @ifstr-index($selector,'.'+config.$state-prefix){ @returntrue; }@else{ @returnfalse; } } //判断父级是否包含':'(用于判断伪类和伪元素) @functioncontainPseudoClass($selector){ $selector:selectorToString($selector); @ifstr-index($selector,':'){ @returntrue; }@else{ @returnfalse; } } //判断父级选择器,是否包含`--``.is-``:`这三种字符 @functionhitAllSpecialNestRule($selector){ @returncontainsModifier($selector)orcontainWhenFlag($selector)or containPseudoClass($selector); } 通过上述代码我们就可以知道 hitAllSpecialNestRule 函数是如何判断父选择器是否含有 Modifier、表示状态的.is-和 伪类的了。 测试实践 BEM 规范接下来,我们要把上面实现的 BEM 规范应用到真实组件中,通过写一个简易的测试组件进行测试实践。首先我们的样式是基于 SCSS 所以我们需要安装 sass。 我们在根目录下执行: pnpminstallsass-D-w 接着我们把上新建的 hooks 模块也安装到根项目上: 我们在根目录下执行: pnpminstall@cobyte-ui/hooks-D-w 而 theme-chalk 模块,我们在本专栏的第二篇的《2. 组件库工程化实战之 Monorepo 架构搭建》已经进行了安装,这里就不用再进行安装了。 我们在 packages 目录下的 components 目录创建一个 icon 目录,再创建以下目录结构: ├──packages │├──components ││├──icon │││├──src ││││└──icon.vue │││└──index.ts ││└──package.json index.ts 文件内容: importIconfrom'./src/icon.vue' exportdefaultIcon icon.vue 文件内容: template i:class="bem.b()" slot/ /i /template scriptsetuplang="ts" import{useNamespace}from'@cobyte-ui/hooks' constbem=useNamespace('icon') /script 我们通过导入上面使用 JS 生成 BEM 规范名称 hooks 函数,然后创建对应的命名实例 bem,然后生成对应的 Block 块的 classname。接下来,我们把这个测试组件渲染到页面上,看看具体生成的效果。 我们去到 play 目录下的 src 目录中的 App.vue 文件中把上面写的测试组件进行引入: template div c-iconIcon/c-icon /div /template scriptsetuplang="ts" importCIconfrom'@cobyte-ui/components/icon' import'@cobyte-ui/theme-chalk/src/index.scss' /script stylescoped/style 我们也把theme-chalk目录中的样式也进行了导入。接着我们在 theme-chalk 目录下的 src 目录新建一个 icon.scss 文件,文件内容如下: @use'mixins/mixins'as @includeb(icon){ background-color: color:#fff; } 这里我们可以看到 SCSS 的@mixin、@include的用法:@mixin用来定义代码块、@include进行引入。 我们需要在 theme-chalk 目录下的 src 目录中的 index.scss 中导入 icon.scss 文件。 index.scss 文件内容: @use'./icon.scss'; 这样我们就实现了所有文件的闭环,最后我们把 play 项目运行起来,看看效果,要运行 play 项目,我们专栏的前面的文章中已经说过了,就是在根目录下执行pnpm run dev即可。 icon-play.png我们也看到已经成功实现了渲染并和如期一样,那么其他样式的测试,我们将在后续具体的组件实现上再进行测试。 经典 CSS 架构 ITCSS本小节我们继续通过学习经典 CSS 架构 ITCSS 来对比学习 Element Plus 的样式系统架构。我们可以向一些经典的 CSS 架构去学习取经,看看人家是怎么做架构的,从而可以在我们做自身的 CSS 架构的时候可以带来一些的启发,然后取长补短,从而可以更加优化自身的 CSS 架构。 ITCSS 基于分层的概念把项目中的样式分为七层,分别如下: Settings 层:维护一些包含字体、颜色等的变量,主要是变量层。Tools 层:工具库,例如 SCSS 中的 @mixin 、@functionGeneric 层:重置和/或标准化样式等,例如 normalize.css、reset.css,主要解决浏览器的样式差异Elements 层:对一些元素进行定制化的设置,如 H1 标签默认样式,A 标签默认样式等Objects 层:类名样式中,不允许出现外观属性,例如 Color,遵循OOCSS方法的页面结构类,主要用来做画面的 layoutComponents 层:UI 组件Trumps 层:实用程序和辅助类,能够覆盖前面的任何内容,也就是设置important!的地方ITCSS 不是一个框架,只是组织样式代码的一种方案,ITCSS 的分层越在上面的分层,被复用性就越广,层的权重是层层递进,作用范围却是层层递减。除了 ITCSS 之外还有其他一些 CSS 架构,比如 SMACSS 、ACSS 等,但它们的核心思想并不是放之四海而皆准的,但是它维护项目样式的思想却是值得借鉴的。我们可以不完全遵守它们的规则,可以根据我们的项目需要进行删减或者保留。 那么根据上面 ITCSS 架构思想,Element Plus 也设置了Settings 层,在theme-chalk/src/common目录下的 var.scss 文件中就维护着各种变量。我们上文中实现的 BEM 规范的mixins目录下的 function.scss、mixins.scss 等则是Tools 层。Generic 层主要解决浏览器的样式差异,这些工作应该在具体的项目中进行处理,而 Element Plus 只是一个第三方的工具库,所以 Element Plus 在这一层不进行设置。Elements 层主要是对一些基础元素拓展一些样式,从而让我们的网站形成一套自己的风格,例如对 H1 标签默认样式,A 标签默认样式的设置。Element Plus 则在theme-chalk/src目录下的 reset.scss 文件进行了设置。Objects 层和Components 层其实就是 OOCSS 层,也就是我们上文所说的 BEM 样式规范,又因为组件库的样式使用一般都分为全量引入和按需引入,所以就根据组件名称分别设置各自样式文件进行维护各自的样式。Trumps 层在 Element Plus 中也是没有的,同样是因为 Element Plus 只是一个第三方工具库,权重是比较低,所以不需要设置权重层。 总结我们先从什么是 OOCSS 开始,OOCSS 主要运用了传统编程类中的封装和继承的特性,OOCSS 为我们提供了一种编写 CSS 代码的思维模型或者说方法论,后续则演化出更加具体的一种实现模式,也就是 BEM。 接着我们从 Element Plus 的 Tabs 组件进行讲解 BEM 的核心思想。BEM 本质上就是 OOCSS,通过 BEM 可以更加语义化我们的选择器名称。BEM 规范非常适用于公共组件,通过 BEM 命名规范可让组件的样式定制具有很高的灵活性。 接着我们介绍如何通过 JS 生成 BEM 规范名称。在编写组件的时候如果通过手写 classname 的名称,那么需要经常写el、-、__、--,那么就会变得非常繁琐,通过上文我们可以知道 BEM 命名规范是具有一定规律性的,所以我们可以通过 JavaScript 按照 BEM 命名规范进行动态生成。 接着学习如何通过 SCSS 生成 BEM 规范样式,并学习了 SCSS 的一些核心知识。 之后写了一个 demo 组件进行验证我们所写的 BEM 规范代码。 最后我们通过学习经典 CSS 架构 ITCSS 的思想,从而更加深入理解 Element Plus 的 CSS 架构设置思想。我们可以看到 Element Plus 的 CSS 架构并不是单一的使用了具体那一种架构,而是融合了多种架构的思想。 所以所谓的 CSS 架构,它们的核心思想并不是放之四海而皆准的,但是它维护项目样式的思想却是值得借鉴的。我们可以不完全遵守它们的规则,可以根据我们的项目需要进行删减或者保留。 Element Plus 的 CSS 样式架构中还有非常多少值得学习的知识,后续具体组件的实现,我们再继续探讨。 阅读原文

上一篇:2019-10-21_AI当自强:独家揭秘旷视自研人工智能算法平台Brain++ 下一篇:2023-12-13_微软小模型击败大模型:27亿参数,手机就能跑

TAG标签:

16
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价