Micro-frontend Learning
Kiểm thử và tooling

Local development nhiều ứng dụng

Điều phối dev server, chọn remote bằng profile và mock API để sửa từng micro-frontend mà vẫn kiểm tra được tích hợp trong shell.

Bạn chỉ muốn sửa bộ lọc catalog, nhưng để mở màn hình lại phải clone cart, chạy backend và chờ tất cả dev server compile. Nếu mỗi thay đổi nhỏ đều cần cả hệ thống trên máy, ranh giới micro-frontend chưa giúp được nhiều cho vòng lặp phát triển.

Mình mặc định chọn chạy app đang sửa cùng shell local, còn các remote không đổi dùng artifact đã pin. Standalone và mocks giúp bạn sửa nhanh; trước khi merge, cần kiểm tra lại trong shell thật để bắt lỗi routing, contract và shared dependencies mà môi trường riêng không thể thấy.

Phạm vi và điều kiện bắt đầu

Các lệnh và file mẫu thuộc shop-workspace riêng, không phải site Next.js + Fumadocs micro-frontend-learn. Bạn cần workspace từ Cấu trúc workspace và tích hợp webpack 5 từ Host và remote đầu tiên.

Catalog trong lab đầu tiên dùng container shop_catalog, alias catalog và expose ./CatalogPage. Cart dưới đây là phần mở rộng giả định đã có container shop_cart và public module tương ứng; bài này không dựng implementation cart. Nếu bạn mới có shell/catalog, bỏ cart khỏi profiles, object scopes của helper và mapping production; không chạy script cart. Trong dev:all, chỉ giữ hai commands và hai tên shell,catalog.

Các URL assets.example.com và release ID bên dưới chỉ minh họa. Hãy thay bằng artifact thật mà browser truy cập được.

Mở Source Explorer: đối chiếu các file trong bài

Các phương án trong bài nằm ở snapshot riêng; đổi Phiên bản trong Source Explorer để đối chiếu, không ghép các config thay thế nhau thành một file. Source tham chiếu giữ nguyên các trích đoạn trong bài, không phải workspace chạy độc lập.

Mục lục

Chọn vòng lặp phát triển phù hợp

Không cần một chế độ chạy duy nhất cho mọi việc. Khi sửa layout, bạn cần feedback nhanh; khi đổi payload thêm giỏ, bạn cần producer và consumer thật. Chọn theo ranh giới bị tác động, không theo số app đang có trong repo.

Chế độChạy trên máyPhù hợp vớiĐiểm chưa chứng minh
StandaloneCatalog với context tối thiểu và API mocksUI, loading/error states, logic nội bộComposition trong shell, auth và router thật
Focused integrationShell + catalog local; cart dùng release đã pinCông việc catalog hằng ngàyMọi tổ hợp version hoặc backend thật
All localShell + catalog + cart localĐổi contract xuyên app, debug lifecycleCandidate mới tương thích với consumer đã release
Local shell với remotes đã pinChỉ shell localLayout, routing và remote selection của shellCode remote candidate đang sửa
Preview/integrationArtifacts build thật và test backendSmoke/E2E trước merge hoặc promotionToàn bộ điều kiện production

Standalone nên dùng chính public component hoặc lifecycle API mà shell consume, với props/context giả ở adapter riêng. Đừng viết một màn hình catalog thứ hai chỉ để chạy demo: bản demo ấy có thể xanh trong khi module expose thật bị hỏng.

Nếu app luôn cần store nội bộ của shell hoặc source cart mới chạy được, đừng dùng mocks để che dependency đó. Hãy xem lại public contract. Mocks nên thay một dependency đã có ranh giới, không hợp thức hóa deep import giữa các app.

Giữ một bản đồ port và runtime rõ ràng

Giữ quy ước của lab thay vì để từng server tự chọn port tiếp theo:

Thành phầnĐịa chỉ mẫuVai trò
Shellhttp://localhost:3000Origin của document khi chạy tích hợp
Cataloghttp://localhost:3001Standalone và /remoteEntry.js của catalog
Carthttp://localhost:3002Standalone và /remoteEntry.js của cart
Backend local, nếu dùnghttp://localhost:4000Đích proxy /api, không phải Federation remote

Trong chế độ focused integration, browser tải như sau:

