Android Gradle 学习笔记(四)构建块基本单元掌握

news2024/11/27 12:57:50

本篇主要学习 Gradle 构建脚本的三个基本组建成:

  • project 项目
  • task 任务
  • property 属性

我将会重点介绍 Task,因为它是所有基础中的基础, project 的知识更多和实战有关(例如模块化),而 property 本身不过就是一个属性变量,没有什么东西可讲。


目录

  • 1. Project
  • 2. Task
    • 2.1 任务的结果
    • 2.2 任务的定义
      • 2.2.1 任务的创建
      • 2.2.2 doFirst 和 doLast 的定义
      • 2.2.3 调用任务
      • 2.2.4 任务配置
      • 2.2.5 参数传递
      • 2.2.6 添加依赖项
      • 2.2.7 跳过任务
        • 2.2.7.1 使用 `onlyIf`
        • 2.2.7.2 使用 `StopExecutionException`
        • 2.2.7.3 启动和禁止任务
    • 2.3 增量构建
    • 2.4 生命周期任务(Lifecycle tasks)
  • 3. Property
  • 参考

1. Project

在 Gradle 中,一个 Project 表示一个项目,每一个 .gradle 文件都会和一个 Project 对象相关联,它最终会编译成 Project 字节码。 它用于表示一个整体的目标,例如你用一个 Project 来做 Android 的自动化打包、做一个 Web 应用等等。

在运行构建时,Gradle 会实例化一个 Project ,你可以在脚本中通过 project 句柄获取该实例,我们可以调用该实例的方法来获取或配置项目的参数。它是我们构建脚本中与 Gradle 交互的主要 Api。

Project 是一个Java接口,位于:org.gradle.api.Project ,它的成员函数如下图所示:
在这里插入图片描述
通过上面这些函数,我们能知道 Project 有这些定义好的能力:

  • 可以在项目上创建新任务(task 函数)
  • 添加依赖和配置 (dependenciesconfigurations 函数)
  • 应用插件或其它脚本 (apply 函数)
  • 可以通过 getter / setter 来获取属性,例如 name、 description 等等。

我们可以通过 project 句柄来使用这些能力,如:

project.description = "rikka the project"
println("project的描述为:${project.description}")

由于构建脚本是基于 Project ,咱们写的代码都是在 Project 的作用域中,了解 Kotlin DSL 的同学会知道,.kts 文件其实就是在一个 lambda 表达式里面编程,而这个 lambda 表达式的接收者就是 Project ,带有接收者的函数类型的特点是我们可以隐式调用接收者的函数,因此我们不用显示声明 project

description = "rikka the project"
println("project的描述为:${description}")

在我们实际的项目中,一般都是多 project 构建,例如 Android 中就有一个 根Project 的 build.gralde 和至少一个 module 的 buidle.gradle。因为项目越复杂,单个 project 承载的任务会越多越臃肿,我们就越有拆分拆模块的必要。这也引申出 模块化 这是个非常重要的概念, Gradle 对于这方面的支持也已经非常强大,会在后面学习到。

2. Task

TaskProject 作用域下的执行单元,和 Project 一样,在 Gradle 中,它被赋予了一些能力。

Task 是一个 Java 接口,位于 org.gradle.api.Task,它的成员函数如下图所示:
在这里插入图片描述
通过这些函数,我们能知道 Task 的能力:

  • 可以依赖其他 Task( dependsOn 函数 )
  • 可以定义执行阶段初始和末尾动作 (doFirstdoLast 函数)
  • 通过 getter / setter 获取一些配置属性

2.1 任务的结果

之前的章节里,在执行任务时,我们会看到:
在这里插入图片描述
请注意最后一行的信息,在平日的 Android 开发中,你会留意到:在点击 build 运行项目时,控制台中会打印 Gradle 的执行信息,可能会有一些 "Task :xxxxx NO-SOURCE""Task :xxxx UP-TO-DATE" 等字样信息, 这些信息其实是 Gradle 任务执行的结果,它有好几种类型,表明任务执行情况,下面介绍这些类型和意义:

  • (没有标签) 或 EXCUTED
    • 任务已经执行动作
    • 任务没有动作,但依赖n个任务,这n个任务都被执行完成
  • UP-TO-DATE
    表示这个任务的结果已经是最新的。如果一个任务只定义了输出, 如果输出不变的话, 它就会被视为 up-to-date
    • 任务第一次被执行时产生了一个结果,这次执行后会将结果和上次进行比对,发现是一样的,说明这个任务其实没有进行任何改动(这里一般被用做增量构建)
    • 任务告诉 Gradle 没有更改其输出
    • 任务没有任何动作,有一些依赖,但是这些依赖的结果都是 “UP-TO-DATE”、“SKIPPED”、“FROM-CACHE
    • 任务没有任何动作,也没有任何依赖
  • FROM-CACHE
    任务的输出从原来的输出缓存中还原。
  • SKIPPED
    任务没有被执行
    • 该任务已经在命令行中被明确排除
    • 任务有个 onlyIf 谓词,但是该谓词返回 false,所以不执行
  • NO-SOURCE
    任务不需要执行其动作
    • 任务有定义输入和输出,但是没有输入。例如在使用 javac 命令编译 Java 时,没有输入的 .java 文件。

