Webpack · Part 10 — Advanced Resolve + Authoring Your Own Loader & Plugin
How module resolution actually works (resolve, alias, extensions, modules, externals), then write a real custom loader and a custom plugin tapping compiler hooks. With a live loader & plugin lab.
You have used loaders and plugins for nine parts {Bạn đã dùng loader và plugin suốt chín phần}. Now you build your own — and once you do, the whole tool stops feeling like magic {Giờ bạn tự viết — và khi làm xong, cả công cụ thôi cảm giác như phép thuật}. First, the piece that runs before any loader: module resolution {Trước hết, phần chạy trước mọi loader: phân giải module}.
Run a tiny loader on real input and read a working plugin skeleton in the lab below {Chạy một loader nhỏ trên input thật và đọc khung plugin hoạt động trong lab bên dưới}:
1. How resolution works {Phân giải hoạt động thế nào}
When your code says import x from "./util", Webpack must turn that string into an exact file on disk {Khi code nói import x from "./util", Webpack phải biến chuỗi đó thành một file chính xác trên đĩa}. That job is resolution {Việc đó là phân giải}:
module.exports = {
resolve: {
extensions: [".tsx", ".ts", ".jsx", ".js", ".json"], // try in order
modules: ["src", "node_modules"], // where to look for bare imports
alias: {
"@": path.resolve(__dirname, "src"),
"@components": path.resolve(__dirname, "src/components"),
},
},
};
extensions— lets you writeimport "./util"and have Webpack try.tsx,.ts,.js… in order {cho phép viếtimport "./util"và để Webpack thử.tsx,.ts,.js… theo thứ tự}.modules— adding"src"lets you writeimport "components/Button"instead of"../../components/Button"{thêm"src"cho phép viếtimport "components/Button"thay vì"../../components/Button"}.alias— short, stable names that survive refactors {tên ngắn, ổn định, sống sót qua refactor}.
2. Aliases vs TypeScript paths {Alias vs paths của TypeScript}
If you use TypeScript, you set the same shortcut in two places — TS needs it for type-checking, Webpack needs it for bundling {Nếu dùng TypeScript, bạn đặt cùng lối tắt ở hai nơi — TS cần để check kiểu, Webpack cần để bundle}:
// tsconfig.json
{ "compilerOptions": { "paths": { "@/*": ["src/*"] } } }
// webpack.config.js
resolve: { alias: { "@": path.resolve(__dirname, "src") } }
Keep them in sync, or use tsconfig-paths-webpack-plugin to derive Webpack aliases from tsconfig automatically {Giữ chúng đồng bộ, hoặc dùng tsconfig-paths-webpack-plugin để suy ra alias Webpack từ tsconfig tự động}.
3. externals — don’t bundle this {externals — đừng bundle cái này}
Sometimes a dependency is loaded another way (a CDN <script>, or it’s provided by the host app) {Đôi khi một phụ thuộc được tải cách khác (CDN <script>, hoặc do app chủ cung cấp)}. Tell Webpack to leave it alone {Bảo Webpack để yên nó}:
module.exports = {
externals: {
react: "React", // expect a global `React` at runtime
"react-dom": "ReactDOM",
},
};
Now import React from "react" compiles to a reference to the global, and React is not in your bundle {Giờ import React from "react" biên dịch thành tham chiếu tới global, và React không nằm trong bundle}. Essential for libraries and micro-frontends (Part 11) {Thiết yếu cho thư viện và micro-frontend (Phần 11)}.
4. Writing a loader {Viết một loader}
A loader is a function that takes source and returns transformed source {Loader là một hàm nhận source và trả về source đã biến đổi}. That’s the whole contract {Đó là toàn bộ hợp đồng}:
// my-banner-loader.js
module.exports = function (source) {
const { author = "anon" } = this.getOptions() || {};
return `/* © ${author} ${new Date().getFullYear()} */\n${source}`;
};
Wire it up by path (loaders are resolved like modules) {Gắn nó qua đường dẫn (loader được phân giải như module)}:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: {
loader: path.resolve("./my-banner-loader.js"),
options: { author: "Vinh" },
},
},
],
},
};
Toggle the options in the lab above to watch the output change line by line {Bật/tắt tùy chọn trong lab phía trên để xem output đổi từng dòng}.
Rules that matter {Quy tắc quan trọng}
- Run right-to-left —
use: ["a-loader", "b-loader"]runsbthena{Chạy phải-sang-trái —brồia}. - Return a string (or call
this.callback(err, code, map)for source maps) {Trả về chuỗi (hoặc gọithis.callbackcho source map)}. - Be pure & cacheable — same input → same output; don’t read mutable globals {Thuần & cache được — cùng input → cùng output}.
- Async?
const cb = this.async(); ...; cb(null, result);{Bất đồng bộ? dùngthis.async()}.
5. Writing a plugin {Viết một plugin}
A plugin is a class with apply(compiler) that taps into one of Webpack’s many hooks {Plugin là một class có apply(compiler) móc vào một trong nhiều hook của Webpack}. Where loaders transform individual files, plugins act on the whole build {Loader biến đổi từng file, plugin tác động lên cả build}:
class BuildManifestPlugin {
apply(compiler) {
// emit hook: add assets before they're written to disk
compiler.hooks.emit.tapAsync("BuildManifestPlugin", (compilation, cb) => {
const files = Object.keys(compilation.assets).join("\n");
compilation.assets["manifest.txt"] = {
source: () => files,
size: () => files.length,
};
cb();
});
}
}
module.exports = BuildManifestPlugin;
plugins: [new BuildManifestPlugin()];
Switch hooks in the lab to see done, emit, and compile skeletons {Đổi hook trong lab để xem khung done, emit, compile}.
The hooks you’ll reach for {Các hook hay dùng}
| Hook | Fires when {Kích hoạt khi} | Typical use {Dùng để} |
|---|---|---|
compile | a compilation starts {một compilation bắt đầu} | logging, timers {log, đồng hồ} |
compilation | compilation object created {tạo object compilation} | tap sub-hooks {móc hook con} |
emit | assets ready, not yet written {asset sẵn sàng, chưa ghi} | add/edit output files {thêm/sửa file output} |
done | build finished {build xong} | report stats, notify {báo cáo, thông báo} |
Webpack’s hook system is Tapable — tap (sync), tapAsync (callback), tapPromise (promise) {Hệ thống hook của Webpack là Tapable — tap, tapAsync, tapPromise}.
6. When to write which {Khi nào viết cái nào}
- Need to transform file content? → loader (e.g. compile a custom
.md-to-JS format) {Cần biến đổi nội dung file? → loader}. - Need to act on the build? (extra assets, bundle-wide checks, injecting globals) → plugin {Cần tác động lên build? → plugin}.
- Need both? Many tools ship a loader and a plugin that talk to each other (
MiniCssExtractPlugindoes exactly this) {Cần cả hai? Nhiều công cụ ship cả loader và plugin nói chuyện với nhau}.
7. Exercises {Bài tập}
1. You keep writing import Button from "../../../components/Button". What two configs make import Button from "@components/Button" work in a TS project? {Bạn cứ viết import dài. Hai cấu hình nào làm @components/Button chạy trong dự án TS?}
Solution {Lời giải}
resolve.alias in webpack.config.js and compilerOptions.paths in tsconfig.json (or tsconfig-paths-webpack-plugin) {resolve.alias trong webpack config và compilerOptions.paths trong tsconfig}.
2. Your library should not bundle React because the host app already has it. What do you configure? {Thư viện của bạn không nên bundle React vì app chủ đã có. Cấu hình gì?}
Solution {Lời giải}
externals: { react: "React", "react-dom": "ReactDOM" } {externals ánh xạ React sang global}.
3. You want every output JS file to start with a license banner. Loader or plugin? {Bạn muốn mọi file JS output bắt đầu bằng banner giấy phép. Loader hay plugin?}
Solution {Lời giải}
Either works, but a loader is simplest for per-file content (or use the built-in webpack.BannerPlugin for a plugin approach) {Cả hai được, nhưng loader đơn giản nhất cho nội dung từng file (hoặc dùng webpack.BannerPlugin sẵn có)}.
Stretch {Nâng cao}: in the lab, enable all three loader options, then write the resulting loader function from memory before revealing the source {trong lab, bật cả ba tùy chọn loader, rồi tự viết hàm loader từ trí nhớ trước khi xem source}.
Key takeaways {Điểm chính}
- Resolution turns import strings into files — tune it with
extensions,modules,alias{Phân giải biến chuỗi import thành file — chỉnh bằngextensions,modules,alias}. - Keep
resolve.aliasandtsconfig.pathsin sync {Giữresolve.aliasvàtsconfig.pathsđồng bộ}. externalskeeps a dependency out of the bundle {externalsgiữ một phụ thuộc khỏi bundle}.- A loader is a function transforming file content, run right-to-left {Loader là hàm biến đổi nội dung file, chạy phải-sang-trái}.
- A plugin is a class with
apply(compiler)tapping hooks to act on the whole build {Plugin là class cóapply(compiler)móc hook để tác động cả build}.
Next up {Tiếp theo}
Part 11 — Module Federation: share code and dependencies between independently deployed apps at runtime, the foundation of modern micro-frontends {Phần 11 — Module Federation: chia sẻ code và phụ thuộc giữa các app triển khai độc lập lúc runtime, nền tảng của micro-frontend hiện đại}.