direction: right
browser: "Browser: document tại\nlocalhost:3000"
shell: "localhost:3000\nShell local"
catalog: "localhost:3001\nCatalog entry + chunks local"
cart: "assets.example.com\nCart entry + chunks k09"
browser -> shell
browser -> catalog
browser -> cart
Browser: document tạilocalhost:3000localhost:3000Shell locallocalhost:3001Catalog entry + chunks localassets.example.comCart entry + chunks k09
Browser: document tạilocalhost:3000localhost:3000Shell locallocalhost:3001Catalog entry + chunks localassets.example.comCart entry + chunks k09
Giữ một bản đồ port và runtime rõ ràng

Cart vẫn chạy trong document shell nếu được tải bằng Federation; origin của file JavaScript không tạo sandbox riêng. Ví dụ, fetch('/api/cart') trong code cart sẽ gọi origin document localhost:3000, không tự gọi origin CDN của cart. API base URL được nhúng sẵn trong artifact có thể cho hành vi khác, nên phải kiểm tra Network của release đã chọn.

Dùng localhost nhất quán trong cấu hình và URL. Với container/WSL, địa chỉ browser truy cập phải là địa chỉ đã publish/forward ra máy host; tên service kiểu catalog:3001 chỉ resolve trong Docker network không phải URL remote hợp lệ cho browser bên ngoài.

Đừng đổi port mà quên mapping

Site docs cũng mặc định chạy ở 3000. Dừng nó hoặc chạy docs ở 3100. Khi remote chuyển từ 3001 sang port khác, phải cập nhật mapping và địa chỉ WebSocket liên quan nếu đã cấu hình cố định; việc server tự mở ở port mới không sửa URL bên shell.

Điều phối nhiều dev server

Chạy song song và quản lý process

Ba terminal là baseline dễ debug nhất. Khi cần một lệnh chung, dùng process runner có prefix log và cơ chế dừng các process con. Với workspace nhỏ này, mình dùng concurrently; chưa cần task orchestrator chỉ để mở ba server.

Chạy tại root shop-workspace, sau khi dừng các dev server:

npm install --save-dev --save-exact concurrently@9

Thêm các scripts sau vào package.json root, giữ các fields và scripts đang có:

shop-workspace/package.json — phần scripts bổ sung
{
  "scripts": {
    "dev:all": "concurrently --kill-others --names shell,catalog,cart \"npm run dev:shell\" \"npm run dev:catalog\" \"npm run dev:cart\""
  }
}
Đối chiếu fragments/package-dev-scripts.json

--kill-others dừng các lệnh còn lại nếu một lệnh kết thúc, dù thành công hay thất bại. Điều này hợp với một nhóm dev server cần sống cùng nhau; không đặt một lệnh build ngắn trong cùng nhóm, vì build xong sẽ làm các server bị dừng.

npm run dev:all

Log có prefix app để bạn biết lỗi compile đến từ đâu. Nhấn Ctrl+C để dừng nhóm và xác nhận các port đã được giải phóng. Compile error thường không làm dev server thoát, nên runner không thể tự biến mọi lỗi compile thành thất bại của cả nhóm.

Không dùng npm run dev --workspaces hoặc nối npm run dev:catalog && npm run dev:shell: các lệnh đó không tạo nhóm server chạy song song. Cũng không cần background process bằng & rồi bỏ qua cleanup, vì process sót lại dễ khiến lượt chạy sau dùng nhầm server cũ.

Readiness không phải process còn sống

Process runner chỉ khởi động lệnh; nó không đảm bảo catalog đã compile xong trước khi shell import remote. Với thao tác thủ công, chờ remote compile thành công rồi mở hoặc reload browser. Bạn không nhất thiết phải trì hoãn process shell, chỉ cần tránh kích hoạt lượt tải đầu quá sớm.

Kiểm tra entry bằng HTTP:

curl --fail --show-error --include http://localhost:3001/remoteEntry.js

Bạn cần response thành công, MIME JavaScript phù hợp và body là code. 200 chứa HTML fallback không phải readiness của remote. Sau đó kiểm tra shell thực sự import và render được module; HTTP entry tốt vẫn chưa chứng minh shared dependencies hay exports đúng.

Nếu thêm bước wait tự động, đặt timeout hữu hạn và kiểm tra resource cần dùng thay vì chỉ TCP port. Với CI smoke, cần readiness sâu hơn: tải entry, resolve public module và đi tới trạng thái UI usable. Tránh sleep 10 như một cam kết rằng mọi máy sẽ compile trong cùng thời gian.

Remote overrides bằng profile

Remote override là chọn implementation khác cho cùng alias public, chẳng hạn alias catalog trỏ từ artifact integration sang dev server local. Host vẫn import catalog/CatalogPage; bạn đổi nơi tải code, không import source catalog vào bundle shell.

