当前位置: 首页 > news >正文

docker系列11:Dockerfile入门

 传送门

docker系列1:docker安装

docker系列2:阿里云镜像加速器

 docker系列3:docker镜像基本命令

docker系列4:docker容器基本命令

docker系列5:docker安装nginx

docker系列6:docker安装redis

docker系列7:docker安装ES

docker系列8:容器卷挂载(上)

docker系列9:容器卷挂载(下)

docker系列10:Dockerfile挂载容器卷

通过Dockerfile文件挂载容器卷回顾

在上一节中介绍了Dockerfile挂载容器卷,其中的Dockerfile文件如下:

FROM ubuntu
RUN mkdir /myvol
RUN echo "hello world" > /myvol/greeting
VOLUME /myvol

该命令的大概意思就是:

  • FROM表示,从标准的ubuntu镜像为准
  • RUN表示执行shell命令,创建一个文件目录/myvol
  • 执行命令,打印"hello world"字符串到greeting文件
  • 最后VOLUME,就是这节要讲到卷挂载!

然后通过Dockerfile生成了一个镜像:

docker build -t getting-started .

并且成功运行该镜像,最后成功实现了容器卷的挂载!

那么什么是Dockerfile文件?Dockerfile的有哪些指令?Dockerfile的编写有哪些最佳实践?接下来就带着这些疑问来探索一下

什么是Dockerfile

要知道Dockerfile是什么,从官网的定义来看看:

Docker can build images automatically by reading the instructions from a Dockerfile. A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image. 

Docker可以通过读取Dockerfile中的指令自动构建镜像。Dockerfile是一个文本文档,其中包含用户可以在命令行上调用的所有命令来组装镜像。

简单来说,可以通过Dockerfile文件来生成Docker镜像!首先Dockerfile文件是一个纯文本,然后在里面编写了一系列的指令,比如选择基础镜像(FROM)、拷贝文件(COPY)、运行脚本(RUN)等等,Docker 顺序执行这个文件里的所有步骤,最后就会创建出一个新的镜像出来。

https://docs.docker.com/build/guide/layers/

从上面的流程可以看出,左边用户编写的Dockerfile文件通过Docker构建之后,成为了右边的镜像。这个镜像跟官方镜像无本质区别(前面从DockerHub上安装的nginx、redis),称为"民间"、"草根"镜像。

镜像分类

 从DockerHub上可以看到对镜像的分类大致有3种:

  • Docker官方镜像(Docker Official Image)
  • 认证镜像(Verified Publisher)
  • 非官方镜像(Sponsored OSS)

官方镜像

像上面提到的nginx、redis都属于官方镜像!

这种镜像都会带上官方构建的标签,属于Docker公司提供的"极品"镜像,不仅质量上乘(有专门的团队负责审核、发布和更新),而且安全性也有保障(经过了严格的漏洞扫描和安全检测),并且也是标准的Dockerfile编写范例,是使用的首选!

认证镜像

认证镜像由Docker认证的出版商提供的高质量镜像。这些产品由商业公司直接发布和维护,比如 Bitnami、Rancher、Ubuntu 等。

这种由大公司"出品"的镜像,一般来说也具有相当高的质量,是不错选择。不过由于Docker认证需要收钱,店大欺客:

因为成为“Verified publisher”是要给 Docker 公司交钱的,而很多公司不想花这笔“冤枉钱”,所以只在 Docker Hub 上开了公司账号,但并不加入认证。

--引自“虚伪”的 Docker 开始清退开源组织,不付费就删除所有镜像!|插件|云原生|存储库|应用程序|docker_网易订阅

所以有些不交钱认证的公司的镜像就被降级为"非官方镜像",比如OpenResty:

非官方镜像

队了上面2种,剩下的就是非官方镜像,也戏称为"民间"镜像!

对于这些镜像的选择,下载量是一个重要的参考标准。 这个时候就是韩信点兵,多多益善了!

这里面也包括我们自己的镜像,如果推送的DockerHub,也是归于这一类镜像中,后面会单开一节来讨论如果上传。

Dockfile有哪些指令

Dockerfile文件命名

