前端入门到精通(四):前端工程化与自动化测试

前端入门到精通(四):前端工程化与自动化测试

引言

随着前端技术的快速发展和前端应用复杂度的不断提升,传统的开发方式已经无法满足现代前端开发的需求。前端工程化和自动化测试作为现代前端开发的重要组成部分,能够帮助开发者提高开发效率、保证代码质量、降低维护成本。本文将带您深入了解前端工程化的各个方面,以及如何实施自动化测试,构建一个高效、稳定的前端开发生态系统。

前端工程化概述

什么是前端工程化?

前端工程化是指将软件工程的方法论应用于前端开发,通过一系列工具、规范和流程,使前端开发变得更加规范化、自动化和高效化。它涉及代码组织、构建、部署、测试等多个方面,旨在解决前端开发中的复杂性问题。

前端工程化的主要内容

前端工程化主要包含以下几个方面:

  1. 模块化:将代码拆分为可复用的模块
  2. 组件化:将UI拆分为独立、可复用的组件
  3. 规范化:制定代码规范、目录结构规范等
  4. 自动化:构建自动化、测试自动化、部署自动化等
  5. 性能优化:代码压缩、懒加载等性能优化策略

为什么需要前端工程化?

  1. 提高开发效率:通过自动化工具减少重复性工作
  2. 保证代码质量:通过规范和工具确保代码风格统一、避免常见错误
  3. 降低维护成本:清晰的代码组织和规范使代码更易于理解和维护
  4. 增强团队协作:统一的开发规范和工具链使团队协作更加顺畅
  5. 提升用户体验:通过性能优化提升应用加载速度和运行性能

构建工具

构建工具是前端工程化的核心,它们负责将源代码转换为生产环境可用的代码。常见的构建工具包括Webpack、Rollup、Parcel等。

Webpack

Webpack是目前最流行的前端构建工具之一,它是一个模块打包器,可以将各种资源(JavaScript、CSS、图片等)视为模块,然后将它们打包成一个或多个bundle。

Webpack的核心概念

  1. Entry:入口点,指定Webpack从哪个文件开始打包
  2. Output:输出,指定打包后的文件存放位置和名称
  3. Loader:加载器,用于处理非JavaScript文件
  4. Plugin:插件,用于执行各种任务,如代码压缩、环境变量注入等
  5. Mode:模式,分为development和production两种,影响默认的优化行为

Webpack的安装与配置

安装Webpack

1
npm install --save-dev webpack webpack-cli

基本配置文件 (webpack.config.js):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: ['file-loader'],
      },
    ],
  },
  plugins: [],
};

Webpack的高级配置

开发服务器

1
npm install --save-dev webpack-dev-server

webpack.config.js中添加:

1
2
3
4
5
6
7
8
9
module.exports = {
  // ...
  devServer: {
    contentBase: './dist',
    port: 3000,
    hot: true,
  },
  // ...
};

代码分割

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
module.exports = {
  // ...
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
      },
    },
  },
  // ...
};

环境变量

1
npm install --save-dev dotenv-webpack

webpack.config.js中添加:

1
2
3
4
5
6
7
8
9
const Dotenv = require('dotenv-webpack');

module.exports = {
  // ...
  plugins: [
    new Dotenv(),
  ],
  // ...
};

Rollup

Rollup是另一个流行的JavaScript模块打包器,它专注于生成更小、更快的代码,特别适合库和框架的开发。

Rollup的核心概念

  1. Input:输入文件
  2. Output:输出配置
  3. Plugins:插件系统,用于扩展功能
  4. External:外部依赖配置

Rollup的安装与配置

安装Rollup

1
npm install --save-dev rollup

基本配置文件 (rollup.config.js):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
export default {
  input: 'src/main.js',
  output: [
    {
      file: 'dist/bundle.cjs.js',
      format: 'cjs',
    },
    {
      file: 'dist/bundle.esm.js',
      format: 'esm',
    },
    {
      file: 'dist/bundle.umd.js',
      format: 'umd',
      name: 'MyLibrary',
    },
  ],
  plugins: [],
};