Ví dụ bên dưới chọn profile lúc webpack khởi động. Đây là build-time configuration của remote URL, dù module vẫn được tải ở runtime. Đổi profile cần khởi động lại dev server shell; không gọi nó là runtime switching.

Khai báo tổ hợp remote

Tạo file cấu hình trong workspace thực hành:

shop-workspace/dev/remotes.profiles.json — schema do ứng dụng tự quy định
{
  "all-local": {
    "catalog": "http://localhost:3001/remoteEntry.js",
    "cart": "http://localhost:3002/remoteEntry.js"
  },
  "catalog-local": {
    "catalog": "http://localhost:3001/remoteEntry.js",
    "cart": "https://assets.example.com/cart/releases/k09/remoteEntry.js"
  },
  "integration": {
    "catalog": "https://assets.example.com/catalog/releases/c42/remoteEntry.js",
    "cart": "https://assets.example.com/cart/releases/k09/remoteEntry.js"
  }
}
Đối chiếu dev/remotes.profiles.json

File này không phải mf-manifest.json và webpack không tự đọc nó. Nó chỉ là input cho helper của shell. Release URL phải giữ entry và toàn bộ chunks/assets liên quan; trỏ entry versioned vào chunks mutable vẫn không tái lập được môi trường.

Pin release giúp bạn biết mình đang kiểm tra candidate local với consumer nào. Tránh tên latest cho baseline chia sẻ giữa team. Hai người mở cùng profile nhưng nhận hai build cart khác nhau sẽ khó so sánh lỗi.

Áp dụng profile trong webpack của shell

Helper sau chuyển profile thành cấu hình remotes kiểu script container. Scope được giữ cố định theo contract của lab, không suy ra từ alias hoặc tên package.

shop-workspace/tools/dev-remotes.cjs
const profiles = require('../dev/remotes.profiles.json');

const scopes = {
  catalog: 'shop_catalog',
  cart: 'shop_cart',
};

const allowedOrigins = new Set([
  'http://localhost:3001',
  'http://localhost:3002',
  'https://assets.example.com',
]);

module.exports = function createDevRemotes(env = {}) {
  const profileName = env.profile ?? 'all-local';
  if (!Object.hasOwn(profiles, profileName)) {
    throw new Error(`Unknown remote profile: ${profileName}`);
  }

  const entries = Object.entries(scopes).map(([alias, scope]) => {
    const entry = profiles[profileName][alias];
    if (typeof entry !== 'string') {
      throw new Error(`Missing entry for ${alias}`);
    }

    const url = new URL(entry);
    if (!allowedOrigins.has(url.origin) || url.username || url.password) {
      throw new Error(`Remote origin is not allowed: ${alias}`);
    }
    if (!url.pathname.endsWith('/remoteEntry.js') || url.search || url.hash) {
      throw new Error(`Invalid remote entry URL: ${alias}`);
    }

    console.info(`[remotes] ${profileName}: ${alias} -> ${url.href}`);
    return [alias, `${scope}@${url.href}`];
  });

  return Object.fromEntries(entries);
};
Đối chiếu tools/dev-remotes.cjs

Khi thay domain minh họa, thay cả allowedOrigins. Đây là kiểm tra lỗi cấu hình ở local, không phải một security policy hoàn chỉnh: origin được phép vẫn có thể phục vụ code xấu hoặc redirect. Mỗi remote vẫn cần nguồn artifact tin cậy và scope đúng.

Webpack config hiện đang export object thì chuyển thành function nhận (env, argv) và trả lại cấu hình đó. Phần liên quan tới lựa chọn remote có dạng sau; đây là trích đoạn để ghép vào config đang có, không phải file đầy đủ:

apps/shell/webpack.config.cjs — trích đoạn trong config function
const createDevRemotes = require('../../tools/dev-remotes.cjs');

// Trong function module.exports = (env = {}, argv = {}) => { ... }
const remotes = argv.mode === 'development'
  ? createDevRemotes(env)
  : {
      catalog: 'shop_catalog@https://assets.example.com/catalog/releases/c42/remoteEntry.js',
      cart: 'shop_cart@https://assets.example.com/cart/releases/k09/remoteEntry.js',
    };

// Dùng remotes trong instance ModuleFederationPlugin hiện có.
new ModuleFederationPlugin({
  name: 'shop_shell',
  remotes,
  shared: {
    react: {
      singleton: true,
      requiredVersion: dependencies.react,
      strictVersion: true,
    },
    'react-dom': {
      singleton: true,
      requiredVersion: dependencies['react-dom'],
      strictVersion: true,
    },
    'react-dom/client': {
      singleton: true,
      packageName: 'react-dom',
      requiredVersion: dependencies['react-dom'],
      strictVersion: true,
    },
  },
});
Đối chiếu apps/shell/webpack.config.cjs

