Lesson 06-Advanced Modular Tool Chain

Advanced Webpack Modular Configuration

Code Splitting (SplitChunksPlugin, Dynamic Imports)

In-Depth SplitChunksPlugin Configuration:

// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all', // Split all types of chunks (all/async/initial)
      minSize: 20000, // Minimum size for generated chunks (20KB)
      minRemainingSize: 0, // Minimum size for remaining chunks after splitting
      minChunks: 1, // Minimum number of references for a module to be split
      maxAsyncRequests: 30, // Maximum parallel requests for on-demand loading
      maxInitialRequests: 30, // Maximum parallel requests for entry points
      enforceSizeThreshold: 50000, // Size threshold to enforce splitting
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10, // Priority
          reuseExistingChunk: true // Reuse existing chunk if module is already split
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        },
        // Custom cache group
        reactVendor: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'react-vendor',
          chunks: 'all',
          priority: 20 // Higher priority
        },
        utilityVendor: {
          test: /[\\/]node_modules[\\/](lodash|moment)[\\/]/,
          name: 'utility-vendor',
          chunks: 'all'
        }
      }
    }
  }
};

Dynamic Import Best Practices:

// Basic dynamic import
const loadModule = () => import('./module.js');

// Dynamic import with Webpack magic comments
const loadModuleWithComments = () => import(
  /* webpackChunkName: "my-chunk" */ 
  /* webpackPrefetch: true */
  './module.js'
);

// Dynamic import with React.lazy
const LazyComponent = React.lazy(() => import(
  /* webpackChunkName: "lazy-component" */
  './LazyComponent'
));

// Dynamic import with error boundary
class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      return <div>Component loading failed</div>;
    }
    return this.props.children;
  }
}

const SafeLazyComponent = () => (
  <ErrorBoundary>
    <React.Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </React.Suspense>
  </ErrorBoundary>
);

Tree Shaking Principles and Configuration

Tree Shaking Core Principles:

  1. ESM Static Analysis: Webpack identifies unused exports by analyzing the static structure of ES modules.
  2. Side Effect Marking: Declares whether a module has side effects via the sideEffects field in package.json.
  3. Compression Optimization: The Terser plugin removes unreferenced code.

Configuration Example:

// webpack.config.js
module.exports = {
  optimization: {
    usedExports: true, // Mark unused exports
    minimize: true,    // Enable code minification
    concatenateModules: true // Scope hoisting
  }
};

package.json Configuration:

{
  "name": "my-library",
  "sideEffects": [
    "*.css",
    "*.global.js"
  ],
  "exports": {
    ".": {
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js"
    },
    "./styles": "./dist/styles.css"
  }
}

Pure Modules Marking:

// Mark pure functions in modules
/*#__PURE__*/
const result = expensiveCalculation();

// Or automatically add markings with Babel plugin
// babel-plugin-transform-remove-pure-annotations

Module Resolution Rules

resolve.alias Configuration:

// webpack.config.js
module.exports = {
  resolve: {
    alias: {
      '@components': path.resolve(__dirname, 'src/components'),
      '@utils': path.resolve(__dirname, 'src/utils'),
      'react': path.resolve(__dirname, 'node_modules/react') // Force specific React version
    }
  }
};

resolve.extensions Configuration:

// webpack.config.js
module.exports = {
  resolve: {
    extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
    // List of extensions that can be omitted
    // Webpack tries these extensions in order
  }
};

Advanced Resolution Configuration:

// webpack.config.js
module.exports = {
  resolve: {
    modules: [
      path.resolve(__dirname, 'src'),
      'node_modules' // Default value
    ],
    mainFields: ['browser', 'module', 'main'], // Fields to look up in order
    mainFiles: ['index'], // Default file names to look for
    symlinks: true, // Follow symbolic links
    cacheWithContext: false // Cache with context
  }
};

Caching and Persistence

Webpack Cache API:

// webpack.config.js
module.exports = {
  cache: {
    type: 'filesystem', // Use filesystem cache
    buildDependencies: {
      config: [__filename] // Invalidate cache when webpack config changes
    },
    cacheDirectory: path.resolve(__dirname, '.webpack_cache'), // Cache directory
    compression: 'gzip' // Compress cache
  }
};

HardSourceWebpackPlugin Configuration:

// webpack.config.js
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

module.exports = {
  plugins: [
    new HardSourceWebpackPlugin({
      // Cache directory
      cacheDirectory: 'node_modules/.cache/hard-source/[confighash]',
      // Config hash calculation
      configHash: function(webpackConfig) {
        return require('node-object-hash')({sort: false}).hash(webpackConfig);
      },
      // Environment hash calculation
      environmentHash: {
        root: process.cwd(),
        directories: [],
        files: ['package-lock.json', 'yarn.lock']
      },
      // Cache version
      version: '1.0.0'
    })
  ]
};