在上面的例子中,通过命令docker build来构建的镜像。这是因为省略了-f参数,默认在当前目录下查看名字为Dockerfile的文件:

Specifies the filepath of the Dockerfile to use. If unspecified, a file named Dockerfile at the root of the build context is used by default.

所以一般对于编写的Dockerfile默认使用Dockerfile这个名字,如果要用其它名字,在build时用-f来指定!

指令详解

Dockerfile支持的指令不少,比如例子里面提到的FROM、RUN、VOLUME,更多的可以查看Dockerfile指令

FROM

有效的Dockerfile必须以FROM指令开头,命令格式为:

FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]

表示选择构建使用的基础镜像,比如ubuntu镜像。其中--platform用在多平台镜像中,一般不用指定:

The optional --platform flag can be used to specify the platform of the image in case FROM references a multi-platform image. For example, linux/amd64linux/arm64, or windows/amd64. By default, the target platform of the build request is used.

可选的--Platform标志可用于指定镜像的平台,以防FROM引用多平台镜像。例如,linux/amd64、linux/arm64或windows/amd64。默认情况下,使用构建请求的目标平台。

也可以指定镜像对应的tag,比如nginx的Dockerfile,选择debian操作系统,版本为bookworm-slim:

RUN

Run指令可以说是Dockerfile 里最重要的一个指令了 ,它可以执行任意的 Shell 命令它也是Dockerfile里面最复杂的一条指令了,这取决于对于shell的编写。比如例子里面的简单shell命令:

RUN mkdir /myvol
RUN echo "hello world" > /myvol/greeting
  • 创建一个文件夹
  • 打印"hello world"到文件greeting文件中

 也可以特别复杂,比如nginx的RUN命令:

这整个就是一个复杂的shell脚本,因为太长了(RUN只能执行一行命令),所以采用续行符 \,命令之间也会用 && 来连接,这样保证在逻辑上是一行,如果写错了,build的时候会报错:

CMD

CMD指令设置从镜像运行容器时要执行的命令,格式为:

  • CMD ["executable","param1","param2"] (exec form)
  • CMD ["param1","param2"] (exec form, as default parameters to ENTRYPOINT)
  • CMD command param1 param2 (shell form)

比如,在容器运行之后,打印出当前目录,修改Dockerfile文件,ls -al

FROM ubuntu
RUN mkdir /myvol
RUN echo "hello world" > /myvol/greeting
CMD ["ls", "-al"]
VOLUME /myvol

构建并运行容器,之后就会执行 ls -al命令:

Dockerfile中只能有一条CMD指令。如果列出多个CMD,则只有最后一个生效。这一点在官网上有明确说明:

There can only be one CMD instruction in a Dockerfile. If you list more than one CMD, only the last one takes effect. 

而且官网上也对RUN与CMD的区别做了说明:

Don't confuse RUN with CMDRUN actually runs a command and commits the result; CMD doesn't execute anything at build time, but specifies the intended command for the image.

不要混淆RUN和CMD。RUN实际上运行一个命令并提交结果;CMD在构建时不执行任何东西,但指定镜像的预期命令。 

图片来自: https://docs.docker.com/guides/docker-overview/

VOLUME

这个卷挂载命令在前面已经讨论过了,这里就不赘述了。

COPY

COPY指令顾名思义,就是复制命令,有点类似于cp命名,格式如下:

COPY [OPTIONS] <src> ... <dest>
COPY [OPTIONS] ["<src>", ... "<dest>"]

其中src表示源文件,dest表示目标文件。比如上面的例子中,在当前目录下建一个test_cp.txt文件,并打包进镜像中:

FROM ubuntu
RUN mkdir /myvol
RUN echo "hello world" > /myvol/greeting
COPY ./test_cp.txt /tmp/test_cp.txt
VOLUME /myvol

然后重新构建并运行,发现构建成功之后,运行对应的镜像,容器中已经有了对应的文件,证明复制成功!

不过这里要注意的是, 拷贝的源文件必须是"构建上下文"路径里的,不能随意指定文件,甚至连绝对路径也不行。

