2024年3月

本文深入解析Docker,一种革命性的容器化技术,从其基本概念、架构和组件,到安装、配置和基本命令操作。文章探讨了Docker在虚拟化、一致性环境搭建及微服务架构中的关键作用,以及其在云计算领域的深远影响,为读者提供了关于Docker技术全面且深入的洞见。

关注【TechLeadCloud】,分享互联网架构、云服务技术的全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人。

file

一、Docker简介

file
Docker是一种开源容器化技术,它允许开发者将应用及其依赖打包到一个轻量级、可移植的容器中。这种方法确保了应用在不同环境中的一致性和效率。Docker的出现标志着云计算和微服务架构的一个重要转折点。

Docker的起源和发展

Docker最初是由Solomon Hykes在DotCloud公司(后改名为Docker Inc.)开发的一个内部项目。自2013年首次公开发布以来,Docker迅速成为容器化技术的代名词,受到了广泛的关注和采用。

Docker的工作原理

Docker通过使用Linux内核的特性(如cgroups和namespace)来隔离应用的运行环境。这不仅使得容器运行高效,而且还提高了安全性。Docker容器与虚拟机相比,由于不需要完整的操作系统,因此更加轻量和快速。

Docker的核心组件

  • Docker Engine
    :负责创建和管理容器。
  • Docker Images
    :包含应用及其运行环境的蓝图。
  • Docker Containers
    :运行中的镜像实例。
  • Docker Hub
    :一个共享和存储容器镜像的公共服务。

Docker的优势

  • 一致性
    :在任何支持Docker的环境中以相同方式运行应用。
  • 便携性
    :容易迁移和扩展。
  • 隔离性
    :提高安全性和稳定性。
  • 资源高效
    :与传统虚拟机相比,更少的性能开销。

Docker的应用场景

  • 微服务架构
    :Docker非常适合微服务架构,每个服务可以独立容器化。
  • 持续集成/持续部署(CI/CD)
    :Docker简化了构建、测试和部署流程。
  • 开发和测试
    :提供一致的开发、测试环境。
  • 云原生应用
    :Docker是构建和部署云原生应用的基础。

Docker与虚拟化技术的比较

虽然Docker和传统的虚拟化技术(如VMware、Hyper-V)在某些方面有相似之处,但它们在性能、资源利用率和速度方面有显著的区别。Docker通过共享主机的内核,减少了资源占用,提高了启动速度。


二、Docker架构和组件全解

file
Docker的架构和组件是理解其工作原理和应用的关键。这部分将深入探讨Docker的核心组件、架构设计,以及它们如何共同工作来提供一个高效、灵活的容器化平台。

Docker的总体架构

Docker采用客户端-服务器(C/S)架构。这种架构包括一个服务器端的Docker守护进程(Docker Daemon)和一个客户端命令行接口(CLI)。守护进程负责创建、运行和管理容器,而CLI则允许用户与Docker守护进程交互。

Docker Daemon(守护进程)

  • 运行在宿主机上。
  • 负责处理Docker API请求,并管理Docker对象,如镜像、容器、网络和卷。

Docker Client(客户端)

  • 用户通过Docker客户端与Docker守护进程交互。
  • 发送命令到Docker Daemon,如
    docker run

    docker build
    等。

Docker Registry(注册中心)

  • 用于存储Docker镜像。
  • Docker Hub是最常用的公共注册中心,但用户也可以搭建私有注册中心。

Docker Images(镜像)

Docker镜像是一个轻量级、可执行的包,包含运行应用所需的一切:代码、运行时、库、环境变量和配置文件。

镜像构成

  • 由多层只读文件系统堆叠而成。
  • 每层代表Dockerfile中的一个指令。
  • 利用联合文件系统(UnionFS)技术来优化存储和提高效率。

镜像版本管理和层缓存

  • 支持标签(Tagging),用于版本控制。
  • 层缓存用于加速构建和部署过程。

Docker Containers(容器)

容器是Docker镜像的运行实例。它在镜像的顶层添加一个可写层,并通过Docker守护进程在用户空间中运行。

容器与虚拟机的区别

  • 容器直接在宿主机的内核上运行,不需要完整的操作系统。
  • 资源占用少,启动速度快。

容器的生命周期管理

  • 创建、启动、停止、移动和删除。
  • 可以通过Docker CLI或API进行管理。

Docker Networks(网络)

Docker网络提供了容器之间以及容器与外部世界之间的通信机制。

网络类型

  • Bridge:默认网络,适用于同一宿主机上的容器通信。
  • Host:移除网络隔离,容器直接使用宿主机的网络。
  • Overlay:用于不同宿主机上的容器间通信。

网络配置

  • 支持端口映射和容器连接。
  • 提供DNS服务,容器可以通过名称互相发现和通信。

Docker Volumes(卷)

Docker卷是一种持久化和共享容器数据的机制。

卷的类型

  • 持久化卷:数据存储在宿主机上,即使容器删除,数据仍然保留。
  • 共享卷:允许不同容器共享数据。

数据管理

  • 可以在运行时动态挂载。
  • 支持数据备份、迁移和恢复。


三、Docker安装与配置

file
Docker的安装和配置是开始使用Docker的第一步。本节将覆盖Docker在主流服务器操作系统上的安装步骤和基本配置方法。

Docker在Linux上的安装

Ubuntu系统

  1. 更新软件包索引:
    sudo apt-get update
  2. 安装必要的包以允许
    apt
    通过HTTPS使用仓库:
    sudo apt-get install apt-transport-https ca-certificates curl software-properties-common
  3. 添加Docker官方GPG密钥:
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
  4. 添加Docker仓库:
    sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
  5. 再次更新软件包索引:
    sudo apt-get update
  6. 安装Docker CE(社区版):
    sudo apt-get install docker-ce

CentOS系统

  1. 安装必要的包:
    sudo yum install -y yum-utils
  2. 添加Docker仓库:
    sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
  3. 安装Docker CE:
    sudo yum install docker-ce
  4. 启动Docker守护进程:
    sudo systemctl start docker

Docker在Windows Server上的安装

