前端入门到精通(四):前端工程化与自动化测试
引言
随着前端技术的快速发展和前端应用复杂度的不断提升,传统的开发方式已经无法满足现代前端开发的需求。前端工程化和自动化测试作为现代前端开发的重要组成部分,能够帮助开发者提高开发效率、保证代码质量、降低维护成本。本文将带您深入了解前端工程化的各个方面,以及如何实施自动化测试,构建一个高效、稳定的前端开发生态系统。
前端工程化概述
什么是前端工程化?
前端工程化是指将软件工程的方法论应用于前端开发,通过一系列工具、规范和流程,使前端开发变得更加规范化、自动化和高效化。它涉及代码组织、构建、部署、测试等多个方面,旨在解决前端开发中的复杂性问题。
前端工程化的主要内容
前端工程化主要包含以下几个方面:
- 模块化:将代码拆分为可复用的模块
- 组件化:将UI拆分为独立、可复用的组件
- 规范化:制定代码规范、目录结构规范等
- 自动化:构建自动化、测试自动化、部署自动化等
- 性能优化:代码压缩、懒加载等性能优化策略
为什么需要前端工程化?
- 提高开发效率:通过自动化工具减少重复性工作
- 保证代码质量:通过规范和工具确保代码风格统一、避免常见错误
- 降低维护成本:清晰的代码组织和规范使代码更易于理解和维护
- 增强团队协作:统一的开发规范和工具链使团队协作更加顺畅
- 提升用户体验:通过性能优化提升应用加载速度和运行性能
构建工具
构建工具是前端工程化的核心,它们负责将源代码转换为生产环境可用的代码。常见的构建工具包括Webpack、Rollup、Parcel等。
Webpack
Webpack是目前最流行的前端构建工具之一,它是一个模块打包器,可以将各种资源(JavaScript、CSS、图片等)视为模块,然后将它们打包成一个或多个bundle。
Webpack的核心概念
- Entry:入口点,指定Webpack从哪个文件开始打包
- Output:输出,指定打包后的文件存放位置和名称
- Loader:加载器,用于处理非JavaScript文件
- Plugin:插件,用于执行各种任务,如代码压缩、环境变量注入等
- 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的核心概念
- Input:输入文件
- Output:输出配置
- Plugins:插件系统,用于扩展功能
- 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:
根据提示选择配置选项,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配置:
- 安装ESLint扩展
- 在项目根目录创建
.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,保证代码质量。常见的前端自动化测试包括单元测试、集成测试和端到端测试。
测试基础概念
测试类型
- 单元测试:测试代码中的最小可测试单元(如函数、组件等)
- 集成测试:测试多个单元如何协同工作
- 端到端测试:测试整个应用的工作流程,模拟真实用户操作
测试术语
- TDD (Test Driven Development):测试驱动开发,先编写测试,再编写代码
- BDD (Behavior Driven Development):行为驱动开发,关注软件的行为和用户故事
- 断言:用于验证代码的输出是否符合预期
- 测试覆盖率:衡量代码被测试覆盖的程度
单元测试
单元测试是测试中最基础的类型,它关注代码中的最小可测试单元。
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:
这会自动创建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
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
|
实践项目:建立前端工程化项目模板
现在,让我们结合前面所学的知识,建立一个完整的前端工程化项目模板,包含构建工具、代码规范、自动化测试等所有必要的配置。
项目概述
我们将创建一个前端项目模板,它包含以下特性:
- 使用Vite作为构建工具
- 使用ESLint和Prettier进行代码规范和格式化
- 使用Jest和React Testing Library进行单元测试和集成测试
- 使用Husky和lint-staged在提交代码前进行检查
- 配置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:
选择以下选项:
- 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文件:
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:
在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. 测试项目
运行测试:
检查代码规范:
构建项目:
如果一切正常,那么我们的前端工程化项目模板就创建成功了!
总结
本文详细介绍了前端工程化和自动化测试的各个方面,包括构建工具、代码规范与质量控制、自动化测试、持续集成与持续部署等内容。通过学习这些知识,我们可以建立一个完整的前端工程化开发流程,提高开发效率,保证代码质量。
在实践项目中,我们创建了一个包含所有必要配置的前端工程化项目模板,这个模板可以作为我们未来项目的基础,也可以根据具体需求进行定制。
前端工程化是一个不断发展的领域,新的工具和技术不断涌现。作为前端开发者,我们需要持续学习和实践,不断优化我们的开发流程和工具链,以适应不断变化的需求和技术环境。
在下一篇文章中,我们将深入探讨前端安全、性能监控和国际化等高级话题,敬请期待!