Chapter 02 Development Environment Setup and Tools

Haiyue
24min

Chapter 2: Development Environment Setup and Tools

Learning Objectives
  1. Configure Chrome Extension development environment
  2. Master developer tools and debugging techniques
  3. Understand commonly used development tools and frameworks

Knowledge Summary

Development Environment Components

The Chrome Extension development environment mainly consists of the following parts:

  • Browser Environment: Chrome browser (dev or stable version)
  • Code Editor: VS Code, WebStorm, etc.
  • Version Control: Git
  • Build Tools: Webpack, Vite, Parcel (optional)
  • Development Frameworks: React, Vue, Angular (optional)

Development Toolchain Architecture

🔄 正在渲染 Mermaid 图表...

Basic Development Environment Setup

Essential Tool Installation

Environment Check Commands:

# Check if tools are installed
google-chrome --version    # Chrome browser
node --version             # Node.js
npm --version              # NPM
git --version              # Git
code --version             # VS Code

Chrome Developer Mode Settings:

  1. Open Chrome browser
  2. Visit chrome://extensions/
  3. Enable “Developer mode” toggle in the top right corner

Project Directory Structure:

my-chrome-extension/
├── src/
│   ├── background/      # Background scripts
│   ├── content/         # Content scripts
│   ├── popup/           # Popup pages
│   ├── options/         # Options pages
│   └── assets/
│       ├── icons/       # Icons
│       └── images/      # Image resources
├── dist/                # Build output
├── test/                # Test files
└── docs/                # Documentation

package.json Configuration:

{
  "name": "my-chrome-extension",
  "version": "1.0.0",
  "description": "Chrome Extension project",
  "scripts": {
    "build": "webpack --mode production",
    "dev": "webpack --mode development --watch",
    "test": "jest",
    "lint": "eslint src/"
  },
  "devDependencies": {
    "webpack": "^5.0.0",
    "webpack-cli": "^4.0.0",
    "copy-webpack-plugin": "^9.0.0",
    "clean-webpack-plugin": "^4.0.0",
    "eslint": "^8.0.0",
    "jest": "^27.0.0"
  }
}

.gitignore Configuration:

# Dependencies
node_modules/

# Build output
dist/
build/
*.zip

# IDE files
.vscode/
.idea/
*.swp
*.swo

# OS files
.DS_Store
Thumbs.db

# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Environment variables
.env
.env.local

# Test coverage
coverage/

VS Code Extension Configuration

Recommended VS Code Extensions (.vscode/extensions.json):

{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "christian-kohler.path-intellisense",
    "formulahendry.auto-rename-tag",
    "ritwickdey.liveserver",
    "ms-vscode.vscode-typescript-tslint-plugin",
    "octref.vetur",
    "dsznajder.es7-react-js-snippets",
    "msjsdiag.debugger-for-chrome",
    "naumovs.color-highlight"
  ]
}

Workspace Settings (.vscode/settings.json):

{
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "editor.tabSize": 2,
  "editor.wordWrap": "on",
  "files.autoSave": "afterDelay",
  "files.autoSaveDelay": 1000,
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact"
  ],
  "emmet.includeLanguages": {
    "javascript": "javascriptreact"
  },
  "search.exclude": {
    "**/node_modules": true,
    "**/dist": true,
    "**/build": true
  }
}

Code Snippets (.vscode/chrome-extension.code-snippets):