上面 UP-TO-DATEFROM-CACHE 看起来有点像,下面引用 What is Different? 来看下它们的区别:

Gradle有两个主要的特性:增量构建任务输出缓存

  • 增量构建
    用于执行那些自上次运行依赖没有发生更改的任务。为了实现这一点, gradle 会将执行的输入输出以“快照”的形式存储在本地目录 .gradle 文件中。当你运行一个已经执行且没有更改的任务时,这个任务会打印 “UP-TO-DATE
  • 任务输出缓存
    主要用于 CI 环境,当在 CI 运行的每个阶段时,会删除和检出本地目录,这意味着保存缓存的 .gradle 本地文件会在每个阶段消失,因此,任务每次被重新构建,增量构建都不会起作用,此时代替它的将是 任务输出缓存,它的能力是在 gradle 执行时,生成一个缓存,应用于填充本地 .gradle 目录,当构建缓存被用来重新填充本地目录时,任务被标记为 “FROM-CACHE”,一旦本地目录被重新填充,下一次执行将把任务标记为“UP-TO-DATE”而不是“FROM-CACHE”。

2.2 任务的定义

2.2.1 任务的创建

我们可以通过 tasktasks 句柄来创建任务,如下代码所示:

task("HelloWorld") {
	println("Hello World")
}


// 这里的用法等于 task("printRikka") {}
tasks.create("printRikka") {
    println("Rikka")
}

在上面代码中,我们使用了 task(taskName) { .. } 创建了一个任务。在每次 Gradle 开启时,会先跑一遍它的配置代码。

这里可能会有些小问题:比如我不想它那么早的运行配置代码,如果我弄了很多 task,一些还是非必要的、动态的,那岂不是容易影响性能?

因此 Gradle 提供了懒加载 task,我们可以通过 tasks.register() { .. } 来创建一个懒任务,它在被执行时,才会去配置运行:

// 定义一个懒任务
tasks.register("printHello") {
    println("Hello")
}

// Kotlin 可以使用委托进行定义任务
val hello by tasks.registering {
    doLast {
        print("Hello Delegate last")
    }
}

运行 Gradle,你会发现啥都没有打印,只有执行这些任务才会打印, Gradle 推荐我们使用 tasks.register 来创建任务以提高性能,之后的代码我也会这样去配置任务信息。

2.2.2 doFirst 和 doLast 的定义

doFirstdoLast 的作用是什么呢?

Task 可以分为两个部分:

  • 配置部分
    Gradle 会在配置阶段去跑一遍 Task, 例如设置描述、组别
  • 执行部分
    Task 有实际的功能,例如签名、删除文件等,可能会被其它任务依赖、调用才会去执行,因此不需要在配置阶段就去执行这些任务。所以 doFirstdoLast 的意义就是在执行阶段进行的代码,它们不会在配置阶段执行。

看下面代码:

task("myTask") {
    doFirst {
        println("first")
    }
    doLast {
        println("last")
    }
    description = "rikka the project"
    println("project的描述为:${description}")
}

若不执行这个 task,而是跑一个简单的 gradle 命令,打印如下:

>  Configure project :
project的描述为:rikka the project

而若执行了 gradle myTask,将会打印:

> Configure project :
project的描述为:rikka the project

> Task :myTask
first
last

doFirst 和 doLast 的特性如下:

  • 通过 doFirst / doLast 来编写具体执功能的代码
  • doFirst / doLast可以指定多个。
  • 外部可以指定一个 task 的doFirst / doLast,而且这些块会比内部指定的先执行
  • 通过 doFirst / doLast, 我们能对原有 task 进行扩展而不破坏原有代码结构,如:
task("printRikka") {
    println("Rikka")
}

