Nginx日志分析工具-NginxPulse开源了
上周写了个[nginx 日志分析系统](https://www.kaisir.cn/post/186),这几天花了点时间把代码整理了下,现已开源。 欢迎各位有需要的开发者自取。
<template>
<div>
<!-- 反例1: 模板中写复杂表达式 -->
<div>
{{ goodsList.filter(item => item.price > 100).map(item => item.name).join(',') }}
</div>
<!-- 反例2: 直接在模板中修改数据 -->
<button @click="goodsList.push({ id: Date.now(), name: '新商品', price: 99 })">
添加商品
</button>
<!-- 反例3: 模板中写复杂计算 -->
<div>
总价: {{ goodsList.reduce((sum, item) => sum + item.price * item.count, 0).toFixed(2) }}
</div>
<!-- 反例4: 用样式控制业务逻辑 -->
<div :style="{ display: isVisible ? 'block' : 'none' }">
内容
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const goodsList = ref([
{ id: 1, name: '商品A', price: 150, count: 2 },
{ id: 2, name: '商品B', price: 80, count: 1 },
]);
const isVisible = ref(true);
</script>
<template>
<div>
<!-- 正例1: 调用简单的 computed -->
<div>{{ filteredGoodsNames }}</div>
<!-- 正例2: 调用方法处理事件 -->
<button @click="handleAddGoods">添加商品</button>
<!-- 正例3: 使用 computed 计算总价 -->
<div>总价: {{ totalPrice }}</div>
<!-- 正例4: 用 v-if 控制渲染 -->
<div v-if="isVisible">内容</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
// 原始数据
const goodsList = ref([
{ id: 1, name: '商品A', price: 150, count: 2 },
{ id: 2, name: '商品B', price: 80, count: 1 },
]);
const isVisible = ref(true);
// 数据处理放在 computed
const filteredGoodsNames = computed(() => {
return goodsList.value
.filter(item => item.price > 100)
.map(item => item.name)
.join(',');
});
const totalPrice = computed(() => {
return goodsList.value
.reduce((sum, item) => sum + item.price * item.count, 0)
.toFixed(2);
});
// 事件处理放在 methods
const handleAddGoods = () => {
goodsList.value.push({
id: Date.now(),
name: '新商品',
price: 99,
count: 1,
});
};
</script>
<template>
<div>
<!-- 反例: 在模板中直接调用接口 -->
<button @click="fetchGoods">加载商品</button>
<div v-for="item in formattedList" :key="item.id">
{{ item.displayName }} - ¥{{ item.formattedPrice }}
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import axios from 'axios';
// 反例1: 存储格式化后的派生数据
const formattedList = ref([
{ id: 1, displayName: '商品A', formattedPrice: '150.00' },
]);
// 反例2: 在事件中直接写接口请求
const fetchGoods = () => {
axios.get('/api/goods').then(res => {
// 每次都要手动格式化并同步到 formattedList
formattedList.value = res.data.map((item: any) => ({
id: item.id,
displayName: `${item.name} (${item.category})`,
formattedPrice: item.price.toFixed(2),
}));
});
};
</script>
<template>
<div>
<button @click="handleFetchGoods" :loading="loading">加载商品</button>
<div v-for="item in formattedGoodsList" :key="item.id">
{{ item.displayName }} - ¥{{ item.formattedPrice }}
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { fetchGoodsList } from '@/api/goods'; // 接口抽离到 api
// 正例1: data 只存储原始数据
const goodsList = ref([]);
const loading = ref(false);
// 正例2: 派生数据用 computed 自动计算
const formattedGoodsList = computed(() => {
return goodsList.value.map(item => ({
id: item.id,
displayName: `${item.name} (${item.category})`,
formattedPrice: item.price.toFixed(2),
}));
});
// 正例3: 接口请求和业务逻辑抽离
const handleFetchGoods = async () => {
try {
loading.value = true;
const res = await fetchGoodsList();
goodsList.value = res.data; // 只更新原始数据,computed 自动更新
} catch (error) {
console.error('加载失败', error);
} finally {
loading.value = false;
}
};
</script>
// src/api/goods.ts - 接口抽离
import request from '@/utils/request';
export const fetchGoodsList = () => {
return request.get('/api/goods');
};
<!-- Table.vue - 反例: 通用组件直接调用业务接口 -->
<template>
<el-table :data="tableData" :loading="loading">
<el-table-column prop="name" label="名称" />
<el-table-column prop="price" label="价格" />
</el-table>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import axios from 'axios';
// 反例: 通用组件内部直接调用业务接口
const tableData = ref([]);
const loading = ref(false);
onMounted(() => {
loading.value = true;
axios.get('/api/goods').then(res => {
tableData.value = res.data;
loading.value = false;
});
});
</script>
<!-- GoodsList.vue - 反例: 父组件传原始数据,子组件内部格式化 -->
<template>
<Table :data="goodsList" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import Table from './Table.vue';
import { fetchGoodsList } from '@/api/goods';
const goodsList = ref([]);
const loadData = async () => {
const res = await fetchGoodsList();
goodsList.value = res.data; // 传原始数据给子组件
};
</script>
<!-- Table.vue - 正例: 通用组件只接收 props,不包含业务逻辑 -->
<template>
<el-table :data="data" :loading="loading" v-bind="$attrs">
<slot />
</el-table>
</template>
<script setup lang="ts">
defineProps<{
data: any[];
loading?: boolean;
}>();
</script>
<!-- GoodsList.vue - 正例: 业务组件处理数据,传给通用组件 -->
<template>
<Table :data="formattedGoodsList" :loading="loading">
<el-table-column prop="name" label="名称" />
<el-table-column prop="price" label="价格" />
</Table>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import Table from './Table.vue';
import { fetchGoodsList } from '@/api/goods';
const goodsList = ref([]);
const loading = ref(false);
// 业务组件负责数据格式化
const formattedGoodsList = computed(() => {
return goodsList.value.map(item => ({
...item,
price: `¥${item.price.toFixed(2)}`,
}));
});
const loadData = async () => {
try {
loading.value = true;
const res = await fetchGoodsList();
goodsList.value = res.data;
} finally {
loading.value = false;
}
};
loadData();
</script>
// src/utils/goods-list/getItem.ts
export const getItem = (list: any[], id: number) => {
return list.find(item => item.id === id);
};
// src/utils/goods-list/setItem.ts
export const setItem = (list: any[], item: any) => {
const index = list.findIndex(i => i.id === item.id);
if (index > -1) {
list[index] = item;
}
};
// src/utils/goods-list/checkItem.ts
export const checkItem = (item: any) => {
return item && item.price > 0;
};
// src/utils/goods-list/filterItem.ts
export const filterItem = (list: any[], condition: any) => {
return list.filter(item => item.price > condition.minPrice);
};
// GoodsList.vue
<script setup lang="ts">
import { getItem } from '@/utils/goods-list/getItem';
import { setItem } from '@/utils/goods-list/setItem';
import { checkItem } from '@/utils/goods-list/checkItem';
import { filterItem } from '@/utils/goods-list/filterItem';
// 调用时需要跨文件引入,逻辑链路变长
const handleUpdate = (id: number, newData: any) => {
const item = getItem(goodsList.value, id);
if (checkItem(item)) {
setItem(goodsList.value, { ...item, ...newData });
}
};
</script>
<!-- GoodsList.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue';
import { fetchGoodsList } from '@/api/goods';
const goodsList = ref([]);
const loading = ref(false);
// 正例: 按业务逻辑拆分成核心函数,函数内聚
const loadGoodsData = async () => {
try {
loading.value = true;
const res = await fetchGoodsList();
goodsList.value = formatGoodsData(res.data);
} catch (error) {
console.error('加载失败', error);
} finally {
loading.value = false;
}
};
// 格式化数据 - 仅当前组件使用,写在组件内
const formatGoodsData = (data: any[]) => {
return data.map(item => ({
...item,
displayPrice: `¥${item.price.toFixed(2)}`,
isAvailable: item.stock > 0,
}));
};
// 如需复用,再抽离到 utils/goods.ts
loadGoodsData();
</script>
<!-- SearchInput.vue -->
<template>
<el-input v-model="inputValue" @input="handleInput" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
const inputValue = defineModel<string>('modelValue');
const emit = defineEmits(['update:modelValue']);
const handleInput = (value: string) => {
emit('update:modelValue', value);
};
</script>
<!-- SearchButton.vue -->
<template>
<el-button @click="handleClick">{{ text }}</el-button>
</template>
<script setup lang="ts">
defineProps<{ text: string }>();
const emit = defineEmits(['click']);
const handleClick = () => {
emit('click');
};
</script>
<!-- SearchWrapper.vue -->
<template>
<div class="search-wrapper">
<SearchInput v-model="keyword" />
<SearchButton text="搜索" @click="handleSearch" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import SearchInput from './SearchInput.vue';
import SearchButton from './SearchButton.vue';
const keyword = ref('');
const emit = defineEmits(['search']);
const handleSearch = () => {
emit('search', keyword.value);
};
</script>
<!-- SearchBar.vue - 正例: 单个组件,逻辑闭环 -->
<template>
<div class="search-bar">
<el-input v-model="keyword" placeholder="请输入关键词" clearable />
<el-button type="primary" @click="handleSearch">搜索</el-button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const keyword = ref('');
const emit = defineEmits(['search']);
const handleSearch = () => {
emit('search', keyword.value);
};
</script>
当工具函数、样式、组合式函数仅被单个组件使用时,应放在该组件所在目录下,而不是全局 src/utils。
组件目录结构示例:
ProductList/
├── index.vue # 主组件
├── index.scss # 组件样式
├── composables/ # 仅当前组件使用的组合式函数
│ ├── useProductData.ts
│ └── useProductFilter.ts
├── utils/ # 仅当前组件使用的工具函数
│ ├── formatProduct.ts
│ └── validateProduct.ts
└── components/ # 仅当前组件使用的子组件
└── ProductCard.vue
src/
utils/
product-list/ # 反例:仅在 ProductList.vue 使用
formatProduct.ts
filterProduct.ts
validateProduct.ts
<!-- src/views/product/ProductList.vue -->
<script setup lang="ts">
import { formatProduct } from '@/utils/product-list/formatProduct';
import { filterProduct } from '@/utils/product-list/filterProduct';
import { validateProduct } from '@/utils/product-list/validateProduct';
// 引入路径长,且这些函数只在当前组件使用
</script>
src/views/product/ProductList/
├── index.vue
├── utils/ # 正例:仅当前组件使用
│ ├── formatProduct.ts
│ └── filterProduct.ts
└── index.scss
// src/views/product/ProductList/utils/formatProduct.ts
// 仅 ProductList 组件使用
export const formatProductPrice = (price: number): string => {
return `¥${price.toFixed(2)}`;
};
export const formatProductName = (name: string, category: string): string => {
return `${name} (${category})`;
};
// src/views/product/ProductList/utils/filterProduct.ts
// 仅 ProductList 组件使用
export const filterByPriceRange = (products: any[], min: number, max: number) => {
return products.filter(p => p.price >= min && p.price <= max);
};
export const filterByCategory = (products: any[], category: string) => {
return products.filter(p => p.category === category);
};
<!-- src/views/product/ProductList/index.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue';
import { formatProductPrice, formatProductName } from './utils/formatProduct';
import { filterByPriceRange } from './utils/filterProduct';
const products = ref([]);
// 引入路径短,且明确表示这些函数属于当前组件
const formattedProducts = computed(() => {
return products.value.map(p => ({
...p,
displayPrice: formatProductPrice(p.price),
displayName: formatProductName(p.name, p.category),
}));
});
</script>
src/
composables/
useProductData.ts # 反例:仅在 ProductList.vue 使用
useProductFilter.ts # 反例:仅在 ProductList.vue 使用
<!-- src/views/product/ProductList.vue -->
<script setup lang="ts">
import { useProductData } from '@/composables/useProductData';
import { useProductFilter } from '@/composables/useProductFilter';
// 看起来像是全局可复用的,但实际上只在当前组件使用
</script>
src/views/product/ProductList/
├── index.vue
├── composables/ # 正例:仅当前组件使用
│ ├── useProductData.ts
│ └── useProductFilter.ts
└── index.scss
// src/views/product/ProductList/composables/useProductData.ts
// 仅 ProductList 组件使用
import { ref, computed } from 'vue';
import { fetchProductList } from '@/api/product';
export const useProductData = () => {
const products = ref([]);
const loading = ref(false);
const error = ref(null);
const loadProducts = async () => {
try {
loading.value = true;
const res = await fetchProductList();
products.value = res.data;
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
};
const formattedProducts = computed(() => {
return products.value.map(p => ({
...p,
displayPrice: `¥${p.price.toFixed(2)}`,
}));
});
return {
products,
formattedProducts,
loading,
error,
loadProducts,
};
};
// src/views/product/ProductList/composables/useProductFilter.ts
// 仅 ProductList 组件使用
import { ref, computed } from 'vue';
export const useProductFilter = (products: any[]) => {
const searchKeyword = ref('');
const priceRange = ref([0, 10000]);
const selectedCategory = ref('');
const filteredProducts = computed(() => {
let result = products.value;
// 按关键词过滤
if (searchKeyword.value) {
result = result.filter(p =>
p.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
);
}
// 按价格范围过滤
result = result.filter(
p => p.price >= priceRange.value[0] && p.price <= priceRange.value[1]
);
// 按分类过滤
if (selectedCategory.value) {
result = result.filter(p => p.category === selectedCategory.value);
}
return result;
});
return {
searchKeyword,
priceRange,
selectedCategory,
filteredProducts,
};
};
<!-- src/views/product/ProductList/index.vue -->
<script setup lang="ts">
import { useProductData } from './composables/useProductData';
import { useProductFilter } from './composables/useProductFilter';
import { onMounted } from 'vue';
// 引入路径短,且明确表示这些 composable 属于当前组件
const { formattedProducts, loading, loadProducts } = useProductData();
const { searchKeyword, priceRange, filteredProducts } = useProductFilter(formattedProducts);
onMounted(() => {
loadProducts();
});
</script>
<template>
<div>
<input v-model="searchKeyword" placeholder="搜索商品" />
<div v-for="product in filteredProducts" :key="product.id">
{{ product.displayName }} - {{ product.displayPrice }}
</div>
</div>
</template>
src/
styles/
product-list.scss # 反例:仅在 ProductList.vue 使用
<!-- src/views/product/ProductList.vue -->
<style lang="scss">
@use '@/styles/product-list.scss';
</style>
src/views/product/ProductList/
├── index.vue
├── index.scss # 正例:组件主样式
├── styles/ # 正例:样式拆分(如果样式较多)
│ ├── variables.scss
│ └── mixins.scss
└── components/
└── ProductCard.vue
└── ProductCard.scss
// src/views/product/ProductList/index.scss
// 仅 ProductList 组件使用
@use './styles/variables.scss';
@use './styles/mixins.scss';
.product-list {
padding: 20px;
&__header {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
&__content {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
}
// src/views/product/ProductList/styles/variables.scss
// 仅 ProductList 组件使用
$product-card-width: 200px;
$product-card-gap: 16px;
$product-header-height: 60px;
// src/views/product/ProductList/styles/mixins.scss
// 仅 ProductList 组件使用
@mixin product-card-hover {
transition: transform 0.2s;
&:hover {
transform: translateY(-4px);
}
}
<!-- src/views/product/ProductList/index.vue -->
<template>
<div class="product-list">
<div class="product-list__header">...</div>
<div class="product-list__content">...</div>
</div>
</template>
<style scoped lang="scss">
@import './index.scss';
</style>
src/
components/
ProductCard.vue # 反例:仅在 ProductList.vue 使用
ProductFilter.vue # 反例:仅在 ProductList.vue 使用
<!-- src/views/product/ProductList.vue -->
<script setup lang="ts">
import ProductCard from '@/components/ProductCard.vue';
import ProductFilter from '@/components/ProductFilter.vue';
// 看起来像是全局组件,但实际上只在当前组件使用
</script>
src/views/product/ProductList/
├── index.vue
├── components/ # 正例:仅当前组件使用
│ ├── ProductCard.vue
│ ├── ProductCard.scss
│ ├── ProductFilter.vue
│ └── ProductFilter.scss
└── index.scss
<!-- src/views/product/ProductList/components/ProductCard.vue -->
<!-- 仅 ProductList 组件使用 -->
<template>
<div class="product-card">
<img :src="product.image" :alt="product.name" />
<h3>{{ product.name }}</h3>
<p class="price">{{ product.displayPrice }}</p>
</div>
</template>
<script setup lang="ts">
defineProps<{
product: any;
}>();
</script>
<style scoped lang="scss">
@import './ProductCard.scss';
</style>
<!-- src/views/product/ProductList/index.vue -->
<script setup lang="ts">
import ProductCard from './components/ProductCard.vue';
import ProductFilter from './components/ProductFilter.vue';
// 引入路径短,且明确表示这些组件属于当前组件
</script>
<template>
<div class="product-list">
<ProductFilter />
<ProductCard
v-for="product in products"
:key="product.id"
:product="product"
/>
</div>
</template>
当以下情况出现时,再考虑抽离到全局:
# 情况1: 多个组件开始使用相同的工具函数
src/views/product/ProductList/utils/formatProduct.ts # 原本在这里
src/views/order/OrderList.vue # 现在也要用
src/views/cart/CartList.vue # 现在也要用
# 应该抽离到:
src/utils/product/formatProduct.ts
// src/utils/product/formatProduct.ts
// 多个组件复用,抽离到全局
export const formatProductPrice = (price: number): string => {
return `¥${price.toFixed(2)}`;
};
export const formatProductName = (name: string, category: string): string => {
return `${name} (${category})`;
};
<!-- src/views/product/ProductList/index.vue -->
<script setup lang="ts">
// 从全局引入
import { formatProductPrice } from '@/utils/product/formatProduct';
</script>
<!-- src/views/order/OrderList.vue -->
<script setup lang="ts">
// 从全局引入
import { formatProductPrice } from '@/utils/product/formatProduct';
</script>
src/views/product/ProductList/
├── index.vue # 主组件
├── index.scss # 组件主样式
│
├── composables/ # 仅当前组件使用的组合式函数
│ ├── useProductData.ts
│ ├── useProductFilter.ts
│ └── useProductPagination.ts
│
├── utils/ # 仅当前组件使用的工具函数
│ ├── formatProduct.ts
│ ├── filterProduct.ts
│ └── validateProduct.ts
│
├── components/ # 仅当前组件使用的子组件
│ ├── ProductCard/
│ │ ├── index.vue
│ │ └── index.scss
│ ├── ProductFilter/
│ │ ├── index.vue
│ │ └── index.scss
│ └── ProductPagination/
│ ├── index.vue
│ └── index.scss
│
└── styles/ # 样式拆分(如果样式较多)
├── variables.scss
├── mixins.scss
└── animations.scss
| 场景 | ❌ 反例(全局) | ✅ 正例(组件目录) |
|---|---|---|
| 仅单组件使用的工具函数 | src/utils/product-list/format.ts |
ProductList/utils/format.ts |
| 仅单组件使用的 composable | src/composables/useProductData.ts |
ProductList/composables/useProductData.ts |
| 仅单组件使用的样式 | src/styles/product-list.scss |
ProductList/index.scss |
| 仅单组件使用的子组件 | src/components/ProductCard.vue |
ProductList/components/ProductCard.vue |
| 多个组件复用 | - | 抽离到全局 src/utils/ 或 src/composables/
|
原则:
utils/、composables/、components/、styles/)src/utils/、src/composables/、src/components/)这些示例展示了如何在实际项目中应用这些原则。