对于Windows Server,可以使用Docker EE(企业版)。

  1. 启用容器功能:在服务器管理器中,添加“容器”角色。
  2. 安装Docker:运行PowerShell脚本来安装Docker EE。
    Install-Module DockerProvider
    Install-Package Docker -ProviderName DockerProvider -RequiredVersion preview
    
  3. 启动Docker服务:
    Start-Service Docker

Docker在macOS上的安装

Docker Desktop for Mac是在macOS上运行Docker的最佳选择。

  1. 下载Docker Desktop for Mac安装程序。
  2. 双击下载的
    .dmg
    文件,然后拖动Docker图标到应用程序文件夹。
  3. 打开Docker应用程序,完成安装。

Docker基本配置

用户组配置

  • 将用户添加到
    docker
    组,以避免每次使用
    docker
    命令时都需要
    sudo

    sudo usermod -aG docker your-username
    

配置Docker启动项

  • 在Linux上,设置Docker随系统启动:

    sudo systemctl enable docker
    

    配置Docker镜像加速

  • 对于某些地区,可能需要配置镜像加速器以提高拉取速度:

    sudo mkdir -p /etc/docker
    sudo tee /etc/docker/daemon.json <<-'EOF'
    {
      "registry-mirrors": ["https://your-mirror-url"]
    }
    EOF
    sudo systemctl daemon-reload
    sudo systemctl restart docker
    


四、Docker基本命令

file
Docker的基本命令是操作和管理Docker容器和镜像的基石。为了便于理解和参考,以下以表格形式列出了Docker的主要命令及其功能描述。

命令 功能描述
docker run 创建并启动一个新容器
docker start 启动一个或多个已停止的容器
docker stop 停止一个运行中的容器
docker restart 重启容器
docker rm 删除一个或多个容器
docker rmi 删除一个或多个镜像
docker ps 列出容器
docker images 列出镜像
docker pull 从镜像仓库拉取或更新指定镜像
docker push 将镜像推送到镜像仓库
docker build 通过Dockerfile构建镜像
docker exec 在运行的容器中执行命令
docker logs 获取容器的日志
docker inspect 获取容器/镜像的详细信息
docker network create 创建一个新的网络
docker volume create 创建一个新的卷
docker attach 连接到正在运行的容器
docker cp 从容器中复制文件/目录到宿主机,反之亦然
docker diff 检查容器文件系统的更改
docker commit 从容器创建新的镜像
docker login 登录到Docker镜像仓库
docker logout 从Docker镜像仓库登出
docker search 在Docker Hub中搜索镜像
docker save 将一个或多个镜像保存到文件
docker load 从文件加载镜像
docker tag 为镜像创建一个新的标签
docker port 列出容器的端口映射或指定容器的特定映射
docker top 显示一个容器中运行的进程


五、总结

通过对Docker的深入探讨,我们可以看到Docker作为一种现代化的容器化技术,在技术领域的影响是多方面的。从Docker的简介到其架构和组件的全面解析,再到实际的安装、配置和基本命令操作,我们了解了Docker如何将复杂的应用容器化过程变得简单高效。

Docker的技术革新

  1. 轻量级虚拟化
    :Docker采用的容器技术,与传统的虚拟机相比,极大地减少了资源消耗,提高了启动速度和性能,这对于资源密集型的应用来说是一个重大突破。

  2. 一致性环境
    :Docker通过容器来保证应用在不同环境中的一致性,解决了“在我的机器上可以运行”的常见问题,这在持续集成和持续部署(CI/CD)中尤为重要。

  3. 微服务架构的推动者
    :Docker的出现和普及推动了微服务架构的发展。它使得开发者可以将应用分解为更小、更易管理的部分,从而提高了系统的可维护性和可扩展性。

Docker在云计算领域的影响

  1. 云原生应用的基石
    :Docker是构建云原生应用的关键。它不仅支持应用的快速部署和扩展,还通过其生态系统(如Kubernetes)支持高级的容器编排。

  2. 资源优化
    :在云环境中,资源的有效利用是核心考虑。Docker通过减少额外的操作系统开销,使得在相同的物理资源上可以运行更多的应用实例。

  3. 多云和混合云策略的加速器
    :Docker的可移植性使得它成为实现多云和混合云策略的理想选择。企业可以轻松地将应用迁移至不同的云服务提供商,或在私有云和公有云之间无缝迁移。

未来展望

Docker已经成为现代软件开发和运维的一个不可或缺的部分,但技术永远在发展。未来,我们可以预见到容器技术将进一步整合更多的安全特性,提供更加智能的资源管理,以及更紧密地与新兴的云原生技术和服务集成,如函数即服务(FaaS)、无服务器计算等。

综上所述,Docker不仅仅是一个技术工具,它代表了一种关于如何构建、部署和管理应用的新思维方式,这对于任何涉足云计算、云原生和软件工程领域的专业人士而言,都是至关重要的。

关注【TechLeadCloud】,分享互联网架构、云服务技术的全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人。
如有帮助,请多关注
TeahLead KrisChang,10+年的互联网和人工智能从业经验,10年+技术和业务团队管理经验,同济软件工程本科,复旦工程管理硕士,阿里云认证云服务资深架构师,上亿营收AI产品业务负责人。

读取文件内容,然后进行处理,在Java中我们通常利用 Files 类中的方法,将可以文件内容加载到内存,并流顺利地进行处理。但是,在一些场景下,我们需要处理的文件可能比我们机器所拥有的内存要大。此时,我们则需要采用另一种策略:部分读取它,并具有其他结构来仅编译所需的数据。

接下来,我们就来说说这一场景:当遇到大文件,无法一次载入内存时候要如何处理。

模拟场景

假设,当前我们需要开发一个程序来分析来自服务器的日志文件,并生成一份报告,列出前 10 个最常用的应用程序。

每天,都会生成一个新的日志文件,其中包含时间戳、主机信息、持续时间、服务调用等信息,以及可能与我们的特定方案无关的其他数据。