常用Rollup插件

1
2
# 安装常用插件
npm install --save-dev @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-babel @rollup/plugin-terser
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import { terser } from '@rollup/plugin-terser';

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

Parcel

Parcel是一个零配置的打包工具,它的设计目标是提供一个简单、快速的开发体验,特别适合快速原型开发。

Parcel的安装与使用

安装Parcel

1
npm install --save-dev parcel-bundler

基本使用

package.json中添加脚本:

1
2
3
4
5
6
{
  "scripts": {
    "dev": "parcel index.html",
    "build": "parcel build index.html"
  }
}

Parcel不需要配置文件,它会自动检测项目中的资源并进行打包。

Vite

Vite是一个新一代的前端构建工具,它基于浏览器原生的ES模块系统,提供了极快的开发服务器启动速度和热模块替换能力。

Vite的安装与使用

安装Vite

1
2
3
npm create vite@latest my-app -- --template react  # React项目
# 或
npm create vite@latest my-app -- --template vue    # Vue项目

基本使用

1
2
3
4
cd my-app
npm install
npm run dev  # 开发模式
npm run build  # 构建生产版本

Vite的配置

Vite支持在项目根目录创建vite.config.js文件来自定义配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000,
  },
  build: {
    outDir: 'dist',
  },
});

代码规范与质量控制

代码规范和质量控制是保证代码质量的重要手段,它们可以帮助团队保持一致的代码风格,减少bug,提高代码可维护性。

ESLint

ESLint是一个JavaScript代码检查工具,它可以帮助开发者发现并修复代码中的问题。

ESLint的安装与配置

安装ESLint

1
npm install --save-dev eslint

初始化ESLint

1
npx eslint --init

根据提示选择配置选项,ESLint会自动生成.eslintrc.js配置文件。

基本配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  extends: ['eslint:recommended'],
  parserOptions: {
    ecmaVersion: 12,
    sourceType: 'module',
  },
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'semi': ['error', 'always'],
    'quotes': ['error', 'single'],
  },
};

ESLint与编辑器集成

大多数现代编辑器都支持ESLint集成,可以在编码过程中实时提示错误。

VS Code配置

  1. 安装ESLint扩展
  2. 在项目根目录创建.vscode/settings.json文件:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact"
  ]
}

Prettier

Prettier是一个代码格式化工具,它可以自动格式化代码,保持代码风格的一致性。

Prettier的安装与配置

安装Prettier

1
npm install --save-dev prettier

创建配置文件 (.prettierrc.js):

1
2
3
4
5
6
7
module.exports = {
  singleQuote: true,
  trailingComma: 'es5',
  tabWidth: 2,
  semi: true,
  arrowParens: 'always',
};

与ESLint配合使用

1
npm install --save-dev eslint-config-prettier eslint-plugin-prettier

修改.eslintrc.js

1
2
3
4
5
module.exports = {
  // ...
  extends: ['eslint:recommended', 'plugin:prettier/recommended'],
  // ...
};

Husky

Husky是一个Git钩子工具,它可以在Git操作(如提交、推送)前执行自定义脚本,用于代码检查和格式化。

Husky的安装与配置

安装Husky

1
2
npm install --save-dev husky
npx husky install

在package.json中添加脚本

1
2
3
4
5
{
  "scripts": {
    "prepare": "husky install"
  }
}

添加pre-commit钩子

1
2
npx husky add .husky/pre-commit "npx eslint . --ext .js,.jsx,.ts,.tsx"
npx husky add .husky/pre-commit "npx prettier --write ."

lint-staged

lint-staged是一个工具,它可以只对Git暂存区的文件执行lint和格式化操作,这样可以加快检查速度,避免对整个项目进行检查。