如果要从本机向镜像复制文件,还要把这些文件放到一个专门的目录,然后在 docker build 里指定"构建上下文"到这个目录才行。在这里例子中,"构建上下文"为当前目录.

所以如果指定当前"构建上下文"为绝对路径:/root/test-docker

docker build -f Dockerfile_copy -t greetting_stared_cp3 /root/test-docker

Dockerfile里面COPY改为相对路径是可以的。由此得出,COPY只能基于相对路径来操作!

 这个原理呢,其实就要从docker build的说起了,跟Dockerfile没有多大关系,后面再讨论。 

WORKDIR

WORKDIR指令是设置工作目录,格式为:

WORKDIR /path/to/workdir

WORKDIR指令为Dockerfile中跟随它的任何RUN、CMD、ENTRYPOINT、COPY和ADD指令设置工作目录,默认为"/"目录下。 比如创建一个Dockerfile文件内容如下:

FROM ubuntu
WORKDIR /usr
WORKDIR share
WORKDIR bug
RUN pwd > greeting

 然后打包运行看下效果:

并且细心的观察就会发现,这个进入容器的运行目录,正是刚才通过WORKDIR设置的/usr/share/bug!所以WORKDIR也代表设置的容器运行的目录,这个通过退出容器再进去也能证实这一点:

在官方推荐指定这个WORKDIR的:

 Therefore, to avoid unintended operations in unknown directories, it's best practice to set your WORKDIR explicitly.

--因此,为了避免未知目录中的意外操作,最好的做法是显式设置WORKDIR。

EXPOSE

EXPOSE指令通知Docker容器在运行时侦听指定的网络端口,您可以指定端口侦听TCP还是UDP,如果不指定协议,则默认为TCP,格式为:

EXPOSE <port> [<port>/<protocol>...]

比如在docker系列7:docker安装ES 里面运行容器的命令里面通过-op指定了端口映射:

$ docker run -d --name elasticsearch --net somenetwork -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch

这个命令里面,-p表示容器对外暴露9200端口与主机的9200端口映射。但是如果Dockerfile里面也可以达到这种效果:

EXPOSE 9200

 对外暴露9200端口,默认是TCP协议。如果要支持UDP协议,要指定协议类型:

EXPOSE 9200/udp

或者一块暴露tcp、udp,要写2条指令:

EXPOSE 9200/tcp
EXPOSE 9200/udp

无论EXPOSE设置如何,都可以在运行时使用-p标志覆盖。这是因为EXPOSE在Dockerfile中,一般运行时要结合真实宿主机的情况来映射端口,所以一般来说EXPOSE更多的是一种文档化的指引,告诉用户哪个服务暴露哪个端口。比如nginx的Dockerfile中,EXPOSE了80,但是真实使用的时候还是要通过-p来指定:

ENV

ENV指令用于设置环境变量,格式为:

ENV <key>=<value> ...

可以一次性设置多个变量:

ENV MY_NAME="John Doe"
ENV MY_DOG=Rex\ The\ Dog
ENV MY_CAT=fluffy
ENV MY_NAME="John Doe" MY_DOG=Rex\ The\ Dog \MY_CAT=fluffy

 然后就可以在Dockerfile中引用这个变量了:

FROM ubuntu
ENV MY_NAME=DOCKER_ENV_TEST
RUN mkdir /myvol
RUN echo $MY_NAME > /myvol/greeting
VOLUME /myvol

然后打包运行看下效果:

通过ENV这种方式设置变量,官方并不推荐,就是因为在镜像里面也会生效,而不是仅仅在Docker构建过程中生效,所以一般推荐ARG:

If an environment variable is only needed during build, and not in the final image, consider setting a value for a single command instead.

Or using ARG, which is not persisted in the final image

ARG

对于ARG指令,刚才已经谈到了,用法跟ENV相似,区别就是在于 ARG 创建的变量只在镜像构建过程中可见,容器运行时不可见,而 ENV 创建的变量不仅能够在构建镜像的过程中使用,在容器运行时也能够以环境变量的形式被应用程序使用。所以可通过ARG 自行查看!

安全性问题

