在上一篇文章中 -- 现代 CSS 之高阶图片渐隐消失术,我们借助了 CSS @Property 及 CSS Mask 属性,成功的实现了这样一种图片渐变消失的效果:

CodePen Demo -- 基于 @property 和 mask 的文本渐隐消失术

但是,这个效果的缺陷也非常明显,虽然借助了 SCSS 简化了非常多的代码,但是,如果我们查看编译后的 CSS 文件,会发现,在利用 SCSS 只有 80 的代码的情况下,编译后的 CSS 文件行数高达 2400+ 行,实在是太夸张了。

究其原因在于,我们利用原生的 CSS 去控制 400 个小块的过渡动画,控制了 400 个 CSS 变量!代码量因而变得如此之大。

CSS Houdini 之 CSS Paint API

那么,如何有效的降低代码量呢?

又或者说,在今天,是否 CSS 还存在着更进一步的功能,能够实现更为强大的效果?

没错,是可以的,这也就引出了今天的主角,CSS Houdini 之 CSS Paint API

首先,什么是 CSS Houdini?

Houdini 是一组底层 API,它们公开了 CSS 引擎的各个部分,从而使开发人员能够通过加入浏览器渲染引擎的样式和布局过程来扩展 CSS。Houdini 是一组 API,它们使开发人员可以直接访问 CSS 对象模型 (CSSOM),使开发人员可以编写浏览器可以解析为 CSS 的代码,从而创建新的 CSS 功能,而无需等待它们在浏览器中本地实现。

而 CSS Paint API 则是 W3C 规范中之一,目前的版本是 CSS Painting API Level 1。它也被称为 CSS Custom Paint 或者 Houdini's Paint Worklet。

简单来说人话,CSS Paint API 的优缺点都很明显。

CSS Paint API 的优点

  1. 实现更为强大的 CSS 功能,甚至是很多 CSS 原本不支持的功能
  2. 将这些自定义的功能,很好的封装起来,当初一个属性快速复用

当然,优点看着很美好,缺点也很明显,CSS Paint API 的缺点

  1. 需要写一定量的 JavaScript 代码,有一定的上手成本
  2. 现阶段兼容的问题

小试牛刀 registerPaint

CSS Houdini 的特性就是 Worklet (en-US)。在它的帮助下,你可以通过引入一行 JavaScript 代码来引入配置化的组件,从而创建模块式的 CSS。不依赖任何前置处理器、后置处理器或者 JavaScript 框架。

废话不多说,我们直接来看一个最简单的例子。

<div style="--color: red"></div>
<div style="--color: blue"></div>
<div style="--color: yellow"></div> <script>
if (CSS.paintWorklet) {
CSS.paintWorklet.addModule('/CSSHoudini.js');
}
</script>
div {
margin: auto;
width: 100px;
height: 100px;
background: paint(drawBg);
}
// 这个文件的名字为 CSSHoudini.js
// 对应上面 HTML 代码中的 CSS.paintWorklet.addModule('/CSSHoudini.js')
registerPaint('drawBg', class { static get inputProperties() {return ['--color']} paint(ctx, size, properties) {
const c = properties.get('--color'); ctx.fillStyle = c;
ctx.fillRect(0, 0, size.width, size.height);
}
});

先看看最终的结果:

看似有点点复杂,其实非常好理解。仔细看我们的 CSS 代码,在 background 赋值的过程中,没有直接写具体颜色,而是借助了一个自定义了 CSS Houdini 函数,实现了一个名为 drawBg 的方法。从而实现的给 Div 上色。

registerPaint 是以 worker 的形式工作,具体有几个步骤:

  1. 建立一个 CSSHoudini.js,比如我们想用 CSS Painting API,先在这个 JS 文件中注册这个模块 registerPaint('drawBg', class),这个 class 是一个类,下面会具体讲到
  2. 我们需要在 HTML 中引入 CSS.paintWorklet.addModule('CSSHoudini.js'),当然 CSSHoudini.js 只是一个名字,没有特定的要求,叫什么都可以,
  3. 这样,我们就成功注册了一个名为 drawBg 的自定义 Houdini 方法,现在,可以用它来扩展 CSS 的功能
  4. 在 CSS 中使用,就像代码中示意的那样 background: paint(drawBg)
  5. 接下来,就是具体的 registerPaint 实现的 drawBg 的内部的代码