{
  "Chrome Extension Manifest": {
    "prefix": "manifest",
    "body": [
      "{",
      "  \"manifest_version\": 3,",
      "  \"name\": \"${1:Extension Name}\",",
      "  \"version\": \"${2:1.0.0}\",",
      "  \"description\": \"${3:Description}\",",
      "  \"permissions\": [${4}],",
      "  \"action\": {",
      "    \"default_popup\": \"popup.html\"",
      "  }",
      "}"
    ],
    "description": "Chrome Extension Manifest V3 template"
  },
  "Content Script": {
    "prefix": "content",
    "body": [
      "// Content script for Chrome Extension",
      "console.log('Content script loaded');",
      "",
      "// Interact with the page DOM",
      "document.addEventListener('DOMContentLoaded', () => {",
      "  ${1:// Your code here}",
      "});"
    ],
    "description": "Content script template"
  },
  "Chrome Storage Get": {
    "prefix": "chromeget",
    "body": [
      "chrome.storage.${1|local,sync|}.get(['${2:key}'], (result) => {",
      "  console.log('Value:', result.${2:key});",
      "  ${3:// Handle the result}",
      "});"
    ],
    "description": "Chrome storage get template"
  }
}

Debug Launch Configuration (.vscode/launch.json):

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Launch Chrome Extension",
      "type": "chrome",
      "request": "launch",
      "url": "http://localhost:3000",
      "webRoot": "${workspaceFolder}/src",
      "runtimeArgs": [
        "--load-extension=${workspaceFolder}/dist"
      ]
    }
  ]
}

Using Developer Tools

Chrome DevTools Features Explained

DevTools Panel Functions:

PanelFunction
ElementsInspect and modify DOM/CSS
ConsoleJavaScript console
SourcesDebug JavaScript code
NetworkMonitor network requests
PerformancePerformance analysis
MemoryMemory analysis
ApplicationStorage and cache management
SecuritySecurity checks

Extension Debugging Guide:

Background Script Debugging:

  1. Open chrome://extensions/
  2. Find your extension
  3. Click the “Service Worker” link
  4. DevTools will open, showing the background script console

Popup Page Debugging:

  1. Right-click the extension icon
  2. Select “Inspect popup”
  3. DevTools will open, showing the popup page

Content Script Debugging:

  1. Press F12 on the target web page
  2. Go to the Sources panel
  3. Find the Content Scripts section on the left
  4. Select your extension script

Useful Console Commands:

CommandDescription
$_Returns the value of the most recent expression
$0-$4Returns recently selected DOM elements
$(selector)Equivalent to document.querySelector
$$(selector)Equivalent to document.querySelectorAll
clear()Clear the console
copy(object)Copy object to clipboard
debug(function)Set breakpoint at function entry
monitor(function)Monitor function calls
monitorEvents(element, events)Monitor element events
getEventListeners(element)Get element’s event listeners

Extension-Specific Commands:

// Get manifest information
chrome.runtime.getManifest()

// Get extension ID
chrome.runtime.id

// Get extension details
chrome.management.getSelf(console.log)

// View local storage
chrome.storage.local.get(null, console.log)

// View permissions
chrome.permissions.getAll(console.log)

Performance Analysis Tools:

ToolPurposeKey Metrics
Performance PanelRecord runtime performanceFPS, CPU usage, network activity, memory
Memory PanelMemory analysis and leak detectionHeap size, object count, memory allocation
Coverage PanelCode coverage analysisJS coverage, CSS coverage

Debugging Techniques and Best Practices

Breakpoint Strategies Explained:

Breakpoint TypeDescriptionHow to Use
Conditional BreakpointPause only under specific conditions (e.g., i === 5)Right-click line number → Add conditional breakpoint
LogpointDon’t pause, only output logRight-click line number → Add logpoint
DOM BreakpointTriggered on DOM changes (subtree modifications, attribute modifications, node removal)Elements panel → Right-click element → Break on
XHR/Fetch BreakpointTriggered on network requestsSources panel → XHR/fetch Breakpoints
Event Listener BreakpointPause when specific events trigger (mouse, keyboard, touch, etc.)Sources panel → Event Listener Breakpoints

Error Handling Pattern Examples:

// 1. Global error capture
window.addEventListener('error', (event) => {
  console.error('Global error:', event.error);
  // Send error report
  reportError({
    message: event.message,
    source: event.filename,
    line: event.lineno,
    column: event.colno,
    error: event.error.stack
  });
});