2024-02-25T00:00:00.000+GMT host7 492 products 0.0.3 PUT 73.182.150.152 eff0fac5-b997-40a3-87d8-02ff2f397b44
2024-02-25T00:00:00.016+GMT host6 123 logout 2.0.3 GET 34.235.76.94 8b97acae-dd36-4e83-b423-12905a4ab38d
2024-02-25T00:00:00.033+GMT host6 50 payments/:id 0.4.6 PUT 148.241.146.59 ac3c9064-4782-46d9-a0b6-69e4d55a5b38
2024-02-25T00:00:00.050+GMT host2 547 orders 1.5.0 PUT 6.232.116.248 2285a81e-c511-41b9-b0ea-a475a0a45805
2024-02-25T00:00:00.067+GMT host4 400 suggestions 0.8.6 DELETE 149.138.227.154 8031b639-700e-4a7c-b257-fcbed0d029ce
2024-02-25T00:00:00.084+GMT host2 644 login 6.90 GET 208.158.145.204 3906a28c-56e4-4e5f-b548-591eab737aa7
2024-02-25T00:00:00.101+GMT host5 339 suggestions 0.8.9 PUT 173.109.21.97 c7dfec8a-5ca8-4d0d-b903-aaf65629fdd0
2024-02-25T00:00:00.118+GMT host9 87 products 2.6.3 POST 220.252.90.140 e5ceef67-2f0f-4c2d-a6d2-c698598aaef2
2024-02-25T00:00:00.134+GMT host0 845 products 9.4.6 GET 136.79.178.188 f28578c1-c37c-47a3-a473-4e65371e0245
2024-02-25T00:00:00.151+GMT host4 675 login 0.89 DELETE 32.159.65.239 d27ff353-e501-43e6-bdce-680d79a07c36

我们的代码将收到日志文件列表,我们的目标是编制一份报告,列出最常用的 10 个服务。但是,要包含在报告中,服务必须在提供的每个日志文件中至少有一个条目。简而言之,一项服务必须每天使用才有资格包含在报告中。

基础实现

解决这个问题的最初方法是考虑业务需求并创建以下代码:

public void processFiles(final List<File> fileList) {
  final Map<LocalDate, List<LogLine>> fileContent = getFileContent(fileList);
  final List<String> serviceList = getServiceList(fileContent);
  final List<Statistics> statisticsList = getStatistics(fileContent, serviceList);
  final List<Statistics> topCalls = getTop10(statisticsList);

  print(topCalls);
}

该方法接收文件列表作为参数,核心流程如下:

  • 创建一个包含每个文件条目的映射,其中Key是 LocalDate,Value是文件行列表。
  • 使用所有文件中的唯一服务名称创建字符串列表。
  • 生成所有服务的统计信息列表,将文件中的数据组织到结构化地图中。
  • 筛选统计信息,获取排名前 10 的服务调用。
  • 打印结果。

可以注意到,这种方法将太多数据加载到内存中,不可避免地会导致
OutOfMemoryError

改进实现

就如文章开头说的,我们需要采用另一种策略:逐行处理文件的模式。

private void processFiles(final List<File> fileList) {
  final Map<String, Counter> compiledMap = new HashMap<>();

  for (int i = 0; i < fileList.size(); i++) {
    processFile(fileList, compiledMap, i);
  }

  final List<Counter> topCalls =
      compiledMap.values().stream()
          .filter(Counter::allDaysSet)
          .sorted(Comparator.comparing(Counter::getNumberOfCalls).reversed())
          .limit(10)
          .toList();

  print(topCalls);
}
  • 首先,它声明一个Map(compiledMap),其中一个String作为键,代表服务名称,以及一个Counter对象(稍后解释),它将存储统计信息。
  • 接下来,它逐一处理这些文件并相应地更新compileMap。
  • 然后,它利用流功能来: 仅过滤具有全天数据的计数器;按调用次数排序;最后,检索前 10 名。

在看整个处理的核心
processFile
方法之前,我们先来分析一下
Counter
类,它在这个过程中也起到了至关重要的作用:

public class Counter {
  @Getter private String serviceName;
  @Getter private long numberOfCalls;
  private final BitSet daysWithCalls;

  public Counter(final String serviceName, final int numberOfDays) {
    this.serviceName = serviceName;
    this.numberOfCalls = 0L;
    daysWithCalls = new BitSet(numberOfDays);
  }

  public void add() {
    numberOfCalls++;
  }

  public void setDay(final int dayNumber) {
    daysWithCalls.set(dayNumber);
  }

  public boolean allDaysSet() {
    return daysWithCalls.stream()
        .mapToObj(index -> daysWithCalls.get(index))
        .reduce(Boolean.TRUE, Boolean::logicalAnd);
  }
}
  • 它包含三个属性:serviceName、numberOfCalls 和 daysWithCalls
  • numberOfCalls 属性通过 add 方法递增,该方法为 serviceName 的每个处理行调用。
  • daysWithCalls 属性是一个 Java BitSet,一种用于存储布尔属性的内存高效结构。它使用要处理的天数进行初始化,每个位代表一天,初始化为 false。
  • setDay 方法将 BitSet 中与给定日期位置相对应的位设置为 true。

allDaysSet 方法负责检查 BitSet 中的所有日期是否都设置为 true。它通过将 BitSet 转换为布尔流,然后使用逻辑 AND 运算符减少它来实现此目的。

private void processFile(final List<File> fileList, 
                         final Map<String, Counter> compiledMap, 
                         final int dayNumber) {
  try (Stream<String> lineStream = Files.lines(fileList.get(dayNumber).toPath())) {
    lineStream
        .map(this::toLogLine)
        .forEach(
            logLine -> {
              Counter counter = compiledMap.get(logLine.serviceName());
              if (counter == null) {
                counter = new Counter(logLine.serviceName(), fileList.size());
                compiledMap.put(logLine.serviceName(), counter);
              }
              counter.add();
              counter.setDay(dayNumber);
            });

  } catch (final IOException e) {
    throw new RuntimeException(e);
  }
}
  • 该过程使用Files类的lines方法逐行读取文件,并将其转换为流。这里的关键特征是lines方法是惰性的,这意味着它不会立即读取整个文件;相反,它会在流被消耗时读取文件。
  • toLogLine 方法将每个字符串文件行转换为具有用于访问日志行信息的属性的对象。
  • 处理文件行的主要过程比预期的要简单。它从与serviceName关联的compileMap中检索(或创建)Counter,然后调用Counter的add和setDay方法。