// 对已定义的任务增加额外功能
// 这里使用 tasks 来索引全局任务, 也可以使用 tasks.getByName("printRikka") 获取该任务
tasks["printRikka"].doFirst {
    println("Hello World")
}

2.2.3 调用任务

当我们创建了一个任务后,后面需要使用这个任务时,该如何调用它呢?

task("printHello")      // 创建一个任务
task<Copy>("copyTask")  // 创建一个复制文件的任务

// 使用 Kotlin 代理来访问

val printHello by tasks.getting    // 指定任务名
println(printHello.name)

val copyTask by tasks.getting(Copy::class)  // 指定任务名和类型
println(copyTask.destinationDir)

也可以通过 tasks 来获取:

tasks.register("printHello")
tasks.register<Copy>("copyTask")

println(tasks["printHello"].name)
println(tasks.named("printHello").get().name)

println(tasks.getByName<Copy>("copyTask").destinationDir)
println(tasks.named<Copy>("copyTask").get().destinationDir)

如果这个任务被定义在别的项目中,我们需要使用相对路径 / 绝对路径的方式来访问:

project(":projectA") {
    tasks.register("printHello")  // 别的项目有一个任务
}

tasks.register("printHello")  // 本项目中有一个同名任务

println(tasks.getByPath("printHello").path)  // 打印 :printHello
println(tasks.getByPath(":printHello").path)  // 打印 :printHello
println(tasks.getByPath("projectA:printHello").path)  // 打印 :projectA:printHello
println(tasks.getByPath(":projectA:printHello").path) // 打印 :projectA:printHello

2.2.4 任务配置

Gradle 内置了一些已有的任务类型,方便我们复用。例如复制文件的任务 Copy,我们通过配置复制的目标文件和目的地,就能轻松创建一个复制任务:

val myCopy = tasks.named<Copy>("myCopy")   // 创建一个 Copy 任务,名称为 myCopy

myCopy {  // 通过 DSL 进行配置
    from("resources")  // 使用 api 来配置数据源
    into("target")     // 使用 api 配置目的地
    include("**/*.txt", "**/*.xml", "**/*.properties")   // 配置 include 信息
}

这种写法比较冗余,因为它的创建代码和执行代码分离了,不太符合我们的习惯,下面这种写法更加易读:

val myCopy by tasks.existing(Copy::class) {  // 创建和执行写在一起, 也可以用 named、getByName
    from("resources")
    into("target")
}
myCopy { include(...) }  // 外部配置

// 也可以这样写:

tasks.register<Copy>("myCopy ") {
   from("resources")
   into("target")
   include(...)
}

2.2.5 参数传递

与有参数可以配置的Task不同,一些任务是需要入参来运行的,之前创建任务的方式不能满足这种条件。为了达到这个目的,我们必须使用 @javax.inject.Inject 注解来创建可入参的任务类:

// 创建一种 Task 类型,它可以接收一个 String 和 Int 作为参数
open class CustomTask @Inject constructor(
    private val message: String,
    private val number: Int
) : DefaultTask()

接下来,就像使用 Copy 类型任务一样,去使用这个 CustomTask

tasks.register<CustomTask>("customTask", "printHello", 11)

// 下面这种方式会麻烦一点:
task("customTask", "type" to CustomTask::class.java, "constructorArgs" to listOf("printHello", 11))

2.2.6 添加依赖项

我们在前面已经提到过任务的依赖了,通过 dependsOn() 即可配置依赖任务,跨项目的任务依赖,也可以通过路径名的方式来添加:

project("projectA") {
    tasks.register("taskA") {
        dependsOn(":projectB:taskB")
        doLast {
            println("taskA")
        }
    }
}

project("projectB") {
    tasks.register("taskB") {
        doLast {
            println("taskB")
        }
    }
}

// 执行 taskA,将会打印:
taskB
taskA

除了通过任务名称进行依赖,也可以直接使用 Task 对象本身:

val taskA by tasks.registering {
    doLast {
        println("taskA")
    }
}

val taskB by tasks.registering {
    doLast {
        println("taskB")
    }
}

taskA {
    dependsOn(taskB)
}

// 执行 taskA, 打印:
taskB
taskA

假如任务需要依赖多个其它任务,一次性写多个 dependsOn 可能会让代码有些臃肿,我们可以使用 Gradle Provider 提供的特性来优化代码:

val taskA by tasks.registering {
    doLast {
        println("taskA")
    }
}

