幾個(gè)簡(jiǎn)單的小例子手把手帶你入門webgl
大廠技術(shù) 高級(jí)前端 Node進(jìn)階
點(diǎn)擊上方 程序員成長(zhǎng)指北,關(guān)注公眾號(hào)
回復(fù)1,加入高級(jí)Node交流群
-
為什么需要有shader ? shader的作用是什么???? -
shader 中的每個(gè)參數(shù)到底是什么意思??怎么去用???
你如果會(huì)了,這篇文章你可以不用看??,不用浪費(fèi)時(shí)間,去看別的文章。如果哪里寫的有問題歡迎大家指正,我也在不斷地學(xué)習(xí)當(dāng)中。
why need shader
這里我結(jié)合自己的思考??,講講webgl的整個(gè)的一個(gè)渲染過程。
渲染管線
「Webgl」的渲染依賴底層「GPU」的渲染能力。所以「WEBGL」 渲染流程和 「GPU」 內(nèi)部的渲染管線是相符的。
「渲染管線的作用是將3D模型轉(zhuǎn)換為2維圖像。」
在早期,渲染管線是不可編程的,叫做「固定渲染管線」,工作的細(xì)節(jié)流程已經(jīng)固定,修改的話需要調(diào)整一些參數(shù)。
現(xiàn)代的 「GPU」 所包含的渲染管線為「可編程渲染管線」,可以通過編程 「GLSL 著色器語言」 來控制一些渲染階段的細(xì)節(jié)。
簡(jiǎn)單來說:就是使用「shader」,我們可以對(duì)畫布中「每個(gè)像素點(diǎn)做處理」,然后就可以生成各種酷炫的效果了。
渲染過程
渲染過程大概經(jīng)歷了下面這么多過程, 因?yàn)楸酒恼碌闹攸c(diǎn)其實(shí)是在著色器,所以我重點(diǎn)分析從「頂點(diǎn)著色器」—— 「片元著色器」的一個(gè)過程
-
「頂點(diǎn)著色器」 -
「圖片裝配」 -
「光柵化」 -
「片元著色器」 -
「逐片段操作(本文不會(huì)分享此內(nèi)容)」 -
「裁剪測(cè)試」 -
「多重采樣操作」 -
「背面剔除」 -
「模板測(cè)試」 -
「深度測(cè)試」 -
「融合」 -
「緩存」
頂點(diǎn)著色器
WebGL就是和GPU打交道,在GPU上運(yùn)行的代碼是一對(duì)著色器,一個(gè)是頂點(diǎn)著色器,另一個(gè)是片元著色器。每次調(diào)用著色程序都會(huì)先執(zhí)行頂點(diǎn)著色器,再執(zhí)行片元著色器。
一個(gè)頂點(diǎn)著色器的工作是生成裁剪空間坐標(biāo)值,通常是以下的形式:
const vertexShaderSource = `
attribute vec3 position;
void main() {
gl_Position = vec4(position,1);
}
`
每個(gè)頂點(diǎn)調(diào)用一次(頂點(diǎn))著色器,每次調(diào)用都需要設(shè)置一個(gè)特殊的全局變量 「gl_Position」。該變量的值就是裁減空間坐標(biāo)值。這里有同學(xué)就問了, 什么是「裁剪空間的坐標(biāo)值」???
其實(shí)我之前有講過,我在講一遍。
何為裁剪空間坐標(biāo)?就是無論你的畫布有多大,裁剪坐標(biāo)的坐標(biāo)范圍永遠(yuǎn)是 -1 到 1 。
看下面這張圖:
如果運(yùn)行一次頂點(diǎn)著色器, 那么gl_Position 就是**(-0.5,-0.5,0,1)** 記住他永遠(yuǎn)是個(gè) 「Vec4」, 簡(jiǎn)單理解就是對(duì)應(yīng)「x、y、z、w」。即使你沒用其他的,也要設(shè)置默認(rèn)值, 這就是所謂的 3維模型轉(zhuǎn)換到我們屏幕中。
頂點(diǎn)著色器需要的數(shù)據(jù),可以通過以下四種方式獲得。
-
attributes 屬性(從緩沖讀取數(shù)據(jù)) -
uniforms 全局變量 (一般用來對(duì)物體做整體變化、 旋轉(zhuǎn)、縮放) -
textures 紋理(從像素或者紋理獲得數(shù)據(jù)) -
varyings 變量 (將頂點(diǎn)著色器的變量 傳給 片元著色器)
Attributes 屬性
屬性可以用 float, vec2, vec3, vec4, mat2, mat3 和 mat4 數(shù)據(jù)類型
所以它內(nèi)建的數(shù)據(jù)類型例如vec2, vec3和 vec4分別代表兩個(gè)值,三個(gè)值和四個(gè)值, 類似的還有mat2, mat3 和 mat4 分別代表 2x2, 3x3 和 4x4 矩陣。你可以做一些運(yùn)算例如常量和矢量的乘法??磶讉€(gè)例子吧:
vec4 a = vec4(1, 2, 3, 4);
vec4 b = a * 2.0;
// b 現(xiàn)在是 vec4(2, 4, 6, 8);
向量乘法 和矩陣乘法 :
mat4 a = ???
mat4 b = ???
mat4 c = a * b;
vec4 v = ???
vec4 y = c * v;
它還支持矢量「調(diào)制」,意味者你可以交換或重復(fù)分量。
v.yyyy === vec4(y, y, y,y )
v.bgra === vec4(v.b,v.g,v.r,v.a)
vec4(v.rgb, 1) === vec4(v.r, v.g, v.b, 1)
vec4(1) === vec4(1, 1, 1, 1)
這樣你在處理圖片的時(shí)候可以輕松進(jìn)「行 顏色通道 對(duì)調(diào)」, 發(fā)現(xiàn)你可以實(shí)現(xiàn)各種各樣的濾鏡了。
后面的屬性在下面實(shí)戰(zhàn)中會(huì)講解:我們接著往下走:
圖元裝配和光柵化
「什么是圖元?」
?「描述各種圖形元素的函數(shù)叫做圖元,描述幾何元素的稱為幾何圖元(點(diǎn),線段或多邊形)。點(diǎn)和線是最簡(jiǎn)單的幾何圖元」經(jīng)過頂點(diǎn)著色器計(jì)算之后的坐標(biāo)會(huì)被組裝成「組合圖元」。
?
「通俗解釋」:「圖元就是一個(gè)點(diǎn)、一條線段、或者是一個(gè)多邊形?!?/strong>
「什么是圖元裝配呢?」
「簡(jiǎn)單理解就是說將我們?cè)O(shè)置的頂點(diǎn)、顏色、紋理等內(nèi)容組裝稱為一個(gè)可渲染的多邊形的過程?!?/strong>
組裝的類型取決于:你最后繪制選擇的圖形類型
gl.drawArrays(gl.TRIANGLES, 0, 3)
「如果是三角形的話,頂點(diǎn)著色器就執(zhí)行三次」
光柵化
「什么是光柵化:」
通過圖元裝配生成的多邊形,計(jì)算像素并填充,「剔除」不可見的部分,「剪裁」掉不在可視范圍內(nèi)的部分。最終生成可見的帶有顏色數(shù)據(jù)的圖形并繪制。
「光柵化流程圖解:」
剔除和剪裁
-
「剔除」:
在日常生活中,對(duì)于不透明物體,背面對(duì)于觀察者來說是不可見的。同樣,在「webgl」中,我們也可以設(shè)定物體的背面不可見,那么在渲染過程中,就會(huì)將不可見的部分剔除,不參與繪制。節(jié)省渲染開銷。
-
「剪裁」:
日常生活中不論是在看電視還是觀察物體,都會(huì)有一個(gè)可視范圍,在可視范圍之外的事物我們是看不到的。類似的,圖形生成后,有的部分可能位于可視范圍之外,這一部分會(huì)被剪裁掉,不參與繪制。以此來提高性能。這個(gè)就是「視椎體」, 在??范圍內(nèi)能看到的東西,才進(jìn)行繪制。
片元著色器
「光珊化后,每一個(gè)像素點(diǎn)都包含了 顏色 、深度 、紋理數(shù)據(jù), 這個(gè)我們叫做片元」
?小tips :每個(gè)像素的顏色由片元著色器的「gl_FragColor」提供
?
接收光柵化階段生成的片元,在光柵化階段中,已經(jīng)計(jì)算出每個(gè)片元的顏色信息,這一階段會(huì)將片元做逐片元挑選的操作,處理過的片元會(huì)繼續(xù)向后面的階段傳遞。「片元著色器運(yùn)行的次數(shù)由圖形有多少個(gè)片元決定的」。
「逐片元挑選」
通過模板測(cè)試和深度測(cè)試來確定片元是否要顯示,測(cè)試過程中會(huì)丟棄掉部分無用的片元內(nèi)容,然后生成可繪制的二維圖像繪制并顯示。
-
**深度測(cè)試:**就是對(duì) 「z」 軸的值做測(cè)試,值比較小的片元內(nèi)容會(huì)覆蓋值比較大的。(類似于近處的物體會(huì)遮擋遠(yuǎn)處物體)。 -
**模板測(cè)試:**模擬觀察者的觀察行為,可以接為鏡像觀察。標(biāo)記所有鏡像中出現(xiàn)的片元,最后只繪制有標(biāo)記的內(nèi)容。
實(shí)戰(zhàn)——繪制個(gè)三角形
在進(jìn)行實(shí)戰(zhàn)之前,我們先給你看一張圖,讓你能大概了解,用原生webgl生成一個(gè)三角形需要那些步驟:
我們就跟著這個(gè)流程圖一步一步去操作:
初始化canvas
新建一個(gè)webgl畫布
<canvas id="webgl" width="500" height="500"></canvas>
創(chuàng)建webgl 上下文:
const gl = document.getElementById('webgl').getContext('webgl')
創(chuàng)建著色器程序
著色器的程序這些代碼,其實(shí)是重復(fù)的,我們還是先看下圖,看下我們到底需要哪些步驟:
那我們就跟著這個(gè)流程圖:一步一步來好吧。
創(chuàng)建著色器
const vertexShader = gl.createShader(gl.VERTEX_SHADER)
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
gl.VERTEX_SHADER 和 gl.FRAGMENT_SHADER 這兩個(gè)是全局變量 分別表示「頂點(diǎn)著色器」 和「片元著色器」
綁定數(shù)據(jù)源
顧名思義:數(shù)據(jù)源,也就是我們的著色器 代碼。
編寫著色器代碼有很多種方式:
-
用 script 標(biāo)簽 type notjs 這樣去寫 -
模板字符串 (比較喜歡推薦這種)
我們先寫頂點(diǎn)著色器:
const vertexShaderSource = `
attribute vec4 a_position;
void main() {
gl_Position = a_position;
}
`
頂點(diǎn)著色器 必須要有 main 函數(shù) ,他是強(qiáng)類型語言, 「記得加分號(hào)哇」 不是js 兄弟們。我這段著色器代碼非常簡(jiǎn)單 定義一個(gè)vec4 的頂點(diǎn)位置, 然后傳給 gl_Position
這里有小伙伴會(huì)問 ?這里「a_position」一定要這么搞??
這里其實(shí)是這樣的哇, 就是我們一般進(jìn)行變量命名的時(shí)候 都會(huì)增加帶有關(guān)鍵詞的前綴 用來區(qū)分每個(gè)變量的名字 他是屬性 還是 全局變量 還是紋理 比如這樣:
uniform mat4 u_mat;
表示個(gè)矩陣,如果不這樣也可以哈。但是要專業(yè)唄,防止bug 影響。
我們接著寫片元著色器:
const fragmentShaderSource = `
void main() {
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`
這個(gè)其實(shí)理解起來非常簡(jiǎn)單哈, 每個(gè)像素點(diǎn)的顏色 是紅色 , gl_FragColor 其實(shí)對(duì)應(yīng)的是 「rgba」 也就是顏色的表示。
有了數(shù)據(jù)源之后開始綁定:
// 創(chuàng)建著色器
const vertexShader = gl.createShader(gl.VERTEX_SHADER)
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
//綁定數(shù)據(jù)源
gl.shaderSource(vertexShader, vertexShaderSource)
gl.shaderSource(fragmentShader, fragmentShaderSource)
是不是很簡(jiǎn)單哈哈哈哈,我覺得你應(yīng)該會(huì)了。
后面著色器的一些操作
其實(shí)后面「編譯著色器」、「綁定著色器」、「連接著色器程序」、「使用著色器程序」 都是一個(gè)api 搞定的事不多說了 直接看代碼:
// 編譯著色器
gl.compileShader(vertexShader)
gl.compileShader(fragmentShader)
// 創(chuàng)建著色器程序
const program = gl.createProgram()
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)
// 鏈接 并使用著色器
gl.linkProgram(program)
gl.useProgram(program)
這樣我們就創(chuàng)建好了一個(gè)著色器程序了。
這里又有人問,我怎么知道我創(chuàng)建的著色器是對(duì)的還是錯(cuò)的呢?我就是很粗心的人呢???好的他來了 如何調(diào)試:
const success = gl.getProgramParameter(program, gl.LINK_STATUS)
if (success) {
gl.useProgram(program)
return program
}
console.error(gl.getProgramInfoLog(program), 'test---')
gl.deleteProgram(program)
「getProgramParameter」 這個(gè)方法用來判斷 我們著色器 「glsl」 語言寫的是不是對(duì)的, 然后你可以通過 「getProgramInfoLog」這個(gè)方法 類似于打 日志 去發(fā)現(xiàn)?了。
數(shù)據(jù)存入緩沖區(qū)
有了著色器,現(xiàn)在我們差的就是數(shù)據(jù)了對(duì)吧。
上文在寫頂點(diǎn)著色器的時(shí)候用到了Attributes屬性,說明是「這個(gè)變量要從緩沖中讀取數(shù)據(jù)」,下面我們就來把數(shù)據(jù)存入緩沖中。
首先創(chuàng)建一個(gè)頂點(diǎn)緩沖區(qū)對(duì)象(Vertex Buffer Object, VBO)
const buffer = gl.createBuffer()
gl.createBuffer()函數(shù)創(chuàng)建緩沖區(qū)并返回一個(gè)標(biāo)識(shí)符,接下來需要為WebGL綁定這個(gè)buffer
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bindBuffer()函數(shù)把標(biāo)識(shí)符buffer設(shè)置為「當(dāng)前緩沖區(qū)」,后面的所有的數(shù)據(jù)都會(huì)都會(huì)被放入當(dāng)前緩沖區(qū),「直到bindBuffer綁定另一個(gè)當(dāng)前緩沖區(qū)」。
我們新建一個(gè)數(shù)組 然后并把數(shù)據(jù)存入到緩沖區(qū)中。
const data = new Float32Array([0.0, 0.0, -0.3, -0.3, 0.3, -0.3])
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW)
因?yàn)?strong style="color: rgb(57, 144, 3);">「JavaScript與WebGL通信必須是二進(jìn)制的」,不能是傳統(tǒng)的文本格式,所以這里使用了ArrayBuffer對(duì)象將數(shù)據(jù)轉(zhuǎn)化為二進(jìn)制,因?yàn)轫旤c(diǎn)數(shù)據(jù)是浮點(diǎn)數(shù),精度不需要太高,所以使用Float32Array就可以了,這是JavaScript與GPU之間大量實(shí)時(shí)交換數(shù)據(jù)的有效方法。
「gl.STATIC_DRAW」 指定數(shù)據(jù)存儲(chǔ)區(qū)的使用方法:緩存區(qū)的內(nèi)容可能會(huì)經(jīng)常使用,但是不會(huì)更改
「gl.DYNAMIC_DRAW」 表示 緩存區(qū)的內(nèi)容經(jīng)常使用,也會(huì)經(jīng)常更改。
「gl.STREAM_DRAW」 表示緩沖區(qū)的內(nèi)容可能不會(huì)經(jīng)常使用
從緩沖中讀取數(shù)據(jù)
「GLSL」著色程序的唯一輸入是一個(gè)屬性值「a_position」。我們要做的第一件事就是從剛才創(chuàng)建的GLSL著色程序中找到這個(gè)屬性值所在的位置。
const aposlocation = gl.getAttribLocation(program, 'a_position')
接下來我們需要告訴「WebGL」怎么從我們之前準(zhǔn)備的緩沖中獲取數(shù)據(jù)給著色器中的屬性。首先我們需要啟用對(duì)應(yīng)屬性
gl.enableVertexAttribArray(aposlocation)
最后是從緩沖中讀取數(shù)據(jù)綁定給被激活的「aposlocation」的位置
gl.vertexAttribPointer(aposlocation, 2, gl.FLOAT, false, 0, 0)
gl.vertexAttribPointer()函數(shù)有六個(gè)參數(shù):
-
讀取的數(shù)據(jù)要綁定到哪 -
表示每次從緩存取幾個(gè)數(shù)據(jù),也可以表示每個(gè)頂點(diǎn)有幾個(gè)單位的數(shù)據(jù),取值范圍是1-4。這里每次取2個(gè)數(shù)據(jù),之前vertices聲明的6個(gè)數(shù)據(jù),正好是3個(gè)頂點(diǎn)的二維坐標(biāo)。 -
表示數(shù)據(jù)類型,可選參數(shù)有g(shù)l.BYTE有符號(hào)的8位整數(shù),gl.SHORT有符號(hào)的16位整數(shù),gl.UNSIGNED_BYTE無符號(hào)的8位整數(shù),gl.UNSIGNED_SHORT無符號(hào)的16位整數(shù),gl.FLOAT32位IEEE標(biāo)準(zhǔn)的浮點(diǎn)數(shù)。 -
表示是否應(yīng)該將整數(shù)數(shù)值歸一化到特定的范圍,對(duì)于類型gl.FLOAT此參數(shù)無效。 -
表示每次取數(shù)據(jù)與上次隔了多少位,0表示每次取數(shù)據(jù)連續(xù)緊挨上次數(shù)據(jù)的位置,WebGL會(huì)自己計(jì)算之間的間隔。 -
表示首次取數(shù)據(jù)時(shí)的偏移量,必須是字節(jié)大小的倍數(shù)。0表示從頭開始取。
渲染
現(xiàn)在著色器程序 和數(shù)據(jù)都已經(jīng)ready 了, 現(xiàn)在就差渲染了。渲染之前和2d canvas 一樣做一個(gè)清除畫布的動(dòng)作:
// 清除canvas
gl.clearColor(0, 0, 0, 0)
gl.clear(gl.COLOR_BUFFER_BIT)
我們用「0、0、0、0」清空畫布,分別對(duì)應(yīng) 「r, g, b, alpha (紅,綠,藍(lán),阿爾法」)值, 所以在這個(gè)例子中我們讓畫布變透明了。
開啟繪制三角形:
gl.drawArrays(gl.TRIANGLES, 0, 3)
-
「第一個(gè)參數(shù)表示繪制的類型」 -
「第二個(gè)參數(shù)表示從第幾個(gè)頂點(diǎn)開始繪制」 -
「第三個(gè)參數(shù)表示繪制多少個(gè)點(diǎn),緩沖中一共6個(gè)數(shù)據(jù),每次取2個(gè),共3個(gè)點(diǎn)」
「繪制類型共有下列幾種」 「看圖:」
這里我們看下畫面是不是一個(gè)紅色的三角形 :
我們創(chuàng)建的數(shù)據(jù)是這樣的:
「畫布的寬度是 500 * 500 轉(zhuǎn)換出來的實(shí)際數(shù)據(jù)其實(shí)是這樣的」
0,0 ====> 0,0
-0.3, -0.3 ====> 175, 325
0.3, -0.3 ====> 325, 325
矩陣的使用
有了靜態(tài)的圖形我們開始著色器,對(duì)三角形做一個(gè)縮放。
改寫頂點(diǎn)著色器:其實(shí)在頂點(diǎn)著色器上加一個(gè)全局變量 這就用到了 著色器的第二個(gè)屬性 uniform
const vertexShaderSource = `
attribute vec4 a_position;
// 添加矩陣代碼
uniform mat4 u_mat;
void main() {
gl_Position = u_mat * a_position;
}
`
然后和屬性一樣,我們需要找到 uniform 對(duì)應(yīng)的位置:
const matlocation = gl.getUniformLocation(program, 'u_mat')
然后初始化一個(gè)縮放舉證:
// 初始化一個(gè)旋轉(zhuǎn)矩陣。
const mat = new Float32Array([
Tx, 0.0, 0.0, 0.0,
0.0, Ty, 0.0, 0.0,
0.0, 0.0, Tz, 0.0,
0.0, 0.0, 0.0, 1.0,
]);
Tx, Ty, Tz 對(duì)應(yīng)的其實(shí)就是 x y z 軸縮放的比例。
最后一步, 將矩陣應(yīng)用到著色器上, 在畫之前, 這樣每個(gè)點(diǎn) 就可以?? 這個(gè)縮放矩陣了 ,所以整體圖形 也就進(jìn)行了縮放。
gl.uniformMatrix4fv(matlocation, false, mat)
三個(gè)參數(shù)分別代表什么意思:
-
全局變量的位置 -
是否為轉(zhuǎn)置矩陣 -
矩陣數(shù)據(jù)
OK 我寫了三角形縮放的動(dòng)畫:
let Tx = 0.1 //x坐標(biāo)的位置
let Ty = 0.1 //y坐標(biāo)的位置
let Tz = 1.0 //z坐標(biāo)的位置
let Tw = 1.0 //差值
let isOver = true
let step = 0.08
function run() {
if (Tx >= 3) {
isOver = false
}
if (Tx <= 0) {
isOver = true
}
if (isOver) {
Tx += step
Ty += step
} else {
Tx -= step
Ty -= step
}
const mat = new Float32Array([
Tx, 0.0, 0.0, 0.0,
0.0, Ty, 0.0, 0.0,
0.0, 0.0, Tz, 0.0,
0.0, 0.0, 0.0, 1.0,
]);
gl.uniformMatrix4fv(matlocation, false, mat)
gl.drawArrays(gl.TRIANGLES, 0, 3)
// 使用此方法實(shí)現(xiàn)一個(gè)動(dòng)畫
requestAnimationFrame(run)
}
效果圖如下:
最后 給大家看一下webgl 內(nèi)部是怎么搞的 一張gif 動(dòng)畫 :
原始的數(shù)據(jù)通過 頂點(diǎn)著色器 生成一系列 新的點(diǎn)。
變量的使用
說完矩陣了下面??,我們開始說下著色器中的varying 這個(gè)變量 是如何和片元著色器進(jìn)行聯(lián)動(dòng)的。
我們還是繼續(xù)改造頂點(diǎn)著色器:
const vertexShaderSource = `
attribute vec4 a_position;
uniform mat4 u_mat;
// 變量
varying vec4 v_color;
void main() {
gl_Position = u_mat * a_position;
v_color = gl_Position * 0.5 + 0.5;
}
`
這里有一個(gè)小知識(shí) , gl_Position 他的值范圍是 「-1 -1」 但是片元著色 他是顏色 他的范圍是 「0 - 1」 , 所以呢這時(shí)候呢,我們就要 做一個(gè)范圍轉(zhuǎn)換 所以為什么要 乘 0.5 在加上 0.5 了, 希望你們明白。
改造下片元著色器:
const fragmentShaderSource = `
precision lowp float;
varying vec4 v_color;
void main() {
gl_FragColor = v_color;
}
`
只要沒一個(gè)像素點(diǎn) 改為由頂點(diǎn)著色器傳過來的就好了。
我們看下這時(shí)候的三角形 變成啥樣子了。
是不是變成彩色三角形了, 這里很多人就會(huì)問, 這到底是怎么形成呢, 本質(zhì)是在三角形的三個(gè)頂點(diǎn), 做線性插值的過程:

本篇文章大概是對(duì)webgl 做了一個(gè)基本的介紹, 和帶你用幾個(gè)簡(jiǎn)單的小例子 帶你入門了glsl 語言, 你以為webgl 就這樣嘛 那你就錯(cuò)了,其實(shí)有一個(gè)texture 我是沒有講的, 后面我去專門寫一篇文章去將紋理貼圖 , 漫反射貼圖、 法線貼圖。希望你關(guān)注下我,不然找不到我了, 如果你覺得本篇文章對(duì)你有幫助的話,歡迎 點(diǎn)贊 、再看、收藏。我們下期再見??, 我是喜歡「圖形的Fly」。
我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對(duì)Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。

“分享、點(diǎn)贊、在看” 支持一波
