🔍 搜尋結果:`;

🔍 搜尋結果:`;

🚀使用 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

如何在 git 撤銷上一個 commit

在這篇文章中,我將展示有時如何在命令列上使用 git 恢復編碼專案中的錯誤變更(提交)。 ### 我為什麼要這樣做? 在我的論文中,我正在研究一個在一個環境中開發的專案,然後在另一個由多個虛擬機器組成的環境中進行測試。所以我所做的每一個重要的改變都可能對專案的功能產生重大影響。有時候,我所做的改變可能沒有達到我預期的結果。然後我必須查看更改並分析上次提交前後專案的行為。 ### 你如何看待最後一次提交? 要測試特定提交,您需要哈希值。要取得哈希值,您可以執行“git log”,然後您將獲得以下輸出: ``` root@debian:/home/debian/test-project# git log commit <last commit hash> Author: Isabel Costa <[email protected]> Date: Sun Feb 4 21:57:40 2018 +0000 <commit message> commit <before last commit hash> Author: Isabel Costa <[email protected]> Date: Sun Feb 4 21:42:26 2018 +0000 <commit message> (...) ``` 您也可以執行“git log --oneline”來簡化輸出: ``` root@debian:/home/debian/test-project# git log --oneline <last commit hash> <commit message> cdb76bf Added another feature d425161 Added one feature (...) ``` 若要測試您認為具有最後工作版本的特定提交(例如:「<before last commit hash>」),您可以輸入以下內容: ``` git checkout <commit hash> ``` 這將使工作儲存庫與此確切提交的狀態相符。 執行此操作後,您將得到以下輸出: ``` root@debian:/home/debian/test-project# git checkout <commit hash> Note: checking out '<commit hash>'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by performing another checkout. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -b with the checkout command again. Example: git checkout -b new_branch_name HEAD is now at <commit hash>... <commit message> ``` 分析特定提交後,如果您決定保持該提交狀態,則可以撤銷上次提交。 ### 如何撤銷此提交? 如果您希望[撤銷/復原上次提交](https://git-scm.com/docs/git-revert),您可以使用從「git log」指令取得的提交雜湊執行下列操作: ``` git revert <commit hash> ``` 此命令將建立一個新的提交,並在訊息開頭包含“Revert”一詞。之後,如果您檢查儲存庫狀態,您會發現 HEAD 在先前測試的提交處已分離。 ``` root@debian:/home/debian/test-project# git status HEAD detached at 69d885e (...) ``` [您不想看到此訊息](https://www.git-tower.com/learn/git/faq/detached-head-when-checkout-commit),因此要修復此問題並附回 HEAD到您的工作儲存庫,您應該簽出您正在處理的分支: ``` git checkout <current branch> ``` 在撰寫這篇文章的過程中,我發現了Atlassian 的教程 —[ 撤消提交和更改](https://www.atlassian.com/git/tutorials/undoing-changes) — ,它很好地描述了這個問題。 ### 概括 - 如果你想測試之前的提交,只需執行`git checkout <test commit hash>`;然後您可以測試專案的最後一個工作版本。 - 如果你想恢復最後一次提交,只需執行「git revert <unwanted commit hash>」;然後你可以推送這個新的提交,這會撤銷你先前的提交。 - 若要修復分離的頭,請執行「git checkout <目前分支>」。 --- 您可以在[Twitter](https://twitter.com/isabelcmdcosta)、[LinkedIn](https://www.linkedin.com/in/isabelcmdcosta)、[Github](https://github.com) 上找到我/isabelcosta)、[Medium](https://medium.com/@isabelcmdcosta) 和[我的個人網站](https://isabelcosta.github.io)。 --- 原文出處:https://dev.to/isabelcmdcosta/how-to-undo-the-last-commit--31mg

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腳本-IMG複製大師,懶人專用小腳本

## 轉載自 https://ithelp.ithome.com.tw/articles/10338052 ## 前情提要 有時候我們需要使用一些圖片,勢必需要圖片的網址,這時候, 也許你可以本機上傳,然後去把網址抓好, 又或者你可以找到網路上別人的圖片,右鍵開啟新分頁,再複製網址; 又或者直接打開檢查(F12)找到src的網址去複製。 光聽這個流程我就覺得,心累。 這時候前端大師,就會寫一套簡單的小腳本來完成這個白爛的工作。 ## 腳本下載 ![](https://i.imgur.com/D1s1kxi.png) https://greasyfork.org/zh-TW/scripts/477141-img%E8%A4%87%E8%A3%BD%E5%A4%A7%E5%B8%AB ## JS程式碼 ``` // ==UserScript== // @name IMG複製大師 // @namespace http://tampermonkey.net/ // @version 0.1 // @description 針對網頁上圖片,點擊就複製其網址 // @author You // @match *://*/* // @icon https://www.highcharts.com/demo/highcharts/spline-plot-bands // @grant none // ==/UserScript== let isEventActive = false; // 用於跟蹤事件的狀態 // 創建一個鏈接元素並設置其屬性 const toastrCssLink = document.createElement('link'); toastrCssLink.rel = 'stylesheet'; toastrCssLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/css/toastr.min.css'; // 創建一個腳本元素並設置其屬性 const toastrScript = document.createElement('script'); toastrScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js'; // 創建另一個腳本元素並設置其屬性 const toastrScript2 = document.createElement('script'); toastrScript2.src = 'https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/js/toastr.min.js'; // 將鏈接元素和腳本元素添加到文檔頭部 document.head.appendChild(toastrCssLink); document.head.appendChild(toastrScript); document.head.appendChild(toastrScript2); // 獲取頁面上的所有img元素 const images = document.querySelectorAll('img'); function removeClickHandler(image) { image.removeEventListener('click', clickHandler); } function toggleEvent() { if (isEventActive) { // 如果事件已經激活,則關閉事件 console.log('IMG複製大師關閉ꐦ°᷄д°᷅'); // 解除事件監聽 images.forEach(removeClickHandler); isEventActive = false; } else { // 如果事件尚未激活,則打開事件 console.log('IMG複製大師啟動ฅ^•ﻌ•^ฅ'); // 遍歷所有img元素 images.forEach((image) => { // 檢查圖像是否已加載 if (image.complete) { // 圖像已加載,直接添加點擊事件處理程序 addClickHandler(image); } else { // 圖像尚未加載,等待加載完成後再添加點擊事件處理程序 image.addEventListener('load', () => { addClickHandler(image); }); } }); isEventActive = true; } } document.addEventListener('keydown', (event) => { if ((event.key === 'q' && event.ctrlKey) || event.key === 'F8') { toggleEvent(); // 切換事件的狀態 } }); function transformImageUrl(url) { // 使用正則表達式匹配URL中的目標部分 const regex = /https:\/\/cache.ptt.cc\/c\/https\/i.imgur.com\/([^?]+)/; const match = url.match(regex); if (match) { // 如果匹配成功,構建新的URL const imgurId = match[1]; return `https://i.imgur.com/${imgurId}`; } else { // 如果沒有匹配到目標部分,返回原始URL return url; } } // 創建一個函數,用於添加點擊事件處理程序並處理圖像的src function addClickHandler(image) { image.addEventListener('click', clickHandler); } // 創建一個函數,用於處理點擊事件 function clickHandler() { let src = this.src; src = transformImageUrl(src); const textArea = document.createElement('textarea'); textArea.value = src; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); // 使用 toastr 進行通知 toastr.success('網址複製完成: ' + src); } ``` ## 觀念筆記 這個小腳本總共使用了大概三種觀念, 第一種就是toast的套件使用, 第二個是監聽鍵盤做開關, 第三個則是程式本身對於img的監聽,把src複製到剪貼簿。 ### 第一部分:套件 使用套件在於腳本,必須利用appendChild,先使用create的API製作出放置CDN的link元素, 再把它append到網頁上,則可以使用其套件。 ### 第二部分:監聽鍵盤 這腳本啟動與關閉是透過監聽鍵盤的,ctrl+q或F8這部分就是純粹監聽。 ``` document.addEventListener('keydown', (event) => { if ((event.key === 'q' && event.ctrlKey) || event.key === 'F8') { toggleEvent(); // 切換事件的狀態 } }); ``` 裡面寫了一個toggleEvent是因為想要設定可以開開關關。 ### 第三部分:程式本身 這邊有一點小技巧是複製到剪貼簿,其實以前文章也有寫過教學,是個常見實用的招數: ``` const textArea = document.createElement('textarea'); textArea.value = src; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); ``` 再來就是針對網頁上全部的img元素,去監聽點擊事件、移除事件這兩個。 ``` const images = document.querySelectorAll('img'); function addClickHandler(image) { image.addEventListener('click', clickHandler); } function removeClickHandler(image) { image.removeEventListener('click', clickHandler); } ``` 之後這兩個事件要寫在toggle裡面,當開啟的條件就執行新增事件;關閉就執行解除綁定 開啟也就是: ``` images.forEach((image) => { // 檢查圖像是否已加載 if (image.complete) { // 圖像已加載,直接添加點擊事件處理程序 addClickHandler(image); } else { // 圖像尚未加載,等待加載完成後再添加點擊事件處理程序 image.addEventListener('load', () => { addClickHandler(image); }); ``` 關閉則是: `images.forEach(removeClickHandler);` ## 心得 這次的小腳本功能非常簡單,寫起來邏輯也是很清晰透明,沒有什麼彎曲。 雖然簡單,但是寫起來功能非常實用! 這就是針對懶人專用的! 其實沒有人規定前端這種東西要寫得多大,多了不起/ᐠ。ꞈ。ᐟ\ 好像你很屌、很懂前端、框架,那個是一回事,能夠完成自己的需求也很美好。 很久沒更新了,這次的酷酷小腳本應該很爽⁽⁽٩(๑˃̶͈̀ ᗨ ˂̶͈́)۶⁾⁾ 好好的來玩前端吧,未來還有更大的宇宙!

如何在微前端 Micro Frontends 架構中處理 CSS