正如我们所看到的,在 Java 中处理大文件而不将整个文件加载到内存中并不是什么复杂的事情。 Files类提供了逐行处理文件的方法,我们还可以在文件处理过程中利用哈希来存储数据,这有助于节省内存。

欢迎关注我的公众号:程序猿DD。第一时间了解前沿行业消息、分享深度技术干货、获取优质学习资源

本文介绍在
Linux
操作系统
Ubuntu
版本中,通过命令行的方式,配置
QGIS
软件的方法。


Ubuntu

Linux
系统中,可以对空间信息加以可视化的
遥感

GIS
软件很少,比如
ArcGIS
下属的
ArcMap
就没有对应的
Linux
版本(虽然有
ArcGIS Server
,但是其没有办法对空间数据加以可视化)。但是,对于
Ubuntu
等桌面系统,我们还是可以使用开源的
QGIS
软件来加以可视化的
GIS
操作的。本文就介绍在
Ubuntu
操作系统中,配置
QGIS
软件的方法。

我们就基于
QGIS
官方给出的命令行配置方法,对其配置加以介绍。此外,关于软件与系统版本的兼容等更进一步的配置信息,大家如果有需要,参考其
官方网站
即可。

首先,我们执行如下的代码,来配置一下
QGIS
安装所需要依赖的资源。其中,
gnupg
是GNU Privacy Guard(
GnuPG
)的一个组件,用于加密和签名数据;
software-properties-common
是一个包含了常用软件源管理工具的软件包,它提供了向系统添加、删除和管理软件源的能力。

sudo apt install gnupg software-properties-common

执行上述代码,如下图所示。

接下来,首先执行如下的代码。这个命令的含义是使用超级用户权限创建一个名为
/etc/apt/keyrings
的目录,并设置该目录的权限为
755
。如果
/etc/apt
目录不存在,命令将自动创建它。

sudo mkdir -m755 -p /etc/apt/keyrings

随后,再执行如下代码。这个命令将从后面那个网站中,下载、安装
QGIS
的签名密钥,安装的位置就是上一句代码指定的文件夹。

sudo wget -O /etc/apt/keyrings/qgis-archive-keyring.gpg https://download.qgis.org/downloads/qgis-archive-keyring.gpg

执行上述代码,如下图所示。

接下来,我们先输入如下的一句代码。这句代码的作用是,查看我们当前操作系统的
codename
(说白了相当于就是操作系统的版本)。

lsb_release -cs

执行上述代码,如下图所示。可以看到,此时显示的,就是我们当前操作系统的
codename

接下来,我们首先配置好如下一个文本内容;其中,第三行引号
:
后面的内容,就是上一句代码执行后我们所获得的操作系统的
codename
;大家这里依据自己的实际情况修改即可。

Types: deb deb-src
URIs: https://qgis.org/debian
Suites: bionic
Architectures: amd64
Components: main
Signed-By: /etc/apt/keyrings/qgis-archive-keyring.gpg

随后,我们需要将上述文本内容,复制到文件
/etc/apt/sources.list.d/qgis.sources
当中去。这里我也记不清楚这个
qgis.sources
文件当时是原本就生成了,还是需要自己创建一个——所以大家就结合实际情况,如果有这个文件,那么直接对文件加以修改;如果没有这个文件,那么可以先用
torch
命令新建一个,然后再修改。关于修改的方式,我这里选择了用
Vim
来修改,所以就通过如下的代码进入文件并修改。

sudo vim /etc/apt/sources.list.d/qgis.sources

执行上述代码,并修改文件,随后如下图所示。

接下来,我们执行如下的代码,更新一下软件库。

sudo apt update

随后,执行如下的代码,即可开始安装
QGIS
了。

sudo apt install qgis qgis-plugin-grass qgis-server

执行上述代码,如下图所示。

等待上述安装进度完成后,我们就结束了安装流程。此时正常情况下,大家就可以打开
QGIS
软件了;但是我这里因为电脑原本就有一个
QGIS
,不知道是不是冲突了,所以每次点击安装后的
QGIS
图标一直没有反应,即使卸载了原有的版本也不行。但只要没有这个问题的话,应该就可以正常打开软件了。

至此,大功告成。

在IPD(集成产品开发)体系中,PDT(Product Development Team,产品开发团队)发挥着至关重要的作用。PDT是一个跨部门、跨职能的协作团队,其成员来自不同的专业领域,包括研发、市场、销售、供应链等。从概念阶段到发布阶段,PDT都以跨部门的形式紧密协作,共同推进产品的研发和商业化过程。

IPD中,PDT团队的结构

在PDT(产品开发团队)的组建与运行过程中,许多公司常常面临核心组各部门代表难以协同合作、项目目标无法有效对齐等挑战,这些问题极大地削弱了PDT的运行效果。如果PDT不能有效运行,公司可能会对其所依赖的IPD(集成产品开发)体系产生怀疑,最终可能选择退回到旧的项目组织形式。

因此,PDT在组建和运行过程中要注意以下几点:

一、PDT定期举行项目例会

PDT的成员来自市场、开发、制造、采购、财务、客户服务、质量等不同功能部门,他们共同协作,以确保产品能够满足市场需求并快速盈利。为了实现这一目标,PDT定期举行项目例会是不可或缺的。

  • 会前

各个部门的汇报人需要提前做好准备工作,要明确标出关键决策点,帮助与会者快速把握重点。参会人员也应提前阅读会议材料,了解会议的目的和讨论的重点。

  • 会中

议题汇报人需要按照规定的顺序进行议题汇报,并且采用举手投票表决的方式,对议题进行“不通过-有条件通过-通过”的顺序表决。这样的流程确保了决策的透明性和公正性,同时也促进了组织内部的沟通和协作。

  • 会后

会议结束后,需要对会上重要结论形成会议记录。根据会议决策,进行安排工作。