// 2. Promise error capture
window.addEventListener('unhandledrejection', (event) => {
  console.error('Unhandled promise rejection:', event.reason);
  event.preventDefault();
});

// 3. Chrome Extension error handling
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  try {
    // Handle message
    handleMessage(request, sender, sendResponse);
  } catch (error) {
    console.error('Message handling error:', error);
    sendResponse({ success: false, error: error.message });
  }
  return true;  // Keep message channel open
});

// 4. Async error handling
async function safeAsyncOperation() {
  try {
    const result = await riskyOperation();
    return { success: true, data: result };
  } catch (error) {
    console.error('Async operation failed:', error);
    return { success: false, error: error.message };
  }
}

// 5. Development environment debugging helper
const DEBUG = true;  // Set to false in production

function debugLog(...args) {
  if (DEBUG) {
    console.log('[DEBUG]', new Date().toISOString(), ...args);
  }
}

// 6. Performance monitoring
function measurePerformance(functionName, fn) {
  return function(...args) {
    const startTime = performance.now();
    const result = fn.apply(this, args);
    const endTime = performance.now();
    console.log(`${functionName} took ${endTime - startTime}ms`);
    return result;
  };
}

Remote Debugging Configuration:

Start Chrome Remote Debugging:

chrome --remote-debugging-port=9222
  • Purpose: Allow external tools to connect for debugging
  • Uses: Automated testing, remote development, CI/CD integration

Connect to Remote Debugging:

  • URL: http://localhost:9222
  • Supported tools: Chrome DevTools, VS Code, Puppeteer
  • Capabilities: Inspect pages, execute scripts, performance analysis

Extension Auto-Reload:

  • Tool: Extension Reloader
  • Benefit: Automatically reload extension after code changes

Common Development Tools and Frameworks

Build Tool Configuration

Webpack Configuration Example (webpack.config.js):

// webpack.config.js
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: process.env.NODE_ENV || 'development',
  devtool: 'inline-source-map',

  entry: {
    popup: './src/popup/popup.js',
    options: './src/options/options.js',
    background: './src/background/background.js',
    content: './src/content/content.js'
  },

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name]/[name].js'
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        use: {
          loader: 'file-loader',
          options: {
            name: '[name].[ext]',
            outputPath: 'assets/'
          }
        }
      }
    ]
  },

  plugins: [
    new CleanWebpackPlugin(),

    new CopyWebpackPlugin({
      patterns: [
        { from: 'src/manifest.json' },
        { from: 'src/assets', to: 'assets' }
      ]
    }),

    new HtmlWebpackPlugin({
      template: 'src/popup/popup.html',
      filename: 'popup/popup.html',
      chunks: ['popup']
    }),

    new HtmlWebpackPlugin({
      template: 'src/options/options.html',
      filename: 'options/options.html',
      chunks: ['options']
    })
  ],

  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          priority: 10
        }
      }
    }
  }
};

Vite Configuration Example (vite.config.js):

// vite.config.js
import { defineConfig } from 'vite';
import { crx } from '@crxjs/vite-plugin';
import manifest from './src/manifest.json';

export default defineConfig({
  plugins: [
    crx({ manifest })
  ],

  build: {
    rollupOptions: {
      input: {
        popup: 'src/popup/index.html',
        options: 'src/options/index.html'
      },
      output: {
        entryFileNames: '[name]/[name].js',
        chunkFileNames: 'chunks/[name]-[hash].js',
        assetFileNames: 'assets/[name]-[hash].[ext]'
      }
    }
  },

  server: {
    port: 3000,
    hmr: {
      port: 3001
    }
  }
});

NPM Scripts Configuration:

{
  "scripts": {
    "dev": "webpack --mode development --watch",
    "build": "webpack --mode production",
    "build:firefox": "cross-env TARGET=firefox webpack --mode production",
    "clean": "rimraf dist",
    "test": "jest",
    "test:watch": "jest --watch",
    "lint": "eslint src/ --ext .js,.jsx",
    "lint:fix": "eslint src/ --ext .js,.jsx --fix",
    "format": "prettier --write 'src/**/*.{js,jsx,css,html}'",
    "analyze": "webpack-bundle-analyzer dist/stats.json",
    "zip": "node scripts/zip.js",
    "release": "npm run clean && npm run build && npm run zip"
  }
}