ModuleFederationPlugin và dependencies là các biến đã có trong config lab đầu tiên. Giữ entry, JSX loader, resolve.extensions, publicPath, uniqueName, HTML plugin và async bootstrap. Thay option remotes của plugin hiện có, không thêm một plugin Federation thứ hai. Giữ shared policy phù hợp với JSX runtime thật nếu bạn đã thay cấu hình lab.

Nhánh production trong trích đoạn chỉ minh họa sự tách biệt. Dùng release mapping hoặc loader production của bạn thay cho URL mẫu; không đưa profile local vào đường build production. Với hệ thống dùng dynamic remotes sẵn, áp dụng profile ở lớp config của loader đó thay vì thêm một loader khác song song.

Tạo lệnh chạy theo công việc

Bổ sung scripts root sau khi helper/config đã được nối:

shop-workspace/package.json — scripts chọn profile
{
  "scripts": {
    "dev:shell:catalog": "npm run dev --workspace=@shop/shell -- --env profile=catalog-local",
    "dev:shell:integration": "npm run dev --workspace=@shop/shell -- --env profile=integration",
    "dev:catalog-in-shell": "concurrently --kill-others --names shell,catalog \"npm run dev:shell:catalog\" \"npm run dev:catalog\""
  }
}
Đối chiếu fragments/package-profile-scripts.json

Chạy công việc catalog bằng:

npm run dev:catalog-in-shell

Bạn chỉ cần hai process local. Shell log phải cho thấy catalog ở port 3001 và cart ở release k09. Nếu shell import cart ngay khi bootstrap, artifact cart phải truy cập được dù bạn chưa mở UI cart.

Muốn chỉ sửa shell, chạy npm run dev:shell:integration. Muốn cả ba local, dùng npm run dev:all; profile mặc định trong helper là all-local.

--env profile=... là input của webpack config, không tự tạo process.env.profile trong code browser. Profile không tồn tại phải fail sớm thay vì âm thầm dùng integration hoặc production. Khi dừng focused mode rồi chuyển sang all-local, mở document mới hoặc reload để bỏ module cache của lượt chạy cũ.

Khi nào cần override ở runtime

Build-time profiles đủ cho phần lớn vòng lặp local. Runtime overrides hữu ích khi muốn mở một preview shell có sẵn với remote của một PR mà không rebuild shell, nhưng chỉ hoạt động nếu shell có runtime config/loader hỗ trợ.

Mình giữ policy đơn giản:

  • Override chỉ có hiệu lực trên local hoặc môi trường preview được phép; build production không đọc query/local storage để chọn URL tùy ý.
  • Chỉ cho alias đã biết, origin/path được cho phép và release phù hợp. Không biến ?catalog=https://... thành script injection API.
  • Chọn mapping trước import đầu tiên, ghi snapshot đã resolve, rồi giữ nó cho vòng đời document.
  • Đổi release hoặc reset override bằng một document mới. Xóa script tag không unload container, shared singleton hay module references đã dùng.

Nếu có nhiều nguồn config, quy định thứ tự rõ, ví dụ override preview hợp lệ → release set của preview → baseline đã pin. Giá trị override sai phải báo lỗi; không nên fallback im lặng làm bạn tưởng đang chạy candidate trong khi browser thực tế tải baseline.

Xem Dynamic remotes cho loader và snapshot policy. Remote đến từ HTTPS shell nhưng dùng HTTP localhost cũng có thể gặp mixed-content hoặc giới hạn truy cập mạng local tùy browser; local shell hoặc HTTPS tunnel được kiểm soát thường dễ vận hành hơn việc tắt security checks.

Mocks đúng ranh giới

Mock API khác mock remote

Hai loại mock phục vụ hai câu hỏi khác nhau:

LoạiThay phần nào?Dùng đểKhông kiểm tra được
API mockResponse HTTP như /api/productsUI và state với dữ liệu/lỗi deterministicAuth, backend validation và business rules thật
Component/lifecycle stubModule public ở vùng mount của shellLayout và fallback mà không chạy remoteEntry, sharing, chunks và exports của artifact thật
Stub Federation remoteContainer thật expose implementation tối thiểuĐường load khi chưa có remote domainHành vi và contract đầy đủ của remote domain