二、一个PDT只有一个PDT经理

在PDT团队中,PDT经理的作用尤为重要,他们不仅是团队的核心领导者,还是跨部门协作的桥梁。特别是像华为的技术巨头中,PDT经理甚至被赋予“小CEO”的称号。

然而,许多公司倾向于从开发部门或市场规划部门提拔管理干部来担任PDT经理。这很容易导致PDT经理在实际运作中过于关注研发工作,而忽视了团队管理的重要性。除此之外,一个PDT经理往往要负责多个PDT的情况并不罕见。这种“身兼数职”的情况不仅分散了PDT经理的精力,还可能影响团队的整体效率。

因此,我们更加提倡每个PDT经理仅全职负责一个PDT。这确保了PDT经理能够全力以赴推进项目,避免了因分散精力而导致的效率低下。同时,还鼓励将相似的项目整合成一个PDT项目,以提高资源利用率。这种做法不仅优化了资源配置,还有助于形成规模效应,提升整体竞争力。

三、有能力的人担任PDT核心代表

PDT中的核心代表是团队的中坚力量,他们负责在项目实施过程中进行任务管理和绩效管理,确保项目能够按照既定的目标和计划推进。这就要求核心代表不仅要具备扎实的专业知识和技能,还要有良好的沟通和协调能力,能够妥善处理各种突发状况,确保PDT的高效运作。

但在实际操作过程中,一些核心代表可能会出现频繁向上级请示来解决问题的情况,这在一定程度上削弱了PDT的自主性和创新性,这就需要企业更加重视人才培养和资源池的建设。

通过制定系统的培训计划,企业可以为核心代表提供持续的学习和发展机会,帮助他们不断提升专业素养和综合能力。同时,通过建立资源池,企业可以实现对核心代表能力的有效整合和优化配置,确保项目在实施过程中能够得到充分的支持和保障。

值得注意的是,企业在建设资源池和规划人才培养计划时,要避免“彼得原理(指某些人员晋升到其无法胜任的岗位后,反而成为组织发展的阻碍)”情况的发生。

四、PDT成立时发布正式任命

在推行IPD(集成产品开发)体系过程中,许多企业往往只关注形式,忽视了赋予PDT经理足够权力的重要性。这种权力缺失不仅阻碍了PDT的有效运作,还可能导致资源浪费、效率低下甚至项目失败。

没有实实在在的赋权,PDT经理难以在项目中发挥领导作用。PDT经理是项目的核心,他们需要拥有决策权、资源分配权以及跨部门协调能力,以确保项目的顺利进行。然而,如果企业没有明确规定部门和PDT经理对各部门代表的管理权限,那么PDT经理在项目执行过程中就可能受到各种阻碍,无法及时作出决策或调动资源。

因此,为了确保PDT的有效运作,企业在成立PDT时需要发布正式的《项目成员任命书》,明确PDT团队的各个成员及其职能角色,让各个成员在履职时就能“有法可依”。

仪式感、责任感……这些在组建PDT团队时,能让团队成员更有团队归属感,从而提高自身积极性。通过建立PDT,企业可以优化资源配置、提高工作效率、激发创新思维,实现共赢和共同成长。相信在未来的发展中,PDT将成为越来越多企业的首选组织形式,推动企业在激烈的市场竞争中取得优势地位。

前端 Typescript 入门

Ant design vue4.x 基于 vue3,示例默认是 TypeScript。比如
table
组件管理。

vue3 官网介绍也使用了 TypeScript,例如:
响应式 API:核心

华为的鸿蒙OS(HarmonyOS)开发中也可以使用 TypeScript

本篇目的用于对 TS 进行扫盲

Tip

ts 路线图

ts 是什么

TS是TypeScript的缩写,由微软开发的一种开源的编程语言

以前官网说“ts 是 js 超级”,现在改为:
TypeScript是具有类型语法的JavaScript。

目前 TypeScript 5.4 已经发布(2024-03) ——
ts 官网

Tip
:ts缺点:开发更费麻烦,要多写东西了,看个人取舍。

环境

基于笔者博文《vue3 入门》,就像这样:

<template>
  <section>
  </section>
</template>

<script  lang="ts" setup name="App">
// ts
</script>

<style>
</style>

也可以直接在
ts在线运行环境
进行。

推导类型和显示注解类型

TS =
类型
+ javascript

ts 编译过程:

  • TypeScript源码 -> TypeScript AST
  • 类型检查器
    检查AST
  • TypeScript AST -> JavaScript 源码

显示注解类型,语法:
value:type
告诉类型检查器,这个 value 类型是 type。请看示例:

<template>
  <p>{{ a }}</p>
  <p>{{ b }}</p>
  <p>{{ c }}</p>
</template>

<script  lang="ts" setup name="App">
// 显示注解类型
let a: number = 1 // a 是数字
let b: string = 'hello' // b 是字符串
let c: boolean[] = [true, false]; // 布尔类型数组
</script>

如果将 a 写成
let a: number = '3'
,vscode 中 a 就会出现红色波浪,移上去会看到提示:
不能将类型“string”分配给类型“number”。

如果想让 typescript 推到类型,就去掉注解,让 ts 自动推导。就像这样:

// 推导类型
let a = 1 // a 是数字
let b = 'hello' // b 是字符串
let c = [true, false]; // 布尔类型数组

去掉注解后,类型并没有变。并且如果尝试修改 a 的类型,ts 也会报错。就像这样:

let a = 1 // a 是数字

// 尝试替换成字符串,vscode 会提示:不能将类型“boolean”分配给类型“number”。
a = true

Tip
:有人说“最好让 ts 推导类型,少数情况才需要显示注解类型”。

另外虽然大量错误 ts 在编译时无法捕获,例如堆栈溢出、网络断连,这些属于运行时异常。ts 能做的是将 js 运行时的报错提到编译时。比如以下代码:

const obj = { width: 10, height: 15 };
// 提示 heigth 属性写错了
const area = obj.width * obj.heigth;

let a = 1 + 2
let b = a + 3
// 鼠标以上 c,可以看到 c 对应的类型
let c = {
  apple: a,
  banana: b
}