Framework Integration Examples

React Integration Example:

// React Popup Component Example
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';

function PopupApp() {
  const [tabs, setTabs] = useState([]);
  const [activeTab, setActiveTab] = useState(null);

  useEffect(() => {
    // Get all tabs
    chrome.tabs.query({}, (tabs) => {
      setTabs(tabs);
    });

    // Get current active tab
    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
      if (tabs[0]) {
        setActiveTab(tabs[0]);
      }
    });
  }, []);

  const handleTabClick = (tabId) => {
    chrome.tabs.update(tabId, { active: true });
  };

  return (
    <div className="popup-container">
      <h2>Tab Manager</h2>
      <div className="current-tab">
        {activeTab && (
          <div>
            <strong>Current Tab:</strong>
            <p>{activeTab.title}</p>
          </div>
        )}
      </div>
      <div className="tab-list">
        <h3>All Tabs</h3>
        {tabs.map(tab => (
          <div
            key={tab.id}
            className="tab-item"
            onClick={() => handleTabClick(tab.id)}
          >
            <img src={tab.favIconUrl} width="16" height="16" />
            <span>{tab.title}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

ReactDOM.render(<PopupApp />, document.getElementById('root'));

Vue Integration Example:

<!-- Vue Popup Component Example -->
<template>
  <div id="app">
    <h2>{{ title }}</h2>
    <div class="settings">
      <label>
        <input type="checkbox" v-model="darkMode" @change="saveSetting">
        Enable Dark Mode
      </label>
      <label>
        <input type="checkbox" v-model="notifications" @change="saveSetting">
        Enable Notifications
      </label>
    </div>
    <div class="actions">
      <button @click="captureTab">Capture Current Page</button>
      <button @click="clearData">Clear Data</button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'PopupApp',
  data() {
    return {
      title: 'Extension Settings',
      darkMode: false,
      notifications: true
    }
  },

  mounted() {
    this.loadSettings();
  },

  methods: {
    loadSettings() {
      chrome.storage.sync.get(['darkMode', 'notifications'], (result) => {
        this.darkMode = result.darkMode || false;
        this.notifications = result.notifications !== false;
      });
    },

    saveSetting() {
      chrome.storage.sync.set({
        darkMode: this.darkMode,
        notifications: this.notifications
      });
    },

    captureTab() {
      chrome.tabs.captureVisibleTab((dataUrl) => {
        // Handle screenshot
        console.log('Screenshot captured');
      });
    },

    clearData() {
      if (confirm('Are you sure you want to clear all data?')) {
        chrome.storage.sync.clear(() => {
          console.log('Data cleared');
          this.loadSettings();
        });
      }
    }
  }
}
</script>

TypeScript Configuration Example (tsconfig.json):

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "jsx": "react",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "types": ["chrome", "node"],
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

TypeScript Type-Safe Message Handling:

// Chrome API type definitions example
interface ExtensionMessage {
  type: string;
  payload: any;
}

interface TabInfo {
  id: number;
  url: string;
  title: string;
  active: boolean;
}

// Type-safe message handling
chrome.runtime.onMessage.addListener(
  (message: ExtensionMessage, sender: chrome.runtime.MessageSender,
   sendResponse: (response?: any) => void) => {
    switch(message.type) {
      case 'GET_TAB_INFO':
        const tabInfo: TabInfo = {
          id: sender.tab?.id || 0,
          url: sender.tab?.url || '',
          title: sender.tab?.title || '',
          active: sender.tab?.active || false
        };
        sendResponse(tabInfo);
        break;
    }
    return true;
  }
);

Development Workflow Optimization

Automated Workflows

Hot Reload Script (hot-reload.js):

// hot-reload.js - Development environment auto-reload script
const filesInDirectory = dir => new Promise(resolve =>
  dir.createReader().readEntries(entries =>
    Promise.all(entries.filter(e => e.name[0] !== '.').map(e =>
      e.isDirectory
        ? filesInDirectory(e)
        : new Promise(resolve => e.file(resolve))
    ))
    .then(files => [].concat(...files))
    .then(resolve)
  )
);

const timestampForFilesInDirectory = dir =>
  filesInDirectory(dir).then(files =>
    files.map(f => f.name + f.lastModified).join()
  );

const watchChanges = (dir, lastTimestamp) => {
  timestampForFilesInDirectory(dir).then(timestamp => {
    if (!lastTimestamp || (lastTimestamp === timestamp)) {
      setTimeout(() => watchChanges(dir, timestamp), 1000);
    } else {
      chrome.runtime.reload();
    }
  });
};

// Only enable in development environment
if (process.env.NODE_ENV === 'development') {
  chrome.management.getSelf(self => {
    if (self.installType === 'development') {
      chrome.runtime.getPackageDirectoryEntry(dir =>
        watchChanges(dir)
      );
    }
  });
}

Git Hooks Configuration:

.git/hooks/pre-commit:

#!/bin/sh
# Run lint checks
npm run lint

# Run tests
npm test

# Check for console.log
FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|jsx|ts|tsx)$')
if [ -n "$FILES" ]; then
    echo "$FILES" | xargs grep -n "console\.log" && {
        echo "Found console.log, please remove before committing"
        exit 1
    }
fi

exit 0

.git/hooks/pre-push:

#!/bin/sh
# Build project
npm run build

# Run full test suite
npm run test:full

exit 0

.git/hooks/commit-msg:

#!/bin/sh
# Validate commit message format
commit_regex='^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{1,50}'

if ! grep -qE "$commit_regex" "$1"; then
    echo "Commit message format error!"
    echo "Format: <type>(<scope>): <subject>"
    echo "Example: feat(popup): add new feature"
    exit 1
fi

CI/CD Pipeline Configuration (.github/workflows/ci.yml):

name: CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2

    - name: Setup Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '16'
        cache: 'npm'

    - name: Install dependencies
      run: npm ci

    - name: Run linting
      run: npm run lint

    - name: Run tests
      run: npm test -- --coverage

    - name: Build extension
      run: npm run build

    - name: Upload build artifacts
      uses: actions/upload-artifact@v2
      with:
        name: extension-build
        path: dist/

  release:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
    - uses: actions/checkout@v2

    - name: Setup Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '16'

    - name: Install dependencies
      run: npm ci

    - name: Build for production
      run: npm run build:prod

    - name: Create release package
      run: npm run package

    - name: Upload to Chrome Web Store
      env:
        CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
        CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
        REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
      run: |
        npx chrome-webstore-upload-cli upload \
          --source extension.zip \
          --extension-id ${{ secrets.EXTENSION_ID }}
Development Efficiency Tips
  1. Use Code Snippets: Create common code templates for quick boilerplate generation
  2. Configure Aliases: Set path aliases to simplify import statements
  3. Enable Hot Reload: Automatically reload extensions without manual refresh
  4. Use Debugging Tools: Fully utilize all Chrome DevTools features
  5. Version Control: Use Git for version management, commit code regularly

Chapter Summary

This chapter provided detailed coverage of Chrome Extension development environment setup and tool usage:

  1. Basic Environment Setup: Configured essential tools and project structure for development
  2. VS Code Configuration: Set up an editor environment suitable for extension development
  3. Debugging Tool Usage: Mastered various debugging techniques in Chrome DevTools
  4. Build Tool Configuration: Learned configuration methods for Webpack and Vite
  5. Framework Integration: Understood integration methods for React, Vue, and TypeScript
  6. Workflow Optimization: Configured automation tools to improve development efficiency

The next chapter will dive into detailed Manifest file configuration.

Categories