Đừng trả JSON hoặc HTML ở /remoteEntry.js rồi gọi đó là remote mock. Nếu test cần đường tải Federation, stub phải là container hợp lệ. Nếu chỉ test UI shell, inject stub ở composition boundary và ghi rõ đang bỏ qua remote loading.

Với công việc catalog, mình ưu tiên catalog thật + API mocks. Bạn giữ được code UI đang sửa và chỉ loại dependency backend khỏi vòng lặp.

Thiết lập MSW cho shell local

Ví dụ này dùng MSW major 2, với API http, HttpResponse và onUnhandledRequest của major đó. Nó bổ sung API mock cho một catalog đã chuyển sang gọi /api/products; catalog trong lab đầu tiên dùng mảng hard-code nên chưa tự gọi endpoint này.

Trong chế độ tích hợp, shell sở hữu một mock worker cho document shell. Không khởi động thêm worker trong module catalog được expose. Từ root workspace:

npm install --save-dev --save-exact msw@2 --workspace=@shop/shell
mkdir -p apps/shell/public
npm exec --workspace=@shop/shell -- msw init public --save

CLI tạo apps/shell/public/mockServiceWorker.js và lưu worker directory trong manifest của shell. Commit generated worker cùng source mocks; sau nâng MSW, sinh lại worker theo version đã cài.

Thay static: false trong dev server shell bằng directory phục vụ worker, giữ các options khác:

apps/shell/webpack.config.cjs — option devServer
devServer: {
  host: 'localhost',
  static: {
    directory: path.resolve(__dirname, 'public'),
  },
},
Đối chiếu apps/shell/fragments/dev-server.js

path là import node:path đã có trong config. /mockServiceWorker.js phải trả JavaScript từ origin shell, không phải HTML do history fallback. localhost được browser xem là môi trường phù hợp cho service worker; khi truy cập qua hostname hoặc IP mạng khác, cần kiểm tra secure context và HTTPS.

apps/shell/src/mocks/handlers.js
import { delay, http, HttpResponse } from 'msw';

const products = [
  { sku: 'ao-thun-den', name: 'Áo thun đen', price: 150000 },
  { sku: 'tui-vai', name: 'Túi vải', price: 80000 },
  { sku: 'binh-nuoc', name: 'Bình nước', price: 120000 },
];

export const handlers = [
  http.get('/api/products', async ({ request }) => {
    const scenario = new URL(request.url).searchParams.get('scenario');
    await delay(300); // Độ trễ minh họa để nhìn thấy loading state.

    if (scenario === 'error') {
      return HttpResponse.json(
        { code: 'CATALOG_UNAVAILABLE' },
        { status: 503 },
      );
    }

    return HttpResponse.json({
      products: scenario === 'empty' ? [] : products,
    });
  }),
];
Đối chiếu apps/shell/src/mocks/handlers.js
apps/shell/src/mocks/browser.js
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';

export const worker = setupWorker(...handlers);
Đối chiếu apps/shell/src/mocks/browser.js

Ba request /api/products, /api/products?scenario=empty và /api/products?scenario=error lần lượt tạo success, empty và error. Query này là convention của mock endpoint, không tự đọc query trên URL trang shell. Muốn nút chọn scenario trong dev UI, dev adapter phải chuyển lựa chọn đó thành request tương ứng; không bắt production API hỗ trợ tham số mock.

Khởi động worker trước khi render

Trong config function webpack của shell, thêm DefinePlugin vào plugins hiện có:

apps/shell/webpack.config.cjs — plugin flag dành cho browser
const { DefinePlugin } = require('webpack');

// Thêm instance này vào plugins trong config function.
new DefinePlugin({
  __SHOP_MOCK_API__: JSON.stringify(
    argv.mode === 'development' &&
    (env.mockApi === true || env.mockApi === 'true')
  ),
});
Đối chiếu apps/shell/fragments/mock-flag.js

Thêm import DefinePlugin cạnh các imports webpack hiện có, còn instance plugin nằm trong config function để đọc được env và argv. Flag chỉ bật ở development; --env mockApi=false không được hiểu thành true chỉ vì chuỗi không rỗng. Nếu dùng TypeScript, khai báo type cho global flag trong file declarations của app.

Giữ async bootstrap và await worker trước khi code UI bắt đầu request:

apps/shell/src/index.js — thay entry bootstrap hiện có
async function start() {
  if (__SHOP_MOCK_API__) {
    const { worker } = await import('./mocks/browser.js');
    await worker.start({
      serviceWorker: { url: '/mockServiceWorker.js' },
      onUnhandledRequest(request, print) {
        const url = new URL(request.url);
        if (url.pathname.startsWith('/api/')) {
          print.error();
        }
      },
    });
  }

  await import('./bootstrap.jsx');
}