// 使用 Provider 来拿到需要依赖的任务, 这里使用 filter 过滤器
taskA {
    dependsOn(provider {
        tasks.filter { task -> task.name.startsWith("lib") }
    })
}

tasks.register("lib1") {
    doLast {
        println("lib1")
    }
}

tasks.register("lib2") {
    doLast {
        println("lib2")
    }
}

tasks.register("notALib") {
    doLast {
        println("notALib")
    }
}

// 执行 gradle taskA, 打印:
lib1,
lib2,
taskA

2.2.7 跳过任务

Gradle 提供了一些方法来跳过任务,它有三种通用方法,下面来介绍它们。

2.2.7.1 使用 onlyIf

通过配置任务的 onlyIf { .. },若 lambda 内部返回 true,该任务才会被执行:

val hello by tasks.registering {
    doLast {
        println("hello world")
    }
}

hello {
    onlyIf { !project.hasProperty("skipHello")}
}

// 执行 gradle hello -PskipHello 输出:
Task :hello SKIPPED

2.2.7.2 使用 StopExecutionException

任务通过在执行过程中主动调用该异常,并不会终止整个 Gradle 构建,而将跳过该动作的执行,然后 Gradle 将继续执行下个任务:

val compile by tasks.registering {
    doLast {
        println("doing compile")
    }
}

compile {
    doFirst {
        // 这里可能会遇到一些问题,不能继续执行该任务了
        if (true) {
            throw StopExecutionException()
        }
    }
}
tasks.register("myTask") {
    dependsOn(compile)
    doLast {
        println("do myTask")
    }
}

// 执行 gradle myTask ,输出:
do myTask

2.2.7.3 启动和禁止任务

每个任务都有个 enable配置,默认为 true。反之将不会执行任何动作,被调用时会被标记为 SKIPPED

val disableMe by tasks.registering {
    doLast {
        println("This should not be printed")
    }
}
disableMe {
    enabled = false
}

// 执行 gradle disableMe 输出:
Task :disableMe SKIPPED

2.3 增量构建

增量构建是 Gradle 最重要的功能之一。在上面我们有介绍到,如果任务执行的结果是 UP-TO-DATE,那么说明该任务的输入输出没有更改,所以没有重新构建,从而节省了时间。那么增量构建是如何进行的呢?下面来简单阐述下原理。

正常情况下,任务存在一些输入,和一些输出,以下面编译 Java 为例子,输入是 Java 源文件和一些配置信息(如调试信息),输出是字节码文件:
在这里插入图片描述
上图中有三个指向 Task 的箭头,但只有两个箭头被认为是输入,这是因为:输入的特征是能够决定输出

  • 源文件:可以决定生成的字节码,所以它肯定是输入
  • 版本号:同上,因为不同的 JDK 版本,源文件会生成不同的字节码
  • Fork:是一些不会影响输出内容的参数,例如 memoryMaxiumSize 属性,它只是决定了编译环境的最大可用内存,所以对于 Gradle 来说,它只是一个任务的内部属性

Gradle 会检查当次执行任务的输入,和上次任务的输入(缓存在本地 .gradle 文件中),如果它们是相同的,则 Gradle 会任务该任务已是最新的,将会输出 UP-TO-DATE,并跳过执行。

具体的增量构建将会在之后的章节中详细讲解。

2.4 生命周期任务(Lifecycle tasks)