lint-staged的安装与配置

安装lint-staged

1
npm install --save-dev lint-staged

在package.json中配置lint-staged

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,scss,md,json}": [
      "prettier --write"
    ]
  }
}

修改Husky的pre-commit钩子

1
npx husky add .husky/pre-commit "npx lint-staged"

自动化测试

自动化测试是前端工程化的重要组成部分,它可以帮助开发者及时发现和修复bug,保证代码质量。常见的前端自动化测试包括单元测试、集成测试和端到端测试。

测试基础概念

测试类型

  1. 单元测试:测试代码中的最小可测试单元(如函数、组件等)
  2. 集成测试:测试多个单元如何协同工作
  3. 端到端测试:测试整个应用的工作流程,模拟真实用户操作

测试术语

  1. TDD (Test Driven Development):测试驱动开发,先编写测试,再编写代码
  2. BDD (Behavior Driven Development):行为驱动开发,关注软件的行为和用户故事
  3. 断言:用于验证代码的输出是否符合预期
  4. 测试覆盖率:衡量代码被测试覆盖的程度

单元测试

单元测试是测试中最基础的类型,它关注代码中的最小可测试单元。

Jest

Jest是Facebook开发的一个JavaScript测试框架,它集成了测试运行器、断言库、快照测试等功能,是React项目默认的测试框架。

Jest的安装与配置

安装Jest

1
npm install --save-dev jest

基本配置 (jest.config.js):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
module.exports = {
  testEnvironment: 'jsdom',
  collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/index.js'],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

package.json中添加脚本:

1
2
3
4
5
6
7
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}
Jest的基本用法

测试函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// sum.js
function sum(a, b) {
  return a + b;
}
module.exports = sum;

// sum.test.js
const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

test('adds negative numbers correctly', () => {
  expect(sum(-1, -1)).toBe(-2);
});

测试异步函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// fetchData.js
async function fetchData() {
  const response = await fetch('https://api.example.com/data');
  return response.json();
}
module.exports = fetchData;

// fetchData.test.js
const fetchData = require('./fetchData');

// 模拟fetch
global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ data: 'test data' }),
  })
);

test('fetches data correctly', async () => {
  const data = await fetchData();
  expect(data).toEqual({ data: 'test data' });
  expect(fetch).toHaveBeenCalledTimes(1);
  expect(fetch).toHaveBeenCalledWith('https://api.example.com/data');
});

React测试库

React测试库(React Testing Library)是一个用于测试React组件的库,它鼓励开发者从用户的角度测试组件,而不是关注组件的实现细节。

React测试库的安装与配置

安装依赖

1
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event

配置jest-dom

创建src/setupTests.js文件:

1
import '@testing-library/jest-dom';

jest.config.js中添加:

1
2
3
4
5
module.exports = {
  // ...
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
  // ...
};
React测试库的基本用法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Button.js
import React from 'react';

function Button({ onClick, children }) {
  return <button onClick={onClick}>{children}</button>;
}

export default Button;

// Button.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';

test('renders button with children', () => {
  render(<Button>Click Me</Button>);
  expect(screen.getByText('Click Me')).toBeInTheDocument();
});

test('calls onClick when clicked', () => {
  const handleClick = jest.fn();
  render(<Button onClick={handleClick}>Click Me</Button>);
  fireEvent.click(screen.getByText('Click Me'));
  expect(handleClick).toHaveBeenCalledTimes(1);
});

集成测试

集成测试关注多个组件或模块如何协同工作。

React集成测试示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// App.js
import React from 'react';
import Input from './Input';
import Button from './Button';

function App() {
  const [text, setText] = React.useState('');
  const [submitted, setSubmitted] = React.useState(false);

  const handleSubmit = () => {
    setSubmitted(true);
  };

  return (
    <div>
      <Input 
        value={text} 
        onChange={(e) => setText(e.target.value)} 
        placeholder="Enter text" 
      />
      <Button onClick={handleSubmit}>Submit</Button>
      {submitted && <p>Submitted: {text}</p>}
    </div>
  );
}