start().catch((error) => {
  console.error('[shell] Startup failed', error);
});
Đối chiếu apps/shell/src/index.js

Handler không khai báo cho /api/ sẽ tạo lỗi thay vì âm thầm đi backend. Asset, entry và chunk không bị coi là API mock bị thiếu. Không coi cách phân loại theo path này là allowlist production; nếu app gọi API với prefix khác, cập nhật policy cho đúng.

Khởi động catalog ở một terminal, shell có API mock ở terminal khác:

Terminal 1 — root shop-workspace
npm run dev:catalog
Terminal 2 — root shop-workspace
npm run dev --workspace=@shop/shell -- --env profile=catalog-local --env mockApi=true

Trên tab shell, sau khi worker báo sẵn sàng, thử:

DevTools Console — origin localhost:3000
await fetch('/api/products?scenario=empty').then((response) => response.json());
// Kết quả mong đợi: { products: [] }

Worker kiểm soát document shell, nên có thể intercept request phù hợp phát sinh từ remote chạy trong document đó. Mở catalog standalone ở 3001 là document và origin khác: nó cần dev bootstrap/worker riêng nếu muốn dùng cùng API mocks. Đừng import file nội bộ từ shell/src vào catalog để chia sẻ handlers; nếu nhiều app thật sự cần chung fixtures, đưa contract fixtures vào package có public entry hoặc phân phối snapshot có version.

Bật mock phải dễ nhìn thấy và dễ tắt

Hiển thị dev badge hoặc log rõ API mocks enabled. Khi chuyển sang backend thật, tắt flag, tạo document mới và kiểm tra service worker của đúng origin trong DevTools Application. Chỉ unregister worker mock liên quan nếu còn stale state; đừng xóa mọi storage hoặc worker trên máy. Production build/deployment cũng cần kiểm tra không ship mock worker hay kích hoạt mock bootstrap ngoài ý muốn.

Dùng fixture như một contract

Fixture phải theo schema mà app thực sự consume: kiểu ID, currency, pagination, error body và field optional. TypeScript types ở checkout hiện tại chưa xác minh payload từ remote/backend build vào thời điểm khác.

Ngoài happy path, thêm dữ liệu rỗng, request chậm, lỗi có mã mà UI xử lý, text dài và dữ liệu biên hợp lệ. Dùng dữ liệu giả có thể commit; không lấy response production chứa thông tin cá nhân rồi đưa vào repo.

Một mock luôn trả 200 và mảng đủ đẹp chỉ chứng minh UI chạy với fixture đẹp. Trước merge, chạy smoke với backend/test account thật theo policy và kiểm tra fixtures bằng public schema hoặc contract tests. Nếu đang sửa logic thêm giỏ, đừng để mock cart chấp nhận mọi SKU trong khi cart thật reject SKU không tồn tại.

Proxy API và authentication

Khi cần backend thật, proxy API qua origin shell giúp giảm cấu hình cross-origin ở browser. Với webpack-dev-server major 5, cấu hình proxy dùng array:

apps/shell/webpack.config.cjs — trích devServer khi dùng backend local
devServer: {
  host: 'localhost',
  proxy: [
    {
      context: ['/api'],
      target: 'http://localhost:4000',
      changeOrigin: true,
    },
  ],
},
Đối chiếu apps/shell/webpack.config.cjs

Ghép vào config đang có thay vì ghi đè static phục vụ mock worker nếu vẫn cần nó cho chế độ khác. Backend phải hiểu path /api/...; nếu backend dùng path khác, cấu hình rewrite theo API thực tế. Remote gọi URL absolute sẽ không tự đi qua proxy này.

changeOrigin sửa Host header ở request gửi tới backend; nó không xử lý mọi vấn đề authentication. Cookie có Domain của môi trường integration, Secure, SameSite hoặc Path không phù hợp vẫn có thể không được browser gửi/lưu như mong đợi. Redirect login, CSRF và callback URL cũng phải hỗ trợ origin local hoặc được kiểm tra trong preview HTTPS.

Dùng test account và môi trường có dữ liệu kiểm thử. Không copy cookie production hay token thật vào profile, URL, source hoặc fixture. Biến được DefinePlugin/bundler nhúng vào JavaScript là dữ liệu public cho browser, không phải nơi giữ secret.