类型断言

请看示例:

let arr = [1, 2, 3]
// r 为3
const r = arr.find(item => item > 2)

// “r”可能为“未定义”。ts(18048)
// const r: number | undefined
r * 5 // {1}

行 {1} 处的 r 会错误提示,说 r 可能会是 undefined。

要解决这个问题,可以使用
类型断言
:用来告诉编译器一个值的具体类型,当开发者比编译器更了解某个值的具体类型时,可以使用类型断言来告诉编译器应该将该值视为特定的类型。

类型断言有两种形式,分别是
尖括号语法

as 语法
。这里说一下 as 语法:
value as type
。请看示例:

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

上述示例改成这样,r 就不会报错了。

// 告诉编译器,r 一定是一个 number
const r = arr.find(item => item > 2) as number

r * 5

Tip
:由于需要人为干预,所以使用起来要谨慎

基础类型

js 基本类型大概有这些:

let num1 = 10;

let str1 = 'Hello';

let isTrue = true;

let undefinedVar;

let nullVar = null;

const symbol1 = Symbol('description');

const bigIntNum = 9007199254740991n;

let notANumber = NaN;
let infinite = Infinity;

console.log(typeof num1); // number
console.log(typeof str1,); // string
console.log(typeof isTrue); // boolean
console.log(typeof undefinedVar); // undefined
console.log(typeof nullVar); // object
console.log(typeof symbol1); // symbol
console.log(typeof bigIntNum); // bigint
console.log(typeof notANumber); // number
console.log(typeof infinite); // number

ts 中基本类型有:

// let v1: String = 'a' - 大写 String 也可以
let v1: string = 'a'
let v2: number = 1
let v3: boolean = true
let v4: null = null
let v5: undefined = undefined

// 字符串或者null
let v6: string | null = null
// 错误:不能将类型“5”分配给类型“1 | 2 | 3”
let v7: 1 | 2 | 3 = 5
// 正确
let v8: 1 | 2 | 3 = 2

联合类型

数组

ts 数组有两种方法,看个人喜好即可。请看示例:


// 方式一
// 定义一个由数字组成的数组
let arr1: number[] = [2, 3, 4]

// 报错:不能将类型“string”分配给类型“number”
let arr2: number[] = [2, 3, 4, '']

// 方式二
let arr3: Array<string> = ['a', 'b', 'c']

// 报错:不能将类型“number”分配给类型“string”。
let arr4: Array<string> = ['a', 'b', 'c', 4]
元组

在 TypeScript 中,元组(Tuple)是一种特殊的数组类型,它允许您指定一个固定长度和对应类型的数组

let arr5:[string, number, string] = ['a', 1, 'b']

// 报错:不能将类型“[string, number]”分配给类型“[string, number, string]”。源具有 2 个元素,但目标需要 3 个。
let arr6:[string, number, string] = ['a', 1]

// 正确
arr6[0] = 'a2'
// 错误:不能将类型“number”分配给类型“string”。
arr6[0] = 1

// 第三个添加 ? 表明可选,这样只传入 2 个数也不会报错
let arr7:[string, number, string?] = ['a', 1, 'b']

枚举

枚举需要使用关键字 enum。请看示例:

// 就像定义对象,不过不需要 =
enum TestEnum {
  a,
  b,
  c,
}
// 1
console.log(TestEnum.b);
// b
console.log(TestEnum[1]);
// string
console.log(typeof TestEnum[1]);

ts 可以自动为枚举类型中的各成员推导对应数字。上面示例推导结果:

enum TestEnum {
  a = 0,
  b = 1,
  c = 2,
}

也可以自己手动设置:

enum TestEnum2 {
  a = 3,
  b = 13,
  c = 23,
}
// 13
console.log(TestEnum2.b);

比如这个,c 就是 b 的下一个数字:

enum TestEnum3 {
  a,
  b = 13,
  c,
}
// 14
console.log(TestEnum3.c);

使用场景
:比如你之前根据订单状态写了如下代码,可以用枚举来增加可读性。

if(obj.state === 0){

}else if(obj.state === 1){

}else if(obj.state === 2){

}else if(obj.state === 3){

}
// 优化后
enum 订单状态{
  取消,
  上线,
  发送,
  退回,
  ...
}

if(obj.state === 订单状态.取消){

}else if(obj.state === 订单状态.上线){

}else if(obj.state === 订单状态.发送){

}else if(obj.state === 订单状态.退回){

}

函数

定义一个函数,参数报错:

// 参数 a 和 b报错。例如:a - 参数“a”隐式具有“any”类型。
function fn1(a, b){
  return a + b
}

定义参数类型:

function fn2(a: number, b : number){
  return a + b
}

定义参数 b 可选,返回值是 number类型。请看示例:

// b是可选。
// 必选的放左侧,可选的放后侧
function fn5(a: number, b?: number): number{
  return 10
}
// 应有 1-2 个参数,但获得 0 个。
fn5()

定义参数 a 的默认值,rest是一个字符串数组:

// a 有一个默认值 10
function fn7(a = 10, b?: number, ...rest:string[]): number{
  return 10
}

fn7(1,2, 'a', 'b')

void

通常用于函数,表示没有 return 的函数。

function fn3(a: number, b : number):void{
  // 不能将类型“number”分配给类型“void”。
  return a + b
}

function fn4(a: number, b : number): void{
  
}

接口

通常用于对象的定义。请看示例:

interface Person{
  name: string,
  age: number
}

const p: Person = {
  name: 'peng',
  age: 18
}

// 报错:类型 "{ name: string; }" 中缺少属性 "age",但类型 "Person" 中需要该属性。ts(2741)
const p2: Person = {
  name: 'peng',
}

类型别名

比如定义了一个变量 v1,其类型可以是 number 或 string,但是好多地方都是这个类型:

let v1: number | string = 3

我们可以通过
type
定义一个
别名
。就像这样:

// 定义别名 Message
type Message = number | string
let v2: Message = 'hello'
// 报错:不能将类型“boolean”分配给类型“Message”
let v3: Message = true

泛型

比如定义如下一个处理 number 的函数:

function fn1(a: number, b:number): number[]{
  return [a, b]
}

假如以后想把这个函数作为一个通用函数,除了可以处理 number,还可以处理 string 等其他类型,比如:

function fn1(a: string, b:string): string[]{
  return [a, b]
}

a: string | number
又交叉了。就像这样:

function fn1(a: string | number, b:string | number): string[]{
  return [a, b]
}

这里可以使用
泛型
,请看示例:

// 定义一个变量,比如 T
function fn1<T>(a: T, b:T): T[]{
  return [a, b]
}

fn1<number>(11, 11)
fn1<string>('a', 'a')
// 正确,ts 会自动推导
fn1('a', 'a')

再看一个泛型示例:

// 参数 arr 是 T 类型的数组
// 返回 T 类型或 undefined
function firstElement<T>(arr: T[]): T | undefined {
    return arr[0];
}

firstElement(['a', 'b'])

函数重载

java 中函数重载是定义多个方法,调用时根据参数
类型

数量
的不同执行不同的方法。例如下面定义两个 add:

// 方法重载示例:两个参数的相加
public int add(int a, int b) {
    return a + b;
}

// 方法重载示例:三个参数的相加
public int add(int a, int b, int c) {
    return a + b + c;
}

ts 这里重载和 java 中的有些不同,可以称之为
函数重载申明

比如首先我们写了一个
数字相加

字符串相加
的方法:

// 数字相加
// 字符串相加
function combine(x: number | string, y: number | string): number | string {
  if (typeof x === 'number' && typeof y === 'number') {
    return x + y;
  } else if (typeof x === 'string' && typeof y === 'string') {
    return x + y;
  }
  // 处理其他情况
  return 'Invalid input';
}

console.log(combine(1, 2)); // 输出:3
console.log(combine('hello', 'world')); // 输出:helloworld

这里有两个问题:

// 问题一:鼠标移动到 combine 显示:
// function combine(x: number | string, y: number | string): number | string
console.log(combine(1, 2));
console.log(combine('hello', 'world'));

// 问题二:传入 number和 string 不合法,但不报错。鼠标移动到 combine 显示:
// function combine(x: number | string, y: number | string): number | string
console.log(combine(1, 'two')); // 输出:Invalid input

现在加上
函数重载申明
,就能解决上述两个问题。请看示例:

// 函数重载
function combine(x: number, y: number): number;
// 变量名可以不是x、y
function combine(x2: string, y2: string): string;
function combine(x: number | string, y: number | string): number | string {
  // 不变
}

// function combine(x: number, y: number): number (+1 overload)
console.log(combine(1, 2));
// function combine(x: string, y: string): string (+1 overload)
console.log(combine('hello', 'world'));

// 报错:没有与此调用匹配的重载。
//   第 1 个重载(共 2 个),“(x: number, y: number): number”,出现以下错误。
//   第 2 个重载(共 2 个),“(x: string, y: string): string”,出现以下错误。ts(2769)
console.log(combine(1, 'two'))

接口继承

直接看示例:

interface Person{
  name: string,
  age: number
}

// Student 继承 Person
interface Student extends Person{
  school: string
}

// 提示p缺少3个属性
// 类型“{}”缺少类型“Student”中的以下属性: school, name, agets(2739)
const p: Student = {

}

Student 继承 Person,有了3个属性。

类的修饰符

类的修饰符有:public、private、protected、static、readonly...。用法请看下文:

比如有这样一段正常的js代码:

class People{
    constructor(name){
        this.name =name;
    }
    // 不需要逗号
    sayName(){
        console.log(this.name)
    }
}
let people = new People('aaron')
people.sayName() // aaron

放在 ts(比如
ts在线运行环境
) 中会报错如下:

Parameter 'name' implicitly has an 'any' type.
Property 'name' does not exist on type 'People'.
Property 'name' does not exist on type 'People'.

