🔍 搜尋結果:class

🔍 搜尋結果:class

🏆如何使用 Taipy 和 PySpark 掌握 📊 大資料管道 🐍

本文將透過一個簡單的範例來示範如何**將 PySpark 與 Taipy 整合**,以將您的 **大資料處理需求** 與 **智慧作業執行** 結合。 #### 讓我們開始吧! ![開始使用](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gyd6pq09thphujynk66n.gif) <小時/> ### 將 PySpark 與 Taipy 結合使用 Taipy 是一個**強大的工作流程編排工具**,具有**易於使用的框架**,可輕鬆應用於您現有的資料應用程式。 Taipy 建立在堅實的概念基礎上: - **場景、任務和資料節點** - 這些概念非常強大,允許開發人員**輕鬆地對其管道進行建模**,即使在沒有明確支援的情況下使用第3 方包也是如此。 <小時/> ![QueenB](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bdhmkkqpyjxko242wa2v.gif) {% cta https://github.com/Avaiga/taipy %} Star ⭐ Taipy 儲存庫 {% endcta %} 我們感謝任何幫助我們發展社區的幫助🌱 <小時/> *如果您已經熟悉 PySpark 和 Taipy,則可以跳至「2. Taipy 設定 (*config.py*)」。 *該部分深入探討了為 Taipy 任務定義函數來執行 PySpark 應用程式的本質。否則,請繼續閱讀!* <小時/> ### 一個簡單的例子:*palmerpenguins* 我們以 [palmerpenguins](https://allisonhorst.github.io/palmerpenguins/) 資料集為例: ``` >>> penguin_df ┌───────┬─────────┬───────────┬────────────────┬───────────────┬───────────────────┬─────────────┬────────┬──────┐ │ index │ species │ island │ bill_length_mm │ bill_depth_mm │ flipper_length_mm │ body_mass_g │ sex │ year │ ├───────┼─────────┼───────────┼────────────────┼───────────────┼───────────────────┼─────────────┼────────┼──────┤ │ 0 │ Adelie │ Torgersen │ 39.1 │ 18.7 │ 181.0 │ 3750.0 │ male │ 2007 │ │ 1 │ Adelie │ Torgersen │ 39.5 │ 17.4 │ 186.0 │ 3800.0 │ female │ 2007 │ │ 2 │ Adelie │ Torgersen │ 40.3 │ 18.0 │ 195.0 │ 3250.0 │ female │ 2007 │ │ 3 │ Adelie │ Torgersen │ NaN │ NaN │ NaN │ NaN │ NaN │ 2007 │ │ 4 │ Adelie │ Torgersen │ 36.7 │ 19.3 │ 193.0 │ 3450.0 │ female │ 2007 │ │ ... │ ... │ ... │ ... │ ... │ ... │ ... │ ... │ ... │ └───────┴─────────┴───────────┴────────────────┴───────────────┴───────────────────┴─────────────┴────────┴──────┘ ``` <小時/> 該資料集僅包含 344 筆記錄——幾乎不是一個需要 Spark 處理的資料集。 然而,該資料集是可存取的,且其大小與演示 Spark 與 Taipy 的整合無關。 如果必須使用更大的資料集進行測試,您可以根據需要多次複製資料。 ![DAG 應用程式](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/exxtbt00ia5y6avzcy8z.png) *我們簡單的企鵝應用程式的 DAG* <小時/> 我們將設計一個執行**兩個主要任務**的工作流程: #### 1- Spark 任務(*spark_process*): - 載入資料; - 依「*物種*」、「*島嶼*」和「*性別*」將資料分組; - 求其他欄位的平均值(「*bill_length_mm*」、「*bill_depth_mm*」、「*flipper_length_mm*」、「*body_mass_g*」); - 儲存資料。 #### 2- Python 任務(*過濾器*): - 載入Spark任務之前儲存的輸出資料; - 給定“*物種*”、“*島嶼*”和“*性別*”,傳回聚合值。 我們的小專案將包含 4 個檔案: ``` app/ ├─ penguin_spark_app.py # the spark application ├─ config.py # the configuration for our taipy workflow ├─ main.py # the main script (including our application gui) ├─ penguins.csv # the data as downloaded from the palmerpenguins git repo ``` <小時/> 您可以找到每個檔案的內容(*penguins.csv* 除外,您可以從 [palmerpenguins 儲存庫](https://github.com/allisonhorst/palmerpenguins/blob/main/inst/extdata/penguins.csv 取得) )在本文的程式碼區塊中。 <小時/> ## 1. Spark 應用程式 (*penguin_spark_app.py*) 通常,我們使用 *spark-submit* 命令列實用程式來執行 PySpark 任務。 您可以在他們自己的文件中閱讀有關以這種方式提交Spark 作業的內容和原因的更多資訊[此處](https://spark.apache.org/docs/latest/submitting-applications.html) 。 當使用 Taipy 進行工作流程編排時,我們可以繼續做同樣的事情。 唯一的區別是,我們不是在命令列中執行命令,而是讓工作流程管道產生一個[子進程](https://docs.python.org/3/library/subprocess.html),它使用以下命令執行Spark 應用程式*火花提交*。 在開始討論之前,我們首先**看看我們的 Spark 應用程式**。 只需瀏覽一下程式碼,然後**繼續閱讀有關此腳本功能的簡短說明**: ``` ### app/penguin_spark_app.py import argparse import os import sys parser = argparse.ArgumentParser() parser.add_argument("--input-csv-path", required=True, help="Path to the input penguin CSV file.") parser.add_argument("--output-csv-path", required=True, help="Path to save the output CSV file.") args = parser.parse_args() import pyspark.pandas as ps from pyspark.sql import SparkSession def read_penguin_df(csv_path: str): penguin_df = ps.read_csv(csv_path) return penguin_df def clean(df: ps.DataFrame) -> ps.DataFrame: return df[df.sex.isin(["male", "female"])].dropna() def process(df: ps.DataFrame) -> ps.DataFrame: """The mean of measured penguin values, grouped by island and sex.""" mean_df = df.groupby(by=["species", "island", "sex"]).agg("mean").drop(columns="year").reset_index() return mean_df if __name__ == "__main__": spark = SparkSession.builder.appName("Mean Penguin").getOrCreate() penguin_df = read_penguin_df(args.input_csv_path) cleaned_penguin_df = clean(penguin_df) processed_penguin_df = process(cleaned_penguin_df) processed_penguin_df.to_pandas().to_csv(args.output_csv_path, index=False) sys.exit(os.EX_OK) ``` <小時/> 我們可以透過在終端機中輸入以下命令來提交此 Spark 應用程式以供執行: ``` spark-submit --master local[8] app/penguin_spark_app.py \ --input-csv-path app/penguins.csv \ --output-csv-path app/output.csv ``` <小時/> 它將執行以下操作: 1.提交*penguin_spark_app.py*應用程式在8個CPU核心上本地執行; 2. 從 *app/penguins.csv* CSV 檔案載入資料; 3. 依「*物種*」、「*島嶼*」和「*性別*」分組,然後按平均值聚合其餘欄位; 4. 將產生的 DataFrame 儲存到 *app/output.csv*。 此後,*app/output.csv* 的內容應如下所示: ![資料](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1bjjxzb6vzypq2cj3mzl.png) <小時/> 另請注意,我們已對 **Spark 應用程式進行了編碼以接收 2 個命令列參數**: 1.  - *input-csv-path* :輸入企鵝 CSV 檔案的路徑;和 2.  - *output-csv-path* :Spark 應用程式處理後儲存輸出 CSV 檔案的路徑。 <小時/> ## 2. Taipy 設定 (*config.py*) 此時,我們有了 *penguin_spark_app.py* PySpark 應用程式,並且需要建立一個 **Taipy 任務來執行此 PySpark 應用程式**。 再次快速瀏覽 *app/config.py* 腳本,然後繼續閱讀: ``` ### app/config.py import datetime as dt import os import subprocess import sys from pathlib import Path import pandas as pd import taipy as tp from taipy import Config SCRIPT_DIR = Path(__file__).parent SPARK_APP_PATH = SCRIPT_DIR / "penguin_spark_app.py" input_csv_path = str(SCRIPT_DIR / "penguins.csv") # -------------------- Data Nodes -------------------- input_csv_path_cfg = Config.configure_data_node(id="input_csv_path", default_data=input_csv_path) # Path to save the csv output of the spark app output_csv_path_cfg = Config.configure_data_node(id="output_csv_path") processed_penguin_df_cfg = Config.configure_parquet_data_node( id="processed_penguin_df", validity_period=dt.timedelta(days=1) ) species_cfg = Config.configure_data_node(id="species") # "Adelie", "Chinstrap", "Gentoo" island_cfg = Config.configure_data_node(id="island") # "Biscoe", "Dream", "Torgersen" sex_cfg = Config.configure_data_node(id="sex") # "male", "female" output_cfg = Config.configure_json_data_node( id="output", ) # -------------------- Tasks -------------------- def spark_process(input_csv_path: str, output_csv_path: str) -> pd.DataFrame: proc = subprocess.Popen( [ str(Path(sys.executable).with_name("spark-submit")), str(SPARK_APP_PATH), "--input-csv-path", input_csv_path, "--output-csv-path", output_csv_path, ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) try: outs, errs = proc.communicate(timeout=15) except subprocess.TimeoutExpired: proc.kill() outs, errs = proc.communicate() if proc.returncode != os.EX_OK: raise Exception("Spark training failed") df = pd.read_csv(output_csv_path) return df def filter(penguin_df: pd.DataFrame, species: str, island: str, sex: str) -> dict: df = penguin_df[(penguin_df.species == species) & (penguin_df.island == island) & (penguin_df.sex == sex)] output = df[["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g"]].to_dict(orient="records") return output[0] if output else dict() spark_process_task_cfg = Config.configure_task( id="spark_process", function=spark_process, skippable=True, input=[input_csv_path_cfg, output_csv_path_cfg], output=processed_penguin_df_cfg, ) filter_task_cfg = Config.configure_task( id="filter", function=filter, skippable=True, input=[processed_penguin_df_cfg, species_cfg, island_cfg, sex_cfg], output=output_cfg, ) scenario_cfg = Config.configure_scenario( id="scenario", task_configs=[spark_process_task_cfg, filter_task_cfg] ) ``` 您也可以**使用[Taipy Studio](https://docs.taipy.io/en/latest/manuals/studio/config/)** 建立Taipy 配置,這是一個Visual Studio Code 擴展,它提供了圖形編輯器建構 Taipy *.toml* 設定檔。 <小時/> ### Taipy 中的 PySpark 任務 我們對產生這部分 DAG 的程式碼部分特別感興趣: ![DAG](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/19t1otocpcrsa5qtdt2n.png) <小時/> 讓我們提取並檢查 *config.py* 腳本的相關部分,該腳本在 Taipy 中建立「*spark_process*」Spark 任務(及其 3 個關聯的資料節點),如上圖所示: ``` ### Code snippet: Spark task in Taipy # -------------------- Data Nodes -------------------- input_csv_path_cfg = Config.configure_data_node(id="input_csv_path", default_data=input_csv_path) # Path to save the csv output of the spark app output_csv_path_cfg = Config.configure_data_node(id="output_csv_path") processed_penguin_df_cfg = Config.configure_parquet_data_node( id="processed_penguin_df", validity_period=dt.timedelta(days=1) ) # -------------------- Tasks -------------------- def spark_process(input_csv_path: str, output_csv_path: str) -> pd.DataFrame: proc = subprocess.Popen( [ str(Path(sys.executable).with_name("spark-submit")), str(SPARK_APP_PATH), "--input-csv-path", input_csv_path, "--output-csv-path", output_csv_path, ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) try: outs, errs = proc.communicate(timeout=15) except subprocess.TimeoutExpired: proc.kill() outs, errs = proc.communicate() if proc.returncode != os.EX_OK: raise Exception("Spark training failed") df = pd.read_csv(output_csv_path) return df spark_process_task_cfg = Config.configure_task( id="spark_process", function=spark_process, skippable=True, input=[input_csv_path_cfg, output_csv_path_cfg], output=processed_penguin_df_cfg, ) ``` <小時/> 由於我們設計 *penguin_spark_app.py* Spark 應用程式來接收 2 個參數(*input_csv_path* 和 *output_csv_path*),因此我們選擇將這 2 個參數表示為 Taipy 資料節點。 請注意,**您的用例可能有所不同,您可以(並且應該!)根據您的需求修改任務、函數和關聯的資料節點**。 例如,您可以: 1. 有一個 Spark 任務,執行一些例行 ETL 並且不回傳任何內容; 2. 偏好對輸入和輸出路徑進行硬編碼,而不是將它們持久化為資料節點;或者 3. 將其他應用程式參數儲存為資料節點並將其傳遞給 Spark 應用程式。 然後,我們將 *spark-submit* 作為 Python 子進程執行,如下所示: ``` subprocess.Popen( [ str(Path(sys.executable).with_name("spark-submit")), str(SPARK_APP_PATH), "--input-csv-path", input_csv_path, "--output-csv-path", output_csv_path, ], ) ``` <小時/> 回想一下,清單元素的順序應保留以下格式,就像它們在命令列上執行一樣: ``` $ spark-submit [spark-arguments] <pyspark-app-path> [application-arguments] ``` <小時/> 同樣,根據我們的用例,我們可以根據需要指定不同的 Spark-submit 腳本路徑、Spark 參數(我們在範例中未提供任何參數)或不同的應用程式參數。 <小時/> ### 讀取並回傳*output_csv_path* 請注意,*spark_process* 函數的結束如下: ``` def spark_process(input_csv_path: str, output_csv_path: str) -> pd.DataFrame: ... df = pd.read_csv(output_csv_path) return df ``` <小時/> 在我們的例子中,我們希望 Taipy 任務在 Spark -  處理資料後輸出資料,以便可以將其寫入 *processed_penguin_df_cfg* [Parquet 資料節點](https://docs.taipy.io/en/latest /手冊/核心/配置/資料節點配置/#parquet)。 我們可以做到這一點的一種方法是手動讀取輸出目標(在本例中為 *output_csv_path*),然後將其作為 Pandas DataFrame 傳回。 但是,如果您不需要 Spark 應用程式的返回資料,您可以簡單地讓 Taipy 任務(透過 *spark_process* 函數)返回 *None*。 <小時/> ### 快取 Spark 任務 由於我們將 *spark_process_task_cfg* 配置為 *True*,當重新執行該場景時,Taipy 將 **跳過 ** ***spark_process 的重新執行*** **任務** 並重複使用持久化任務輸出:* processed_penguin_df_cfg* Pandas DataFrame。 但是,我們也為 *processed_penguin_df_cfg* 資料節點定義了 1 天的 *validity_period*,因此如果 DataFrame 最後一次快取超過一天,Taipy 仍會重新執行任務。 <小時/> ## 3. 建構 GUI (*main.py*) 我們將透過**建立我們在本文開頭看到的 GUI** 來完成我們的應用程式: ![應用程式](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bvfpy6aobtbzdhbf55sv.png) <小時/> 如果您不熟悉 Taipy 的 GUI 功能,可以在此處找到[快速入門](https://docs.taipy.io/en/latest/getting_started/getting-started-gui/)。 無論如何,您只需為 *app/main.py* 複製並貼上以下程式碼,因為它不是我們的重點: ``` ### app/main.py from pathlib import Path from typing import Optional import taipy as tp from config import scenario_cfg from taipy.gui import Gui, notify valid_features: dict[str, list[str]] = { "species": ["Adelie", "Chinstrap", "Gentoo"], "island": ["Torgersen", "Biscoe", "Dream"], "sex": ["Male", "Female"], } selected_species = valid_features["species"][0] selected_island = valid_features["island"][0] selected_sex = valid_features["sex"][0] selected_scenario: Optional[tp.Scenario] = None data_dir = Path(__file__).with_name("data") data_dir.mkdir(exist_ok=True) def scenario_on_creation(state, id, payload): _ = payload["config"] date = payload["date"] label = payload["label"] properties = payload["properties"] # Create scenario with selected configuration scenario = tp.create_scenario(scenario_cfg, creation_date=date, name=label) scenario.properties.update(properties) # Write the selected GUI values to the scenario scenario.species.write(state.selected_species) scenario.island.write(state.selected_island) scenario.sex.write(state.selected_sex.lower()) output_csv_file = data_dir / f"{scenario.id}.csv" scenario.output_csv_path.write(str(output_csv_file)) notify(state, "S", f"Created {scenario.id}") return scenario def scenario_on_submission_change(state, submittable, details): """When the selected_scenario's submission status changes, reassign selected_scenario to force a GUI refresh.""" state.selected_scenario = submittable selected_data_node = None main_md = """ <|layout|columns=1 4|gap=1.5rem| <lhs|part| # Spark with **Taipy**{: .color-primary} ## Scenario <|{selected_scenario}|scenario_selector|on_creation=scenario_on_creation|> ---------- ## Scenario info <|{selected_scenario}|scenario|on_submission_change=scenario_on_submission_change|> |lhs> <rhs|part|render={selected_scenario}| ## Selections <selections|layout|columns=1 1 1 2|gap=1.5rem| <|{selected_species}|selector|lov={valid_features["species"]}|dropdown|label=Species|> <|{selected_island}|selector|lov={valid_features["island"]}|dropdown|label=Island|> <|{selected_sex}|selector|lov={valid_features["sex"]}|dropdown|label=Sex|> |selections> ---------- ## Output **<|{str(selected_scenario.output.read()) if selected_scenario and selected_scenario.output.is_ready_for_reading else 'Submit the scenario using the left panel.'}|text|raw|class_name=color-primary|>** ## Data node inspector <|{selected_data_node}|data_node_selector|display_cycles=False|> **Data node value:** <|{str(selected_data_node.read()) if selected_data_node and selected_data_node.is_ready_for_reading else None}|> <br/> ---------- ## DAG <|Scenario DAG|expandable| <|{selected_scenario}|scenario_dag|> |> |rhs> |> """ def on_change(state, var_name: str, var_value): if var_name == "selected_species": state.selected_scenario.species.write(var_value) elif var_name == "selected_island": state.selected_scenario.island.write(var_value) elif var_name == "selected_sex": state.selected_scenario.sex.write(var_value.lower()) if __name__ == "__main__": tp.Core().run() gui = Gui(main_md) gui.run(title="Spark with Taipy") ``` <小時/> 然後,從專案資料夾中,您可以執行主腳本,如下所示: ``` $ taipy run app/main.py ``` <小時/> ## 結論 現在您已經看到如何將 PySpark 與 Taipy 結合使用的範例,請繼續嘗試使用這兩個工具來**增強您自己的資料應用程式**! 如果您一直在努力應對其他工作流程編排工具減慢您的工作並妨礙您的工作,請不要讓它阻止您嘗試 Taipy。 Taipy 易於使用,並且努力不限制自己可以使用的第 3 方軟體包 - **其強大而靈活的框架使其可以輕鬆適應任何資料應用程式**。 <小時/> ![GIF 結束](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/839kmsq22emwpkuerxys.gif) 希望您喜歡這篇文章! <小時/> 您可以在此[儲存庫](https://medium.com/r?url=https%3A%2F%2Fgithub.com%2FAvaiga%2Fdemo-pytorch-penguin-app)上找到所有程式碼和資料。 --- 原文出處:https://dev.to/taipy/how-to-master-big-data-pipelines-with-taipy-and-pyspark-14oe

🧙‍♂️ CSS 技巧:從 height: 0 變成 height: auto!

如果你使用 CSS 時間夠長,那麼你很可能至少嘗試過一次從 `height: 0` 到 `auto` 的**轉換**...結果卻發現它**確實如此。行不通!** 😢 ➡️幸運的是,今天這個問題實際上有一個**解決方案**:它在底層使用**CSS Grid**,而且它**如此簡單**並且**完美執行!** 讓我們從一個實際的例子開始。我建立了這個簡單的**手風琴**: https://codepen.io/francescovetere/pen/MWLXjNx 它的 **HTML** 非常簡單: ``` <div class="accordion"> <div class="accordion-title">Hover me!</div> <div class="accordion-body"> <div> <p>Lorem ipsum ...</p> </div> </div> </div> ``` 如果您將滑鼠**懸停在手風琴上,您會注意到出現**下拉選單**。這很酷,但是如果我們想讓它看起來有一個很好的**平滑過渡怎麼辦?** **我實際上嘗試**在之前的程式碼筆中通過在“height”屬性上加入一些過渡來做到這一點: ``` .accordion-body { height: 0; transition: 500ms height ease; } .accordion:hover .accordion-body { height: auto; } ``` ❌不幸的是,**這不起作用**:從 `height: 0` 轉換到 `height: auto`,正如我之前所說,** CSS 是不可能的。** 🤔 如何解決這個問題? 好吧,**第一個解決方案**可以將 `height` 屬性設為 **固定數字**,而不是 `auto`。 這是可行的,但它**不是一個很好的方法**:為了計算這個固定數字,我們必須求助於**JavaScript**,以便計算我們的“.accordion-body”實際上是多少高. .. **不是我們真正的目標!** 😕 我們仍然可以實現這種效果,但是使用 **僅 CSS** 解決方案嗎? 💡 **實際上,是的!** 為什麼我們不直接使用 `max-height` 來代替呢? ``` .accordion-body { max-height: 0; transition: 500ms max-height ease; } .accordion:hover .accordion-body { max-height: 200px; } ``` 這將是結果: https://codepen.io/francescovetere/pen/ExrRNVa 由於我們為「max-height」定義了一個**固定值**,瀏覽器現在能夠**正確地執行轉換**。 😕 **唯一的問題**是,由於我們為 `max-height` 定義了一個固定值,現在內容可能 **可能溢出**: https://codepen.io/francescovetere/pen/VwgdmbW 如果您確定您的內容永遠不會達到特定高度...那麼使用此方法就完全沒問題!** 只需為“max-height”使用適當的值,然後您就可以了很好,可以走了。 **但請注意**,“max-height”的**值越高,**過渡變得越奇怪**(嘗試在之前的程式碼筆中加入“max-height: 1000px”,然後查看事情發生了怎樣的變化!)。 🤔 **我們可以做得更好嗎?** 我們可以先避免任何固定的 `height`/`max-height` 嗎? 🎉 **CSS 格來拯救!** ✅ 我們實際上可以使用一個**巧妙的技巧**,它基本上包括使用**單一網格專案**製作**CSS網格**。 那麼我們真正要做的就是採用「grid-template-rows」並使其從「0fr」過渡到「1fr」:這樣,我們的網格項將從 0 過渡到「自然」高度。就是這麼簡單: ``` .accordion-body { display: grid; grid-template-rows: 0fr; transition: 250ms grid-template-rows ease; } .accordion:hover .accordion-body { grid-template-rows: 1fr; } .accordion-body > div { overflow: hidden; } ``` 這感覺**乾淨多了。** 沒有固定的高度,沒有花哨的東西,只是我們的手風琴按預期工作。 **太棒了!** 😄 https://codepen.io/francescovetere/pen/BaGwBRx 此解決方案的**一個警告**是,您實際上需要為“.accordion-body”的內部“div”設定“overflow:hidden”才能使其工作。在我看來,這個額外的 CSS 是完全值得的,但請在評論中告訴我你的想法! 🎁 **額外小提示** 這個技巧之所以有效,是因為「網格模板行」(以及更一般地說,「網格軌道」)的「動畫性」。 對於某些瀏覽器來說,這是一個**新功能**:如果您存取[此頁面](https://caniuse.com/mdn-css_properties_grid-template-rows_animation),您會注意到**網格追蹤可動畫性**例如,僅從 **版本 107 開始登陸 **Chrome**。 在我撰寫本文時,所有主要瀏覽器都支援此功能,但如果您想在生產程式碼中使用此功能,請務必首先**檢查相容性**! --- 就這樣!如果您已經了解這個很棒的 CSS 功能,請隨時發表評論並告訴我! 😉 下次見! 👋 --- 原文出處:https://dev.to/francescovetere/css-trick-transition-from-height-0-to-auto-21de

使用 Markdown 與 React 在 Github 文件中顯示淺色模式、深色模式

警告:所表達的觀點可能不適合所有受眾! 😂 ## 簡介 在本文結束時,您將了解並能夠根據使用者偏好 - **深色**或**淺色**模式展示您的 Markdown 影像。 1. 我將介紹如何在 GitHub README.md 中加入兩個圖像 - 根據所選的“主題”,您的圖像將正確回應。 2. 我將引導您在 Markdown 中合併影像的過程,並示範如何使用 React 使它們回應。 😎 ___ ## 你使用淺色還是深色? 我不了解你的情況,但無論平台如何,如果他們可以選擇在淺色和深色模式之間切換,那就沒有競爭了。 淺色主題正在切換為深色,事實上,當然在我寫這篇文章的時候! ![深色主題](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s0yfjc2yv5pfyacu74go.png) 話雖如此,在軟體開發的快速發展中,創造無縫的使用者體驗至關重要。 這種體驗的一部分涉及適應使用者偏好,例如淺色和深色模式。 我還記得幾年前,Github 宣布了用戶可以切換到「深色模式」的選項,這是一件非常大的事情。 ![GitHub 深色主題](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qi10urco3o6fdojm6ipf.png) 【Github揭曉黑暗主題的重要時刻】(https://t.co/HEotvXVJ7R) 🤩 2020 年 12 月 8 日🎆 近年來,使用者介面中深色和淺色模式選項的出現已成為一種流行趨勢。 我絕對不是唯一一個喜歡使用深色主題選項的人,根據 Android 用戶的說法,[91.8% 的用戶更喜歡深色模式](https://www.androidauthority.com/dark-mode-poll-results-1090716/) 所以我們可以猜測這個數字在所有作業系統中都相當高。 這當然可能會引起激烈的爭論,所以我會盡力將自己的觀點降到最低。 ![輕模式迷因](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m3yiepj8a46rwhu69fgw.png) ## 改善使用者體驗 主要目標是透過在應用程式中提供選項來改善用戶體驗。 有多種方法可以建立每個圖像的多個版本,在本教程中我們不會深入討論細節。 只要確保您的圖像在兩個主題中脫穎而出並具有透明背景,您就會獲得成功。 **_讓我們開始派對吧!_** ## GitHub 自述文件中的響應式圖像 您有一個專案並想讓您的 GitHub 專案 README.md 真正流行嗎? 無論使用者使用什麼淺色主題,我們都需要一種方法來指定圖像應在 Markdown 中顯示哪種主題(淺色或深色)。 當您想要根據使用者選擇的配色方案優化圖片的顯示時,這特別有用,並且它涉及將 **HTML `<picture>`** 元素與 `prefers-color-scheme` 媒體功能結合使用如下所示。 繼續將圖片檔案直接拖曳到 GitHub 中並放在“srcset=”後面。 ``` <picture> <source media="(prefers-color-scheme: dark)" srcset="https://github.com/boxyhq/.github/assets/66887028/df1c9904-df2f-4515-b403-58b14a0e9093"> <source media="(prefers-color-scheme: light)" srcset="https://github.com/boxyhq/.github/assets/66887028/e093a466-72ea-41c6-a292-4c39a150facd"> <img alt="BoxyHQ Banner" src="https://github.com/boxyhq/jackson/assets/66887028/b40520b7-dbce-400b-88d3-400d1c215ea1"> </picture> ``` 瞧! ![SAML Jackson 暗模式](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q51g41fjfqnposn50una.png) ![SAML Jackson 燈光模式](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d0xzs88txjylnixilaqu.png) 太好了,你有 5 秒嗎? https://github.com/boxyhq/jackson --- ## 使用 React 在 Markdown 中回應影像 假設今天我將像平常一樣用 Markdown 編寫博客,並將其發佈到我的網站上。 我使用的圖像需要根據使用者偏好做出回應,但在 Markdown 中不可能偵聽本地儲存和設定狀態中的「主題」變更。 ![本機儲存](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vrjz4to8x17h63dtybxn.png) 值得慶幸的是,如果我們將 React 匯入到 Markdown 檔案中,但先建立一個元件,就有一種方法可以解決這個困境。 ## 反應文件 ``` src/components/LightDarkToggle.js import React, { useEffect, useState } from 'react'; function ToggleImages() { // Define a state variable to track the user's login status const [currentTheme, setcurrentTheme] = useState(localStorage.getItem('theme')); // Add an event listener for the 'storage' event inside a useEffect useEffect(() => { const handleStorageChange = (event) => { console.log('Storage event detected:', event); // Check the changed key and update the state accordingly console.log("event", event.key) if (event.key === 'theme') { setcurrentTheme(event.newValue); } }; window.addEventListener('storage', handleStorageChange); // Clean up the event listener when the component unmounts return () => { window.removeEventListener('storage', handleStorageChange); }; }, []); // The empty dependency array ensures that this effect runs once when the component mounts return ( <div className="image-container"> {currentTheme == 'light'? ( <img id="light-mode-image" src="/img/blog/boxyhq-banner-light-bg.png" alt="Light Mode Image" ></img> ):( <img id="dark-mode-image" src="/img/blog/boxyhq-banner-dark-bg.png" alt="Dark Mode Image" ></img> )} </div> ); } export default ToggleImages; ``` 我在程式碼中加入了註釋和一些控制台日誌,以幫助了解正在發生的事情,但讓我們快速分解它。 - React useState 鉤子管理 `currentTheme` 的狀態,它代表使用者選擇的儲存在本機儲存中的主題。 - useEffect 掛鉤用於為「儲存」事件新增事件偵聽器。當儲存事件發生時(表示本機儲存發生變化),元件會檢查變更的鍵是否為“theme”,並相應地更新“currentTheme”狀態。 - 此元件根據使用者選擇的主題呈現不同的影像,如果主題是“淺色”,則顯示淺色模式影像;如果主題是其他主題,則顯示深色模式影像。 酷,讓我們繼續吧! ## 降價文件 讓我們為新部落格建立一個 .md 檔案。 ``` --- slug: light-and-dark-mode-responsive-images title: 'Light and Dark Mode Responsive Images' tags_disabled: [ developer, react, javascript, open-source, ] image: /img/blog/light-dark.png author: Nathan Tarbert author_title: Community Engineer @BoxyHQ author_url: https://github.com/NathanTarbert author_image_url: https://boxyhq.com/img/team/nathan.jpg --- import ToggleImages from '../src/components/LightDarkToggle.js'; ## 🤩 Let's start this blog off with a bang! Our business logo is now responsive with each user's preference, whether it's **light** or **dark** mode! <div> <ToggleImages /> </div> More blog words... ``` 此時,我們只需匯入 React 元件並將其呈現在 Markdown 檔案中。 由於這是一個 Next.js 應用程式,讓我們啟動伺服器“npm run dev”並查看結果。 ![貓鼓滾](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lyjzjqgcwaubyj5ve1o3.gif) ![網站深色模式](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qraltb34mrl9y8j9jppq.png) 並切換到淺色主題 ![網站燈光模式](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u33jgzha5fbfy6tlb4hs.png) 讓我們打開控制台來查看我們的事件 ![console.log](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qpxz5gbhwt308vatsnkp.png) 你有它! 這些是在 Markdown 中展示響應式映像的幾種方法,其中一個範例使用 React 來幫助我們在本地儲存中設定狀態。 我希望您喜歡這篇文章,如果您喜歡開發,請在 [X (Twitter)](https://twitter.com/nathan_tarbert) 上關注我,我們下次再見! --- 原文出處:https://dev.to/nathan_tarbert/the-zebras-guide-to-showcase-your-images-in-light-dark-17f5

🦃 Reacts-giving:為專業人士提供 11 個 React 元件👩🏻‍🌾🍁

## 簡介 我收集了最好的 React 元件,您可以使用它來建立強大的 Web 應用程式。 每個都有自己的味道。 別忘了表達你的支持🌟 現在,讓我們仔細閱讀這段程式碼! 🍽️ ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j2p4508nvzg74qd060lx.gif) --- ## 1. [CopilotPortal](https://github.com/RecursivelyAI/CopilotKit):將可操作的 GPT 聊天機器人嵌入您的網路應用程式中。 ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0s5nodilnbgy2myna6ny.png) 將 GPT 支援的聊天機器人插入您的 React 應用程式中。 可以將 RAG 與雲端和應用程式狀態即時整合。 需要幾行程式碼才能嵌入。 ``` import "@copilotkit/react-ui/styles.css"; import { CopilotProvider } from "@copilotkit/react-core"; import { CopilotSidebarUIProvider } from "@copilotkit/react-ui"; export default function App(): JSX.Element { return ( <CopilotProvider chatApiEndpoint="/api/copilotkit/chat"> <CopilotSidebarUIProvider> <YourContent /> </CopilotSidebarUIProvider> </CopilotProvider> ); } ``` https://github.com/RecursivelyAI/CopilotKit --- ## 2. [ClickVote](https://github.com/clickvote/clickvote) - 按讚、投票並查看任何上下文 ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xubftfmy9xum98zjgv5m.png) 輕鬆將點讚、按讚和評論加入到您的網路應用程式中。 用於加入這些元件的簡單反應程式碼。 ``` import { ClickVoteProvider } from '@clickvote/react'; import { ClickVoteComponent } from '@clickvote/react'; import { LikeStyle } from '@clickvote/react'; <ClickVoteProvider> <ClickVoteComponent id={CONTEXT} voteTo={ID}> {(props) => <LikeStyle {...props} />} </ClickVoteComponent> </ClickVoteProvider> ``` https://github.com/clickvote/clickvote --- ## 3. [React Flow](https://github.com/xyflow/xyflow) - 建立可拖曳工作流程的最佳方式! ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8hy0bsacbfzctin4r7tq.png) 專為建立基於節點的編輯器和互動式圖表而客製化的 React 元件。 它具有高度可自訂性,提供拖放功能以實現高效的工作流程建立。 ``` import ReactFlow, { MiniMap, Controls, Background, useNodesState, useEdgesState, addEdge, } from 'reactflow'; <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} > <MiniMap /> <Controls /> <Background /> </ReactFlow> ``` https://github.com/xyflow/xyflow --- ## 4. [CopilotTextarea](https://github.com/RecursivelyAI/CopilotKit/tree/main/CopilotKit/packages/react-textarea) - React 應用程式中的 AI 驅動寫作 ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uye8z6aac1015iiqd3lk.png) 具有 Github CopilotX 功能的任何 React `<textarea>` 的直接替代品。 自動完成、插入、編輯。 可以即時或由開發人員提前提供任何上下文。 ``` import { CopilotTextarea } from "@copilotkit/react-textarea"; import { CopilotProvider } from "@copilotkit/react-core"; // Provide context... useMakeCopilotReadable(...) // in your component... <CopilotProvider> <CopilotTextarea/> </CopilotProvider>` ``` https://github.com/RecursivelyAI/CopilotKit --- ## 5. [Novu](https://github.com/novuhq/novu) - 將應用程式內通知新增至您的應用程式! ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c81fkg15xucqbyg4xctt.png) 用於在一個地方管理所有通訊管道的簡單元件和 API:電子郵件、SMS、Direct 和 Push 您可以使用此 React 元件為您的應用程式新增應用程式內通知。 ``` import { NovuProvider, PopoverNotificationCenter, NotificationBell, IMessage, } from "@novu/notification-center"; <NovuProvider subscriberId={"SUBSCRIBER_ID"} applicationIdentifier={"APPLICATION_IDENTIFIER"} > <PopoverNotificationCenter colorScheme="dark"> {({ unseenCount }) => <NotificationBell unseenCount={unseenCount} />} </PopoverNotificationCenter> </NovuProvider> ``` https://github.com/novuhq/novu --- ## 6. [ReactIcons](https://github.com/react-icons/react-icons) - 最受歡迎的反應圖示集合 ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l1sj51u7omogoa5v7di6.png) 輕鬆將 Font Awesome、Material Design 等中的流行圖標加入到您的 React 應用程式中。 為開發人員提供簡單、廣泛的選擇。 ``` import { FaBeer } from "react-icons/fa"; function Question() { return ( <h3> Lets go for a <FaBeer />? </h3> ); } ``` https://github.com/react-icons/react-icons --- ## 7. [React-dropzone](https://github.com/react-dropzone/react-dropzone) - 新增 HTML5 拖放 UI。 ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/apr4lbjc1i0glbs0kize.png) 用於實作 HTML5 拖放區域的簡單 React 鉤子,重點放在檔案互動。 它提供了一個易於使用的介面,用於向 React 應用程式加入檔案拖放功能。 ``` import React from 'react'; import {useDropzone} from 'react-dropzone'; const Basic = (props)=>{ const {acceptedFiles, getRootProps, getInputProps} = useDropzone(); const files = acceptedFiles.map(file => ( <li key={file.path}> {file.path} - {file.size} bytes </li> )); return ( <section className="container"> <div {...getRootProps({className: 'dropzone'})}> <input {...getInputProps()} /> <p>Drag 'n' drop some files here, or click to select files</p> </div> <aside> <h4>Files</h4> <ul>{files}</ul> </aside> </section> ); } export default Basic; ``` https://github.com/react-dropzone/react-dropzone --- ## 8. [React ChartJS 2](https://github.com/reactchartjs/react-chartjs-2) - 建立和整合各種圖表。 ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k820fg3ep6cocfukqdny.png) 用於在 React 應用程式中繪製圖表的即插即用解決方案,類似於 Chart.js 功能。 啟用動態、互動式圖表。 適用於即時資料或預定義資料集。 ``` import React from 'react'; import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js'; import { Doughnut } from 'react-chartjs-2'; ChartJS.register(ArcElement, Tooltip, Legend); const data = { labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], datasets: [ { label: '# of Votes', data: [12, 19, 3, 5, 2, 3], backgroundColor: [ 'rgba(255, 99, 132, 0.2)', ], borderColor: [ 'rgba(255, 99, 132, 1)', ], borderWidth: 1, }, ], }; export default function ShowChart() { return <Doughnut data={data} />; } ``` https://github.com/reactchartjs/react-chartjs-2 ## 9. [Redux](https://github.com/reduxjs/redux) - 可預測的狀態容器庫 ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a7iv2maik6xq4w5yl21y.png) JavaScript 應用程式中 Redux 的無縫補充,提供可靠的狀態管理。 確保一致的應用程式行為。 便於輕鬆除錯和測試。 與各種庫整合。 https://github.com/reduxjs/redux --- ## 10. [Blueprint](https://github.com/palantir/blueprint) - Palantir 的密集 UI 庫 ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/176noa7v8f25ll0jixqn.png) 提供一組用於建立複雜且資料豐富的介面的元件和樣式。 設計和開發具有現代外觀和感覺的類似桌面的 Web 應用程式。 由 Palantir 開發 ``` import React from 'react'; import '@blueprintjs/core/lib/css/blueprint.css'; import { H3, H4, OL, Pre } from "@blueprintjs/core"; function App() { return ( <div style={{ display: 'block', width: 500, padding: 30 }}> <h4>ReactJS Blueprint HTML Elements Component</h4> Heading Component: <H4>H4 Size Heading</H4> <H3>H3 Size Heading</H3> <br></br> OrderList Component: <OL> <li>1st item</li> <li>2nd item</li> </OL> Pre Component: <Pre>Sample Pre</Pre> </div> ); } ``` https://github.com/palantir/blueprint --- ## 11. [Headless UI](https://github.com/tailwindlabs/headlessui) - 可存取的 Tailwind 整合 UI 元件。 ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vsxfiivef7du8u3g5i5l.png) 在 React 和 Vue 應用程式中建立可存取的 UI 元件。 適用於即時資料或預定義資料集,使其成為現代 Web 開發專案的寶貴補充 ``` import React, { useState } from 'react'; import { Dialog } from '@headlessui/react'; function MyDialog() { let [isOpen, setIsOpen] = useState(true); return ( <Dialog open={isOpen} onClose={() => setIsOpen(false)} className="relative z-50"> {/* The backdrop, rendered as a fixed sibling to the panel container */} <div className="fixed inset-0 bg-black/30" aria-hidden="true" /> {/* Full-screen container to center the panel */} <div className="fixed inset-0 flex w-screen items-center justify-center p-4"> {/* Your dialog content goes here */} </div> </Dialog> ); } ``` https://github.com/tailwindlabs/headlessui --- 保存這些元件,以便像朝聖者一樣專業地建造。 謝謝大家,節日快樂! --- 原文出處:https://dev.to/copilotkit/reacts-giving-11-react-components-for-aspiring-pros-eck