API mock xử lý request trước backend, nên để mock bật rồi kiểm tra proxy có thể cho kết luận sai. Khi debug backend, xác nhận flag mock tắt và Network cho thấy request/response của backend thật. Mình không chọn nới CORS toàn bộ, allowedHosts: 'all' hoặc secure: false làm bước sửa lỗi mặc định; hãy xác định request và policy nào đang chặn trước.

HMR và reload khi thay remote

Một remote cập nhật được trong trang standalone không đảm bảo nó hot-update được trong shell. Đường HMR còn phụ thuộc dev-server client, WebSocket endpoint, runtime integration và React Refresh. hot: true không tự cài React Refresh, cũng không hứa giữ state khi thay remote.

Mình dùng full reload làm baseline của integration rồi mới tối ưu HMR:

Bạn vừa đổiThao tác baseline
Component catalogChờ catalog compile xong, reload shell và kiểm tra UI
Webpack config hoặc scope/exposeRestart dev server liên quan, reload shell
Profile remote URLRestart shell với profile mới, tạo document mới
Shared dependency/versionRestart các app bị tác động, tạo document mới
Handler hoặc flag API mockChờ rebuild hoặc restart khi đổi flag, tạo document mới và kiểm tra worker

Nếu WebSocket lỗi, kiểm tra browser đang connect tới dev server nào. Khi có reverse proxy hoặc HTTPS tunnel, có thể cần devServer.client.webSocketURL phù hợp với địa chỉ browser nhìn thấy. HTTP entry đúng không chứng minh WebSocket cũng được forward; ngược lại, HMR socket lỗi không nhất thiết làm lượt tải module đầu tiên thất bại.

Disable cache trong Network chủ yếu tác động HTTP cache khi DevTools mở. Nó không gỡ module đã evaluate khỏi runtime và không tự bỏ service worker/cache storage. Reload tạo document mới; stale response từ service worker vẫn cần xử lý ở tầng riêng.

Quy trình sửa catalog từ đầu đến cuối

Giả sử bạn đang thêm empty state cho bộ lọc catalog. Một lượt làm việc có thể đi theo thứ tự sau:

  1. Chọn phạm vi: logic chỉ nằm trong catalog, public props/event không đổi. Dùng standalone để sửa UI, focused integration để kiểm tra trong shell.
  2. Chuẩn bị baseline: npm ci từ lockfile ở checkout mới, kiểm tra port, chọn profile catalog-local với cart release đã pin. Nếu dùng API mocks, bật flag rõ ràng.
  3. Khởi động: chạy catalog và shell, đợi compile/worker readiness rồi mở http://localhost:3000.
  4. Xác nhận đang dùng candidate: xem log profile và Network. Catalog entry/chunks phải đến từ 3001; cart từ release đã chọn. Đổi tạm một text catalog để xác nhận source local xuất hiện trong shell, rồi hoàn tác.
  5. Kiểm tra states: success, empty, loading, error; đổi route rồi quay lại để kiểm tra state/lifecycle theo contract. Không chỉ xác nhận lần render đầu.
  6. Kiểm tra ranh giới thật: tắt API mock và chạy smoke với test backend nếu thay đổi liên quan dữ liệu. Kiểm tra routing, auth context và interaction với cart trong shell.
  7. Kiểm tra build: build catalog và shell bằng script đã có; chạy smoke trên artifact production trong preview/integration. Dev server xanh không chứng minh asset paths, chunks và shared behavior của production build đúng.
  8. Ghi bằng chứng rồi dừng: ghi candidate commit, profile, release cart, scenario và kết quả vào PR. Dừng các process local, kiểm tra không còn server sót giữ port.

Nếu bạn đổi payload thêm giỏ, all-local có thể cần để sửa cả hai phía, nhưng vẫn phải test catalog mới với cart đã release. Producer/consumer cùng source mới pass không chứng minh rollout độc lập an toàn.

Một ca lỗi đáng thử trước PR: giữ shell, dừng catalog, rồi mở document mới và vào route catalog. Fallback phải nằm ở vùng catalog trong khi phần shell độc lập còn dùng được. Dừng server sau khi module đã tải mà UI vẫn còn trên màn hình không phải bài test remote unavailable.

Local development trong multi-repo

Focused integration không yêu cầu shell, catalog và cart cùng repository. Điều quan trọng là có cấu hình composition dùng được mà không cần checkout mọi app.

Một dev kit cho multi-repo nên cung cấp:

Thông tinNội dung cần có
BaselineShell release hoặc shell checkout, remote release set đã pin
Public contractAlias, scope, expose, props/events/lifecycle và compatibility window
Local overridePort, profile/loader policy, cách reset override
API/authTest backend, test account, fixtures và proxy conventions
DiagnosticsMapping đã resolve, release IDs và owner của từng app