如何在微前端中處理 CSS?畢竟,樣式始終是*任何* UI 片段所需要的東西,但是,它也是全局共享的東西,因此是潛在的衝突來源。 在這篇文章中,我想回顧一下現有的不同策略來馴服 CSS 並使其擴展以開發微前端。如果這裡的任何內容對您來說聽起來很合理,那麼也可以考慮研究一下[“微前端的藝術”](https://microfrontends.art/)。 **本文的程式碼可以在[github.com/piral-samples/css-in-mf](https://github.com/piral-samples/css-in-mf)找到。請務必查看示例實現。** CSS 的處理是否會影響每個微前端解決方案?讓我們檢查可用的類型來驗證這一點。 原文出處:https://dev.to/florianrappl/css-in-micro-frontends-4jai ## 微前端的類型 過去我寫了很多關於存在哪些類型的微前端、為什麼存在以及何時應該使用什麼類型的微前端架構的文章。採用 Web 方法意味著使用 iframe 來使用來自不同微前端的 UI 片段。在這種情況下,沒有任何限制,因為無論如何每個片段都是完全隔離的。 在任何其他情況下,無論您的解決方案使用客戶端還是伺服器端組合(或介於兩者之間的東西),您最終都會得到在瀏覽器中評估的樣式。因此,在所有其他情況下,您都會關心 CSS。讓我們看看這裡有哪些選項。 ## 無特殊處理 好吧,第一個 - 也許是最(或根據觀點,最不)明顯的解決方案是不進行任何特殊處理。相反,每個微前端都可以附帶額外的樣式表,然後在渲染微前端的元件時附加這些樣式表。 理想情況下,每個元件僅在首次渲染時加載所需的樣式,但是,由於這些樣式中的任何一個都可能與現有樣式衝突,我們也可以假裝在微前端的任何元件渲染時加載所有“有問題的”樣式。 ![衝突](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fewl99l6y04edyjnni2j.png) 這種方法的問題在於,當給出諸如“div”或“div a”之類的通用選擇器時,我們還將重新設置其他元素的樣式,而不僅僅是原始微前端的片段。更糟糕的是,類和屬性也不是故障保護措施。像“.foobar”這樣的類也可以在另一個微前端中使用。 **您將在引用的演示存儲庫中找到兩個衝突的微前端的示例,網址為 [solutions/default](https://github.com/piral-samples/css-in-mf/tree/main/solutions)。** 擺脫這種痛苦的一個好方法是進一步隔離元件 - 就像 Web 元件一樣。 ## 影子 DOM 在自定義元素中,我們可以打開一個影子根來將元素附加到專用的迷你文件,該迷你文件實際上與其父文件屏蔽。總的來說,這聽起來是一個好主意,但與這裡介紹的所有其他解決方案一樣,沒有硬性要求。 ![Shadow DOM](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xp18uw9sxffq1l7oj6mc.png) 理想情況下,微前端可以自由決定“如何”實現元件。因此,實際的 Shadow DOM 集成必須由微前端完成。 使用 Shadow DOM 有一些缺點。最重要的是,雖然 Shadow DOM 內部的樣式保留在內部,但全局樣式也不會影響 Shadow DOM。乍一看,這似乎是一個優勢,但是,由於整篇文章的主要目標只是隔離微前端的樣式,因此您可能會錯過諸如應用某些全局設計系統(例如 Bootstrap)之類的要求。 要使用 Shadow DOM 進行樣式設置,我們可以通過“link”引用或“style”標籤將樣式放入 Shadow DOM 中。由於 Shadow DOM 是無樣式的,並且外部的樣式不會傳播到其中,因此我們實際上需要它。除了編寫一些內聯樣式之外,我們還可以使用捆綁器將“.css”(或者類似“.shadow.css”的內容)視為原始文本。這樣,我們只會得到一些文本。 對於 esbuild,我們可以配置 `piral-cli-esbuild` 的預製配置,如下所示: ``` module.exports = function(options) { options.loader['.css'] = 'text'; options.plugins.splice(0, 1); return options; }; ``` 這會刪除初始 CSS 處理器 (SASS) 並為“.css”文件配置標準加載器。現在,shadow DOM 中的某些樣式的工作方式如下: ``` import css from "./style.css"; customElements.define(name, class extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); } connectedCallback() { this.style.display = "contents"; const style = this.shadowRoot.appendChild(document.createElement('style')); style.textContent = css; } }); ``` 上面的程式碼是一個有效的自定義元素,從樣式角度來看它是透明的(“display:contents”),即只有其內容會反映在渲染樹中。它託管一個包含單個“style”元素的影子 DOM。 `style` 的內容設置為 `style.css` 文件的文本。 **您將在 [`solutions/shadow-dom`](https://github.com/piral-samples/css-in-mf/tree/main) 引用的演示存儲庫中找到兩個衝突的微前端的示例/解決方案/shadow-dom)。** 域元件避免使用影子 DOM 的另一個原因是,並非每個 UI 框架都能夠處理影子 DOM 中的元素。因此,無論如何都必須尋找替代解決方案。一種方法是轉而使用一些 CSS 約定。 ## 使用命名約定 如果每個微前端都遵循全局 CSS 約定,那麼就可以在元級別上避免衝突。最簡單的約定是在每個類前面加上微前端的名稱。因此,舉例來說,如果一個微前端稱為“shopping”,另一個微前端稱為“checkout”,那麼兩者都會將其“active”類分別重命名為“shopping-active”/“checkout-active”。 ![約定](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6l8r1s59v1dcu3yp022u.png) 這同樣適用於其他可能存在衝突的名稱。舉個例子,在微前端稱為“shopping”的情況下,我們將其稱為“shopping-primary-button”,而不是像“primary-button”這樣的ID。如果由於某種原因,我們需要設置元素的樣式,我們應該使用後代選擇器(例如“.shopping img”)來設置“img”標籤的樣式。現在,這適用於具有“shopping”類的 *some* 元素中的“img”元素。這種方法的問題是購物微前端也可能使用其他微前端的元素。如果我們看到“div.shopping > div.checkout img”怎麼辦?儘管“img”現在由通過“checkout”微前端帶來的元件託管/集成,但它的樣式將由“shopping”微前端 CSS 設計。這並不理想。 儘管命名約定在一定程度上解決了問題,但它們仍然容易出錯並且使用起來很麻煩。如果我們重命名微前端會怎樣?如果微前端在不同的應用程式中獲得不同的名稱怎麼辦?如果我們在某些時候忘記應用命名約定怎麼辦?這就是工具幫助我們的地方。 ## CSS 模塊 自動引入一些前綴並避免命名衝突的最簡單方法之一是使用 CSS 模塊。根據您選擇的捆綁器,這可以是開箱即用的,也可以通過一些配置更改來實現。 ![CSS 模塊](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o49rdmokn5kvn8ncjjrt.png) ``` // Import "default export" from CSS import styles from './style.modules.css'; // Apply <div className={styles.active}>Active</div> ``` 導入的模塊是一個生成的模塊,保存將其原始類名(例如“active”)映射到生成的類名的值。生成的類名通常是 CSS 規則內容與原始類名混合的哈希值。這樣,名稱應該盡可能唯一。 作為示例,讓我們考慮使用“esbuild”建置的微前端。對於“esbuild”,您需要一個插件(“esbuild-css-modules-plugin”)和相應的配置更改以包含 CSS 模塊。 使用 Piral 我們只需要調整 `piral-cli-esbuild` 已經帶來的配置。我們刪除標準 CSS 處理(使用 SASS)並用插件替換: ``` const cssModulesPlugin = require('esbuild-css-modules-plugin'); module.exports = function(options) { options.plugins.splice(0, 1, cssModulesPlugin()); return options; }; ``` 現在我們可以在程式碼中使用 CSS 模塊,如上所示。 CSS 模塊有一些缺點。首先,它附帶了一些標準 CSS 的語法擴展。這對於區分我們想要導入的樣式(因此要進行預處理/哈希)和應保持原樣的樣式(即稍後在不導入的情況下使用)是必要的。另一種方法是將CSS直接帶入JS文件中。 ## CSS-in-JS CSS-in-JS 最近的名聲很差,但是,我認為這是一個誤解。我也更喜歡將其稱為“CSS-in-Components”,因為它為元件本身帶來了樣式。一些框架(Astro、Svelte 等)甚至允許通過其他方式直接執行此操作。經常被提及的缺點是性能 - 這通常是由於在瀏覽器中編寫 CSS 造成的。然而,這並不總是必要的,在最好的情況下,CSS-in-JS 庫實際上是建置時間驅動的,即沒有任何性能缺陷。 ![CSS-in-JS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/36w7yuksmdzgstastjxv.png) 然而,當我們談論 CSS-in-JS(或 CSS-in-Components)時,我們需要考慮現有的各種選項。為簡單起見,我只包含三個:情感、樣式元件和香草提取物。讓我們看看它們如何幫助我們在將微前端整合到一個應用程式中時避免衝突。 ### Emotion Emotion 是一個非常酷的庫,它附帶了 React 等框架的幫助程序,但沒有將這些框架設置為先決條件。情感可以很好地優化和預先計算,並允許我們使用可用的 CSS 技術的完整庫。 使用“純粹”情感相當容易;首先安裝包: ``` npm i @emotion/css ``` 現在您可以在程式碼中使用它,如下所示: ``` import { css } from '@emotion/css'; const tile = css` background: blue; color: yellow; flex: 1; display: flex; justify-content: center; align-items: center; `; // later <div className={tile}>Hello from Blue!</div> ``` `css` 幫助器允許我們編寫被解析並放置在樣式表中的 CSS。返回值是生成的類的名稱。 如果我們特別想使用 React,我們還可以使用 Emotion 中的 jsx 工廠(引入了一個名為 css 的新標準 prop)或 styled 幫助器: ``` npm i @emotion/react @emotion/styled ``` 現在感覺很像樣式是 React 本身的一部分。例如,“styled”幫助器允許我們定義新元件: ``` const Output = styled.output` border: 1px dashed red; padding: 1rem; font-weight: bold; `; // later <Output>I am groot (from red)</Output> ``` 相比之下,“css”輔助屬性使我們能夠稍微縮短符號: ``` <div css={` background: red; color: white; flex: 1; display: flex; justify-content: center; align-items: center; `}> Hello from Red! </div> ``` 總而言之,這會生成不會衝突的類名,並提供避免樣式混合的穩健性。 “styled”助手尤其受到流行的“styled-components”庫的啟發。 ### 樣式元件 “styled-components”庫可以說是最流行的 CSS-in-JS 解決方案,並且常常是此類解決方案聲譽不佳的原因。從歷史上看,這實際上是在瀏覽器中編寫 CSS 的全部內容,但在過去幾年中,他們確實極大地推進了這一點。今天,您也可以對所使用的樣式進行一些非常好的伺服器端組合。 與“emotion”相比,安裝(針對 React)需要更少的軟體包。唯一的缺點是打字是事後才想到的 - 所以你需要安裝兩個包才能完全喜歡 TypeScript: ``` npm i styled-components --save npm i @types/styled-components --save-dev ``` 安裝後,該庫就已經完全可用: ``` import styled from 'styled-components'; const Tile = styled.div` background: blue; color: yellow; flex: 1; display: flex; justify-content: center; align-items: center; `; // later <Tile>Hello from Blue!</Tile> ``` 其原理與“情感”相同。因此,讓我們探索另一種選擇,嘗試從一開始就實現零成本,而不是事後的想法。 ### Vanilla Extract 我之前寫的關於利用類型更接近元件(並避免不必要的執行時成本)的內容正是最新一代 CSS-in-JS 庫所涵蓋的內容。最有前途的庫之一是“@vanilla-extract/css”。 使用該庫有兩種主要方式: - 與您的捆綁器/框架集成 - 直接使用 CLI 在此示例中,我們選擇前者 - 並將其集成到“esbuild”。為了使集成正常工作,我們需要使用“@vanilla-extract/esbuild-plugin”包。 現在我們將其集成到建置過程中。使用 `piral-cli-esbuild` 配置,我們只需將其加入到配置的插件中: ``` const { vanillaExtractPlugin } = require("@vanilla-extract/esbuild-plugin"); module.exports = function (options) { options.plugins.push(vanillaExtractPlugin()); return options; }; ``` 為了使 Vanilla Extract 正常工作,我們需要編寫 `.css.ts` 文件,而不是普通的 `.css` 或 `.sass` 文件。這樣的文件可能如下所示: ``` import { style } from "@vanilla-extract/css"; export const heading = style({ color: "blue", }); ``` 這都是有效的 TypeScript。我們最終會得到一個類名的導出 - 就像我們從 CSS 模塊、Emotion 中得到的一樣 - 你明白了。 所以最後,上面的樣式將像這樣應用: ``` import { heading } from "./Page.css.ts"; // later <h2 className={heading}>Blue Title (should be blue)</h2> ``` 這將在建置時完全處理——而不是執行時成本。 您可能會感興趣的另一種方法是使用 CSS 實用程序庫,例如 Tailwind。 ## CSS 實用程序,例如 Tailwind 這是一個獨立的類別,但我認為既然 Tailwind 是這個類別中的主要工具,我將只介紹 Tailwind。 Tailwind 的主導地位甚至達到了甚至有人問“你寫 CSS 還是 Tailwind?”之類的問題。這與 jQuery 在 DOM 操作領域的統治地位非常相似。 2010 年,人們問“這是 JavaScript 還是 jQuery?”。 無論如何,使用 CSS 實用程序庫的優點是根據使用情況生成樣式。這些樣式不會衝突,因為實用程序庫始終以相同的方式定義它們。因此,每個微前端將僅附帶實用程序庫中根據需要顯示微前端所需的部分。 ![Tailwind](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8waj7k3823oq77qhkzmb.png) 如果使用 Tailwind 和 esbuild,我們還需要安裝以下軟體包: ``` npm i autoprefixer tailwindcss esbuild-style-plugin ``` esbuild的配置比之前複雜一點。 `esbuild-style-plugin` 本質上是 esbuild 的 PostCSS 插件;所以必須正確配置: ``` const postCssPlugin = require("esbuild-style-plugin"); module.exports = function (options) { const postCss = postCssPlugin({ postcss: { plugins: [require("tailwindcss"), require("autoprefixer")], }, }); options.plugins.splice(0, 1, postCss); return options; }; ``` 在這裡,我們刪除了默認的 CSS 處理插件 (SASS),並將其替換為 PostCSS 插件 - 使用 PostCSS 的“autoprefixer”和“tailwindcss”擴展。 現在我們需要加入一個有效的 *tailwind.config.js* 文件: ``` module.exports = { content: ["./src/**/*.tsx"], theme: { extend: {}, }, plugins: [], }; ``` 這本質上是配置 Tailwind 的最低要求。它只是提到應該掃描 `tsx` 文件以了解 Tailwind 實用程序類的使用情況。然後找到的類將被放入 CSS 文件中。 因此,CSS 文件還需要知道生成/使用的聲明應包含在哪裡。至少我們只有以下 CSS: ``` @tailwind utilities; ``` 還有其他“@tailwind”指令。例如,Tailwind 帶有重置和基礎層。然而,在微前端中,我們通常不關心這些層。這屬於應用程式 shell 或編排應用程式的關注範圍 - 而不是域應用程式。 然後,CSS 將被替換為 Tailwind 中已指定的類: ``` <div className="bg-red-600 text-white flex flex-1 justify-center items-center">Hello from Red!</div> ``` ## 比較 迄今為止提出的幾乎每種方法都是微前端的可行競爭者。一般來說,這些溶液也可以混合。一個微前端可以採用影子 DOM 方法,而另一個微前端則對 Emotion 感到滿意。第三個圖書館可能會選擇使用香草精。 最後,唯一重要的是所選擇的解決方案是無碰撞的,並且不會帶來(巨大的)執行時成本。雖然某些方法比其他方法更有效,但它們都提供了所需的樣式隔離。 |方法|遷移工作|可讀性|穩健性|性能影響| | ----------- | -------------- | -------------- | ---------- | ------------------ | |大會 |中等|高|低|無 | | CSS 模塊 |低|高|中等|從無到低| |影子 DOM |低到中|高|高|低| | JS 中的 CSS |高|中到高|高|從無到高| |順風|高|中等|高|無 | 性能影響很大程度上取決於實施。例如,對於 CSS-in-JS,如果解析和組合在執行時完全完成,您可能會產生很大的影響。如果樣式已經預先解析但僅在執行時組合,則影響可能很小。如果使用像香草精這樣的解決方案,您基本上不會產生任何影響。 對於 Shadow DOM,主要的性能影響可能是 Shadow DOM 內部元素的投影或移動(本質上為零)以及“style”標籤的重新評估。然而,這是相當低的,甚至可能會產生一些性能優勢,給定的樣式總是切中要害,並且僅專用於要在影子 DOM 中顯示的某個元件。 在示例中,我們有以下捆綁包大小: |方法|索引 [kB] |頁碼 [kB] |表 [kB] |總體 [kB] |尺寸 [%] | | ----------------- | ---------- | --------- | ----------- | ------------ | ------ | |默認| 1.719 | 1.719 1.203 | 1.203 0.245 | 0.245 3.167 | 3.167 100% | |大會 | 1.761 | 1.761 1.241 | 1.241 0.269 | 0.269 3.271 | 3.271 103% | | CSS 模塊 | 2.149 | 2.149 2.394 | 2.394 0 | 4.543 | 4.543 143% | |影子 DOM | 10.044 | 10.044 1.264 | 1.264 0 | 11.308 | 11.308 357% | |情感| 1.670 | 1.670 1.632 | 1.632 25.785 | 25.785 29.087 | 29.087 918% | |樣式元件 | 1.618 | 1.618 1.612 | 1.612 63.073 | 63.073 66.303 | 66.303 2093% | |香草精 | 1.800 | 1.800 1.257 | 1.257 0.314 | 0.314 3.371 | 3.371 106% | |順風| 1.853 | 1.853 1.247 | 1.247 0.714 | 0.714 3.814 | 3.814 120% | 對這些數字持保留態度,因為在情感和样式元件的情況下,執行時可以(並且可能甚至應該)共享。另外,給定的示例微前端確實很小(所有 UI 片段的總體大小為 3kB)。對於更大的微前端,增長肯定不會像這裡描述的那麼問題。 Shadow DOM 解決方案的大小增加可以通過我們提供的簡單實用腳本來解釋,該腳本可以輕鬆地將現有的 React 渲染包裝到 Shadow DOM 中(無需生成新樹)。如果這樣的實用程序是集中共享的,那麼其大小將更接近其他更輕量級的解決方案。 ## 結論 在微前端解決方案中處理 CSS 並不困難 - 只需從一開始就以結構化和有序的方式完成,否則就會出現衝突和問題。一般來說,建議選擇 CSS 模塊、Tailwind 或可擴展的 CSS-in-JS 實現等解決方案。

[好文轉載]哇操!你敢信?花式寫todo-list,body裡面一行都沒有也能搞?

[【前端動手玩創意】哇操!你敢信?花式寫todo-list,body裡面一行都沒有也能搞?](https://ithelp.ithome.com.tw/articles/10312056) ## 前情提要 如果說蟑螂怕拖鞋,老鼠愛大米,任何事物都有相生相剋的道理,那麼前端最害怕什麼呢? 恐怕就是todo-list。 這應該是前端的四大惡夢之一,並不是因為太難了,而是因為太簡單而且太無聊了, 不只無聊,還是每次教學都必須寫的一個磨難,簡直是瞬間殺死靈魂中隊前端的愛。 今天要來用不同風格寫出todo-list,把代辦事項寫出新滋味。 body裡面一行都沒有也能搞? 就是可以搞,讓我們馬上來欣賞這段有特別滋味的玩法!ε(*´・∀・`)з゙ ## 程式碼 ``` <!DOCTYPE html> <html> <head> <title>Todo List</title> </head> <body></body> <script> //渲染DOM AddHtml(); //資料內容 function AddHtml() { const Data = { h1: "Todo List", button: "Add", }; //主體的DOM const html = `<h1>${Data.h1}</h1> <input type="text" id="input" /> <button onclick="CheckNull()">${Data.button}</button> <ul id="list"></ul>`; //渲染至畫面上 document.body.innerHTML += html; } //邏輯確認:是否有輸入值 function CheckNull() { if (!input.value.trim("")) { return; } else { AddItem(); } } //新增代辦事項 function AddItem() { const li = `<li>${document.getElementById("input").value}</li>`; document.getElementById("list").innerHTML += li; input.value = ""; } </script> </html> ``` [可以貼到try it的地方執行看看效果](https://www.w3schools.com/jsref/tryit.asp?filename=tryjsref_form_submit) ## 觀念筆記 所謂MVC架構就是模型、視圖、控制,背後其實有一個元素貫串任何程式,那就是:「邏輯」 每當你要下手寫一個程式卻不知道怎麼開始動手, 那就是邏輯沒有通順。 網頁不外乎就是畫面,以及資料,還有使用者的操作。 對於todo-list來說就是一個簡單的input畫面,使用者輸入的資料,點下按鈕送出的操作。 這段程式特地不寫任何一行body裡面的東西, 玩弄了一般人對於程式區分的概念。 用模板符的特性讓所有一切交給變數與函式處理,最後三個function: AddHtml、CheckNull、AddItem完美演繹了代辦清單這個網頁所需要構成的底層邏輯。 必須先加入畫面,然後確認使用者操作的條件,最後新增資料。 這樣子的流程對於新手或許不習慣,但是以邏輯面來看比一般人的寫法清晰許多。 這也是未來框架導入後,對資料流更敏感的培養練習。 ## 心得 無聊的todo-list竟然可以搞成這樣!? 真的是太有趣了(๑╹◡╹๑)! 原來前端的惡夢也可以變成寫不同風格的天堂,沒錯,寫程式就是這麼千變萬化! 重點是背後的邏輯抓到了,那麼想要怎麼寫,真的就是藝術的發揮。 請繼續關注, 跟著我們這個系列繼續動手玩創意!!未來還有更多更新鮮的花招呢(。◕∀◕。)

[前端學習] 用JS動態修改CSS的三種寫法,花式JS玩弄樣式,嚇死你的同事!

## 前情提要 還在寫行內樣式醜醜的嗎?教你不用寫css檔案,使用javascript來動態修改樣式的招數! 這樣子處理的層次又更不一樣, 前端之路有你有我,馬上來看看這是怎麼完成的。 ## 要修改CSS怎麼做呢?物件儲存 首先樣式寫成物件 ```css= const die={ display:"none" } ``` 為什麼要寫成物件呢?修改一種,可能看不出來,修改很多樣式就能感受到, 物件這個儲存方式是有它特別的美感與好處的。 下面我來舉例: ```css= const wrapperStyle = { position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%,-50%)', width: '350px', backgroundColor: '#261d4a', borderRadius: '5px', transition: 'box-shadow .5s ease', overflow: 'hidden' }; ``` ## 語法介紹 Object.assign 使用範例 `Object.assign(element.style,Style)` element是你要修改的元素,Style是樣式寫的object 這是修改一個的情況,當像是上面很多屬性要改的話,我們物件與JS的好處就產生了! 直接使用迴圈來全部套入: ```javascript= const wrapper = document.getElementsByClassName('wrapper'); for (let i = 0; i < wrapper.length; i++) { Object.assign(wrapper[i].style, wrapperStyle); } ``` [MDN官網說明](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) [JS 原力覺醒 Day27 - JS 常用 API - Object.assign && Object.defineProperty](https://ithelp.ithome.com.tw/notifications) ## 還有沒有別的寫法? 有。 ``` document.getElementById("myElement").style.cssText = ` display: block; position: absolute; `; ``` 這樣寫也跟原生css寫起來差不多,比較舒服。 用這種比較分離畫面與程式的寫法, 體感上爽度更高。 就不會是找到元素後還要.style.display="none" 一直.屬性.進去點點點這樣。 ## 說好的三種呢? ``` function setStyle( objId, propertyObject ) { var elem = document.getElementById(objId); for (var property in propertyObject) elem.style[property] = propertyObject[property]; } setStyle('myElement', {'fontsize':'12px', 'left':'200px'}); ``` 這種寫成function的風格我也很愛,感覺很舒服,跟第一個的有點類似卻不太一樣。 這個是把迴圈弄再函式了,所以呼叫一次就會全部修改。 第一個則是把物件內的東西丟給迴圈,一次一次去做改動變化, 邏輯上一模一樣,手段跟風格差異而已唷。 [stackoverflow](https://stackoverflow.com/questions/3968593/how-can-i-set-multiple-css-styles-in-javascript) ## 心得 使用這個小技巧很有趣,之後我們會應用到「JS油猴系列-遮蔽廣告腳本,自定義新增名單」文章裡面。 當要寫腳本,很需要JS動態修改CSS的時候,這種小技巧就佔了很大的角色呢٩(๑•̀ω•́๑)۶! 是不是沒有想到居然可以用JS修改呀! 加上我認為比較差的寫法,就不只三種了呢。 趕快選一種喜歡的, 練習成自己的寶藏知識吧!

在 React 使用 useWorker 來跑多執行緒,大幅改善 UX 效能

使用 useWorker 在單獨的執行緒中,處理昂貴且阻塞 UI 的任務。 眾所周知,Javascript 是一種單線程語言。所以,做任何昂貴的任務,它都會阻塞 UI 互動。用戶需要等到它完成,才能執行剩餘的其他操作,這會帶來糟糕的用戶體驗。 為了克服和執行這些任務,Javascript 有一個名為 [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) 的解決方案,它允許在 web 中執行消耗效能的任務時,瀏覽器不會阻塞用戶界面,使用戶體驗非常流暢。 本篇文章簡單介紹這個 hook。 原文出處:https://dev.to/nilanth/multi-threaded-react-app-using-useworker-gf8 --- ## Web Workers Web Worker 是一個在後台執行而不影響用戶界面的腳本,因為它在一個單獨的線程而不是主線程中執行。所以它不會對用戶交互造成任何阻塞。 Web Worker 主要用於在 Web 瀏覽器中執行昂貴的任務,例如對大量資料進行排序、CSV 導出、圖像處理等。 ![工作線程](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p2ikpn8va49460arsuum.png) 參考上圖,我們可以看到一個昂貴的任務是由一個工作線程並行執行的,而不會阻塞主線程。當我們在主線程中執行相同的操作時,會導致 UI 阻塞。 ## React 中的 Concurrent mode 呢? React [並發模式](https://beta.reactjs.org/reference/react/startTransition) 不會並行執行任務。它將非緊急任務轉移,接著立即執行緊急任務。它使用相同的主線程來處理。 正如我們在下圖中看到的,緊急任務是使用上下文切換來處理的。例如,如果一個表正在呈現一個大型資料集,並且用戶試圖搜尋某些內容,React 會將任務切換為用戶搜尋,並首先處理它,如下所示。 ![任務切換](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7nc8978zlpi8gmrcxlnd.png) 當為同一任務使用 worker 時,表格渲染在一個單獨的線程中並行執行。檢查下圖。 ![反應工作者](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4tnoy22tqchmq8cuavdp.png) ## useWorker [useWorker](https://github.com/alewin/useWorker) 是一個通過 React Hooks 在簡單配置中使用 Web Worker API 的函式庫。它支持在不阻塞 UI 的情況下執行昂貴任務,支援 promise 而不是事件監聽器,還有一些值得注意的功能: 1. 結束超時 worker 的選項 2. 遠程依賴 3. Transferable 4. Worker status > useWorker 是一個 3KB 的庫 ## 為什麼不用 JavaScript 內置 web worker? 在使用 javascript web worker 時,我們需要加入一些複雜的配置,來設置多行程式碼的 worker。使用 useWorker,我們可以簡化 worker 的設置。讓我們在下面的程式碼中,看看兩者之間的區別。 ## 在 React App 中使用 JS Web Worker 時 ``` // webworker.js self.addEventListener("message", function(event) { var numbers = [...event.data] postMessage(numbers.sort()) }); ``` ``` //index.js var webworker = new Worker("./webworker.js"); webworker.postMessage([3,2,1]); webworker.addEventListener("message", function(event) { console.log("Message from worker:", event.data); // [1,2,3] }); ``` ## 在 React App 中使用 useWorker 時 ``` // index.js const sortNumbers = numbers => ([...numbers].sort()) const [sortWorker] = useWorker(sortNumbers); const result = await sortWorker([1,2,3]) ``` 正如我之前所說,與普通的 javascript worker 相比,`useWorker()` 簡化了配置。 讓我們與 React App 整合並執行高 CPU 密集型任務,來看看 useWorker() 的實際執行情況。 ## Quick Start 要將 useWorker() 加入到 React 專案,請使用以下命令 `yarn add @koale/useworker` 安裝套件後,導入 useWorker()。 `import { useWorker, WORKER_STATUS } from "@koale/useworker";` 我們從函式庫中導入 useWorker 和 WORKER_STATUS。 **useWorker()** 鉤子返回 workerFn 和 controller。 1. `workerFn` 是允許使用 web worker 執行函數的函數。 2.controller 由 status 和 kill 參數組成。 status 參數返回 worker 的狀態和用於終止當前執行的 worker 的 kill 函數。 讓我們用一個例子來看看 **useWorker()**。 使用 **useWorker()** 和主線程對大型陣列進行排序 首先建立一個SortingArray元件,加入如下程式碼 ``` //Sorting.js import React from "react"; import { useWorker, WORKER_STATUS } from "@koale/useworker"; import { useToasts } from "react-toast-notifications"; import bubleSort from "./algorithms/bublesort"; const numbers = [...Array(50000)].map(() => Math.floor(Math.random() * 1000000) ); function SortingArray() { const { addToast } = useToasts(); const [sortStatus, setSortStatus] = React.useState(false); const [sortWorker, { status: sortWorkerStatus }] = useWorker(bubleSort); console.log("WORKER:", sortWorkerStatus); const onSortClick = () => { setSortStatus(true); const result = bubleSort(numbers); setSortStatus(false); addToast("Finished: Sort", { appearance: "success" }); console.log("Buble Sort", result); }; const onWorkerSortClick = () => { sortWorker(numbers).then((result) => { console.log("Buble Sort useWorker()", result); addToast("Finished: Sort using useWorker.", { appearance: "success" }); }); }; return ( <div> <section className="App-section"> <button type="button" disabled={sortStatus} className="App-button" onClick={() => onSortClick()} > {sortStatus ? `Loading...` : `Buble Sort`} </button> <button type="button" disabled={sortWorkerStatus === WORKER_STATUS.RUNNING} className="App-button" onClick={() => onWorkerSortClick()} > {sortWorkerStatus === WORKER_STATUS.RUNNING ? `Loading...` : `Buble Sort useWorker()`} </button> </section> <section className="App-section"> <span style={{ color: "white" }}> Open DevTools console to see the results. </span> </section> </div> ); } export default SortingArray; ``` 這裡我們配置了 useWorker 並傳遞了 bubleSort 函數來使用 worker 執行昂貴的排序。 接下來,將以下程式碼加入到 App.js 元件,並導入 SortingArray 元件。 ``` //App.js import React from "react"; import { ToastProvider } from "react-toast-notifications"; import SortingArray from "./pages/SortingArray"; import logo from "./react.png"; import "./style.css"; let turn = 0; function infiniteLoop() { const lgoo = document.querySelector(".App-logo"); turn += 8; lgoo.style.transform = `rotate(${turn % 360}deg)`; } export default function App() { React.useEffect(() => { const loopInterval = setInterval(infiniteLoop, 100); return () => clearInterval(loopInterval); }, []); return ( <ToastProvider> <div className="App"> <h1 className="App-Title">useWorker</h1> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <ul> <li>Sorting Demo</li> </ul> </header> <hr /> </div> <div> <SortingArray /> </div> </ToastProvider> ); } ``` 我們已將 React 徽標加入到 **App.js** 元件,該組件每 **100ms** 旋轉一次,以直觀地表示執行昂貴任務時的阻塞和非阻塞 UI。 執行上面的程式碼時,我們可以看到兩個按鈕 Normal Sort 和 Sort using **useWorker()**。 接下來,單擊 Normal Sort 按鈕在主線程中對陣列進行排序。我們可以看到 React 徽標停止旋轉幾秒鐘。由於排序任務阻塞了UI渲染,所以排序完成後logo又開始旋轉了。這是因為兩個任務都在主線程中處理。檢查以下gif ![正常排序](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k74iuo6qd36qbvpr9iaz.gif) 讓我們使用 [chrome 性能記錄](https://developer.chrome.com/docs/devtools/performance/) 檢查其性能分析。 ![性能正常](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8eclh8a6o7g9xzta03ic.png) 我們可以看到 Event: click 任務用了 **3.86 秒** 來完成這個過程,它阻塞了主線程 3.86 秒。 接下來,讓我們嘗試使用 **useWorker()** 選項進行排序。點擊它的時候我們可以看到 react logo 還在不間斷的旋轉。由於 useWorker 在不阻塞 UI 的情況下在後台執行排序。這使得用戶體驗非常流暢。檢查以下gif ![工人排序](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xe5ci0p3hy5nca4321cg.gif) 您還可以使用 `sortWorkerStatus` 在控制台中看到 worker 狀態為 **RUNNING**、**SUCCESS**。 讓我們看看這種方法的性能分析結果 ![主線程](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bxsl696fld6u79dqpwra.png) ![工作線程](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fcnwe2lczguyxgfqftrj.png) 正如我們所看到的,第一張圖片表示主線程中沒有長時間執行的進程。在第二張圖片中,我們可以看到排序任務由一個單獨的工作線程處理。所以主線程沒有阻塞任務。 您可以在以下沙箱中試用整個範例。 https://codesandbox.io/embed/useworker-sorting-example-041qhc?fontsize=14&hidenavigation=1&theme=dark ## 何時使用worker 1.圖像處理 2. 排序或處理大資料集。 3. 大資料CSV或Excel導出。 4. 畫布繪圖 5. 任何 CPU 密集型任務。 ## useWorker 的限制 1. web worker 無權存取 window 物件和 document。 2. 當 worker 正在執行時,我們不能再次呼叫它,直到它完成或被中止。為了解決這個問題,我們可以建立兩個或多個 useWorker() 鉤子實例。 3. Web Worker 無法返回函數,因為響應是序列化的。 4. Web Workers 受限於最終用戶機器的可用 CPU 內核和內存。 ## 結論 Web Worker 允許在 React 應用程式中使用多線程來執行昂貴的任務而不會阻塞 UI。而 useWorker 允許在 React 應用程式中以簡化的掛鉤方法使用 Web Worker API。 Worker 不應該被過度使用,我們應該只在必要時使用它,否則會增加管理 Worker 的複雜性。

JavaScript 系列五:第1課 ── 學會 Cookie 相關功能

## 課程目標 認識並且能使用 Cookie 相關功能 ## 課程內容 大名鼎鼎的 cookie,很多人在科技新聞上都聽過 某某公司利用網頁 cookie 追蹤個資、某某公司偷偷用 cookie 分享個資 這類新聞提到的 cookie,就是本課在說的 cookie 目前為止的作業,你會發現只要重新整理網頁,畫面上的所有東西就都歸零回預設值了 因為我們說網頁 HTTP 協議是「無狀態」的,也就是每次打開網頁的「狀態」不會被保留 這樣的設計當然有好處以及方便的地方,因為每次打開網頁都是獨立事件、彼此不會互相影響,開發起來單純很多 但是在實務上,經常需要「有狀態」才能完成某些功能,例如「已登入帳號」的狀態,否則網站會變很難用 Cookie 技術就是為了這個原因而誕生 --- Cookie 主要有兩個特性 - cookie 的內容,主機端也能存取 - cookie 的內容,網頁重新整理之後還會保留 Cookie 會出現在 HTTP Request 內,一起送到主機 會員登入功能、各種廣告追蹤功能,就是這樣做到的 HTTP 協議不熟沒關係,反正就是前端工程師設定過 cookie 值之後,後端工程師也能在主機端拿到那些值就對了 --- 注意,由於 cookie 會包含一些網站上的敏感資訊,jsfiddle 等等線上程式碼實驗工具、以及各種免費架站工具,幾乎通通都關閉了 cookie 功能 我目前找到一個可以用 cookie 的架站工具 https://replit.com/ 本課請使用 replit 來跑範例程式碼、寫作業 註冊登入之後,點 Create -> Templates -> 選 HTML, CSS, JS 那個 -> Create Repl 就可以開始寫了,但是右邊的預覽視窗,因為是 iframe,一樣不能用 cookie 請點擊右上方的 Open in a new tab,就可以正常跑 cookie 功能了 --- 來看一些範例程式碼吧 ``` Your Age: <input type="text"> Your Name: <input type="text"> <button onclick="save()">Save</button> <script> if (document.cookie) { alert(document.cookie); var arr = document.cookie.split(';'); var myage = arr[0].split('=')[1]; var myname = arr[1].split('=')[1]; document.querySelectorAll('input')[0].value = myage; document.querySelectorAll('input')[1].value = myname; } function save() { var myage = document.querySelectorAll('input')[0].value; var myname = document.querySelectorAll('input')[1].value; document.cookie = 'myage=' + myage; document.cookie = 'myname=' + myname; alert('saved!'); } </script> ``` 這個範例中,輸入年齡、名字一次,接著重新整理網頁,會發現資料一樣存在欄位裡面,不用重新輸入! 存取值,都是透過 `document.cookie` 這個物件屬性,值的格式是 `name=value` 雖然 `document.cookie=` 寫了兩次,但第二次並不會蓋掉第一次的值,而是兩個都存進 cookie 了 (這個設計,很糟糕,沒錯,讓人看得很混亂。應該是瀏覽器的設計者,有他們的苦衷,我們先不細究原因,先知道怎麼用就好) 然後,讀取的時候,多個值會用分號 `;` 隔開,所以讀取時我用了 `.split()` 這個字串函式 除此之外,cookie 還可以設定「使用期限」以及「限定網域」,但我這邊就先不示範 總之,cookie 的效果大概就是這樣,稍微理解就好。然後這些值,後端工程師在伺服器端,也會收到,就這樣! --- 如上所示,cookie 寫起來,有點麻煩 實務上,每次都這樣手寫,太囉唆,通常會直接找套件來用,我是用這款 https://github.com/js-cookie/js-cookie 用來操作 cookie 的 API,漂亮多了! --- 關於 cookie 的 debug 除錯,也要知道一下 打開瀏覽器開發者工具 -> Application -> Cookies 可以看到網站上有哪些 cookie,也可以直接在這邊修改、刪除 cookie --- cookie 還有一些特性,以及大小限制、使用限制 這邊不細談,稍微知道怎麼用 cookie 就好 有興趣的話,請上網搜尋一下,多研究一些 cookie 的細節 ## 課後作業 請使用 https://replit.com/ 來寫作業 假設你正在幫客戶寫一個「成人限制級網站」 頁面第一次打開會顯示蓋板警告,下方有兩個按鈕 ``` 警告︰您即將進入之網頁內容需滿十八歲方可瀏覽。 根據「兒童及少年福利與權益保障法」規定,本網站已於非闔家皆宜之網頁加以標示。若您尚未年滿十八歲,請點選離開。若您已滿十八歲,亦不可將本站之內容派發、傳閱、出售、出租、交給或借予年齡未滿18歲的人士瀏覽,或將本網站內容向該人士出示、播放或放映。 您年滿十八歲嗎? [離開] [是,我已年滿十八歲] ``` - 點擊離開,把用戶跳轉到 google 首頁 - 點擊確認,就關閉蓋板警告,在網頁上顯示一張美女 or 帥哥圖片:參考圖庫 https://unsplash.com/ - 只要按過確認,重新整理之後,蓋板警告就不會再跳出來 做出以上功能,你就完成這次的課程目標了!

JavaScript 系列一:第6課 ── 認識資料型態與轉換

## 課程目標 學會「數字」資料型態 學會基本的資料型態轉換 ## 課程內容 在之前的課程中,我們學會了宣告字串、以及字串的連接 ``` var str1 = '新年'; var str2 = '快樂'; var str3 = var1 + str2; alert(str3); ``` 這課我們要學「數字」這種資料型態以及一些基本操作 ``` var x = 30; var y = 5; var z1 = x + y; var z2 = x - y; var z3 = x * y; var z4 = x / y; alert(z1); alert(z2); alert(z3); alert(z4); ``` 以上是加減乘除的範例,很簡單吧! 要注意的是,長得像數字的字串,其實依然是字串 ``` var a1 = '100'; var a2 = 100; ``` 上面的 `a1` `a2` 兩個變數,看起來很像,但資料型態不一樣 雖然直接進行字串連接或者加減乘除,程式不會出問題,但那其實是 JavaScript 引擎知道你懶惰,「猜測」你需要型態轉換,自動幫你轉換 但其實手動把型態轉換清楚比較好,比較不會有誤會 比方說,上面的 `a1 + a2` 如果都當成數字,會是 `200`;如果都當成字串,那字串連接會是 `'100100'` 這種情況就很容易造成出錯,所以要養成習慣,把型態轉換清楚 要把數字轉成字串,可以使用「數字」這種資料型態內建的 `.toString()` 函式 ``` var a1 = '100'; var a2 = 100; var a3 = a1 + a2.toString(); alert(a3); ``` 要把字串轉成數字,也有一個內建函式 `parseInt()` 可以使用 ``` var a1 = '100'; var a2 = 100; var a3 = parseInt(a1) + a2; alert(a3); ``` 這些範例都去 jsfiddle 跑跑看,就會很清楚了! --- 使用函式的時候,有時像在直接呼叫一個「函式」,例如 `alert()` `parseInt(a1)` 有時又像在呼叫「物件」的「函式」,例如 `document.getElementById()` `a2.toString()` 你或許會覺得,怎麼不設計成 `a1.toInt()` 跟 `parseString(a2)` 呢? 這些其實是程式語言設計者的主觀喜好、偏好、習慣而已,不重要,反正能跑就好了,你就先用就對了 --- 有件事要提一下,前面的課程,有說到如何取得用戶輸入的內容 ``` var str = document.getElementById('keyword').value; ``` 在 JavaScript 之中,用戶在 html 元素輸入的內容,一律都會是「字串」 就算用戶是輸入阿拉伯數字,在 JavaScript 中拿到值之後,如果需要數學計算的話,要記得把型態轉成數字 ## 課後作業 接續前一課的作業,雖然詳細的報價,你希望在電話中跟對方談,因為要了解客製化細節,才能報價 但是你覺得先在網頁上,顯示初步的報價,可以節省一些時間,比較有效率 這次的作業要用簡單的數學,來初步顯示報價 --- 目前的欄位有顧客名稱、服裝分類、服裝類型 請再增加一個文字輸入欄位,讓客戶能輸入「訂購數量」 為了簡單起見,男裝不論類型,一件都報 600 元;女裝不論類型,一件都報 500 元 在點擊訂購按鈕之後,訂單細節裡面,顯示「初步估價」給客戶看,類似這樣: ``` ---------- |您的訂單    | |顧客姓名:XXX| |服裝分類:XXX| |服裝類型:XXX| |訂購數量:XXX| |初步估價:XXX| ---------- ``` 做出以上功能,你就完成這次的課程目標了!

JavaScript 系列一:第1課 ── 基本的函數操作、onclick 事件

## 課程目標 學會定義函數的基本方法 學會呼叫函數的基本方法 學會 onclick 事件的用法 ## 課程內容 請打開這個網站,用這個網站來練習&寫作業: https://jsfiddle.net 在學 html/css 時,我們會給元素一些屬性,例如 id 與 class 屬性,連結的 href 屬性,圖片的 src 屬性 同樣的語法,還可以用來做一些互動式效果、功能 例如,可以這樣在按鈕上做出「點擊後」觸發的效果 ``` <button onclick="myFunction()">Click me</button> ``` onclick 我們叫「事件屬性」。也是屬性的一種,語法跟一般屬性相同 屬性內容是去執行 `myFunction`,後面的小括號 `()` 代表執行的意思 這邊的 `myFunction` 是所謂的「函數」。名稱是隨便命名的。根據互動的內容,取一個自己看得懂的名稱就行了 小括號裡面可以傳參數,這邊我們沒用到參數,所以直接小括號包起來 `()` 就可以 如何定義 `myFunction` 函數的內容呢?這樣即可 ``` function myFunction() { alert('你點擊了按鈕!'); alert('這是很好的開始!'); } ``` 這個函數中,使用了 JavaScript 在瀏覽器中內建的 `alert` 函數。這函數會根據參數,跳出小視窗,把參數顯示在畫面上 在 JavaScript 中,一段動作的結尾都會加上分號 `;` 用來代表這段結束 分號其實可以省略不寫,程式一樣會正常執行,但加上分號,看起來更清楚一點點 有點像寫文章時結尾的「句點」,有人習慣寫,有人經常不寫,但加了比較清楚 請在 jsfiddle 試試看上面的範例,把按鈕寫在 html 區塊,把函數定義寫在 JavaScript 區塊,實驗一下、玩玩看! ## 課後作業 假設你家裡是開成衣批發工廠的,你用 html/css 寫了一個工廠的介紹網頁 在「線上下單」頁面,你希望引導客戶聯絡你,你親口介紹、聊過才報價、提高成交率 你不想把商品報價、聯絡方式直接顯示在畫面上,你希望是點擊按鈕之後,才顯示聯絡資訊 這次作業,需要你實作這個功能。 --- 請使用 jsfiddle,做一個「我要訂購」按鈕 點擊這個按鈕,會跳出 alert 訊息顯示「謝謝您對我們的衣服有興趣!請致電 0987-654-321,會有專人提供您報價!」 接著做一個「認識工廠」按鈕 點擊這個按鈕,會跳出 alert 訊息顯示「我們工廠位於新北市,通過國際 ISO9001 認證,品質讓您放心!」 請稍微替這兩個按鈕加一點 css 屬性,弄得漂亮一點,漂亮的按鈕,會讓客戶更想點 做出這兩個按鈕,你就完成這次的課程目標了!

在 React 中使用 Design Patterns:以 Strategy Pattern 舉例

在 React 前端開發時,常常會需要在不同的元件、hook、utils 中寫一些邏輯。 有些時候,使用策略模式會很有幫助,這篇文章給您參考。 - 原文出處:https://dev.to/itshugo/applying-design-patterns-in-react-strategy-pattern-enn ## 出問題了:霰彈槍手術(Shotgun Surgery) Shotgun Surgery 是一種程式寫很爛的信號。想對程式規格做一點小修改,需要改一大堆地方。 ![](https://refactoring.guru/images/refactoring/content/smells/shotgun-surgery-01-2x.png) 在專案中通常如何發生?假設我們需要為產品寫一個報價卡片,我們根據客戶所在國家,調整價格、貨幣、折扣方式和文字說明: ![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5iz9oso7ozaqm0qibd76.png) 大多數工程師可能會這樣寫: - 元件:`PricingCard`、`PricingHeader`、`PricingBody`。 - Utility functions:`getDiscountMessage`(在 **utils/discount.ts** 中),`formatPriceByCurrency`(在 **utils/price.ts** 中)。 - `PricingBody` 元件會計算最終價格。 完整程式碼範例:https://codesandbox.io/s/react-strategy-pattern-problem-h59r02?from-embed 現在假設我們需要更改一個國家/地區的定價計劃,或為另一個國家/地區添加新的定價計劃。您將如何處理這段修改?您必須至少修改 3 個地方,並向已經滿亂的 `if-else` 區塊添加更多條件: - 修改 `PricingBody` 組件。 - 修改 `getDiscountMessage` 函數。 - 修改 `formatPriceByCurrency` 函數。 如果您聽說過 S.O.L.I.D 原則,我們已經違反了前 2 條原則:單一職責原則和開閉原則。 ## 解決方法:策略模式 策略模式非常簡單。我們可以簡單的理解為,每個國家的定價方案都是一個策略。在那個策略類別中,會實現該策略的所有相關邏輯。 假設您熟悉 OOP,我們可以有一個實現共享/公共邏輯的抽像類別(`PriceStrategy`),然後具有不同邏輯的策略將繼承該抽像類別。 `PriceStrategy` 抽像類別如下所示: ``` import { Country, Currency } from '../../types'; abstract class PriceStrategy { protected country: Country = Country.AMERICA; protected currency: Currency = Currency.USD; protected discountRatio = 0; getCountry(): Country { return this.country; } formatPrice(price: number): string { return [this.currency, price.toLocaleString()].join(''); } getDiscountAmount(price: number): number { return price * this.discountRatio; } getFinalPrice(price: number): number { return price - this.getDiscountAmount(price); } shouldDiscount(): boolean { return this.discountRatio > 0; } getDiscountMessage(price: number): string { const formattedDiscountAmount = this.formatPrice( this.getDiscountAmount(price) ); return `It's lucky that you come from ${this.country}, because we're running a program that discounts the price by ${formattedDiscountAmount}.`; } } export default PriceStrategy; ``` 我們只需將實例化的策略作為 prop 傳遞給 PricingCard 元件: ``` <PricingCard price={7669} strategy={new JapanPriceStrategy()} /> ``` `PricingCard` 的 props 定義為: ``` interface PricingCardProps { price: number; strategy: PriceStrategy; } ``` 同樣,如果您了解 OOP,那麼我們不僅在使用繼承,而且還在此處使用多態性(Polymorphism)。 完整程式碼範例:https://codesandbox.io/s/react-strategy-pattern-solution-mm0cvm?from-embed 使用這個解決方案,我們只需要添加一個新的策略類別,而不需要修改任何現有程式碼。這樣,我們也滿足了 S.O.L.I.D 原則。 ## 結論 因為我們在 React 程式碼中檢測到程式碼發臭:Shotgun Surgery,所以我們使用了策略模式來解決它。 ### Before ![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7wn31uolfo6xfy3mh8fo.png) ### After ![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xheeb2splcwj2kkqwsfo.png) 現在邏輯都存在一個地方,不再分佈在許多地方。 --- 以上是 Strategy Pattern 的簡單說明,希望對您有幫助。

寫出更好 JavaScript 的幾個實務技巧:新手推薦

JavaScript 寫起來有很大彈性,但如何改善自己的 JavaScript 品質呢?以下是一份實用的參考指南。 - 原文出處:https://dev.to/taillogs/practical-ways-to-write-better-javascript-26d4 # 使用 TypeScript 改善 JS 的第一個技巧就是,不要寫 JS。寫 TypeScript 吧。它是微軟寫的一個語言,算是 JS 的母集合(一般的 JS 可以直接在 TS 裡面跑)。TS 在 JS 之上添加了一個全面的型別系統。長期下來,整個前端生態系,幾乎都支援 TS 語言了。下面介紹一下 TS 的優點。 **TypeScript 可以確保「型別安全」** 型別安全代表編譯器驗證了所有型別是否以「合法」的方式被使用。換句話說,如果你創建一個接受「數字」的函數 `foo`: ``` function foo(someNum: number): number { return someNum + 5; } ``` 該「foo」函數呼叫時要傳一個數字: 正確用法 ``` console.log(foo(2)); // prints "7" ``` 錯誤用法 ``` console.log(foo("two")); // invalid TS code ``` 除了需要花時間加型別以外,沒有其他缺點了。好處則是顯而易見。型別安全針對常見錯誤提供了額外預防,這對於像 JS 這樣鬆散的語言來說,是一件好事。 **TypeScript 的型別系統,讓你能重構大型應用程式** 重構大型 JS 應用程式是一場噩夢。主要是因為它不會檢查函數簽名。也就是說,JS函數隨便呼叫,編譯器都不會先報錯。舉個例,如果我有一個函數「myAPI」,由 1000 個不同的服務使用: ``` function myAPI(someNum, someString) { if (someNum > 0) { leakCredentials(); } else { console.log(someString); } } ``` 我稍微更改了呼叫簽名: ``` function myAPI(someString, someNum) { if (someNum > 0) { leakCredentials(); } else { console.log(someString); } } ``` 我必須自已 100% 去確認,使用此功能的1000 個地方,都有正確更新用法。漏掉的地方,在執行時就會壞掉。在 TS 中相同的情境如下: before ``` function myAPITS(someNum: number, someString: string) { ... } ``` after ``` function myAPITS(someString: string, someNum: number) { ... } ``` 如您所見,「myAPITS」函數跟在 JavaScript 中一樣修改。但是,這段程式碼無法順利產出 JavaScript,而是會出現無效 TypeScript 的錯誤提示,因為用到它的1000個地方,現在型別就出錯了。正是因為「型別安全」,這 1000 個案例會阻止編譯。 **TypeScript 使團隊溝通架構更容易** 正確設定 TS 後,要先定義介面跟類型,不然很難寫程式碼。這也順便提供了一種簡潔、可溝通的架構提案方法。例如,如果我想跟後端提出新的「請求」類型,可以使用 TS 將以下內容提交給團隊。 ``` interface BasicRequest { body: Buffer; headers: { [header: string]: string | string[] | undefined; }; secret: Shhh; } ``` 雖然也是要寫一點程式碼,但可以先提供以上內容作為初步想法、取得回饋,而不用一次設計完功能。像這樣強迫開發者先定義介面跟 API,能讓程式碼品質更好。 總體而言,TS 已經發展成為一種成熟且更可預測的 vanilla JS 替代品。當然還是要熟 JS,但我現在大多數新專案都是直接寫 TS。 # 使用現代功能 JavaScript是世界上最流行的程式設計語言之一。你可能會以為這語言已經被百萬開發者摸透了,其實不是。很多新功能是近期才加上去的。 **`async` 與 `await`** 長期以來,非同步、事件驅動的 callback 是 JS 開發中不可避免的: 傳統 callback ``` makeHttpRequest('google.com', function (err, result) { if (err) { console.log('Oh boy, an error'); } else { console.log(result); } }); ``` 上述程式碼會有越寫越多層的問題。JS 添加了一個新概念叫 Promises,可以避免嵌套問題。 Promises ``` makeHttpRequest('google.com').then(function (result) { console.log(result); }).catch(function (err) { console.log('Oh boy, an error'); }); ``` 與 callback 相比,Promise 的最大優勢是可讀性和可鏈性。 雖然 Promise 很棒,但它們仍然有一些不足之處。寫起來感覺有點怪。為了解決這個問題,ECMAScript 委員會添加一種使用 promise 的新方法,`async` 和 `await`: `async` 與 `await` ``` try { const result = await makeHttpRequest('google.com'); console.log(result); } catch (err) { console.log('Oh boy, an error'); } ``` 需要注意的是,`await` 的內容必須先被宣告為 `async`: 以前面 makeHttpRequest 舉例 ``` async function makeHttpRequest(url) { // ... } ``` 也可以直接 `await` 一個 Promise,因為 `async` 函數實際上只是 Promise 比較花哨的包裝寫法。這也意味著,`async/await` 代碼和 Promise 代碼在功能上是等價的。因此,請大方使用 `async/await` 吧。 **`let` and `const`** 以前大家寫 JS 都只能用 `var`。`var` 的變數作用域有一些獨特規則,一直讓人很困惑。從 ES6 開始,有了 `const` 跟 `let` 可以替代使用,幾乎不用再寫 `var` 了。 至於何時要用 const 何時要用 let 呢?永遠從 const 先開始使用。const 因為不可修改、更可預期,會讓程式碼品質更好。使用 let 的場景比較少,我寫 const 的頻率比 let 高 20 倍左右。 **箭頭 `=>` 函數** 箭頭函數是在JS中宣告匿名函數的簡潔方法。通常作為 callback 或 event hook 傳遞。 傳統的匿名 function ``` someMethod(1, function () { // has no name console.log('called'); }); ``` 在大多數情況下,這種風格沒有任何“不對”。但它的變數作用域很“有趣”,常導致許多意想不到的錯誤。有了箭頭函數之後,就沒這問題了。 匿名箭頭函數 ``` someMethod(1, () => { // has no name console.log('called'); }); ``` 除了更簡潔之外,箭頭函數還具有更實用的變數範圍界定。箭頭函數從定義它們的作用域繼承「this」。 在某些情況下,箭頭函數甚至可以更簡潔: ``` const added = [0, 1, 2, 3, 4].map((item) => item + 1); console.log(added) // prints "[1, 2, 3, 4, 5]" ``` 箭頭函數可以包括隱式的「return」語句。就不用寫大括弧、分號。 不過,這跟“var”被完全取代不同。傳統匿名函數仍然有需要的地方,例如類別方法定義。話雖如此,如果你總是預設使用箭頭函數,程式碼錯誤會少很多。 **展開運算子 `...`** 提取一個物件的鍵/值對,並將它們添加為另一個物件的子物件,是一種很常見的場景。從歷史上看,有幾種方法可以實現,但通通都很笨拙: ``` const obj1 = { dog: 'woof' }; const obj2 = { cat: 'meow' }; const merged = Object.assign({}, obj1, obj2); console.log(merged) // prints { dog: 'woof', cat: 'meow' } ``` 這寫法以前很常見,也很囉唆。多虧了「展開運算子」,再也不需要那樣寫了: ``` const obj1 = { dog: 'woof' }; const obj2 = { cat: 'meow' }; console.log({ ...obj1, ...obj2 }); // prints { dog: 'woof', cat: 'meow' } ``` 最棒的是,與陣列也可無縫接軌: ``` const arr1 = [1, 2]; const arr2 = [3, 4]; console.log([ ...arr1, ...arr2 ]); // prints [1, 2, 3, 4] ``` 它可能不是近期 JS 中最重要的功能,但它是我的最愛之一。 **範本文字(範本字串)** 字串處理太常見了,程式語言要能處理範本字串,才夠好用。處理動態內容、多行文字時,都會需要它: ``` const name = 'Ryland'; const helloString = `Hello ${name}`; ``` 我認為程式碼不言自明。看起來好多了。 **物件解構** 物件解構是一種從資料集合(物件、陣列等)中提取值,而無需反覆運算數據或顯式訪問其鍵的方法: 老方法 ``` function animalParty(dogSound, catSound) {} const myDict = { dog: 'woof', cat: 'meow', }; animalParty(myDict.dog, myDict.cat); ``` 新方法 ``` function animalParty(dogSound, catSound) {} const myDict = { dog: 'woof', cat: 'meow', }; const { dog, cat } = myDict; animalParty(dog, cat); ``` 不只這樣。您還可以在函數的簽名中定義解構: 解構範例二 ``` function animalParty({ dog, cat }) {} const myDict = { dog: 'woof', cat: 'meow', }; animalParty(myDict); ``` 它也適用於陣列: 解構範例三 ``` [a, b] = [10, 20]; console.log(a); // prints 10 ``` --- 以上,希望對您有幫助!

20 個一行 JavaScript 就能完成的小任務

一行程式碼可以做到很多事。這邊有20個小任務都用一行就可以完成,給您參考! - 原文出處:https://dev.to/saviomartin/20-killer-javascript-one-liners-94f # 獲取瀏覽器 Cookie 的值 讀取 `document.cookie` 來查 cookie 的值 ``` const cookie = name => `; ${document.cookie}`.split(`; ${name}=`).pop().split(';').shift(); cookie('_ga'); // Result: "GA1.2.1929736587.1601974046" ``` # 將 RGB 轉換為十六進制 ``` const rgbToHex = (r, g, b) => "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); rgbToHex(0, 51, 255); // Result: #0033ff ``` # 複製到剪貼板 使用 `navigator.clipboard.writeText` 輕鬆將任何文字複製到剪貼板。 ``` const copyToClipboard = (text) => navigator.clipboard.writeText(text); copyToClipboard("Hello World"); ``` # 檢查日期是否有效 使用以下程式碼檢查給定日期是否有效。 ``` const isDateValid = (...val) => !Number.isNaN(new Date(...val).valueOf()); isDateValid("December 17, 1995 03:24:00"); // Result: true ``` # 查找一年中的第幾天 根據給定日期,找出是第幾天。 ``` const dayOfYear = (date) => Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24); dayOfYear(new Date()); // Result: 272 ``` # 將字串開頭大寫 Javascript 沒有內建的 capitalize 函數。我們可以使用以下程式碼來完成。 ``` const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1) capitalize("follow for more") // Result: Follow for more ``` # 求兩天之間的天數 使用以下程式碼查找 2 個給定日期之間的天數。 ``` const dayDif = (date1, date2) => Math.ceil(Math.abs(date1.getTime() - date2.getTime()) / 86400000) dayDif(new Date("2020-10-21"), new Date("2021-10-22")) // Result: 366 ``` # 清除所有 Cookie 透過 document.cookie 存取 cookie 並清除它,就可輕鬆清除網頁中的所有 cookie。 ``` const clearCookies = document.cookie.split(';').forEach(cookie => document.cookie = cookie.replace(/^ +/, '').replace(/=.*/, `=;expires=${new Date(0).toUTCString()};path=/`)); ``` # 生成隨機十六進制顏色碼 使用“Math.random”和“padEnd”屬性,生成隨機的十六進制顏色碼。 ``` const randomHex = () => `#${Math.floor(Math.random() * 0xffffff).toString(16).padEnd(6, "0")}`; console.log(randomHex()); // Result: #92b008 ``` # 從陣列中刪除重複項 使用 JavaScript 中的 `Set` 輕鬆刪除重複項。 ``` const removeDuplicates = (arr) => [...new Set(arr)]; console.log(removeDuplicates([1, 2, 3, 3, 4, 4, 5, 5, 6])); // Result: [ 1, 2, 3, 4, 5, 6 ] ``` # 從 URL 獲取查詢參數 您可以從 `window.location` 或原始 URL `goole.com?search=easy&page=3` 中輕鬆找出查詢參數 ``` const getParameters = (URL) => { URL = JSON.parse('{"' + decodeURI(URL.split("?")[1]).replace(/"/g, '\\"').replace(/&/g, '","').replace(/=/g, '":"') +'"}'); return JSON.stringify(URL); }; getParameters(window.location) // Result: { search : "easy", page : 3 } ``` # 把日期物件轉成時間 把日期物件以“hour::minutes::seconds”格式轉成時間。 ``` const timeFromDate = date => date.toTimeString().slice(0, 8); console.log(timeFromDate(new Date(2021, 0, 10, 17, 30, 0))); // Result: "17:30:00" ``` # 檢查數字是偶數還是奇數 ``` const isEven = num => num % 2 === 0; console.log(isEven(2)); // Result: True ``` # 求數的平均值 使用 `reduce` 方法計算多個數字之間的平均值。 ``` const average = (...args) => args.reduce((a, b) => a + b) / args.length; average(1, 2, 3, 4); // Result: 2.5 ``` # 滾動到頂部 您可以使用 `window.scrollTo(0, 0)` 方法自動滾動到頂部。將 `x` 和 `y` 都設為 0。 ``` const goToTop = () => window.scrollTo(0, 0); goToTop(); ``` # 反轉字串 您可以使用 `split`、`reverse` 和 `join` 方法輕鬆反轉字串。 ``` const reverse = str => str.split('').reverse().join(''); reverse('hello world'); // Result: 'dlrow olleh' ``` # 檢查陣列是否為空 ``` const isNotEmpty = arr => Array.isArray(arr) && arr.length > 0; isNotEmpty([1, 2, 3]); // Result: true ``` # 取得選中的文字 使用內建的 getSelection 屬性取得用戶選取中的文字。 ``` const getSelectedText = () => window.getSelection().toString(); getSelectedText(); ``` # 打亂陣列 使用 `sort` 和 `random` 方法打亂陣列。 ``` const shuffleArray = (arr) => arr.sort(() => 0.5 - Math.random()); console.log(shuffleArray([1, 2, 3, 4])); // Result: [ 1, 4, 3, 2 ] ``` # 檢測深色模式 使用以下程式碼,檢查用戶的設備是否處於深色模式。 ``` const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches console.log(isDarkMode) // Result: True or False ``` --- 希望這些程式碼,有給您一些靈感!

10 個現代 JavaScript 中的好用語法:新手推薦

在 ES6 版本之後,Javascript 多了很多好用的語法,建議新手一定要學起來這幾招。 - 原文出處:https://dev.to/azure/modern-javascript-10-things-you-should-be-using-starting-today-1adm # -1- 展開運算子 Spred operator 在物件或陣列前面寫 ... 可以很輕鬆地把資料展開,舉例來說: ## Spread array ``` let firstHalf = [ 'one', 'two']; let secondHalf = ['three', 'four', ...firstHalf]; ``` 寫起來非常簡潔。不然原本要這樣寫: ## NO Array spread ``` let firstHalf = [ 'one', 'two']; let secondHalf = ['three', 'four']; for(var i=0, i <firstHalf.length; i++ ) { secondHalf.push(firstHalf[i]); } ``` 用在物件也可以,能夠合併屬性: ## Spread object ``` const hero = { name: 'Xena - Warrior Princess', realName: 'Lucy Lawless' } const heroWithSword = { ...hero, weapon: 'sword' } ``` 不然的話,就要用迴圈來跑全部屬性: ## NO Object spread ``` let keys = Object.keys(hero); let obj = {}; for(var i=0; i< keys.length; i++) { obj[keys[i]] = keys[props[i]]; } ``` 舊寫法其實有 Object.assign() 可以用,看起來會像: ``` const heroWithSword = Object.assign({}, hero, {weapon:"sword"}) ``` 但展開運算子還是簡潔多了: ``` const heroWithSword = { ...hero, weapon: 'sword' } ``` # -2- 其餘參數 Rest parameter 其餘參數能夠將剩下的參數蒐集到一個陣列中。JavaScript 有足夠彈性可以處理這些參數。通常會這樣寫: ``` function add(first, second, ...remaining) { return first + second; } ``` 以上只是加總前兩個參數,也就是 add(1,2) 或 add(1,2,3, 4) 結果一樣。接著這樣改寫: ``` function add(first, second, ...remaining) { return first + second + remaining.reduce((acc, curr) => acc + curr, 0); } ``` 這樣就會把全部參數都加總在一起了。 如您所見,前面加上 ... 就可以搜集其餘參數。這是一種命名變數的方式而已,比較少人知道這招,但可以知道一下。 # -3- 字串插值 String interpolation 看過這種寫法嗎? ``` class Product { constructor(name, description, price) { this.name = name; this.description = description; this.price = price; } getDescription() { return " Full description \n" + " name: " + this.name + " description: " + this.description } } ``` 看看那個 getDescription() 方法,又長、跨行、又難讀。在大多數程式語言都只能這樣寫。幸好,有些程式語言支援字串插值,包括 JavaScript,所以可以改寫如下: ``` getDescription() { return `Full description \n: name: ${this.name} description ${this.description} `; } ``` 反引號 ` 可以定義多行字串。再使用 ${} 進行插值。是不是好多了呢:) # -4- 屬性縮寫 Shorthand properties 雖然還沒搞懂,你可能早就在用了。在 ES5 需要這樣寫: ``` function createCoord(x, y) { return { x: x, y: y } } ``` 在 ES6 之後,如果 : 後面名稱一樣,可以直接省略,像這樣: ``` function createCoord(x, y) { return { x, y } } ``` 簡潔多了吧? # -5- 方法屬性 Method properties 在物件裡面指向方法的寫法。以下是 ES5 寫法: ``` const math = { add: function(a,b) { return a + b; }, sub: function(a,b) { return a - b; }, multiply: function(a,b) { return a * b; } } ``` 在 ES6 之後可以少寫一大堆,像這樣: ``` const math = { add(a,b) { return a + b; }, sub(a,b) { return a - b; }, multiply(a,b) { return a * b; } } ``` # -6- 解構 Destructuring 解構用得好,有益身心健康。 ## Object destructuring 這段: ``` function handle(req, res) { const name = req.body.name; const description = req.body.description; const url = req.url; log('url endpoint', url); // lots of logic happening dbService.createPerson( name, description ) } ``` 有點難讀吧,但要從多層深度挖資料,就會寫成這樣。還能怎麼辦?其實有以下妙招喔: ``` function handle(req, res) { const { body: { name, description }, url }, = req; log('url endpoint', url); // lots of logic happening dbService.createPerson( name, description ) ``` 變成俐落的一行了。 ## Array destructuring 不只物件能用,陣列也可以用,像這段: ``` const array = [1,2,3,4,5,6]; const a = array[0]; const c = array[2]; ``` 可以更優雅地改寫成這樣: ``` const array = [1,2,3,4,5,6]; const [a, ,c, ...remaining] = array; // remaining = [4,5,6] ``` 上面的模式配對,就可以拆出我們需要的變數。如果要跳過某值,就寫 , , 即可。我另外多寫了一個 rest parameter 來取得其餘資料。 ## Parameter matching 函數的參數也能這樣寫。當函數有超過 2-3 個參數時,業界慣例常常是這樣寫: ``` function doSomething(config) { if(config.a) { ... } if(config.b) { ... } if(config.c) { ... } } ``` 更漂亮的寫法其實是: ``` function doSomething({ a, b, c }) { if(a) { ... } if(b) { ... } if(c) { ... } } ``` # -7- 陣列方法 ES6 有大量好用的陣列方法,例如: - find(),回傳符合條件的項目,否則回傳 null - findIndex(),尋找項目的索引 - some(), 確認是否包含符合條件的項目 - includes(), 確認是否包含某個項目 舉例說明如下: ``` const array = [{ id: 1, checked: true }, { id: 2 }]; arr.find(item => item.id === 2) // { id: 2 } arr.findIndex(item => item.id === 2) // 1 arr.some(item => item.checked) // true const numberArray = [1,2,3,4]; numberArray.includes(2) // true ``` # -8- Promises + Async/Await 在只有 callback 能寫的年代,這種寫法很常見: ``` function doSomething(cb) { setTimeout(() => { cb('done') }, 3000) } doSomething((arg) => { console.log('done here', arg); }) ``` 用來寫非同步任務時,就會這樣寫。但現在我們有 promise 能寫了,所以可以這樣: ``` function doSomething() { return new Promise((resolve, reject) => { setTimeout(() => { resolve('done') }, 3000) }) } doSomething().then(arg => { console.log('done here', arg); }) ``` 多個流程還能整個串起來,就像這樣: ``` getUser() .then(getOrderByUser) .then(getOrderItemsByOrder) .then(orderItems => { // do something with order items }) ``` ## Async/await 接著還有 async/await 可以用。上方的範例,相關 promise 變成: ``` async function getItems() { try { const user = await getUser(); const order = await getOrderByUser(user); const items = await getOrderItemsByOrder(order); return items; } catch(err) { // handle error here, the suggestion to return something or rethrow } } getItems().then(items => { // do something with order items }) ``` 看起來像是同步,但其實非同步的一段程式碼:) # -9- 模塊 Modules 幾乎所有程式語言都支援模塊功能。將程式碼分為多個檔案,各自為獨立單元,就是模塊了。像這樣: ``` // math.js export function add(a,b) { return a + b; } export function sub(a,b) { return a - b; } export default (a,b) => a * b; // main.js import mult, { add, sub } from './math'; mult(2, 4) // 8 add(1,1) // 2 sub(1,2) // -1 ``` 使用 export 關鍵字來表示 add 跟 sub 是公開可用的。export default 代表不指定函數時直接導入的內容。在 main.js 中,將預設導入命名為 mult,然後指定導入函數 add 和 sub。 # -10- 箭頭函數 + this 整篇文章一直在用箭頭函數,其實只是另一種函數的寫法而已。以前要這樣寫: ``` function printArray(arr) { // do something } ``` 現在可以這樣寫: ``` const printArray = (arr) => { // do something } ``` ## 單行函數 函數還可以用一行寫完: ``` const add = (a,b) => a + b ``` 這種寫法可以省略 return 直接回傳結果。要回傳物件也可以,這樣即可: ``` const create = (a,b) = > ({ x: a, y: b }) ``` ## Lexical this 以前常常會搞混 this 代表什麼,舉例來說: ``` let array = [1,2,3]; function sum() { this.total = 0; arr.forEach(function(item) { this.total+= item; // `this` is the inner functions `this`, BAD }) return total; } ``` 這個例子中,this 在 forEach 指錯地方了。以前的解法是: ``` function sum() { this.total = 0; var self = this; arr.forEach(function(item) { self.total+= item; // now we are using `self`, it solves it but feels hacky }) return total; } ``` 雖然多寫個 self 變數可以解決,但實在很鳥。新的箭頭函數寫法則解決了這問題: ``` function sum() { this.total = 0; arr.forEach((item) => { this.total+= item; // all is well `this` points to outer function }) return total; } ``` # 結論 ES6 之後有很多好東西可以用,今天先介紹一些,希望對大家有幫助!

一行 JavaScript 就能完成的 7 個小任務

一行程式碼有時可以做到很多事。這邊有七個小任務都用一行就可以完成,給您參考! - 原文出處:https://dev.to/ruppysuppy/7-killer-one-liners-in-javascript-one # 洗亂陣列 在需要將陣列隨機打亂的時候,這行會很好用。 ``` const shuffleArray = (arr) => arr.sort(() => Math.random() - 0.5); // Testing const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; console.log(shuffleArray(arr)); ``` # 複製到剪貼板 在寫網站功能的時候,有時會需要這個功能。 ``` const copyToClipboard = (text) => navigator.clipboard?.writeText && navigator.clipboard.writeText(text); // Testing copyToClipboard("Hello World!"); ``` # 不重複元素 每種語言都有實作自己的 Hash List ,在 JavaScript 中,叫做 Set。可以使用 Set Data Structure 輕鬆地找出不重複元素。 ``` const getUnique = (arr) => [...new Set(arr)]; // Testing const arr = [1, 1, 2, 3, 3, 4, 4, 4, 5, 5]; console.log(getUnique(arr)); ``` # 檢測深色模式 深色模式日益流行,如果用戶在他們的設備中啟用了深色模式,最好將您的應用程序也切換到深色模式。 ``` const isDarkMode = () => window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches; // Testing console.log(isDarkMode()); ``` # 滑到頂部 新手常常發現滑動元素很難做。最簡單的方法是使用 scrollIntoView。加上 behavior 可以讓動畫更流暢。 ``` const scrollToTop = (element) => element.scrollIntoView({ behavior: "smooth", block: "start" }); ``` # 滑到底部 一樣使用 scrollIntoView 就可以,只需要將 block 值改成 end ``` const scrollToBottom = (element) => element.scrollIntoView({ behavior: "smooth", block: "end" }); ``` # 產生隨機顏色 如果您需要隨機色碼,可以參考這段: ``` const generateRandomHexColor = () => `#${Math.floor(Math.random() * 0xffffff).toString(16)}`; ``` --- 謝謝閱讀,希望對您有幫助!