作為開發者賺取額外現金的 50 種方法💰

目前大環境不好,但作為開發人員,我們擁有一套獨特的技能,如果您知道在哪裡尋找,這些技能的需求量很大! 這篇文章簡要概述了 50 個作為開發人員可以用來賺取額外收入的副業 --- ### 1. 銷售注意力 基於參與度的收入是指您將根據使用者在您的網站、個人資料或消費您的內容上花費的時間來獲得收入分成。它通常很小,至少對於較小的網站或創作者來說是這樣,但隨著時間的推移,它會增加,任何人都可以啟用它 - 所以你不會有任何損失。 - [Brave](https://creators.brave.com/) - 為使用 Brave 瀏覽器造訪您的網站、個人資料或查看您的內容的使用者付費。資金以 [BAT](https://basicattentiontoken.org/) 形式存入您的 Uphold 帳戶,然後可以以美元、英鎊或歐元形式提取至您的銀行帳戶 - [Flattr](https://flattr.com/) - 付費使用 Flattr 的用戶將其資金分配給用戶存取過其內容的創作者 > 幾年前,我親自報名了 Brave Rewards。在驗證了我的網域和個人資料的所有權後,我每月一直賺幾英鎊 - 到目前為止大約 200 英鎊以上(儘管我是 Firefox 用戶!)。雖然不多,但只需付出很少的努力,就值得了。 有關其工作原理的更多訊息,請查看 [webmonetization.org](https://webmonetization.org/) 規範,該規範利用了 [付款指針](https://paymentpointers.org/)通過[ILP](https://interledger.org/) 透過使用簡單的`<link rel="monetization" href="your-pointer-here" />` 標籤來串流來自支援WM 的訪客的收入。 --- ### 2. API 即服務 RapidAPI 等平台可讓您從 API 中[賺取被動收入](https://rapidapi.com/guides/earn-a-passive-venue-by-monetizing-apis-as-a-developer)。 建置並部署簡單的 API 後,您可以將其匯入 RapidAPI Hub,選擇使用和定價計劃,然後點擊發布。您的 API 可大可小,如您所願。 如果您正在為一個簡單的第一個專案尋找靈感,請考慮將開放資料集轉換為 API。對於初學者,RapidAPI 有一個關於如何入門的[影片系列](https://rapidapi.com/courses/build-and-sell-your-own-api)。其他想法可能包括將現有套件包裝為 API、向其他服務(如 OpenAI)加入功能或建置執行一些簡單計算的端點。 --- ### 3. 發放賞金 這些是開源專案的熱門功能請求。用戶可以在承諾一定金額的情況下提供“賞金”,然後將其支付給第一個完成並合併該功能的開發人員。 - [BOSS.dev](https://www.boss.dev/) - 完成功能請求和錯誤修復,獎金從 30 美元到 1000 美元不等。 --- ### 4. 贊助商 如果您在 GitHub 或其他平台上有業務,那麼啟用贊助是一種為您的工作帶來收入的有益方式。 不要忘記啟用贊助商按鈕。這適用於各種平台以及 GitHub 贊助商 - [GitHub Sponsors](https://github.com/sponsors) - 對於開發人員(無論規模大小)來說都是一個不錯的選擇。如果支持者已經在 GitHub 上,則零費用且進入門檻低 - [Patreon](https://www.patreon.com/) - 允許向您的支持者提供福利和獨家內容。如果您在 GitHub 以外的其他平台上有業務,這是一個不錯的選擇 - [LibrePay](https://liberapay.com/) - 針對那些建立開源內容的人 - [Open Collective](https://opencollective.com/) - 如果您正在為特定專案籌集資金,並使用收益來支持該專案(而不是個人),那麼這是一個不錯的選擇 - [Steday](https://steadyhq.com/en) - [TideLift](https://tidelift.com) - 更針對那些開發企業級開源專案的人,潛在收入更大,但僅限於最大的專案 - [LFX](https://lfx.linuxfoundation.org/) - 由 Linux 基金會提供 > 贊助(特別是GitHub 贊助商)是我個人最喜歡的方法之一,因為付費是可選的,所以你不會阻止那些無力承擔費用的人存取,而且那些支持你的人已經知道他們會預先得到什麼,所以您永遠不會讓客戶失望。 --- ### 5. 小費 您可能遇到過這樣的情況,您發現某個部落格文章、SO 答案、GitHub 儲存庫或論壇回應非常有幫助,以至於您希望可以為作者買一杯啤酒來表示感謝。 支援這些小額一次性付款的平台可以免費註冊,並且在您的個人資料中或在部落格文章末尾加入「提示」按鈕不會有任何損失。 - [Ko-fi](https://ko-fi.com/) - [請我喝杯咖啡](https://www.buymeacoffee.com/) - [Tipeee](https://en.tipeee.com/) - [PayPal Me](https://www.paypal.com/paypalme/) 提示:不要乞求。建立一些有用的內容,然後將提示連結放在底部。 --- ### 6. 企業贊助 許多具有一定下載量/經常性用戶的開源專案將開始被希望贊助創作者作品的公司接洽,以換取他們的公司徽標+連結包含在自述文件頂部附近。與個人贊助不同,這些贊助通常起價為 100-500 美元/月,專案使用量越大,贊助金額就越多。 --- ### 7. 黑客松 編碼競賽一直在遠端進行。這些通常由公司贊助,並向獲獎者支付現金獎勵。 - [程式碼之夏](https://summerofcode.withgoogle.com/) - 由 Google 執行,您將收到[貢獻者津貼](https://developers.google.com/open-source/gsoc/help/student-stipends) 成功接受後,金額從750 美元到6000 美元不等,金額取決於您所在的國家/地區和專案規模 - [CodeHeat](https://codeheat.org/) - 由 FOSS Asia 運營,每兩週 100 新元,外加較小的獎品 - [HackerEarth](https://www.hackerearth.com/challenges/hackathon/) - [Hackathon.com](https://www.hackathon.com/online) - [Devfolio 黑客松](https://devfolio.co/hackathons/upcoming) > 當我還是學生時,我[曾經參加過很多](https://alicia.omg.lol/hackathons) 黑客馬拉松(大部分是面對面的),並且經常能夠通過參加各種活動來資助我的暑假活動!這也是認識新朋友、學習新事物的好方法,而且非常有趣! --- ### 8. 依賴套件的贊助 如果您有一個軟體包(例如 NPM 模組),那麼在您的設定檔中啟用贊助將允許您的程式碼的使用者在財務上做出貢獻。 - NPM 資金 - 您可能熟悉執行“npm 基金”,並查看您正在使用的正在尋求資金的軟體包清單。新增了 [npm 基金](https://docs.npmjs.com/cli/v6/commands/npm-fund),以便更輕鬆地向專案所依賴的依賴項的維護者捐款。如果您維護 NPM 包,只需在 package.json 中包含「funding」字段,用戶將能夠更輕鬆地支援您。 - [StackAid](https://www.stackaid.us/) - 只需安裝 StackAid GitHub 應用程式並連結您的 Stripe 帳戶,直接或間接使用您專案的支持者捐贈的部分資金將分配給您每個月 - GitHub Sponsors - GitHub Sponsors 再次出現,因為它[讓用戶提供他們最常用的依賴項](https://github.com/sponsors/explore) - 儘管這是一個手動過程,而不是自動的。 --- ### 9. 回報問題 如果您注重安全性,或喜歡在應用程式中尋找錯誤和漏洞,那麼這款就適合您。最受歡迎的平台是[HackerOne](https://hackerone.com/opportunities/all/search?ordering=Highest+bounties),每個負責任地披露的錯誤都可以在其中賺取20 到200,000 美元的收入。 許多其他網站也直接提供負責任的揭露政策,他們會獎勵您的工作。如果您對此感興趣,我在以下位置保留了 1000 多個賞金計劃的清單:[https://bug-bounties.as93.net](https://bug-bounties.as93.net) > 我個人透過這種方法取得了很大的成功,而且也很有趣 - 所以我強烈推薦它! 其他值得查看的平台包括: - [HackerOne](https://www.hackerone.com/) - 排名第一的平台,最多的賞金以及良好的保護和支付率 - [Immunefi](https://immunefi.com/) - 專門針對 Web3 - [BugCrowd](https://www.bugcrowd.com/bug-bounty-list/) - [Intigriti](https://www.intigriti.com/) - [issuehunt](https://issuehunt.io/) --- ### 10.開放核心模型 這是您的大部分程式碼都是開源的,但某些擴充功能或附加元件(特別是針對企業客戶的擴充或附加元件)被授權為專有的。 因此,開發者可以在其他開源專案中自由使用該軟體。然而,公司必須為使用企業特定的模組或整合付費。 請記住,這通常說起來容易做起來難。您需要能夠分離專有功能,而大公司通常會採取一切措施(包括違反許可限制)來避免付費。 --- ### 11. 付費升級套件 這些服務可以輕鬆為常見註冊管理機構提供高級/付費方案。例如,如果您希望分發 NPM 模組的高級版本,或對特定軟體包功能收費,這可能是個不錯的選擇。 - [PrivJS](https://www.privjs.com/) - 分發 Node 套件的進階版本 - [CodeShip](https://codecodeship.com/) - 私人註冊中心,用戶需要付費才能使用你的包 --- ### 12.贊助支持 在開源專案中加入專業支援計劃選項使客戶能夠支付一次性或持續的幫助和支援費用,以啟動和執行。 這可以透過您自己的系統啟用,也可以使用現有的贊助平台(例如Patreon 和GitHub Sponsors),或使用專門的服務(例如[Otechie](https://otechie.com/))來啟用,該服務加入了付費功能+ 支援通過嵌入的聊天對話框。 [Calendly](https://calendly.com/) 等工具可以讓客戶將時間放入日曆中,或者對於較大的專案,投資專用的客戶支援平台,例如[HelpScout](https://www.helpscout.com/) 可能會讓這件事變得更容易。 --- ### 13. 寫文件 - [撰寫文件](https://www.writethedocs.org/) 是所有文件的首選位置。 - [文件季節](https://developers.google.com/season-of-docs) - 在 Google 的支持下,每年都有技術作家為開源專案做出貢獻。參與專案將獲得 5,000 至 15,000 美元的贈款,然後通常透過 Open Collective 分發給貢獻者。 - 如果你環顧四周,你會發現還有很多產品正在尋找技術作家。 Julia 列出了一份[好名單](https://dev.to/juliafmorgado/get-paid-to-write-technical-articles-16cl),列出了願意付費讓你撰寫技術內容的公司 - 版權也屬於這一類。 [scripted](https://www.scripted.com/) 等服務可讓您透過校對或編輯其他文字內容來賺錢。 即使只是記錄您自己的和其他開發人員的儲存庫也是一個不錯的起點。 如果專案被記錄下來,它的價值就會大幅增加。如果沒有文件,潛在使用者、客戶或開發人員將不知道它的用途、如何使用它、如何在其基礎上建立或如何做出貢獻。 > 我可能是唯一的一個,但我個人喜歡寫文件。 [我的所有專案](https://github.com/Lissy93?tab=repositories) 包括完整的使用、開發和貢獻文件。這促進了它們的成功和採用。我覺得如果你不花一點時間向人們展示如何使用它,那麼花幾個小時建立一些很棒的東西是沒有意義的。 --- ### 14. 廣告 在你跳過這一點之前——我也討厭廣告。它們很煩人,並且經常涉及某種形式的跟踪,從而損害用戶的隱私。但是,對於開源專案,還有一些其他選項沒有這些缺點。 - [Ethical Ads](https://www.ethicalads.io/) - [Carbon Ads](https://www.carbonads.net/open-source) 如果您正在維護獲得穩定流量的 GitHub 儲存庫、網站、部落格或服務,那麼這是一個不錯的選擇。通常每月至少需要約 10,000 個用戶,但如果您每月獲得 50,000 以上的用戶,您將獲得更好的回報。 --- ### 15. 出售你的程式碼 > 我個人不同意這種方法,只是因為出售的許多程式碼都是開源軟體的糟糕的重新設計版本,並且並不總是給予原始作者適當的榮譽。也就是說,一些開發商確實設法讓它發揮作用,建造簡單的專案然後將其出售。 - [IndieMaker](https://indiemaker.co/) - 出售您的整個專案 - [PieceX](https://www.piecex.com/) - 出售現成的原始碼 - [Codester](https://www.codester.com/info/seller) - 針對 PHP 和 Wordpress --- ### 16.銷售內容 當您查看開發人員的副業時,這是一個常見的建議。但有充分的理由 - 如果您能夠建立高品質的內容,您可以賺到很多錢。特別是如果您對新興領域有深入的了解。 銷售內容的熱門網站包括: - [GumRoad](https://gumroad.com/) - 程式碼、課程、貼文、藝術、設計、媒體(10% 費用) - [AppSumo](https://sell.appsumo.com/) - 程式碼、應用程式、擴充功能、課程、範本等 --- ### 17.寫作 這是一套獨特的技能。要么您非常擅長編寫引人入勝的內容,要么您對特定的熱門領域有深入的了解。否則,如果您對此感興趣,請考慮電子書出版,如果您的書不成功,也不會造成任何損失。 - [LeanPub](https://leanpub.com/) - 一個自助出版技術/開發電子書和課程的平台,具有豐厚的收入模式(您可以保留 70%) - [Amazon KDP](https://kdp.amazon.com/en_US/) - 發佈至 Amazon Kindle,並立即向全球數百萬用戶提供(亞馬遜將收取至少 30% 的佣金,可能會更多)小出版商) - [SmashWords](https://www.smashwords.com/) 和 [Draft2Digital](https://draft2digital.com/sw/) - 分發給全球其他電子書賣家,這是一種簡單的開始出版。他們收取的佣金比亞馬遜少,但比 LeanPub 多。 --- ### 18.補助金 補助金和企業贊助涉及多個領域,包括開源、創新、DeFi、人工智慧等。它們通常是為了幫助您在從事特定工作時支付短期生活費用。 - [GitHub Sponsors](https://github.com/sponsors) - 為個人和組織提供經濟支援開源開發者的平台。金額依贊助情況而有所不同。 - [Google Summer of Code (GSoC)](https://summerofcode.withgoogle.com/) - 學生開發者為開源專案做出貢獻的全球計劃,津貼通常為 1500 美元到 3300 美元不等。 - [Mozilla 開源支援 (MOSS)](https://www.mozilla.org/en-US/moss/) - 為開源軟體開發提供資助,特別是與 Mozilla 使命相符的專案。 - [Linux 基金會資助](https://www.linuxfoundation.org/) - 為從事 Linux 基金會專案的開發人員提供各種資助和獎學金。 - [NumFOCUS 小額發展補助金](https://numfocus.org/programs/small-development-grants) - 支援資料科學和科學計算的小型專案。資助金額各不相同(所有申請人均分配 285,000 美元)。 - [Apache 軟體基金會贊助](https://www.apache.org/foundation/sponsorship.html) - 對 Apache 軟體專案的財務支持,重點關注 Apache 軟體生態系統。 - [Outreachy](https://www.outreachy.org/) - 為技術領域代表性不足的群體提供為期三個月的實習機會,津貼通常約為 5,500 美元。 - [奈特基金會](https://knightfoundation.org/grants/) - 為促進優質新聞業的技術專案提供資助。根據專案範圍的不同,贈款金額差異很大。 - [原型基金](https://prototypefund.de/) - 在六個月內提供高達 47,500 歐元的開源原型支持,重點支持德國的軟體開發人員。 - [斯隆基金會](https://sloan.org/programs/digital-technology) - 為開放科學社群計畫提供資助,特別是那些增強研究中的開源軟體的計畫。 - [Chan Zuckerberg Initiative 開源軟體專案](https://chanzuckerberg.com/rfa/) - 專注於支援對生物醫學研究至關重要的開源軟體。資助金額各不相同。 - [Raspberry Pi 基金會](https://www.raspberrypi.org/grants/) - 為涉及 Raspberry Pi 和計算教育的教育計畫提供補助。 - [GitCoin](https://gitcoin.co/) - 一個為開源專案提供資金的眾籌平台,特別是在以太坊和 Web3 領域。資金根據社區支持而有所不同。 - [NLnet 基金會](https://nlnet.nl/foundation/) - 支援網路科技與網路研究計畫。補助金額各不相同。 - [開放技術基金](https://www.opentech.fund/) - 支持開發促進人權和開放社會的開放技術的專案。資金各不相同。 --- ### 19. 舉辦活動 活動空間是一個利潤豐厚的行業,尤其是如果您能夠舉辦一場精彩的活動並為自己贏得大型贊助商的話。雖然不適合所有人,但舉辦活動可以帶來以下 10 個潛在收入來源: - **門票銷售**:透過收取入場費來產生收入。使用 [Eventbrite](https://www.eventbrite.co.uk/)、[Meetup](https://meetup.com/) 或 [Ticketmaster](https://ticketmaster.com) 等平台取得門票管理。 - **贊助**:確保科技公司的財務捐助,以換取活動中的促銷機會。 - **研討會和培訓課程**:提供特定技術或程式語言的專業實務學習經驗,收取額外費用。 - **虛擬活動**:使用[Zoom](https://zoom.us/)、[WebEx](https://www.webex.com/) 或 [Hopin](https://hopin.com/)。 - **黑客馬拉松**:舉辦收取報名費的程式設計競賽,或尋找贊助商來支付費用並提供獎金。 - **社交活動**:針對技術專業人士的社交活動收費,可能會吸引招聘公司的贊助。 - **演講活動**:利用您在特定技術領域的專業知識,組織並負責演講活動或小組討論。 - **企業培訓及靜修**:為企業內部培訓或團隊建立活動提供活動組織服務。 - **聯盟行銷**:在活動期間利用科技產品或服務的聯盟行銷來獲取額外收入。 - **產品發布**:與科技公司合作舉辦產品發布活動,為您提供收費的組織服務。 --- ### 20.研究 您的意見很有價值,尤其是作為開發人員。有些研究人員會付錢給你參加他們的研究、調查或智庫。通常,好的研究機會很少而且相距甚遠,或者報酬相當低。 這類工作的熱門平台包括:[Testable Minds](https://minds.testable.org/)、[Respondent](https://app.respondent.io/signup) --- ### 21. 建立課程 - [Skillshare](https://www.skillshare.com/teach) - 根據課程觀看分鐘數提供付款以及推薦獎金。 - [Coursera](https://www.coursera.org/for-universities) - 與機構合作提供課程;付款通常基於收入分享協議。 - [LinkedIn Learning](https://www.linkedin.com/learning/instructors) - 講師可以為專業人士建立課程;薪酬詳細資訊由 LinkedIn 安排。 - [Thinkific](https://www.thinkific.com/) - 提供建立、行銷和銷售線上課程的工具,具有各種定價計劃,包括免費選項。 - [Kajabi](https://kajabi.com/) - 線上課程、行銷、支付和網站建立的一體化平台。 - [Podia](https://www.podia.com/) - 提供一個用於舉辦課程、網路研討會和數位下載的平台,並直接向觀眾銷售。 - [Pluralsight](https://www.pluralsight.com/teach) - 專注於科技與創意課程;根據課程的受歡迎程度向教師支付版稅。 - [MasterClass](https://www.masterclass.com/teach) - 高品質、名人主導的課程;講師通常是各自領域的知名專家或名人。 - [uTeach](https://ueach.io/) - [NewLine](https://www.newline.co/) --- ### 22.時事通訊 隨著流行的社群媒體管道變得更加集中和受控,電子郵件通訊和基於訂閱的 RSS 來源正在慢慢捲土重來。 這種模式的工作方式要么是提供對技術主題或新聞的有價值的見解,並建立一個龐大的(因此有價值的)訂閱者基礎,要么是向少數用戶收取更新費用。 提供此功能的流行平台包括: - [子堆疊](https://substack.com) - [ButtonDown](https://buttondown.email/) - [ConvertKit](https://convertkit.com/) - [穩定](https://steadyhq.com) - [幽靈](https://ghost.org/) --- ### 23. 僅限會員的網站 - [MemberSpace](https://www.memberspace.com/) - 讓您能夠為網站的某些部分付費,僅供會員使用 - [Patreon](https://www.patreon.com/) - 因設定具有獨家內容和福利的會員等級而廣受歡迎。 - [Substack](https://substack.com/) - 新聞通訊的理想選擇;提供付費訂閱獨家內容的能力。 - [Ghost](https://ghost.org/) - 內建會員和訂閱功能的專業發布平台。 - [Podia](https://www.podia.com/) - 允許銷售會員資格、線上課程和數位下載。 - WordPress 與 [MemberPress 外掛程式](https://memberpress.com/) - 供 WP 使用者建立會員網站的外掛程式。 - [Wild Apricot](https://www.wildapricot.com/) - 與您的網站整合的會員管理軟體。 - [Kajabi](https://kajabi.com/) - 提供用於建立線上課程、會員網站等的工具,重點是行銷。 - [Mighty Networks](https://www.mightynetworks.com/) - 建立一個包含會員資格、訂閱和課程的社群。 --- ### 24. VIP 貼文 還有許多公司會為您在其平台上分享的優質貼文付費。這既可以提高您的知名度(幫助您擴大人脈並獲得未來的工作),也可以帶來一些短期收入。 如果您正在努力獲得這些計劃的錄取,請先編寫自己的帖子並將其發佈到流行的基於開發的社交網絡(例如 DEV.to!)。這將增強您的寫作技巧,並幫助您向潛在公司展示您的知識。 例如,以下網站將為高品質的訪客貼文付費: - [Linode](https://www.linode.com/lp/write-for-linode/) - [日誌火箭](https://blog.logrocket.com/become-a-logrocket-guest-author/) - [Smashing 雜誌](https://www.smashingmagazine.com/contact/?Becoming%20an%20Author/Reviewer%20(自動回覆)) - [Auth0](https://auth0.com/apollo-program) - [CSS 技巧](https://css-tricks.com/guest-writing-for-css-tricks/) - [DelftStack](https://www.delftstack.com/write-for-us/) - [DigitalOcean](https://www.digitalocean.com/community/pages/write-for-digitalocean) - [Infatica](https://infatica.io/contribute/) - [蜜罐](https://blog.honeypot.io/write-for-honeypot/) - [進階編碼](https://premiumcoding.com/write-for-us-premiumcoding/) - [反思](https://reflectoring.io/contribute/become-an-author/) - [Strapi](https://strapi.io/write-for-the-community) - [Android 權威](https://www.authoritymedia.com/jobs) - [SitePoint](https://www.sitepoint.com/write-for-us/) - [TutorialsPoint](https://www.tutorialspoint.com/about/tutorials_writing.htm) - [真正的Python](https://realpython.com/jobs/tutorial-writer/) - [Dart Creations](https://www.dart-creations.com/about-us/write-for-us.html) Dmytro Spilka 編制了一份包含 300 多個[接受訪客貼文的網站](https://solvid.co.uk/180-websites-that-accept-guest-posts/) 的清單。另一個很棒的清單[由 Julia 在 Dev.to 上整理](https://dev.to/juliafmorgado/get-paid-to-write-technical-articles-16cl)。 --- ### 25. 諮詢 您可能沒有意識到,您從日常工作中累積的技能和經驗對許多公司來說可能非常有價值。尤其是尚無法聘請全職專家的新創公司和小型企業。對能夠提供最新趨勢、工具和最佳實踐見解的專業人士的需求非常高。 尖端: - 以適當的速度開始的最佳方式是透過網路和口碑。但如果做不到這一點,總有自由工作網站可以幫助您累積經驗。 - 記錄你所獲得的經驗,或在你工作的過程中建立一個投資組合,因為這將幫助你在未來獲得更好的工作。 - 在開始任何專案之前,請先明確您的空閒時間、條款、日薪和工作範圍。 - 切勿拒絕潛在的聯絡人。您會驚訝地發現,即使多年後,誰可能會重新與您聯繫並尋求諮詢支援。 --- ### 26. 指導 無論您的級別如何,您作為開發人員的經驗都可以真正幫助經驗不足的其他人。指導是一種非常有益的方式,可以幫助他人,同時也能帶來一些額外的收入。 - [MentorCruise](https://mentorcruise.com/) - 主要是長期的,按月付費,非常適合建立專業關係(每個學員每月賺取 50-500 美元) - [CodeMentor](https://www.codementor.io/) - 更適合短期,按小時收費,非常適合解決特定問題(每小時賺取 60-300 美元) --- ### 27.輔導 隨著 CompSci 現在成為國家課程的一部分(至少在英國和大部分歐洲),大量學生(11 歲至 18 歲以上)正在尋找導師來幫助他們獲得編碼技能並準備考試。收入範圍為每小時 15 美元到 150 美元以上,具體取決於級別、經驗和背景。 - [Super Prof](https://www.superprof.co.uk/) - 列出您的全球服務(30-300 美元/小時) - [The Profs](https://www.theprofs.co.uk/become-a-private-tutor/) - 經過驗證的導師(收入未知) - [我的導師](https://www.mytutor.co.uk/) - 僅限英國,(22-55 英鎊/小時) - [Tutor.com](https://www.tutor.com/) - 美國高中學費($75-$100/小時) --- ### 28.社群媒體 市場存在巨大空白,等待主流社群媒體平台上真正優秀的、注重發展的影響者來填補。 許多社群媒體平台允許您透過內容貨幣化,您通常會按觀看次數付費,金額根據內容類別、地區和聲譽而有所不同。但請注意,您通常必須擁有一定數量的追蹤者才有資格,而且您還將受到「演算法」的支配。 - YouTube - 每年至少需要 1,000 訂閱者 + 4,000 小時觀看時間 - X - 需要 Twitter Blue 訂閱,無最低追蹤人數 - TikTok - 需要至少 10k 追蹤者 + 100k 瀏覽量/月 - Instagram - 需要至少 10k 追蹤者 - Snap - 1,000 名追蹤者,1,000 次瀏覽/月,10 多個每月貼文 - Facebook - 10k 追蹤者或 600k 影片觀看分鐘 - Twitch - 350 位每月付費訂閱者 --- ### 29.品牌優惠 繼上面的社群媒體部分之後,一旦您成功突破了數百名訂閱者,您可能還可以開始考慮品牌交易,這有助於帶來額外收入。同樣,這些需要您的受眾達到一定程度的參與度,您可能還需要同意提供贊助的公司的條款。 --- ### 30.串流媒體 開發串流是一個快速成長的利基市場,不要指望立即[加入排行榜](https://twitch.pages.dev/),但它可能是一個很好的起點,特別是如果您已經有串流媒體經驗(例如影片遊戲)。 Nick Taylor 寫了一篇關於 [開發串流媒體入門的精彩文章](https://dev.to/nickytonline/getting-started-with-streaming-on-twitch-4im7)。 --- ### 31.SaaS 如果您能夠做到這一點,那麼它就是開源專案的最佳收入模式之一。您的程式碼仍然是 100% 免費和開源的,用戶仍然可以免費下載和自行託管它,但您還提供付費/託管計劃,您可以在其中託管應用程式並負責小型伺服器的所有伺服器管理經常性費用。 此模型符合開源精神,同時也使您的應用程式可供更廣泛的用戶使用。 [Stripe](https://stripe.com/docs/payments) 等服務讓您的應用程式接受付款和新增訂閱功能變得非常簡單。 --- ### 32.微型 SaaS 如果從頭開始建立一個生產就緒的應用程式聽起來像是一項艱鉅的任務(因為它確實如此!),那麼另一種方法就是 Micro-SaaS 應用程式。這些是較小的應用程式,它們執行一項非常具體的任務,例如: - 自動執行重複和/或乏味的任務。 - 執行目前手動計算的計算。 - 連接不同的系統。 - 取代 Excel 電子表格解決方法。 - 填補宿主生態系中缺失功能的空白 - 加強報告 --- ### 33. 寫外掛 與 SaaS 應用程式不同,一旦建置並發布了擴展,通常不需要太多的持續管理。您也可能會發現,如果您的專案為已經完善的網站加入功能,那麼您的專案會更容易快速獲得關注。 儘管網路擴展似乎是一個過時的或完全飽和的市場,但仍然有很多可以做的事情,而且這些對於新開發人員來說都是很棒的專案。 以下是一些可以幫助您入門的想法: - [WA Web Plus](https://chrome.google.com/webstore/detail/wa-web-plus-by-elbruz-tec/ekcgkejcjdcmonfpmnljobemcbpnkamh) 已下載 200 萬次(22k 評級),收費 12 美元/每個用戶的月。為什麼不為 Telegram、Threema、Wire、Messenger 等建立類似的東西呢? - Runkeeper擁有4500萬用戶,但UI在資料顯示方式方面有所欠缺。為什麼不建立一個擴充功能來加入更好的報告、過濾以及與相關外部資料的組合? (與 [Elevate for Strava](https://chrome.google.com/webstore/detail/elevate-for-strava/dhiaggccakkgdfcadnklkbljcgicpckn) 類似,但適用於 RunKeeper) - 選擇一個提供基本服務但 UI 過於不切實際的網站(也許是 Microsoft Azure?),然後建立一個擴充功能以簡化導覽、顯示關鍵指標或提供不那麼難看的使用者體驗 - 使用人工智慧增強任何現有網站。這比聽起來容易得多,您的擴充功能可以利用 OpenAI 的 API 等服務來總結網頁,或重新措辭選定的內容(用於複製/貼上到作業中!?) - 如果您知道某個網站的使用者數量很高,但 UI 很糟糕,那麼一個簡單的擴充想法可以是應用 CSS 覆蓋來重新設計它的樣式。例如亞馬遜、雅虎、Instagram 都是高流量網站,設計改善空間巨大(深色模式?!) - 即使是簡單的獨立擴展應用程式也可能具有很大的潛力。就像番茄計時器、貨幣轉換器、IP 位址小部件或只是一個網路應用程式快捷方式。 --- ### 34. 發布應用程式 建立簡單的應用程式或遊戲,並將其在平台應用程式商店上提供,使您能夠透過簡單的盈利模型來瞄準數百萬客戶。所有主流應用程式商店 - Google Play、Apple App Store、Windows Store、Steam 等都提供對付費應用程式、進階功能和應用程式內購買的支援。 請記住,在發布第一個應用程式之前通常需要支付安裝費,應用程式商店也會從您的收入中抽取一部分,並且小型創作者獲得單次或雙次下載的情況並不罕見。人物。 --- ### 35. 為小型企業開發網站 許多小型企業都專注於自己的業務,沒有時間或專業知識建立自己的網站。作為開發人員,這是我們能夠很快完成的事情,如果您也託管他們的網站,您將能夠收取定期付款。 一旦您開始進行網頁設計和開發,並為一些客戶提供服務,您就會發現透過口碑和展示您的作品集來找到未來的工作要容易得多。 為了在這方面取得成功,您可能還需要設計、溝通和銷售方面的技能。 --- ### 36. 兜售網域 隨著新 TLD 的湧入,域名經銷商市場正在迎來第二波受歡迎。域名翻轉涉及註冊未來可能有價值的域名,然後將其轉售給想要將該名稱用於企業或專案的買家。 雖然這可能有利可圖,但它確實涉及高風險,並且需要對市場有充分的了解。 尖端: - 研究簡短或令人難忘的域名,或者可能具有較高關鍵字潛力的域名(您可以使用諸如使用 Google 關鍵字規劃師等工具來幫助進行這項研究) - 停放您目前未使用的域名,以便您同時獲得一些廣告收入 - 查看最近過期的域名,特別是那些正在使用的域名,因為這些域名可能會收到流量 - 接收流量的網域更有價值。因此,請考慮在您持有網域時為其建立網站、應用程式或登入頁面 --- ### 37. 使用者測試 開發應用程式的公司通常需要獲得用戶的回饋。這就是用戶測試服務的用武之地。您花 10-30 分鐘嘗試給定的網站或應用程式,然後提供反饋或填寫調查,並獲得報酬! 儘管並非特定於開發人員,但憑藉您的技術背景,您會發現自己具有獨特的優勢,可以快速完成這些工作並提供良好的反饋,從而使您比普通用戶更快地賺錢。您還將獲得有關用戶測試流程如何運作的寶貴見解,這可能對您在自己的應用程式上進行委託測試時有用。 - [嘗試我的 UI](https://www.trymyui.com/) - 每個網站或應用程式測試平均費用為 10 美元 - [Userlytics](https://www.userlytics.com/user-experience-research/paid-ux-testing/) - 根據測試的複雜程度和長度,賺取 5 至 50 美元之間 - [使用者測試](https://www.usertesting.com/get-paid-to-test) - 透過 PayPal 付款,在測試會話期間需要螢幕共用和/或網路攝影機存取。每次測試賺取約 10 美元,較長時間或現場會議的某些測試最高可支付 50 美元 - [TestingTime](https://www.testingtime.com/en/become-a-paid-testuser/) - 面對面或視訊通話測試的選項。不太定期,但測試時間更長。當您考慮到會話之間的延遲時,報酬比其他選擇更低 - [IntelliZoom](https://www.intellizoom.com/) - 每 10 分鐘學習可賺取 2 至 10 美元。透過 PayPal 付款,延遲 3-5 天 --- ### 38.微任務 與開發人員的具體關係不大,但如果您來自技術背景,您可能會發現這些工作比那些沒有開發技能的工作更有利可圖。 - [Amazon Mechanical Turk](https://www.mturk.com/) - 外包虛擬微任務的眾包市場 - [Sequence Works](https://sequence.work/contributors/) - 影像標註、資料標記與分類 - [App Jobber](https://en.appjobber.com/) - 市場調查,去商店拍攝特定植入式廣告的照片 - [GigWalk](https://www.gigwalk.com/gigwalkers/) - 應用程式為基礎的行動微任務 - 請造訪 [GigWorker.com](https://gigworker.com/) 以了解更多微任務和零工工作 --- ### 39. 調查 儘管對具有某些技能(如軟體工程)的參與者的需求較高,但調查的報酬往往很低,因此可以賺更多一點。即便如此,除非您有大量時間,或使用比美元弱得多的貨幣,否則這可能不是一個好的選擇。 這些通常涉及測試新產品或服務,並提供回饋 - 或回答問題以協助市場研究活動。 有很多不同的基於調查的公司,所以我不會全部連結到它們。但 [Swagbucks](https://www.swagbucks.com/)、[20Cogs](https://20cogs.co.uk/)、[TestingTime](https://www.testingtime.com/en/become-a-paid-testuser) 是一些著名的。 --- ### 40.去中心化節點 這可能不適合所有人,因為收益通常以加密貨幣形式支付,而加密貨幣的波動性非常大。但是,您可以自願為許多 Web3 專案執行節點(通常在 Rasperry Pi、雲端伺服器或備用筆記型電腦上),這將為您支付正常執行時間、頻寬、磁碟空間、運算、IP/代理或其他一些費用。計算資源。 作為開發人員,管理基礎設施是我們所擅長的,因此,如果您有任何閒置資源,您也許可以將它們投入使用,並在睡覺時賺取一些額外的現金。 - [Storj](https://www.storj.io/node):執行Storj節點,用於去中心化雲端運算 - [Network3](https://network3.io/):用於訓練和驗證模型的 AIoT 第 2 層 - [Flux](https://runonflux.io/):去中心化基礎設施 - [Mysterium](https://mystnodes.com/): P2P VPN 節點 - [Koii](https://www.koii.network/node): 分散式雲 - [Helium](https://www.helium.com/mine):提供遠端物聯網設備無線連接 - [Filecoin](https://filecoin.io/):它是一個去中心化儲存網絡,將雲端儲存轉變為演算法市場。用戶可以出租閒置的儲存空間並賺取 Filecoin 代幣。 - [Sia Network](https://sia.tech/host):這是一個由區塊鏈技術所保障的去中心化儲存平台。 Sia 透過去中心化網路儲存和加密您的檔案。您可以透過出租未使用的硬碟空間來賺取 Siacoins。 - [Crust Network](https://wiki.crust.network/docs/en/nodeOverview):與 Filecoin、Sia 類似,Crust 支援 IPFS 等多種儲存層協議,並為應用層提供儲存介面。 - [Arweave](https://www.arweave.org/):一個基於區塊鏈的平台,以永久和去中心化的方式提供資料儲存。透過託管資料,用戶可以獲得 Arweave 代幣獎勵。 - [BitTorrent](https://docs.btfs.io/v2.0/docs/install-run-btfs20-node):該平台標記了世界上最大的文件共享協議,使用戶能夠通過在網路上。 - [HOLO](https://holo.host/):Holochain 應用程式 (hApps) 的點對點託管平台。在電腦上託管 hApp 的用戶將獲得 HOT 代幣獎勵。 --- ### 41.其他 Web3 方法 加密產業還有許多其他賺取被動收入的方式,從PoS 質押、持有生息數位資產、借貸、流動性挖礦、雲端挖礦、賺取股息的代幣、流動性挖礦、交易、本地/ PoW 思維、NFT 等等。很少。 我不會在這裡連結到任何具體細節,因為這是一個風險非常高的行業,因此您自己進行研究很重要。但作為技術專家,我們能夠理解任何給定協議或 Web3 資產背後的基本概念,並確定其可行性。 我的建議是閱讀白皮書,如果你不能立即理解它,那就遠離它!這是狂野的西部,所以除非一個專案的基本面是堅實的,否則你應該做好失去投資的任何資金的準備。 --- ### 42. 聯盟行銷 聯盟行銷對於那些剛開始的人來說是眾所周知的無利可圖,但我將其包含在此處是因為作為開發人員,有一定的空間來自動化許多過程。此外,您行銷的服務越細分,支付的佣金通常就越高。因此,如果您融入了技術社區,您可能處於銷售小批量高回報服務的有利位置。 同樣,如果您已經有了追蹤者(社交、部落格、YouTube 頻道...),那麼聯盟行銷可能更有意義,因為如果您獲得了大量點擊。 值得注意的是,您可能不應該在未透露它是附屬連結的情況下共享附屬連結。並儘量避免宣傳您自己沒有使用過或不會推薦給朋友的產品。 > 作為範例,[此處](https://notes.aliciasykes.com/p/3Ia4JzPw43) 是我使用過且擁有附屬帳戶的一些服務。我從未從其中任何一個身上賺過任何有意義的錢。 --- ### 43.經銷商 這涉及建立一個應用程式來包裝現有服務,同時加入 USP - 技術、客戶支援、UI 或其他功能。如果您有行銷或銷售背景,這可能適合您。如果您想加入功能或使流程自動化,那麼將需要大量的前期工作,但您將能夠更好地獲得收入。 您可以在大多數主要行業中找到提供經銷商計劃的服務提供者。 一些例子包括: - [Supermetrics](https://supermetrics.com/):行銷報告、分析、資料整合、20% 經常性佣金。 - [Keap](https://keap.com/):CRM、銷售與行銷自動化、20-30% 經常性佣金。 - [Klaviyo](https://www.klaviyo.com/):電子郵件與簡訊行銷,5–15% 一次性付款,10–20% 收入分成。 - [Drift](https://www.drift.com/):即時聊天軟體,20%收益分成。 - [ActiveCampaign](https://www.activecampaign.com/):電子郵件行銷、CRM、20–30% 佣金或折扣模式。 - [HubSpot](https://www.hubspot.com/):CRM、入站行銷、銷售、20% 營收分成。 - [Gorgias](https://www.gorgias.com/):電子商務幫助台,20% 收入分成。 - [Shopify](https://www.shopify.com/):電子商務平台,佣金20%,Shopify Plus 10%。 - [LiveChat](https://partners.livechat.com/):客戶服務平台,即時聊天,委託20%。 - [GetResponse](https://www.getresponse.com/):電子郵件行銷、線上活動管理、子帳號 35% 折扣、35% 經常性佣金。 --- ### 44. 人體實驗 這與技術根本無關。但作為程式設計師,我們通常可以在任何地方工作 - 那麼為什麼不在有報酬的地方編寫程式碼呢? 通常,您的收入在 2,000 美元到 10,000 美元之間,具體取決於試用期、持續時間、是否為住宅和具體情況。 像[流感營](https://flucamp.com/) 這樣的地方將支付您 4,000 英鎊,讓您在舒適的酒店式套房中入住兩週,同時他們會測試新的治療方法。那些患有氣喘等特定疾病的人可能可以透過參加更專業的試驗來賺取更多收入 --- ### 45.自由職業 自由職業可能會根據您的技能、經驗和您所在的領域而有所不同。對於新自由工作者來說,某些領域(例如網頁開發)的費率往往非常低,但您擁有的經驗和客戶滿意度越高,您就越能夠充電。 開發人員零工工作的三個主要平台是: - [Fiverr](https://www.Fiverr.com/):Fiverr 以其多元化的市場而聞名,非常適合剛開始從事自由職業的開發人員 - [Upwork](https://www.upwork.com/work):Upwork 迎合了廣泛的專業人士,但它對經驗豐富的開發人員特別有利。它提供了長期合約和高薪工作的潛力。該平台適合喜歡從事更實質專案的人。 - [People per Hour](https://www.peopleperhour.com/):這個平台對歐洲市場的開發者有好處。它強調當地的商業聯繫,並在短期和長期專案之間提供良好的平衡。 --- ### 46. 說話 面對面的和移除的開發者聚會和活動在全球各地不斷發生。這些活動需要演講者,許多人願意付費以獲得良好的演講。支付的金額根據規模、觀眾、主題、演講者(你!)和其他因素而有很大差異。通常,您必須先自願在當地的小型技術聚會上發表演講,然後逐步提高。 --- ### 47. 遠端技術支持 這不是最迷人的角色,但較小的公司通常無法聘請全職的專門技術支援人員,因此您可以找到很多兼職工作。如果您擁有雲端經驗或認證,這些的薪資等級會大幅提高。只需查看任何求職板(例如 [WeWorkRemotley](https://weworkremotely.com/)),您就會看到大量職位。 請注意,您通常需要在特定時間內有空,並期望您可以在給定的時間內回覆。在申請之前,請確保這是您可以解決的問題。 --- ### 49. 投資 是的,這不是副業——但聽我說完… 如果您每年的收入為 6 萬美元,生活成本為 4 萬美元,那麼 5 年後您可能會有 10 萬美元的儲蓄。如果您將其投資於年平均回報率為10 - 15% 的標準普爾500 指數- 您每月可能會從您的投資中獲得超過1,000 美元的額外收入,並且您的投資能力越強,收入就會不斷增加儲蓄(當然,投資可以減少也可以增加)。這已經比這裡列出的許多副業更好的回報了! --- ### 50. 就業 我們不要忘記,儘管目前情況看起來很艱難,但身為開發人員,即使只有一兩年的經驗,我們也處於非常幸運的地位,與平均收入者相比,我們的薪水很高。 如果你的工作不適合你——換公司通常是提高薪資的可靠方法,如果你不喜歡目前的工作,這可能是值得考慮的事情。 也許經歷了這一切之後,你所追求的不是副業,而是更好的「主業」? --- ## 真實的說話 儘管您可能會在 IndieHackers 和 Instagram 上看到一些內容,但副業並不是全部。這通常需要大量的工作,但回報卻非常有限。因此,在在這裡或其他地方進行任何事情之前,請退後一步,思考「我為什麼要這樣做?」。如果您這樣做是為了累積經驗、學習新技能並享受樂趣——那就太好了。如果你這樣做是為了快速致富 - 你可能會非常失望。 還有一點要注意的是,儘管看起來不公平,但與那些剛起步的人相比,那些已經擁有強大追隨者或幾個成功的開源專案的人將處於更好的位置來利用機會。 因此,從短期來看,作為一名開發人員,您的時間可能會更好地花在提升自己身上。如果您不確定從哪裡開始,這裡有 5 個關鍵提示: - **網絡** - 建立你的網絡,參加聚會、黑客馬拉松和開發活動,加入社區,結交朋友 - **開源** - 將您的工作放在那裡,公開學習,建立您感興趣的迷你專案,並且不要害怕失敗 - **經驗** - 獲得實務經驗,申請實習機會,作為自由開發人員提供服務 - **基礎知識** - 確保您對電腦科學基礎知識有深入的了解,其餘的就會容易得多 - **玩得開心!** - 你自然會在你真正熱愛的領域做得更好。如果你不喜歡你正在做的事情,請退一步,考慮不同的方法是否更適合你 --- ## 免責聲明 - 以上列表僅供您參考。 - 我沒有親自測試過這裡列出的所有服務。 - 如果您有 - 我很想聽聽您的回饋。同樣,如果有任何需要加入或刪除的內容,請在下面告訴我。 - 並非所有服務都在所有國家/地區提供(此清單主要針對英國/歐洲和美國🇬🇧🇪🇺🇺🇸) - 有些平台會抽取您的收入。這通常是一個很小的數額,但重要的是你要考慮到 - 如果您已經擁有大量追隨者或流行的開源專案,賺錢通常會容易得多 - 有些方法涉及風險。儘管我已盡力強調這一點,但請記住,您的投資可能會下降而不是上升 - 您的結果可能會有所不同 - 沒有保證 --- 原文出處:https://dev.to/lissy93/50-ways-to-bring-in-extra-cash-as-a-developer-19b6

