<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Frontend · Nhân Tài Đức</title>
    <link>https://jared.lynskey.co.nz/vi/tags/frontend/</link>
    <description>Những câu chuyện ngắn về kỹ thuật phần mềm và lãnh đạo đội ngũ kỹ sư — performance review, tuyển dụng và mở rộng team, CI/CD, giám sát DevOps và AI agent. Viết từ Seoul.</description>
    <generator>Hugo</generator>
    <language>vi</language>
    <copyright>© 2026 Nhân Tài Đức</copyright>
    <lastBuildDate>Wed, 28 Oct 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/vi/tags/frontend/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Orval: sinh mã máy khách API (API client) có định kiểu và mock từ đặc tả OpenAPI</title>
      <link>https://jared.lynskey.co.nz/vi/posts/2024/2024-04-10-openapitools/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/vi/posts/2024/2024-04-10-openapitools/</guid>
      <pubDate>Wed, 10 Apr 2024 00:30:03 +0000</pubDate>
      <dc:creator>Me</dc:creator>
      <category>API Development</category>
      <category>Orval</category>
      <category>OpenAPI</category>
      <category>TypeScript</category>
      <category>React Query</category>
      <category>MSW</category>
      <category>Frontend</category>
      <category>Backend</category>
      <category>Teamwork</category>
      <category>Efficiency</category>
      <description>Orval đọc bản đặc tả OpenAPI rồi sinh ra mã máy khách TypeScript có định kiểu, các hook React Query và mock MSW, để không ai phải tự tay viết lớp gọi API nữa. Cách cài đặt, cách phần mock hoạt động, và Orval so với openapi-generator, openapi-typescript và Hey API ra sao.</description>
      <content:encoded><![CDATA[<p>Orval là một công cụ sinh mã (code generator): nó đọc bản đặc tả OpenAPI (Swagger) rồi viết sẵn lớp gọi API cho frontend của bạn. Bạn có một hàm có định kiểu hoặc một hook React Query cho từng endpoint, các mô hình dữ liệu TypeScript khớp chính xác với hợp đồng API (contract), và nếu bật thêm thì có cả các handler mock của MSW trả về dữ liệu giả do Faker tạo ra. Bạn chỉ cần commit một tệp cấu hình, chạy <code>orval</code>, rồi chạy lại mỗi khi đặc tả thay đổi.</p>
<p>Ở bài đầu tiên trong loạt bài này, tôi đã giải thích vì sao các đội nên thống nhất một bản hợp đồng API theo chuẩn OpenAPI trước khi ai bắt tay vào viết mã. Bài này nói về công cụ giúp bản hợp đồng đó thực sự phát huy tác dụng.</p>
<p>Tự tay viết lớp gọi API cho frontend là một trong những việc chẳng ai dám nhận là mình thích. Bạn chép các kiểu dữ liệu (type) từ tài liệu của backend, viết đi viết lại hàm bọc (wrapper) cho yêu cầu lần thứ bốn mươi, rồi ba tuần sau có ai đó đổi tên một trường và thế là một nửa số chỗ gọi API của bạn âm thầm hỏng. Orval xóa sổ luôn công việc đó. Chỉ cần trỏ nó tới bản đặc tả (spec) OpenAPI, Orval sẽ sinh mã máy khách cho bạn: hàm có định kiểu cho từng endpoint, mô hình dữ liệu khớp chính xác với hợp đồng, và cả dữ liệu giả lập (mock) để bạn phát triển trước khi backend kịp làm xong.</p>

<h2 class="relative group">Vì sao đội frontend thích Orval
    <div id="vì-sao-đội-frontend-thích-orval" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#v%c3%ac-sao-%c4%91%e1%bb%99i-frontend-th%c3%adch-orval" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Phần sinh dữ liệu giả lập là phần bị đánh giá thấp nhất. Một khi đặc tả đã chốt, frontend có thể dựng một máy chủ giả rồi làm toàn bộ giao diện dựa trên nó, thay vì ngồi chờ những endpoint lúc nào cũng &ldquo;sắp xong rồi&rdquo;. Khi backend thật được triển khai, bạn chỉ việc trỏ máy khách sang đó và gần như mọi thứ chạy được luôn, vì cả hai bên đều làm dựa trên cùng một tài liệu.</p>
<p>Cái lợi còn lại là phần mã nhàm chán giờ đúng ngay từ khi được sinh ra. Không còn ai gõ nhầm tên trường trong phần thân yêu cầu (request body) tự viết, vì đơn giản là chẳng còn ai phải tự viết phần đó nữa.</p>

<h2 class="relative group">Vì sao đội backend cũng thích
    <div id="vì-sao-đội-backend-cũng-thích" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#v%c3%ac-sao-%c4%91%e1%bb%99i-backend-c%c5%a9ng-th%c3%adch" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Khi bản đặc tả là nguồn dữ liệu gốc duy nhất, những trao đổi hằng ngày giữa hai bên cũng thay đổi hẳn. Thay vì những chuỗi tin nhắn kiểu &ldquo;endpoint này trả về gì ấy nhỉ?&rdquo;, frontend chỉ việc đọc các kiểu dữ liệu đã được sinh ra. Lập trình viên backend ít bị làm phiền hơn, mà lại nhận được góp ý sớm hơn: nếu frontend làm dựa trên dữ liệu giả lập rồi thấy API có chỗ nào khó dùng, bạn sẽ biết ngay từ lúc sửa còn rẻ, chứ không phải đợi đến khi nó đã lên môi trường production.</p>

<h2 class="relative group">Orval nằm ở đâu trong quy trình làm việc
    <div id="orval-nằm-ở-đâu-trong-quy-trình-làm-việc" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#orval-n%e1%ba%b1m-%e1%bb%9f-%c4%91%c3%a2u-trong-quy-tr%c3%acnh-l%c3%a0m-vi%e1%bb%87c" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Chẳng có gì cầu kỳ cả. Backend đề xuất bản đặc tả, hai đội cùng rà soát, frontend dùng Orval để sinh mã máy khách và dữ liệu giả lập rồi bắt đầu làm, còn backend thì hiện thực các endpoint thật theo đúng hợp đồng đó. Khi đặc tả thay đổi, cả hai bên sinh lại mã, và trình biên dịch sẽ chỉ ra chính xác chỗ nào bị hỏng. Phần cuối đó mới là mấu chốt: những chỗ lệch nhau giữa frontend và backend sẽ hiện ra thành lỗi biên dịch, thay vì thành lỗi trên production.</p>

<h2 class="relative group">Cài đặt và cấu hình
    <div id="cài-đặt-và-cấu-hình" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#c%c3%a0i-%c4%91%e1%ba%b7t-v%c3%a0-c%e1%ba%a5u-h%c3%acnh" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Cài Orval dưới dạng gói phụ thuộc chỉ dùng khi phát triển (dev dependency), cùng với những thư viện mà mã được sinh ra sẽ import. Nếu muốn có hook React Query và mock thì đó là TanStack Query, MSW và Faker:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npm install --save-dev orval msw @faker-js/faker
</span></span><span class="line"><span class="cl">npm install @tanstack/react-query</span></span></code></pre></div></div>
<p>Sau đó thêm tệp <code>orval.config.ts</code> ở thư mục gốc của dự án. Cấu hình dưới đây gần giống ví dụ trong tài liệu chính thức của Orval:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;orval&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">petstore</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">input</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">target</span><span class="o">:</span> <span class="s1">&#39;./openapi.yaml&#39;</span><span class="p">,</span> <span class="c1">// a path or a URL
</span></span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nx">output</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">mode</span><span class="o">:</span> <span class="s1">&#39;single&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">target</span><span class="o">:</span> <span class="s1">&#39;./src/api/petstore.ts&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">schemas</span><span class="o">:</span> <span class="s1">&#39;./src/api/model&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">client</span><span class="o">:</span> <span class="s1">&#39;react-query&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">httpClient</span><span class="o">:</span> <span class="s1">&#39;fetch&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">mock</span>: <span class="kt">true</span><span class="p">,</span> <span class="c1">// MSW handlers plus Faker data
</span></span></span><span class="line"><span class="cl">      <span class="nx">override</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">fetch</span><span class="o">:</span> <span class="p">{</span> <span class="nx">includeHttpResponseReturnType</span>: <span class="kt">false</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div></div>
<p>Tùy chọn <code>client</code> quyết định bạn nhận được gì: các hàm <code>fetch</code> hoặc <code>axios</code> thuần, hook cho React Query, Vue Query, Svelte Query hay SWR, service cho Angular, hoặc schema Zod. Dòng <code>includeHttpResponseReturnType: false</code> khiến máy khách fetch trả về thẳng phần thân phản hồi (response body) thay vì <code>{ data, status }</code>, mà thường thì component chỉ cần đúng phần đó.</p>
<p>Thêm một script vào <code>package.json</code> để không ai phải nhớ câu lệnh:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;scripts&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;api&#34;</span><span class="p">:</span> <span class="s2">&#34;orval&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>Chạy <code>npm run api</code> là các hook và mô hình dữ liệu được sinh ra. Sau đó component dùng chúng như mọi hook React Query khác, với kiểu dữ liệu đã có sẵn đầy đủ:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-tsx" data-lang="tsx"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">useListPets</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;./api/petstore&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kd">function</span> <span class="nx">PetList() {</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="p">{</span> <span class="nx">data</span>: <span class="kt">pets</span><span class="p">,</span> <span class="nx">isLoading</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">useListPets</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">isLoading</span><span class="p">)</span> <span class="k">return</span> <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span><span class="nx">Loading</span><span class="err">…</span><span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;;</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">ul</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span><span class="nx">pets</span><span class="o">?</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">pet</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">&lt;</span><span class="nt">li</span> <span class="na">key</span><span class="o">=</span><span class="p">{</span><span class="nx">pet</span><span class="p">.</span><span class="nx">id</span><span class="p">}&gt;{</span><span class="nx">pet</span><span class="p">.</span><span class="nx">name</span><span class="p">}&lt;/</span><span class="nt">li</span><span class="p">&gt;)}</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>Thử đổi tên một trường trong đặc tả rồi chạy lại <code>npm run api</code>, bạn sẽ thấy <code>pet.name</code> bị gạch đỏ ở mọi chỗ đang dùng nó. Đó chính là cách bắt chỗ lệch giữa frontend và backend mà tôi nói ở trên, và bạn chẳng tốn gì để có nó.</p>

<h2 class="relative group">Phát triển dựa trên mock
    <div id="phát-triển-dựa-trên-mock" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#ph%c3%a1t-tri%e1%bb%83n-d%e1%bb%b1a-tr%c3%aan-mock" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Khi bật <code>mock: true</code>, Orval còn viết thêm cho mỗi endpoint một handler xử lý yêu cầu (request handler) của MSW, trả về dữ liệu do Faker tạo ra theo đúng cấu trúc (schema) của phản hồi, kèm một hàm trả về toàn bộ các handler đó. Đưa danh sách này cho <code>setupWorker</code> của MSW khi chạy trên trình duyệt, hoặc cho <code>setupServer</code> khi chạy kiểm thử, là ứng dụng sẽ chạy trên một backend giả vốn đã khớp sẵn với hợp đồng API. Mỗi handler còn nhận thêm phần ghi đè (override), nên một bài kiểm thử có thể trả về một payload cụ thể, một danh sách rỗng hay một trạng thái lỗi, mà không ai phải tự tay viết dữ liệu mẫu (fixture).</p>
<p>Chính phần này giúp frontend bắt tay vào làm ngay từ ngày đầu tiên. Mock không phải một thứ riêng mà ai đó phải ngồi bảo trì; chúng được sinh lại từ cùng bản đặc tả với mã máy khách, nên không thể nào bị tụt lại so với đặc tả.</p>

<h2 class="relative group">So sánh Orval với openapi-generator, openapi-typescript và Hey API
    <div id="so-sánh-orval-với-openapi-generator-openapi-typescript-và-hey-api" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#so-s%c3%a1nh-orval-v%e1%bb%9bi-openapi-generator-openapi-typescript-v%c3%a0-hey-api" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Cả bốn công cụ đều bắt đầu từ bản đặc tả OpenAPI, nhưng mỗi công cụ sinh ra đến đâu thì khác nhau.</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th>Sinh ra những gì</th>
					<th>Phù hợp nhất khi</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>Orval</strong></td>
					<td>Hàm máy khách có định kiểu hoặc hook cho framework (React Query, SWR, Vue, Svelte, Angular), mô hình dữ liệu, mock MSW, schema Zod</td>
					<td>Frontend viết bằng TypeScript, muốn có cả hook lẫn mock từ cùng một tệp cấu hình</td>
			</tr>
			<tr>
					<td><strong>openapi-generator</strong></td>
					<td>Mã máy khách và khung mã phía máy chủ (server stub) cho hàng chục ngôn ngữ, trong đó có TypeScript</td>
					<td>Đội cần biến cùng một bản đặc tả thành mã Java, Kotlin, Swift và TypeScript</td>
			</tr>
			<tr>
					<td><strong>openapi-typescript</strong></td>
					<td>Chỉ sinh kiểu dữ liệu, đi kèm <code>openapi-fetch</code> làm một lớp bọc fetch nhỏ gọn có định kiểu</td>
					<td>Bạn chỉ cần kiểu dữ liệu, gần như không muốn thêm mã nào chạy lúc runtime</td>
			</tr>
			<tr>
					<td><strong>Hey API</strong> (<code>@hey-api/openapi-ts</code>)</td>
					<td>Một SDK có định kiểu, với các plugin cho TanStack Query, Zod và nhiều thứ khác</td>
					<td>Phạm vi gần giống Orval, nhưng xây dựng quanh một hệ thống plugin</td>
			</tr>
	</tbody>
</table>
<p>Nếu chỉ cần kiểu dữ liệu thì openapi-typescript là lựa chọn nhẹ nhất, khó mà chê được. openapi-generator đáng dùng khi cùng một bản đặc tả phải sinh mã cho nhiều ngôn ngữ; mã TypeScript nó sinh ra vẫn chạy được, nhưng đọc lên cứ như được viết cho Java trước rồi mới chuyển sang. Giữa Orval và Hey API thì chủ yếu là chuyện sở thích, và bạn thấy mã sinh ra của bên nào dễ đọc hơn. Tôi vẫn dùng Orval vì mock được sinh ra trong cùng một lần chạy với các hook, và chính tính năng đó đã thay đổi cách đội frontend và đội backend làm việc với nhau.</p>
<p>Đây không phải loại công cụ hào nhoáng gì. Nó chỉ lặng lẽ dẹp bỏ cả một kiểu tranh cãi giữa các đội, và lần nào có cơ hội đổi như vậy, tôi cũng sẵn lòng.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2024/2024-04-10-openapitools/featured.jpg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>Phát triển API-first với OpenAPI: frontend và backend làm song song</title>
      <link>https://jared.lynskey.co.nz/vi/posts/2023/2023-10-08-open-api/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/vi/posts/2023/2023-10-08-open-api/</guid>
      <pubDate>Sun, 08 Oct 2023 00:30:03 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>OpenAPI</category>
      <category>backend</category>
      <category>frontend</category>
      <category>API design</category>
      <category>software development</category>
      <category>team collaboration</category>
      <description>Phát triển theo hướng API-first trong thực tế: thống nhất hợp đồng API bằng OpenAPI ngay từ đầu, dựng một máy chủ giả lập (mock server), thế là frontend không còn phải ngồi chờ backend nữa.</description>
      <content:encoded><![CDATA[<p>Trong những dự án tôi từng làm, thứ làm trễ tiến độ đều đặn nhất không phải là những bài toán kỹ thuật khó, mà là cảnh frontend ngồi không chờ một endpoint &ldquo;sắp xong rồi&rdquo;. Cách chữa mà đội tôi ở công ty chốt lại vừa nhàm vừa hiệu quả: thống nhất hợp đồng API (API contract) bằng OpenAPI trước, rồi mới có ai bắt đầu viết mã.</p>

<h3 class="relative group">Cái vòng chờ đợi
    <div id="cái-vòng-chờ-đợi" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#c%c3%a1i-v%c3%b2ng-ch%e1%bb%9d-%c4%91%e1%bb%a3i" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Quy trình truyền thống thì chạy tuần tự. Backend thiết kế và xây API, và chỉ khi endpoint đã chạy được thật thì frontend mới bắt đầu tích hợp. Backend chậm ngày nào thì lịch của frontend lãnh đủ ngày đó, mà ngày phát hành thường lại phụ thuộc vào lịch của frontend.</p>

<h3 class="relative group">Hợp đồng trước, mã nguồn sau
    <div id="hợp-đồng-trước-mã-nguồn-sau" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#h%e1%bb%a3p-%c4%91%e1%bb%93ng-tr%c6%b0%e1%bb%9bc-m%c3%a3-ngu%e1%bb%93n-sau" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Với quy trình đặc tả trước (schema-first), hai bên bắt đầu gần như cùng lúc. Trước khi có dòng mã nào, toàn bộ cấu trúc của API được đưa vào một bản đặc tả OpenAPI (OpenAPI schema): mọi endpoint, mô hình dữ liệu của yêu cầu và phản hồi, phương thức xác thực và mã lỗi. Tài liệu đó trở thành bản hợp đồng giữa frontend và backend.</p>
<p>Lợi ích thấy ngay. Trước khi tính năng được xây, ai cũng hiểu nó theo cùng một cách. Frontend có thể dựng giao diện trong lúc backend vẫn đang hiện thực các endpoint. Khi có thay đổi thì xử lý cũng nhanh hơn, vì chỉ có một nguồn thông tin chuẩn đã được thống nhất cần cập nhật. Và bạn có thể sinh máy chủ giả lập (mock server) thẳng từ bản đặc tả, nên frontend bắt đầu kiểm thử được ngay cả khi chưa có endpoint thật nào.</p>

<h3 class="relative group">Áp dụng thực tế ra sao
    <div id="áp-dụng-thực-tế-ra-sao" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%c3%a1p-d%e1%bb%a5ng-th%e1%bb%b1c-t%e1%ba%bf-ra-sao" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<ol>
<li>Thu thập yêu cầu, như với bất kỳ việc gì khác.</li>
<li>Thiết kế bản đặc tả: endpoint, mô hình yêu cầu/phản hồi, xác thực, mã lỗi.</li>
<li>Cho cả frontend lẫn backend cùng rà soát. Đây là lúc bản hợp đồng thể hiện rõ giá trị của nó.</li>
<li>Sinh máy chủ giả lập (Swagger và Postman đều làm được) và để frontend tích hợp với bản giả lập đó.</li>
<li>Backend hiện thực các endpoint thật theo đúng bản đặc tả.</li>
<li>Khi endpoint thật lần lượt xong thì thay dần bản giả lập bằng endpoint thật, đồng thời kiểm thử liên tục để chắc chắn hai bên vẫn làm đúng hợp đồng.</li>
<li>Tiếp tục trao đổi với nhau. Bản đặc tả là tài liệu sống, chứ không phải thứ khắc lên bia đá.</li>
</ol>

<h3 class="relative group">Những điểm vướng
    <div id="những-điểm-vướng" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#nh%e1%bb%afng-%c4%91i%e1%bb%83m-v%c6%b0%e1%bb%9bng" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Tất nhiên cách này cũng có cái giá của nó. Viết một bản đặc tả kỹ lưỡng ngay từ đầu tốn khá nhiều thời gian. Khi yêu cầu thay đổi thì bản đặc tả phải đổi trước, và ảnh hưởng sẽ lan sang cả hai đội. Ngoài ra, ai cũng cần biết dùng OpenAPI và các công cụ đi kèm ở mức cơ bản.</p>
<p>Dù vậy, tôi chưa từng thấy trường hợp nào mà công sức bỏ ra lúc đầu lại tốn hơn mớ hỗn loạn lúc tích hợp mà nó giúp tránh được. Frontend bắt tay vào làm từ ngày đầu tiên, còn các cuộc tranh cãi thì diễn ra trên một bản tài liệu, chứ không phải trên những bản dựng hỏng.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2023/2023-10-08-open-api/featured.jpg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>Zdog: thư viện JavaScript giả 3D, và lá cờ Hàn Quốc mừng Gwangbokjeol</title>
      <link>https://jared.lynskey.co.nz/vi/posts/2019/2019-08-08-gwangbokjeol/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/vi/posts/2019/2019-08-08-gwangbokjeol/</guid>
      <pubDate>Thu, 08 Aug 2019 00:00:00 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>Zdog</category>
      <category>JavaScript</category>
      <category>Frontend</category>
      <category>Canvas</category>
      <category>Korea</category>
      <description>Zdog là một engine giả 3D nhỏ gọn cho canvas và SVG: hình khối nằm trong không gian 3D nhưng được vẽ phẳng. Bài có một ví dụ tối giản và CodePen lá cờ Hàn Quốc tôi làm nhân dịp Gwangbokjeol.</description>
      <content:encoded><![CDATA[<p>Tuần sau là Gwangbokjeol (광복절), ngày Quang phục 15/8 của Hàn Quốc, kỷ niệm ngày đất nước thoát khỏi ách thống trị của Nhật. Theo truyền thống, vào dịp này người ta treo quốc kỳ trước nhà hoặc ngoài cửa sổ căn hộ. Nhân ngày lễ, và cũng để luyện thêm tay nghề JavaScript, tôi thử dùng một thư viện giả 3D khá hay để làm một lá cờ ảo.</p>
<p>Zdog là một thư viện JavaScript nhỏ gọn để vẽ hình minh hoạ kiểu 3D lên <code>&lt;canvas&gt;</code> hoặc SVG. Các hình khối có vị trí thật trong không gian 3D và xoay được như vật thể 3D, nhưng lại được vẽ thành hình phẳng, nên kết quả trông giống một bức minh hoạ phẳng có thể xoay qua xoay lại hơn là một bản dựng 3D (3D render). Làm một lá cờ đơn giản thì rất hợp.</p>

<h2 class="relative group">Ví dụ Zdog tối giản
    <div id="ví-dụ-zdog-tối-giản" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#v%c3%ad-d%e1%bb%a5-zdog-t%e1%bb%91i-gi%e1%ba%a3n" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Hai hình khối, kéo chuột để xoay, và một vòng lặp hoạt ảnh (animation loop):</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">canvas</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;zdog-canvas&#34;</span> <span class="na">width</span><span class="o">=</span><span class="s">&#34;240&#34;</span> <span class="na">height</span><span class="o">=</span><span class="s">&#34;240&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">canvas</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;https://unpkg.com/zdog@1/dist/zdog.dist.min.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">illo</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Zdog</span><span class="p">.</span><span class="nx">Illustration</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">element</span><span class="o">:</span> <span class="s1">&#39;.zdog-canvas&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">dragRotate</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="c1">// drag to turn the scene
</span></span></span><span class="line"><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// a square, pushed back
</span></span></span><span class="line"><span class="cl">  <span class="k">new</span> <span class="nx">Zdog</span><span class="p">.</span><span class="nx">Rect</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">addTo</span><span class="o">:</span> <span class="nx">illo</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">width</span><span class="o">:</span> <span class="mi">80</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">height</span><span class="o">:</span> <span class="mi">80</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">translate</span><span class="o">:</span> <span class="p">{</span> <span class="nx">z</span><span class="o">:</span> <span class="o">-</span><span class="mi">30</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nx">stroke</span><span class="o">:</span> <span class="mi">8</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">color</span><span class="o">:</span> <span class="s1">&#39;#0047a0&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// a filled circle, pulled forward
</span></span></span><span class="line"><span class="cl">  <span class="k">new</span> <span class="nx">Zdog</span><span class="p">.</span><span class="nx">Ellipse</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">addTo</span><span class="o">:</span> <span class="nx">illo</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">diameter</span><span class="o">:</span> <span class="mi">60</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">translate</span><span class="o">:</span> <span class="p">{</span> <span class="nx">z</span><span class="o">:</span> <span class="mi">30</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nx">stroke</span><span class="o">:</span> <span class="mi">8</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">color</span><span class="o">:</span> <span class="s1">&#39;#cd2e3a&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">fill</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kd">function</span> <span class="nx">animate</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">illo</span><span class="p">.</span><span class="nx">rotate</span><span class="p">.</span><span class="nx">y</span> <span class="o">+=</span> <span class="mf">0.02</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">illo</span><span class="p">.</span><span class="nx">updateRenderGraph</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="nx">requestAnimationFrame</span><span class="p">(</span><span class="nx">animate</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="nx">animate</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span></span></span></code></pre></div></div>
<p>Mỗi hình được gắn vào khung cảnh bằng <code>addTo</code>, và chính <code>translate.z</code> tạo ra cảm giác 3D: xoay khung cảnh thì hình tròn sẽ vòng ra phía trước hình vuông. <code>updateRenderGraph()</code> cập nhật và vẽ lại toàn bộ, nên nó nằm trong vòng lặp.</p>

<h2 class="relative group">Tài liệu chính thức
    <div id="tài-liệu-chính-thức" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#t%c3%a0i-li%e1%bb%87u-ch%c3%adnh-th%e1%bb%a9c" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p><a href="https://zzz.dog/"  target="_blank" rel="noreferrer">https://zzz.dog/</a></p>
<p>Toàn bộ mã nguồn của ví dụ này có trên CodePen, bạn cứ vào xem.</p>

<h2 class="relative group">Ví dụ của tôi
    <div id="ví-dụ-của-tôi" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#v%c3%ad-d%e1%bb%a5-c%e1%bb%a7a-t%c3%b4i" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<div class="codepen" data-height="700" data-theme-id="light" data-default-tab="result" data-user="jared-lynskey" data-slug-hash="voRGqp" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="광복절">
  <span>Xem Pen <a href="https://codepen.io/jared-lynskey/pen/voRGqp/">
  광복절</a> của Jared Lynskey (<a href="https://codepen.io/jared-lynskey">@jared-lynskey</a>)
  trên <a href="https://codepen.io">CodePen</a>.</span>
</div>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2019/2019-08-08-gwangbokjeol/featured.jpeg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>Cách dùng SCSS trong Hugo với Hugo Pipes</title>
      <link>https://jared.lynskey.co.nz/vi/posts/2019/2019-08-06-understandingsass/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/vi/posts/2019/2019-08-06-understandingsass/</guid>
      <pubDate>Tue, 06 Aug 2019 00:00:00 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>css</category>
      <category>frontend</category>
      <category>webdev</category>
      <category>gohugo</category>
      <description>Cách dùng SCSS trong Hugo: tôi thiết lập Hugo Pipes để biên dịch SCSS cho chính trang web này, chỉ trong bốn bước ngắn.</description>
      <content:encoded><![CDATA[<p>CSS của trang này cứ phình dần thành một tệp dài dằng dặc, lặp đi lặp lại, nên cuối cùng tôi cũng chịu chuyển nó sang SASS. Nếu bạn đang chạy một trang GoHugo thì đây là kiểu thay đổi chỉ tốn mười lăm phút, nhưng về sau lần nào đụng vào tệp CSS bạn cũng thấy xứng đáng.</p>
<p>Dưới đây là cách tôi thiết lập.</p>

<h2 class="relative group">Sao lại dùng SASS?
    <div id="sao-lại-dùng-sass" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#sao-l%e1%ba%a1i-d%c3%b9ng-sass" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>CSS thuần sẽ bắt đầu rối khi trang web lớn hơn dăm ba trang. SASS cho bạn biến, quy tắc lồng nhau, mixin và hàm, nên những thứ như màu sắc hay các đoạn bố cục dùng chung chỉ cần đặt ở một chỗ, thay vì sao chép khắp nơi. Hay nhất với người dùng Hugo là Hugo Pipes tự biên dịch SCSS giúp bạn — không cần công cụ dựng (build tool) riêng, cũng không cần script theo dõi thay đổi (watcher).</p>
<p>Hai trang nên mở sẵn trong lúc làm:</p>
<ul>
<li><a href="https://sass-lang.com/"  target="_blank" rel="noreferrer">Tài liệu SASS</a></li>
<li><a href="https://gohugo.io/functions/css/sass/"  target="_blank" rel="noreferrer">Tài liệu hỗ trợ SCSS/SASS</a> của Hugo</li>
</ul>

<h2 class="relative group">Thiết lập
    <div id="thiết-lập" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#thi%e1%ba%bft-l%e1%ba%adp" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>

<h3 class="relative group">1. Sắp xếp lại thư mục assets
    <div id="1-sắp-xếp-lại-thư-mục-assets" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-s%e1%ba%afp-x%e1%ba%bfp-l%e1%ba%a1i-th%c6%b0-m%e1%bb%a5c-assets" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Hugo tìm thư mục <code>assets</code> ở thư mục gốc của dự án, nên bạn hãy tạo thư mục này (hoặc chuyển thư mục đang có) ra đó. Nếu muốn để nó nằm bên trong <code>resources</code>, hãy khai báo cho Hugo trong <code>config.toml</code>:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="nx">assetDir</span> <span class="p">=</span> <span class="s2">&#34;resources/assets&#34;</span></span></span></code></pre></div></div>

<h3 class="relative group">2. Tạo thư mục <code>sass</code>
    <div id="2-tạo-thư-mục-sass" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-t%e1%ba%a1o-th%c6%b0-m%e1%bb%a5c-sass" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Bên trong <code>assets</code>, tạo một thư mục tên là <code>sass</code>. Các tệp SCSS của bạn sẽ nằm ở đây.</p>

<h3 class="relative group">3. Thêm tệp đầu vào (entry point)
    <div id="3-thêm-tệp-đầu-vào-entry-point" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-th%c3%aam-t%e1%bb%87p-%c4%91%e1%ba%a7u-v%c3%a0o-entry-point" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Tạo tệp <code>main.scss</code> bên trong thư mục <code>sass</code>. Đây là tệp mà Hugo sẽ biên dịch, nên cứ thêm vào một ít SCSS để kiểm tra xem nó đã chạy chưa:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-scss" data-lang="scss"><span class="line"><span class="cl"><span class="c1">// assets/sass/main.scss
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Variables
</span></span></span><span class="line"><span class="cl"><span class="nv">$primary-color</span><span class="o">:</span> <span class="mh">#3498db</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Mixin
</span></span></span><span class="line"><span class="cl"><span class="k">@mixin</span><span class="nf"> center</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="na">display</span><span class="o">:</span> <span class="ni">flex</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="na">justify-content</span><span class="o">:</span> <span class="ni">center</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="na">align-items</span><span class="o">:</span> <span class="ni">center</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Styles
</span></span></span><span class="line"><span class="cl"><span class="nt">body</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="na">font-family</span><span class="o">:</span> <span class="n">Arial</span><span class="o">,</span> <span class="ni">sans-serif</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="na">background-color</span><span class="o">:</span> <span class="nv">$primary-color</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nt">h1</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">@include</span><span class="nd"> center</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="na">color</span><span class="o">:</span> <span class="ni">white</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>

<h3 class="relative group">4. Gắn vào mẫu giao diện (template)
    <div id="4-gắn-vào-mẫu-giao-diện-template" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#4-g%e1%ba%afn-v%c3%a0o-m%e1%ba%abu-giao-di%e1%bb%87n-template" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Trong partial <code>head.html</code>, nạp tệp CSS đã biên dịch rồi rút gọn (minify) nó:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">style</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">resources</span><span class="p">.</span><span class="nx">Get</span><span class="w"> </span><span class="s">&#34;sass/main.scss&#34;</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nx">resources</span><span class="p">.</span><span class="nx">ToCSS</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nx">resources</span><span class="p">.</span><span class="nx">Minify</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nx">link</span><span class="w"> </span><span class="nx">rel</span><span class="p">=</span><span class="s">&#34;stylesheet&#34;</span><span class="w"> </span><span class="nx">href</span><span class="p">=</span><span class="s">&#34;{{ $style.Permalink }}&#34;</span><span class="p">&gt;</span></span></span></code></pre></div></div>
<p>Chỉ cần nhớ là tên tệp ở đây phải khớp với tệp đầu vào thật của bạn (<code>main.scss</code>).</p>

<h2 class="relative group">Chia nhỏ mã định kiểu
    <div id="chia-nhỏ-mã-định-kiểu" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#chia-nh%e1%bb%8f-m%c3%a3-%c4%91%e1%bb%8bnh-ki%e1%bb%83u" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Khi mọi thứ lớn dần, hãy tách SCSS thành các partial rồi import chúng từ <code>main.scss</code>:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-scss" data-lang="scss"><span class="line"><span class="cl"><span class="c1">// assets/sass/_variables.scss
</span></span></span><span class="line"><span class="cl"><span class="nv">$primary-color</span><span class="o">:</span> <span class="mh">#3498db</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// assets/sass/_mixins.scss
</span></span></span><span class="line"><span class="cl"><span class="k">@mixin</span><span class="nf"> center</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="na">display</span><span class="o">:</span> <span class="ni">flex</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="na">justify-content</span><span class="o">:</span> <span class="ni">center</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="na">align-items</span><span class="o">:</span> <span class="ni">center</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// assets/sass/main.scss
</span></span></span><span class="line"><span class="cl"><span class="k">@import</span> <span class="s2">&#34;variables&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">@import</span> <span class="s2">&#34;mixins&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nt">body</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="na">background-color</span><span class="o">:</span> <span class="nv">$primary-color</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nt">h1</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">@include</span><span class="nd"> center</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="na">color</span><span class="o">:</span> <span class="ni">white</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>Nói thật là chỉ có vậy thôi. Bốn bước, và Hugo sẽ biên dịch lại mã định kiểu mỗi lần dựng trang. Tôi cứ tưởng sẽ phải vật lộn với quy trình xử lý tài nguyên (asset pipeline), ai ngờ mọi thứ chạy ngon luôn.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2019/2019-08-06-understandingsass/featured.jpeg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
