整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

JavaScript 的 7 种设计模式

文地址:Understanding Design Patterns in JavaScript

原文作者:Sukhjinder Arora

译者:HelloGitHub-Robert

当启动一个新的项目时候,我们不应该马上开始编程。而是首先应该定义项目的目的和范围,然后列出其功能或规格。如果你已经开始编程或者正在从事一个复杂的项目,则应该选择一个最适合你项目的设计模式。

什么是设计模式?

在软件工程中,设计模式是针对软件设计中常见问题的可重用解决方案。设计模式也是经验丰富的开发人员针对特定问题的最佳实践。它可以被当作编程的模板。

为什么要使用设计模式?

许多工程师要么认为设计模式浪费时间,要么不知道如何恰当的使用设计模式。但如果能正确使用设计模式,则可以帮助你写出更好的可读性更高的代码,并且代码更容易被维护和理解。

最重要的是,设计模式为软件开发人员提供了通用的词汇表。它们能让学习你代码的人很快了解代码的意图。例如,如果你的项目中使用了装饰器模式,那么新的开发可以很快就知道这段代码的作用,从而他们可以将更多精力放在解决业务问题上,而不是试图理解代码在做什么。

我们已经知道了什么是设计模式和它的重要性,下面我们深入研究一下 JavaScript 中的 7 种设计模式。

一、模块模式

模块是一段独立的代码,因此我们可以更新模块而不会影响代码的其它部分。模块还允许我们通过为变量创建单独的作用域来避免命名空间污染。当它们与其它代码解耦时,我们还可以在其它项目中重用模块。

模块是任何现代 JavaScript 应用程序不可或缺的一部分,有助于保持代码干净,独立和有条理。在 JavaScript 中有许多方法可以创建模块,其中一种是模块模式。

与其它编程语言不同,JavaScript 没有访问修饰符,也就是说,你不能将变量声明为私有的或公共的。因此,模块模式也可用来模拟封装的概念。

模块模式使用 IIFE(立即调用的函数表达式),闭包和函数作用域来模拟封装的概念。例如:

const myModule = (function() {  
  const privateVariable = 'Hello World';  
  function privateMethod() {
    console.log(privateVariable);
  }
  return {
    publicMethod: function() {
      privateMethod();
    }
  }
})();
myModule.publicMethod();

由于是 IIFE 因此代码会被立即执行,并将返回对象赋值给了 myModule 变量。由于闭包,即使在 IIFE 完成后,返回的对象仍可以访问 IIFE 内部定义的函数和变量。

因此,IIFE 内部定义的变量和函数对外部是看不见的,从而使其成为 myModule 模块的私有成员。

执行代码后,myModule 变量看起来像下面所示:

const myModule = {
  publicMethod: function() {
    privateMethod();
  }};

因此当我们调用 publicMethod() 时候,它将调用 privateMethod() 例如:

// Prints 'Hello World'
module.publicMethod();

二、揭示模块模式

揭示模块模式是 Christian Heilmann 对模块模式的略微改进。模块模式的问题在于,我们必须创建新的公共函数才能调用私有函数和变量。

在这种模式下,我们将返回的对象的属性映射到要公开暴露的私有函数上。这就是为什么将其称为揭示模块模式。例如:

const myRevealingModule = (function() {  
  let privateVar = 'Peter';
  const publicVar  = 'Hello World';
  function privateFunction() {
    console.log('Name: '+ privateVar);
  }
  
  function publicSetName(name) {
    privateVar = name;
  }
  function publicGetName() {
    privateFunction();
  }
  /** reveal methods and variables by assigning them to object     properties */
return {
    setName: publicSetName,
    greeting: publicVar,
    getName: publicGetName
  };
})();
myRevealingModule.setName('Mark');
// prints Name: Mark
myRevealingModule.getName();

这种模式让我们更容易知道哪些函数和变量是公共的,无形中提高了代码的可读性。执行代码后 myRevealingModule 看起来像下所示:

const myRevealingModule = {
  setName: publicSetName,
  greeting: publicVar,
  getName: publicGetName
};

当我们调用 myRevealingModule.setName('Mark') 时,实际调用了内部的 publicSetName。当调用 myRevealingModule.getName() 时,实际调用了内部的 publicGetName 例如:

myRevealingModule.setName('Mark');
// prints Name: Mark
myRevealingModule.getName();

与模块模式相比,揭示模块模式的优势有:

  • 通过修改 return 语句中的一行,我们可以将成员从公共变为为私人,反之亦然。
  • 返回的对象不包含任何函数定义,所有右侧表达式都在 IIFE 中定义,从而使代码清晰易读。

三、ES6 模块

在 ES6 之前,JavaScript 没有内置模块,因此开发人员必须依靠第三方库或模块模式来实现模块。但是自从 ES6,JavaScript 内置了模块。

ES6 的模块是以文件形式存储的。每个文件只能有一个模块。默认情况下,模块内的所有内容都是私有的。通过使用 export 关键字来暴露函数、变量和类。模块内的代码始终在严格模式下运行。

3.1 导出模块

有两种方法可以导出函数和变量声明:

  • 在函数和变量声明的前面添加 export 关键字。例如:
// utils.js
export const greeting = 'Hello World';
export function sum(num1, num2) {
  console.log('Sum:', num1, num2);
  return num1 + num2;
}
export function subtract(num1, num2) {
  console.log('Subtract:', num1, num2);
  return num1 - num2;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
  • 在代码的最后添加 export 关键字来暴露函数和变量。例如:
// utils.js
function multiply(num1, num2) {
  console.log('Multiply:', num1, num2);
  return num1 * num2;
}
function divide(num1, num2) {
  console.log('Divide:', num1, num2);
  return num1 / num2;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
export {multiply, divide};

3.2 导入模块

与导出模块相似,有两种使用 import 关键字导入模块的方法。例如:

  • 一次导入多个项目
// main.js
// importing multiple items
import { sum, multiply } from './utils.js';
console.log(sum(3, 7));
console.log(multiply(3, 7));
  • 导入所有模块
// main.js
// importing all of module
import * as utils from './utils.js';
console.log(utils.sum(3, 7));
console.log(utils.multiply(3, 7));

3.3 导入导出中使用别名

  • 重命名导出
// utils.js
function sum(num1, num2) {
  console.log('Sum:', num1, num2);
  return num1 + num2;
}
function multiply(num1, num2) {
  console.log('Multiply:', num1, num2);
  return num1 * num2;
}
export {sum as add, multiply};
  • 重命名导入
// main.js
import { add, multiply as mult } from './utils.js';
console.log(add(3, 7));
console.log(mult(3, 7));

四、单例模式

一个单例对象是只能实例化一次的对象。如果不存在,则单例模式将创建类的新实例。如果存在实例,则仅返回对该对象的引用。重复调用构造函数将始终获取同一对象。

JavaScript 是一直内置单例的语言。我们只是不称它们为单例,我们称它们为对象字面量。例如:

const user = {
  name: 'Peter',
  age: 25,
  job: 'Teacher',
  greet: function() {
    console.log('Hello!');
  }
};

因为 JavaScript 中的每个对象都占用一个唯一的内存位置,并且当我们调用该 user 对象时,实际上是在返回该对象的引用。

如果我们尝试将 user 变量复制到另一个变量并修改该变量。例如:

const user1 = user;
user1.name = 'Mark';

我们将看到两个对象都被修改,因为 JavaScript 中的对象是通过引用而不是通过值传递的。因此,内存中只有一个对象。例如:

// prints 'Mark'
console.log(user.name);
// prints 'Mark'
console.log(user1.name);
// prints true
console.log(user === user1);

可以使用构造函数来实现单例模式。例如:

let instance = null;

function User() {
  if(instance) {
    return instance;
  }
  instance = this;
  this.name = 'Peter';
  this.age = 25;
  
  return instance;
}
const user1 = new User();
const user2 = new User();
// prints true
console.log(user1 === user2);

调用此构造函数时,它将检查 instance 对象是否存在。如果对象不存在,则将 this 变量分配给 instance 变量。如果该对象存在,则只返回该对象。

单例也可以使用模块模式来实现。例如:

const singleton = (function() {
  let instance;
  
  function init() {
    return {
      name: 'Peter',
      age: 24,
    };
  }
  return {
    getInstance: function() {
      if(!instance) {
        instance = init();
      }
      
      return instance;
    }
  }
})();
const instanceA = singleton.getInstance();
const instanceB = singleton.getInstance();
// prints true
console.log(instanceA === instanceB);

在上面的代码中,我们通过调用 singleton.getInstance 方法来创建一个新实例。如果实例已经存在,则此方法仅返回该实例。如果该实例不存在,则通过调用该 init() 函数创建一个新实例。

五、工厂模式

工厂模式使用工厂方法创建对象而不需要指定具体的类或构造函数的模式。

工厂模式用于创建对象而不需要暴露实例化的逻辑。当我们需要根据特定条件生成不同的对象时,可以使用此模式。例如:

class Car{
  constructor(options) {
    this.doors = options.doors || 4;
    this.state = options.state || 'brand new';
    this.color = options.color || 'white';
  }
}
class Truck {
  constructor(options) {
    this.doors = options.doors || 4;
    this.state = options.state || 'used';
    this.color = options.color || 'black';
  }
}
class VehicleFactory {
  createVehicle(options) {
    if(options.vehicleType === 'car') {
      return new Car(options);
    } else if(options.vehicleType === 'truck') {
      return new Truck(options);
      }
  }
}

这里,创建了一个 Car 和一个 Truck 类(具有一些默认值),该类用于创建新的 cartruck对象。而且定义了一个VehicleFactory 类,用来根据 options 对象中的 vehicleType 属性来创建和返回新的对象。

const factory = new VehicleFactory();
const car = factory.createVehicle({
  vehicleType: 'car',
  doors: 4,
  color: 'silver',
  state: 'Brand New'
});
const truck= factory.createVehicle({
  vehicleType: 'truck',
  doors: 2,
  color: 'white',
  state: 'used'
});
// Prints Car {doors: 4, state: "Brand New", color: "silver"}
console.log(car);
// Prints Truck {doors: 2, state: "used", color: "white"}
console.log(truck);

我为类 VehicleFactory 创建了一个新的 factory 对象。然后,我们通过调用 factory.createVehicle 方法并且传递 options 对象,其 vehicleType 属性可能为 car 或者 truck 来创建新 CarTruck 对象。

六、装饰器模式

装饰器模式用于扩展对象的功能,而无需修改现有的类或构造函数。此模式可用于将特征添加到对象中,而无需修改底层的代码。

此模式的一个简单示例为:

function Car(name) {
  this.name = name;
  // Default values
  this.color = 'White';
}
// Creating a new Object to decorate
const tesla= new Car('Tesla Model 3');
// Decorating the object with new functionality
tesla.setColor = function(color) {
  this.color = color;
}
tesla.setPrice = function(price) {
  this.price = price;
}
tesla.setColor('black');
tesla.setPrice(49000);
// prints black
console.log(tesla.color);

这种模式的一个更实际的例子是:

假设汽车的成本取决于其功能的数量。如果没有装饰器模式,我们将不得不为不同的功能组合创建不同的类,每个类都有一个 cost 方法来计算成本。例如:

class Car() {
}

class CarWithAC() {
}

class CarWithAutoTransmission {
}

class CarWithPowerLocks {
}

class CarWithACandPowerLocks {
}

但是,通过装饰器模式,我们可以创建一个基类 car 并且通过装饰器函数给不同的对象添加对应的成本逻辑。

class Car {
  constructor() {
  // Default Cost
  this.cost = function() {
  return 20000;
  }
}
}
// Decorator function
function carWithAC(car) {
  car.hasAC = true;
  const prevCost = car.cost();
  car.cost = function() {
    return prevCost + 500;
  }
}
// Decorator function
function carWithAutoTransmission(car) {
  car.hasAutoTransmission = true;
   const prevCost = car.cost();
  car.cost = function() {
    return prevCost + 2000;
  }
}
// Decorator function
function carWithPowerLocks(car) {
  car.hasPowerLocks = true;
  const prevCost = car.cost();
  car.cost = function() {
    return prevCost + 500;
  }
}

首先,我们创建了小轿车的基类 Car。然后针对要添加的特性创建了装饰器并且此装饰器以 Car 对象为参数。然后通过返回更新后的小汽车成本来覆盖对象的成本函数,且添加了一个用来标识某个特性是否已经被添加的属性。

要添加新的功能,我们只需要像下面一样就可以:

const car = new Car();
console.log(car.cost());
carWithAC(car);
carWithAutoTransmission(car);
carWithPowerLocks(car);

最后,我们可以像这样计算汽车的成本:

// Calculating total cost of the car
console.log(car.cost());

结论

我们已经了解了 JavaScript 中使用的各种设计模式,但是这里没有涉及到可以用 JavaScript 实现的设计模式。

尽管了解各种设计模式很重要,但不要过度使用它们也同样重要。在使用设计模式之前,你应该仔细考虑你的问题是否适合该设计模式。要知道某个模式是否适合你的问题,应该好好研究该设计模式以及它的应用。

最后,欢迎优秀的你加入 HelloGitHub 的「译文亦舞」系列,让你的才华舞动起来!把优秀的文章分享给更多的人。要求:

  • 平时浏览 GitHub、开源、编程、程序员等英文资讯和文章
  • 想把自己阅读到优秀的英文文章分享给更多的人
  • 翻译准确但不是直翻或机翻
  • 保证每月至少翻译或校正 1 篇高质量文章
  • 了解 Markdown 和排版规则
  • 联系我

关注 HelloGitHub 头条号第一时间收到推送

993

IETF(互联网工程任务组)发布了第一个HTML草案,而且还是通过因特网草案的形式发布,所以当时并没有人鸟它,因此这个HTML1.0并没有被大众承认,但为了好排名,只好给个名分它。

互联网工程任务组

1995

IETF继续发布了HTML2.0,这也是首次被大众所认可,HTML2.0实际上是各个版本的超集,当时各大浏览器厂商有自己独特的标准,这些标准互不兼容,谁也不服谁,很明显给开发带来极大的困难。于是IETF干脆当了一次和事佬,将一些优秀的标准集合在一起,希望让大家一起遵守。虽然大佬级别的任然不鸟它,可却得到了二、三阶梯的浏览器厂商们支持并拥护。

1997

随着层出不穷的标准推出,标准统一化的工作很艰难。W3C终于出来主持大局,取代IETF,成为HTML的标准组织,发布了HTML3.2推荐标准。W3C靠着一系列的手段,让大佬们都得往自己的标准兼容,就连顽固不化的IE,也得要部分兼容。

W3C大佬

1999

在新千年前,W3C推出了HTML 4.01。这一年让HTML到达了它的第一个高潮。该推荐版本就是我们熟知的HTML的第四个标准版本,可以说是现代HTML5的基石。已经被大部分浏览器厂商采纳,开发人员只需要按照该标准进行开发即可。从1993-1999之间短短的6年时间,HTML语言有着很大的发展,基于诸多人的努力,终于产生了我们现在用的HTML语言。

h4使得html更加完善

2000

在新千年的开始,W3C决定搞事情。当时css3刚崛起,因为css一直是由W3C规划升级版本,语法规则比较单一标准;而HTML接手比较晚,导致HTML的语法规则很松散,能自动包容错误,不能称之为严格的规范。正好当时有一款语言XML,语法规则很严谨,并且可以实现同样的功能用于网页展示,所以W3C希望XML能够替代HTML,但是W3C担心严谨的语法规则会让浏览器厂商和程序编写人员一时间接受不了,所以就发布了一个过渡版本XHTML1.0,然后再向XML转变。

XML写法极其严格

2004

W3C试图用严格的标准来使互联网发展更加规范。出发点虽好,但效果不佳,因为改用XHTML标准将导致互联网99%的HTML网页需要重写。所以Opera、Mozilla基金会、苹果这些浏览器厂商则组建了民间组织WHATWG。WHATWG希望标准应当具备向后兼容性,能够跟随市场及技术的发展而动态调整,所有的改动改善都是HTML5,宽松的写法;而不希望是W3C制定一个死的标准,颁布之后再不修改,添加只能发布新版本 5.1,5.2等,严格的写法增加开发人员的负担;HTML由此分裂成两个阵营的版本。

两大阵营

2006

在W3C还在争论是XHTML2还是HTML5的时候,互联网格局已经发生了变化。人们不再满足看新闻和发邮件,更多的时候看流视频和网页游戏。此时此刻HTML正处于内战当中,这块新需求功能就被Adobe的Flash抢去了。

Adobe通过Flash不仅攫取了大量的财富,而且也狠狠打了W3C的一巴掌。于此同WHATWG在HTML上取得突破性进展。W3C为了尽快夺回Adobe抢占的市场,也为了自身的话语权,转而与WHATWG合作,一边推行以WHATWG的成果作为基础的HTML5,另一边则推行自家的XHTML2.0。

W3C同时推行两套标准

2008

第一个HTML5草案诞生。同年,IE、Chrome、FireFox、Safari几大浏览器巨头开始相继支持HTML5。

官方html5的logo

2009

W3C宣布终止XHTML2的项目,解散了XHTML团队。HTML 5既支持松散语法,也支持XHTML1般的严格语法。

2014

W3C发布了历时8年定稿的html5推荐标准 ,也就是html第五个标准版本。此后也发布了5.1、5.2版本。

W3C发布了html5.1、html5.2


1999~2014 15年的暗黑中世纪还是文艺复兴?

历经15年,html从统一到分裂,再到对峙,最后到统一,才再一次回到正轨。有人称这15年来,技术标准停滞不前,甚至还出现了倒退的嫌疑,等同于欧洲的黑暗中世纪;

取消原先的宽松写法

而也有人称这15年,是技术发展的黄金时期,新标准挑战旧权威的地位,迫使旧标准要么融合,要么被淘汰。而新标准挑战成功后,带来的连锁反应,则让更多的新标准来挑战旧权威,等同于欧洲的文艺复兴时期;

被废除的H4的元素


h5火爆的原因

HTML5迅速被引起火爆,一方面他成功抓住了移动端的热潮,游戏、购物等都离不开手机;另一方面,HTML5并不是单纯的修改语法,而是提供了全新的框架和平台,包括提供免插件的视频、图像动画、本体存储、跨平台以及更多酷炫而且重要的功能,并使这些应用标准化。

在2019年的今天,html的关注点早已不是兼容性,在现代浏览器下,追求更多的是高性能和高体验,如跨平台和vr等。兼容性问题大多数存在于使用率极低的旧时代的浏览器里。

VR只是其中的一个方向


一段话概括

W3C开发完html4以后,开发下一代html的时候,把亲儿子抛弃了,扶持干儿子xhtml。苹果公司、Mozilla基金会等不忍心,于是另起炉灶创立了民间组织WHATWG,继续抚养html。后来W3C的干儿子不争气,html则在WHATWG抚养下越发强大,W3C见状便和WHATWG合作(威逼),把培养好的亲儿子html接回来,当做下一代的html标准,因为亲爹(W3C)和干爹(WHATWG)对于html发展道路有不同的看法,这也导致了现存了两套html的标准,这也是导致兼容性问题出现的根本原因。

现存的两套标准


最后结论

纵观目前为止HTML标准的发展历程,可以说就是商业利益不断博弈的过程。它的诞生,既是为了满足人们的需求,也是为了达到洗牌利益重新分配的目的。它的强大,既是为了造福全球,也是为了击败竞争对手。就结果而言,巨头们出于各自的目的造出了强大的HTML5。它的崛起,意味着变革将至。谁能在这场变革中更好的利用它,谁便可以占得先机。

更多精彩内容可以移步到视频区,视频效果更佳哦ヾ(゚∀゚ゞ)

更多教学内容请持续关注我,或搜索关注

微信公众号:前端旺

:点击上方"蓝色字体"↑ 可以订阅噢!

摘要 51RGB官方微信

网页制作中规范使用DIV+CSS命名规则,可以改善优化功效特别是团队合作时候可以提供合作制作效率,具体DIV CSS命名规则CSS命名大全内容篇。

常用DIV+CSS命名大全集合,即CSS命名规则

我们开发CSS+DIV网页(Xhtml)时候,比较困惑和纠结的事就是CSS命名,特别是新手不知道什么地方该如何命名,怎样命名才是好的方法。

一、命名规则说明

1、所有的命名最好都小写

2、属性的值一定要用双引号("")括起来,且一定要有值如 class="helloweb" , id="helloweb"

3、每个标签都要有开始和结束,且要有正确的层次,排版有规律工整

4、空元素要有结束的tag或于开始的tag后加上"/"

5、表现与结构完全分离,代码中不涉及任何的表现元素,如:style、font、bgColor、border 等

6、<h1>到<h6>的定义,应遵循从大到小的原则,体现文档的结构,并有利于搜索引擎的查询。

7、给每一个表格和表单加上一个唯一的、结构标记 id

8、给图片加上alt 标签,优点是在于在图片发生错误时,alt 可以体现给用户

9、尽量使用英文命名原则

10、尽量不缩写,除非一看就明白的单词

下面给大家介绍常见CSS命名和DIV CSS命名方法。

二、相对网页外层重要部分CSS样式命名

外套 wrap ------------------用于最外层

头部 header ---------------用于头部

主要内容 main ------------用于主体内容(中部)

左侧 main-left -------------左侧布局

右侧 main-right -----------右侧布局

导航条 nav -----------------网页菜单导航条

内容 content --------------用于网页中部主体

底部 footer -----------------用于底部

三、DIV+CSS命名参考表

以下为CSS样式命名与CSS文件命名参考表,DIV CSS命名集合:

CSS样式命名说明
网页公共命名
#wrapper页面外围控制整体布局宽度
#container或#content容器,用于最外层
#layout布局
#head, #header页头部分
#foot, #footer页脚部分
#nav主导航
#subnav二级导航
#menu菜单
#submenu子菜单
#sideBar侧栏
#sidebar_a, #sidebar_b左边栏或右边栏
#main页面主体
#tag标签
#msg #message提示信息
#tips小技巧
#vote投票
#friendlink友情连接
#title标题
#summary摘要
#loginbar登录条
#searchInput搜索输入框
#hot热门热点
#search搜索
#search_output搜索输出和搜索结果相似
#searchBar搜索条
#search_results搜索结果
#copyright版权信息
#branding商标
#logo网站LOGO标志
#siteinfo网站信息
#siteinfoLegal法律声明
#siteinfoCredits信誉
#joinus加入我们
#partner合作伙伴
#service服务
#regsiter注册
arr/arrow箭头
#guild指南
#sitemap网站地图
#list列表
#homepage首页
#subpage二级页面子页面
#tool, #toolbar工具条
#drop下拉
#dorpmenu下拉菜单
#status状态
#scroll滚动
.tab标签页
.left .right .center居左、中、右
.news新闻
.download下载
.banner广告条
电子贸易相关
.products产品
.products_prices产品价格
.products_description产品描述
.products_review产品评论
.editor_review编辑评论
.news_release最新产品
.publisher生产商
.screenshot缩略图
.faqs常见问题
.keyword关键词
.blog博客
.forum论坛
CSS文件命名说明
master.css,style.css主要的
module.css模块
base.css基本共用
layout.css布局,版面
themes.css主题
columns.css专栏
font.css文字、字体
forms.css表单
mend.css补丁
print.css打印

CSS命名其它说明:

DIV+CSS命名小结:无论是使用“.”(小写句号)选择符号开头命名,还是使用“#”(井号)选择符号开头命名都无所谓,但我们最好遵循,主要的、重要的、特殊的、最外层的盒子用“#”(井号)选择符号开头命名,其它都用“.”(小写句号)选择符号开头命名,同时考虑命名的CSS选择器在HTML中重复使用调用。

通常我们最常用主要命名有:wrap(外套、最外层)、header(页眉、头部)、nav(导航条)、menu(菜单)、title(栏目标题、一般配合h1\h2\h3\h4标签使用)、content (内容区)、footer(页脚、底部)、logo(标志、可以配合h1标签使用)、banner(广告条,一般在顶部)、copyRight(版权)。其它可根据自己需要选择性使用。

建议:主要的、重要的、最外层的盒子用“#”(井号)选择符号开头命名,其它都用“.”(小写句号)选择符号开头命名。

2.CSS样式文件命名如下:

主要的 master.css

布局,版面 layout.css

专栏 columns.css

文字 font.css

打印样式 print.css

主题 themes.css

四、英文命名技巧

如果遇到不常用的,可以借助翻译工具进行翻译取其英文命名。

以上为DIV+CSS的命名规则总结,相信通过规范的CSS 命名会给你以后做网站网页的维护带来方便。

想认识志同道合的朋友一起学习web

加入我们的学习QQ群 190166743

丰富的学习资源,周一到周四免费直播公开课

长按图片,识别二维码即可入群

你可能感兴趣的精彩内容

微信:UI设计自学平台加关注

长按关注:《UI设计自学平台》

↓↓↓