生命周期任务是不能自行工作的任务,它们通常没有任何动作,它们将一些东西进行了抽象:

  • 工作流程步骤(例如: checkbuild
  • 可构建的事务 (例如:创建一个用于调试的 32位 的可执行文件 debug32Executable
  • 一个父任务,用于组合一系列子任务(例如: 运行所有编译任务的 comileAll

插件都定义了一些标准的生命周期任务,例如 buildassemblecheck,所有主流语言都有相应的插件,它们都具有相同的生命周期任务。除非你去指定执行这些任务,否则它们的执行将会取决其任务依赖。

3. Property

在 Gradle 中,属性有两种,一种是 Project 和 Task 提供 getter / setter 来获取和配置的属性,这些我们在上面的 Task 中有学到,很多任务会自己定义配置属性,另外一种则是自定义属性。

对于自定义属性,我们可以轻松的使用 val、var 创建一个本作用域使用的属性,也就是局部变量:

// 定义一个变量
val dest = "dest"

// 这里表示一个复制任务,将 source 目标复制到 dest
tasks.register<Copy>("copy") {
    from("source")
    into(dest)
}

有局部变量,也就有全局变量,在 Gradle 中,全局变量被称为 “额外属性”、“外部属性”,可以通过 extra 定义,也可以使用 Kotlin 的委托:

// 使用委托定义一个外部变量
val myVersion by extra("1.1.0.RELEASE")
val myEmail by extra { "build@rikka.org" }

// 使用 extra 为 project 作用域定义属性
extra["myProp"] = "myValue"
extra.set("myProp1", "myValue1")

tasks.register("printProperties") {
    doLast {
        println(myVersion)
        println(myEmail)
        println(project.extra["myProp"])
        println(project.extra["myProp1"])
        println(extra["myProp2"])   // 没有定义,执行到这里会报错
    }
}

调用 priontProerties 任务:
在这里插入图片描述

参考

  • Gradle user guide 中文版
  • Gradle官方文档

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/364577.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

新手怎么快速学会并上手SQL语言?收藏版

SQL可以说是程序员、数据库管理员DBA、数据分析师等需要做数据处理和分析岗位最常用的程序语言了。 尤其是一些岗位&#xff0c;工作中并非以数据库为核心&#xff0c;只是偶尔用些数据不需要深度学习&#xff0c;不得不自己去做一些取数的事情&#xff0c;只要学会通过SQL取数…

PS_高低频和中性灰——双曲线

高低频 高低频磨皮&#xff1a;把皮肤分成两个图层&#xff0c;一层是纹理层也就是皮肤的毛孔。 一层是皮肤光滑层没有皮肤细节。 高频”图层为细节层&#xff0c;我们用图章工具修高频 “低频”图层为颜色层&#xff0c;我们用混合画笔修低频 原理&#xff1a;修颜色亮度光影…

用户投稿——详解我了解的 TDengine 以及它所在的时序数据库“战场”

作者&#xff1a;大数据模型本篇文章出自 2022 年“用 TDengine&#xff0c;写 TDengine”征文投稿活动。因为工作的关系&#xff0c;最近几年我接触到过各种国产数据库&#xff0c;唯独对 TDengine 念念不忘。在众多数据库中&#xff0c;TiDB 一枝独秀&#xff0c;OceanBase 出…

渗透中超全的Google hack语法

inurl:Login 将返回url中含有Login的网页intitle:后台登录管理员 将返回含有管理员后台的网页intext:后台登录 将返回含有后台的网页inurl:/admin/login.php 将返回含有admin后台的网页inurl:/phpmyadmin/index.php 将返回含有phpmyadmin后台的网页site:http://baidu.com inur:…

OpenStack手动分布式部署Keystone【Queens版】

目录 Keystone简介 1、登录数据库配置&#xff08;在controller执行&#xff09; 1.1登录数据库 1.2数据库里创建keystone 1.3授权对keystone数据库的正确访问 1.4退出数据库 2、数据库导入Keystone表&#xff08;在controller执行&#xff09; 2.1安装httpd mod_wsgi 2.2备…

nn.Conv2d与nn.Conv3d介绍

nn.Conv2d 1&#xff09;输入的维度为&#xff1a; 2&#xff09;输出的维度为&#xff1a; 其中&#xff0c;Cout是由参数out_channels&#xff08;(int) – 卷积产生的通道数&#xff09;决定。 Hout和Wout计算如下&#xff1a; 3&#xff09;在池化层的计算&#xff1a; …

Windows服务器使用cwRsync实现同步

软件分为客户端和服务器端 实现目的&#xff1a;将服务端C:\tongbu目录中的文件定时同步到客户端C:\target目录中去。 cwRsyncServer&#xff08;服务端&#xff09;配置步骤 1.双击运行wRsyncServer_4.1.0_Installer.exe。 2.这里创建的账户是操作系统的&#xff0c;创建的…

canvas初体验

canvas介绍 Canvas 最初由Apple于2004 年引入&#xff0c;用于Mac OS X WebKit组件&#xff0c;为仪表板小部件和Safari浏览器等应用程序提供支持。后来&#xff0c;它被Gecko内核的浏览器&#xff08;尤其是Mozilla Firefox&#xff09;&#xff0c;Opera和Chrome实现&#x…

webpack实现静态资源缓存

一、配置缓存 我们使用 webpack 来打包我们的模块化后的应用程序&#xff0c;webpack 会生成一个可部署的/dist目录&#xff0c;然后把打包后的内容放置在此目录中。只要/dist目录中的内容部署到 server 上&#xff0c;client&#xff08;通常是浏览器&#xff09;就能够访问此…

OKR之剑·总结篇01:如何开好一场OKR复盘会

作者&#xff1a;vivo 互联网平台产品研发团队 本文是《OKR 之剑》系列之总结第 1 篇—— OKR复盘是OKR生命周期中的重要环节&#xff0c;起着承上启下的重要作用。本篇将和你一起探讨什么是OKR复盘、为什么要进行OKR复盘&#xff0c;接着向你展示团队在OKR复盘方面的实践和改…

【MyBatis】代理开发核心配置文件介绍(二)

&#x1f697;MyBatis学习第二站站~ &#x1f6a9;起始站&#xff1a;MyBatis概述&环境搭建(一) &#x1f6a9;本文已收录至专栏&#xff1a;数据库学习之旅 &#x1f44d;希望您能有所收获 一.Mapper代理开发 (1) 引入 在上一篇中我们有提到一点&#xff0c;MyBatis可以…

Linux原来是这么管理内存的(最透彻的一篇)

Linux 内存管理模型非常直接明了&#xff0c;因为 Linux 的这种机制使其具有可移植性并且能够在内存管理单元相差不大的机器下实现 Linux&#xff0c;下面我们就来认识一下 Linux 内存管理是如何实现的。 基本概念 每个 Linux 进程都会有地址空间&#xff0c;这些地址空间由三…

堆的概念和结构以及堆排序

前言 普通的二叉树是不适合用数组来存储的&#xff0c;因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结 构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储&#xff0c;需要注意的是这里的堆和操作系统 虚拟进程地址空间中的堆是两回事&#xff0c…

Sofa-jraft的Rpc调用服务端分析

在sofa-jraft中&#xff0c;关于RPC的服务端是RpcServer在RpcServer中的init方法中&#xff1a;初始化了连接事件监听器&#xff0c;这个里面就是一个map&#xff0c;然后可以添加事件监听的处理器&#xff0c;初始化userProcessors, codec 是一个编码和解码器的工厂&#xff0…

VPS使用ProxySU搭建节点服务器

本文所有教程及源码、软件仅为技术研究。不涉及计算机信息系统功能的删除、修改、增加、干扰&#xff0c;更不会影响计算机信息系统的正常运行。不得将代码用于非法用途&#xff0c;如侵立删&#xff01; VPS使用ProxySU搭建节点服务器 环境 contos8win10 64位ProxySU 4.1.7 服…

02-mysql高级-

文章目录mysql高级1&#xff0c;约束1.1 概念1.2 分类1.3 非空约束1.4 唯一约束1.5 主键约束1.6 默认约束1.7 约束练习1.8 外键约束1.8.1 概述1.8.2 语法1.8.3 练习2&#xff0c;数据库设计2.1 数据库设计简介2.2 表关系(一对多)mysql高级 今日目标 掌握约束的使用 掌握表关系…

自动化测试——css元素定位

文章目录一、css定位场景二、css相对定位的优点三、css的调试方法1、表达式中含有字符串&#xff1a;表达式中的引号一定和外面字符串的引号相反四、css基础语法1、标签定位2、class定位特别注意&#xff1a;当class类型的属性值包含多个分割值&#xff0c;$(.s_tab s_tab_1z9n…

快捷式~node.js环境搭建

1、安装包官网下载&#xff1a;Node.js (nodejs.org) 2、安装完成后修改环境变量 在上面已经完成了 node.js 的安装&#xff0c;即使不进行此步骤的环境变量配置也不影响node.js的使用 但是&#xff0c;若不进行环境变量配置&#xff0c;那么在使用命令安装 node.js全局模块 …

Linux服务:Nginx服务配置及相关模块

目录 一、Nginx配置文件 1、主配置文件解析 2、子配置文件启用 二、子配置文件使用 1、创建虚拟主机实验 2、基于端口虚拟主机实验 三、Nginx模块 1、access模块 2、自定义错误页面 3、状态页开启 一、Nginx配置文件 1、主配置文件解析 ①yum安装主配置文件位置&…

docker上发布 sunnyNgrok 实现内外网穿透,容器内执行命令

最近在使用内外网穿透的工具时发现国内版的Ngrok还挺好用的&#xff0c;但是在dockerHub上搜镜像时发现不知道使用哪一个&#xff0c;索性便自己创建一个docker容器。 1、创建自己想要创建docker镜像的文件夹&#xff0c;我创建的名为“sunny-Ngrok” 2、在文件内创建Dockerfi…