🔥 大幅提升你的 NextJS 能力:嘗試手寫一個 GitHub 星星監視器 🤯

在本文中,您將學習如何建立 **GitHub 星數監視器** 來檢查您幾個月內的星數以及每天獲得的星數。 - 使用 GitHub API 取得目前每天收到的星星數量。 - 在螢幕上每天繪製美麗的星星圖表。 - 創造一個工作來每天收集新星星。 ![吉米](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n524rmr0gpgr79p4qlhj.gif) --- ## 你的後台工作平台🔌 [Trigger.dev](https://trigger.dev/) 是一個開源程式庫,可讓您使用 NextJS、Remix、Astro 等為您的應用程式建立和監控長時間執行的作業!   [![GiveUsStars](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bm9mrmovmn26izyik95z.gif)](https://github.com/triggerdotdev/trigger.dev) 請幫我們一顆星🥹。 這將幫助我們建立更多這樣的文章💖 https://github.com/triggerdotdev/trigger.dev --- ## 這是你需要知道的 😻 取得 GitHub 上星星數量的大部分工作將透過 GitHub API 完成。 GitHub API 有一些限制: - 每個請求最多 100 名觀星者 - 最多 100 個同時請求 - 每小時最多 60 個請求 [TriggerDev](https://github.com/triggerdotdev/trigger.dev) 儲存庫擁有超過 5000 顆星,實際上不可能在合理的時間內(即時)計算所有星數。 因此,我們將採用與 [GitHub Stars History](https://star-history.com/) 相同的技巧。 - 取得星星總數 (**5,715**) 除以每頁 **100** 結果 = **58 頁** - 設定我們想要的最大請求量(**20 頁最大**)除以 **58 頁** = 跳過 3 頁。 - 從這些頁面中獲取星星**(2000 顆星)**,然後獲取剩餘的星星,我們將按比例加入到其他日期(**3715 顆星**)。 它會為我們繪製一個漂亮的圖表,並在需要的地方用星星凸起。 當我們每天獲取新數量的星星時,事情就會變得容易得多。 我們將用目前擁有的星星總數減去 GitHub 上的新星星數量。 **我們不再需要迭代觀星者。** --- ## 讓我們來設定一下 🔥 我們的申請將包含一頁: - 新增您想要監控的儲存庫。 - 查看儲存庫清單及其 GitHub 星圖。 - 刪除那些你不再想要的。 ![StarsOverTime](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rbii15mn1tyuz63kjphk.png) > 💡 我們將使用 NextJS 新的應用程式路由器,在安裝專案之前請確保您的節點版本為 18+。 > 使用 NextJS 設定一個新專案 ``` npx create-next-app@latest ``` 我們必須將所有星星保存到我們的資料庫中! 在我們的示範中,我們將使用 SQLite 和 `Prisma`。 它非常容易安裝,但可以隨意使用任何其他資料庫。 ``` npm install prisma @prisma/client --save ``` 在我們的專案中安裝 Prisma ``` npx prisma init --datasource-provider sqlite ``` 轉到“prisma/schema.prisma”並將其替換為以下模式: ``` generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } model Repository { id String @id @default(uuid()) month Int year Int day Int name String stars Int @@unique([name, day, month, year]) } ``` 然後執行 ``` npx prisma db push ``` 我們基本上已經在 SQLite 資料庫中建立了一個名為「Repository」的新表: - 「月」、「年」、「日」是日期。 - `name` 儲存庫的名稱 - 「星星」以及該特定日期的星星數量。 你還可以看到我們在底部加入了一個`@@unique`,這意味著我們可以將`name`,`month`,`year`,`day`一起重複記錄。它會拋出一個錯誤。 讓我們新增 Prisma 客戶端。 建立一個名為「helper」的新資料夾,並新增一個名為「prisma.ts」的新文件,並在其中新增以下程式碼: ``` import {PrismaClient} from '@prisma/client'; export const prisma = new PrismaClient(); ``` 我們稍後可以使用該「prisma」變數來查詢我們的資料庫。 --- ## 應用程式 UI 骨架 💀 我們需要一些函式庫來完成本教學: - **Axios** - 向伺服器發送請求(如果您覺得更舒服,可以隨意使用 fetch) - **Dayjs -** 很棒的處理日期的函式庫。它是 moment.js 的替代品,但不再完全維護。 - **Lodash -** 很酷的資料結構庫。 - **react-hook-form -** 處理表單的最佳函式庫(驗證/值/等) - **chart.js** - 我選擇繪製 GitHub 星圖的函式庫。 讓我們安裝它們: ``` npm install axios dayjs lodash @types/lodash chart.js react-hook-form react-chartjs-2 --save ``` 建立一個名為“components”的新資料夾並新增一個名為“main.tsx”的新文件 新增以下程式碼: ``` "use client"; import {useForm} from "react-hook-form"; import axios from "axios"; import {Repository} from "@prisma/client"; import {useCallback, useState} from "react"; export default function Main() { const [repositoryState, setRepositoryState] = useState([]); const {register, handleSubmit} = useForm(); const submit = useCallback(async (data: any) => { const {data: repositoryResponse} = await axios.post('/api/repository', {todo: 'add', repository: data.name}); setRepositoryState([...repositoryState, ...repositoryResponse]); }, [repositoryState]) const deleteFromList = useCallback((val: List) => () => { axios.post('/api/repository', {todo: 'delete', repository: `https://github.com/${val.name}`}); setRepositoryState(repositoryState.filter(v => v.name !== val.name)); }, [repositoryState]) return ( <div className="w-full max-w-2xl mx-auto p-6 space-y-12"> <form className="flex items-center space-x-4" onSubmit={handleSubmit(submit)}> <input className="flex-grow p-3 border border-black/20 rounded-xl" placeholder="Add Git repository" type="text" {...register('name', {required: 'true'})} /> <button className="flex-shrink p-3 border border-black/20 rounded-xl" type="submit"> Add </button> </form> <div className="divide-y-2 divide-gray-300"> {repositoryState.map(val => ( <div key={val.name} className="space-y-4"> <div className="flex justify-between items-center py-10"> <h2 className="text-xl font-bold">{val.name}</h2> <button className="p-3 border border-black/20 rounded-xl bg-red-400" onClick={deleteFromList(val)}>Delete</button> </div> <div className="bg-white rounded-lg border p-10"> <div className="h-[300px]]"> {/* Charts Component */} </div> </div> </div> ))} </div> </div> ) } ``` **超簡單的React元件** - 允許我們新增新的 GitHub 庫並將其發送到伺服器 POST 的表單 - `/api/repository` `{todo: 'add'}` - 刪除我們不需要 POST 的儲存庫 - `/api/repository` `{todo: 'delete'}` - 所有新增的庫及其圖表的清單。 讓我們轉到本文的複雜部分,新增儲存庫。 --- ## 數星星 ![CountingStars](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4m2j6046myxwv2c8kwla.gif) 在「helper」內部建立一個名為「all.stars.ts」的新檔案並新增以下程式碼: ``` import axios from "axios"; import dayjs from "dayjs"; import utc from 'dayjs/plugin/utc'; dayjs.extend(utc); const requestAmount = 20; export const getAllGithubStars = async (owner: string, name: string) => { // Get the amount of stars from GitHub const totalStars = (await axios.get(`https://api.github.com/repos/${owner}/${name}`)).data.stargazers_count; // get total pages const totalPages = Math.ceil(totalStars / 100); // How many pages to skip? We don't want to spam requests const pageSkips = totalPages < requestAmount ? requestAmount : Math.ceil(totalPages / requestAmount); // Send all the requests at the same time const starsDates = (await Promise.all([...new Array(requestAmount)].map(async (_, index) => { const getPage = (index * pageSkips) || 1; return (await axios.get(`https://api.github.com/repos/${owner}/${name}/stargazers?per_page=100&page=${getPage}`, { headers: { Accept: "application/vnd.github.v3.star+json", }, })).data; }))).flatMap(p => p).reduce((acc: any, stars: any) => { const yearMonth = stars.starred_at.split('T')[0]; acc[yearMonth] = (acc[yearMonth] || 0) + 1; return acc; }, {}); // how many stars did we find from a total of `requestAmount` requests? const foundStars = Object.keys(starsDates).reduce((all, current) => all + starsDates[current], 0); // Find the earliest date const lowestMonthYear = Object.keys(starsDates).reduce((lowest, current) => { if (lowest.isAfter(dayjs.utc(current.split('T')[0]))) { return dayjs.utc(current.split('T')[0]); } return lowest; }, dayjs.utc()); // Count dates until today const splitDate = dayjs.utc().diff(lowestMonthYear, 'day') + 1; // Create an array with the amount of stars we didn't find const array = [...new Array(totalStars - foundStars)]; // Set the amount of value to add proportionally for each day let splitStars: any[][] = []; for (let i = splitDate; i > 0; i--) { splitStars.push(array.splice(0, Math.ceil(array.length / i))); } // Calculate the amount of stars for each day return [...new Array(splitDate)].map((_, index, arr) => { const yearMonthDay = lowestMonthYear.add(index, 'day').format('YYYY-MM-DD'); const value = starsDates[yearMonthDay] || 0; return { stars: value + splitStars[index].length, date: { month: +dayjs.utc(yearMonthDay).format('M'), year: +dayjs.utc(yearMonthDay).format('YYYY'), day: +dayjs.utc(yearMonthDay).format('D'), } }; }); } ``` 那麼這裡發生了什麼事: - `totalStars` - 我們計算圖書館擁有的星星總數。 - `totalPages` - 我們計算頁數 **(每頁 100 筆記錄)** - `pageSkips` - 由於我們最多需要 20 個請求,因此我們檢查每次必須跳過多少頁。 - `starsDates` - 我們填充每個日期的星星數量。 - `foundStars` - 由於我們跳過日期,我們需要計算實際找到的星星總數。 - `lowestMonthYear` - 尋找我們擁有的恆星的最早日期。 - `splitDate` - 最早的日期和今天之間有多少個日期? - `array` - 一個包含 `splitDate` 專案數量的空陣列。 - `splitStars` - 我們缺少的星星數量,需要按比例加入每個日期。 - 最終返回 - 新陣列包含自開始以來每天的星星數量。 所以,我們已經成功建立了一個每天可以給我們星星的函數。 我嘗試過這樣顯示,結果很混亂。 您可能想要顯示每個月的星星數量。 此外,您可能想要累積星星**而不是:** - 二月 - 300 顆星 - 三月 - 200 顆星 - 四月 - 400 顆星 **如果有這樣的就更好了:** - 二月 - 300 顆星 - 三月 - 500 顆星 - 四月 - 900 顆星 兩個選項都有效。 **這取決於你想展示什麼!** 因此,讓我們轉到 helper 資料夾並建立一個名為「get.list.ts」的新檔案。 這是文件的內容: ``` import {prisma} from "./prisma"; import {groupBy, sortBy} from "lodash"; import {Repository} from "@prisma/client"; function fixStars (arr: any[]): Array<{name: string, stars: number, month: number, year: number}> { return arr.map((current, index) => { return { ...current, stars: current.stars + arr.slice(index + 1, arr.length).reduce((acc, current) => acc + current.stars, 0), } }).reverse(); } export const getList = async (data?: Repository[]) => { const repo = data || await prisma.repository.findMany(); const uniqMonth = Object.values( groupBy( sortBy( Object.values( groupBy(repo, (p) => p.name + '-' + p.year + '-' + p.month)) .map(current => { const stars = current.reduce((acc, current) => acc + current.stars, 0); return { name: current[0].name, stars, month: current[0].month, year: current[0].year } }), [(p: any) => -p.year, (p: any) => -p.month] ),p => p.name) ); const fixMonthDesc = uniqMonth.map(p => fixStars(p)); return fixMonthDesc.map(p => ({ name: p[0].name, list: p })); } ``` 首先,它將所有按日的星星轉換為按月的星星。 稍後我們會累積每個月的星星數量。 這裡要注意的一件主要事情是 `data?: Repository[]` 是可選的。 我們制定了一個簡單的邏輯:如果我們不傳遞資料,它將為我們資料庫中的所有儲存庫傳遞資料。 如果我們傳遞資料,它只會對其起作用。 為什麼問? - 當我們建立一個新的儲存庫時,我們需要在將其新增至資料庫後處理特定的儲存庫資料。 - 當我們重新載入頁面時,我們需要取得所有資料。 現在,讓我們來處理我們的星星建立/刪除路線。 轉到“src/app/api”並建立一個名為“repository”的新資料夾。在該資料夾中,建立一個名為「route.tsx」的新檔案。 在那裡加入以下程式碼: ``` import {getAllGithubStars} from "../../../../helper/all.stars"; import {prisma} from "../../../../helper/prisma"; import {Repository} from "@prisma/client"; import {getList} from "../../../../helper/get.list"; export async function POST(request: Request) { const body = await request.json(); if (!body.repository) { return new Response(JSON.stringify({error: 'Repository is required'}), {status: 400}); } const {owner, name} = body.repository.match(/github.com\/(?<owner>.*)\/(?<name>.*)/).groups; if (!owner || !name) { return new Response(JSON.stringify({error: 'Repository is invalid'}), {status: 400}); } if (body.todo === 'delete') { await prisma.repository.deleteMany({ where: { name: `${owner}/${name}` } }); return new Response(JSON.stringify({deleted: true}), {status: 200}); } const starsMonth = await getAllGithubStars(owner, name); const repo: Repository[] = []; for (const stars of starsMonth) { repo.push( await prisma.repository.upsert({ where: { name_day_month_year: { name: `${owner}/${name}`, month: stars.date.month, year: stars.date.year, day: stars.date.day, }, }, update: { stars: stars.stars, }, create: { name: `${owner}/${name}`, month: stars.date.month, year: stars.date.year, day: stars.date.day, stars: stars.stars, } }) ); } return new Response(JSON.stringify(await getList(repo)), {status: 200}); } ``` 我們共享 DELETE 和 CREATE 路由,這些路由通常不應在生產中使用,但我們在本文中這樣做是為了讓您更輕鬆。 我們從請求中取得 JSON,檢查「repository」欄位是否存在,並且它是 GitHub 儲存庫的有效路徑。 如果是刪除請求,我們使用 prisma 根據儲存庫名稱從資料庫中刪除儲存庫並傳回請求。 如果是建立,我們使用 getAllGithubStars 來獲取資料以保存到我們的資料庫中。 > 💡 由於我們已經在 `name`、`month`、`year` 和 `day` 上放置了唯一索引,如果記錄已經存在,我們可以使用 `prisma` `upsert` 來更新資料 最後,我們將新累積的資料回傳給客戶端。 最困難的部分完成了🍾 --- ## 主頁人口 💽 我們還沒有建立我們的主頁元件。 **我們開始做吧。** 前往“app”資料夾建立或編輯“page.tsx”並新增以下程式碼: ``` "use server"; import Main from "@/components/main"; import {getList} from "../../helper/get.list"; export default async function Home() { const list: any[] = await getList(); return ( <Main list={list} /> ) } ``` 我們使用與 getList 相同的函數來取得累積的所有儲存庫的所有資料。 我們還修改主要元件以支援它。 編輯 `components/main.tsx` 並將其替換為: ``` "use client"; import {useForm} from "react-hook-form"; import axios from "axios"; import {Repository} from "@prisma/client"; import {useCallback, useState} from "react"; interface List { name: string, list: Repository[] } export default function Main({list}: {list: List[]}) { const [repositoryState, setRepositoryState] = useState(list); const {register, handleSubmit} = useForm(); const submit = useCallback(async (data: any) => { const {data: repositoryResponse} = await axios.post('/api/repository', {todo: 'add', repository: data.name}); setRepositoryState([...repositoryState, ...repositoryResponse]); }, [repositoryState]) const deleteFromList = useCallback((val: List) => () => { axios.post('/api/repository', {todo: 'delete', repository: `https://github.com/${val.name}`}); setRepositoryState(repositoryState.filter(v => v.name !== val.name)); }, [repositoryState]) return ( <div className="w-full max-w-2xl mx-auto p-6 space-y-12"> <form className="flex items-center space-x-4" onSubmit={handleSubmit(submit)}> <input className="flex-grow p-3 border border-black/20 rounded-xl" placeholder="Add Git repository" type="text" {...register('name', {required: 'true'})} /> <button className="flex-shrink p-3 border border-black/20 rounded-xl" type="submit"> Add </button> </form> <div className="divide-y-2 divide-gray-300"> {repositoryState.map(val => ( <div key={val.name} className="space-y-4"> <div className="flex justify-between items-center py-10"> <h2 className="text-xl font-bold">{val.name}</h2> <button className="p-3 border border-black/20 rounded-xl bg-red-400" onClick={deleteFromList(val)}>Delete</button> </div> <div className="bg-white rounded-lg border p-10"> <div className="h-[300px]]"> {/* Charts Components */} </div> </div> </div> ))} </div> </div> ) } ``` --- ## 顯示圖表! 📈 前往“components”資料夾並新增一個名為“chart.tsx”的新檔案。 新增以下程式碼: ``` "use client"; import {Repository} from "@prisma/client"; import {useMemo} from "react"; import React from 'react'; import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, } from 'chart.js'; import { Line } from 'react-chartjs-2'; ChartJS.register( CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend ); export default function ChartComponent({repository}: {repository: Repository[]}) { const labels = useMemo(() => { return repository.map(r => `${r.year}/${r.month}`); }, [repository]); const data = useMemo(() => ({ labels, datasets: [ { label: repository[0].name, data: repository.map(p => p.stars), borderColor: 'rgb(255, 99, 132)', backgroundColor: 'rgba(255, 99, 132, 0.5)', tension: 0.2, }, ], }), [repository]); return ( <Line options={{ responsive: true, }} data={data} /> ); } ``` 我們使用“chart.js”函式庫來繪製“Line”類型的圖表。 這非常簡單,因為我們在伺服器端完成了所有資料結構。 這裡需要注意的一件大事是我們「匯出預設值」我們的 ChartComponent。那是因為它使用了「Canvas」。這在伺服器端不可用,我們需要延遲載入該元件。 讓我們修改“main.tsx”: ``` "use client"; import {useForm} from "react-hook-form"; import axios from "axios"; import {Repository} from "@prisma/client"; import dynamic from "next/dynamic"; import {useCallback, useState} from "react"; const ChartComponent = dynamic(() => import('@/components/chart'), { ssr: false, }) interface List { name: string, list: Repository[] } export default function Main({list}: {list: List[]}) { const [repositoryState, setRepositoryState] = useState(list); const {register, handleSubmit} = useForm(); const submit = useCallback(async (data: any) => { const {data: repositoryResponse} = await axios.post('/api/repository', {todo: 'add', repository: data.name}); setRepositoryState([...repositoryState, ...repositoryResponse]); }, [repositoryState]) const deleteFromList = useCallback((val: List) => () => { axios.post('/api/repository', {todo: 'delete', repository: `https://github.com/${val.name}`}); setRepositoryState(repositoryState.filter(v => v.name !== val.name)); }, [repositoryState]) return ( <div className="w-full max-w-2xl mx-auto p-6 space-y-12"> <form className="flex items-center space-x-4" onSubmit={handleSubmit(submit)}> <input className="flex-grow p-3 border border-black/20 rounded-xl" placeholder="Add Git repository" type="text" {...register('name', {required: 'true'})} /> <button className="flex-shrink p-3 border border-black/20 rounded-xl" type="submit"> Add </button> </form> <div className="divide-y-2 divide-gray-300"> {repositoryState.map(val => ( <div key={val.name} className="space-y-4"> <div className="flex justify-between items-center py-10"> <h2 className="text-xl font-bold">{val.name}</h2> <button className="p-3 border border-black/20 rounded-xl bg-red-400" onClick={deleteFromList(val)}>Delete</button> </div> <div className="bg-white rounded-lg border p-10"> <div className="h-[300px]]"> <ChartComponent repository={val.list} /> </div> </div> </div> ))} </div> </div> ) } ``` 您可以看到我們使用“nextjs/dynamic”來延遲載入元件。 我希望將來 NextJS 能為客戶端元件加入類似「使用延遲載入」的內容 😺 --- ## 但是新星呢?來認識一下 Trigger.Dev! 每天加入新星星的最佳方法是執行 cron 請求來檢查新加入的星星並將其加入到我們的資料庫中。 不要使用 Vercel cron / GitHub 操作,或(上帝禁止)為此建立一個新伺服器。 我們可以使用 [Trigger.DEV](http://Trigger.DEV) 直接與我們的 NextJS 應用程式搭配使用。 那麼就讓我們來設定一下吧! 註冊 [Trigger.dev 帳號](https://trigger.dev/)。 註冊後,建立一個組織並為您的工作選擇一個專案名稱。 ![新組織](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bdnxq8o7el7t4utvgf1u.jpeg) 選擇 Next.js 作為您的框架,並按照將 Trigger.dev 新增至現有 Next.js 專案的流程進行操作。 ![NextJS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e4kt7e5r1mwg60atqfka.jpeg) 否則,請點選專案儀表板側邊欄選單上的「環境和 API 金鑰」。 ![開發金鑰](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ser7a2j5qft9vw8rfk0m.png) 複製您的 DEV 伺服器 API 金鑰並執行下面的程式碼片段以安裝 Trigger.dev。 仔細按照說明進行操作。 ``` npx @trigger.dev/cli@latest init ``` 在另一個終端中執行以下程式碼片段,在 Trigger.dev 和您的 Next.js 專案之間建立隧道。 ``` npx @trigger.dev/cli@latest dev ``` 讓我們建立 TriggerDev 作業! 您將看到一個新建立的資料夾,名為“jobs”。 在那裡建立一個名為“sync.stars.ts”的新文件 新增以下程式碼: ``` import { cronTrigger, invokeTrigger } from "@trigger.dev/sdk"; import { client } from "@/trigger"; import { prisma } from "../../helper/prisma"; import axios from "axios"; import { z } from "zod"; // Your first job // This Job will be triggered by an event, log a joke to the console, and then wait 5 seconds before logging the punchline. client.defineJob({ id: "sync-stars", name: "Sync Stars Daily", version: "0.0.1", // Run a cron every day at 23:00 AM trigger: cronTrigger({ cron: "0 23 * * *", }), run: async (payload, io, ctx) => { const repos = await io.runTask("get-stars", async () => { // get all libraries and current amount of stars return await prisma.repository.groupBy({ by: ["name"], _sum: { stars: true, }, }); }); //loop through all repos and invoke the Job that gets the latest stars for (const repo of repos) { getStars.invoke(repo.name, { name: repo.name, previousStarCount: repo?._sum?.stars || 0, }); } }, }); const getStars = client.defineJob({ id: "get-latest-stars", name: "Get latest stars", version: "0.0.1", // Run a cron every day at 23:00 AM trigger: invokeTrigger({ schema: z.object({ name: z.string(), previousStarCount: z.number(), }), }), run: async (payload, io, ctx) => { const stargazers_count = await io.runTask("get-stars", async () => { const { data } = await axios.get( `https://api.github.com/repos/${payload.name}`, { headers: { authorization: `token ${process.env.TOKEN}`, }, } ); return data.stargazers_count as number; }); await prisma.repository.upsert({ where: { name_day_month_year: { name: payload.name, month: new Date().getMonth() + 1, year: new Date().getFullYear(), day: new Date().getDate(), }, }, update: { stars: stargazers_count - payload.previousStarCount, }, create: { name: payload.name, stars: stargazers_count - payload.previousStarCount, month: new Date().getMonth() + 1, year: new Date().getFullYear(), day: new Date().getDate(), }, }); }, }); ``` 我們建立了一個名為“Sync Stars Daily”的新作業,該作業將在每天下午 23:00 執行 - 它在 cron 文本中的表示為:`0 23 * * *` 我們在資料庫中取得所有目前儲存庫,按名稱將它們分組,並對星星進行求和。 由於一切都在 Vercel 無伺服器上執行,因此我們可能會在檢查所有儲存庫時遇到逾時。 為此,我們將每個儲存庫傳送到不同的作業。 我們使用“invoke”建立新作業,然後在“獲取最新的星星”中處理它們 我們迭代所有新儲存庫並獲取當前的星星數量。 我們用舊的星星數量去除新的星星數量,得到今天的星星數量。 我們使用“prisma”將其新增至資料庫。沒有比這更簡單的了! 最後一件事是編輯“jobs/index.ts”並將內容替換為: ``` export * from "./sync.stars"; ``` 你就完成了🥳 --- ## 讓我們聯絡吧! 🔌 作為開源開發者,我們邀請您加入我們的[社群](https://discord.gg/nkqV9xBYWy),以做出貢獻並與維護者互動。請隨時造訪我們的 [GitHub 儲存庫](https://github.com/triggerdotdev/trigger.dev),貢獻並建立與 Trigger.dev 相關的問題。 本教學的源程式碼可在此處取得: [https://github.com/triggerdotdev/blog/tree/main/stars-monitor](https://github.com/triggerdotdev/blog/tree/main/stars-monitor) 感謝您的閱讀! --- 原文出處:https://dev.to/triggerdotdev/take-nextjs-to-the-next-level-create-a-github-stars-monitor-130a