需要修改如下两处即可消除所有错误:

 class People{
-    constructor(name){
+    // 消除ts报错:类型“People”上不存在属性“name”
+    name: string
+    constructor(name: string){       
         this.name =name;
     }
     // 不需要逗号

其中
name: string
的作用:声明 People 类有个必填属性。实例化 People 类的时候,必须传入一个 string 类型的 name 属性。

接着加一个可选属性 age:

  // 通过?将 age 改成可选。解决:属性“age”没有初始化表达式,且未在构造函数中明确赋值。
  age?: number

可以设置默认值:

  // 根据默认值推断类型,而且是必选属性
  money = 100

Tip
:稍后我们会看到对应的 js 是什么样子。

属性默认是
public
,自身可以用,继承的子类中也可以使用。public 还可以这么写,效果和上例等价:

    constructor(name){
-    name: string
-    constructor(name: string){       
+    constructor(public name: string){       
         this.name =name;
     }

另外还有
private
表明只能在类中使用。
protected
只能在类和子类中使用。请看示例:

class People{
    ...
    // 属性默认是 public,自身可以用、继承也能用
    public money2 = 200
    private money3 = 300
    protected money4 = 400
    constructor(name: string){
        this.name =name;
    }
    sayName(){
        console.log(this.name)
    }
}
let people = new People('aaron')

console.log(people.money);

// 属性“money3”为私有属性,只能在类“People”中访问。ts(2341)
console.log('people.money3: ', people.money3); // 300
// 属性“money4”受保护,只能在类“People”及其子类中访问。ts(2445)
console.log('people.money4: ', people.money4); // 400


:虽然 vscode 报错,但浏览器控制台还是输出了。
或许 ts 只是静态编译,对应的js 没有做特殊处理
,比如 private 声明 money4,实际上并没有实现。请看
ts在线运行环境
ts 对应的 js:

// ts
class People{
    name: string
    age?: number
    money = 100
    public money2 = 200
    private money3 = 300
    protected money4 = 400
    constructor(name: string){
        this.name =name;
    }
    sayName(){
        console.log(this.name)
    }
}
let people = new People('aaron')

console.log('people.money3: ', people.money3);
console.log('people.money4: ', people.money4);
// 对应的js
"use strict";
class People {
    constructor(name) {
        this.money = 100;
        this.money2 = 200;
        this.money3 = 300;
        this.money4 = 400;
        this.name = name;
    }
    sayName() {
        console.log(this.name);
    }
}
let people = new People('aaron');
console.log('people.money3: ', people.money3);
console.log('people.money4: ', people.money4);

js 中
静态属性
使用如下:

    protected money4 = 400
    // 静态属性
+   static flag = 110

console.log('People.flag: ', People.flag);

例如将静态属性设置成私有,只能在类中使用。请看示例:

  // 静态属性
  private static flag = 110

// 报错:属性“flag”为私有属性,只能在类“People”中访问。
console.log('People.flag: ', People.flag);

多个修饰符
可以一起使用,但有时候需要注意顺序,vscode 也会给出提示。就像这样:

// “static”修饰符必须位于“readonly”修饰符之前。ts(1029)
readonly static flag = 110

比如定义一个静态只读属性:

  static readonly flag2 = 110

// 报错:无法为“flag2”赋值,因为它是只读属性。ts(2540)
People.flag2 = 666666

类的存取器

感觉就是 js 的 get 和 set。比如下面就是一个 js 的get、set示例:

class People {
    constructor(name) {
        this.name = name;
    }
    get name() {
        return 'apple';
    }
    set name(v) {
        console.log('set', v);
    }
}
let people = new People('aaron') // set aaron

people.name = 'jia' // set jia
console.log(people.name); // apple

对应 ts 中的存取器就是这样:

class People{
    constructor(name: string){
        this.name = name;
    }
    get name(){
      return 'apple'
    }
    set name(v){
      console.log('set', v)
    }
}
let people = new People('aaron') // set aaron

people.name = 'jia' // set jia
console.log(people.name); // apple

注:这个例子很可能会栈溢出,就像这样:

class People{
    constructor(name: string){
        this.name = name;
    }
    get name(){
      return 'apple'
    }
    set name(v){
      console.log('v: ', v);
      // 栈溢出
      // 报错:VM47:10 Uncaught RangeError: Maximum call stack size exceeded
      this.name = v
    }
}
let people = new People('aaron')

所以可以这么写:

class People {
    private _name: string = ''
  
    get name(): string{
      return 'peng'
    }
  
    set name(val: string){
      this._name = val
    }
  }
  let people = new People()
  
  people.name

  // 报错:属性“_name”为私有属性,只能在类“People”中访问。ts(2341)
  people._name

不写类型,ts 也会自动推导,比如去除类型后也可以。就像这样:

// 自动推导类型
class People {
    private _name = 'peng'
  
    get name(){
      return 'peng'
    }
  
    set name(val){
      this._name = val
    }
  }
let people = new People()

抽象类

抽象类(abstract),
不允许被实例化,抽象属性和抽象方法必须被子类实现
。更像一个规范。请看示例

abstract class People {
  // 可以有抽象属性和方法
  abstract name: string
  abstract eat(): void
  // 也可以有普通属性和方法
  say() {
    console.log('hello: ' + this.name)
  }
}

// 如果不实现 name 和 eat 方法则报错
class Student extends People{
  name: string = '学生'

  // 既然没报错 - 抽象类中返回是 void,这里返回string
  eat(){
    return 'eat apple'
  }
}

const s1 = new Student()
s1.say()

console.log(s1.eat()); // eat apple

抽象类定义了一个抽象属性、一个抽象方法,一个
具体方法
。子类必须实现抽象属性和抽象方法,子类实例可以直接访问抽象类中具体的方法。请看对应的 js 代码,你就能很明白。

class People {
    say() {
        console.log('hello: ' + this.name);
    }
}
class Student extends People {
    constructor() {
        super(...arguments);
        this.name = '学生';
    }
    eat() {
        return 'eat apple';
    }
}
const s1 = new Student();
s1.say();
console.log(s1.eat());

类实现接口

前面我们用接口定义了一个类型:

interface Person{
  name: string,
  age: number
}

const p: Person = {
  name: 'peng',
  age: 18
}

抽象类如果只写抽象方法和属性,那么就和接口很相同了。另外接口用
interface
关键字定义,子类可以实现
implements
(注意这个单词是
复数
) 多个接口(不能同时继承多个)。请看示例:

interface People {
  name: string
  eat(): void
}

interface A{
  age: number
}

// 实现两个接口,所有属性和方法都需要实现
class Student implements People, A{
  name: string = '学生'
  age = 100
  // 既然没报错
  eat(){
    return 'eat apple'
  }
}

const s1 = new Student()

console.log(s1.eat()); // eat apple

泛型类

使用类时,除了可以使用接口来规范行为,还可以将类和泛型结合,称为
泛型类

比如现在 deal 是处理 string 的方法:

class People {
    value: string;

    constructor(value: string) {
        this.value = value;
    }

    deal(): string {
        return this.value;
    }
}

const p1 = new People('peng')
p1.deal()

后面我需要 deal 又能处理 number,这样就可以使用泛型。就像这样:

class People<T> {
    value: T;

    constructor(value: T) {
        this.value = value;
    }

    deal(): T {
        return this.value;
    }
}

const p1 = new People('peng')
p1.deal()

const p2 = new People(18)
p2.deal()

多个泛型写法如下:

class Pair<T, U> {
    private first: T;
    private second: U;

    constructor(first: T, second: U) {
        this.first = first;
        this.second = second;
    }

    public getFirst(): T {
        return this.first;
    }

    public getSecond(): U {
        return this.second;
    }
}

// 使用带有多个泛型类型参数的泛型类
let pair1 = new Pair<number, string>(1, "apple");
console.log(pair1.getFirst()); // 1
console.log(pair1.getSecond()); // apple

let pair2 = new Pair<string, boolean>("banana", true);
console.log(pair2.getFirst()); // banana
console.log(pair2.getSecond()); // true

其他

Error Lens
:提供了一种更直观的方式来展示代码中的问题,如错误、警告和建议,以帮助开发者更快速地识别和解决问题。

vscode 直接安装后,会将红色错误提示直接显示出来,无需将鼠标移到红色波浪线才能看到错误提示。