本文將指導您使用 K8S
,Docker
,Yarn workspace
,TypeScript
,esbuild
,Express
和 React
來設定構建一個基本的雲原生 Web
應用程式。 在本教程的最後,您將擁有一個可完全構建和部署在 K8S
上的 Web
應用程式。
設定專案
該專案將被構造為 monorepo
。 monorepo
的目標是提高模組之間共享的程式碼量,並更好地預測這些模組如何一起通訊(例如在微服務架構中)。出於本練習的目的,我們將使結構保持簡單:
app
,它將代表我們的React website
。server
,它將使用Express
服務我們的app
。common
,其中一些程式碼將在app
和server
之間共享。
設定專案之前的唯一要求是在機器上安裝 yarn
。 Yarn
與 npm
一樣,是一個程式包管理器,但效能更好,功能也略多。 您可以在官方文件中閱讀有關如何安裝它的更多資訊。
Workspaces(工作區)
進入到要初始化專案的資料夾,然後通過您喜歡的終端執行以下步驟:
- 使用
mkdir my-app
建立專案的資料夾(可以自由選擇所需的名稱)。 - 使用
cd my-app
進入資料夾。 - 使用
yarn init
初始化它。這將提示您建立初始package.json
檔案的相關問題(不用擔心,一旦建立檔案,您可以隨時對其進行修改)。如果您不想使用yarn init
命令,則始終可以手動建立檔案,並將以下內容複製到其中:
{
"name": "my-app",
"version": "1.0.0",
"license": "UNLICENSED",
"private": true // Required for yarn workspace to work
}
現在,已經建立了 package.json
檔案,我們需要為我們的模組app
,common
和 server
建立資料夾。 為了方便 yarn workspace
發現模組並提高專案的可讀性(readability
),我們將模組巢狀在 packages
資料夾下:
my-app/
├─ packages/ // 我們當前和將來的所有模組都將存在的地方
│ ├─ app/
│ ├─ common/
│ ├─ server/
├─ package.json
我們的每個模組都將充當一個小型且獨立的專案,並且需要其自己的 package.json
來管理依賴項。要設定它們中的每一個,我們既可以使用 yarn init
(在每個資料夾中),也可以手動建立檔案(例如,通過 IDE
)。
軟體包名稱使用的命名約定是在每個軟體包之前都使用 @my-app/*
作為字首。這在 NPM
領域中稱為作用域(您可以在此處閱讀更多內容)。您不必像這樣給自己加上字首,但以後會有所幫助。
一旦建立並初始化了所有三個軟體包,您將具有如下所示的相似之處。
app
包:
{
"name": "@my-app/app",
"version": "0.1.0",
"license": "UNLICENSED",
"private": true
}
common
包:
{
"name": "@my-app/common",
"version": "0.1.0",
"license": "UNLICENSED",
"private": true
}
server
包:
{
"name": "@my-app/server",
"version": "0.1.0",
"license": "UNLICENSED",
"private": true
}
最後,我們需要告訴 yarn
在哪裡尋找模組,所以回去編輯專案的 package.json
檔案並新增以下 workspaces
屬性(如果您想了解更多有關詳細資訊,請檢視 Yarn
的 workspaces 文件)。
{
"name": "my-app",
"version": "1.0",
"license": "UNLICENSED",
"private": true,
"workspaces": ["packages/*"] // 在這裡新增
}
您的最終資料夾結構應如下所示:
my-app/
├─ packages/
│ ├─ app/
│ │ ├─ package.json
│ ├─ common/
│ │ ├─ package.json
│ ├─ server/
│ │ ├─ package.json
├─ package.json
現在,您已經完成了專案的基礎設定。
TypeScript
現在,我們將第一個依賴項新增到我們的專案:TypeScript
。TypeScript
是 JavaScript
的超集,可在構建時實現型別檢查。
通過終端進入專案的根目錄,執行 yarn add -D -W typescript
。
- 引數
-D
將TypeScript
新增到devDependencies
,因為我們僅在開發和構建期間使用它。 - 引數
-W
允許在工作空間根目錄中安裝一個包,使其在app
、common
和server
上全域性可用。
您的 package.json
應該如下所示:
{
"name": "my-app",
"version": "1.0",
"license": "UNLICENSED",
"private": true,
"workspaces": ["packages/*"],
"devDependencies": {
"typescript": "^4.2.3"
}
}
這還將建立一個 yarn.lock
檔案(該檔案確保在專案的整個生命週期中依賴項的預期版本保持不變)和一個 node_modules
資料夾,該資料夾儲存依賴項的 binaries
。
現在我們已經安裝了 TypeScript
,一個好習慣是告訴它如何執行。為此,我們將新增一個配置檔案,該檔案應由您的 IDE
拾取(如果使用 VSCode
,則會自動獲取)。
在專案的根目錄下建立一個 tsconfig.json
檔案,並將以下內容複製到其中:
{
"compilerOptions": {
/* Basic */
"target": "es2017",
"module": "CommonJS",
"lib": ["ESNext", "DOM"],
/* Modules Resolution */
"moduleResolution": "node",
"esModuleInterop": true,
/* Paths Resolution */
"baseUrl": "./",
"paths": {
"@flipcards/*": ["packages/*"]
},
/* Advanced */
"jsx": "react",
"experimentalDecorators": true,
"resolveJsonModule": true
},
"exclude": ["node_modules", "**/node_modules/*", "dist"]
}
您可以輕鬆地搜尋每個 compileoptions
屬性及其操作,但對我們最有用的是 paths
屬性。例如,這告訴 TypeScript
在 @my-app/server
或 @my-app/app
包中使用 @my-app/common
匯入時在哪裡查詢程式碼和 typings
。
您當前的專案結構現在應如下所示:
my-app/
├─ node_modules/
├─ packages/
│ ├─ app/
│ │ ├─ package.json
│ ├─ common/
│ │ ├─ package.json
│ ├─ server/
│ │ ├─ package.json
├─ package.json
├─ tsconfig.json
├─ yarn.lock
新增第一個 script
Yarn workspace
允許我們通過 yarn workspace @my-app/*
命令模式訪問任何子包,但是每次鍵入完整的命令將變得非常多餘。為此,我們可以建立一些 helper script
方法來提升開發體驗。開啟專案根目錄下的 package.json
,並向其新增以下 scripts
屬性。
{
"name": "my-app",
"version": "1.0",
"license": "UNLICENSED",
"private": true,
"workspaces": ["packages/*"],
"devDependencies": {
"typescript": "^4.2.3"
},
"scripts": {
"app": "yarn workspace @my-app/app",
"common": "yarn workspace @my-app/common",
"server": "yarn workspace @my-app/server"
}
}
現在可以像在子包中一樣執行任何命令。例如,您可以通過鍵入 yarn server add express
來新增一些新的依賴項。這將直接向 server
包新增新的依賴項。
在後續部分中,我們將開始構建前端和後端應用程式。
準備 Git
如果計劃使用 Git
作為版本控制工具,強烈建議忽略生成的檔案,例如二進位制檔案或日誌。
為此,請在專案的根目錄下建立一個名為 .gitignore
的新檔案,並將以下內容複製到其中。這將忽略本教程稍後將生成的一些檔案,並避擴音交大量不必要的資料。
# Logs
yarn-debug.log*
yarn-error.log*
# Binaries
node_modules/
# Builds
dist/
**/public/script.js
資料夾結構應如下所示:
my-app/
├─ packages/
├─ .gitignore
├─ package.json
新增程式碼
這部分將著重於將程式碼新增到我們的 common
、app
和 server
包中。
Common
我們將從 common
開始,因為此包將由 app
和 server
使用。它的目標是提供共享的邏輯(shared logic
)和變數(variables
)。
檔案
在本教程中,common
軟體包將非常簡單。首先,從新增新資料夾開始:
src/
資料夾,包含包的程式碼。
建立此資料夾後,將以下檔案新增到其中:
src/index.ts
export const APP_TITLE = 'my-app';
現在我們有一些要匯出的程式碼,我們想告訴 TypeScript
從其他包中匯入它時在哪裡尋找它。為此,我們將需要更新 package.json
檔案:
package.json
{
"name": "@my-app/common",
"version": "0.1.0",
"license": "UNLICENSED",
"private": true,
"main": "./src/index.ts" // 新增這一行來為 TS 提供入口點
}
我們現在已經完成了 common
包!
結構提醒:
common/
├─ src/
│ ├─ index.ts
├─ package.json
App
依賴項
該 app
包將需要以下依賴項:
從專案的根目錄執行:
yarn app add react react-dom
yarn app add -D @types/react @types/react-dom
(為TypeScript
新增型別typings
)
package.json
{
"name": "@my-app/app",
"version": "0.1.0",
"license": "UNLICENSED",
"private": true,
"dependencies": {
"@my-app/common": "^0.1.0", // Notice that we've added this import manually
"react": "^17.0.1",
"react-dom": "^17.0.1"
},
"devDependencies": {
"@types/react": "^17.0.3",
"@types/react-dom": "^17.0.2"
}
}
檔案
要建立我們的 React
應用程式,我們將需要新增兩個新資料夾:
- 一個
public/
資料夾,它將儲存基本HTML
頁面和我們的assets
。 - 一個
src/
資料夾,其中包含我們應用程式的程式碼。
一旦建立了這兩個資料夾,我們就可以開始新增 HTML
檔案,該檔案將成為我們應用程式的宿主。
public/index.html
<!DOCTYPE html>
<html>
<head>
<title>my-app</title>
<meta name="description" content="Welcome on my application!" />
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<!-- 這個 div 是我們將注入 React 應用程式的地方 -->
<div id="root"></div>
<!-- 這是包含我們的應用程式的指令碼的路徑 -->
<script src="script.js"></script>
</body>
</html>
現在我們有了要渲染的頁面,我們可以通過新增下面的兩個檔案來實現非常基本但功能齊全的 React
應用程式。
src/index.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { App } from './App';
ReactDOM.render(<App />, document.getElementById('root'));
此程式碼從我們的 HTML
檔案掛接到 root div
中,並將 React元件樹
注入其中。
src/App.tsx
import { APP_TITLE } from '@flipcards/common';
import * as React from 'react';
export function App(): React.ReactElement {
const [count, setCount] = React.useState(0);
return (
<div>
<h1>Welcome on {APP_TITLE}!</h1>
<p>
This is the main page of our application where you can confirm that it
is dynamic by clicking the button below.
</p>
<p>Current count: {count}</p>
<button onClick={() => setCount((prev) => prev + 1)}>Increment</button>
</div>
);
}
這個簡單的 App
元件將呈現我們的應用標題和動態計數器。這將是我們的 React tree
的入口點。隨意新增您想要的任何程式碼。
就是這樣!我們已經完成了非常基本的 React
應用程式。目前它並沒有太大的作用,但是我們總是可以稍後再使用它並新增更多功能。
結構提醒:
app/
├─ public/
│ ├─ index.html
├─ src/
│ ├─ App.tsx
│ ├─ index.tsx
├─ package.json
Server
依賴項
server
軟體包將需要以下依賴項:
從專案的根目錄執行:
yarn server add cors express
yarn server add -D @types/cors @types/express
(為TypeScript
新增型別typings
)
package.json
{
"name": "@my-app/server",
"version": "0.1.0",
"license": "UNLICENSED",
"private": true,
"dependencies": {
"@my-app/common": "^0.1.0", // 請注意,我們已手動新增了此匯入
"cors": "^2.8.5",
"express": "^4.17.1"
},
"devDependencies": {
"@types/cors": "^2.8.10",
"@types/express": "^4.17.11"
}
}
檔案
現在我們的 React
應用程式已經準備就緒,我們需要的最後一部分是伺服器來為其提供服務。首先為其建立以下資料夾:
- 一個
src/
資料夾,包含我們伺服器的程式碼。
接下來,新增 server
的主檔案:
src/index.ts
import { APP_TITLE } from '@flipcards/common';
import cors from 'cors';
import express from 'express';
import { join } from 'path';
const PORT = 3000;
const app = express();
app.use(cors());
// 服務來自 "public" 資料夾的靜態資源(例如:當有影像要顯示時)
app.use(express.static(join(__dirname, '../../app/public')));
// 為 HTML 頁面提供服務
app.get('*', (req: any, res: any) => {
res.sendFile(join(__dirname, '../../app/public', 'index.html'));
});
app.listen(PORT, () => {
console.log(`${APP_TITLE}'s server listening at http://localhost:${PORT}`);
});
這是一個非常基本的 Express
應用程式,但如果除了單頁應用程式之外我們沒有任何其他服務,那麼這就足夠了。
結構提醒:
server/
├─ src/
│ ├─ index.ts
├─ package.json
構建應用
Bundlers(打包構建捆綁器)
為了將 TypeScript
程式碼轉換為可解釋的 JavaScript
程式碼,並將所有外部庫打包到單個檔案中,我們將使用打包工具。JS/TS
生態系統中有許多捆綁器,如 WebPack、Parcel 或 Rollup,但我們將選擇 esbuild。與其他捆綁器相比,esbuild
自帶了許多預設載入的特性(TypeScript
, React
),並有巨大的效能提升(快了 100
倍)。如果你有興趣瞭解更多,請花時間閱讀作者的常見問題解答。
這些指令碼將需要以下依賴項:
從專案的根目錄執行:yarn add -D -W esbuild ts-node
。
package.json
{
"name": "my-app",
"version": "1.0",
"license": "UNLICENSED",
"private": true,
"workspaces": ["packages/*"],
"devDependencies": {
"esbuild": "^0.9.6",
"ts-node": "^9.1.1",
"typescript": "^4.2.3"
},
"scripts": {
"app": "yarn workspace @my-app/app",
"common": "yarn workspace @my-app/common",
"server": "yarn workspace @my-app/server"
}
}
Build(編譯構建)
現在,我們擁有構建應用程式所需的所有工具,因此讓我們建立第一個指令碼。
首先在專案的根目錄下建立一個名為 scripts/
的新資料夾。
我們的指令碼將用 TypeScript
編寫,並從命令列使用 ts-node
執行。儘管存在用於 esbuild
的 CLI
,但是如果您要傳遞更復雜的引數或將多個工作流組合在一起,則可以通過 JS
或 TS
使用該庫,這更加方便。
在 scripts/
資料夾中建立一個 build.ts
檔案,並在下面新增程式碼(我將通過註釋解釋程式碼的作用):
scripts/build.ts
import { build } from 'esbuild';
/**
* 在構建期間傳遞的通用選項。
*/
interface BuildOptions {
env: 'production' | 'development';
}
/**
* app 包的一個構建器函式。
*/
export async function buildApp(options: BuildOptions) {
const { env } = options;
await build({
entryPoints: ['packages/app/src/index.tsx'], // 我們從這個入口點讀 React 應用程式
outfile: 'packages/app/public/script.js', // 我們在 public/ 資料夾中輸出一個檔案(請記住,在 HTML 頁面中使用了 "script.js")
define: {
'process.env.NODE_ENV': `"${env}"`, // 我們需要定義構建應用程式的 Node.js 環境
},
bundle: true,
minify: env === 'production',
sourcemap: env === 'development',
});
}
/**
* server 軟體包的構建器功能。
*/
export async function buildServer(options: BuildOptions) {
const { env } = options;
await build({
entryPoints: ['packages/server/src/index.ts'],
outfile: 'packages/server/dist/index.js',
define: {
'process.env.NODE_ENV': `"${env}"`,
},
external: ['express'], // 有些庫必須標記為外部庫
platform: 'node', // 為 Node 構建時,我們需要為其設定環境
target: 'node14.15.5',
bundle: true,
minify: env === 'production',
sourcemap: env === 'development',
});
}
/**
* 所有軟體包的構建器功能。
*/
async function buildAll() {
await Promise.all([
buildApp({
env: 'production',
}),
buildServer({
env: 'production',
}),
]);
}
// 當我們從終端使用 ts-node 執行指令碼時,將執行此方法
buildAll();
該程式碼很容易解釋,但是如果您覺得遺漏了部分,可以檢視 esbuild
的 API文件 以獲取完整的關鍵字列表。
我們的構建指令碼現已完成! 我們需要做的最後一件事是在我們的 package.json
中新增一個新命令,以方便地執行構建操作。
{
"name": "my-app",
"version": "1.0",
"license": "UNLICENSED",
"private": true,
"workspaces": ["packages/*"],
"devDependencies": {
"esbuild": "^0.9.6",
"ts-node": "^9.1.1",
"typescript": "^4.2.3"
},
"scripts": {
"app": "yarn workspace @my-app/app",
"common": "yarn workspace @my-app/common",
"server": "yarn workspace @my-app/server",
"build": "ts-node ./scripts/build.ts" // Add this line here
}
}
現在,您可以在每次對專案進行更改時從專案的根資料夾執行 yarn build
來啟動構建過程(如何新增hot-reloading
,稍後討論)。
結構提醒:
my-app/
├─ packages/
├─ scripts/
│ ├─ build.ts
├─ package.json
├─ tsconfig.json
Serve(提供服務)
我們的應用程式已經構建好並可以提供給全世界使用,我們只需要向 package.json
新增最後一個命令即可:
{
"name": "my-app",
"version": "1.0",
"license": "UNLICENSED",
"private": true,
"workspaces": ["packages/*"],
"devDependencies": {
"esbuild": "^0.9.6",
"ts-node": "^9.1.1",
"typescript": "^4.2.3"
},
"scripts": {
"app": "yarn workspace @my-app/app",
"common": "yarn workspace @my-app/common",
"server": "yarn workspace @my-app/server",
"build": "ts-node ./scripts/build.ts",
"serve": "node ./packages/server/dist/index.js" // Add this line here
}
}
由於我們現在正在處理純 JavaScript
,因此可以使用 node
二進位制檔案啟動伺服器。因此,繼續執行 yarn serve
。
如果您檢視控制檯,您將看到伺服器正在成功偵聽。你也可以開啟一個瀏覽器,導航到 http://localhost:3000 來顯示你的 React
應用?!
如果你想在執行時改變埠,你可以用一個環境變數作為字首來啟動 serve
命令: PORT=4000 yarn serve
。
Docker ?
本節將假定您已經熟悉容器的概念。
為了能夠根據我們的程式碼建立映象,我們需要在計算機上安裝 Docker
。要了解如何基於 OS
進行安裝,請花一點時間檢視官方文件
。
Dockerfile
要生成 Docker
映象,第一步是在我們專案的根目錄下建立一個 Dockerfile
(這些步驟可以完全通過 CLI
來完成,但是使用配置檔案是定義構建步驟的預設方式)。
FROM node:14.15.5-alpine
WORKDIR /usr/src/app
# 儘早安裝依賴項,以便如果我們應用程式中的
# 某些檔案發生更改,Docker無需再次下載依賴項,
# 而是從下一步(“ COPY ..”)開始。
COPY ./package.json .
COPY ./yarn.lock .
COPY ./packages/app/package.json ./packages/app/
COPY ./packages/common/package.json ./packages/common/
COPY ./packages/server/package.json ./packages/server/
RUN yarn
# 複製我們應用程式的所有檔案(.gitignore 中指定的檔案除外)
COPY . .
# 編譯 app
RUN yarn build
# Port
EXPOSE 3000
# Serve
CMD [ "yarn", "serve" ]
我將嘗試儘可能詳細地說明這裡發生的事情以及這些步驟的順序為什麼很重要:
FROM
告訴Docker
將指定的基礎映象用於當前上下文。在我們的案例中,我們希望有一個可以執行Node.js
應用程式的環境。WORKDIR
設定容器中的當前工作目錄。COPY
將檔案或資料夾從當前本地目錄(專案的根目錄)複製到容器中的工作目錄。如您所見,在此步驟中,我們僅複製與依賴項相關的檔案。這是因為Docker
將每個構建中的命令的每個結果快取為一層。因為我們要優化構建時間和頻寬,所以我們只想在依賴項發生更改(通常比檔案更改發生的頻率小)時重新安裝它們。RUN
在shell
中執行命令。EXPOSE
是用於容器的內部埠(與我們的應用程式的PORT env
無關)。 這裡的任何值都應該很好,但是如果您想了解更多資訊,可以檢視官方文件。CMD
的目的是提供執行容器的預設值。
如果您想了解更多有關這些關鍵字的資訊,可以檢視 Dockerfile參考。
新增 .dockerignore
使用 .dockerignore
檔案不是強制性的,但強烈建議您使用以下檔案:
- 確保您沒有將垃圾檔案複製到容器中。
- 使
COPY
命令的使用更加容易。
如果您已經熟悉它,它的工作原理就像 .gitignore
檔案一樣。您可以將以下內容複製到與 Dockerfile
相同級別的 .dockerignore
檔案中,該檔案將被自動提取。
README.md
# Git
.gitignore
# Logs
yarn-debug.log
yarn-error.log
# Binaries
node_modules
*/*/node_modules
# Builds
*/*/build
*/*/dist
*/*/script.js
隨意新增任何您想忽略的檔案,以減輕您的最終映象。
構建 Docker Image
現在我們的應用程式已經為 Docker
準備好了,我們需要一種從 Docker
生成實際映象的方法。為此,我們將向根 package.json
新增一個新命令:
{
"name": "my-app",
"version": "1.0.0",
"license": "MIT",
"private": true,
"workspaces": ["packages/*"],
"devDependencies": {
"esbuild": "^0.9.6",
"ts-node": "^9.1.1",
"typescript": "^4.2.3"
},
"scripts": {
"app": "yarn workspace @my-app/app",
"common": "yarn workspace @my-app/common",
"server": "yarn workspace @my-app/server",
"build": "ts-node ./scripts/build.ts",
"serve": "node ./packages/server/dist/index.js",
"docker": "docker build . -t my-app" // Add this line
}
}
docker build . -t my-app
命令告訴 docker
使用當前目錄(.
)查詢 Dockerfile
,並將生成的映象(-t
)命名為 my-app
。
確保執行了 Docker
守護程式,以便在終端中使用 docker
命令。
現在該命令已經在我們專案的指令碼中,您可以使用 yarn docker
執行它。
在執行該命令後,您應該期望看到以下終端輸出:
Sending build context to Docker daemon 76.16MB
Step 1/12 : FROM node:14.15.5-alpine
---> c1babb15a629
Step 2/12 : WORKDIR /usr/src/app
---> b593905aaca7
Step 3/12 : COPY ./package.json .
---> e0046408059c
Step 4/12 : COPY ./yarn.lock .
---> a91db028a6f9
Step 5/12 : COPY ./packages/app/package.json ./packages/app/
---> 6430ae95a2f8
Step 6/12 : COPY ./packages/common/package.json ./packages/common/
---> 75edad061864
Step 7/12 : COPY ./packages/server/package.json ./packages/server/
---> e8afa17a7645
Step 8/12 : RUN yarn
---> 2ca50e44a11a
Step 9/12 : COPY . .
---> 0642049120cf
Step 10/12 : RUN yarn build
---> Running in 15b224066078
yarn run v1.22.5
$ ts-node ./scripts/build.ts
Done in 3.51s.
Removing intermediate container 15b224066078
---> 9dce2d505c62
Step 11/12 : EXPOSE 3000
---> Running in f363ce55486b
Removing intermediate container f363ce55486b
---> 961cd1512fcf
Step 12/12 : CMD [ "yarn", "serve" ]
---> Running in 7debd7a72538
Removing intermediate container 7debd7a72538
---> df3884d6b3d6
Successfully built df3884d6b3d6
Successfully tagged my-app:latest
就是這樣!現在,我們的映象已建立並註冊在您的機器上,供 Docker
使用。 如果您希望列出可用的 Docker
映象,則可以執行 docker image ls
命令:
→ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
my-app latest df3884d6b3d6 4 minutes ago 360MB
像這樣執行命令
通過命令列執行一個可用的 Docker
映象非常簡單:docker run -d -p 3000:3000 my-app
-d
以分離模式執行容器(在後臺)。-p
設定暴露容器的埠(格式為[host port]:[container port]
)。因此,如果我們想將容器內部的埠3000
(還記得Dockerfile
中的EXPOSE
引數)暴露到容器外部的埠8000
,我們將把8000:3000
傳遞給-p
標誌。
你可以確認你的容器正在執行 docker ps
。這將列出所有正在執行的容器:
如果您對啟動容器有其他要求和疑問,請在此處找到更多資訊。
→ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
71465a89b58b my-app "docker-entrypoint.s…" 7 seconds ago Up 6 seconds 0.0.0.0:3000->3000/tcp determined_shockley
現在,開啟瀏覽器並導航到以下URL http://localhost:3000,檢視您正在執行的應用程式?!
我是為少
微信:uuhells123
公眾號:黑客下午茶
加我微信(互相學習交流),關注公眾號(獲取更多學習資料~)