使用 Python 和 Pytest 自動化 API 測試

您是否想過如何使用 python 測試您的 API?在本文中,我們將學習如何使用 Python 和 pytest 框架來測試我們的 API。 對於本教程,您需要安裝 python,您可以在[此處](https://www.python.org/downloads/)下載它 --- ### 簡介: - [什麼是 Python 和 Pytest 框架](#what-is-python-and-pytest-framework) - [我們專案的設定](#configuration-of-our-project) - [使用python建立虛擬環境](#creation-of-virtual-environment-with-python) - [測試的依賴關係設定](#setup-of-dependency-for-the-tests) - [建立我們的第一個測試](#creating-our-first-test) - [將被測試的 API 的定義](#definition-of-the-api-that-will-be-tested) - [建立我們的測試](#creating-our-test) - [重建我們的測試](#refactoring-our-tests) - [產生 html 報告結果](#generate-html-report-result) - [結論](#conclusion) --- ## 什麼是 Python 和 Pytest 框架 「Python」是一種高級通用程式語言,以其簡單性和可讀性而聞名。它由 Guido van Rossum 建立,於 1991 年首次發布。 Python 的設計易於學習,並且具有乾淨簡潔的語法,這使其成為初學者和經驗豐富的程式設計師的流行選擇。 「pytest」框架可以輕鬆編寫小型、可讀的測試,並且可以擴展以支援應用程式和程式庫的複雜功能測試。 --- ## 我們專案的配置 ### 用python建立虛擬環境 在開始建立之前,我們先來了解一下什麼是Python上的虛擬環境。 Python 中的虛擬環境是一個獨立的目錄或資料夾,可讓您為專案建立和管理隔離的 Python 環境。透過環境,您可以輕鬆管理依賴項,避免與不同版本的 python 發生衝突。 虛擬環境(除其他外)是: - 用於包含支援專案(庫或應用程式)所需的特定 Python 解釋器以及軟體庫和二進位檔案。預設情況下,它們與其他虛擬環境中的軟體以及作業系統中安裝的 Python 解釋器和庫隔離。 - 包含在專案目錄中的目錄中,通常名為“venv”或“.venv”,或在許多虛擬環境的容器目錄下,例如“~/.virtualenvs”。 - 未簽入原始碼控制系統(例如 Git)。 - 被認為是一次性的 - 應該很容易刪除並從頭開始重新建立它。您沒有在環境中放置任何專案程式碼 - 不被視為可移動或可複製 - 您只需在目標位置重新建立相同的環境。 您可以在[此處](https://docs.python.org/3/library/venv.html#venv-def)閱讀有關 python 環境的更多資訊。 #### 視窗 首先,為您的專案建立一個資料夾,然後打開 cmd 並使用命令 cd 導航到該資料夾: ``` cd tests_with_python ``` 如果您不知道資料夾在哪裡,可以執行命令“ls”,您將看到資料夾列表,並且可以瀏覽它們。在我們的專案資料夾中,執行以下命令: ``` python -m venv name_of_environment ``` 您的環境名稱可以是任何人,只需記住python 區分大小寫,請查看[PEP 8 風格指南](https://peps.python.org/pep-0008/) 以了解有關Python 約定的更多資訊. 要啟動我們的環境,我們使用以下命令: ``` name_of_environment\Scripts\Activate ``` 如果一切正確,您的環境將被激活,並且在 cmd 上您將看到如下所示: ``` (name_of_environment) C:\User\tests ``` 要停用您的環境,只需執行: ``` deactivate ``` #### Linux 或 MacOS 為您的專案建立一個資料夾,然後打開 cmd 並使用命令 cd 導航到該資料夾: ``` cd tests_with_python ``` 要啟動我們的環境,我們使用以下命令: ``` source name_of_environment/bin/activate ``` 如果一切正確,您的環境將被激活,並且在 cmd 上您將看到如下所示: ``` (name_of_environment) your_user_name tests % ``` 要停用您的環境,只需執行: ``` deactivate ``` ### 設定測試的依賴關係 當我們要測試 API 時,我們需要安裝依賴項來幫助我們進行測試,首先我們將安裝「requests」函式庫來幫助我們發出請求: PS:在執行此命令之前請確保您的環境已激活 ``` pip install requests ``` 為了進行測試,我們將安裝「pytests」框架: ``` pip install pytest ``` --- ## 建立我們的第一個測試 ### 將要測試的 API 的定義 在本教程中,我們將使用返回小行星列表的 Nasa API:[Asteroids - NeoWs](https://api.nasa.gov/#donkiGST),我們將測試檢索基於小行星列表的端點在他們最接近地球的日期。 關於API: - 基本網址:`https://api.nasa.gov/neo/rest/v1/feed` - 查詢參數: |參數|類型|預設|描述| | --------|---------|--------|--------------------| |start_date|YYYY-MM-DD|無|小行星搜尋的開始日期| |end_date|YYYY-MM-DD|start_date後7天|小行星搜尋的結束日期| |api_key|字串|DEMO_KEY|用於擴展用途的 api.nasa.gov 密鑰| 在本教程中,我們將重點放在三種類型的測試: - 合約:如果 API 能夠驗證傳送的查詢參數 - 狀態:狀態程式碼是否正確 - 身份驗證:即使這個API不需要令牌,我們也可以用它來做測試 我們的場景: |方法|測試|預期結果 | | --------|--------|--------------------| |獲取 |搜尋成功 | - 傳回狀態程式碼 200<br/> 正文回應包含小行星清單| |獲取 |無需任何查詢參數即可搜尋 | - 返回狀態碼403<br/>| |獲取 |僅搜尋開始日期| - 傳回狀態程式碼 200 <br/> 主體回應包含小行星清單| |獲取 |僅搜尋結束日期| - 傳回狀態程式碼 200 <br/> 主體回應包含小行星清單| |獲取 |在有效日期範圍內搜尋| - 傳回狀態碼 200<br/> - 正文回應包含所有非空白欄位| |獲取 |當開始日期大於結束日期時進行搜尋| - 傳回狀態程式碼 400 <br/>| |獲取 |使用無效的 API 令牌進行搜尋| - 傳回狀態程式碼 403 <br/> 主體回應包含小行星清單| ### 建立我們的測試 首先,我們將建立一個名為「tests.py」的文件,我們將在該文件中編寫測試。為了幫助我們使用良好的實踐並編寫良好的自動化測試,讓我們使用 [TDD(測試驅動開發)](https://www.browserstack.com/guide/what-is-test-driven-development?psafe_param=1?keyword=&campaignid=&adgroupid=&adid=8784011037660164696&utm_source=google&utm_medium=cpc&utm_platform=paidads&utm_content=602353912717&utm_campapaidads&utm_content=602353912717&utm_campa.utm_term=+&gad_source=1&gclid=CjwKCAiAxreqBhAxEiwAfGfndN8P705lwnkvEFnCz_lueR2hnhmZXgboBQEtKTaCIRbhcb1SXOxBYhoC-WoQAD_BwwE)技術。 該技術包括: - 紅色 - 進行失敗的測試 - 綠色 - 使此測試通過 - 重構 - 重構所做的事情,刪除重複的內容 為了編寫一套好的測試,我們將使用 3A 技術: - 安排:準備上下文。 - 行動:執行我們想要示範的行動。 - 斷言:表明我們預期的結果確實發生了。 從紅色開始,使用 3A 技術,我們將編寫第一個測試「成功搜尋小行星」: ``` import pytest def test_search_asteroids_with_sucess(): # Arrange: api_key = "DEMO_KEY" #Act: response = make_request(api_key) #Assertion: assert response.status_code == 200 # Validation of status code data = response.json() # Assertion of body response content: assert len(data) > 0 assert data["element_count"] > 0 ``` - 安排:我們建立一個變數來插入 api_key,在此步驟中,您可以插入執行測試所需的任何資料。通常,在這一步驟我們會建立模擬資料。 - Act:在這一步驟中我們呼叫了負責發出請求的方法 - 斷言:我們驗證回應 `方法或類別的名稱應以 test 開頭` 若要執行我們的測試,請在命令提示字元中執行: ``` pytest test.py ``` 我們將收到一個錯誤,因為我們沒有建立執行請求的方法: ``` test.py F [100%] ====================================================================== FAILURES ====================================================================== _________________________________________________________ test_search_asteroids_with_sucess __________________________________________________________ def test_search_asteroids_with_sucess(): > response = make_request() E NameError: name 'make_request' is not defined test.py:5: NameError ============================================================== short test summary info =============================================================== FAILED test.py::test_search_asteroids_with_sucess - NameError: name 'make_request' is not defined ================================================================= 1 failed in 0.01s ================================================================== ``` 現在,讓我們建立方法來執行請求: ``` import requests def make_request(api_key): base_url = "https://api.nasa.gov/neo/rest/v1/feed/" response = requests.get(f'{base_url}?api_key={api_key}') return response ``` 現在,再次執行我們的測試: ``` ================================================================ test session starts ================================================================= platform darwin -- Python 3.11.5, pytest-7.4.3, pluggy-1.3.0 rootdir: /Users/Documents/tests_python collected 1 item test.py . [100%] ================================================================= 1 passed in 20.22s ================================================================= ``` --- ## 重構我們的測試 現在我們已經了解如何使用 pytest 建立測試以及如何建立請求,我們可以編寫其他測試並開始重構測試。我們要做的第一個重構是從測試文件中刪除請求方法。我們將建立一個名為「make_requests.py」的新文件,其中將包含我們的請求,並將我們所做的請求移至此文件: ``` import requests def make_request(api_key): base_url = "https://api.nasa.gov/neo/rest/v1/feed/" response = requests.get(f'{base_url}?api_key={api_key}') return response ``` 現在,我們需要考慮在其他測試中重複使用此方法,因為我們需要為不同的測試傳遞不同的參數。我們可以透過很多方法來做到這一點,在本教程中,我們將參數的名稱從“api_key”更改為“query_parameters”。我們這樣做是為了讓我們的方法更加靈活,我們可以一次傳遞參數進行測試: ``` import requests def make_request(query_parameters): base_url = "https://api.nasa.gov/neo/rest/v1/feed/" response = requests.get(f'{base_url}?{query_parameters}') return response ``` 之後,我們需要更改我們的測試文件。我們將導入我們建立的這個方法: ``` from make_requests import make_request ``` 為了以更好的方式組織我們的測試,並遵循 pytest 文件的建議,我們將測試移至類別「TestClass」: 再次執行我們的測試: ``` ============================= test session starts ============================== collecting ... collected 7 items test.py::TestClass::test_search_asteroids_with_sucess test.py::TestClass::test_search_asteroids_with_query_parameters_empty test.py::TestClass::test_search_asteroids_with_start_date test.py::TestClass::test_search_asteroids_with_end_date test.py::TestClass::test_search_asteroids_in_valid_range test.py::TestClass::test_search_asteroids_in_invalid_range test.py::TestClass::test_search_asteroids_in_invalid_token ============================== 7 passed in 5.85s =============================== PASSED [ 14%]PASSED [ 28%]PASSED [ 42%]PASSED [ 57%]PASSED [ 71%]PASSED [ 85%]PASSED [100%] Process finished with exit code 0 ``` ### 產生 html 報告結果 為了更好地視覺化您的測試結果,我們可以使用 pytest-html-reporter 庫產生報告 html,為此,我們首先需要安裝該套件: ``` pip install pytest-html ``` 若要產生報告,請在執行測試時新增: ``` pytest test.py --html-report=./report/report.html ``` 將產生一個包含測試結果的 .html 文件,如下所示: ![report_example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eol3v7iex84t7prmyb9c.png) ## 結論 本文是一篇教程,介紹如何開始使用 python 和 pytest 框架為 API 編寫自動化測試以及如何產生一個報告 html。 您可以在[此處](https://github.com/aliciamarianne1507/tests_python)存取本教學中使用的專案。 我希望這些內容對您有用。 如果您有任何疑問,請隨時與我聯繫! 親親,下週見💅🏼 --- 原文出處:https://dev.to/m4rri4nne/automating-your-api-tests-using-python-and-pytest-23cc

如何寫文章:與世界分享您的知識!

分享書面知識是掌握特定主題的好方法,也是改善社區中思想組織、溝通和明顯自我推銷的好方法。這種技術和社會文章的製作對於寫作者和閱讀者來說都非常重要,永遠記住:「你今天比那些昨天開始的人知道的更多」。 ## 費曼方法以及為什麼要製作內容 首先,我們需要討論為什麼我們應該分享內容,無論是文字格式(本文的重點)還是任何其他格式。為了開始這個討論,重要的是要了解費曼的方法是什麼,以及它如何幫助我們自信和掌握該學科,從而將學習效果提高 10 倍。 費曼方法是由一位非常重要的物理學家理查德·費曼建立的,目的是開發一種新的學習方法,這個新提議假設了一個核心事實:「如果你不能清楚、簡單地解釋某件事,那麼你就還不算搞懂這件事」 這句話有助於我們思考我們的學習應該如何建置,因為從我們開始考慮教授我們正在學習的內容的那一刻起,我們就更加專注於掌握該學科的基本基礎,並為迫使你學習的疑慮做好準備。另一方面,看起來問題完全不同。 當為此類情況做準備時,明顯的結果是對所研究的主題有極大的信心和掌握。 我特別喜歡這種方法,唯一的問題是,當我們離開學術環境時,很難找到對你現在正在學習的同一學科感興趣的人,要么因為你的學校裡沒有IT人。友誼週期或僅僅因為對特定主題有興趣。 對於這個問題我們有一個非常不可思議的解決方案,叫做「公共學習」!這種做法包括在技術社群線上分享您的學習內容,無論是製作影片、進行直播還是本文的目標:寫作! 像 dev.to 這樣的平台(您現在正在使用它閱讀:D)旨在使「公共學習」的想法變得越來越簡單,並且更接近那些正在消費的人,因為現在可以製作能夠達到的文章與我們有相同興趣的人可以:學習、回答問題甚至提出改變和正確的想法。難以置信,對吧? ## 收集想法並激勵自己寫作 ![寫迷因](https://github.com/cherryramatisdev/public_zet/assets/86631177/66b243e4-5d12-4901-929f-d458db2b6fe0) 靈感過程可能是在線撰寫文章之前最煩人的階段之一,我們經常陷入瘋狂技術的無限循環中以提出令人難以置信的想法,而事實上,解決方案最終非常簡單:接受你的想法並消費它們,盡可能多的內容。 尋找想法並建立自己的語言的最實用方法是閱讀其他人已經就您感興趣的主題發表的文章,無論是程式語言還是特定的 IT 主題等;這種內容消費來自許多不同的來源,例如技術文章、YouTube 影片、科技泡沫推文、Github 討論和許多其他可能的地方。 嗯,我知道這樣說似乎簡化了一些不簡單的事情,我同意你的觀點!不僅僅是閱讀或觀看網路上存在的所有內容才能使我們有能力製作相同的內容,使這些人脫穎而出的最重要技能是**組織到達大腦的想法**。 ### 維護第二個大腦 我們的大腦是一個優秀的資訊吸收機器,實際上是一塊儲存我們周圍所有資訊的海綿。這台機器的一個大問題是,隨著時間的推移,它在組織方面變得很糟糕,這主要是為了節省能源,因為我們不需要一直記住一切,但知道我們可以做什麼來將我們想要的資訊儲存在一個機器中。組織方式?好吧,好吧,年輕的蚱蜢,我們當然需要停止相信我們的大腦! 維護「第二個大腦」是作家和研究人員中非常著名的做法,它由一個物理或虛擬位置組成,您可以在其中複製您所消耗的小塊內容以及使用您自己的話對該主題進行的觀察。這堆筆記將構成您的“第二個大腦”,並使您能夠快速找到任何內容並參考其作者,而不會忘記任何內容! 長話短說,消耗盡可能多的內容,將其儲存在可以儲存和搜尋的第二個大腦中,最後挑戰自己寫作!無論是您想學習的主題、您最近學到的特定內容,還是您已經掌握多年的內容。 ## 了解平台並找到自己的語言 了解我們透過撰寫內容要接觸的平台和受眾非常重要,這樣我們就可以過濾我們將如何建立文章的整體結構,對嗎?在*我看來*,[dev.to](https://dev.to) 是一個非常非正式的平台,它重視大量以教程形式呈現的內容,具有對話風格並且開門見山,以此通過這些訊息,我們可以推斷出一些建立文章的方法,以便我們可以用讀者已知的模型來說明我們的想法。 這是否意味著您將製作的所有內容都是簡單、非正式的教學?決不!這只是意味著你可以塑造你的內容來包含這種更非正式、對話和直接的語言,即使所涵蓋的主題非常複雜,這甚至成為簡化複雜性的一個非常有趣的挑戰。 > 簡化複雜問題的能力將伴隨您的餘生,建立類比和範例以促進理解和辨識所提出的問題和建議的解決方案非常重要。 ## 學習 Markdown 和良好格式設定的一般技巧 我們在dev.to 上製作文章的方式是使用一種稱為[Markdown](https://www.markdownguide.org) 的標記語言(與HTML 完全相同),雖然它非常簡單,但重要的是要有一個當我們談論組織並使文字美觀時,我們可以做很多事情,類似於我們如何在 Microsoft Word 中產生複雜的結構,我們應該能夠使用 Markdown 程式碼產生相同的結構。 強調結構良好的教育材料的重要性始終很重要(畢竟,您正在閱讀這篇文章正是因為這個原因,對吧?),當談到卓越和品質時,我不能不推薦 [4noobs](https://github.com/he4rt/4noobs),它在一個存儲庫中匯集了有關各種IT 主題的多個免費課程和文本格式,對於本文的主題,我建議使用[markdown4noobs](https://github.com/jpaulohe4rt/markdown4noobs )學習 Markdown 標記語言。 ### 文字操作和程式碼區塊的基礎知識 Markdown 讓我們可以使用超級基本和必要的結構來標記文字的各個部分,例如粗體、斜體、突出顯示、標題層級等。下面我們將快速了解如何使用正確的語法執行每個操作。 ``` # Primeiro titulo equivalente a um h1 ## Segundo titulo equivalente a um h2 ### Terceiro titulo equivalente a um h3 #### Quarto titulo equivalente a um h4 `Texto em highlight` **Texto em negrito** *Texto em itálico* ``` Markdown 語言的這些技巧使我們能夠以自己喜歡的方式控制敘述並使閱讀更容易理解,在文本中間使用**粗體**來吸引註意力,使用突出顯示甚至使用突出顯示來明確“技術術語”說明性圖像介紹了段落的要點,同時使文字的整體氛圍更易於閱讀。 另一個值得一提的重要事情是我上面使用的特定區塊,它在編寫技術文章時非常有用,因為它允許更多地突出顯示文字區塊,並且它允許您在編寫程式碼區塊時啟用語法突出顯示,它的使用方式如下: > 免責聲明:由於 markdown 不允許區塊內有區塊,所以我選擇用截圖來展示: ![程式碼區塊](https://github.com/cherryramatisdev/public_zet/assets/86631177/61de98aa-e7bb-4baa-91c4-afca9db2991f) 在「反引號」符號之後,我們可以包含語言的名稱(在我的例子中為 ruby),以便 dev.to 可以啟用特定於該程式語言的語法突出顯示。 ### 目錄 如果您的文章超過兩千字邊距或至少有 4 個主要標題,我強烈建議您定義一個「目錄」或「目錄」。目錄用於指導閱讀本文將要介紹的要點。要建立一個目錄,我將在下面示範一些技巧: #### 在 dev.to 平台上,使用無序列表而不是編號列表 Markdown 中的清單使用起來非常簡單,它們有兩種**主要**類型:無序和編號。 ``` - Uma lista - Não - Ordenada 1. Uma lista 2. Numerada 3. Aqui ``` 在 dev.to 中使用編號列表的問題是它們沒有對齊,正如我們在下面的範例中看到的那樣,所以我通常不建議使用它們,我總是嘗試使用無序列表,如果有必要應用一些順序,在手動未排序的列表符號後使用數字。 ![清單沒有 dev.to](https://github.com/cherryramatisdev/public_zet/assets/86631177/0ab1a9c1-efb3-40d5-b90f-7cacb7d20f77) #### 如何組織標題的連結 假設您已經了解如何在 Markdown 中建立連結(因為您閱讀了 markdown4noobs,對吧?),讓我們學習在標題中指示連結的簡單技巧以及如何建立目錄。 目錄範例如下: ``` ## Table of contents - [What is metaprogramming anyway?](#what-is-metaprogramming-anyway) - [In ruby everything is an object, what does that mean?](#in-ruby-everything-is-an-object-what-does-that-mean) - [But what about rails? How this framework applies that concept for maximum developer experience](#but-what-about-rails-how-this-framework-applies-that-concept-for-maximum-developer-experience) - [How to define methods dynamically](#how-to-define-methods-dynamically) - [Using hooks to detect moments on the instantiation of the class](#using-hooks-to-detect-moments-on-the-instantiation-of-the-class) - [Conclusion](#conclusion) ``` 正如您所看到的,定義連結第二部分的總體思路是在標題旁邊以特定格式包含一個主題標籤“#”,遵循以下規則: - 用連字號「-」取代所有空格 - 將整個標題保留為小寫 就是這樣!帶有重音符號的標題可以保持不變,沒有任何問題,Markdown 理解相同的標準文本,如下所示: ``` - [Um título com muitos acentos e çedilha](#um-título-com-muitos-acentos-e-çedilha) ``` ## 技術文章的基本結構 現在我們對如何標記文字以使其清晰易讀有了一個有趣的想法,讓我們了解文章的結構。需要強調的是,模型並不適用於所有可能的文本類型,其想法是提供一個必須根據上下文進行調整和更改的整體想法。 首先,定義開頭段落以吸引讀者了解您將在整篇文章中剖析的問題或情況非常重要,這樣做很重要,因為第一段將由 dev.to 用於 e-行銷傳播、電子郵件或社交媒體。開頭段落的範例可以在您正在閱讀的同一篇文章或我在下面留下的其他文章中找到: ![段落初始範例 1](https://github.com/cherryramatisdev/public_zet/assets/86631177/a76e0a72-60a7-4864-b2e9-f43922a8e0fb) ![段落初始範例 2](https://github.com/cherryramatisdev/public_zet/assets/86631177/64fc0d55-eed4-4c81-b4cf-193cf0d594a6) 我們的想法是始終在文本中使用問題和停頓,以便我們能夠實現直接的對話式交流,並始終嘗試以最普遍的方式呈現情況,以便任何閱讀者都非常好奇並願意閱讀。 在第一段演示之後,定義[目錄](#table-of-content) 來引導使用者了解文章的主要標題非常重要,在這方面我個人不建議列出副標題標題旁邊,因為它們使目錄變得非常大,對於閱讀者來說不是很有用,顯然,如果您認為列出字幕非常重要,那麼完全值得包括在內。 轉到文章的正文,我們進入一個非常主觀的領域,因為它在很大程度上取決於所涵蓋的主題以理解其標題和段落的結構。我將假設簡單教程模型中的一篇文章能夠從某個地方開始。 我總是建議使用三個“附屬標題”來指導您的文章並提供靈感以通過更多細節擴展內容。這些衛星標題如下: -「技術或問題簡介」:這段將幫助我們詳細說明文章開頭所說的內容,回答我們為激發好奇心而建立的問題,並更深入地研究將與特定主題一起討論的主題。 - `優點與缺點`:此時我們將明確本文將介紹的解決方案的優點和缺點,無論是架構、程式碼標準、語言、框架等。根據您的主題,此段落的存在可能非常具體,但如果您以教程的形式呈現解決方案,它通常非常有用! - `結論`:這一點更多的是一種意見,而不是一般規則,但我認為有一個段落將表明閱讀過程的結束是非常重要的,這樣我們就可以留下最終的論點,謝謝,聯繫方式以及任何其他有趣的訊息。 圍繞著這三個主要標題,我們可以用說明性的寫作來發展我們的文章,提供實際的例子或類比,使讀者更容易想像問題和解決方案。同樣重要的是要強調在過於深入地進行類比時要小心,它們非常有用,但是當你濫用它們並且永遠不會帶著明確的解決方案和解釋回到現實世界時,它們可能會成為一劑強心針。 關於文章結構的一般提示是保持閱讀光線的總體感覺,因此強烈建議使用圖像(無論是放鬆的表情包還是更好地說明所要表達的觀點的圖形),因為開發人員.to平台支持更多非正式技術人員的文章,濫用這種更接近的語言是一個非常準確的策略。 ## 如何複習並提升寫作水平 ![程式碼審查迷因](https://github.com/cherryramatisdev/public_zet/assets/86631177/ba71cb93-5734-423f-ab32-7718bf5bca5d) 好吧,現在我們已經很好地了解瞭如何建立我們的文章、如何使用 Markdown 保持文章美觀以及如何考慮我們的語言針對特定平台的情況,還缺少什麼嗎?好吧,現在剩下的就是要明白我們並不完美,我們會犯錯誤,因此,我們需要一個好的策略來回顧我們剛剛用我們學到的技術製作的文章。 為了幫助寫作,我強烈建議使用提供即時 Markdown 預覽的編輯器,例如 [VSCode](https://code.visualstudio.com/Docs/languages/markdown) 或社群最喜歡的 [Obsidian](https://obsidian.md)。這篇文章甚至是用黑曜石寫的! 在複習方面,我們有一些非常有趣的工具可以幫助我們進行寫作的不同方面: - [LanguageTool](https://languagetool.org):這個工具是我最喜歡的,它可以處理所有拼字更正,最酷的部分是,在這個工具中,您可以提供上下文提示,可以改進句子並更正特定的程式設計術語,例如語言名稱,因為他們的資料庫是超級更新的。 - [Deepl](https://www.deepl.com/translator):進入人工智慧世界,Deepl 使用深度學習提供令人難以置信的翻譯介面,但不僅如此!有了它,我們可以獲得第二意見,以一種非常簡單的方式重新表述我們的段落,您只需將文本翻譯成英語,然後再次將英語文本翻譯成葡萄牙語;通常在Google翻譯中,這會破壞上下文,但該工具保留了上下文並改進了表達方式,以便您對同一段落有第二個感知。 - [ChatGPT](http://chat.openai.com) 或 [Bard](https://bard.google.com): 好吧,在這裡我承認我沒有太多知識,而且我不使用很多,但是這些介面人工智慧聊天可以幫助我們提出不同的觀點,重新措詞現有的段落,甚至開始寫一個段落。 **重要提示:我需要強調的是,您應該只使用這些工具來獲取想法或幫助改寫您已經編寫的文本,請不要使用人工智慧生成整篇文章** - [社群](https://heartdevs.com):在 He4rt 開發者社群中,我們嘗試為在 dev.to 平台上撰寫技術文章提供盡可能多的幫助。我們透過提供一個論壇來做到這一點,您可以在文章仍在進行中時發布您的文章,並在發表之前獲得社區的反饋。發表後,我們還為活躍的人做宣傳工作! **免責聲明:顯然我提到的是 He4rt,因為我們有一個專注於此的專案,但一般的教訓是與整個社區分享您的進展。** ## 結論和致謝 這是我在[100 天的程式碼](https://www.100diasdecodigo.dev) 挑戰之後發布的最後一篇文章,這是一個非常激烈的挑戰,需要大量的學習,我發現了一種新的熱情:寫作和分享知識!我甚至無法感謝 He4rt 社區在這段漫長的旅程中對我 100% 的支持。我希望這篇文章對任何閱讀它的人都有用,並激勵任何人在線上分享知識,以便我們可以建立一個更安全、資訊更豐富的網路。 我還要特別感謝本文的審稿人: - [阿尼巴爾‧索倫](https://github.com/anibalsolon) - [艾莉西亞瑪麗安](https://github.com/m4rri4nne) - [米格爾·S·巴博薩](https://github.com/m1guelsb) - [克林頓·羅查](https://github.com/Clintonrocha98) - [塞繆爾·羅德里格斯](https://github.com/SamucaDev) 願原力與你同在! 🍒 --- 原文出處:https://dev.to/he4rt/compartilhando-seu-conhecimento-com-o-mundo-como-escrever-artigos-5ghc

【Python 🐍精通】Python Linked List 及基本 Linked List 操作🛠️

在上一篇文章中,我們了解了物件導向程式設計並對 Python 的 Magic/Dunder 方法進行了全面概述。 **Python 中的物件導向程式設計 (OOP)**:Python 中的這種範例圍繞著建立可重複使用程式碼。它涉及使用類別作為物件的藍圖。這些物件可以具有定義其行為和交互的屬性(資料)和方法(函數)。 **Python 的 Magic/Dunder 方法**:Python 中的 Magic 或 Dunder(雙底線)方法是名稱以雙底線開頭和結尾的特殊方法(例如,`__init__`、`__str__`、`__repr__`)。 您可以在這裡閱讀相關內容。 👇 https://dev.to/swirl/python-mastery-pythons-object-oriented-programming-overview-and-fundamentals-22m1 今天,我們將對其進行擴展,並使用物件導向程式設計的知識來理解和建立 Python 中的鍊錶。並會對其執行一些操作。 ## 開源 Python 專案:[Swirl](https://github.com/swirlai/swirl-search) [![旋流搜尋](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9icq3q8vmtzrse1cbqcj.gif)](https://github.com/swirlai/swirl-search) 如果您對 Python 感興趣,您將會💖 [**Swirl**](https://github.com/swirlai/swirl-search)。 Swirl 是一個開源搜尋平台,它將為您提供以下知識: - Python - 人工智慧 - 在任何產品中整合大型語言模型 - 了解如何開發搜尋平台。 檢查我們的 GitHub 儲存庫: https://github.com/swirlai/swirl-search 如果您能夠:我們將非常高興: https://github.com/swirlai/swirl-search ## Linked List 連結列表是物件有序的集合。它是一種資料結構,旨在將資料保存在不連續的記憶體區塊中。 與使用連續記憶體區塊的陣列或傳統列表不同,鍊錶儲存在非連續記憶體位置。這種設計允許高效的插入和刪除,而無需重新排列整個資料結構。 這種設計允許高效的插入和刪除,而不需要重新排列整個資料結構。 ### 基本鍊錶 基本鍊錶是一種線性資料結構,其中每個元素(稱為節點)包含兩部分:資料和對清單中下一個節點的引用。這種結構允許有效地插入和刪除元素,因為它不需要移動元素,這與陣列不同。 典型的節點設計: **資料**:包含資料,可以是數字、地址、文字等。 **Next**:指向下一個資料節點或儲存下一個資料節點的位址。 ![Python 中鍊錶的節點](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cus930t03hko9x0xeac2.png) 第一個節點稱為列表的頭,最後一個節點指向 None(在 Python 中)(或在其他語言中為 Null),稱為尾節點。 當你把很多節點收集在一起時,它就變成了一個鍊錶。 ![Python 中的鍊錶](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ndkxz3274ylfqva4y2kh.png) ### 鍊錶的好處和操作的時間複雜度 連結清單有很多好處,特別是在動態資料操作場景中。以下是一些主要優勢: 1. **動態大小**:與陣列不同,鍊錶可以動態增長或縮小大小,這對於記憶體使用來說是高效的。 2. **易於插入/刪除**:插入或刪除節點相對簡單,因為它通常只涉及更改一些引用,而不需要像陣列中那樣移動元素。 3. **靈活性**:它們可以實現其他資料結構,如堆疊、佇列和圖鄰接表。 |運營|時間複雜度| |----------------|-----------------| |存取 | O(n) | |搜尋 | O(n) | |插入| O(1) | O(1) | |刪除 | O(1) | O(1) | _注意_:我們考慮的是單鍊錶。 ## 在 Python 中實作鍊錶。 這是將在 Python 中建立節點的程式碼。如果您對 `__repr__` 方法感到困惑,請注意。請查看本系列中的[上一篇文章](https://dev.to/swirl/python-mastery-pythons-object-oriented-programming-overview-and-fundamentals-22m1)。 ``` class Node: def __init__(self, data): self.data = data self.next = None def __repr__(self): return f"Node({self.data})" ``` 連結列表類別的程式碼。這利用了 Node 類別來建立資料並將它們連接在一起。 ``` class LinkedList: def __init__(self): self.head = None def append(self, data): new_node = Node(data) if self.head is None: self.head = new_node return last_node = self.head while last_node.next: last_node = last_node.next last_node.next = new_node def __repr__(self): nodes = [] current = self.head while current: nodes.append(repr(current)) current = current.next return "->".join(nodes) ``` 這段程式碼做了兩件事: 1. **追加**:在鍊錶末端追加一個節點。 2. `__repr__` :此方法遍歷鍊錶並以Pythonic方式列印它。 1. 這也可以使用稱為 traverse 的方法來完成。 _這是呼叫「__repr__」方法的「print(llist)」的輸出: ![在 Python 中列印連結清單](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8krtl50nkifa5nhvsh4j.png) ### 遍歷鍊錶。 遍歷鍊錶就是遍歷每個節點並列印它的過程。 ``` def traverse(linked_list): current = linked_list.head while current: print(current.data) current = current.next llist = LinkedList() llist.append(1) llist.append(2) llist.append(3) print("Traversing the linked list:") traverse(llist) ``` ![Python中遍歷鍊錶](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mzzy5pwzixnwpgmfqynw.png) ## 反轉鍊錶 這個想法是迭代鍊錶,並且對於每個節點,將其“下一個”指標切換為指向前一個節點而不是下一個節點。這將幫助我們反轉鍊錶。 ``` def reverse_linked_list(head): previous = None current = head while current: next_node = current.next current.next = previous previous = current current = next_node return previous llist = LinkedList() llist.append(1) llist.append(2) llist.append(3) print("Original List:", llist) new_head = reverse_linked_list(llist.head) llist.head = new_head print("Reversed List:", llist) ``` ![反轉連結清單](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s05ouz9eegzukkkuofw8.png) ## 在連結清單中插入值 我們已經有一個追加函數,可以將值加到鍊錶的末尾。但是,如果我們想要一個在特定位置加入的方法,並且如果該位置不存在,則將值附加到末尾,該怎麼辦? ``` class LinkedList: def insert_after_value(self, data_after, data_to_insert): if self.head is None: return current = self.head while current: if current.data == data_after: new_node = Node(data_to_insert) new_node.next = current.next current.next = new_node return current = current.next self.append(data_to_insert) ``` ## 刪除鍊錶中的節點 若要從鍊錶中刪除節點,請建立一個函數,該函數將鍊錶的頭和要刪除的節點的資料作為參數。並遍歷鍊錶,直到找到資料,然後將其刪除。 ``` class LinkedList: def delete_node(self, data): current = self.head if current is None or current.data == data: self.head = current.next if current else None return while current.next: if current.next.data == data: current.next = current.next.next return current = current.next ``` ![刪除連結清單中的節點](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/12hjdfchdy0ofrjyyt10.png) 感謝您閱讀本文。在本系列的後續文章中,我們將討論 Python 和 Python 資料結構的更複雜的細節。 ## 為 [Swirl] 做出貢獻(https://github.com/swirlai/swirl-search) [![為 Swirl 做出貢獻](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/itecirhtu5lghtlr6m2a.jpg)](https://github.com/swirlai/swirl-search) [Swirl](https://github.com/swirlai/swirl-search) 是一個開源 Python 專案。為 Swirl 做出貢獻可以幫助您獲得生產級的 Python 知識並提高您的技能。 檢查我們的 GitHub 儲存庫: https://github.com/swirlai/swirl-search 如果您能夠:我們將非常高興: https://github.com/swirlai/swirl-search 謝謝閱讀, 你們都令人嘆為觀止。 ![你太棒了](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kqhf5vqr2lj4z5kq4gdr.gif) --- 原文出處:https://dev.to/swirl/python-mastery-overview-of-linked-list-in-python-essential-linked-list-operations-hn3

🚀使用 NextJS、Trigger.dev 和 GPT4 做一個履歷表產生器🔥✨

## 簡介 在本文中,您將學習如何使用 NextJS、Trigger.dev、Resend 和 OpenAI 建立簡歷產生器。 😲 - 加入基本詳細訊息,例如名字、姓氏和最後工作地點。 - 產生詳細訊息,例如個人資料摘要、工作經歷和工作職責。 - 建立包含所有資訊的 PDF。 - 將所有內容傳送到您的電子郵件 ![猴子手錶](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/23k6hee187s62k8y1dmd.gif) *** ## 你的後台工作平台🔌 [Trigger.dev](https://trigger.dev/) 是一個開源程式庫,可讓您使用 NextJS、Remix、Astro 等為您的應用程式建立和監控長時間執行的作業!   [![GiveUsStars](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bm9mrmovmn26izyik95z.gif)](https://github.com/triggerdotdev/trigger.dev) 請幫我們一顆星🥹。 這將幫助我們建立更多這樣的文章💖 https://github.com/triggerdotdev/trigger.dev --- ## 讓我們來設定一下吧🔥 使用 NextJS 設定一個新專案 ``` npx create-next-app@latest ``` 我們將建立一個包含基本資訊的簡單表單,例如: - 名 - 姓 - 電子郵件地址 - 你的頭像 - 以及你今天為止的經驗! ![輸入](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/01mmvn0lvw1p1i4knoa8.png) 我們將使用 NextJS 的新應用程式路由器。 開啟`layout.tsx`並加入以下程式碼 ``` import { GeistSans } from "geist/font"; import "./globals.css"; const defaultUrl = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000"; export const metadata = { metadataBase: new URL(defaultUrl), title: "Resume Builder with GPT4", description: "The fastest way to build a resume with GPT4", }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en" className={GeistSans.className}> <body className="bg-background text-foreground"> <main className="min-h-screen flex flex-col items-center"> {children} </main> </body> </html> ); } ``` 我們基本上是為所有頁面設定佈局(即使我們只有一頁。) 我們設定基本的頁面元資料、背景和全域 CSS 元素。 接下來,讓我們打開“page.tsx”並加入以下程式碼: ``` <div className="flex-1 w-full flex flex-col items-center"> <nav className="w-full flex justify-center border-b border-b-foreground/10 h-16"> <div className="w-full max-w-6xl flex justify-between items-center p-3 text-sm"> <span className="font-bold select-none">resumeGPT.</span> </div> </nav> <div className="animate-in flex-1 flex flex-col opacity-0 max-w-6xl px-3"> <Home /> </div> </div> ``` 這設定了我們的resumeGPT 的標題和主要的家庭元件。 <小時/> ## 建立表單的最簡單方法 保存表單資訊並驗證欄位最簡單的方法是使用react-hook-form。 我們將上傳個人資料照片。 為此,我們不能使用基於 JSON 的請求。 我們需要將 JSON 轉換為有效的表單資料。 那麼就讓我們把它們全部安裝吧! ``` npm install react-hook-form object-to-formdata axios --save ``` 建立一個名為 Components 的新資料夾,新增一個名為「Home.tsx」的新文件,並新增以下程式碼: ``` "use client"; import React, { useState } from "react"; import {FormProvider, useForm} from "react-hook-form"; import Companies from "@/components/Companies"; import axios from "axios"; import {serialize} from "object-to-formdata"; export type TUserDetails = { firstName: string; lastName: string; photo: string; email: string; companies: TCompany[]; }; export type TCompany = { companyName: string; position: string; workedYears: string; technologies: string; }; const Home = () => { const [finished, setFinished] = useState<boolean>(false); const methods = useForm<TUserDetails>() const { register, handleSubmit, formState: { errors }, } = methods; const handleFormSubmit = async (values: TUserDetails) => { axios.post('/api/create', serialize(values)); setFinished(true); }; if (finished) { return ( <div className="mt-10">Sent to the queue! Check your email</div> ) } return ( <div className="flex flex-col items-center justify-center p-7"> <div className="w-full py-3 bg-slate-500 items-center justify-center flex flex-col rounded-t-lg text-white"> <h1 className="font-bold text-white text-3xl">Resume Builder</h1> <p className="text-gray-300"> Generate a resume with GPT in seconds 🚀 </p> </div> <FormProvider {...methods}> <form onSubmit={handleSubmit(handleFormSubmit)} className="p-4 w-full flex flex-col" > <div className="flex flex-col lg:flex-row gap-4"> <div className="flex flex-col w-full"> <label htmlFor="firstName">First name</label> <input type="text" required id="firstName" placeholder="e.g. John" className="p-3 rounded-md outline-none border border-gray-500 text-white bg-transparent" {...register('firstName')} /> </div> <div className="flex flex-col w-full"> <label htmlFor="lastName">Last name</label> <input type="text" required id="lastName" placeholder="e.g. Doe" className="p-3 rounded-md outline-none border border-gray-500 text-white bg-transparent" {...register('lastName')} /> </div> </div> <hr className="w-full h-1 mt-3" /> <label htmlFor="email">Email Address</label> <input type="email" required id="email" placeholder="e.g. [email protected]" className="p-3 rounded-md outline-none border border-gray-500 text-white bg-transparent" {...register('email', {required: true, pattern: /^\S+@\S+$/i})} /> <hr className="w-full h-1 mt-3" /> <label htmlFor="photo">Upload your image 😎</label> <input type="file" id="photo" accept="image/x-png" className="p-3 rounded-md outline-none border border-gray-500 mb-3" {...register('photo', {required: true})} /> <Companies /> <button className="p-4 pointer outline-none bg-blue-500 border-none text-white text-base font-semibold rounded-lg"> CREATE RESUME </button> </form> </FormProvider> </div> ); }; export default Home; ``` 您可以看到我們從「使用客戶端」開始,它基本上告訴我們的元件它應該只在客戶端上執行。 為什麼我們只需要客戶端? React 狀態(輸入變更)僅在用戶端可用。 我們設定兩個接口,「TUserDetails」和「TCompany」。它們代表了我們正在使用的資料的結構。 我們將“useForm”與“react-hook-form”一起使用。它為我們的輸入建立了本地狀態管理,並允許我們輕鬆更新和驗證我們的欄位。您可以看到,在每個「輸入」中,都有一個簡單的「註冊」函數,用於指定輸入名稱和驗證並將其註冊到託管狀態。 這很酷,因為我們不需要使用像“onChange”這樣的東西 您還可以看到我們使用了“FormProvider”,這很重要,因為我們希望在子元件中擁有“react-hook-form”的上下文。 我們還有一個名為「handleFormSubmit」的方法。這是我們提交表單後呼叫的方法。您可以看到我們使用“serialize”函數將 javascript 物件轉換為 FormData,並向伺服器發送請求以使用“axios”啟動作業。 您可以看到另一個名為“Companies”的元件。該元件將讓我們指定我們工作過的所有公司。 那麼讓我們努力吧。 建立一個名為「Companies.tsx」的新文件 並加入以下程式碼: ``` import React, {useCallback, useEffect} from "react"; import { TCompany } from "./Home"; import {useFieldArray, useFormContext} from "react-hook-form"; const Companies = () => { const {control, register} = We(); const {fields: companies, append} = useFieldArray({ control, name: "companies", }); const addCompany = useCallback(() => { append({ companyName: '', position: '', workedYears: '', technologies: '' }) }, [companies]); useEffect(() => { addCompany(); }, []); return ( <div className="mb-4"> {companies.length > 1 ? ( <h3 className="font-bold text-white text-3xl my-3"> Your list of Companies: </h3> ) : null} {companies.length > 1 && companies.slice(1).map((company, index) => ( <div key={index} className="mb-4 p-4 border bg-gray-800 rounded-lg shadow-md" > <div className="mb-2"> <label htmlFor={`companyName-${index}`} className="text-white"> Company Name </label> <input type="text" id={`companyName-${index}`} className="p-2 border border-gray-300 rounded-md w-full bg-transparent" {...register(`companies.${index}.companyName`, {required: true})} /> </div> <div className="mb-2"> <label htmlFor={`position-${index}`} className="text-white"> Position </label> <input type="text" id={`position-${index}`} className="p-2 border border-gray-300 rounded-md w-full bg-transparent" {...register(`companies.${index}.position`, {required: true})} /> </div> <div className="mb-2"> <label htmlFor={`workedYears-${index}`} className="text-white"> Worked Years </label> <input type="number" id={`workedYears-${index}`} className="p-2 border border-gray-300 rounded-md w-full bg-transparent" {...register(`companies.${index}.workedYears`, {required: true})} /> </div> <div className="mb-2"> <label htmlFor={`workedYears-${index}`} className="text-white"> Technologies </label> <input type="text" id={`technologies-${index}`} className="p-2 border border-gray-300 rounded-md w-full bg-transparent" {...register(`companies.${index}.technologies`, {required: true})} /> </div> </div> ))} <button type="button" onClick={addCompany} className="mb-4 p-2 pointer outline-none bg-blue-900 w-full border-none text-white text-base font-semibold rounded-lg"> Add Company </button> </div> ); }; export default Companies; ``` 我們從 useFormContext 開始,它允許我們取得父元件的上下文。 接下來,我們使用 useFieldArray 建立一個名為 Companies 的新狀態。這是我們擁有的所有公司的一個陣列。 在「useEffect」中,我們新增陣列的第一項以對其進行迭代。 當點擊“addCompany”時,它會將另一個元素推送到陣列中。 我們已經和客戶完成了🥳 --- ## 解析HTTP請求 還記得我們向“/api/create”發送了一個“POST”請求嗎? 讓我們轉到 app/api 資料夾並在該資料夾中建立一個名為「create」的新資料夾,建立一個名為「route.tsx」的新檔案並貼上以下程式碼: ``` import {NextRequest, NextResponse} from "next/server"; import {client} from "@/trigger"; export async function POST(req: NextRequest) { const data = await req.formData(); const allArr = { name: data.getAll('companies[][companyName]'), position: data.getAll('companies[][position]'), workedYears: data.getAll('companies[][workedYears]'), technologies: data.getAll('companies[][technologies]'), }; const payload = { firstName: data.get('firstName'), lastName: data.get('lastName'), photo: Buffer.from((await (data.get('photo[0]') as File).arrayBuffer())).toString('base64'), email: data.get('email'), companies: allArr.name.map((name, index) => ({ companyName: allArr.name[index], position: allArr.position[index], workedYears: allArr.workedYears[index], technologies: allArr.technologies[index], })).filter((company) => company.companyName && company.position && company.workedYears && company.technologies) } await client.sendEvent({ name: 'create.resume', payload }); return NextResponse.json({ }) } ``` > 此程式碼只能在 NodeJS 版本 20+ 上運作。如果版本較低,將無法解析FormData。 該程式碼非常簡單。 - 我們使用 `req.formData` 將請求解析為 FormData - 我們將基於 FormData 的請求轉換為 JSON 檔案。 - 我們提取圖像並將其轉換為“base64” - 我們將所有內容傳送給 TriggerDev --- ## 製作履歷並將其發送到您的電子郵件📨 建立履歷是我們需要的長期任務 - 使用 ChatGPT 產生內容。 - 建立 PDF - 發送到您的電子郵件 由於某些原因,我們不想發出長時間執行的 HTTP 請求來執行所有這些操作。 1. 部署到 Vercel 時,無伺服器功能有 10 秒的限制。我們永遠不會準時到達。 2.我們希望讓用戶不會長時間掛起。這是一個糟糕的使用者體驗。如果用戶關閉窗口,整個過程將失敗。 ### 介紹 Trigger.dev! 使用 Trigger.dev,您可以在 NextJS 應用程式內執行後台進程!您不需要建立新伺服器。 他們也知道如何透過將長時間執行的作業無縫地分解為短期任務來處理它們。 註冊 [Trigger.dev 帳號](https://trigger.dev/)。註冊後,建立一個組織並為您的工作選擇一個專案名稱。 ![CreateOrg](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/shf1jsb4gio1zrjtz91d.jpeg) 選擇 Next.js 作為您的框架,並按照將 Trigger.dev 新增至現有 Next.js 專案的流程進行操作。 ![下一頁](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5guppb6rot13myu6th5c.jpeg) 否則,請點選專案儀表板側邊欄選單上的「環境和 API 金鑰」。 ![複製](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x5gh527u7sthp6clkcfa.png) 複製您的 DEV 伺服器 API 金鑰並執行下面的程式碼片段以安裝 Trigger.dev。仔細按照說明進行操作。 ``` npx @trigger.dev/cli@latest init ``` 在另一個終端中,執行以下程式碼片段以在 Trigger.dev 和 Next.js 專案之間建立隧道。 ``` npx @trigger.dev/cli@latest dev ``` 讓我們建立 TriggerDev 作業! 前往新建立的資料夾 jobs 並建立一個名為「create.resume.ts」的新檔案。 新增以下程式碼: ``` client.defineJob({ id: "create-resume", name: "Create Resume", version: "0.0.1", trigger: eventTrigger({ name: "create.resume", schema: z.object({ firstName: z.string(), lastName: z.string(), photo: z.string(), email: z.string().email(), companies: z.array(z.object({ companyName: z.string(), position: z.string(), workedYears: z.string(), technologies: z.string() })) }), }), run: async (payload, io, ctx) => { } }); ``` 這將為我們建立一個名為「create-resume」的新工作。 如您所見,我們先前從「route.tsx」發送的請求進行了架構驗證。這將為我們提供驗證和“自動完成”。 我們將在這裡執行三項工作 - 聊天GPT - PDF建立 - 電子郵件發送 讓我們從 ChatGPT 開始。 [建立 OpenAI 帳戶](https://platform.openai.com/) 並產生 API 金鑰。 ![ChatGPT](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ashau6i2sxcpd0qcxuwq.png) 從下拉清單中按一下「檢視 API 金鑰」以建立 API 金鑰。 ![ApiKey](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4bzc6e7f7avemeuuaygr.png) 接下來,透過執行下面的程式碼片段來安裝 OpenAI 套件。 ``` npm install @trigger.dev/openai ``` 將您的 OpenAI API 金鑰新增至 `.env.local` 檔案中。 ``` OPENAI_API_KEY=<your_api_key> ``` 在根目錄中建立一個名為「utils」的新資料夾。 在該目錄中,建立一個名為「openai.ts」的新文件 新增以下程式碼: ``` import { OpenAI } from "openai"; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY!, }); export async function generateResumeText(prompt: string) { const response = await openai.completions.create({ model: "text-davinci-003", prompt, max_tokens: 250, temperature: 0.7, top_p: 1, frequency_penalty: 1, presence_penalty: 1, }); return response.choices[0].text.trim(); } export const prompts = { profileSummary: (fullName: string, currentPosition: string, workingExperience: string, knownTechnologies: string) => `I am writing a resume, my details are \n name: ${fullName} \n role: ${currentPosition} (${workingExperience} years). \n I write in the technologies: ${knownTechnologies}. Can you write a 100 words description for the top of the resume(first person writing)?`, jobResponsibilities: (fullName: string, currentPosition: string, workingExperience: string, knownTechnologies: string) => `I am writing a resume, my details are \n name: ${fullName} \n role: ${currentPosition} (${workingExperience} years). \n I write in the technolegies: ${knownTechnologies}. Can you write 3 points for a resume on what I am good at?`, workHistory: (fullName: string, currentPosition: string, workingExperience: string, details: TCompany[]) => `I am writing a resume, my details are \n name: ${fullName} \n role: ${currentPosition} (${workingExperience} years). ${companyDetails(details)} \n Can you write me 50 words for each company seperated in numbers of my succession in the company (in first person)?`, }; ``` 這段程式碼基本上建立了使用 ChatGPT 的基礎設施以及 3 個函數:「profileSummary」、「workingExperience」和「workHistory」。我們將使用它們來建立各部分的內容。 返回我們的「create.resume.ts」並新增作業: ``` import { client } from "@/trigger"; import { eventTrigger } from "@trigger.dev/sdk"; import { z } from "zod"; import { prompts } from "@/utils/openai"; import { TCompany, TUserDetails } from "@/components/Home"; const companyDetails = (companies: TCompany[]) => { let stringText = ""; for (let i = 1; i < companies.length; i++) { stringText += ` ${companies[i].companyName} as a ${companies[i].position} on technologies ${companies[i].technologies} for ${companies[i].workedYears} years.`; } return stringText; }; client.defineJob({ id: "create-resume", name: "Create Resume", version: "0.0.1", integrations: { resend }, trigger: eventTrigger({ name: "create.resume", schema: z.object({ firstName: z.string(), lastName: z.string(), photo: z.string(), email: z.string().email(), companies: z.array(z.object({ companyName: z.string(), position: z.string(), workedYears: z.string(), technologies: z.string() })) }), }), run: async (payload, io, ctx) => { const texts = await io.runTask("openai-task", async () => { return Promise.all([ await generateResumeText(prompts.profileSummary(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)), await generateResumeText(prompts.jobResponsibilities(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)), await generateResumeText(prompts.workHistory(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies)) ]); }); }, }); ``` 我們建立了一個名為「openai-task」的新任務。 在該任務中,我們使用 ChatGPT 同時執行三個提示,並返回它們。 --- ## 建立 PDF 建立 PDF 的方法有很多種 - 您可以使用 HTML2CANVAS 等工具並將 HTML 程式碼轉換為映像,然後轉換為 PDF。 - 您可以使用「puppeteer」之類的工具來抓取網頁並將其轉換為 PDF。 - 您可以使用不同的庫在後端建立 PDF。 在我們的例子中,我們將使用一個名為「jsPdf」的簡單函式庫,它是在後端建立 PDF 的非常簡單的函式庫。我鼓勵您使用 Puppeteer 和更多 HTML 來建立一些更強大的 PDF 檔案。 那我們來安裝它 ``` npm install jspdf @typs/jspdf --save ``` 讓我們回到「utils」並建立一個名為「resume.ts」的新檔案。該文件基本上會建立一個 PDF 文件,我們可以將其發送到使用者的電子郵件中。 加入以下內容: ``` import {TUserDetails} from "@/components/Home"; import {jsPDF} from "jspdf"; type ResumeProps = { userDetails: TUserDetails; picture: string; profileSummary: string; workHistory: string; jobResponsibilities: string; }; export function createResume({ userDetails, picture, workHistory, jobResponsibilities, profileSummary }: ResumeProps) { const doc = new jsPDF(); // Title block doc.setFontSize(24); doc.setFont('helvetica', 'bold'); doc.text(userDetails.firstName + ' ' + userDetails.lastName, 45, 27); doc.setLineWidth(0.5); doc.rect(20, 15, 170, 20); // x, y, width, height doc.addImage({ imageData: picture, x: 25, y: 17, width: 15, height: 15 }); // Reset font for the rest doc.setFontSize(12); doc.setFont('helvetica', 'normal'); // Personal Information block doc.setFontSize(14); doc.setFont('helvetica', 'bold'); doc.text('Summary', 20, 50); doc.setFontSize(10); doc.setFont('helvetica', 'normal'); const splitText = doc.splitTextToSize(profileSummary, 170); doc.text(splitText, 20, 60); const newY = splitText.length * 5; // Work history block doc.setFontSize(14); doc.setFont('helvetica', 'bold'); doc.text('Work History', 20, newY + 65); doc.setFontSize(10); doc.setFont('helvetica', 'normal'); const splitWork = doc.splitTextToSize(workHistory, 170); doc.text(splitWork, 20, newY + 75); const newNewY = splitWork.length * 5; // Job Responsibilities block doc.setFontSize(14); doc.setFont('helvetica', 'bold'); doc.text('Job Responsibilities', 20, newY + newNewY + 75); doc.setFontSize(10); doc.setFont('helvetica', 'normal'); const splitJob = doc.splitTextToSize(jobResponsibilities, 170); doc.text(splitJob, 20, newY + newNewY + 85); return doc.output("datauristring"); } ``` 該文件包含三個部分:「個人資訊」、「工作歷史」和「工作職責」區塊。 我們計算每個區塊的位置和內容。 一切都是以“絕對”的方式設置的。 值得注意的是“splitTextToSize”將文字分成多行,因此它不會超出螢幕。 ![恢復](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hdolng9e5ojev895x8i5.png) 現在,讓我們建立下一個任務:再次開啟 `resume.ts` 並新增以下程式碼: ``` import { client } from "@/trigger"; import { eventTrigger } from "@trigger.dev/sdk"; import { z } from "zod"; import { prompts } from "@/utils/openai"; import { TCompany, TUserDetails } from "@/components/Home"; import { createResume } from "@/utils/resume"; const companyDetails = (companies: TCompany[]) => { let stringText = ""; for (let i = 1; i < companies.length; i++) { stringText += ` ${companies[i].companyName} as a ${companies[i].position} on technologies ${companies[i].technologies} for ${companies[i].workedYears} years.`; } return stringText; }; client.defineJob({ id: "create-resume", name: "Create Resume", version: "0.0.1", integrations: { resend }, trigger: eventTrigger({ name: "create.resume", schema: z.object({ firstName: z.string(), lastName: z.string(), photo: z.string(), email: z.string().email(), companies: z.array(z.object({ companyName: z.string(), position: z.string(), workedYears: z.string(), technologies: z.string() })) }), }), run: async (payload, io, ctx) => { const texts = await io.runTask("openai-task", async () => { return Promise.all([ await generateResumeText(prompts.profileSummary(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)), await generateResumeText(prompts.jobResponsibilities(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)), await generateResumeText(prompts.workHistory(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies)) ]); }); console.log('passed chatgpt'); const pdf = await io.runTask('convert-to-html', async () => { const resume = createResume({ userDetails: payload, picture: payload.photo, profileSummary: texts[0], jobResponsibilities: texts[1], workHistory: texts[2], }); return {final: resume.split(',')[1]} }); console.log('converted to pdf'); }, }); ``` 您可以看到我們新增了一個名為「convert-to-html」的新任務。這將為我們建立 PDF,將其轉換為 base64 並返回。 --- ## 讓他們知道🎤 我們即將到達終點! 剩下的唯一一件事就是與用戶分享。 您可以使用任何您想要的電子郵件服務。 我們將使用 Resend.com 造訪[註冊頁面](https://resend.com/signup),建立帳戶和 API 金鑰,並將其儲存到 `.env.local` 檔案中。 ``` RESEND_API_KEY=<place_your_API_key> ``` ![密鑰](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yncrarbwcs65j44fs91y.png) 將 [Trigger.dev Resend 整合套件](https://trigger.dev/docs/integrations/apis/resend) 安裝到您的 Next.js 專案。 ``` npm install @trigger.dev/resend ``` 剩下要做的就是加入我們的最後一項工作! 幸運的是,Trigger 直接與 Resend 集成,因此我們不需要建立新的「正常」任務。 這是最終的程式碼: ``` import { client } from "@/trigger"; import { eventTrigger } from "@trigger.dev/sdk"; import { z } from "zod"; import { prompt } from "@/utils/openai"; import { TCompany, TUserDetails } from "@/components/Home"; import { createResume } from "@/utils/resume"; import { Resend } from "@trigger.dev/resend"; const resend = new Resend({ id: "resend", apiKey: process.env.RESEND_API_KEY!, }); const companyDetails = (companies: TCompany[]) => { let stringText = ""; for (let i = 1; i < companies.length; i++) { stringText += ` ${companies[i].companyName} as a ${companies[i].position} on technologies ${companies[i].technologies} for ${companies[i].workedYears} years.`; } return stringText; }; client.defineJob({ id: "create-resume", name: "Create Resume", version: "0.0.1", integrations: { resend }, trigger: eventTrigger({ name: "create.resume", schema: z.object({ firstName: z.string(), lastName: z.string(), photo: z.string(), email: z.string().email(), companies: z.array(z.object({ companyName: z.string(), position: z.string(), workedYears: z.string(), technologies: z.string() })) }), }), run: async (payload, io, ctx) => { const texts = await io.runTask("openai-task", async () => { return Promise.all([ await generateResumeText(prompts.profileSummary(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)), await generateResumeText(prompts.jobResponsibilities(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies[0].technologies)), await generateResumeText(prompts.workHistory(payload.firstName, payload.companies[0].position, payload.companies[0].workedYears, payload.companies)) ]); }); console.log('passed chatgpt'); const pdf = await io.runTask('convert-to-html', async () => { const resume = createResume({ userDetails: payload, picture: payload.photo, profileSummary: texts[0], jobResponsibilities: texts[1], workHistory: texts[2], }); return {final: resume.split(',')[1]} }); console.log('converted to pdf'); await io.resend.sendEmail('send-email', { to: payload.email, subject: 'Resume', html: 'Your resume is attached!', attachments: [ { filename: 'resume.pdf', content: Buffer.from(pdf.final, 'base64'), contentType: 'application/pdf', } ], from: "Nevo David <[email protected]>", }); console.log('Sent email'); }, }); ``` 我們在檔案頂部的「Resend」實例載入了儀表板中的 API 金鑰。 我們有 ``` integrations: { resend }, ``` 我們將其加入到我們的作業中,以便稍後在“io”內部使用。 最後,我們的工作是發送 PDF `io.resend.sendEmail` 值得注意的是其中的附件,其中包含我們在上一步中產生的 PDF 文件。 我們就完成了🎉 ![我們完成了](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/esfhlds2qv1013c6x2h3.png) 您可以在此處檢查並執行完整的源程式碼: https://github.com/triggerdotdev/blog --- ## 讓我們聯絡吧! 🔌 作為開源開發者,我們邀請您加入我們的[社群](https://discord.gg/nkqV9xBYWy),以做出貢獻並與維護者互動。請隨時造訪我們的 [GitHub 儲存庫](https://github.com/triggerdotdev/trigger.dev),貢獻並建立與 Trigger.dev 相關的問題。 本教學的源程式碼可在此處取得: https://github.com/triggerdotdev/blog/tree/main/blog-resume-builder 感謝您的閱讀! --- 原文出處:https://dev.to/triggerdotdev/creating-a-resume-builder-with-nextjs-triggerdev-and-gpt4-4gmf

React 設計模式 Design Patterns

![](https://refine.ams3.cdn.digitaloceanspaces.com/blog-banners/retool-alternative.png) ## 介紹: React 開發人員可以透過使用設計模式來節省時間和精力,設計模式提供了一種使用經過測試且可信賴的解決方案來解決問題的快速方法。它們支援低耦合的內聚模組,從而幫助 React 開發人員建立可維護、可擴展且高效的應用程式。在本文中,我們將探索 React 設計模式並研究它們如何改進 React 應用程式的開發。 ## 容器和表示模式 容器和表示模式是一種旨在將反應程式碼中的表示邏輯與業務邏輯分離的模式,從而使其模組化、可測試並遵循關注點分離原則。 大多數情況下,在 React 應用程式中,我們需要從後端/儲存取得資料或計算邏輯並在 React 元件上表示該計算的結果。在這些情況下,容器和表示模式大放異彩,因為它可用於將元件分為兩類,即: * 容器元件,充當負責資料取得或計算的元件。 * 表示元件,其工作是將獲取的資料或計算值呈現在 UI(使用者介面)上。 容器和表示模式的範例如下所示: ``` import React, { useEffect } from 'react'; import CharacterList from './CharacterList'; const StarWarsCharactersContainer:React.FC = () => { const [characters, setCharacters] = useState<Character>([]) const [isLoading, setIsLoading] = useState<boolean>(false); const [error, setError] = useState<boolean>(false); const getCharacters = async () => { setIsLoading(true); try { const response = await fetch("https://akabab.github.io/starwars-api/api/all.json"); const data = await response.json(); setIsLoading(false); if (!data) return; setCharacters(data); } catch(err) { setError(true); } finally { setIsLoading(true); } }; useEffect(() => { getCharacters(); }, []); return <CharacterList loading={loading} error={error} characters={characters} />; }; export default StarWarsCharactersContainer; ``` ``` // the component is responsible for displaying the characters import React from 'react'; import { Character } from './types'; interface CharacterListProps { loading: boolean; error: boolean; users: Character[]; } const CharacterList: React.FC<CharacterListProps> = ({ loading, error, characters }) => { if (loading && !error) return <div>Loading...</div>; if (!loading && error) return <div>error occured.unable to load characters</div>; if (!characters) return null; return ( <ul> {characters.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> ); }; export default CharacterList; ``` ## 有 Hooks 的元件組合 Hooks 是 React 16.8 中首次推出的全新功能。從那時起,他們在開發 React 應用程式中發揮了至關重要的作用。掛鉤是基本函數,可授予功能元件存取狀態和生命週期方法(以前僅可用於類別元件)的功能。另一方面,掛鉤可以專門設計來滿足元件要求並具有其他用例。 我們現在可以隔離所有狀態邏輯(一種需要反應性狀態變數的邏輯),並使用自訂掛鉤在元件中組合或使用它。因此,程式碼更加模組化和可測試,因為鉤子鬆散地綁定到元件,因此可以單獨測試。 帶有鉤子的元件組合示例如下所示: ``` // creating a custom hook that fetches star wars characters export const useFetchStarWarsCharacters = () => { const [characters, setCharacters] = useState<Character>([]) const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(false); const controller = new AbortController() const getCharacters = async () => { setIsLoading(true); try { const response = await fetch( "https://akabab.github.io/starwars-api/api/all.json", { method: "GET", credentials: "include", mode: "cors", headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, signal: controller.signal } ); const data = await response.json(); setIsLoading(false); if (!data) return; setCharacters(data); } catch(err) { setError(true); } finally { setIsLoading(true); } }; useEffect(() => { getCharacters(); return () => { controller.abort(); } }, []); return [ characters, isLoading, error ]; }; ``` 建立自訂鉤子後,我們將其導入到我們的 **StarWarsCharactersContainer** 元件中並使用它; ``` // importing the custom hook to a component and fetch the characters import React from 'react'; import { Character } from './types'; import { useFetchStarWarsCharacters } from './useFetchStarWarsCharacters'; const StarWarsCharactersContainer:React.FC = () => { const [ characters, isLoading, error ] = useFetchStarWarsCharacters(); return <CharacterList loading={loading} error={error} characters={characters} />; }; export default StarWarsCharactersContainer; ``` --- <橫幅隨機/> --- ## 使用Reducers進行狀態管理 大多數情況下,處理元件中的許多狀態會導致許多未分組狀態的問題,這可能是處理起來很麻煩且具有挑戰性的。在這種情況下,減速器模式可能是有用的選擇。我們可以使用減速器將狀態分類為某些操作,這些操作在執行時可以變更分組的狀態。 此模式允許使用它的開發人員控制元件和/或掛鉤的狀態管理,讓他們在發送事件時管理狀態變更。 使用減速器模式的範例如下所示: ![圖片描述](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mbob3gmfxws8k4ti0cyx.png) 在上面的程式碼中,元件調度兩個操作: * “**login**”操作類型會觸發狀態更改,影響三個狀態值,即 **loggedIn**、**user**、**token**。 *“**註銷**”操作只是將狀態重設為其初始值。 ## 提供者的資料管理 提供者模式對於資料管理非常有用,因為它利用上下文 API 透過應用程式的元件樹傳遞資料。這種模式是一種有效的解決支柱鑽井問題的方法,這一直是 React 開發中普遍關注的問題。 為了實現提供者模式,我們首先建立一個提供者元件。 Provider 是 Context 物件提供給我們的一個高階元件。我們可以利用React提供的createContext方法來建構一個Context物件。 ``` export const ThemeContext = React.createContext(null); export function ThemeProvider({ children }) { const [theme, setTheme] = React.useState("light"); return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> ); } ``` 建立提供者後,我們將使用建立的提供者元件封裝依賴上下文 API 中的資料的元件。 為了從上下文 API 取得資料,我們呼叫 useContext 鉤子,它接受上下文作為參數(在本例中為 **ThemeContext**)。 ``` import { useContext } from 'react'; import { ThemeProvider, ThemeContext } from "../context"; const HeaderSection = () => { <ThemeProvider> <TopNav /> </ThemeProvider>; }; const TopNav = () => { const { theme, setTheme } = useContext(ThemeContext); return ( <div style={{ backgroundColor: theme === "light" ? "#fff" : "#000 " }}> ... </div> ); }; ``` ## 使用 HOC(高階元件)增強元件 高階元件接受一個元件作為參數,並傳回一個注入了附加資料或功能的增壓元件。 React 中 HOC 的可能性是由於 React 更喜歡組合而不是繼承。 高階元件 (HOC) 模式提供了一種增加或修改元件功能的機制,促進元件重複使用和程式碼共用。 HOC 模式的範例如下所示: ``` import React from 'react' const higherOrderComponent = Component => { return class HOC extends React.Component { state = { name: 'John Doe' } render() { return <Component name={this.state.name {...this.props} /> } } const AvatarComponent = (props) => { return ( <div class="flex items-center justify-between"> <div class="rounded-full bg-red p-4"> {props.name} </div> <div> <p>I am a {props.description}.</p> </div> </div> ) } const SampleHOC = higherOrderComponent(AvatarComponent); const App = () => { return ( <div> <SampleHOC description="Frontend Engineer" /> </div> ) } export default App; ``` 在上面的程式碼中, **<AvatarComponent/>** 由 **higherOrderComponent** 提供 props,它將在內部使用。 ## 複合元件 複合元件模式是一種 React 設計模式,用於管理由子元件組成的父元件。 這種模式背後的原理是將父元件分解為更小的元件,然後使用 props、上下文或其他反應資料管理技術來管理這些較小元件之間的互動。 當需要建立由較小元件組成的可重複使用、多功能元件時,這種模式會派上用場。它使開發人員能夠建立複雜的 UI 元件,這些元件可以輕鬆自訂和擴展,同時保持清晰簡單的程式碼結構。 複合元件模式的用例範例如下所示: ``` import React, { createContext, useState } from 'react'; const ToggleContext = createContext(); function Toggle({ children }) { const [on, setOn] = useState(false); const toggle = () => setOn(!on); return ( <ToggleContext.Provider value={{ on, toggle }}> {children} </ToggleContext.Provider> ); } Toggle.On = function ToggleOn({ children }) { const { on } = useContext(ToggleContext); return on ? children : null; } Toggle.Off = function ToggleOff({ children }) { const { on } = useContext(ToggleContext); return on ? null : children; } Toggle.Button = function ToggleButton(props) { const { on, toggle } = useContext(ToggleContext); return <button onClick={toggle} {...props} />; } function App() { return ( <Toggle> <Toggle.On>The button is on</Toggle.On> <Toggle.Off>The button is off</Toggle.Off> <Toggle.Button>Toggle</Toggle.Button> </Toggle> ); } ``` ## 道具組合 這需要從幾個相關的 props 建立一個物件,並將其作為單個 props 傳遞給元件。 這種模式允許我們清理程式碼並使管理 props 變得更簡單,當我們想要將大量相關屬性傳遞給元件時,它特別有用。 ``` import React from 'react'; function P(props) { const { color, size, children, ...rest } = props; return ( <p style={{ color, fontSize: size }} {...rest}> { children } </p> ); } function App() { const paragraphProps = { color: "red", size: "20px", lineHeight: "22px" }; return <P {...paragraphProps}>This is a P</P>; } ``` ## 受控輸入 受控輸入模式可用於處理輸入欄位。此模式涉及使用事件處理程序在輸入欄位的值發生變更時更新元件狀態,以及將輸入欄位的目前值儲存在元件狀態中。 由於React 控制元件的狀態和行為,因此該模式使程式碼比不受控制的輸入模式更具可預測性和可讀性,後者不使用元件的狀態,而是直接透過DOM(文件物件模型)對其進行控制。 受控輸入模式的用例範例如下所示: ``` import React, { useState } from 'react'; function ControlledInput() { const [inputValue, setInputValue] = useState(''); const handleChange = (event) => { setInputValue(event.target.value); }; return ( <input type="text" value={inputValue} onChange={handleChange} /> ); } ``` ## 使用forwardRefs 管理自訂元件 稱為 ForwardRef 的高階元件將另一個元件作為輸入並輸出一個傳遞原始元件引用的新元件。透過這樣做,子元件的 ref(可用於檢索底層 DOM 節點或元件實例)可供父元件存取。 當建立與第三方程式庫或應用程式中的另一個自訂元件互動的自訂元件時,在工作流程中包含 ForwardRef 模式非常有幫助。透過授予對庫的 DOM 節點或另一個元件的 DOM 實例的存取權限,它有助於將此類元件的控制權轉移給您。 forwardRef 模式的用例範例如下所示: ``` import React from "react"; const CustomInput = React.forwardRef((props, ref) => ( <input type="text" {...props} ref={ref} /> )); const ParentComponent = () => { const inputRef = useRef(null); useEffect(() => { inputRef.current.focus(); }, []); return <CustomInput ref={inputRef} />; }; ``` 在上面的程式碼中,我們使用「forwardRefs」從元件「<ParentComponent/>」觸發了另一個元件「<CustomInput/>」的焦點。 # 結論 我們在本文中討論了 React 設計模式,包括高階元件、容器呈現元件模式、複合元件、受控元件等等。透過將這些設計模式和最佳實踐合併到您的 React 專案中,您可以提高程式碼質量,促進團隊協作,並使您的應用程式更具可擴展性、靈活性和可維護性。 --- 原文出處:https://dev.to/refine/react-design-patterns-230o

🧠 自己用 JavaScript 手寫:人工智慧/神經網路......! 😱 不使用任何套件!🤯

您是否嘗試過實際建立神經網路?不,我也沒有……直到今天! 在這篇文章中,我們將介紹我學到的一些東西,以及一些用 vanilla JS 編寫的非常簡單的神經網路的 2 個演示。 ## 介紹 今天早些時候,我正在閱讀 @supabase_io [“AI 內容風暴”](https://supabase.com/blog/supabase-ai-content-storm) 文章。 我突然意識到一件事。我得到了神經網路......但我實際上根本沒有得到它們! 就像,我得到了神經元的概念。但數學是如何運作的呢? 特別是如何使用「反向傳播」來訓練神經網路?偏差和權重如何發揮作用?什麼或誰是 sigmoid? ETC。 現在,明智的做法是閱讀大量文章,獲取一個庫並使用它。 **但我不懂事。** 因此,我閱讀了大量文章……然後決定建立我的第一個神經網路。 但這還不夠難,所以我決定用 JavaScript 來做(因為每個人似乎都使用 Python...)。哦,我決定在沒有任何庫的情況下完成它。哦,我也想在其中建立一個視覺化工具。 我有些不對勁……我似乎在痛苦中茁壯成長。 無論如何,我做到了,這就是我學到的東西。 ## 注意:這不是教學課程 聽著,我想澄清一下,這不是教學! 這只是我分享一些我在學習和我的**第一個**神經網路時發現有趣的事情。 請注意,這裡強調**第一**,所以請不要將其視為除了有趣的東西之外的任何東西。 我也盡力解釋每個部分及其作用,但與所有事情一樣,你對某些東西越熟練,你就越能更好地解釋它......所以我的一些解釋可能有點“偏離” ! 無論如何,既然所有這些都已經解決了,讓我們繼續吧! 如果您想直接跳到[最終演示](#the-demo),請繼續! ## 第一步 好的,首先,我可以建立的最基本的神經網路是什麼? 經過一番閱讀後,我發現神經網路可以像一些輸入神經元和一些輸出神經元一樣簡單。 每個輸入神經元都連接到一個輸出神經元,然後我們可以為每個連接加入權重。 考慮到這一點,我必須想出一個易於理解的問題來解決,但又足夠複雜以確保我的網路正常運作。 我決定建立一個神經網絡,它獲取圖表上某個點的 X 和 Y 座標,然後根據它們是正還是負為它們分配一個「團隊」(顏色)。 這樣我們就有了 2 個輸入(X 和 Y 位置),然後有 4 個輸出: 1. X > 0 且 Y > 0 2. X < 0 且 Y > 0 3. X > 0 且 Y < 0 4. X < 0 且 Y < 0 由於這裡的要求非常簡單,我們可以擺脫一些「隱藏」神經元(這是我稍後將介紹的內容)並讓事情變得超級簡單! 所以本質上我們必須建構一個看起來像這樣的神經網路: ![左側 2 個圓圈透過線連接到右側 4 個圓圈。深色背景上的霓虹燈風格。](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x9rh1tbxrndmjsz5b7oo.png) 左邊的圓圈是我們的輸入(X 和 Y 位置),右邊的圓圈是我們之前討論的輸出。 ### 我們的第一個神經元 好的,現在我們可以真正開始了。 現在我實際上並沒有先建置神經元。事實上,我實際上首先建立了一個視覺化工具,因為這是查看事情是否正常運作的最簡單方法,但我稍後會介紹這一點。 因此,讓我們建立一個神經元(或更具體地說,一些神經元及其連接)。 幸運的是,神經元其實非常簡單! (或者我應該說,它們*可以*非常簡單......它們在大型語言模型(LLM)等中變得更加複雜。) 簡單神經元具有偏差(將其視為內部權重,我們將加入到最終計算中以對每個神經元進行加權的數字),並透過每個連接之間的權重連接到其他神經元。 現在回想起來,單獨加入到每個神經元的連接可能是一個更好的主意,但我決定將每層神經元和每層連接作為單獨的專案加入,因為這樣更容易理解。 所以建構我的第一個神經網路的程式碼如下所示: ``` class NeuralNetwork { constructor(inputLen, outputLen) { this.inputLen = inputLen; this.outputLen = outputLen; this.weights = Array.from({ length: this.outputLen }, () => Array.from({ length: this.inputLen }, () => Math.random()) ); this.bias = Array(this.outputLen).fill(0); } } const neuralNetwork = new NeuralNetwork(2, 4); ``` 好的,我跳過了一些步驟,所以讓我們簡要介紹一下每個部分。 `this.inputLen = inputLen;` 和 `this.outputLen = outputLen;` 只是為了讓我們可以引用輸入和輸出的數量。 `this.weights = [...]` 是連線。現在看起來可能有點嚇人,但這就是我們正在做的事情: - 建立輸出神經元陣列(`outputLen`) - 將長度為“inputLen”的陣列加入到每個陣列條目,並用 0 到 1 之間的一些隨機值填充它以開始我們的工作。 該程式碼的輸出範例如下所示: ``` this.weights = [ [0.7583747881712366,0.4306037998314902], [0.40553698492617807,0.4419651593960727], [0.852978801662627,0.9762509253699836], [0.8701610553353811,0.5583309725764114] ] ``` 它們本質上代表以下內容: ``` [input 1 to output 1, input 2 to output 1], [input 1 to output 2, input 2 to output 2], [input 1 to output 3, input 2 to output 3], [input 1 to output 4, input 2 to output 4], ``` 然後我們還有「this.bias」。 這適用於輸出層中的每個神經元。我們稍後用它來加入輸出值,使一些神經元更強,一些神經元更弱。 它只是一個由 4 個 0 組成的陣列來讓我們開始,因為我們不想要初始偏差! 現在,雖然這是一個神經網絡,但它完全沒有用。 我們無法實際使用它......如果我們確實使用它,它產生的結果將是完全隨機的! 所以我們需要解決這些問題! ### 使用我們的網路! 我們需要做的第一件事是實際獲取一些輸入,透過我們的網路執行它們並收集輸出。 這是我想出來的: ``` propagate(inputs) { const output = new Array(this.outputLen); for (let i = 0; i < this.outputLen; i++) { output[i] = 0; for (let j = 0; j < this.inputLen; j++) { output[i] += this.weights[i][j] * inputs[j]; } output[i] += this.bias[i]; output[i] = this.sigmoid(output[i]); } return output; } sigmoid(x) { return 1 / (1 + Math.exp(-x)); } ``` 現在這裡有兩件有趣的事情。 ####乙狀結腸 首先,一件有趣的事情是我們的“sigmoid”函數。它所做的只是沿著「S 形」曲線將我們輸入的值(例如 12)轉換為 0 到 1 之間的值。 這是我們將價值觀從極端標準化為更統一的**且始終為正值的方式。** 進一步閱讀後,這裡還有其他關於如何將值更改為 0 到 1 之間的選項,但我還沒有完全探索它們(例如 [ReLU](https://en.wikipedia.org/wiki/Rectifier_(neural_networks)) 如果你想閱讀相關內容)。 我確信有一些很好的解釋為什麼需要這樣做,但在我的猴腦中,這只是將值保持在 0 和 1 之間的方法,以便乘法保持在一定範圍內並且值被「展平」。 這樣你就不會在過於強大的神經元之間出現「失控」的路徑。 例如,假設您有一個權重為 16 的連接和一個權重為 1 的連接,使用我們的 sigmoid 函數,我們可以將其從 16 倍的差異減少到大約 35% 的差異(「sigmoid(1)」為0.73 ,執行我們的函數後,`sigmoid(16)` 為0.99)。 這也意味著負值變成正值。 因此,透過 sigmoid 函數執行值意味著負數會轉換為 0 到 0.5 之間的值,0 的值恰好變成 0.5,大於 0 的值變成 0.5 到 1 之間的值。 ![描繪兩條虛線之間的 S 型曲線的圖形。中間有一條虛線表示0.5。深色背景上的霓虹燈發光造型。](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dr3460emdf5m77nc0rn2.png) 如果你考慮一下,這是有道理的,因為當我們開始將負數和正數相乘時,我們可以極大地改變我們的輸出。 例如,如果我們在 100 的路徑中有一個負神經元,而其餘的都是正神經元,這會將強值更改為弱值,並且可能會導致問題。 不管怎樣,隨著我閱讀更多和實驗更多,我相信我會更好地理解這一部分! #### 我需要偏見嗎? 第二個有趣的是「output[i] += this.bias[i];」。 好吧,在這個神經網路中,所有 4 個輸出都同樣重要,而且我們沒有隱藏神經元,所以我後來刪除了它以簡化程式碼! 但諷刺的是,在我們更複雜的神經網路上,由於網路反向傳播的工作方式,我需要重新加入輸出神經元的偏差。否則,一個輸出神經元會一直啟動。 我無法弄清楚這是否是必要的步驟,或者我的神經網路是否犯了一個錯誤,而這是為了彌補它。 再次提醒大家,我還在學習中,只是掌握了基礎知識,所以我不知道它是什麼! 🤣 #### 我們快到了 上面的其餘程式碼相當簡單。我們只是將每個輸入乘以與每個輸出相關的權重(並加入不必要的偏差!)。 事實上,我們現在就可以執行它,但結果會很糟糕!讓我們來解決這個問題! ### 訓練時間到了! 好吧,神經網路的最後一個重要部分是訓練它! 現在,隨著這篇文章越來越長,我將只介紹以下訓練程式碼的要點(順便說一下,我花了將近一個小時來編寫......我告訴過你我對此是個菜鳥! ) ``` train(inputs, target) { const output = this.propagate(inputs); const errors = new Array(this.outputLen); for (let i = 0; i < this.outputLen; i++) { errors[i] = target[i] - output[i]; for (let j = 0; j < this.inputLen; j++) { this.weights[i][j] += this.learningRate * errors[i] * output[i] * (1 - output[i]) * inputs[j]; } this.bias[i] += this.learningRate * errors[i]; } } ``` “為什麼花了這麼長時間?”我聽到你問了!好吧,它讓我思考了所有需要相乘才能更新每個權重的位元。 另外 `this.learningRate` 也需要一點時間來適應。這只是降低我們改變權重的速率,這樣我們就不會“超過”每個權重的目標值,但是將其調整到合理的值需要經驗......我沒有經驗並且設置得太低,所以我的程式碼看起來被破壞了! 經過一番擺弄後,我將數值定為 0.1(而不是 0.01 🤦🏼‍♂️),突然間事情開始變得更好了! 是的,所以我們有一個訓練函數。但請記住,此訓練功能僅進行一次訓練。 我們需要對我們的網路進行多次訓練,希望每次訓練都能使其更加準確。 我們將在一秒鐘內討論這一點,但我想分享一個快速的側面/我學到的東西。 #### 訓練資料調整 我知道我們甚至還沒有涵蓋最終的訓練資料,但這是我學到的一個有趣的觀點,適合這裡(因為它解釋了為什麼我花了這麼長時間來編寫這個訓練函數)。 最初我產生了數百個不同的訓練 X 和 Y 座標,全部都是隨機的。 但經過進一步閱讀後,我只產生 4 個靜態訓練點,得到了更好的結果: ``` const trainingData = [ { x: -0.5, y: -0.5, label: "blue" }, { x: 0.5, y: -0.5, label: "red" }, { x: -0.5, y: 0.5, label: "green" }, { x: 0.5, y: 0.5, label: "purple" } ]; ``` 一旦你明白了,就很有意義了! 我們希望「拉」值更接近目標,上述值是我們每個區域的確切「中心點」。 因此,對於給定的距離,我們的錯誤率將始終保持一致。 這意味著我們的神經網路學習得更快,因為我們的錯誤率更大,這取決於它們距離 X 更遠還是距離 Y 更遠。 我可以更好地解釋這一點,但這超出了本文的範圍。希望如果您仔細考慮一下,那麼它也會像對我一樣為您「點擊」! 諷刺的是,我回到了更大模型的更隨機的資料集,因為我想真正測試我對學習率、過度訓練等的理解。 ## 我們有一個有效且有用的神經網路! 實際上,這就是我們的整個神經網路。 不過,我們需要做一件事。 我們的訓練函數需要執行很多次! 因此,我們需要最後一個函數來做到這一點,它獲取我們的訓練資料並執行我們的訓練函數數百次: ``` function train() { for (let i = 0; i < 10000; i++) { const data = trainingData[Math.floor(Math.random() * trainingData.length)]; neuralNetwork.train([data.x, data.y], encode(data.label)); } console.log("Training complete"); } ``` ### 金髮女孩迭代 請注意,我們在「for」循環中訓練網路 10,000 次。 10,000 次迭代足以訓練這個特定的神經網路。但對於我們稍後將介紹的更複雜的問題,我需要更多的迭代(並降低學習率)。 這是機器學習有趣的部分之一,您需要足夠地訓練神經網路(這很難做到正確),但如果訓練太多,就會發生“過度擬合”,並且實際上開始得到更糟糕的結果。因此,為了獲得最佳結果,需要完美平衡! 不管怎樣,已經很多了,我們終於迎來了第一個示範! ## 簡單的普通 JS 神經網路演示 雖然有點亂,但我們的神經網路和所有訓練部分都在下面 CodePen 的前 67 行。 其餘程式碼行實際上執行我們的網路(“neuralNetwork.propagate([x, y]);”大約第 85 行),然後將點及其預測顏色輸出到“<canvas>”上。 「encode」和「decode」純粹是為了獲取我們的輸出神經元,找到哪個神經元具有最高的激活,然後將其映射到我們可視化的顏色。 這是最後要理解的事。我們的輸出神經元都會有一個值。神經網路不僅僅輸出 1, 0, 0, 0。 相反,它會輸出每個輸出神經元的「確定性」或猜測。因此我們將得到類似「0.92,0.76,0.55,0.87」的輸出。 這就是為什麼我們有“解碼”函數,它找到最高輸出的神經元並將其作為我們的最終猜測! ``` // this line finds the max value of all of our output neurons and then returns its index so we can use that to classify our X and Y coordinates. const maxIndex = output.indexOf(Math.max(...output)); ``` ### 用法和實際演示 要使用該範例,您有 3 個按鈕: - **訓練** - 在我們的神經網路開始未經訓練和隨機化時對其進行訓練。 - **分類點** - 這是為了執行我們的神經網路。它將在圖表上繪製點並為它們分配顏色。我建議在訓練之前和之後執行這個。 - **重設** - 這將建立一個未經訓練的新神經網路。非常適合在訓練前後測試點的分類。 另請注意,每個區域都根據該區域應顯示的顏色進行著色。它真的可以讓您看到隨機且未經訓練的神經網路距離成功還有多遠(重置然後對測試點進行分類)! 玩吧! https://codepen.io/GrahamTheDev/pen/abPxoqb ### 我們最基本的神經網路結束 這樣我們就有了最基本的神經網路! 它可以很好地滿足我們的需求,並且我們設法了解了一些關於反向傳播(我們在主類別中的“train”函數)以及權重和偏差的知識。 但這是非常有限的。如果我們將來想做更高級的事情,我們需要加入一些隱藏神經元! ## 版本 2 - 隱藏神經元 好的,那為什麼要隱藏神經元呢?他們的目的是什麼? 在更複雜的範例中,它們充當獲取輸入並為它們的分類方式加入更多維度的方式。 我們仍然使用 2 個輸入神經元和 4 個輸出神經元,但這次我們在中間加入了一個附加層(我們可以更改和調整其中神經元的數量)。 所以我們的神經網路看起來像這樣: ![左側 2 個圓圈透過線連接到中間的 6 個圓圈,然後透過線連接到右側的 4 個圓圈。深色背景上的霓虹燈風格。](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z0f1204y2d96wucq6fzv.png) 由於神經網路需要處理更多的輸入並進行更複雜的計算,隱藏層中的額外神經元使它們能夠更好地對輸入進行分類並提供更好的結果。 隱藏層也可以是不同的「深度」。 假設我們有 2 個輸入神經元。我們可以將它們連接到 6 個「隱藏」神經元,然後將它們連接到 4 個輸出神經元。 但我們也可以將第一層的 6 個神經元連結到第二層隱藏神經元。第二層可能有 8 個神經元,然後連接到我們的 4 個輸出神經元。 但要遵循的內容還有很多,這是為了讓我學習基礎知識,所以我選擇加入一個隱藏層。這也意味著我可以將每個連接層保留為單獨的陣列,這在現階段更容易理解! ### 那麼有什麼新內容呢? 沒有太大變化,只是我們有了更多的連結和更多的神經元! 您可以將其視為串聯加入 2 個原始神經網絡,只是第一個神經網路的輸出現在充當第二個神經網路的輸入。 雖然程式碼可能更加複雜,但我們的神經網路遵循相同的原則。 這是程式碼: ``` class NeuralNetwork { constructor(inputSize, hiddenSize, outputSize) { this.inputSize = inputSize; this.hiddenSize = hiddenSize; this.outputSize = outputSize; this.weightsInputToHidden = Array.from({ length: hiddenSize }, () => Array.from({ length: inputSize }, () => Math.random() * 2 - 1) ); this.biasHidden = Array(hiddenSize).fill(0); this.weightsHiddenToOutput = Array.from({ length: outputSize }, () => Array.from({ length: hiddenSize }, () => Math.random() * 2 - 1) ); this.biasOutput = Array(outputSize).fill(0); this.learningRate = document.querySelector('#learningRate').value; // Adjusted learning rate this.hiddenLayer = new Array(this.hiddenSize); } feedForward(inputs) { for (let i = 0; i < this.hiddenSize; i++) { this.hiddenLayer[i] = 0; for (let j = 0; j < this.inputSize; j++) { this.hiddenLayer[i] += this.weightsInputToHidden[i][j] * inputs[j]; } this.hiddenLayer[i] += this.biasHidden[i]; this.hiddenLayer[i] = sigmoid(this.hiddenLayer[i]); } const output = new Array(this.outputSize); for (let i = 0; i < this.outputSize; i++) { output[i] = 0; for (let j = 0; j < this.hiddenSize; j++) { output[i] += this.weightsHiddenToOutput[i][j] * this.hiddenLayer[j]; } output[i] += this.biasOutput[i]; output[i] = sigmoid(output[i]); } return output; } train(inputs, target) { for (let i = 0; i < this.hiddenSize; i++) { this.hiddenLayer[i] = 0; for (let j = 0; j < this.inputSize; j++) { this.hiddenLayer[i] += this.weightsInputToHidden[i][j] * inputs[j]; } this.hiddenLayer[i] += this.biasHidden[i]; this.hiddenLayer[i] = sigmoid(this.hiddenLayer[i]); } const output = new Array(this.outputSize); for (let i = 0; i < this.outputSize; i++) { output[i] = 0; for (let j = 0; j < this.hiddenSize; j++) { output[i] += this.weightsHiddenToOutput[i][j] * this.hiddenLayer[j]; } output[i] += this.biasOutput[i]; output[i] = sigmoid(output[i]); } const errorsOutput = new Array(this.outputSize); const errorsHidden = new Array(this.hiddenSize); for (let i = 0; i < this.outputSize; i++) { errorsOutput[i] = target[i] - output[i]; for (let j = 0; j < this.hiddenSize; j++) { this.weightsHiddenToOutput[i][j] += this.learningRate * errorsOutput[i] * output[i] * (1 - output[i]) * this.hiddenLayer[j]; } this.biasOutput[i] += this.learningRate * errorsOutput[i]; } for (let i = 0; i < this.hiddenSize; i++) { errorsHidden[i] = 0; for (let j = 0; j < this.outputSize; j++) { errorsHidden[i] += this.weightsHiddenToOutput[j][i] * errorsOutput[j]; } this.biasHidden[i] += this.learningRate * errorsHidden[i]; for (let j = 0; j < this.inputSize; j++) { this.weightsInputToHidden[i][j] += this.learningRate * errorsHidden[i] * this.hiddenLayer[i] * (1 - this.hiddenLayer[i]) * inputs[j]; } } } } ``` 現在,不要被嚇倒,我剛剛複製了幾個循環,其中要操作的目標資料集略有不同。 我們加入了一組額外的偏差(對於我們的隱藏層)和一組額外的連接:我們的輸入層到我們的隱藏層,然後我們的隱藏層現在連接到我們的輸出層。 最後,我們的「train」函數有一些額外的循環,只是為了透過每個步驟進行反向傳播。 唯一值得一提的其他變化是我們現在有第三個輸入參數(中間),用於隱藏神經元的數量。 ### 醜陋,但似乎有用 看,我想再說一遍,這是我一邊學習一邊學習,所以程式碼反映了這一點。 這裡有很多重複,可擴展性不太好。 然而,據我所知,它是有效的。 話雖如此,雖然它有效,但它的性能似乎比我們原來的、簡單得多的神經網路要差。 這要么意味著我犯了一個錯誤(可能),要么是我沒有「撥入」正確的訓練設定。 說到這裡... ### 加入一些變數來玩 由於這更複雜,我在一些快速設定中「迴避」了。 現在我們可以更新: - **訓練資料大小** - 我們產生的不同隨機點的數量 - **訓練迭代** - 我們從訓練集中選擇隨機資料點並將其輸入神經網路上的「訓練」函數的次數。 - **學習率** - 我們根據錯誤調整速度的乘數。 - **隱藏節點(超過2個!)** - 調整第二層有多少個隱藏節點(需要您再次初始化網絡,否則會損壞!) - **要分類的點** - 傳遞給我們訓練過的神經網路並繪製在圖表上的點數。 這意味著我們可以更快地處理這些值,看看它們對我們的神經網路及其準確性有什麼影響! ### 最後一件事 哦,我加入了一個按鈕來視覺化神經網路的樣子。 無論如何按“可視化神經元和權重”,但它還沒有完成。我也沒有立即完成它的打算,因為我想完全重新設計建構神經網路的方法,使其更具可擴展性。 不過,按鈕就在那裡,請隨意按下。更好的是,請隨時為我修復它! 🤣💗 ## 演示 控制項與以前相同,加上前面 2 個小節中提到的輸入。 試試一下,看看是否可以微調學習率、神經元數量和訓練設定以獲得真正準確的結果! https://codepen.io/GrahamTheDev/pen/qBLwBxP 請務必更新一些值,重新初始化神經網絡,嘗試使用不同數量的隱藏神經元等。 如果您像我一樣是初學者,希望您能開始理解一些事情! ## 結論 用 vanilla JS 建立神經網路真的很有趣。 我沒有見過很多人這樣做,所以我希望它對你或至少對某人有用! 我學到了很多關於偏差、反向傳播(神經網路的關鍵)等的知識。 顯然這個例子和這裡學到的東西只是機器學習的1%。但對於像我這樣的微小的、未優化的神經網路和巨大的數十億參數模型來說,核心原理是相同的。 這個例子就像機器學習 (ML) 和神經網路的「hello world」。 接下來,我真的想嘗試建立一個更大的、結構更好、更容易擴展的神經網絡,看看我們是否可以進行一些光學字元辨識(OCR)。您可以將其視為機器學習和神經網路的“待辦事項清單”! ### 發表評論。 您是神經網路專家嗎?告訴我我哪裡錯了! 你是像我一樣的初學者嗎?那麼請告訴我這是否有助於您理解,至少一點點!或者,如果這實際上讓事情變得更加混亂! 😱 最重要的是,如果這篇文章激發了您對我糟糕的編碼做鬼臉,或者想要建置您自己的神經網絡......那麼我很高興它對您產生了一些影響,並且很樂意聽到它! 💗 --- 原文出處:https://dev.to/grahamthedev/a-noob-learns-ai-my-first-neural-networkin-vanilla-jswith-no-libraries-1f92

使用 CSS 建立漂亮的 HTML 表格

原文出處:https://dev.to/dcodeyt/creating-beautiful-html-tables-with-css-428l 讓您的 HTML 表格看起來很棒很容易 - 在今天的帖子中,我們將看看大約 30 行 CSS 來做到這一點! https://www.youtube.com/watch?v=biI9OFH6Nmg ### 寫 HTML 讓我們為該表編寫一些樣板 HTML 程式碼。 ``` <table class="styled-table"> <thead> <tr> <th>Name</th> <th>Points</th> </tr> </thead> <tbody> <tr> <td>Dom</td> <td>6000</td> </tr> <tr class="active-row"> <td>Melissa</td> <td>5150</td> </tr> <!-- and so on... --> </tbody> </table> ``` 請注意,我們有兩個類別: * `.styled-table` 這樣我們就不會竄改網站上的每個 `<table>` * `.active-row` 將是「活動」行 - 這個類別用於突出顯示特定行並將獲得它自己的 CSS,我們很快就會看到 ### 表格樣式 讓我們先定位主要的「<table>」元素: ``` .styled-table { border-collapse: collapse; margin: 25px 0; font-size: 0.9em; font-family: sans-serif; min-width: 400px; box-shadow: 0 0 20px rgba(0, 0, 0, 0.15); } ``` 其中大部分都是不言自明的,但讓我們來看看主要的: * `box-shadow` 在桌子周圍加入微妙的透明陰影 * `border-collapse` 確保單元格邊框之間沒有空格 ### 設定標題樣式 對於標題,我們可以簡單地更改背景顏色並對文字應用一些基本樣式: ``` .styled-table thead tr { background-color: #009879; color: #ffffff; text-align: left; } ``` ### 移至表格儲存格 讓我們把事情分開一點: ``` .styled-table th, .styled-table td { padding: 12px 15px; } ``` ### 以及表格行... 為此,我們要做三件事: 1. 為每行新增下邊框進行分隔 2. 每隔一行新增淺色背景以提高可讀性 3. 在最後一行加上深色邊框以表示表格的結尾 ``` .styled-table tbody tr { border-bottom: 1px solid #dddddd; } .styled-table tbody tr:nth-of-type(even) { background-color: #f3f3f3; } .styled-table tbody tr:last-of-type { border-bottom: 2px solid #009879; } ``` ### 最後,讓活動行看起來有所不同 為此,我們只需對文字進行更改: ``` .styled-table tbody tr.active-row { font-weight: bold; color: #009879; } ``` **就是這樣!** 如果您有任何改進建議,請在下面的回覆中告訴我 😁 ## [立即報名👉 JavaScript DOM 速成課程](https://www.udemy.com/course/the-ultimate-javascript-dom-crash-course/?referralCode=DC343E5C8ED163F337E1) 如果您正在學習 Web 開發,您可以在下面的連結中找到有關 JavaScript DOM 的完整課程👇 [https://www.udemy.com/course/the-ultimate-javascript-dom-crash-course/?referralCode=DC343E5C8ED163F337E1](https://www.udemy.com/course/the-ultimate-javascript-dom-crash-course/?referralCode=DC343E5C8ED163F337E1) ![課程縮圖](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d2z3timz15pb93howzzk.jpg)

使用 Services 和 RxJS 在 Angular 中進行簡單的狀態管理

原文出處:https://dev.to/avatsaev/simple-state-management-in-angular-with-only-services-and-rxjs-41p8 軟體開發中最具挑戰性的事情之一是狀態管理。目前,Angular 應用程式有幾個狀態管理庫:NGRX、NGXS、Akita...它們都有不同的狀態管理風格,最受歡迎的是 [NGRX](https://ngrx.io/),它幾乎遵循React 世界的FLUX/Redux 原則(基本上使用單向資料流和不可變資料結構,但使用RxJS可觀察流)。 但是,如果您不想學習、設定、使用整個狀態管理庫,也不想處理一個簡單專案的所有樣板文件,如果您只想使用您已經熟悉的工具來**管理狀態,該怎麼辦?Anular 開發人員 * *,並且仍然獲得狀態管理庫提供的效能最佳化和一致性(推送變更偵測,一種不可變資料流)。 **免責聲明:這不是針對狀態管理庫的帖子。我們確實在工作中使用NGRX,它確實幫助我們在非常大和複雜的應用程式中管理非常複雜的狀態,但正如我常說的,NGRX 使簡單應用程式的事情變得複雜,並簡化了複雜應用程式的事情,請記住這一點。* * 在這篇文章中,我將向您展示一種僅使用 **RxJS** 和 **依賴注入** 來管理狀態的簡單方法,我們所有的元件樹都將使用 OnPush 更改檢測策略。 想像我們有一個簡單的 Todo 應用程式,我們想要管理它的狀態,我們已經設定了元件,現在我們需要一個服務來管理狀態,讓我們建立一個簡單的 Angular 服務: ``` // todos-store.service.ts @Injectable({provideIn: 'root'}) export class TodosStoreService { } ``` 所以我們需要的是,一種提供待辦事項清單的方法,一種加入、刪除、過濾和完成待辦事項的方法,我們將使用 getter/setter 和 RxJS 的行為主題來執行此操作: 首先我們建立在 todos 中讀寫的方法: ``` // todos-store.service.ts @Injectable({provideIn: 'root'}) export class TodosStoreService { // - We set the initial state in BehaviorSubject's constructor // - Nobody outside the Store should have access to the BehaviorSubject // because it has the write rights // - Writing to state should be handled by specialized Store methods (ex: addTodo, removeTodo, etc) // - Create one BehaviorSubject per store entity, for example if you have TodoGroups // create a new BehaviorSubject for it, as well as the observable$, and getters/setters private readonly _todos = new BehaviorSubject<Todo[]>([]); // Expose the observable$ part of the _todos subject (read only stream) readonly todos$ = this._todos.asObservable(); // the getter will return the last value emitted in _todos subject get todos(): Todo[] { return this._todos.getValue(); } // assigning a value to this.todos will push it onto the observable // and down to all of its subsribers (ex: this.todos = []) private set todos(val: Todo[]) { this._todos.next(val); } addTodo(title: string) { // we assaign a new copy of todos by adding a new todo to it // with automatically assigned ID ( don't do this at home, use uuid() ) this.todos = [ ...this.todos, {id: this.todos.length + 1, title, isCompleted: false} ]; } removeTodo(id: number) { this.todos = this.todos.filter(todo => todo.id !== id); } } ``` 現在讓我們建立一個方法來設定待辦事項的完成狀態: ``` // todos-store.service.ts setCompleted(id: number, isCompleted: boolean) { let todo = this.todos.find(todo => todo.id === id); if(todo) { // we need to make a new copy of todos array, and the todo as well // remember, our state must always remain immutable // otherwise, on push change detection won't work, and won't update its view const index = this.todos.indexOf(todo); this.todos[index] = { ...todo, isCompleted } this.todos = [...this.todos]; } } ``` 最後是一個可觀察的來源,它將只為我們提供已完成的待辦事項: ``` // todos-store.service.ts // we'll compose the todos$ observable with map operator to create a stream of only completed todos readonly completedTodos$ = this.todos$.pipe( map(todos => todos.filter(todo => todo.isCompleted)) ) ``` 現在,我們的待辦事項商店看起來像這樣: ``` // todos-store.service.ts @Injectable({providedIn: 'root'}) export class TodosStoreService { // - We set the initial state in BehaviorSubject's constructor // - Nobody outside the Store should have access to the BehaviorSubject // because it has the write rights // - Writing to state should be handled by specialized Store methods (ex: addTodo, removeTodo, etc) // - Create one BehaviorSubject per store entity, for example if you have TodoGroups // create a new BehaviorSubject for it, as well as the observable$, and getters/setters private readonly _todos = new BehaviorSubject<Todo[]>([]); // Expose the observable$ part of the _todos subject (read only stream) readonly todos$ = this._todos.asObservable(); // we'll compose the todos$ observable with map operator to create a stream of only completed todos readonly completedTodos$ = this.todos$.pipe( map(todos => todos.filter(todo => todo.isCompleted)) ) // the getter will return the last value emitted in _todos subject get todos(): Todo[] { return this._todos.getValue(); } // assigning a value to this.todos will push it onto the observable // and down to all of its subsribers (ex: this.todos = []) private set todos(val: Todo[]) { this._todos.next(val); } addTodo(title: string) { // we assaign a new copy of todos by adding a new todo to it // with automatically assigned ID ( don't do this at home, use uuid() ) this.todos = [ ...this.todos, {id: this.todos.length + 1, title, isCompleted: false} ]; } removeTodo(id: number) { this.todos = this.todos.filter(todo => todo.id !== id); } setCompleted(id: number, isCompleted: boolean) { let todo = this.todos.find(todo => todo.id === id); if(todo) { // we need to make a new copy of todos array, and the todo as well // remember, our state must always remain immutable // otherwise, on push change detection won't work, and won't update its view const index = this.todos.indexOf(todo); this.todos[index] = { ...todo, isCompleted } this.todos = [...this.todos]; } } } ``` 現在我們的智慧元件可以存取商店並輕鬆操作它: *(PS:我建議使用[ImmutableJS](https://immutable-js.github.io/immutable-js)而不是手動管理不變性)* ``` // app.component.ts export class AppComponent { constructor(public todosStore: TodosStoreService) {} } ``` ``` <!-- app.component.html --> <div class="all-todos"> <p>All todos</p> <app-todo *ngFor="let todo of todosStore.todos$ | async" [todo]="todo" (complete)="todosStore.setCompleted(todo.id, $event)" (remove)="todosStore.removeTodo($event)" ></app-todo> </div> ``` 這是完整的最終結果: [StackBlitz 上具有真實 REST API 的完整範例](https://stackblitz.com/edit/angular-rxjs-store?file=src%2Fapp%2Ftodos-store.service.ts) 這也是一種可擴展的狀態管理方式,您可以使用Angular 強大的DI 系統輕鬆地將其他儲存服務注入彼此,將其可觀察量與管道運算符結合起來以建立更複雜的可觀察量,並注入HttpClient 等服務以從伺服器提取資料例如。無需所有 NGRX 樣板或安裝其他狀態管理庫。盡可能保持簡單和輕鬆。 --- 在 Twitter 上關注我,以了解更多有趣的 Angular 相關內容:https://twitter.com/avatsaev

您的下一個專案,可以試試看 HTMX 技術!

原文出處:https://dev.to/turculaurentiu91/why-you-should-choose-htmx-for-your-next-project-o7j 在本文中,我們將旨在了解為什麼您下次為 Web 應用程式選擇技術堆疊時應該考慮 HTMX 作為 React 的替代品。我們將研究傳統 HTTP JSON API + React 帶來的複雜性和挑戰,以及如何透過使用 HTMX 輕鬆避免它們。 **注意**:在本文中,我將討論 React,但它可以替換為任何其他前端框架,如 Angular、Vue、Svelte 或 Solid,但我談論 React 是因為它是大多數 Web 的預設技術開發人員預設為。 ### HTMX 到底是什麼 如果您還不知道,[HTMX](https://htmx.org/) 是一個小型瀏覽器 (JS) 庫,它使用一些屬性擴展了 HTML,允許您使用來自伺服器。它還使 HTML 能夠對所有動詞發出 HTTP 請求,而不僅僅是 GET 和 POST。 ## React 解決了什麼問題 React 是一個 JavaScript 程式庫,可協助您透過保持使用者介面與狀態同步來編寫高度互動的應用程式。您告訴它如何渲染給定的狀態,每次更新狀態時,它都會重新渲染(盡可能有效率)UI 以反映狀態變更。 每次狀態發生變化時,您都會通知庫它發生了變化,並提供新的狀態,它將處理 UI 更新。 需要本地記憶體狀態的高互動應用程式的範例可以是您可以在網路上找到的各種文字編輯器(VSCode)之一、拖放看板(如 Trello 或 JIRA)、視訊播放器或聊天室。 什麼不是此類應用程式的範例?您正在建立的待辦事項清單、您正在閱讀的新聞網站、您發布的部落格以及周圍的大多數網站。如果我們看一下 [80/20 規則](https://en.wikipedia.org/wiki/Pareto_principle) >80% 的結果來自 20% 的原因,80% 的結果來自 20% 的努力。 你可以說 80% 使用 React 的 Web 應用程式不需要本地狀態。對於那 20% 需要它的人來說,你可以說它只是應用程式的一小部分(大約 20%),其餘部分只能用 HTML 來表達。 **這些數字是編造的,我沒有任何研究來支持這一點** ## React 也解決了哪些問題,使其被現代網站廣泛採用 HTML 已經過時了。使用 HTML 製作應用程式的舊方法涉及頁面、連結和表單的集合,這些頁面、連結和表單向使用者描述給定資源的當前狀態以及他們可以執行哪些操作來更改它。 每次使用者與資源互動時,應用程式只能重新載入整個頁面以顯示資源的新狀態。 幾年後,FaceBook 推出了 React,這是一個 JS 程式庫,可讓開發人員建立單頁應用程式 (SPA)。導航時不再需要重新加載整個頁面,狀態更新的酷過渡、對用戶的有趣反饋以及其他使 Web 開發人員在其網站中採用 SPA 框架的細節。 ## 複雜性問題 ![AI 產生圖像,透過 next.js 展示現代應用程式的瘋狂複雜性](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xpk0v0nz0d45hv91i166.png) 如果你不理解上面的架構,不用擔心,沒有什麼好理解的。我要求 ChantGPT 為我生成它,由於它過於複雜且沒有任何意義,它完美地反映了現代 Web 應用程式目前的預設基礎架構。 一個很酷的程式設計原則是**KISS**,它代表“保持簡單,愚蠢”,或者有些人可能喜歡開玩笑,“保持簡單,愚蠢!” 現代開發人員預設建立 Web 應用程式的當前基礎設施和技術堆疊極其複雜,做了很多不必要的事情,只是因為它很酷! 當您自己建立第一個 POC 時,效果很好,但下一刻您加入了更多團隊成員,並且使用多次迭代和「擁抱」更改的敏捷方式,它有點崩潰,原因是我們將往下看一下。 ## 傳統 HTTP JSON API + React 的狀態管理問題 在 Web 應用程式中,您經常要做的就是從資料庫中獲取資源的狀態並將其呈現給使用者。讓我們以任務管理應用程式為例。使用者有一個任務列表,每個任務都有一個狀態: - 任務的標題 - 說明 - 任務完成時的標誌 - 截止日期(可選) 我們通常將此狀態儲存在資料庫中,並將此資訊呈現給用戶,您必須: - 從使用者有權存取的資料庫中取得所有任務。 - 可以選擇轉換資料(也許您儲存完成的日期並從中計算「is_completed」標誌)。 - 將資料序列化為 JSON。 - 透過 HTTP 請求取得資料。 - (可選但通常)根據模式驗證資料,可能使用 YUP 或 ZOD。 - 使用 Redux、Zustand、react-query 或其他狀態管理庫將 JSON 轉換為狀態並將其儲存在快取中。 - 轉換 HTML 中的狀態,通常會決定使用者可以使用資料做什麼。 簡而言之,我們正在描述如何在 JavaScript 中渲染所有資源的所有可能狀態,在瀏覽器中下載所述 JavaScript,然後 JavaScript 下載一堆 JSON 格式的資料並將其渲染(如果它知道如何)瀏覽器顯示為HTML! 向使用者顯示任務清單需要做大量工作,特別是當任務僅在使用者變更時才更改時,應用程式必須將應用程式置於載入狀態,發出另一個 HTTP 請求(以PUT 或PATCH 或DELETE)使快取值(狀態)無效並重新獲取它以顯示更改的任務。 或者更糟的是,當用戶更改某些任務時,樂觀地更新本地狀態並立即顯示更改,並在幕後執行更新請求,僅在他們成功更新後通知用戶更新失敗。 這是非常容易出錯的。對於這個待辦事項應用程式來說,它可能會很有效,因為您是唯一的開發人員,並且該應用程式足夠大,您可以在腦海中保留正在發生的所有事情的地圖。但是當你的團隊規模更大時,尤其是當你將團隊分成前端和後端時,溝通不良可能會導致很多問題。 後端可能使用“is_completed”標誌,而前端可能需要“is_active”標誌。後端可能會將經過 Markdown 處理的「描述」傳送到 HTML,而前端可能希望它不被處理。後端可能會將“描述”設為可選,以允許使用者在前端不同步時保存草稿,並且您會看到許多“未捕獲的類型錯誤:無法讀取未定義的屬性(讀取“toLowerCase” )” 另一方面,在 HTMX 上,您可以直接在範本上呈現 HTML,只要後端語言允許,就可以確保類型安全。您僅向瀏覽器發送相關訊息,向使用者提供對資源的適當控制,並指示瀏覽器或 HTMX 如何解釋使用者操作以及後端對這些操作的回應。所有應用程式狀態都是 HTML,這個概念稱為 [HATEOAS](https://htmx.org/essays/hateoas/) ## 傳統 HTTP JSON API + React 對文件的需求 為了讓兩個團隊(後端和前端)獨立工作並透過 HTTP JSON API 進行通信,您需要擁有適當的 API 文件。您還需要記錄如何計算使用者可以對給定資源執行哪些操作以顯示控制項。 大多數此類文件寫起來都很痛苦,特別是因為通常需要在實作之前編寫,而開發人員尚未完全理解問題的範圍,因此前端可以並行開發。這通常會在開發過程中進行許多更新,以適應開發過程中出現的問題,並可能導致團隊之間的版本不一致。 您還需要對 API 進行版本控制,並注意不要在非主要版本變更中引入重大變更。您無法再在不變更主要版本的情況下變更欄位名稱。您還需要保持 API 的多個版本運作或強制前端團隊進行調整。 大多數時候,文件已經過時了。有些必須緊急修復,有些新要求是在發布前一天提出的,現在您的文件已經過時,即使在很短的時間內。而且您必須記住更新它,或者更糟的是,您建立了一張票來記住它,而其他人拿起它,但沒有完整的圖片並記錄了錯誤! ## 重複的邏輯問題 對於每個資源,您必須實施授權策略。您必須確定目前使用者是否可以將任務 **46234** 標記為已完成。您必須在後端程式碼的某個位置編寫此檢查。否則,您的應用程式將開放給_不安全的直接物件參考_,或任何使用 Postman 的人都可以將您的任務標記為已完成。 您還必須在前端實現相同的邏輯,僅當用戶有權標記已完成時才顯示標記按鈕(讓我們假設您可以與其他用戶共享您的任務,但只有您可以更改它們)。 現在每次這個邏輯改變時,你都必須在兩個應用程式中實作它,並同時發布它或擁有多個版本的API。 ## 效能問題 為了使用 React 在瀏覽器中呈現網站,您需要將佔用大量內存和解析/處理影響的 React 程式碼、狀態管理庫程式碼、腳趾 UI 庫程式碼、CSS-IN- 捆綁在一起。JS 庫程式碼、應用程式程式碼以及我們透過NPM 安裝和使用的任何js 程式庫(我們並不羞於安裝新套件,請參閱[leftpad 問題](https://www.theregister.com/2016/03/23/npm_left_pad_chaos/))。這通常會導致透過網路傳送大塊的 JavaScript 資源。當然,您可以在瀏覽器中緩存,但在現代敏捷開發中,每個衝刺至少部署一次,因此這無法解決任何問題。這會消耗網路流量和電池,這對行動裝置來說是一個經常被忽視的問題。 上述JavaScript需要由瀏覽器來解釋,消耗處理能力和電池。 JavaScript,尤其是 ReactDOM,需要追蹤 DOM 的鏡像。在其之上加入普通 DOM 和本地狀態緩存,以及所有渲染函數,以及所有“useMemo”、“useCallback”和“useState”。也要加入需要在記憶體中保存所有上下文變數的所有閉包。 JavaScript 引擎並不以其記憶體效率而聞名!您會聽到人們抱怨瀏覽器消耗了多少內存,但他們低估了他們存取的網站所消耗的內存量。 所有這些加起來,最終會耗盡用戶的電池和記憶體。當然,您可以付出努力並優化所有這些,或使用其他程式庫(如 Svelte),但所有這些努力都可以用於為您的用戶提供更有意義的功能。 ## 服務端渲染的需要 近年來,我們播種了伺服器端渲染專用框架(如「Next.js」)的興起。它們的流行凸顯了對以 HTML 格式交付內容的需求,特別是出於可存取性優化、效能和搜尋引擎優化的原因。 你不想等待瀏覽器下載 JavaScript 來渲染頁面,然後等待 JavaScript 發出 HTTP 請求來獲取內容然後渲染它,你希望它立即渲染,特別是對於上面的情況折疊內容。 這又增加了一層複雜性,包括: - 基礎設施,現在您還需要另一台伺服器用於前端應用程式 - 程式碼更複雜,包括什麼程式碼在伺服器上執行以及什麼在瀏覽器上執行的思維導圖 - 部署管道現在更加複雜 - 測試基礎設施現在更加複雜 - 現在解決問題變得更加困難,您需要了解問題是在瀏覽器上、在客戶端應用程式伺服器上還是在 API 伺服器上 ## 解決這些問題 Web 開發社群各自使用自己的語言或開發技術,以不同的方式解決這些問題: - Next.js(以及 Nuxt 等) - 反應伺服器元件 - 拉維爾 - 慣性.JS - 活線 - 點網 - Blazor 頁面 - 靈藥 - 鳳凰即時查看 - 鐵鏽 - 樂浦伺服器功能 還有許多我忘記或從未聽說過的其他解決方案! 無論如何,此類解決方案的存在和流行證明了這些問題是有效的並且在 Web 開發人員的日常生活中遇到過。否則他們不會不遺餘力地解決這些問題,尤其是以開源的方式! 還有 [Turbo](https://turbo.hotwired.dev/) 以及採用它們的框架、Ruby on Rails、PHP Symphony 以及其他可能以與 HTMX 相同的方式解決相同問題的框架。選擇 HTMX 只是個人喜好,但你絕對應該了解這一點,這和 HTMX 一樣酷! 在所有這些中,HTMX 脫穎而出,不僅因為它不會將您鎖定到特定技術,您可以透過對模板進行微小更改來從 PHP 切換到 Rust,而且還因為它完全消除了對有狀態元件的需求,或者需要追蹤與資源無關的應用程式的某種狀態。 例如,讓我們採用確認對話方塊模式。您通常最終要做的是,您有一個本地記憶體狀態(如果它是開啟的),並根據該狀態將其顯示給使用者。在 HTMX 中,狀態 **IS THE HTML** 意味著當您按一下開啟模式時,您 **GET** `tasks/{taskId}/confirm-delete` 並將回應 HTML 嵌入到 DOM 中。當它被刪除時,您就完全刪除了模式和任務!這以一種獨特且極其簡單的方式解決了上述所有問題,您不需要: - 追蹤狀態 - 知道如何渲染對話框 - 記錄API - 檢查使用者是否可以刪除任務(在前端) - 您的後端應用程式始終負責 - 您可以獲得更好的安全性,因為您不會向瀏覽器發送不相關的資料並竊取敏感資訊 - 你會得到更好的表現 __*最重要的是,您可以讓您的應用程式保持簡單,只有在解決使用者問題時才允許複雜性!*__ 您只需指示 HTMX 從何處獲取對話框以及將其放置在何處,一切就完成了! ``` <!-- the delete button --> @if ($chirp->user->is(auth()->user())) <form> @csrf @method('delete') <x-dropdown-link :component="'button'" type="submit" hx-get="{{ route('chirps.confirm-destroy', $chirp) }}" hx-swap="beforeend" hx-target="closest .chirp" > {{ __('Delete') }} </x-dropdown-link> </form> @endif <!-- the dialog template --> <div class="modal fixed z-10 inset-0 overflow-y-auto flex justify-center items-center bg-black bg-opacity-50" style="backdrop-filter: blur(14px);"> <div class="bg-white rounded p-6"> <h2 class="text-xl border-b pb-2 mb-2">Confirm Action</h2> <p>Are you sure you want to delete this chirp?</p> <div class="flex justify-end mt-4 gap-4"> <x-secondary-button _="on click remove closest .modal" > Cancel </x-secondary-button> <form> @csrf <x-danger-button hx-delete="{{route('chirps.destroy', $chirp)}}" hx-target="closest .chirp" hx-swap="delete"> Delete </x-danger-button> </form> </div> </div> </div> ``` _這個範例來自我的 [HTMX with Laravel] 教學(https://dev.to/turculaurentiu91/laravel-htmx--g0n) ,請看!_ 就像這樣,當我們單擊刪除按鈕時,我們指示 HTMX 對 `chirps/{chirp}/confirm-destroy` 執行 **GET** 請求,並將結果 HTML 放在最接近的父級 `< 之前div class="chirp">` 結束(在底部)。在刪除對話方塊中,當使用者確認時,我們指示 HTMX 對 `chirps/{chirp}` 端點執行 **DELETE** 請求,成功後,我們刪除具有 `chirp` 類別的最接近的父級。 ## 結論 在不斷發展的 Web 開發領域,看到 HTMX 等倡導簡單性和回歸基礎的工具令人耳目一新。透過利用 HTML 和 HTTP 的強大功能,HTMX 允許開發人員建立動態 Web 應用程式,而無需傳統 JavaScript 框架的複雜性和開銷。 因此,下次您開始新專案或考慮重構現有專案時,請嘗試 HTMX。您可能會驚訝於用這麼少的錢就能取得如此大的成就。

44 個 React 前端面試問題

原文出處:https://dev.to/m_midas/44-react-frontend-interview-questions-2o63 ## 介紹 在面試 React 前端開發人員職位時,為技術問題做好充分準備至關重要。 React 已經成為建立使用者介面最受歡迎的 JavaScript 程式庫之一,雇主通常專注於評估候選人對 React 核心概念、最佳實踐和相關技術的理解。在本文中,我們將探討 React 前端開發人員面試期間常見問題的完整清單。透過熟悉這些問題及其答案,您可以增加成功的機會並展示您在 React 開發方面的熟練程度。因此,讓我們深入探討您應該準備好在 React 前端開發人員面試中解決的關鍵主題。 ![](https://media.giphy.com/media/AYECTMLNS4o67dCoeY/giphy.gif) ### 1.你知道哪些React hooks? - `useState`:用於管理功能元件中的狀態。 - `useEffect`:用於在功能元件中執行副作用,例如取得資料或訂閱事件。 - `useContext`:用於存取功能元件內的 React 上下文的值。 - `useRef`:用於建立對跨渲染持續存在的元素或值的可變引用。 - `useCallback`:用於記憶函數以防止不必要的重新渲染。 - `useMemo`:用於記憶值,透過快取昂貴的計算來提高效能。 - `useReducer`:用於使用reducer函數管理狀態,類似於Redux的工作方式。 - `useLayoutEffect`:與 useEffect 類似,但效果在所有 DOM 變更後同步運作。 這些鉤子提供了強大的工具來管理狀態、處理副作用以及重複使用 React 功能元件中的邏輯。 [了解更多](https://react.dev/reference/react) ### 2.什麼是虛擬 DOM? 虛擬 DOM 是 React 中的一個概念,其中建立實際 DOM(文件物件模型)的輕量級虛擬表示並將其儲存在記憶體中。它是一種用於優化 Web 應用程式效能的程式設計技術。 當 React 元件的資料或狀態發生變更時,虛擬 DOM 會被更新,而不是直接操作真實 DOM。然後,虛擬 DOM 計算元件的先前狀態和更新狀態之間的差異,稱為「比較」過程。 一旦辨識出差異,React 就會有效地僅更新真實 DOM 的必要部分以反映變更。這種方法最大限度地減少了實際 DOM 操作的數量,並提高了應用程式的整體效能。 透過使用虛擬 DOM,React 提供了一種建立動態和互動式使用者介面的方法,同時確保最佳效率和渲染速度。 ### 3. 如何渲染元素陣列? 要渲染元素陣列,可以使用“map()”方法迭代該陣列並傳回一個新的 React 元素陣列。 ``` const languages = [ "JavaScript", "TypeScript", "Python", ]; function App() { return ( <div> <ul>{languages.map((language) => <li>{language}</li>)}</ul> </div> ); } ``` [了解更多](https://react.dev/learn/rendering-lists) ### 4. 受控元件和非受控元件有什麼不同? 受控元件和非受控元件之間的區別在於**它們如何管理和更新其狀態**。 受控元件是狀態由 React 控制的元件。元件接收其當前值並透過 props 更新它。當值改變時它也會觸發回調函數。這意味著該元件不儲存其自己的內部狀態。相反,父元件管理該值並將其傳遞給受控元件。 ``` import { useState } from 'react'; function App() { const [value, setValue] = useState(''); return ( <div> <h3>Controlled Component</h3> <input name="name" value={name} onChange={(e) => setValue(e.target.value)} /> <button onClick={() => console.log(value)}>Get Value</button> </div> ); } ``` 另一方面,不受控制的元件使用 refs 或其他方法在內部管理自己的狀態。它們獨立儲存和更新狀態,不依賴 props 或回呼。父元件對不受控元件的狀態控制較少。 ``` import { useRef } from 'react'; function App() { const inputRef = useRef(null); return ( <div className="App"> <h3>Uncontrolled Component</h3> <input type="text" name="name" ref={inputRef} /> <button onClick={() => console.log(inputRef.current.value)}>Get Value</button> </div> ); } ``` [了解更多](https://react.dev/learn/sharing-state- Between-components#driven-and-uncontrol-components) ### 5. 基於類別的 React 元件和函數式 React 元件有什麼不同? 基於類別的元件和函數式元件之間的主要區別在於**它們的定義方式以及它們使用的語法。** 基於類別的元件被定義為 ES6 類別並擴展了 `React.Component` 類別。他們使用「render」方法傳回定義元件輸出的 JSX (JavaScript XML)。類別元件可以透過「this.state」和「this.setState()」存取元件生命週期方法和狀態管理。 ``` class App extends React.Component { state = { value: 0, }; handleAgeChange = () => { this.setState({ value: this.state.value + 1 }); }; render() { return ( <> <p>Value is {this.state.value}</p> <button onClick={this.handleAgeChange}> Increment value </button> </> ); } } ``` 另一方面,函數元件被定義為簡單的 JavaScript 函數。他們接受 props 作為參數並直接返回 JSX。功能元件無權存取生命週期方法或狀態。然而,隨著 React 16.8 中 React Hooks 的引入,功能元件現在可以管理狀態並使用其他功能,例如上下文和效果。 ``` import { useState } from 'react'; const App = () => { const [value, setValue] = useState(0); const handleAgeChange = () => { setValue(value + 1); }; return ( <> <p>Value is {value}</p> <button onClick={handleAgeChange}> Increment value </button> </> ); } ``` 一般來說,功能元件被認為更簡單、更容易閱讀和測試。建議盡可能使用函數式元件,除非有特定需要基於類別的元件。 ### 6. 元件的生命週期方法有哪些? 生命週期方法是一種掛鉤元件生命週期不同階段的方法,可讓您在特定時間執行特定程式碼。 以下是主要生命週期方法的清單: 1. `constructor`:這是建立元件時呼叫的第一個方法。它用於初始化狀態和綁定事件處理程序。在功能元件中,您可以使用“useState”鉤子來實現類似的目的。 2. `render`:此方法負責渲染 JSX 標記並傳回螢幕上要顯示的內容。 3. `componentDidMount`:元件在 DOM 中渲染後立即呼叫該方法。它通常用於初始化任務,例如 API 呼叫或設定事件偵聽器。 4. `componentDidUpdate`:當元件的 props 或 state 改變時呼叫該方法。它允許您執行副作用、根據更改更新元件或觸發其他 API 呼叫。 5. `componentWillUnmount`:在元件從 DOM 刪除之前呼叫此方法。它用於清理在`componentDidMount`中設定的任何資源,例如刪除事件偵聽器或取消計時器。 一些生命週期方法,例如“componentWillMount”、“componentWillReceiveProps”和“componentWillUpdate”,已被棄用或替換為替代方法或掛鉤。 至於“this”,它指的是類別元件的當前實例。它允許您存取元件內的屬性和方法。在函數式元件中,不使用“this”,因為函數未綁定到特定實例。 ### 7. 使用 useState 有什麼特色? `useState` 傳回一個狀態值和一個更新它的函數。 ``` const [value, setValue] = useState('Some state'); ``` 在初始渲染期間,傳回的狀態與作為第一個參數傳遞的值相符。 `setState` 函數用於更新狀態。它採用新的狀態值作為參數,並**對元件的重新渲染進行排隊**。 `setState` 函數也可以接受回呼函數作為參數,該函數將先前的狀態值作為參數。 [了解更多](https://react.dev/reference/react/useState) ### 8. 使用 useEffect 有什麼特別之處? `useEffect` 鉤子可讓您在功能元件中執行副作用。 稱為 React 渲染階段的功能元件的主體內部不允許突變、訂閱、計時器、日誌記錄和其他副作用。這可能會導致用戶介面中出現令人困惑的錯誤和不一致。 相反,建議使用 useEffect。傳遞給 useEffect 的函數將在渲染提交到螢幕後執行,或者如果您傳遞一組依賴項作為第二個參數,則每次依賴項之一發生變更時都會呼叫該函數。 ``` useEffect(() => { console.log('Logging something'); }, []) ``` [了解更多](https://react.dev/reference/react/useEffect) ### 9. 如何追蹤功能元件的卸載? 通常,「useEffect」會建立在元件離開畫面之前需要清理或重設的資源,例如訂閱或計時器辨識碼。 為了做到這一點,傳遞給`useEffect`的函數可以傳回一個**清理函數**。清理函數在元件從使用者介面刪除之前執行,以防止記憶體洩漏。此外,如果元件渲染多次(通常是這種情況),則在執行下一個效果之前會清除上一個效果。 ``` useEffect(() => { function handleChange(value) { setValue(value); } SomeAPI.doFunction(id, handleChange); return function cleanup() { SomeAPI.undoFunction(id, handleChange); }; }) ``` ### 10. React 中的 props 是什麼? Props 是從父元件傳遞給元件的資料。道具 是唯讀的,無法更改。 ``` // Parent component const Parent = () => { const data = "Hello, World!"; return ( <div> <Child data={data} /> </div> ); }; // Child component const Child = ({ data }) => { return <div>{data}</div>; }; ``` [了解更多](https://react.dev/learn/passing-props-to-a-component) ### 11. 什麼是狀態管理器?您曾與哪些狀態管理器合作過或認識哪些狀態管理器? 狀態管理器是幫助管理應用程式狀態的工具或程式庫。它提供了一個集中式儲存或容器來儲存和管理可由應用程式中的不同元件存取和更新的資料。 狀態管理器可以解決幾個問題。首先,將資料和與其相關的邏輯與元件分開是一個很好的做法。其次,當使用本機狀態並在元件之間傳遞它時,由於元件可能存在深層嵌套,程式碼可能會變得複雜。透過擁有全域存儲,我們可以存取和修改來自任何元件的資料。 除了 React Context,Redux 或 MobX 通常用作狀態管理庫。 [了解更多](https://mobx.js.org/README.html) [了解更多](https://redux-toolkit.js.org/) ### 12. 在什麼情況下可以使用本地狀態,什麼時候應該使用全域狀態? 如果本機狀態僅在一個元件中使用且不打算將其傳遞給其他元件,則建議使用本機狀態。本地狀態也用在表示清單中單一專案的元件中。但是,如果元件分解涉及嵌套元件且資料沿層次結構傳遞,則最好使用全域狀態。 ### 13. Redux中的reducer是什麼,它需要哪些參數? 減速器是一個純函數,它將狀態和操作作為參數。在減速器內部,我們追蹤接收到的操作的類型,並根據它修改狀態並傳回一個新的狀態物件。 ``` export default function appReducer(state = initialState, action) { // The reducer normally looks at the action type field to decide what happens switch (action.type) { // Do something here based on the different types of actions default: // If this reducer doesn't recognize the action type, or doesn't // care about this specific action, return the existing state unchanged return state } } ``` [了解更多](https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers) ### 14. 什麼是操作以及如何更改 Redux 中的狀態? Action 是一個簡單的 JavaScript 物件,必須有一個字段 一種。 ``` { type: "SOME_TYPE" } ``` 您也可以選擇新增一些資料作為**有效負載**。為了 改變狀態,需要呼叫我們傳遞給它的調度函數 行動 ``` { type: "SOME_TYPE", payload: "Any payload", } ``` [了解更多](https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers) ### 15. Redux 實作了哪一種模式? Redux 實作了 **Flux 模式**,這是應用程式可預測的狀態管理模式。它透過引入單向資料流和應用程式狀態的集中儲存來幫助管理應用程式的狀態。 [了解更多](https://www.newline.co/fullstack-react/30-days-of-react/day-18/#:~:text=Flux%20is%20a%20pattern%20for,default% 20method %20用於%20處理%20資料。) ### 16. Mobx 實作哪一種模式? Mobx 實作了**觀察者模式**,也稱為發布-訂閱模式。 [了解更多](https://www.patterns.dev/posts/observer-pattern) ### 17. 使用 Mobx 的特徵是什麼? Mobx 提供了「observable」和「compulated」等裝飾器來定義可觀察狀態和反應函數。以action修飾的動作用於修改狀態,確保追蹤所有變更。 Mobx 還提供自動依賴追蹤、不同類型的反應、對反應性的細粒度控制,以及透過 mobx-react 套件與 React 無縫整合。總體而言,Mobx 透過根據可觀察狀態的變化自動執行更新過程來簡化狀態管理。 ### 18.如何存取Mobx狀態下的變數? 您可以透過使用「observable」裝飾器將變數定義為可觀察來存取狀態中的變數。這是一個例子: ``` import { observable, computed } from 'mobx'; class MyStore { @observable myVariable = 'Hello Mobx'; @computed get capitalizedVariable() { return this.myVariable.toUpperCase(); } } const store = new MyStore(); console.log(store.capitalizedVariable); // Output: HELLO MOBX store.myVariable = 'Hi Mobx'; console.log(store.capitalizedVariable); // Output: HI MOBX ``` 在此範例中,使用“observable”裝飾器將“myVariable”定義為可觀察物件。然後,您可以使用“store.myVariable”存取該變數。對「myVariable」所做的任何變更都會自動觸發相關元件或反應的更新。 [了解更多](https://mobx.js.org/actions.html) ### 19.Redux 和 Mobx 有什麼差別? Redux 是一個更簡單、更固執己見的狀態管理庫,遵循嚴格的單向資料流並促進不變性。它需要更多的樣板程式碼和顯式更新,但與 React 具有出色的整合。 另一方面,Mobx 提供了更靈活、更直觀的 API,且樣板程式碼更少。它允許您直接修改狀態並自動追蹤更改以獲得更好的性能。 Redux 和 Mobx 之間的選擇取決於您的特定需求和偏好。 ### 20.什麼是 JSX? 預設情況下,以下語法用於在 React 中建立元素。 ``` const someElement = React.createElement( 'h3', {className: 'title__value'}, 'Some Title Value' ); ``` 但我們已經習慣這樣看 ``` const someElement = ( <h3 className='title__value'>Some Title Value</h3> ); ``` 這正是標記所謂的 jsx。這是一種語言的擴展 簡化對程式碼和開發的認知 [了解更多](https://react.dev/learn/writing-markup-with-jsx#jsx-putting-markup-into-javascript) ### 21.什麼是道具鑽探? Props 鑽取是指透過多層嵌套元件傳遞 props 的過程,即使某些中間元件不直接使用這些 props。這可能會導致程式碼結構複雜且繁瑣。 ``` // Parent component const Parent = () => { const data = "Hello, World!"; return ( <div> <ChildA data={data} /> </div> ); }; // Intermediate ChildA component const ChildA = ({ data }) => { return ( <div> <ChildB data={data} /> </div> ); }; // Leaf ChildB component const ChildB = ({ data }) => { return <div>{data}</div>; }; ``` 在此範例中,「data」屬性從 Parent 元件傳遞到 ChildA,然後從 ChildA 傳遞到 ChildB,即使 ChildA 不會直接使用該屬性。當存在許多層級的嵌套或當元件樹中更靠下的元件需要存取資料時,這可能會成為問題。它會使程式碼更難維護和理解。 可以透過使用其他模式(如上下文或狀態管理庫(如 Redux 或 MobX))來緩解 Props 鑽探。這些方法允許元件存取資料,而不需要透過每個中間元件傳遞 props。 ### 22. 如何有條件地渲染元素? 您可以使用任何條件運算符,包括三元。 ``` return ( <div> {isVisible && <span>I'm visible!</span>} </div> ); ``` ``` return ( <div> {isOnline ? <span>I'm online!</span> : <span>I'm offline</span>} </div> ); ``` ``` if (isOnline) { element = <span>I'm online!</span>; } else { element = <span>I'm offline</span>; } return ( <div> {element} </div> ); ``` [了解更多](https://react.dev/learn/conditional-rendering) ### 23. useMemo 的用途是什麼?它是如何運作的? `useMemo` 用於緩存和記憶 計算結果。 傳遞建立函數和依賴項陣列。只有當任何依賴項的值發生變更時,`useMemo` 才會重新計算記憶值。此優化有助於避免 每次渲染都需要昂貴的計算。 使用第一個參數,函數接受執行計算的回調,使用第二個依賴項陣列,僅當至少一個依賴項發生變更時,該函數才會重新執行計算。 ``` const memoValue = useMemo(() => computeFunc(paramA, paramB), [paramA, paramB]); ``` [了解更多](https://react.dev/reference/react/useMemo) ### 24. useCallback 的用途是什麼?它是如何運作的? `useCallback` 掛鉤將傳回回呼的記憶版本,僅當依賴項之一的值發生變更時,該版本才會變更。 當將回調傳遞給依賴連結相等性來防止不必要的渲染的最佳化子元件時,這非常有用。 ``` const callbackValue = useCallback(() => computeFunc(paramA, paramB), [paramA, paramB]); ``` [了解更多](https://react.dev/reference/react/useCallback) ### 25. useMemo 和 useCallback 有什麼不同? 1. `useMemo` 用於儲存計算結果,而 `useCallback` 用於儲存函數本身。 2. `useMemo` 快取計算值,如果依賴項沒有改變,則在後續渲染時傳回它。 3. `useCallback` 快取函數本身並傳回相同的實例,除非相依性發生變更。 ### 26.什麼是 React Context? React Context 是一項功能,它提供了一種透過元件樹傳遞資料的方法,而無需在每個層級手動傳遞 props。它允許您建立一個全域狀態,樹中的任何元件都可以存取該狀態,無論其位置如何。當您需要在未透過 props 直接連接的多個元件之間共用資料時,上下文就非常有用。 React Context API 由三個主要部分組成: 1. `createContext`:此函數用於建立一個新的上下文物件。 2. `Context.Provider`:該元件用於向上下文提供值。它包裝了需要存取該值的元件。 3. `Context.Consumer` 或 `useContext` 鉤子:此元件或鉤子用於使用上下文中的值。它可以在上下文提供者內的任何元件中使用。 透過使用 React Context,您可以避免道具鑽探(透過多個層級的元件傳遞道具)並輕鬆管理更高層級的狀態,使您的程式碼更有組織性和效率。 [了解更多](https://react.dev/learn/passing-data-deeply-with-context) ### 27. useContext 的用途是什麼?它是如何運作的? 在典型的 React 應用程式中,資料使用 props 從上到下(從父元件到子元件)傳遞。但這樣的使用方法對於某些類型的道具來說可能過於繁瑣 (例如,所選語言、UI 主題),必須傳遞給應用程式中的許多元件。上下文提供了一種在元件之間共享此類資料的方法,而無需明確傳遞 props 樹的每一層。 呼叫 useContext 的元件將始終在以下情況下重新渲染: 上下文值發生變化。如果重新渲染元件的成本很高,您可以使用記憶來優化它。 ``` const App = () => { const theme = useContext(ThemeContext); return ( <div style={{ color: theme.palette.primary.main }}> Some div </div> ); } ``` [了解更多](https://react.dev/reference/react/useContext) ### 28. useRef 的用途是什麼?它是如何運作的? `useRef` 傳回一個可修改的 ref 物件,一個屬性。其中的當前值由傳遞的參數初始化。傳回的物件將在元件的整個生命週期內持續存在,並且不會因渲染而改變。 通常的用例是以命令式存取後代 風格。 IE。使用 ref,我們可以明確引用 DOM 元素。 ``` const App = () => { const inputRef = useRef(null); const buttonClick = () => { inputRef.current.focus(); } return ( <> <input ref={inputRef} type="text" /> <button onClick={buttonClick}>Focus on input tag</button> </> ) } ``` [了解更多](https://react.dev/reference/react/useRef) ### 29. 什麼是 React.memo()? `React.memo()` 是一個高階元件。如果您的元件始終使用不變的 props 渲染相同的內容,您可以將其包裝在「React.memo()」呼叫中,以在某些情況下提高效能,從而記住結果。這意味著 React 將使用上次渲染的結果,避免重新渲染。 `React.memo()` 只影響 props 的變更。如果一個功能元件被包裝在 React.memo 中並使用 useState、useReducer 或 useContext,那麼當狀態或上下文發生變化時,它將重新渲染。 ``` import { memo } from 'react'; const MemoComponent = memo(MemoComponent = (props) => { // ... }); ``` [了解更多](https://react.dev/reference/react/memo) ### 30.React Fragment是什麼? 從元件傳回多個元素是 React 中的常見做法。片段可讓您形成子元素列表,而無需在 DOM 中建立不必要的節點。 ``` <> <OneChild /> <AnotherChild /> </> // or <React.Fragment> <OneChild /> <AnotherChild /> </React.Fragment> ``` [了解更多](https://react.dev/reference/react/Fragment) ### 31.什麼是 React Reconciliation? 協調是一種 React 演算法,用於區分一棵元素樹與另一棵元素樹,以確定需要替換的部分。 協調是我們過去所說的虛擬 DOM 背後的演算法。這個定義聽起來是這樣的:當你渲染一個 React 應用程式時,描述該應用程式的元素樹是在保留的記憶體中產生的。然後這棵樹被包含在渲染環境中——例如,瀏覽器應用程式,它被翻譯成一組 DOM 操作。當應用程式狀態更新時,會產生一棵新樹。將新樹與前一棵樹進行比較,以便準確計算並啟用重繪更新的應用程式所需的操作。 [了解更多](https://react.dev/learn/preserving-and-resetting-state) ### 32.為什麼使用map()時需要列表中的鍵? 這些鍵可幫助 React 確定哪些元素已更改, 新增或刪除。必須指定它們以便 React 可以匹配 隨著時間的推移陣列元素。選擇鍵的最佳方法是使用能夠清楚區分清單專案與其鄰居的字串。大多數情況下,您將使用資料中的 ID 作為金鑰。 ``` const languages = [ { id: 1, lang: "JavaScript", }, { id: 2, lang: "TypeScript", }, { id: 3, lang: "Python", }, ]; const App = () => { return ( <div> <ul>{languages.map((language) => ( <li key={`${language.id}_${language.lang}`}>{language.lang}</li> ))} </ul> </div> ); } ``` [了解更多](https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key) ### 33. 如何在 Redux Thunk 中處理非同步操作? 要使用 Redux Thunk,您需要將其作為中間件導入。動作建立者不僅應該傳回一個物件,還應該傳回以調度為參數的函數。 ``` export const addUser = ({ firstName, lastName }) => { return dispatch => { dispatch(addUserStart()); } axios.post('https://jsonplaceholder.typicode.com/users', { firstName, lastName, completed: false }) .then(res => { dispatch(addUserSuccess(res.data)); }) .catch(error => { dispatch(addUserError(error.message)); }) } ``` [了解更多](https://redux.js.org/usage/writing-logic-thunks) ### 34.如何追蹤功能元件中物件欄位的變化? 為此,您需要使用“useEffect”掛鉤並將物件的欄位作為依賴項陣列傳遞。 ``` useEffect(() => { console.log('Changed!') }, [obj.someField]) ``` ### 35.如何存取DOM元素? 引用是使用 React.createRef() 或 useRef() 鉤子建立的,並透過 ref 屬性附加到 React 元素。透過存取已建立的引用,我們可以使用「ref.current」來存取 DOM 元素。 ``` const App = () => { const myRef = useRef(null); const handleClick = () => { console.log(myRef.current); // Accessing the DOM element }; return ( <div> <input type="text" ref={myRef} /> <button onClick={handleClick}>Click Me</button> </div> ); } export default App; ``` ### 36.什麼是自訂鉤子? 自訂鉤子是一個允許您在不同元件之間重複使用邏輯的功能。它是一種封裝可重複使用邏輯的方法,以便可以在多個元件之間輕鬆共用和重複使用。自訂掛鉤是通常以 **use ** 開頭的函數,並且可以根據需要呼叫其他掛鉤。 [了解更多](https://react.dev/learn/reusing-logic-with-custom-hooks) ### 37.什麼是公共API? 在索引檔案的上下文中,公共 API 通常是指向外部模組或元件公開並可存取的介面或函數。 以下是表示公共 API 的索引檔案的程式碼範例: ``` // index.js export function greet(name) { return `Hello, ${name}!`; } export function calculateSum(a, b) { return a + b; } ``` 在此範例中,index.js 檔案充當公共 API,其中導出函數“greet()”和“calculateSum()”,並且可以透過匯入它們從其他模組存取它們。其他模組可以導入並使用這些函數作為其實現的一部分: ``` // main.js import { greet, calculateSum } from './index.js'; console.log(greet('John')); // Hello, John! console.log(calculateSum(5, 3)); // 8 ``` 透過從索引檔案匯出特定函數,我們定義了模組的公共 API,允許其他模組使用這些函數。 ### 38. 建立自訂鉤子的規則是什麼? 1. 鉤子名稱以「use」開頭。 2. 如果需要,使用現有的鉤子。 3. 不要有條件地呼叫鉤子。 4. 將可重複使用邏輯提取到自訂掛鉤中。 5. 自訂hook必須是純函數。 6. 自訂鉤子可以傳回值或其他鉤子。 7. 描述性地命名自訂掛鉤。 [了解更多](https://react.dev/learn/reusing-logic-with-custom-hooks) ### 39.什麼是SSR(伺服器端渲染)? 伺服器端渲染(SSR)是一種用於在伺服器上渲染頁面並將完整渲染的頁面傳送到客戶端進行顯示的技術。它允許伺服器產生網頁的完整 HTML 標記(包括其動態內容),並將其作為對請求的回應傳送到客戶端。 在傳統的用戶端渲染方法中,用戶端接收最小的 HTML 頁面,然後向伺服器發出額外的資料和資源請求,這些資料和資源用於在客戶端渲染頁面。這可能會導致初始頁面載入時間變慢,並對搜尋引擎優化 (SEO) 產生負面影響,因為搜尋引擎爬蟲很難對 JavaScript 驅動的內容建立索引。 透過 SSR,伺服器透過執行必要的 JavaScript 程式碼來產生最終的 HTML 來負責渲染網頁。這意味著客戶端從伺服器接收完全呈現的頁面,從而減少了額外資源請求的需要。 SSR 縮短了初始頁面載入時間,並允許搜尋引擎輕鬆索引內容,從而實現更好的 SEO。 SSR 通常用於框架和函式庫中,例如用於 React 的 Next.js 和用於 Vue.js 的 Nuxt.js,以啟用伺服器端渲染功能。這些框架為您處理伺服器端渲染邏輯,讓實作 SSR 變得更加容易。 ### 40.使用SSR有什麼好處? 1. **改進初始載入時間**:SSR 允許伺服器將完全渲染的 HTML 頁面傳送到客戶端,從而減少客戶端所需的處理量。這可以縮短初始載入時間,因為使用者可以更快地看到完整的頁面。 2. **SEO友善**:搜尋引擎可以有效地抓取和索引SSR頁面的內容,因為完全渲染的HTML在初始回應中可用。這提高了搜尋引擎的可見度並有助於更好的搜尋排名。 3. **可存取性**:SSR 確保禁用 JavaScript 或使用輔助技術的使用者可以存取內容。透過在伺服器上產生 HTML,SSR 為所有使用者提供可靠且易於存取的使用者體驗。 4. **低頻寬環境下的效能**:SSR減少了客戶端需要下載的資料量,有利於低頻寬或高延遲環境下的使用者。這對於行動用戶或網路連線速度較慢的用戶尤其重要。 雖然 SSR 提供了這些優勢,但值得注意的是,與客戶端渲染方法相比,它可能會帶來更多的伺服器負載和維護複雜性。應仔細考慮快取、可擴展性和伺服器端渲染效能最佳化等因素。 ### 41.你知道Next.js的主要功能有哪些? 1. `getStaticProps`:此方法用於在建置時取得資料並將頁面預先渲染為靜態 HTML。它確保資料在建置時可用,並且不會因後續請求而更改。 ``` export async function getStaticProps() { const res = await fetch('https://api.example.com/data'); const data = await res.json(); return { props: { data } }; } ``` 2. `getServerSideProps`:此方法用於在每個請求上取得資料並在伺服器上預先渲染頁面。當您需要取得可能經常變更或特定於使用者的資料時,可以使用它。 ``` export async function getServerSideProps() { const res = await fetch('https://api.example.com/data'); const data = await res.json(); return { props: { data } }; } ``` 3. `getStaticPaths`:此方法在動態路由中使用,用於指定建置時應預先渲染的路徑清單。它通常用於獲取帶有參數的動態路由的資料。 ``` export async function getStaticPaths() { const res = await fetch('https://api.example.com/posts'); const posts = await res.json(); const paths = posts.map((post) => ({ params: { id: post.id } })); return { paths, fallback: false }; } ``` [了解更多](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating) ### 42.什麼是 Linters? Linters 是用來檢查原始程式碼是否有潛在錯誤、錯誤、風格不一致和可維護性問題的工具。它們可幫助執行編碼標準並確保整個程式碼庫的程式碼品質和一致性。 Linters 的工作原理是掃描原始程式碼並將其與一組預先定義的規則或指南進行比較。這些規則可以包括語法和格式約定、最佳實踐、潛在錯誤和程式碼異味。當 linter 發現違反規則時,它會產生警告或錯誤,突出顯示需要注意的特定行或多行程式碼。 使用 linter 可以帶來幾個好處: 1. **程式碼品質**:Linter 有助於辨識和防止潛在的錯誤、程式碼異味和反模式,從而提高程式碼品質。 2. **一致性**:Linter 強制執行編碼約定和風格指南,確保整個程式碼庫的格式和程式碼結構一致,即使多個開發人員正在處理同一個專案時也是如此。 3. **可維護性**:透過儘早發現問題並促進良好的編碼實踐,linter 有助於程式碼的可維護性,使程式碼庫更容易理解、修改和擴展。 4. **效率**:Linter 可以透過自動化程式碼審查流程並在常見錯誤在開發或生產過程中引起問題之前捕獲它們來節省開發人員的時間。 一些流行的 linter 包括用於 JavaScript 的 ESLint 以及用於 CSS 和 Sass 的 Stylelint。 [了解更多](https://eslint.org/docs/latest/use/getting-started) ### 43.你知道哪些 React 架構解決方案? 有多種用於建立 React 專案的架構解決方案和模式。一些受歡迎的包括: 1. **MVC(模型-視圖-控制器)**:MVC 是一種傳統的架構模式,它將應用程式分為三個主要元件 - 模型、視圖和控制器。 React 可以在 View 層中使用來渲染 UI,而其他程式庫或框架可以用於 Model 和 Controller 層。 2. **Flux**:Flux是Facebook專門針對React應用程式所推出的應用架構。它遵循單向資料流,其中資料沿著單一方向流動,從而更容易理解和除錯應用程式的狀態變更。 3. **原子設計**:原子設計並不是React特有的,而是將UI分割成更小、可重複使用元件的設計方法。它鼓勵建立小型、獨立且可以組合以建立更複雜的 UI 的元件。 4. **容器和元件模式**:此模式將表示(元件)與邏輯和狀態管理(容器)分開。元件負責渲染 UI,而容器則處理業務邏輯和狀態管理。 5. **功能切片設計**:它是一種用於組織和建構 React 應用程式的現代架構方法。它旨在透過根據功能或模組劃分應用程式程式碼庫來解決可擴展性、可維護性和可重用性的挑戰。 ### 44.什麼是特徵切片設計? 它是一種用於組織和建立 React 應用程式的現代架構方法。它旨在透過根據功能或模組劃分應用程式程式碼庫來解決可擴展性、可維護性和可重用性的挑戰。 在功能切片設計中,應用程式的每個功能或模組都組織到一個單獨的目錄中,其中包含所有必要的元件、操作、reducers 和其他相關檔案。這有助於保持程式碼庫的模組化和隔離性,使其更易於開發、測試和維護。 功能切片設計促進了關注點的清晰分離,並將功能封裝在各個功能中。這允許不同的團隊或開發人員獨立地處理不同的功能,而不必擔心衝突或依賴性。 ![功能切片設計](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/amysbtftfjkuss87yu8v.png) **我強烈建議點擊“了解更多”按鈕以了解功能切片設計** [了解更多](https://dev.to/m_midas/feature-sliced-design-the-best-frontend-architecture-4noj) ## 了解更多 如果您還沒有閱讀過,我強烈建議您閱讀我關於前端面試問題的其餘文章。 https://dev.to/m_midas/52-frontend-interview-questions-javascript-59h6 https://dev.to/m_midas/41-frontend-interview-questions-css-4imc https://dev.to/m_midas/15-most-common-frontend-interview-questions-4njp ## 結論 總之,面試 React 前端開發人員職位需要對框架的核心概念、原理和相關技術有深入的了解。透過準備本文中討論的問題,您可以展示您的 React 知識並展示您建立高效且可維護的使用者介面的能力。請記住,不僅要專注於記住答案,還要理解基本概念並能夠清楚地解釋它們。 此外,請記住,面試不僅涉及技術方面,還旨在展示您解決問題的能力、溝通能力以及團隊合作能力。透過將技術專業知識與強大的整體技能相結合,您將具備在 React 前端開發人員面試中脫穎而出的能力,並在這個令人興奮且快速發展的領域找到您夢想的工作。 祝你好運!

JS教學-文章縮圖預覽,夢幻連動的腳本教學!

## 前情提要 常常寫文章的時候都會運用到圖文並茂, 如果可以設定文章的圖片封面就是一個很棒的設計。 今天的JS腳本教學已經夢幻連動! 原本IT幫的文章與codeLove像是平行宇宙,只有偶爾轉載,現在, 教學內容直接來針對codelove來寫一個小小有趣小功能XD ## 目標設計 今天以codelove這個論壇為例, 讓我們來手動創造一個小腳本:分析文章內容,以正則去匹配圖片網址, 然後透過create的API寫出div,並且監聽事件, 當點擊圖片的時候,把圖片網址放到封面圖片的input欄位。 ## 效果 會根據textArea change的時候來匹配出網址。 並且創建div與img元素去放入網頁。 ![](https://i.imgur.com/sHcMDU8.png) 點圖片後會把圖片網址丟入。 ![](https://i.imgur.com/MWhkOB7.png) ## 程式碼內容 ``` // 獲取textarea元素 var textarea = document.querySelector("body > div.container.px-0 > div > div.col-12.col-lg-9 > div > div > form > textarea"); var featuredImageInput = document.querySelector('input[name="featured_image"]'); textarea.addEventListener('change', function(event) { // 檢查是否存在imgBox元素 let imgBoxCheck = document.querySelector('.imgBox'); if (imgBoxCheck) { // 如果存在,則刪除imgBox元素 imgBoxCheck.parentNode.removeChild(imgBoxCheck); } // 獲取文本框的內容 var ContentText = textarea.value; // 創建一個空數組來存儲圖片鏈接 var imgArray = []; // 使用正則表達式查找圖片鏈接 var urlRegex = /https?:\/\/[^\s/$.?#].[^\s]*/gi; // 匹配以http或https開頭的鏈接 var matches = ContentText.match(urlRegex); if (matches) { // 疊代所有匹配項並篩選出以.png或.jpg結尾的鏈接 for (var i = 0; i < matches.length; i++) { if (matches[i].match(/\.(png|jpg)$/i)) { imgArray.push(matches[i]); } } } // 使用正則表達式匹配鏈接部分 var regex = /!\[\]\(([^)]+)\)/g; var match; while ((match = regex.exec(ContentText)) !== null) { imgArray.push(match[1]); } // 創建一個包含圖片的容器div(imgBox)並設置為display: flex let imgBox = document.createElement('div'); imgBox.className = 'imgBox'; // 添加類名 imgBox.style.display = 'flex'; // 平均分配圖片大小 var imageSize = 100 / imgArray.length; // 假設100%寬度,根據圖片數量計算每張圖片的寬度 // 將圖片鏈接轉換為img元素並插入到imgBox imgArray.forEach(function (imgUrl) { var imgDiv = document.createElement('div'); var img = document.createElement('img'); img.src = imgUrl; img.className = 'imgCover'; img.addEventListener('click', function () { // 設置name="featured_image"的輸入框的值為被點擊圖片的src屬性 featuredImageInput.value = img.src; }); // 設置img的寬度為100% img.style.width = '100%'; img.style.height = '100%'; imgDiv.appendChild(img); imgBox.appendChild(imgDiv); }); // 將imgBox插入到textarea後面 h5.insertAdjacentElement('afterend', imgBox); textarea.style.overflow = 'auto'; imgBox.style.border="5px solid gray" imgBox.style.marginTop="5px" }); // 創建一個包含CSS規則的<style>元素 var style = document.createElement('style'); style.type = 'text/css'; style.textContent = '.imgBox:hover .imgCover:not(:hover) { filter: grayscale(100%); opacity: 0.75; }'; // 將<style>元素添加到<head>中 document.head.appendChild(style); h5= document.createElement('h5') h5.innerText='請選擇你的文章縮圖:' h5.style.marginTop="50px" h5.style.fontSize="16px" textarea.insertAdjacentElement('afterend', h5); ``` ## 觀念筆記 這次的腳本內容其實沒有太多新的知識點,算是一個很好的複習。 querySelector addEventListener insertAdjacentElement forEach 以及正則表達式的使用。 ## 心得後記 網頁上的功能,其實就是這些基礎的運用配合起來!(°ཀ°) 所以不用擔心前端沒有東西練習,其實點子四處都有,練習也都很實用。 而不是每次都在重複輪迴相同的東西、感到疲倦! 這次的前端小試身手就帶來一個回鍋菜,下次敬請期待更有趣的變化。 喜歡記得追蹤或留言唷。 還沒挑戰過此系列其他內容的,可以試著去挑戰看看◑ω◐

不是共產黨,但是審查低質量雜訊很重要!自己寫一個JS腳本過濾!

本文轉自:https://ithelp.ithome.com.tw/articles/10338948 ## 前情提要 資訊大爆炸。 有時候我們瀏覽技術文章,不一定真的是想學深奧的高級技術。 然而劣質低端的文章充斥著,則會降低我們學習的效率、甚至變成噪音與雜訊,干擾我們的思緒。 因此針對某些洗文或是質量很低的作者,我們必須列為思想犯, 否則會降低閱讀質量,平白浪費自己的時間、平台的版面、上網的電力、看到垃圾資訊的副作用等等.... ## 構思來源 如果有個思想審查警衛可以:**去除那些垃圾低端,稱不上技術文章的雜訊。** 以確保未來瀏覽文章的時候,不會再被洗文打擾, 也可以針對不喜歡的主題去封鎖,讓時間與精神更能專注於自己想要學的資訊。 阻止一些垃圾就是喜歡把自己尚未整理的白痴內容一直丟上來, 什麼都還不懂,把技術文章當成個人日記簿,寫一堆自我囈語、無病呻吟, 每天大量狂發文章,昭告天下以為這就是努力,欺騙自己也浪費別人的人生。 ## 「思想審查警衛」出動! ## 功能 1. 把頭像屏蔽 2. 加上思想通緝犯、紅字與刪除線 3. 新增封殺按鈕 4. 版面通知封殺名單與文章數 5. 透過ajax確認某id的最新ID ## 效果截圖 ![](https://i.imgur.com/kgUYJbl.png) 此截圖僅是腳本示範,跟其使用者無關, 本人沒有任何覺得此使用者發的文章是差勁的意味,我認為非常上進、值得學習。 ![](https://i.imgur.com/G27Ea2F.png) 此圖也只是隨機挑user使用,純粹作為範例用途,不代表我個人意見。 ## 腳本下載 https://greasyfork.org/zh-TW/scripts/477283-%E6%80%9D%E6%83%B3%E7%8A%AF%E5%B0%81%E6%AE%BA ## 原理說明 鄭重聲明,這個示範真的毫無任何私人意味,此腳本也只是針對不同的主題去隱藏, 例如我想學python就不想看到JS的文章,因此使用此工具幫忙隱藏罷了。 取名做思想警察、比喻成清除垃圾等都只是文學上的趣味。 請勿當真Ꮚ・ꈊ・Ꮚ 這次的技術可說比較難,認真難很多!但是趣味性以及功能性是無比的超越! 可以說幾乎是我寫系列文以來,最頂、最派的一篇! 不過很多觀念已經出現過,在【前端小試身手】系列裡面,每次的腳本都是主打實用, 因此cookie或localstorage這種技術當然都會運用到。 ## JS原始碼 ``` // ==UserScript== // @license MIT // @name 👮思想犯封殺 // @namespace http://tampermonkey.net/ // @version 0.1 // @description 把廢文製造機轟出去 // @author You // @match https://ithelp.ithome.com.tw/articles* // @match https://ithelp.ithome.com.tw/users/* // @icon https://www.google.com/s2/favicons?sz=64&domain=ithome.com.tw // @grant none // @run-at document-end // ==/UserScript== let URL = window.location.href; let ArticleSite = "https://ithelp.ithome.com.tw/articles?tab=tech" let UserSite = "https://ithelp.ithome.com.tw/users/" // 判斷URL的開頭部分 if (URL.startsWith(ArticleSite)) { //文章頁執行清理垃圾程序 CleanGarbage(); } else if (URL.startsWith(UserSite)) { UserCheck(); } else { console.log("這邊不執行腳本"); } // 餅乾儲存的機制函數------------------------------------------------- function setListInCookie(list) { document.cookie = 'myList=' + JSON.stringify(list) + '; expires=Wed, 31 Dec 2099 23:59:59 GMT;'; } // 從 Cookie 中獲取 list function getListFromCookie() { var cookieValue = document.cookie.replace(/(?:(?:^|.*;\s*)myList\s*\=\s*([^;]*).*$)|^.*$/, "$1"); return JSON.parse(cookieValue) || []; } function CleanGarbage(){ // 從本地存儲中獲取數據 var myListData = localStorage.getItem("myList"); // 解析數據到變量 list var list = myListData ? JSON.parse(myListData) : []; //list = getListFromCookie()||[]; // 儲存要刪除的字符串名單 // 找到所有CLASS是"qa-list__info-link"的<a>元素 var linkElements = document.querySelectorAll('.qa-list__info-link'); var removedCount = 0; // 初始化已清除的垃圾數量 for (var j = 0; j < list.length; j++) { // 遍歷這些<a>元素,確保文本內容包含"伍貳捌",然後刪除其父元素 for (var i = 0; i < linkElements.length; i++) { if (linkElements[i].textContent.includes(list[j])) { // 開始向上查找父元素 var parentElement = linkElements[i].parentElement; while (parentElement) { // 如果找到具有"classname"為"qa-list"的<div>元素,則刪除它 if (parentElement.classList.contains('qa-list')) { parentElement.remove(); removedCount++; // 增加清除的數量 console.warn('抓到"'+list[j]+'"這位思想犯'); break; // 找到並刪除後,結束循環 } parentElement = parentElement.parentElement; } } } if (removedCount>0){ // 顯示已清除的垃圾數量 console.log('已清除他的 ' + removedCount + ' 篇垃圾');} removedCount=0; } } //------------------------------------------------------- // 為了防止五百八改名,我們針對他的ID去ajax得到他最新的名稱 function FindBitch() { // 使用 Fetch API 獲取指定 URL 的內容 return fetch("https://ithelp.ithome.com.tw/users/20163468") .then(response => response.text()) .then(data => { // 創建一個臨時 div 元素以容納頁面內容 var tempDiv = document.createElement("div"); tempDiv.innerHTML = data; // 查找 class 為 "profile-header__name" 的元素 var profileNameElement = tempDiv.querySelector(".profile-header__name"); if (profileNameElement) { // 刪除元素內的所有 <span> 元素 var spanElements = profileNameElement.querySelectorAll("span"); spanElements.forEach(function(span) { span.remove(); }); // 讀取元素的文本內容,去掉前導和尾隨空格 var text = profileNameElement.textContent.trim(); // 返回處理後的文本內容 return text; } else { return "未找到元素"; } }) .catch(error => { console.error("發生錯誤: " + error); return "發生錯誤"; }); } //------------------------------------------------------- function UserCheck(){ //轉換資料從餅乾到localstorage var currentCookieValue = getCookie("myList"); // 2. 存儲數據到本地存儲 if (currentCookieValue) { var list = JSON.parse(currentCookieValue); // 存儲到本地存儲 localStorage.setItem("myList", JSON.stringify(list)); }else{ FindBitch() .then(text => { let FirstKill = [text]; setListInCookie(FirstKill); }) .catch(error => { console.error("找不到五百八:", error); }); } // 刪除不需要的ID document.querySelector('.profile-header__account').remove(); //封殺按鈕------------------------------------------------- // 找到具有class為"profile-header__right"的元素 var profileRightElement = document.querySelector('.profile-header__right'); var pullRightElement = profileRightElement.querySelector('.pull-right'); // 創建一個新按鈕元素 var BlockBtn = document.createElement('button'); BlockBtn.textContent = '封殺'; // 添加樣式和類名到按鈕 BlockBtn.style.marginTop = '10px'; BlockBtn.style.width = '100%'; BlockBtn.className = 'btn btn-trace trace_btn_border BlockBtn'; // 將按鈕元素添加到"pull-right"元素內部 pullRightElement.appendChild(BlockBtn); // 通緝犯名單的cookie------------------------------------------------ // 從 Cookie 中加載 list(例如,頁面加載時) list = getListFromCookie()||[]; let UserBlock = document.querySelector('.profile-header__name'); let text = UserBlock.textContent.trim(); // 如果使用者已經在封殺名單內的判斷,已存在或不存在 if (list.includes(text)) { BlockStart(BlockBtn); } else { // 針對封殺按鈕進行監聽事件 BlockBtn.addEventListener('click', function() { list.push(text); setListInCookie(list); //先加入到名單內,然後再執行封殺事件 BlockStart(); // 輸出到控制台 console.log('黑名單新增:' + text); //本地儲存機制----------------------------- let currentCookieValue = getCookie("myList"); let list2 = JSON.parse(currentCookieValue); // 存儲到本地存儲 localStorage.setItem("myList", JSON.stringify(list2)); }); } } // ------------------------------------------------ //封殺事件的函數 function BlockStart(){ let BlockBtn = document.querySelector('.BlockBtn'); BlockBtn.textContent = '已封殺'; BlockBtn.disabled = true; BadText(); BadImg(); } //封殺事件函數裡面的細項函數 function BadText(){ // 標記這傢夥是垃圾------------------------------------------------- let UserBlock = document.querySelector('.profile-header__name'); let newHeading = document.createElement('h1'); newHeading.textContent = '思想通緝犯'; // 把思想通緝犯這幾個大字加上去 UserBlock.parentElement.insertBefore(newHeading, UserBlock); UserBlock.style.textDecoration = "line-through"; UserBlock.style.color = "red"; } function BadImg(){ //圖片進行網點作業XD------------------------------------------------- var originalImage = document.querySelector('.profile-header__avatar'); // 創建一個包含交叉紅線的覆蓋層 <div> 元素 var overlayDiv = document.createElement('div'); overlayDiv.style.position = 'absolute'; overlayDiv.style.width = '150px'; overlayDiv.style.height = '150px'; overlayDiv.style.background = 'linear-gradient(45deg, black 50%, transparent 50%), linear-gradient(-45deg, black 50%, transparent 50%)'; overlayDiv.style.backgroundSize = '5px 5px, 5px 5px'; overlayDiv.style.backgroundPosition = '0 0, 0 2px'; // 將覆蓋層疊加到圖片上 originalImage.parentNode.appendChild(overlayDiv); // 設置覆蓋層的位置,以與原始圖像對齊 overlayDiv.style.top = originalImage.offsetTop + 'px'; overlayDiv.style.left = originalImage.offsetLeft + 'px'; // 設置覆蓋層的z-index,以確保它在圖片上方 overlayDiv.style.zIndex = '2'; } ``` ## 觀念筆記 這個腳本開發足足花了我一整個晚上,將近八小時之久。 有趣的是,其中為了進行測試才選某些user當作隱藏對象,否則腳本執行上會出錯。 細心的人若觀察原始碼,也會發現裡面有個firstKill, 那是必須要的段落,先設置好cookie的首要內容物,才有辦法繼續操作下去( ิ◕㉨◕ ิ) 也就是初始化的概念XD 另一個有趣的點是,為了防止使用者改名導致腳本出錯,我甚至不惜再寫一段ajax, 去更新一下ID,這樣不管人家怎麼改,都逃不了, 要改成「別抓我」也沒用,這個腳本都還是可以run。 ## 心得後記 我只能說這篇是自從「備份IT幫發文、一眼全覽」最強的JS教學文章! 超派,真的不騙( メ∀・) 有些人喜歡看前端小試身手,有些人喜歡前端動手玩創意; 其實這兩個系列的本質都是JS的教學,是可以互相連接的,但也有很多不同的重心。 這個系列就是以腳本為主,重點在於創意與發想,打到使用者痛點。 【前端動手玩創意】則是以建構網站為起點, 任何元素與概念都會變成網頁上的一部分,算是比較基礎工的建立。 如果對JS的強大感興趣,那麼可以把這兩個系列交互看,反覆的閱讀、實際動手操作, 這樣一來的學習非常踏實,甚至比YT學習都來的高效率、實際。 尤其此系列都是原創發想的腳本,當然超強! 喜歡記得關注,未來還有更多超酷的前端內容可以玩,下課⧸⎩⎠⎞͏(・∀・)⎛͏⎝⎭⧹

laravel 的單元測試功能 Mail::fake() 簡單原理分析

為了提升系統穩定性,最近替某電商客戶的專案寫單元測試 有一部份核心功能會用到第三方 API,這讓測試變很難寫,因為沒有本地的狀態變化可以比較 想到 laravel 有一個關於 mail 的測試功能,有類似情況 簡單來說就是在 phpunit 中這樣啟動之後 ``` Mail::fake(); ``` 就可以這樣來檢查是否有執行某 api ``` Mail::assertSent(OrderShipped::class); ``` 能否用同樣原理,處理我面對的情況呢? 決定來看一下原始碼! --- 首先是 fake 會直接換掉 Mailer 的實體 `framework/src/Illuminate/Support/Facades/Mail.php` ``` public static function fake() { $actualMailManager = static::isFake() ? static::getFacadeRoot()->manager : static::getFacadeRoot(); return tap(new MailFake($actualMailManager), function ($fake) { static::swap($fake); }); } ``` 可見是實作同樣 interface 的測試專用類別! `framework/src/Illuminate/Support/Testing/Fakes/MailFake.php` 然後寄信就改成,把變數存進陣列即可 ``` public function send($view, array $data = [], $callback = null) { if (! $view instanceof Mailable) { return; } $view->mailer($this->currentMailer); if ($view instanceof ShouldQueue) { return $this->queue($view, $data); } $this->currentMailer = null; $this->mailables[] = $view; } ``` 然後 assertSent 那些就檢查陣列內容,比對一下即可 ``` public function assertSent($mailable, $callback = null) { [$mailable, $callback] = $this->prepareMailableAndCallback($mailable, $callback); if (is_numeric($callback)) { return $this->assertSentTimes($mailable, $callback); } $message = "The expected [{$mailable}] mailable was not sent."; if (count($this->queuedMailables) > 0) { $message .= ' Did you mean to use assertQueued() instead?'; } PHPUnit::assertTrue( $this->sent($mailable, $callback)->count() > 0, $message ); } ``` inside of the `sent` function ``` return $this->mailablesOf($mailable)->filter(fn ($mailable) => $callback($mailable)); ``` --- 所以,目前這樣看起來,在需要測試跟「第三方 API」有關的功能時,可以使用同樣原理 使用 laravel 提供的 mock 功能,把會用到的第三方套件 mock 掉,套件的核心 function 就存個變數或陣列之類的 然後多寫個類似 `assertSent` 這樣的方法,應該就可以做到不錯的測試效果~! 簡單來說,就是設法做出「本地的狀態變化可以比較」就是了 我來往這方向寫寫看,有心得再補充分享~

寫單元測試的五個技巧分享

測試程式碼是軟體開發最重要的方面之一,因為它可以確保產品的品質、可擴展性和可靠性。 但是,如果沒有任何指導,編寫有效的測試可能會很困難。測試程式碼可能比實際功能的程式碼更複雜、更難維護! 原文出處:https://dev.to/one-beyond/the-5-principles-of-unit-testing-1p5f ## 1.精簡和準確的測試 測試程式碼必須**簡單且易於使用**。任何查看測試的人都應該立即知道測試的內容及其目的是什麼。開發測試應該以很少的精力和時間投入帶來巨大的價值。 > _您是否需要超過 30 秒的時間來閱讀和理解您的測試?重寫它!_ 測試“覆蓋率”是重點嗎?不! **僅測試必要的部份就好**。為了敏捷性和簡單性,最好放棄一些測試,**只測試主要業務邏輯和主要邊緣情況就好**。 ## 2.測試行為,而不是元件實作 不要檢查程式碼中的每一行和內部變數。測試時,您應該**專注於結果**。即使方法內的程式碼被重構,結果也應該永遠保持不變! 這樣,如果程式碼庫發生更改,您**不需要重寫測試**。 ``` // Wrong ❌ - Test behaviour describe('Evaluation Service', () => { describe('Register Students', () => { it('Should add new students to the evaluation service', () => { const studentJosh = { id: 1, name: 'Josh McLovin', average: 6.98, } evaluationService.addStudent(studentJosh) expect(evaluationService._students[0].name).toBe('Josh') expect(evaluationService._students[0].average).toBe(6.98) }) }) }) ``` ``` // Right ✅ - Test behaviour describe('Evaluation Service', () => { describe('Register Students', () => { it('Should add new students to the evaluation service', () => { const studentJosh = { id: 1, name: 'Josh McLovin', average: 6.98, } evaluationService.addStudent(studentJosh) expect(evaluationService.getStudentAverage('Josh')).toBe(6.98) }) }) }) ``` ## 3. 關於命名和結構:AAA 模式 您是否曾經遇到過名為“_它應該[...]正確_”的失敗測試,但花了幾分鐘才找到問題所在? 良好命名和結構可以讓您快速且準確地找到任何失敗的測試,最終節省您的寶貴時間。因此,讓我們深入探討兩個關鍵原則,以便在下次測試時牢記: ### 3.1 經過深思熟慮的測試命名: 在命名您的測試時,請嘗試合併以下資訊:   - **正在測試什麼**?   - **什麼情況**?   - **預期結果**是什麼? ``` // Right ✅ - Test naming // 1. What is being tested: describe('Evaluation Service', () => { describe('Evaluate Students', () => { // 2 & 3. Context and expected result it('If the student grade is below the minimum grade, student should be suspended', () => { const students = [ { name: 'Mark', grade: 4.25 }, { name: 'Colin', grade: 6.7 }, { name: 'Ben', grade: 5.3 }, ] const result = evaluationService.evaluateStudents({ students, minGrade: 5 }) expect(result['Mark']).toBe('suspended') }) }) }) ``` ### 3.2 測試程式碼結構的AAA模式: 如果您想維護一個可讀且易於理解的測試套件,請按如下方式建立測試: - **安排**:設定模擬所需情況所需的所有程式碼。這可以包括初始化變數、模擬響應、實例化被測單元等。 - **行為**:執行正在測試的內容,通常在一行程式碼中。 - **斷言**:檢查得到的結果是否為預期的結果。與上面的一樣,這應該只需要一行。 ``` // Right - AAA Testing Pattern describe('Evaluation Service', () => { describe('Average Calculation', () => { it('Should calculate the average grade of all the students', () => { // Arrange: create an object with the student names and their grades const students = [ { name: 'Mark', grade: 4 }, { name: 'Colin', grade: 10 }, { name: 'Ben', grade: 7 }, { name: 'Tim', grade: 3 }, ] // Act: execute the getAverage method const avg = evaluationService.getAverage(students) // Assert: check if the result is the expected one -> (4+10+7+3)/4 = 6 expect(avg).toEqual(6) }) }) }) ``` ##4。確定性和隔離測試 如果一個失敗的測試使您的整個套件變紅,那麼您可能沒有以正確的方式處理它! 測試應該**獨立和隔離**,一次針對並處理一個特定邏輯,從而完成更快、更穩定的測試套件。 **如果您不**獨立編寫測試會發生什麼? - 您將無法找出錯誤和問題的**確切原因和位置**。 - 重構測試時,您必須**更新和同步多個測試**。 - 您將無法以任何順序執行測試,這可能會導致**破壞或跳過某些斷言或期望**。 ## 5。基於屬性的測試和真實資料 厭倦了在測試中編寫大量可能的輸入?基於屬性的測試可以為您做到這一點!……那是什麼? 基於屬性的測試建立了數百種可能的組合,強調了測試並**增加了發現以前未被注意到的錯誤的機會**。這種方法甚至可以傳回可能導致意外結果的輸入。 [JSVerify](https://github.com/jsverify/jsverify) 或 [Fast-Check](https://github.com/dubzzz/fast-check) 等函式庫提供了促進基於屬性的測試的基本工具。 但是,如果您不想深入進行如此廣泛的測試,那麼盡可能利用**真實資料**至關重要。像“abc”或“1234”這樣的輸入可能會在實際失敗時錯誤地通過測試。 ``` // Wrong ❌ - False Positive - Test that passes even though it shouldn't class EvaluationService { _students = []; addStudent(student) { // Add the student if the name has no numbers if(!student.name.matches(/^([^0-9]*)$/)){ this._students.push(student); } } } describe('Evaluation Service', () => { describe('Register Students', () => { it('Should add students to the Evaluation service', () => { const mockStudent = { id: 2, name: 'username', average: 7 } // Won't fail because the name is a string without number -> We are not checking what happens if the user // inputs a name with a number. evaluationService.addStudent(mockStudent) expect(evaluationService.getStudentAverage('username')).toBe(7) }) }) }) // In the example above, we are making a test so the test passes, instead of looking for edge cases with realistic data ``` ##額外提示! 如果您在測試元件中的任何邏輯時遇到困難,這可能反映出您也許應該將元件邏輯分解為更小、更容易且可測試的程式碼片段! 總而言之,遵循此最佳實踐可能會帶來一種新的、可讀且可維護的方法來測試您喜愛的生產程式碼。 > 經過測試的應用程式才是可靠的應用程式!