export default App;

// App.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import App from './App';

test('submits text correctly', () => {
  render(<App />);
  
  // 输入文本
  const input = screen.getByPlaceholderText('Enter text');
  fireEvent.change(input, { target: { value: 'test text' } });
  
  // 点击提交按钮
  const button = screen.getByText('Submit');
  fireEvent.click(button);
  
  // 验证提交结果
  expect(screen.getByText('Submitted: test text')).toBeInTheDocument();
});

端到端测试

端到端测试模拟真实用户操作,测试整个应用的工作流程。常见的端到端测试工具有Cypress、Puppeteer等。

Cypress

Cypress是一个现代化的端到端测试工具,它提供了实时重载、时间旅行调试等功能,使端到端测试变得更加简单和高效。

Cypress的安装与配置

安装Cypress

1
npm install --save-dev cypress

package.json中添加脚本:

1
2
3
4
5
6
{
  "scripts": {
    "cypress:open": "cypress open",
    "cypress:run": "cypress run"
  }
}

首次运行Cypress

1
npx cypress open

这会自动创建Cypress的目录结构和配置文件。

Cypress的基本用法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// cypress/integration/app.spec.js
describe('App', () => {
  beforeEach(() => {
    cy.visit('http://localhost:3000');
  });

  it('should display the app title', () => {
    cy.contains('My App').should('be.visible');
  });

  it('should submit form correctly', () => {
    cy.get('input[placeholder="Enter text"]').type('test text');
    cy.contains('Submit').click();
    cy.contains('Submitted: test text').should('be.visible');
  });
});

持续集成与持续部署(CI/CD)

持续集成(CI)和持续部署(CD)是现代软件开发中的重要实践,它们可以帮助团队更快、更安全地交付软件。

持续集成(CI)

持续集成是指开发人员频繁地将代码集成到共享仓库中,每次集成都会自动运行构建和测试,以便及早发现问题。

GitHub Actions

GitHub Actions是GitHub提供的CI/CD服务,它可以在代码推送到仓库时自动运行工作流。

创建GitHub Actions工作流

在项目根目录创建.github/workflows/ci.yml文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
name: CI

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

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Use Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '16'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Build
      run: npm run build
    
    - name: Deploy to GitHub Pages
      uses: peaceiris/actions-gh-pages@v3
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: ./dist

8. 创建一个示例组件和测试

创建src/components/Button/Button.tsx文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React from 'react';
import './Button.css';

interface ButtonProps {
  onClick?: () => void;
  children: React.ReactNode;
  disabled?: boolean;
  variant?: 'primary' | 'secondary';
}

const Button: React.FC<ButtonProps> = ({ 
  onClick, 
  children, 
  disabled = false, 
  variant = 'primary' 
}) => {
  return (
    <button 
      className={`button button--${variant}`} 
      onClick={onClick} 
      disabled={disabled}
    >
      {children}
    </button>
  );
};

export default Button;

创建src/components/Button/Button.css文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
.button {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
  transition: background-color 0.3s;
}

.button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.button--primary {
  background-color: #007bff;
  color: white;
}

.button--primary:hover:not(:disabled) {
  background-color: #0056b3;
}

.button--secondary {
  background-color: #6c757d;
  color: white;
}

.button--secondary:hover:not(:disabled) {
  background-color: #545b62;
}

创建src/components/Button/Button.test.tsx文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';