上面的步骤搞明白后,核心的逻辑,都在我们自定义的 drawBg 这个方法后面定义的 class 里面。CSS Painting API 非常类似于 Canvas,这里面的核心逻辑就是:

  1. 可以通过 static get inputProperties() {} 拿到各种从 CSS 传递进来的 CSS 变量
  2. 通过一套类似 Canvas 的 API 完成整个图片的绘制工作

而我们上面 DEMO 做的事情也是如此,获取到 CSS 传递进来的 CSS 变量的值。然后,通过 ctx.fillStylectx.fillRect 完成整个背景色的绘制。

使用 registerPaint 实现自定义背景图案

OK,了解了上面最简单的 DEMO 之后,接下来我们尝试稍微进阶一点点。利用 registerPaint 实现一个 circleBgSet 的自定义 CSS 方法,实现类似于这样一个背景图案:

CodePen Demo -- CSS Hudini Example - Background Circle

首先,我们还是要在 HTML 中,利用 CSS.paintWorklet.addModule('') 注册引入我们的 JavaScript 文件。

<div style=""></div>

<script>
if (CSS.paintWorklet) {
CSS.paintWorklet.addModule('/CSSHoudini.js'');
}
</script>

其次,在 CSS 中,我们只需要在调用 background 属性的时候,传入我们即将要实现的方法:

div {
width: 100vw;
height: 1000vh;
background: paint(circleBgSet);
--gap: 3;
--color: #f1521f;
--size: 64;
}

可以看到,核心在于 background: paint(circleBgSet),我们将绘制背景的工作,交给接下来我们要实现的 circleBgSet 方法。同时,我们定义了 3 个 CSS 变量,它们的作用分别是:

  1. --gap:表示圆点背景的间隙
  2. -color:表示圆点的颜色
  3. --size:表示圆点的最大尺寸

好了,接下来,只需要在 JavaScript 文件中,利用 CSS Painting API 实现 circleBgSet 方法即可。

来看看完整的 JavaScript 代码:

// 这个文件的名字为 CSSHoudini.js
registerPaint(
"circleBgSet",
class {
static get inputProperties() {
return [
"--gap",
"--color",
"--size"
];
} paint(ctx, size, properties) {
const gap = properties.get("--gap");
const color = properties.get("--color");
const eachSize = properties.get("--size");
const halfSize = eachSize / 2; const n = size.width / eachSize;
const m = size.height / eachSize; ctx.fillStyle = color; for (var i = 0; i < n + 1; i++) {
for (var j = 0; j < m + 1; j++) { let x = i * eachSize + ( j % 2 === 0 ? halfSize : 0);
let y = j * eachSize / gap;
let radius = i * 0.85; ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.fill();
}
}
}
}
);

代码其实也不多,并且核心的代码非常好理解。这里,我们再简单的解释下:

  1. static get inputProperties() {},我们在 CSS 代码中定义了一些 CSS 变量,而需要取到这些变量的话,需要利用到这个方法。它使我们能够访问所有 CSS 自定义属性和它们设置的值。

  2. paint(ctx, size, properties) {} 核心绘画的方法,其中 ctx 类似于 Canvas 2D 画布的 ctx 上下文对象,size 表示 PaintSize 对象,可以拿到对于元素的高宽值,而 properties 则是表示 StylePropertyMapReadOnly 对象,可以拿到 CSS 变量相关的信息

  1. 最终,仔细看看我们的 paint() 方法,核心做的就是拿到 CSS 变量后,基于双重循环,把我们要的图案绘制在画布上。这里核心就是调用了下述 4 个方法,对 Canvas 了解的同学不难发现,这里的 API 和 Canvas 是一模一样的。

    • ctx.fillStyle = color
    • ctx.beginPath()
    • ctx.arc(x, y, radius, 0, 2 * Math.PI)
    • ctx.fill()

这里,其实 CSS Houdini 的画布 API 是 Canvas API 的是一样的,具体存在这样一些映射,我们在官方规范 CSS Painting API Level 1 - The 2D rendering context 可以查到:

还记得我们上面传入了 3 个 CSS 变量吗?这里我们只需要简单改变上面的 3 个 变量,就可以得到不一样的图形。让我们试一试:

div {
width: 100vw;
height: 1000vh;
background: paint(circleBgSet);
// --gap: 3;
// --color: #f1521f;
// --size: 64;
--gap: 6;
--color: #ffcc00;
--size: 75;
}

又或者:

div {
width: 100vw;
height: 1000vh;
background: paint(circleBgSet);
// --gap: 3;
// --color: #f1521f;
// --size: 64;
--gap: 4;
--color: #0bff00;
--size: 50;
}

利用 registerPaint 实现自定义 mask

有了上面的铺垫,下面我们开始实现我们今天的主题,利用 registerPaint 自定义方法还原实现这个效果,简化 CSS 代码量:

自定义的 paint 方法,不但可以用于 background,你想得到的地方,其实都可以。

能力越大,责任越大!在 Houdini 的帮助下你能够在 CSS 中实现你自己的布局、栅格、或者区域特性,但是这么做并不是最佳实践。CSS 工作组已经做了许多努力来确保 CSS 中的每一项特性都能正常运行,覆盖各种边界情况,同时考虑到了安全、隐私,以及可用性方面的表现。如果你要深入使用 Houdini,确保你也把以上这些事项考虑在内!并且先从小处开始,再把你的自定义 Houdini 推向一个富有雄心的项目。

因此,这里,我们利用 CSS Houdini 的 registerPaint 实现自定义的 mask 属性绘制。

首先,还是一样,HTML 中需要引入一下定义了 registerPaint 方法的 JavaScript 文件:

<div></div>

<script>
if (CSS.paintWorklet) {
CSS.paintWorklet.addModule('/CSSHoudini.js');
}
</script>

首先,我们会实现一张简单的图片:


div {
width: 300px;
height: 300px;
background: url(https://tvax4.sinaimg.cn/large/6f8a2832gy1g8npte0txnj21jk13a4qr.jpg);
}

效果如下:

当然,我们的目标是利用 registerPaint 实现自定义 mask,那么需要添加一些 CSS 代码:


div {
width: 300px;
height: 300px;
background: url(https://tvax4.sinaimg.cn/large/6f8a2832gy1g8npte0txnj21jk13a4qr.jpg);
mask: paint(maskSet);
--size-m: 10;
--size-n: 10;
}

这里,我们 mask: paint(maskSet) 表示使用我们自定义的 maskSet 方法,并且,我们引入了两个 CSS 自定义变量 --size-m--size-n,表示我们即将要用 mask 属性分隔图片的行列数。

接下来,就是具体实现新的自定义 mask 方法。当然,这里我们只是重新实现一个 mask,而 mask 属性本身的特性,透明的地方背后的内容将会透明这个特性是不会改变的。

JavaScript 代码:

// 这个文件的名字为 CSSHoudini.js
registerPaint(
"maskSet",
class {
static get inputProperties() {
return ["--size-n", "--size-m"];
} paint(ctx, size, properties) {
const n = properties.get("--size-n");
const m = properties.get("--size-m");
const width = size.width / n;
const height = size.height / m; for (var i = 0; i < n; i++) {
for (var j = 0; j < m; j++) {
ctx.fillStyle = "rgba(0,0,0," + Math.random() + ")";
ctx.fillRect(i * width, j * height, width, height);
}
}
}
}
);

这一段代码非常好理解,我们做的事情就是拿到两个 CSS 自定义变量 --size-n--size-m 后,通过一个双循环,依次绘制正方形填满整个 DIV 区域,每个小正方形的颜色为带随机透明度的黑色。

记住,mask 的核心在于,透过颜色的透明度来隐藏一个元素的部分或者全部可见区域。因此,整个图片将变成这样:

当然,我们这个自定义 mask 方法也是可以用于 background 的,如果我们把这个方法作用于 backgorund,你会更好理解一点。

div {
width: 300px;
height: 300px;
background: paint(maskSet);
// mask: paint(maskSet);
--size-m: 10;
--size-n: 10;
}

实际的图片效果是这样:

好,回归正题,我们继续。我们最终的效果还是要动画效果,Hover 的时候让图片方块化消失,肯定还是要和 CSS @property 自定义变量发生关联的,我们简单改造下代码,加入一个 CSS @property 自定义变量。

@property --transition-time {
syntax: '<number>';
inherits: false;
initial-value: 1;
} div {
width: 300px;
height: 300px;
background: url(https://tvax4.sinaimg.cn/large/6f8a2832gy1g8npte0txnj21jk13a4qr.jpg);
mask: paint(maskSet);
--size-m: 10;
--size-n: 10;
--transition-time: 1;
transition: --transition-time 1s linear;
} div:hover {
--transition-time: 0;
}

这里,我们引入了 --transition-time 这个变量。接下来,让他在 maskSet 函数中,发挥作用:

registerPaint(
"maskSet",
class {
static get inputProperties() {
return ["--size-n", "--size-m", "--transition-time"];
} paint(ctx, size, properties) {
const n = properties.get("--size-n");
const m = properties.get("--size-m");
const t = properties.get("--transition-time");
const width = size.width / n;
const height = size.height / m; for (var i = 0; i < n; i++) {
for (var j = 0; j < m; j++) {
ctx.fillStyle = "rgba(0,0,0," + (t * (Math.random() + 1)) + ")";
ctx.fillRect(i * width, j * height, width, height);
}
}
}
}
);

这里,与上面唯一的变化在于这一行代码:ctx.fillStyle = "rgba(0,0,0," + (t * (Math.random() + 1)) + ")"

对于每一个小格子的 mask,我们让他的颜色值的透明度设置为 (t * (Math.random() + 1))

  1. 其中 t 就是 --transition-time 这个变量,记住,在 hover 的过程中,它的值会逐渐从 1 衰减至 0
  2. (Math.random() + 1) 表示先生成一个 0 ~ 1 的随机数,再让这个随机数加 1,加 1 的目的是让整个值必然大于 1,处于 1 ~ 2 的范围
  3. 由于一开始 --transition-time 的值一开始是 1,所以乘以 (Math.random() + 1) 的值也必然大于 1,而最终在过渡过程中 --transition-time 会逐渐变为 0, 整个表达式的值也最终会归于 0
  4. 由于上述 (3)的值控制的是每一个 mask 小格子的透明度,也就是说每个格子的透明度都会从一个介于 1 ~ 2 的值逐渐变成 0,借助这个过程,我们完成了整个渐隐的动画

看看最终的效果:

CodePen Demo -- CSS Hudini Example

是的,细心的同学肯定会发现,文章一开头给的 DEMO 是切分了 400 份 mask 的,而我们上面实现的效果,只用了 100 个 mask。

这个非常好解决,我们不是传入了 --size-n--size-m 两个变量么?只需要修改这两个值,就可以实现任意格子的 Hover 渐隐效果啦。还是上面的代码,简单修改 CSS 变量的值:

div:nth-child(1) {
--size-m: 4;
--size-n: 4;
}
div:nth-child(2) {
--size-m: 6;
--size-n: 6;
}
div:nth-child(3) {
--size-m: 10;
--size-n: 10;
}
div:nth-child(4) {
--size-m: 15;
--size-n: 15;
}

结果如下:

CodePen Demo -- CSS Hudini Example

到这里,还有一个小问题,可以看到,在消失的过程中,整个效果非常的闪烁!每个格子其实闪烁了很多次。

这是由于在过渡的过程中,ctx.fillStyle = "rgba(0,0,0," + (t * (Math.random() + 1)) + ")" 内的 Math.random() 每一帧都会重新被调用并且生成全新的随机值,因此整个动画过程一直在疯狂闪烁。

如何解决这个问题?在这篇文章中,我找到了一种利用伪随机,生成稳定随机函数的方法:Exploring the CSS Paint API: Image Fragmentation Effect

啥意思呢?就是我们希望每次生成的随机数都是都是一致的。其 JavaScript 代码如下:

const mask = 0xffffffff;
const seed = 30; /* update this to change the generated sequence */
let m_w = (123456789 + seed) & mask;
let m_z = (987654321 - seed) & mask; let random = function() {
m_z = (36969 * (m_z & 65535) + (m_z >>> 16)) & mask;
m_w = (18000 * (m_w & 65535) + (m_w >>> 16)) & mask;
var result = ((m_z << 16) + (m_w & 65535)) >>> 0;
result /= 4294967296;
return result;
}

我们利用上述实现的随机函数 random() 替换掉我们代码原本的 Math.random(),并且,mask 小格子的 ctx.fillStyle 函数,也稍加变化,避免每一个 mask 矩形小格子的渐隐淡出效果同时发生。

修改后的完整 JavaScript 代码如下:

registerPaint(
"maskSet",
class {
static get inputProperties() {
return ["--size-n", "--size-m", "--transition-time"];
} paint(ctx, size, properties) {
const n = properties.get("--size-n");
const m = properties.get("--size-m");
const t = properties.get("--transition-time");
const width = size.width / n;
const height = size.height / m;
const l = 10; const mask = 0xffffffff;
const seed = 100; /* update this to change the generated sequence */
let m_w = (123456789 + seed) & mask;
let m_z = (987654321 - seed) & mask; let random = function () {
m_z = (36969 * (m_z & 65535) + (m_z >>> 16)) & mask;
m_w = (18000 * (m_w & 65535) + (m_w >>> 16)) & mask;
var result = ((m_z << 16) + (m_w & 65535)) >>> 0;
result /= 4294967296;
return result;
}; for (var i = 0; i < n; i++) {
for (var j = 0; j < m; j++) {
ctx.fillStyle = 'rgba(0,0,0,'+((random()*(l-1) + 1) - (1-t)*l)+')';
ctx.fillRect(i * width, j * height, width, height);
}
}
}
}
);

还是上述的 DEMO,让我们再来看看效果,分别设置了不同数量的 mask 渐隐消失:

CodePen Demo -- CSS Hudini Example & Custom Random

Wow!修正过后的效果不再闪烁,并且消失动画也并非同时进行。在 Exploring the CSS Paint API: Image Fragmentation Effect 这篇文章中,还介绍了一些其他利用 registerPaint 实现的有趣的 mask 渐隐效果,感兴趣可以深入再看看。

这样,我们就将原本 2400 行的 CSS 代码,通过 CSS Painting API 的 registerPaint,压缩到了 50 行以内的 JavaScript 代码。

当然,CSS Houdini 的本事远不止于此,本文一直在围绕 background 描绘相关的内容进行阐述(mask 的语法也是背景 background 的一种)。在后续的文章我将继续介绍在其他属性上的应用。

兼容性如何?

那么,CSS Painting API 的兼容性到底如何呢?

CanIUse - registerPaint 数据如下(截止至 2022-11-23):

Chrome 和 Edge 基于 Chromium 内核的浏览器很早就已经支持,而主流浏览器中,Firefox 和 Safari 目前还不支持。

CSS Houdini 虽然强大,目前看来要想大规模上生产环境,仍需一段时间的等待。让我们给时间一点时间!

最后

好了,本文到此结束,希望本文对你有所帮助

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

现代 CSS 高阶技巧,像 Canvas 一样自由绘图构建样式!的更多相关文章

  1. css高級技巧

    1.鼠標顯示 a:小手cursor:pointer b:默認cursor:default c:勾選文本cursor:text d:拖動cursor:move 2.css三種佈局模型 a.流動模型(默認 ...

  2. 数独高阶技巧之八——SDC

    在本系列的第四篇“简单异数链”中,向大家介绍了XY-Wing等一系列Wing类技巧,并提到可以用(拐弯的)数组的观念来理解这些结构,经过第六篇ALS的学习之后,大家回过头再去看Wing,应该可以发现相 ...

  3. 数独高阶技巧入门之六——ALS

    ​在这个系列的第一篇(链及其简单应用)以及第四篇(简单异数链)中已经简单介绍过ALS结构的定义,即n格中存在n+1个不同的候选数 (双值格可视为特殊的ALS结构) .根据数独规则,在组成ALS的候选数 ...

  4. 数独高阶技巧入门之七——AIC & Nice Loop

    AIC(交替推理链,Alternate Inference Chain) 在简单异数链一文中我们介绍过XY-Chain技法,AIC可以看作是XY-Chain的扩展.有别于XY-Chain仅局限于双值格 ...

  5. 数独高阶技巧入门之三——Fish

    术语Fish代表了一组工作原理相同的关于特定候选数的解题技巧(Fish技巧直接产生自数独规则——每个单元内的数字都不能重复),Fish家族成员包括“体型”从小到大的X-Wing.Swordfish. ...

  6. 读《实战 GUI 产品的自动化测试》之:第四步,高阶技巧

    转自:http://www.ibm.com/developerworks/cn/rational/r-cn-guiautotesting4/ 定义测试控件库 本系列前几篇文章对 IBM 框架做了介绍, ...

  7. 高阶 CSS 技巧在复杂动效中的应用

    最近我在 CodePen 上看到了这样一个有意思的动画: 整个动画效果是在一个标签内,借助了 SVG PATH 实现.其核心在于对渐变(Gradient)的究极利用. 完整的代码你可以看看这里 -- ...

  8. Kotlin高阶函数实战

    前言 1. 高阶函数有多重要? 高阶函数,在 Kotlin 里有着举足轻重的地位.它是 Kotlin 函数式编程的基石,它是各种框架的关键元素,比如:协程,Jetpack Compose,Gradle ...

  9. 《前端之路》之 JavaScript 进阶技巧之高阶函数(下)

    目录 第二章 - 03: 前端 进阶技巧之高阶函数 一.防篡改对象 1-1:Configurable 和 Writable 1-2:Enumerable 1-3:get .set 2-1:不可扩展对象 ...

  10. JavaScript系列--JavaScript数组高阶函数reduce()方法详解及奇淫技巧

    一.前言 reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值. reduce() 可以作为一个高阶函数,用于函数的 compose. reduce()方 ...

随机推荐

  1. 组合总和 II

    组合总和 II 题目介绍 给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合. candidates ...

  2. 【JAVA】普通IO数据拷贝次数的问题探讨

    最近看到网上有些文章在讨论JAVA中普通文件IO读/写的时候经过了几次数据拷贝,如果从系统调用开始分析,以读取文件为例,数据的读取过程如下(以缓存I/O为例): 应用程序调用read函数发起系统调用, ...

  3. DeepHyperX代码理解-HamidaEtAl

    代码复现自论文<3-D Deep Learning Approach for Remote Sensing Image Classification> 先对部分基础知识做一些整理: 一.局 ...

  4. ImGUI 1.87 绘制D3D外部菜单

    ImGUI 它是与平台无关的C++轻量级跨平台图形界面库,没有任何第三方依赖,可以将ImGUI的源码直接加到项目中使用,该框架通常会配合特定的D3Dx9等图形开发工具包一起使用,ImGUI常用来实现进 ...

  5. 实时营销引擎在vivo营销自动化中的实践 | 引擎篇04

    作者:vivo 互联网服务器团队 本文是<vivo营销自动化技术解密>的第5篇文章,重点分析介绍在营销自动化业务中实时营销场景的背景价值.实时营销引擎架构以及项目开发过程中如何利用动态队列 ...

  6. Apache Dolphin Scheduler 3.0.1 发布,对核心及UI相关进行优化

    点亮 ️ Star · 照亮开源之路 GitHub:https://github.com/apache/dolphinscheduler ​ 版本发布 感谢本次的 Release Manager -- ...

  7. vue3中$attrs的变化与inheritAttrs的使用

    在vue3中的$attrs的变化 $listeners已被删除合并到$attrs中. $attrs现在包括class和style属性. 也就是说在vue3中$listeners不存在了.vue2中$l ...

  8. 12.-ORM-条件查询&查询谓词

    一.条件查询 filter(条件) 语法:MyModel.objects.filter(属性1=值1,属性2=值2) 作用:返回包含次条件的全部数据集 返回值:QuerySet容器对象,内部存放MyM ...

  9. Docker在windows系统以及Linux系统的安装

    Docker简介和安装 Docker是什么 Docker 是一个应用打包.分发.部署的工具 你也可以把它理解为一个轻量的虚拟机,它只虚拟你软件需要的运行环境,多余的一点都不要, 而普通虚拟机则是一个完 ...

  10. Git 02: git管理码云代码仓库 + IDEA集成使用git

    Git项目搭建 创建工作目录与常用指令 工作目录(WorkSpace)一般就是你希望Git帮助你管理的文件夹,可以是你项目的目录,也可以是一个空目录,建议不要有中文. 日常使用只要记住下图6个命令: ...