Cache Strategy Comparison:

FeatureWebpack Cache APIHardSourceWebpackPlugin
Official SupportYesThird-Party Plugin
PersistenceYesYes
Cache GranularityFine-GrainedMedium-Grained
Configuration ComplexityMediumHigh
Use CaseProduction BuildsDevelopment Builds
Cache Invalidation ControlPreciseModerate

Webpack Module Federation and Micro-Frontend Support

Complete Module Federation Configuration:

// Host application webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host_app',
      remotes: {
        app1: 'app1@http://localhost:3001/remoteEntry.js',
        app2: 'app2@http://localhost:3002/remoteEntry.js'
      },
      shared: {
        react: {
          singleton: true,
          requiredVersion: '^17.0.2'
        },
        'react-dom': {
          singleton: true,
          requiredVersion: '^17.0.2'
        },
        lodash: {
          eager: true // Load immediately instead of on-demand
        }
      }
    })
  ]
};

// Remote application webpack.config.js
new ModuleFederationPlugin({
  name: 'app1',
  filename: 'remoteEntry.js',
  exposes: {
    './Button': './src/components/Button',
    './utils': './src/utils'
  },
  shared: {
    react: {
      singleton: true,
      requiredVersion: '^17.0.2'
    },
    'react-dom': {
      singleton: true,
      requiredVersion: '^17.0.2'
    }
  }
});

Dynamic Remote Configuration:

// Dynamically load remote modules
async function loadRemoteModule(remoteName, modulePath) {
  if (!window[remoteName]) {
    await loadRemoteEntry(remoteName);
  }

  const container = window[remoteName];
  await container.init(__webpack_share_scopes__.default);
  const factory = await container.get(modulePath);
  return factory();
}

async function loadRemoteEntry(remoteName) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = `http://localhost:300${remoteName === 'app1' ? 1 : 2}/remoteEntry.js`;
    script.onload = () => resolve();
    script.onerror = reject;
    document.head.appendChild(script);
  });
}

// Usage example
loadRemoteModule('app1', './Button')
  .then(Button => {
    ReactDOM.render(<Button />, document.getElementById('root'));
  })
  .catch(err => console.error('Module loading failed', err));

Advanced Rollup Modular Configuration

Deep Optimization for Tree Shaking

Side Effect Analysis Configuration:

// rollup.config.js
export default {
  treeshake: {
    annotations: true, // Respect /*#__PURE__*/ annotations
    correctVarValueBeforeDeclaration: true, // Correct variable values before declaration
    propertyReadSideEffects: false, // Assume property access has no side effects
    tryCatchDeoptimization: false, // Do not optimize try-catch blocks
    unknownGlobalSideEffects: false // Assume unknown globals have no side effects
  }
};

Pure Module Marking:

// package.json
{
  "sideEffects": [
    "*.css",
    "*.global.js",
    "**/*.scss"
  ]
}

// Or more precise control
{
  "sideEffects": false // Entire package has no side effects
}

Manual Pure Function Marking:

// Mark pure functions in code
/*#__PURE__*/
const result = expensiveCalculation();

// Rollup recognizes these markings and optimizes unused calls

Plugin System

Custom Plugin Development:

// my-rollup-plugin.js
export default function myPlugin(options = {}) {
  return {
    name: 'my-plugin', // Plugin name (required)

    // Called at build start
    buildStart() {
      console.log('Build started');
    },

    // Called when loading individual files
    load(id) {
      if (id.endsWith('.custom')) {
        return `export default "Processed custom file content";`;
      }
      return null; // Return null to skip processing
    },

    // Called during code transformation
    transform(code, id) {
      if (id.endsWith('.js')) {
        // Simple transformation example
        return code.replace(/console\.log/g, 'console.debug');
      }
      return null;
    },

    // Called during chunk generation
    generateBundle(options, bundle) {
      // Modify generated bundle
      for (const chunk of Object.values(bundle)) {
        if (chunk.type === 'chunk') {
          chunk.code += '\n// Added trailing code';
        }
      }
    }
  };
}

Plugin Usage Example:

// rollup.config.js
import myPlugin from './my-rollup-plugin';
import babel from '@rollup/plugin-babel';
import resolve from '@rollup/plugin-node-resolve';

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.js',
    format: 'esm'
  },
  plugins: [
    myPlugin(),
    resolve(),
    babel({ exclude: 'node_modules/**' })
  ]
};

Multiple Entries and Code Splitting

Multiple Entry Configuration:

// rollup.config.js
export default [
  {
    input: 'src/main.js',
    output: {
      file: 'dist/main.js',
      format: 'cjs'
    }
  },
  {
    input: 'src/admin.js',
    output: {
      file: 'dist/admin.js',
      format: 'cjs'
    }
  }
];

Code Splitting (Using @rollup/plugin-multi-entry):

// rollup.config.js
import multi from '@rollup/plugin-multi-entry';

export default {
  input: ['src/entry1.js', 'src/entry2.js'],
  output: {
    dir: 'dist',
    format: 'esm',
    manualChunks(id) {
      if (id.includes('node_modules')) {
        return 'vendor';
      }
    }
  },
  plugins: [multi()]
};

Dynamic Import Support:

// rollup.config.js
export default {
  input: 'src/index.js',
  output: {
    dir: 'dist',
    format: 'esm',
    chunkFileNames: '[name]-[hash].js' // Naming for dynamically imported chunks
  }
};

Deep Integration of Rollup with ES Modules

ESM Output Configuration:

// rollup.config.js
export default {
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.esm.js',
    format: 'esm', // ES module format
    exports: 'named' // Named exports
  }
};

Integration with TypeScript:

// rollup.config.js
import typescript from '@rollup/plugin-typescript';

export default {
  input: 'src/index.ts',
  output: {
    file: 'dist/bundle.js',
    format: 'esm'
  },
  plugins: [
    typescript({
      tsconfig: './tsconfig.json',
      declaration: true, // Generate declaration files
      declarationDir: 'dist/types'
    })
  ]
};

Integration with Babel:

// rollup.config.js
import babel from '@rollup/plugin-babel';

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.js',
    format: 'esm'
  },
  plugins: [
    babel({
      babelHelpers: 'bundled',
      exclude: 'node_modules/**',
      presets: ['@babel/preset-env']
    })
  ]
};

Rollup in Library Development

Library Development Configuration Example:

// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import { terser } from 'rollup-plugin-terser';

export default {
  input: 'src/index.ts',
  output: [
    {
      file: 'dist/library.cjs.js',
      format: 'cjs' // CommonJS format
    },
    {
      file: 'dist/library.esm.js',
      format: 'esm' // ES module format
    },
    {
      file: 'dist/library.umd.js',
      format: 'umd', // UMD format
      name: 'MyLibrary' // UMD global variable name
    }
  ],
  plugins: [
    resolve(),
    commonjs(),
    typescript({
      tsconfig: './tsconfig.json'
    }),
    process.env.NODE_ENV === 'production' && terser() // Minify in production
  ]
};

package.json Configuration:

{
  "name": "my-library",
  "version": "1.0.0",
  "main": "dist/library.cjs.js", // CommonJS entry
  "module": "dist/library.esm.js", // ES module entry
  "browser": "dist/library.umd.js", // Browser UMD entry
  "types": "dist/types/index.d.ts", // Type declaration file
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "rollup -c",
    "build:watch": "rollup -c -w"
  },
  "devDependencies": {
    "@rollup/plugin-commonjs": "^22.0.0",
    "@rollup/plugin-node-resolve": "^13.0.0",
    "@rollup/plugin-typescript": "^8.0.0",
    "rollup": "^2.60.0",
    "rollup-plugin-terser": "^7.0.0",
    "typescript": "^4.0.0"
  }
}

Vite Modularization Principles

ESM-Based Development Server

Vite Development Server Working Principles:

  1. Native ESM Imports: Directly leverages browser-native ES module support.
  2. File System Routing: Automatic routing based on the file system.
  3. Instant Server Startup: No bundling required, fast startup.
  4. On-Demand Compilation: Compiles files only when requested by the browser.

Vite Configuration Example:

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000,
    open: true,
    fs: {
      strict: true, // Restrict access to files outside project root
      allow: ['..'] // Allow access to parent directory (use with caution)
    }
  },
  esbuild: {
    jsxFactory: 'h', // Custom JSX factory function
    jsxFragment: 'Fragment'
  }
});

Vite Development Server Features:

  • HMR (Hot Module Replacement): Extremely fast module updates.
  • Dependency Pre-Bundling: Pre-compiles node_modules dependencies into ESM.
  • CSS Handling: Native CSS imports and hot updates.
  • Static Asset Handling: Directly serves static files.

On-Demand Compilation and Hot Updates

On-Demand Compilation Mechanism:

  1. Browser Request: Browser requests an ESM module.
  2. Vite Interception: Vite server intercepts the request.
  3. On-Demand Transformation: Transforms files based on their type.
  4. Return Result: Returns transformed code.

HMR Implementation Principles:

// Example: Using Vite’s HMR API
if (import.meta.hot) {
  import.meta.hot.accept('./module.js', (newModule) => {
    // Callback when module updates
    console.log('Module updated', newModule);
  });

  import.meta.hot.dispose(() => {
    // Cleanup before module replacement
    console.log('Module about to be replaced');
  });
}

HMR Configuration Options:

// vite.config.js
export default defineConfig({
  server: {
    hmr: {
      protocol: 'ws', // Use WebSocket protocol
      host: 'localhost',
      port: 3000,
      path: '/hmr' // HMR WebSocket path
    }
  }
});

Rollup Bundling and Production Optimization

Production Build Configuration:

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig(({ mode }) => ({
  plugins: [
    react(),
    mode === 'analyze' && visualizer() // Production analysis plugin
  ],
  build: {
    minify: 'terser', // Use Terser for minification
    sourcemap: true, // Generate sourcemap
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            return 'vendor';
          }
        },
        entryFileNames: '[name].[hash].js',
        chunkFileNames: '[name].[hash].js',
        assetFileNames: '[name].[hash][extname]'
      }
    }
  }
}));

Build Optimization Strategies:

  1. Code Splitting: Properly configure manualChunks.
  2. Preload Hints: Use <link rel="modulepreload">.
  3. Resource Compression: Terser for JS, CSS minification.
  4. Caching Strategy: Content-hashed file names.

Vite’s Module Caching Mechanism

Development Environment Caching:

  1. Dependency Pre-Build Cache: node_modules/.vite directory.
  2. File System Cache: In-memory file states.
  3. HMR Cache: Module update states.

Production Environment Caching:

  1. Content-Hashed File Names: Generate hashes based on file content.
  2. Long-Term Caching Strategy: Configure correct HTTP cache headers.
  3. Service Worker Caching: Optional integration.

Cache Control Configuration:

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        assetFileNames: 'assets/[name]-[hash][extname]',
        chunkFileNames: 'js/[name]-[hash].js',
        entryFileNames: 'js/[name]-[hash].js'
      }
    }
  },
  server: {
    fs: {
      strict: true,
      cachedChecks: true // Enable cache checks
    }
  }
});

Vite Integration with Vue and React Modularization

Vite + React Integration:

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: ['@babel/plugin-transform-runtime']
      },
      fastRefresh: true // Fast refresh
    })
  ]
});

React Component Modularization Example:

// src/components/Button.jsx
import React from 'react';
import PropTypes from 'prop-types';

const Button = ({ text, onClick, variant }) => {
  return (
    <button 
      className={`btn ${variant}`} 
      onClick={onClick}
    >
      {text}
    </button>
  );
};

Button.propTypes = {
  text: PropTypes.string.isRequired,
  onClick: PropTypes.func,
  variant: PropTypes.oneOf(['primary', 'secondary'])
};

Button.defaultProps = {
  variant: 'primary'
};

export default Button;

// src/App.jsx
import React, { lazy, Suspense } from 'react';
import Button from './components/Button';

const LazyComponent = lazy(() => import('./components/LazyComponent'));

function App() {
  return (
    <div>
      <Button text="Click me" onClick={() => console.log('Clicked')} />
      <Suspense fallback={<div>Loading...</div>}>
        <LazyComponent />
      </Suspense>
    </div>
  );
}

export default App;

Vite + Vue Integration:

// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': '/src'
    }
  }
});

Vue Single File Component Modularization Example:

<!-- src/components/Button.vue -->
<template>
  <button :class="['btn', variant]" @click="onClick">
    <slot></slot>
  </button>
</template>

<script>
export default {
 Hydrology: 'AppButton',
  props: {
    variant: {
      type: String,
      default: 'primary',
      validator: value => ['primary', 'secondary'].includes(value)
    }
  },
  methods: {
    onClick() {
      this.$emit('click');
    }
  }
}
</script>

<style scoped>
.btn {
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
}
.primary {
  background-color: #1890ff;
  color: white;
}
.secondary {
  background-color: #f5f5f5;
  color: #333;
}
</style>

<!-- src/App.vue -->
<template>
  <div>
    <Button text="Click me" @click="handleClick" />
    <Suspense>
      <template #default>
        <div LazyComponent />
      </div>
      <template #fallback>
        return div>Loading...</div>
      </template>
    </Suspense>
  </div>
</template>

<script>
import { defineAsyncComponent } from 'vue';
import Button from './components/Button.vue';

const LazyComponent = defineAsyncComponent(() =>
  import('./components/LazyComponent.vue')
);

export default {
  components: {
    Button,
    LazyComponent
  },
  methods: {
    handleClick() {
      console.log('Button clicked');
    }
  }
}
</script>

Share your love