Dockerfile里面明确提到了,不建议使用变量来传递敏感数据,这又是另外一个话题了

MAINTAINER

MAINTAINER指令设置生成镜像的作者字段,格式为:

MAINTAINER <name>

比如在上面的Dockerfile增加作者信息:

总结

图片来自: 百度安全验证

命令式与声明式

上面介绍了Dockerfile文件与对应的指令,通过编写Dockerfile文件一步步的指定docker构建要执行的命令,“告诉”计算机每步该做什么,所有的步骤都列清楚,这样程序才能够一步步走下去,最后完成任务。这种就是命令式的文件,与之相反Kubernetes编写的YAML文件,就是"声明式"的:

apiVersion: v1
kind: Pod
metadata:name: ngx-podlabels:env: demoowner: chronospec:containers:- image: nginx:alpinename: ngxports:- containerPort: 80

 "声明式"不关心具体的过程,更注重结果(是不是有点耳熟?)

关于镜像的上传

在下一节,会尝试打包自己的镜像并推送到镜像仓库中,并用Dockerfile文件编写打包自己的java程序!

相关文章:

docker系列11:Dockerfile入门

传送门 docker系列1&#xff1a;docker安装 docker系列2&#xff1a;阿里云镜像加速器 docker系列3&#xff1a;docker镜像基本命令 docker系列4&#xff1a;docker容器基本命令 docker系列5&#xff1a;docker安装nginx docker系列6&#xff1a;docker安装redis docker系…...

LVS(Linux virual server)详解

目录 一、LVS&#xff08;Linux virual server&#xff09;是什么&#xff1f; 二、集群和分布式简介 2.1、集群Cluster 2.2、分布式 2.3、集群和分布式 三、LVS运行原理 3.1、LVS基本概念 3.2、LVS集群的类型 3.2.1 nat模式 3.2.2 DR模式 3.2.3、LVS工作模式总结 …...

Session共享方法

在Web开发中&#xff0c;会话&#xff08;Session&#xff09;管理是跟踪用户与服务器之间交互的一种常见方法。Session 共享通常指的是在一个应用集群或多个应用服务之间保持用户的会话状态一致。这在负载均衡、微服务架构或者分布式系统中尤为重要 一、基于SQL的session管理…...

Ubuntu 22.04 Docker安装笔记

1、准备一台虚机 可以根据《VMware Workstation安装Ubuntu 22.04笔记》来准备虚拟机。完成后&#xff0c;根据需求安装必要的软件&#xff0c;并设置root权限进行登录。 sudo apt update sudo apt install iputils-ping -y sudo apt install vim -y允许root ssh登录&#xff1…...

编程-设计模式 6:适配器模式

设计模式 6&#xff1a;适配器模式 定义与目的 定义&#xff1a;适配器模式将一个类的接口转换成客户希望的另一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。目的&#xff1a;该模式的主要目的是解决接口不匹配的问题&#xff0c;使得一个…...

ERC721 概念解释

目录 FeaturesVotesAccess ControlUpgradeabilityFeatures Mintable: 允许创建新的代币(minting)。合约的所有者或有权限的账户可以调用 mint 函数来生成新的代币,并将其分配给指定的地址。 Auto Increment Ids:自动递增 ID。每次创建新的代币时,代币的 ID 会自动递增,确保…...

数据结构(其五)--串

目录 12.串 12.1 基本操作 12.2 串的存储结构 12.3 字符串的模式匹配算法 (1).朴素模式匹配算法 (2).KMP算法 i.next[]数组的求解 ii.next[]数组的优化——nextval数组 iii.手算nextval数组 iiii.机算nextval数组 + KMP函数 12.串 串,即字符串(string),由零个或多…...

LeetCode Hot100 LRU缓存

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类&#xff1a; LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存int get(int key) 如果关键字 key 存在于缓存中&#xff0c;则返回关键字的值&#xff0c;否则返回 -…...

GESP C++ 2024年06月一级真题卷

一、单选题&#xff08;每题 2 分&#xff0c;共 30 分&#xff09; 第 1 题 在 C 中&#xff0c;下列不可做变量的是 ( ) 。 A. five-Star B. five_star C. fiveStar D. _fiveStar 答案&#xff1a;A 解析&#xff1a;标识符命名规则&#xff0c;标识符由字母、数…...

