分布式-springboot基础入门
B站播放地址:https://www.bilibili.com/video/BV1PE411i7CV?t=51
博客地址:https://www.cnblogs.com/hellokuangshen/p/12516870.html
狂神参考雷锋阳的视频讲的,建议视频还是看雷锋阳的
1、学习路线
2、什么是SpringBoot
SpringBoot
是一个Javaweb
的开发框架,和SpringMVC
类似,相比其他Javaweb
框架,SpringBoot
简化开发,约定大于配置,能迅速地开发web
应用,几行代码开发一个http
接口。
随着Spring
不断的发展,涉及的领域越来越多,项目整合开发需要配合各种各样的文件,使Spring
框架的使用变得没那么简单,人称“配置地狱”。SpringBoot
正是在这样的背景下被抽象出来的开发框架,目的是为了让大家更容易地使用Spring
、更容易地集成各种常用的中间件和开源框架。
SpringBoot
是基于Spring
开发,SpringBoot
本身并不提供Spring
框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于Spring
框架的应用程序。也就是说,SpringBoot
并不是用来替代Spring
的解决方案,而是和Spring
框架紧密结合用于提升Spring
开发者体验的工具。SpringBoot
****以约定大于配置的核心思想,默认帮我们进行了很多配置,多数SpringBoot
应用只需要很少的Spring
配置。同时,SpringBoot
集成了大量常用的第三方库配置(例如Redis
、MongoDB
、RabbitMQ
等),SpringBoot
应用中这些第三方库几乎可以零配置的开箱即用。
SpringBoot
的优点:
- 为所有
Spring
开发者更快的入门; - 开箱即用,即提供各种默认配置来简化项目配置;
- 内嵌式容器简化
web
项目; - 没有冗余代码生成和
XML
配置的要求。
3、微服务概念介绍
3.1 什么是微服务?
微服务是一种架构风格,它要求我们开发一个应用的时候,这个应用必须构建成一系列子服务的组合,可以通过http
或者RPC
的方式进行服务间的通信。
3.2 单体应用服务
所谓单体应用架构(all in one)是指,我们将一个应用中的所有应用服务都封装在一个应用中,比如把数据库访问、web访问等功能都放到一个war包里。
单体应用架构的优点是:易于开发和测试,也十分方便部署,当需要扩展是,只需将war包复制多份,然后放到多台服务器上,再做个负载均衡即可。
单体应用架构的缺点是:哪怕要修改一个十分微小的地方,都需要停掉整个服务,重新打包,部署这个应用的war包,各服务的部署升级等可能依赖其他服务,不够灵活。特别是对于一个大型应用,我们不可能把所有内容都放在一个应用中。
3.3 微服务架构
所谓微服务架构,就是打破之前单体应用服务的架构方式,把每个功能元素独立出来抽象成一个模块,不同的应用程序可能是不同的模块组合而成的,每个模块就称为一个微服务,微服务间可以独立地打包部署升级。
这样做的好处是:
- 节省了调用资源;
- 每个微服务都是一个可替换的,可独立升级的软件代码。
有关微服务的文章博客,可以参考:
- 原文地址:http://martinfowler.com/articles/microservices.html
- 翻译:https://www.cnblogs.com/liuning8023/p/4493156.html
4、第一个SpringBoot程序
- 可以在官网直接下载后,导入IDEA开发(https://start.spring.io/);
- 直接使用IDEA创建一个springboot项目(一般开发直接在IDEA中创建);
目录结构:
Main:
package com.jerry;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Springboot01HelloworldApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot01HelloworldApplication.class, args);
}
}
Controller:
package com.jerry.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/hello")
public class HelloController {
@GetMapping("hello")
@ResponseBody
public String hello()
{
return "byte dance";
}
}
使用方法:
打开浏览器,输入 http://localhost:8080/hello/hello,会返回一个网页,网页显示byte dance字符串。
5、SpringBoot自动装配原理
5.1 启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
启动器:就是SpringBoot的启动场景,有多种启动器,比如spring-boot-starter-web,就会帮我们自动导入web环境所有的依赖,springboot将每一个功能场景都生成一个对应的启动器,我们需要什么样的功能和场景,只需要引入对应的starter即可。
5.2 主程序
// 标注这个类是一个springboot应用
@SpringBootApplication
public class Springboot01HelloworldApplication {
public static void main(String[] args) {
// 将springboot应用启动
SpringApplication.run(Springboot01HelloworldApplication.class, args);
}
}
我们可以把 @SpringBootApplication
看作是 @Configuration
、@EnableAutoConfiguration
、@ComponentScan
注解的集合。
根据 SpringBoot 官网,这三个注解的作用分别是:
@EnableAutoConfiguration
:启用 SpringBoot 的自动配置机制;@ComponentScan
: 扫描被@Component
、@Service
、@Controller
注解修饰的 bean,注解默认会扫描该类所在的包下所有的类;@Configuration
:允许在 Spring 上下文中注册额外的 bean 或导入其他配置类。
5.3 自动装配原理
总结:springboot所有自动配置都是在启动的时候扫描并加载,spring.factories所有的自动配置都在这里面,但是不一定生效,要判断条件是否成立,只有导入了对应的starter,就有对应的启动器了,有了启动器,我们自动装配就会生效,配置就会成功。
TMD还没讲怎么用呢就开始讲底层是不是撒比???
6、主启动类如何运行(后续补充)
7、Yaml
7.1 Springboot配置文件
Springboot的配置文件一般位于resource目录下,配置文件的作用:修改SpringBoot自动配置的默认值,因为SpringBoot在底层都给我们配置好了。
Springboot使用一个全局的配置文件,配置文件名称是固定的
application.properties
语法结构:key=value
不推荐使用
application.yml
语法结构:key:空格 value
7.2 Yaml语法
7.2.1 Yaml概述
Yaml全称:“Yet Another Markup Language”,仍然是一种标记语言。
以前的配置文件,大多数使用xml来配置,比如一个简单的端口配置,对比一下xml和yaml配置:
<server>
<port>8080</port>
</server>
server:
port: 8080
7.2.2 yaml基本语法
- yaml大小写敏感
- 使用缩进表示层级关系
- 缩进不允许用tab,只允许用空格
- '#'表示注释,没有多行注释
yaml支持普通的k-v、数组/列表、映射(map)和自定义类的实例。
(1)普通的k-v
name: zhangjian
(2)数组/列表
意思是数组和列表都是一样的写法,有两种格式:不写成一行和写成一行。
不写成一行:
hobbies:
- code
- music
写成一行:
hobbies: [code, music]
注意:code和music逗号后面的空格可有可不有。
(3)映射(map)
数据结构就是Java里的map
结构,同样有两种格式:不写成一行和写成一行。
不写成一行:
scoreMap:
- Math: 100
- English: 100
写成一行:
scoreMap: {Math: 100, English: 100}
注意:key和value中间的:后面一定要有空格,不同的k-v对,即逗号后面的空格可有可不有。
(4)自定义类的实例
Bean如下:
public class Dog {
private String name;
private int age;
}
Bean对应的yml写法如下:
dog:
name: miumiu
age: 1
Student的成员属性有Dog的实例,yml写法如下:
student:
name: zhangjian
age: 17
isMarry: false
birthday: 1993/05/16
scoreMap: {Math: 100, English: 100}
hobbies: [code,music]
dog:
name: miumiu
age: 1
7.2.3 占位符
${}
person:
name: qinjiang${random.uuid} # 随机uuid
age: ${random.int} # 随机int
happy: false
birth: 2000/01/01
maps: {k1: v1,k2: v2}
lists:
- code
- girl
- music
dog:
name: ${person.hello:other}_旺财
age: 1
8、给属性赋值的几种方式
8.1 通过Yaml配置文件赋值
在application.yml
配置文件中将类的属性值配好,在要配置属性的类中,通过注解@ConfigurationProperties(prefix = "XXX")
将yml配置文件中的配置项和实体类相关联,@ConfigurationProperties作用:将配置文件中配置的每一个属性的值,映射到这个类中,告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定,举例如下:
Student类:
package com.jerry.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
@ConfigurationProperties(prefix = "student")
public class Student {
private String name;
private Integer age;
private Date birthday;
private Boolean isMarry;
private Map<String, Integer> scoreMap;
private List<String> hobbies;
private Dog dog;
}
注意Student类被注解@ConfigurationProperties(prefix = "student")
修饰。
Dog类:
package com.jerry.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
public class Dog {
private String name;
private int age;
}
yml配置文件:
student:
name: zhangjian
age: 17
isMarry: false
birthday: 1993/05/16
scoreMap: {Math: 100, English: 100}
hobbies: [code,music]
dog:
name: miumiu
age: 1
Student类被注解@ConfigurationProperties(prefix = "student")
修饰,其中prefix="student",这个student对应的就是yml配置文件里的student。
测试类:
package com.jerry;
import com.jerry.pojo.Student;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class Springboot01HelloworldApplicationTests {
@Autowired
private Student student;
@Test
void contextLoads() {
System.out.println(student);
}
}
注意@Autowired
自动将student属性注入到Springboot01HelloworldApplicationTests
类中。
8.2 @PropertySource加载指定的配置文件
不推荐
8.3 @Value配置(推荐,公司基本都用这个)
还需搭配@Autowired
和@Component
注解使用。
8.4 小结
- 如果我们在某个业务中,只需要获取配置文件中的某个值,可以使用一下 @value;
- 如果说,我们专门编写了一个JavaBean来和配置文件进行一一映射,就直接@configurationProperties,不要犹豫!
9、JSR303校验
见SpringBoot常用注解 6、参数校验部分:https://www.yuque.com/docs/share/5e853752-a1ba-4d24-ad6f-6c7ec5147dbc?#
10、多环境配置及配置文件位置
实际开发过程中可能有多种环境,比如研发环境、类生产环境、现网环境等,每一类环境对应的配置文件都不一样,比如不同的环境对接不同的数据源,因此就存在这种场景:需要灵活地切换不同的配置文件供Spring加载。
SpringBoot提供两种方式进行多环境配置:
- properties
- yml
10.1 properties多环境配置
我们在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml , 用来指定多个环境版本,例如:
- application-test.properties,代表测试环境配置
- application-dev.properties,代表开发环境配置
当上述配置文件同时存在于resource目录下时,它****默认使用application.properties主配置文件;
当我们需要切换成test环境或者dev环境的配置时,只需要在application.properties主配置文件中加入:
spring.profiles.active=测试级别(dev or test)
可通过在不同的配置文件中配置不同的端口号验证。
10.2 yml多环境配置
和properties配置文件中一样,但是使用yml去实现多环境配置不需要创建多个配置文件,更加方便了 !
server:
port: 8081
#选择要激活哪个环境块
spring:
profiles:
active: test
---
server:
port: 8083
spring:
profiles: dev #配置环境的名称
---
server:
port: 8084
spring:
profiles: myEnv #配置环境的名称
如果yml和properties同时对dev或者test环境编写了配置文件 , 默认会使用dev或者test环境的properties配置文件。
11、SpringBoot进行web开发
用SpringBoot进行web开发要解决的问题:
- 静态资源的导入
- 首页
- 模板引擎(Thymeleaf)
- 装配SpringMVC
- 增删改查
- 拦截器
- 国际化(实现中英文切换)
12、静态资源导入
12.1 web静态资源和动态资源
静态资源和动态资源的概念
- 静态资源:一般客户端发送请求到web服务器,web服务器从内存在取到相应的文件,返回给客户端,客户端解析并渲染显示出来。
- 动态资源:一般客户端请求的动态资源,先将请求交于web容器,web容器连接数据库,数据库处理数据之后,将内容交给web服务器,web服务器返回给客户端解析渲染处理。
静态资源和动态资源的区别
- 静态资源一般都是设计好的html页面,而动态资源依靠设计好的程序来实现按照需求的动态响应;
- 静态资源的交互性差,动态资源可以根据需求自由实现;
- 在服务器的运行状态不同,静态资源不需要与数据库参于程序处理,动态资源可能需要多个数据库的参与运算。
12.2 静态资源导入的几种方式
12.2.1 webjars + jQuery
没啥实际意义。
12.2.2 静态资源映射规则
将静态资源放在以下目录,可以被SpringBoot
识别到:
"classpath:/META-INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"
其中classpath为resource目录:
我们可以在resources根目录下新建对应的文件夹,都可以存放我们的静态文件,一般遵循以下规则:
- static存放静态资源;
- public存放公共的资源;
- resource存放上传的图片等资源。
优先级:resource > static > public。
12.3 自定义资源路径
我们也可以自己指定特定目录来存放静态资源,在application.properties中配置:
spring.resources.static-locations=classpath:/coding/,classpath:/kuang/
其中classpath为上面截图中的resource目录,/coding和/kuang是我们自定义存放静态资源的目录,一旦自己定义了静态文件目录的路径,原来的自动配置就都会失效了,不推荐自己再自定义一个资源路径。
13、首页定制
首页就是我们访问一个网站首先出现的页面,比如百度,我们将首页对应的index.html(文件名必须是这个,springboot源码写死的)文件放在上面讲的静态资源目录下,比如public或者static目录下,如下:
重新运行springboot,打开浏览器,如图:
我们可以精心编写首页对应的html文件,浏览器渲染index.html后,会为我们呈现绚丽的网站首页画面!
14、Thymeleaf模板引擎
14.1 什么是web的模板引擎
模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,是用于前端脚本简化字符串拼接的。模板引擎提供一个模板,后端传来的数据源(json或者字符串格式)通过这个模板引擎的处理生成一个html文本,浏览器再通过渲染这个html文本给用户呈现最终的网站页面,流程如下:
Thymeleaf
是SpringBoot
推荐的模板引擎,此外FreeMarker
也是使用较多的模板引擎。
SpringBoot
使用Thymeleaf
模板引擎,需要引入Thymeleaf
的maven
依赖:
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
此外,还需要在html
文本里引入Thymeleaf
的命名空间:
<html lang="en" xmlns:th="http://www.thymeleaf.org">
14.2 Thymeleaf语法
15、SpringMVC自动配置
X、分布式 Dubbo + Zookeeper + SpringBoot
X.1 分布式理论
分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统。分布式系统是由一组通过网络进行通信,为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器、处理更多的数据。
分布式系统是建立在网络之上的软件系统。
首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(比如加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为,分布式系统要解决的问题本事就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题...
可以参考Dubbo官网的背景介绍:http://dubbo.apache.org/zh-cn/docs/user/preface/background.html
以下摘自上面的连接(感觉很浓缩很高度,但是凭我现在的开发经验还不能领会...):
随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进。
单一应用架构
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。
垂直应用架构
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,提升效率的方法之一是将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。
分布式服务架构
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。
流动计算架构
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键。
Todo:上面四种模式需要网上查资料弄清楚,最好每种模式对应一个结构图,需要总结!
X.2 RPC框架
参考:https://www.yuque.com/zhangjian-mbxkb/uxqnxx/ag2a4f
X.2.1 什么是RPC?
RPC(Remote Procedure Call)是远程过程调用,是一种进程间通信的方式,RPC是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显示编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。
举个例子,两台服务器A、B,一个应用a部署在服务器A上,另一个应用b部署在服务器B上,应用a想要调用服务器B上的应用b提供的方法,由于应用a和应用b不在一个内存空间,需要通过网络来表达调用的语义和传达调用的数据。为什么要用RPC呢?就是因为无法在一个进程内,甚至一台计算内通过本地调用的方式完成的需求,比如不同系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地函数一样去调远程函数。
推荐阅读文章:https://www.jianshu.com/p/2accc2840a1b
X.2.2 RPC基本原理
Todo:补图!需要花时间至少知道个大概流程。
RPC两大核心模块:通讯、序列化。
X.2.3 http和RPC的区别和联系
既然有了http,为什么还需要RPC实现服务间的通信呢?
分布式-springboot基础入门的更多相关文章
- springBoot 基础入门
来处:是spring项目中的一个子项目 优点 (被称为搭建项目的脚手架) 减少一切xml配置,做到开箱即用,快速上手,专注于业务而非配置 从创建项目上: -- 快速创建独立运 ...
- SpringBoot基础入门
1.SpringBoot核心相关内容 1.1入口类 SpringBoot通常有一个入口类*Application,内部有一个main方法,是启动SpringBoot的入口.使用@SpringBootA ...
- SpringBoot基础篇-SpringBoot快速入门
SpringBoot基础 学习目标: 能够理解Spring的优缺点 能够理解SpringBoot的特点 能够理解SpringBoot的核心功能 能够搭建SpringBoot的环境 能够完成applic ...
- SpringBoot之基础入门-专题一
SpringBoot之基础入门-专题一 一.Spring介绍 1.1.SpringBoot简介 在初次学习Spring整合各个第三方框架构建项目的时候,往往会有一大堆的XML文件的配置,众多的dtd或 ...
- Spring Boot 2.x零基础入门到高级实战教程
一.零基础快速入门SpringBoot2.0 1.SpringBoot2.x课程全套介绍和高手系列知识点 简介:介绍SpringBoot2.x课程大纲章节 java基础,jdk环境,maven基础 2 ...
- [转]小D课堂 - 零基础入门SpringBoot2.X到实战_汇总
原文地址:https://www.cnblogs.com/wangjunwei/p/11392825.html 第1节零基础快速入门SpringBoot2.0 小D课堂 - 零基础入门SpringBo ...
- 小D课堂 - 零基础入门SpringBoot2.X到实战_汇总
第1节零基础快速入门SpringBoot2.0 小D课堂 - 零基础入门SpringBoot2.X到实战_第1节零基础快速入门SpringBoot2.0_1.SpringBoot2.x课程介绍和高手系 ...
- rabbitmq(一)-基础入门
原文地址:https://www.jianshu.com/p/e186a7fce8cc 在学东西之前,我们先有一个方法论,知道如何学习.学习一个东西一般都遵循以下几个环节: xxx是什么,诞生的原因, ...
- 学习SpringBoot,整合全网各种优秀资源,SpringBoot基础,中间件,优质项目,博客资源等,仅供个人学习SpringBoot使用
学习SpringBoot,整合全网各种优秀资源,SpringBoot基础,中间件,优质项目,博客资源等,仅供个人学习SpringBoot使用 一.SpringBoot系列教程 二.SpringBoot ...
随机推荐
- 翻译 | 30个 Python3 的最佳实践,技巧和窍门
1.使用 Python3 如果你关注 Python 的话,应该会知道 Python 2 已经于今年(2020 年)1 月 1 日正式弃用了.这份教程的很多例子都是只支持 Python 3 的,如果你还 ...
- HarmonyOS(LiteOs_m) 官方例程移植到STM32初体验
HarmonyOS(LiteOs_m) 官方例程移植到STM32初体验 硬件平台 基于正点原子战舰V3开发板 MCU:STM32F103ZET6 片上SRAM大小:64KBytes 片上FLASH大小 ...
- WEB安全讨论-表单登录是先验证验证码还是密码
表单登录是先验证验证码还是密码? 肯定是验证码呀!!!这是毋庸置疑的.但是发现有人会验证密码,感觉先验证密码和先验证验证码是一个概念是一样的.但是其实是完全不一样的.下面我们来一起详细的剖析一下: 消 ...
- 项目API接口鉴权流程总结
权益需求对接中,公司跟第三方公司合作,有时我们可能作为甲方,提供接口给对方,有时我们也作为乙方,调对方接口,这就需要API使用签名方法(Sign)对接口进行鉴权.每一次请求都需要在请求中包含签名信息, ...
- 敏捷史话(三):笃定前行的勇者——Ken Schwaber
很多人之所以平凡,并不在于能力的缺失,而是因为缺乏迈出一步的勇气.只有少部分的人可以带着勇气和坚持,走向不凡.Ken Schwaber 就是这样的人,他带着他的勇气和坚持在敏捷的道路上不断前行,以实现 ...
- LeetCode703 流中第k大的元素
前言: 我们已经介绍了二叉搜索树的相关特性,以及如何在二叉搜索树中实现一些基本操作,比如搜索.插入和删除.熟悉了这些基本概念之后,相信你已经能够成功运用它们来解决二叉搜索树问题. 二叉搜索树的有优点是 ...
- 【Shell】使用awk sed获取一行内容的两个值
突然有需求需要一个脚本,同时获取到每一行数据的两个值,下面做了一个例子模板,仅供记录参考 cat test.txt id=1,name=zclinux1 id=2,name=zclinux2 id= ...
- kubernets之secret资源
一 对于一些保密度比较高的文件,k8s又是如何存储的呢? 针对那些保密度比较高的配置文件,例如证书以及一些认证配置不能直接存储在configmap中,而是需要存储在另外一种资源中,需要对存储在里面的 ...
- PAT练习num4-D进制的A+B
输入两个非负 10 进制整数 A 和 B (≤),输出 A+B 的 D (1)进制数. 输入格式: 输入在一行中依次给出 3 个整数 A.B 和 D. 输出格式: 输出 A+B 的 D 进制数. 输入 ...
- CSS实现迷你键盘
最近做了一个迷你键盘的dome,这里分享给大家 dome下载地址(点击下载) 代码如下: <!DOCTYPE html> <html lang="en" > ...