describe('Button', () => {
  test('renders children correctly', () => {
    render(<Button>Click Me</Button>);
    expect(screen.getByText('Click Me')).toBeInTheDocument();
  });

  test('calls onClick when clicked', () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Click Me</Button>);
    fireEvent.click(screen.getByText('Click Me'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  test('does not call onClick when disabled', () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick} disabled>Click Me</Button>);
    fireEvent.click(screen.getByText('Click Me'));
    expect(handleClick).not.toHaveBeenCalled();
  });

  test('applies primary variant by default', () => {
    render(<Button>Click Me</Button>);
    expect(screen.getByText('Click Me')).toHaveClass('button--primary');
  });

  test('applies secondary variant when specified', () => {
    render(<Button variant="secondary">Click Me</Button>);
    expect(screen.getByText('Click Me')).toHaveClass('button--secondary');
  });
});

9. 更新App组件

修改src/App.tsx文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React from 'react';
import Button from './components/Button/Button';
import './App.css';

function App() {
  const [count, setCount] = React.useState(0);

  const handleIncrement = () => {
    setCount((prevCount) => prevCount + 1);
  };

  const handleDecrement = () => {
    setCount((prevCount) => prevCount - 1);
  };

  return (
    <div className="app">
      <header className="app-header">
        <h1>前端工程化模板</h1>
      </header>
      <main className="app-main">
        <div className="counter">
          <h2>计数器: {count}</h2>
          <div className="counter-buttons">
            <Button onClick={handleDecrement} disabled={count === 0}>
              - 减少
            </Button>
            <Button onClick={handleIncrement} variant="primary">
              + 增加
            </Button>
          </div>
        </div>
      </main>
    </div>
  );
}

export default App;