在 Ubuntu Server 上配置静态 IP 地址

在 Ubuntu Server 上配置静态 IP 地址 测试时使用的Ubuntu server版本是22.04 一、Ubuntu 17.10之前版本 使用 ifupdown 配置文件来设置静态 IP。配置文件通常位于 /etc/network/interfaces。 1.1 编辑 /etc/network/interfaces 文件&#xff1a; sudo vim /etc/network/in…...

数据结构——栈的讲解(超详细)

前言&#xff1a; 小编已经在前面讲完了链表和顺序表的内容&#xff0c;下面我们继续乘胜追击&#xff0c;开始另一个数据结构&#xff1a;栈的详解&#xff0c;下面跟上小编的脚步&#xff0c;开启今天的学习之路&#xff01; 目录 1.栈的概念和结构 1.1.栈的概念 1.2.栈的结构…...

三防平板助力MES系统,实现工厂移动式生产报工

在当今竞争激烈的制造业环境中&#xff0c;提高生产效率、优化生产流程以及实现精准的生产管理已经成为企业生存和发展的关键。 MES系统作为连接企业计划层和控制层的桥梁&#xff0c;在实现生产过程的信息化、数字化和智能化方面发挥着重要作用。与此同时&#xff0c;三防平板…...

WEB渗透Bypass篇-常规函数绕过

常规函数绕过 <?php echo exec(whoami);?> ------------------------------------------------------ <?php echo shell_exec(whoami);?> ------------------------------------------------------ <?php system(whoami);?> ------------------------…...

C++从入门到起飞之——string类的模拟实现 全方位剖析!

&#x1f308;个人主页&#xff1a;秋风起&#xff0c;再归来~&#x1f525;系列专栏&#xff1a;C从入门到起飞 &#x1f516;克心守己&#xff0c;律己则安 目录 1、多文件之间的关系 2、模拟实现常用的构造函数 2.1 无参构造函数 2.2 有参的构造函数 2.3 析构函…...

数据库国产化大趋势下,还需要学习Oracle吗?

由于众所周知的原因&#xff0c;近两年各行各业都开始了数据库国产化替代的进程&#xff0c;从国外商业数据库替换到国产或者开源数据库&#xff0c;相信很多的数据库从业人员会把部分精力转移到其他数据库产品的学习中&#xff0c;也有一些人在大肆的宣扬Oracle已经过时了&…...

WebLogic

二、WebLogic 2.1 后台弱口令GetShell 漏洞描述 通过弱口令进入后台界面&#xff0c;上传部署war包&#xff0c;getshell 影响范围 全版本(前提后台存在弱口令) 漏洞复现 默认账号密码:weblogic/Oracle123weblogic常用弱口令: Default Passwords | CIRT.net这里注意&am…...

Aspose.Words.dll 插入模板表格,使用的是邮件合并MailMerge功能,数据源是DataTable或list对象,实例

本实例中的实例功能有: 1、 Aspose.Words.dll 插入模板指定域替换为文字或html标签,见1 2、Aspose.Words.dll 插入模板表格,使用的是邮件合并MailMerge功能,数据源是DataTable或List对象(将list转换成DataTable),见1和2 3、word转换Pdf文件,见1 4、将多个word输出文…...

同时打开多个微信

注&#xff1a; 以下方法用到的 D:\微信\WeChat\WeChat.exe是我的电脑微信路径&#xff0c;可右击桌面微信快捷方式 > 属性 > 目标查看 以下方法都需要先关掉已登录的微信后操作 <一> 找到微信路径 新建一个txt文件输入以下内容 start D:\微信\WeChat\WeChat.exe …...

MPU6050的STM32数据读取

目录 1. 概述2. STM32G030对MPU6050的读取3. STM32F1xx对MPU6050的读取 1. 概述 项目中&#xff0c;往往需要根据不同的环境使用不同的芯片处理某些数据&#xff0c;当使用不同的芯片对六轴陀螺仪芯片MPU6050进行数据处理中&#xff0c;硬件的连接、I/O口的设置往往需要根据相…...

