jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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 write import "./util" and have Webpack try .tsx, .ts, .js… in order {cho phép viết import "./util" và để Webpack thử .tsx, .ts, .js… theo thứ tự}.
  • modules — adding "src" lets you write import "components/Button" instead of "../../components/Button" {thêm "src" cho phép viết import "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-leftuse: ["a-loader", "b-loader"] runs b then a {Chạy phải-sang-trái — b rồi a}.
  • Return a string (or call this.callback(err, code, map) for source maps) {Trả về chuỗi (hoặc gọi this.callback cho 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ùng this.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}

HookFires when {Kích hoạt khi}Typical use {Dùng để}
compilea compilation starts {một compilation bắt đầu}logging, timers {log, đồng hồ}
compilationcompilation object created {tạo object compilation}tap sub-hooks {móc hook con}
emitassets ready, not yet written {asset sẵn sàng, chưa ghi}add/edit output files {thêm/sửa file output}
donebuild finished {build xong}report stats, notify {báo cáo, thông báo}

Webpack’s hook system is Tapabletap (sync), tapAsync (callback), tapPromise (promise) {Hệ thống hook của Webpack là Tapabletap, 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 (MiniCssExtractPlugin does exactly this) {Cần cả hai? Nhiều công cụ ship cả loader 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 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ằng extensions, modules, alias}.
  • Keep resolve.alias and tsconfig.paths in sync {Giữ resolve.aliastsconfig.paths đồng bộ}.
  • externals keeps a dependency out of the bundle {externals giữ 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}.