修改src/App.css文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
.app {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.app-header {
  text-align: center;
  margin-bottom: 40px;
}

.app-header h1 {
  font-size: 32px;
  color: #333;
}

.app-main {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.counter {
  text-align: center;
}

.counter h2 {
  margin-bottom: 20px;
  color: #333;
}

.counter-buttons {
  display: flex;
  gap: 10px;
  justify-content: center;
}

10. 测试项目

运行测试:

1
npm test

检查代码规范:

1
npm run lint

构建项目:

1
npm run build

如果一切正常,那么我们的前端工程化项目模板就创建成功了!

总结

本文详细介绍了前端工程化和自动化测试的各个方面,包括构建工具、代码规范与质量控制、自动化测试、持续集成与持续部署等内容。通过学习这些知识,我们可以建立一个完整的前端工程化开发流程,提高开发效率,保证代码质量。

在实践项目中,我们创建了一个包含所有必要配置的前端工程化项目模板,这个模板可以作为我们未来项目的基础,也可以根据具体需求进行定制。

前端工程化是一个不断发展的领域,新的工具和技术不断涌现。作为前端开发者,我们需要持续学习和实践,不断优化我们的开发流程和工具链,以适应不断变化的需求和技术环境。

在下一篇文章中,我们将深入探讨前端安全、性能监控和国际化等高级话题,敬请期待!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

#### GitLab CI

GitLab CI是GitLab提供的CI/CD服务,它使用`.gitlab-ci.yml`文件来定义CI/CD流程

**创建GitLab CI配置文件**

在项目根目录创建`.gitlab-ci.yml`文件:

```yaml
stages:
  - test
  - build

variables:
  NODE_ENV: 'development'

install_dependencies:
  stage: .pre
  image: node:16
  script:
    - npm ci
  artifacts:
    paths:
      - node_modules/

lint:
  stage: test
  image: node:16
  script:
    - npm run lint

test:
  stage: test
  image: node:16
  script:
    - npm test

build:
  stage: build
  image: node:16
  script:
    - npm run build
  artifacts:
    paths:
      - dist/

持续部署(CD)

持续部署是指将经过测试的代码自动部署到生产环境或预生产环境。

GitHub Pages部署示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# .github/workflows/deploy.yml
name: Deploy to GitHub Pages

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Use Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '16'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Build
      run: npm run build
    
    - name: Deploy to GitHub Pages
      uses: peaceiris/actions-gh-pages@v3
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: ./dist

Netlify部署示例

Netlify提供了简单的前端应用部署服务,可以通过配置文件或UI界面进行设置。

创建Netlify配置文件 (netlify.toml):

1
2
3
4
5
6
7
8
[build]
  command = "npm run build"
  publish = "dist"

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

实践项目:建立前端工程化项目模板

现在,让我们结合前面所学的知识,建立一个完整的前端工程化项目模板,包含构建工具、代码规范、自动化测试等所有必要的配置。

项目概述

我们将创建一个前端项目模板,它包含以下特性:

  1. 使用Vite作为构建工具
  2. 使用ESLint和Prettier进行代码规范和格式化
  3. 使用Jest和React Testing Library进行单元测试和集成测试
  4. 使用Husky和lint-staged在提交代码前进行检查
  5. 配置GitHub Actions进行CI/CD

创建项目

1. 使用Vite初始化项目

1
2
npm create vite@latest frontend-template -- --template react
cd frontend-template

2. 安装必要的依赖

1
2
3
4
5
# 基础依赖
npm install

# 开发依赖
npm install --save-dev eslint prettier husky lint-staged @testing-library/react @testing-library/jest-dom @testing-library/user-event jest jest-environment-jsdom @babel/preset-env @babel/preset-react @babel/preset-typescript

3. 配置ESLint

初始化ESLint:

1
npx eslint --init

选择以下选项:

  • How would you like to use ESLint? To check syntax and find problems
  • What type of modules does your project use? JavaScript modules (import/export)
  • Which framework does your project use? React
  • Does your project use TypeScript? Yes
  • Where does your code run? Browser
  • What format do you want your config file to be in? JavaScript

修改.eslintrc.js配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true,
    'jest/globals': true,
  },
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended',
    'plugin:jest/recommended',
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  plugins: [
    'react',
    '@typescript-eslint',
    'prettier',
    'jest',
  ],
  rules: {
    'react/react-in-jsx-scope': 'off',
    'prettier/prettier': 'error',
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
  },
  settings: {
    react: {
      version: 'detect',
    },
  },
};

4. 配置Prettier

创建.prettierrc.js文件:

1
2
3
4
5
6
7
8
module.exports = {
  singleQuote: true,
  trailingComma: 'es5',
  tabWidth: 2,
  semi: true,
  arrowParens: 'always',
  printWidth: 80,
};

创建.prettierignore文件:

1
node_modules

5. 配置Jest

创建jest.config.js文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
module.exports = {
  testEnvironment: 'jsdom',
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/main.tsx',
    '!src/**/*.d.ts',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
};

创建src/setupTests.js文件:

1
import '@testing-library/jest-dom';

创建babel.config.js文件:

1
2
3
4
5
6
7
module.exports = {
  presets: [
    '@babel/preset-env',
    '@babel/preset-react',
    '@babel/preset-typescript',
  ],
};

6. 配置Husky和lint-staged

初始化Husky:

1
npx husky install

package.json中添加脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{
  "scripts": {
    "prepare": "husky install",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
    "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
    "format": "prettier --write ."
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,scss,md,json}": [
      "prettier --write"
    ]
  }
}

添加Git钩子:

1
2
npx husky add .husky/pre-commit "npx lint-staged"
npx husky add .husky/pre-push "npm test"

7. 配置GitHub Actions

创建.github/workflows/ci.yml文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
name: CI

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

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Use Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '16'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run lint
      run: npm run lint
    
    - name: Run tests
      run: npm test
    
    - name: Build
      run: npm run build

创建.github/workflows/deploy.yml文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
name: Deploy to GitHub Pages

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Use Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '16'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Build
      run: npm run build
    
    - name: Deploy to GitHub Pages
      uses: peaceiris/actions-gh-pages@v3
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: ./dist

8. 创建一个示例组件和测试

创建src/components/Button/Button.tsx文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React from 'react';
import './Button.css';

interface ButtonProps {
  onClick?: () => void;
  children: React.ReactNode;
  disabled?: boolean;
  variant?: 'primary' | 'secondary';
}

const Button: React.FC<ButtonProps> = ({ 
  onClick, 
  children, 
  disabled = false, 
  variant = 'primary' 
}) => {
  return (
    <button 
      className={`button button--${variant}`} 
      onClick={onClick} 
      disabled={disabled}
    >
      {children}
    </button>
  );
};

export default Button;

创建src/components/Button/Button.css文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
.button {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
  transition: background-color 0.3s;
}

.button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.button--primary {
  background-color: #007bff;
  color: white;
}

.button--primary:hover:not(:disabled) {
  background-color: #0056b3;
}

.button--secondary {
  background-color: #6c757d;
  color: white;
}

.button--secondary:hover:not(:disabled) {
  background-color: #545b62;
}

创建src/components/Button/Button.test.tsx文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';

describe('Button', () => {
  test('renders children correctly', () => {
    render(<Button>Click Me</Button>);
    expect(screen.getByText('Click Me')).toBeInTheDocument();
  });

  test('calls onClick when clicked', () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Click Me</Button>);
    fireEvent.click(screen.getByText('Click Me'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  test('does not call onClick when disabled', () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick} disabled>Click Me</Button>);
    fireEvent.click(screen.getByText('Click Me'));
    expect(handleClick).not.toHaveBeenCalled();
  });

  test('applies primary variant by default', () => {
    render(<Button>Click Me</Button>);
    expect(screen.getByText('Click Me')).toHaveClass('button--primary');
  });

  test('applies secondary variant when specified', () => {
    render(<Button variant="secondary">Click Me</Button>);
    expect(screen.getByText('Click Me')).toHaveClass('button--secondary');
  });
});

9. 更新App组件

修改src/App.tsx文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React from 'react';
import Button from './components/Button/Button';
import './App.css';

function App() {
  const [count, setCount] = React.useState(0);

  const handleIncrement = () => {
    setCount((prevCount) => prevCount + 1);
  };

  const handleDecrement = () => {
    setCount((prevCount) => prevCount - 1);
  };

  return (
    <div className="app">
      <header className="app-header">
        <h1>前端工程化模板</h1>
      </header>
      <main className="app-main">
        <div className="counter">
          <h2>计数器: {count}</h2>
          <div className="counter-buttons">
            <Button onClick={handleDecrement} disabled={count === 0}>
              - 减少
            </Button>
            <Button onClick={handleIncrement} variant="primary">
              + 增加
            </Button>
          </div>
        </div>
      </main>
    </div>
  );
}

export default App;

修改src/App.css文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
.app {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.app-header {
  text-align: center;
  margin-bottom: 40px;
}

.app-header h1 {
  font-size: 32px;
  color: #333;
}

.app-main {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.counter {
  text-align: center;
}

.counter h2 {
  margin-bottom: 20px;
  color: #333;
}

.counter-buttons {
  display: flex;
  gap: 10px;
  justify-content: center;
}

10. 测试项目

运行测试:

1
npm test

检查代码规范:

1
npm run lint

构建项目:

1
npm run build

如果一切正常,那么我们的前端工程化项目模板就创建成功了!

总结

本文详细介绍了前端工程化和自动化测试的各个方面,包括构建工具、代码规范与质量控制、自动化测试、持续集成与持续部署等内容。通过学习这些知识,我们可以建立一个完整的前端工程化开发流程,提高开发效率,保证代码质量。

在实践项目中,我们创建了一个包含所有必要配置的前端工程化项目模板,这个模板可以作为我们未来项目的基础,也可以根据具体需求进行定制。

前端工程化是一个不断发展的领域,新的工具和技术不断涌现。作为前端开发者,我们需要持续学习和实践,不断优化我们的开发流程和工具链,以适应不断变化的需求和技术环境。

在下一篇文章中,我们将深入探讨前端安全、性能监控和国际化等高级话题,敬请期待!

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计