【微信小程序开发】——奶茶点餐小程序的制作(二)

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;开发者-曼亿点 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 曼亿点 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a…...

React 第五十五节 Router 中 useAsyncError的使用详解

前言 useAsyncError 是 React Router v6.4 引入的一个钩子&#xff0c;用于处理异步操作&#xff08;如数据加载&#xff09;中的错误。下面我将详细解释其用途并提供代码示例。 一、useAsyncError 用途 处理异步错误&#xff1a;捕获在 loader 或 action 中发生的异步错误替…...

C++实现分布式网络通信框架RPC(3)--rpc调用端

目录 一、前言 二、UserServiceRpc_Stub 三、 CallMethod方法的重写 头文件 实现 四、rpc调用端的调用 实现 五、 google::protobuf::RpcController *controller 头文件 实现 六、总结 一、前言 在前边的文章中&#xff0c;我们已经大致实现了rpc服务端的各项功能代…...

【kafka】Golang实现分布式Masscan任务调度系统

要求&#xff1a; 输出两个程序&#xff0c;一个命令行程序&#xff08;命令行参数用flag&#xff09;和一个服务端程序。 命令行程序支持通过命令行参数配置下发IP或IP段、端口、扫描带宽&#xff0c;然后将消息推送到kafka里面。 服务端程序&#xff1a; 从kafka消费者接收…...

进程地址空间(比特课总结)

一、进程地址空间 1. 环境变量 1 &#xff09;⽤户级环境变量与系统级环境变量 全局属性&#xff1a;环境变量具有全局属性&#xff0c;会被⼦进程继承。例如当bash启动⼦进程时&#xff0c;环 境变量会⾃动传递给⼦进程。 本地变量限制&#xff1a;本地变量只在当前进程(ba…...

前端倒计时误差!

提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...

《从零掌握MIPI CSI-2: 协议精解与FPGA摄像头开发实战》-- CSI-2 协议详细解析 (一)

CSI-2 协议详细解析 (一&#xff09; 1. CSI-2层定义&#xff08;CSI-2 Layer Definitions&#xff09; 分层结构 &#xff1a;CSI-2协议分为6层&#xff1a; 物理层&#xff08;PHY Layer&#xff09; &#xff1a; 定义电气特性、时钟机制和传输介质&#xff08;导线&#…...

Linux简单的操作

ls ls 查看当前目录 ll 查看详细内容 ls -a 查看所有的内容 ls --help 查看方法文档 pwd pwd 查看当前路径 cd cd 转路径 cd .. 转上一级路径 cd 名 转换路径 …...

基于Uniapp开发HarmonyOS 5.0旅游应用技术实践

一、技术选型背景 1.跨平台优势 Uniapp采用Vue.js框架&#xff0c;支持"一次开发&#xff0c;多端部署"&#xff0c;可同步生成HarmonyOS、iOS、Android等多平台应用。 2.鸿蒙特性融合 HarmonyOS 5.0的分布式能力与原子化服务&#xff0c;为旅游应用带来&#xf…...

解决本地部署 SmolVLM2 大语言模型运行 flash-attn 报错

出现的问题 安装 flash-attn 会一直卡在 build 那一步或者运行报错 解决办法 是因为你安装的 flash-attn 版本没有对应上&#xff0c;所以报错&#xff0c;到 https://github.com/Dao-AILab/flash-attention/releases 下载对应版本&#xff0c;cu、torch、cp 的版本一定要对…...

土地利用/土地覆盖遥感解译与基于CLUE模型未来变化情景预测;从基础到高级,涵盖ArcGIS数据处理、ENVI遥感解译与CLUE模型情景模拟等

&#x1f50d; 土地利用/土地覆盖数据是生态、环境和气象等诸多领域模型的关键输入参数。通过遥感影像解译技术&#xff0c;可以精准获取历史或当前任何一个区域的土地利用/土地覆盖情况。这些数据不仅能够用于评估区域生态环境的变化趋势&#xff0c;还能有效评价重大生态工程…...