Bạn có thể clone shell/catalog hoặc dùng shell artifact qua local composition server rồi override catalog. Phương án thứ hai đòi shell thực sự hỗ trợ runtime config; không phải tải một shell bất kỳ là đổi được remote URL.

Shared packages trong multi-repo nên được publish/pin hoặc consume bằng cơ chế đã thống nhất. npm link tiện cho một thử nghiệm, nhưng có thể đổi dependency resolution và tạo React instance ngoài shared policy dự kiến. Đừng coi kết quả từ linked source là kết quả kiểm tra artifact release; hãy kiểm tra lại package/artifact đúng version.

Khi chia sẻ bug, ghi source commit của app local và release IDs của baseline, không chỉ “cart integration”. Thêm scenario mock và resolved entry URL, bỏ token hoặc query nhạy cảm khỏi log. Đây là phần nhỏ nhưng giúp người khác tái hiện mà không đoán bạn đã mở bao nhiêu terminal.

Chẩn đoán theo lớp

Debug từ process và tài nguyên ngoài vào runtime rồi mới tới UI. Lỗi request đầu tiên thường hữu ích hơn một loạt exception phát sinh sau đó.

Dấu hiệuKiểm tra trướcHướng xử lý
EADDRINUSE hoặc UI không giống source hiện tạiProcess giữ port, cwd và profileDừng process đã nhận diện; không đổi port tùy tiện
Shell chỉ chạy được khi tất cả repo có mặtDeep imports, dependencies ngầm, context quá rộngThu hẹp public contract; không dùng mocks để che boundary bị phá
Entry 404 hoặc trả HTMLURL, Federation plugin, proxy/history fallbackKiểm tra body/MIME; không coi status 200 là đủ
Entry local nhưng UI vẫn baselineProfile thực tế, module cache và tab cũRestart shell nếu đổi build config, mở document mới
Entry đúng nhưng chunk 404URL chunk, publicPath, artifact retentionSửa base URL và giữ graph entry/chunks nhất quán
Standalone HMR tốt, shell không cập nhậtWebSocket endpoint và integration HMRDùng reload baseline; kiểm tra proxy WebSocket riêng
Worker không đăng ký đượcOrigin, secure context, script URL và scopePhục vụ worker tại origin document, không trả HTML fallback
Mock pass nhưng backend failFlag mock, schema, auth/cookie/CSRFTắt mock, kiểm tra request thật và contract
CORS/CSP/mixed-content errorLoại request và policy cụ thể trong ConsoleCấu hình origin/sources đúng; không tắt bảo mật browser
Invalid hook call hoặc shared init lỗiHooks, dependency resolution, React sharing và loader ownerKhông mặc định mọi lỗi đều do cache; không nuốt lỗi init
All-local pass, release candidate failConsumer version, production chunks và shared policyTest candidate với baseline artifacts, không chỉ HEAD cùng revision

Classic script remote, ESM remote và fetch manifest/API có yêu cầu CORS khác nhau. Nếu Console không báo CORS, đừng thêm wildcard header như một phản xạ. Tương tự, cho phép origin remote ở script-src chưa đủ nếu chunks hoặc HMR connections đến nguồn khác; đối chiếu từng request với policy hiện hành.

Checklist trước khi gửi PR

  • Có thể chạy app standalone hoặc focused integration mà không checkout app không liên quan.
  • Các port, hostname và resolved remote mapping đúng, không có server cũ giữ port.
  • Remote baseline được pin bằng release ID, entry và chunks nhất quán.
  • Shell consume public module thật, không import implementation xuyên apps.
  • API mock có success, empty, loading và error scenarios phù hợp contract.
  • Mock worker có một owner cho mỗi document; startup đợi worker trước request đầu tiên.
  • Mocks và remote overrides không hoạt động ngoài môi trường được phép.
  • Đã kiểm tra trong shell với routing/context/consumer thật, không chỉ standalone.
  • Có smoke trên production artifacts khi thay đổi chạm composition, assets hoặc sharing.
  • PR ghi app commit, baseline releases và scenario đủ để tái hiện, không chứa credential.

Để kiểm chứng setup ngay, hãy chạy catalog-local, xác nhận một thay đổi text chỉ ở catalog xuất hiện trong shell, rồi mở document mới khi catalog đã tắt. Hai phép thử này cho biết bạn đang sửa đúng remote và đang xử lý đúng boundary tải lỗi trước khi tối ưu tốc độ HMR.

Tài liệu tham khảo

Đọc tiếp

On this page