feat:增加项目实战模块
This commit is contained in:
100
docs/Web/Vue/Browser-Process.md
Normal file
100
docs/Web/Vue/Browser-Process.md
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: 浏览器渲染全流程解析
|
||||
date: 2025-12-13
|
||||
---
|
||||
|
||||
# 一、网络请求阶段
|
||||
1. DNS 解析
|
||||
|
||||
  浏览器先解析输入的网址对应的 IP 地址,先检查本地 DNS 缓存,若没有则向本地 DNS 服务器发起查询。最终通过 DNS 系统获取服务器的 IP 地址。
|
||||
|
||||
2. 建立网络连接(TCP/IP 握手)
|
||||
|
||||
  通过三次握手建立客户端(浏览器)与服务器的 TCP 连接。若网址使用 HTTPS,还需完成 TLS/SSL 握手,确保数据加密传输。
|
||||
|
||||
3. 发起 HTTP/HTTPS 请求
|
||||
|
||||
  浏览器向服务器发送请求报文,包含:
|
||||
- **请求行**:方法(GET)、URL、协议版本(如 HTTP/1.1)。
|
||||
- **请求头**:包含浏览器信息(User-Agent)、缓存策略(Cache-Control)等。
|
||||
- **若为 POST 请求**:还包含请求体数据。
|
||||
|
||||
4. 服务器处理请求
|
||||
|
||||
  服务器接收到请求后,根据 URL 路由到对应的后端服务(如 Node.js、Java 等)。
|
||||
- **若为 Vue3 项目**:
|
||||
* 若为单页应用(SPA),服务器通常返回一个通用的 `index.html` 页面(前端路由由 Vue Router 处理)。
|
||||
* 若涉及 API 请求(如获取数据),服务器返回 JSON 格式的数据(如通过 Express、Spring Boot 等接口)。
|
||||
|
||||
5. 返回响应结果
|
||||
|
||||
  服务器返回响应报文,包含:
|
||||
- **状态行**:状态码(如 200 OK、404 Not Found)。
|
||||
- **响应头**:包含内容类型(Content-Type: text/html)、缓存控制(Cache-Control: max-age=3600)等。
|
||||
- **响应体**:
|
||||
* 若为 `index.html`:返回 HTML 文本(包含 Vue3 项目的入口脚本引用,如 `<script src="/static/js/app.js"></script>`)。
|
||||
* 若为 API 数据:返回 JSON 格式的数据(如用户信息、列表数据等)。
|
||||
|
||||
|
||||
# 二、解析与执行阶段
|
||||
1. 解析 HTML 并构建 DOM 树
|
||||
|
||||
  浏览器逐行解析 HTML 内容,构建 **DOM 树**(Document Object Model)。
|
||||
- 遇到 `<link rel="stylesheet">` 标签时,异步加载 CSS 文件并构建 **CSSOM 树**(CSS Object Model)。
|
||||
- 遇到 `<script>` 标签时:
|
||||
* 若为 **同步脚本**(`<script src="app.js">`):暂停 HTML 解析,下载并执行脚本。
|
||||
* 若为 **异步脚本**(`<script async src="app.js">`):异步下载脚本,不阻塞 HTML 解析。
|
||||
|
||||
2. 加载 Vue3 相关资源
|
||||
|
||||
  浏览器下载并执行 Vue3 的核心库(`vue.runtime.global.js`)、项目打包后的 JS 文件(如 `app.js`)和 CSS 文件(如 `app.css`)。
|
||||
::: tip
|
||||
- `app.js`:包含 Vue3 应用的入口逻辑(如 `createApp(App).mount('#app')`)、组件定义、路由配置等。
|
||||
- `app.css`:包含全局样式或组件样式(通过 `<style>` 标签在组件中定义的样式会被编译为内联样式)。
|
||||
:::
|
||||
|
||||
3. 执行 Vue3 应用初始化
|
||||
|
||||
  当浏览器执行到 Vue3 入口脚本时,会触发以下流程:
|
||||
- **创建应用实例**:通过 `createApp(App)` 创建 Vue 应用实例。
|
||||
- **配置插件和路由**:
|
||||
* 安装路由插件 `vue-router`:`app.use(router)`。
|
||||
* 安装状态管理插件(如 `pinia`):`app.use(pinia)`。
|
||||
- **挂载应用到 DOM**:通过 `app.mount('#app')` 将 Vue 组件渲染到 HTML 中的指定容器(如 `<div id="app"></div>`)。
|
||||
|
||||
4. Vue3 组件渲染流程
|
||||
|
||||
  **初始化组件**:从根组件(`App.vue`)开始,递归解析组件树,初始化响应式数据。
|
||||
- **模板编译**:
|
||||
* 若使用单文件组件(`.vue`),模板(`<template>`)会被编译为渲染函数(`render`)。
|
||||
- **响应式依赖收集**:Vue3 通过 `Proxy` 和 `Reflect` 实现响应式,当组件访问数据(如 `message`)时,自动建立依赖关系。
|
||||
- **首次渲染(挂载阶段)**:
|
||||
* 根据渲染函数生成 **虚拟 DOM(VNode)**。
|
||||
* 通过 `patch` 函数将虚拟 DOM 渲染为真实 DOM,并插入到页面中。
|
||||
* 例如,根组件渲染后,`#app` 容器内会生成对应的 HTML 结构。
|
||||
|
||||
5. 处理异步操作(如数据请求)
|
||||
|
||||
  在组件的 `setup` 函数或生命周期钩子(如 `onMounted`)中,通过 `axios` 或 Vue3 内置的 `useFetch` 发起 API 请求:
|
||||
- **数据更新与重新渲染**:当响应数据返回后,更新响应式数据(如 `data.value`),Vue3 检测到变化后,重新生成虚拟 DOM 并与真实 DOM 对比,执行 **差异化更新(Diff 算法)**,只更新变化的部分。
|
||||
|
||||
# 三、页面渲染与交互阶段
|
||||
1. 合成图层并绘制页面
|
||||
|
||||
  浏览器将 DOM 树和 CSSOM 树合并为 **渲染树(Render Tree)**,计算每个元素的布局(Layout)和样式(Style)。通过 **图层合成(Composite)** 将不同层级的元素绘制到页面上,最终显示完整的 UI。
|
||||
|
||||
2. 用户交互与组件更新
|
||||
|
||||
- 当用户触发事件(如点击按钮、输入表单)时:
|
||||
* Vue3 通过 `v-on` 指令绑定事件处理函数(如 `@click="handleClick"`)。
|
||||
* 事件处理函数中修改响应式数据,触发组件重新渲染。
|
||||
- 点击按钮时,`count` 的值更新,Vue3 自动重新渲染按钮文本。
|
||||
|
||||
# 四、总结
|
||||
::: tip
|
||||
输入网址 → DNS解析 → TCP连接 → 服务器返回index.html → 解析HTML加载Vue3脚本 →
|
||||
执行createApp初始化 → 挂载组件到DOM → 首次渲染(虚拟DOM转真实DOM) →
|
||||
异步请求数据 → 数据更新触发重新渲染 → 页面最终呈现 → 用户交互驱动组件更新
|
||||
:::
|
||||
|
||||
  通过以上流程,浏览器从输入网址到完成 Vue3 应用的渲染,充分利用了前端框架的响应式机制和虚拟 DOM 技术,实现高效的页面更新与交互。
|
||||
202
docs/Web/Vue/ElTable.md
Normal file
202
docs/Web/Vue/ElTable.md
Normal file
@@ -0,0 +1,202 @@
|
||||
---
|
||||
title: Element Plus 表格组件
|
||||
date: 2026-05-27
|
||||
---
|
||||
|
||||
# 一、加载中
|
||||
```vue
|
||||
<el-table
|
||||
v-loading="dispatchStore.isLoading"
|
||||
element-loading-text="加载中..."
|
||||
:data="dispatchStore.contractList"
|
||||
border
|
||||
stripeempty-text="暂无记录"
|
||||
class="common-table">
|
||||
|
||||
</el-table>
|
||||
```
|
||||
|
||||
::: tip
|
||||
要配合pinia存储使用,获取数据前将loading置为true,完成后置为false。
|
||||
:::
|
||||
|
||||
# 二、展开行
|
||||
```vue
|
||||
<template>
|
||||
<el-table :data="dispatchStore.contractList"
|
||||
:row-key="getRowKey"
|
||||
:expand-row-keys="expandedRows"
|
||||
@expand-change="handleExpandChange">
|
||||
<el-table-column type="expand">
|
||||
<template #default="props">
|
||||
<el-table :data="props.row.dispatchList"
|
||||
border stripeempty-text="暂无记录"
|
||||
class="dispatch-content"
|
||||
style="width: 100%">
|
||||
<el-table-column fixed type="index" align="center" width="60" />
|
||||
<el-table-column prop="customer" label="终端客户" align="center" width="180"/>
|
||||
</el-table>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const expandedRows = ref<number[]>([])
|
||||
|
||||
// 获取行的唯一标识
|
||||
const getRowKey = (row: IDispatch) => {
|
||||
return row.id
|
||||
}
|
||||
|
||||
// 处理展开行变化
|
||||
const handleExpandChange = (row: IDispatch, expandedRowsList: IDispatch[]) => {
|
||||
if (expandedRowsList.length > 0) {
|
||||
// 只保留最新展开的行
|
||||
expandedRows.value = [getRowKey(row)]
|
||||
} else {
|
||||
expandedRows.value = []
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
  通过类型expand和插槽props实现,可以展示任意格式数据。
|
||||
  增加表格的`row-key`、`expand-row-key`属性可以实现只展开某一行功能。
|
||||
|
||||
# 三、排序和筛选
|
||||
```vue
|
||||
<template>
|
||||
<el-table :data="dispatchStore.contractList">
|
||||
<el-table-column prop="contractNum" label="合同编号" align="center" min-width="10%"
|
||||
:filters=contractNumFilters :filter-method="filterColumnMethod" sortable>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useDispatchStore } from '@/stores/dispatch.ts'
|
||||
import { computed } from 'vue'
|
||||
import type { IContract } from '@/types/dispatch.ts'
|
||||
|
||||
const dispatchStore = useDispatchStore()
|
||||
|
||||
type ContractField = keyof IContract
|
||||
|
||||
const filterColumnMethod = (value, row, column) => {
|
||||
return row[column.property] === value
|
||||
}
|
||||
|
||||
const getFiltersByKey = (key: ContractField) => {
|
||||
const result = [...new Set(dispatchStore.contractList.map((item) => item[key]).filter((value) => value != null && value !==''))]
|
||||
|
||||
return result.map((item) => ({
|
||||
text: item,
|
||||
value: item
|
||||
}))
|
||||
}
|
||||
|
||||
const contractNumFilters = computed(() => getFiltersByKey('contractNum'))
|
||||
</script>
|
||||
```
|
||||
|
||||
  在`el-table-column`中添加`sortable`即可实现排序功能。
|
||||
  在`el-table-column`中添加`filters`和`filter-method`实现筛选功能。
|
||||
  `filters`为原始数据中该字段下的所有值,并转为指定格式,也可以自定义筛选字段。
|
||||
|
||||
# 四、表头搜索
|
||||
```vue
|
||||
<template>
|
||||
<el-table :data="dispatchStore.contractList" >
|
||||
<el-table-column prop="contractNum" label="合同编号" align="center" min-width="10%">
|
||||
<template #header>
|
||||
<div class="custom-header">
|
||||
<span :class="{ 'active': contractNumSearch }">合同编号</span>
|
||||
<el-popover ref="contractNumPopoverRef" placement="bottom" width="230" trigger="click">
|
||||
<template #default>
|
||||
<el-select v-model="contractNumSearch" filterable clearable
|
||||
placeholder="请选择或输入合同编号" @change="applySearch('contractNum')">
|
||||
<el-option v-for="(item, index) in contractNumValue"
|
||||
:key="index" :label="item" :value="item"/>
|
||||
</el-select>
|
||||
<div style="margin-top: 8px;display: flex;justify-content: flex-end;">
|
||||
<el-button type="primary" size="small" @click="applySearch('contractNum')">搜索</el-button>
|
||||
<el-button size="small" @click="resetSearch('contractNum')">重置</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #reference>
|
||||
<el-icon class="search-icon"><Search/></el-icon>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useDispatchStore } from '@/stores/dispatch.ts'
|
||||
import { computed, ref } from 'vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import type { IContract } from '@/types/dispatch.ts'
|
||||
import type { PopoverInstance } from 'element-plus'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
const dispatchStore = useDispatchStore()
|
||||
|
||||
type ContractField = keyof IContract
|
||||
|
||||
const getFiltersByKey = (key: ContractField) => {
|
||||
const result = [...new Set(dispatchStore.contractList.map((item) => item[key]).filter((value) => value != null && value !== ''))]
|
||||
|
||||
return result.map((item) => ({
|
||||
text: item,
|
||||
value: item
|
||||
}))
|
||||
}
|
||||
|
||||
const contractNumFilters = computed(() => getFiltersByKey('contractNum'))
|
||||
|
||||
const contractNumValue = computed(() => contractNumFilters.value.map((item) => item.value))
|
||||
|
||||
const contractNumPopoverRef = ref<PopoverInstance>()
|
||||
const contractNumSearch = ref('')
|
||||
|
||||
const popoverMap = new Map<string, Ref<PopoverInstance | undefined>>([
|
||||
['contractNum', contractNumPopoverRef]
|
||||
])
|
||||
const searchMap = new Map<string, Ref<string>>([
|
||||
['contractNum', contractNumSearch]
|
||||
])
|
||||
|
||||
const applySearch = async (field: ContractField) => {
|
||||
const searchRef = searchMap.get(field)
|
||||
|
||||
if (!searchRef) return
|
||||
|
||||
if (!searchRef.value) {
|
||||
await dispatchStore.queryContractList()
|
||||
return
|
||||
}
|
||||
if (!searchRef.value.trim()) return
|
||||
|
||||
dispatchStore.contractList = dispatchStore.contractList.filter((item) => {
|
||||
const value = item[field]
|
||||
return typeof value === 'string' && value.includes(searchRef.value)
|
||||
})
|
||||
popoverMap.get(field)?.value?.hide()
|
||||
}
|
||||
|
||||
const resetSearch = async (field: string) => {
|
||||
const searchRef = searchMap.get(field)
|
||||
|
||||
if (searchRef) {
|
||||
searchRef.value = ''
|
||||
}
|
||||
popoverMap.get(field)?.value?.hide()
|
||||
await dispatchStore.queryContractList()
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
  表头搜索主要利用`header`插槽和`el-popover`组件实现。
|
||||
313
docs/Web/Vue/ElUpload.md
Normal file
313
docs/Web/Vue/ElUpload.md
Normal file
@@ -0,0 +1,313 @@
|
||||
---
|
||||
title: Element Plus 上传器
|
||||
date: 2026-01-29
|
||||
---
|
||||
|
||||
# 一、上传图片
|
||||
```vue
|
||||
<template>
|
||||
<el-upload
|
||||
v-model:file-list="imageFileList"
|
||||
list-type="picture-card"
|
||||
:on-preview="handlePreview"
|
||||
:limit="3"
|
||||
:before-upload="beforeUpload"
|
||||
:http-request="httpRequest"
|
||||
:on-exceed="handleExceed"
|
||||
:before-remove="beforeRemove"
|
||||
:on-remove="handleRemove"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
请上传小于10M的图片
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-dialog v-model="dialogVisible">
|
||||
<el-image :src="dialogImageUrl" alt="Preview Image" />
|
||||
</el-dialog>
|
||||
</el-upload>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { UploadRequestOptions } from 'element-plus'
|
||||
import type { UploadProps, UploadUserFile, UploadRawFile } from 'element-plus'
|
||||
import { getFileExtension, getFileHashName, validateImage } from '@/utils/file.ts'
|
||||
import { uploadFileApi } from '@/apis/file.ts'
|
||||
|
||||
const imageFileList = ref<UploadUserFile[]>([])
|
||||
const dialogImageUrl = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
|
||||
const baseUrl = ''
|
||||
const apiImageList = [] as string[]
|
||||
|
||||
onMounted(async () => {
|
||||
imageFileList.value = []
|
||||
|
||||
// 根据后端传来的List数据将信息添加文件列表中
|
||||
apiImageList.forEach((item, index) => {
|
||||
imageFileList.value.push({
|
||||
name: index.toString(),
|
||||
url: `${baseUrl}${item}`
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
|
||||
const maxFileMax = 1024 * 1024 * 10
|
||||
const fileType = rawFile.type
|
||||
const fileName = rawFile.name
|
||||
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
|
||||
|
||||
if (!validateImage(fileType, fileExtension)) {
|
||||
ElMessage.error('请上传图片')
|
||||
return false
|
||||
}
|
||||
|
||||
if (rawFile.size > maxFileMax) {
|
||||
ElMessage.error('请上传10M以内的文件!')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const httpRequest: UploadProps['httpRequest'] = async (options: UploadRequestOptions): Promise<any> => {
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '文件上传中 请等待',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
|
||||
try {
|
||||
// 重命名文件
|
||||
const filePath = `${baseUrl}/${getFileHashName(options.file.name)}.${getFileExtension(options.file.name)}`
|
||||
|
||||
const data = new FormData()
|
||||
data.append('file', options.file)
|
||||
data.append('filePath', filePath)
|
||||
|
||||
// 调用后端接口上传文件
|
||||
await uploadFileApi(data)
|
||||
apiImageList.push(filePath)
|
||||
ElMessage.success('上传成功')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
ElMessage.error('上传失败 请联系管理员')
|
||||
} finally {
|
||||
loading.close()
|
||||
}
|
||||
}
|
||||
|
||||
const handleExceed: UploadProps['onExceed'] = () => {
|
||||
ElMessage.warning('最多上传3张图片')
|
||||
}
|
||||
|
||||
const handleRemove: UploadProps['onRemove'] = (uploadFile, uploadFiles) => {
|
||||
const index = imageFileList.value.findIndex((item) => item.uid === uploadFile.uid)
|
||||
apiImageList.splice(index, 1)
|
||||
}
|
||||
|
||||
const handlePreview: UploadProps['onPreview'] = (uploadFile) => {
|
||||
dialogImageUrl.value = uploadFile.url!
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const beforeRemove: UploadProps['beforeRemove'] = () => {
|
||||
return ElMessageBox.confirm('确认删除该文件?').then(() => true, () => false)
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
# 二、上传视频
|
||||
```vue
|
||||
<template>
|
||||
<el-upload
|
||||
v-model:file-list="videoFileList"
|
||||
:limit="1"
|
||||
:before-upload="beforeUpload"
|
||||
:http-request="httpRequest"
|
||||
:on-exceed="handleExceed"
|
||||
:before-remove="beforeRemove"
|
||||
:on-remove="handleRemove"
|
||||
>
|
||||
<el-button type="primary">点击上传</el-button>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
请上传小于200M的视频
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { UploadRequestOptions } from 'element-plus'
|
||||
import type { UploadProps, UploadUserFile, UploadRawFile } from 'element-plus'
|
||||
import { getFileExtension, getFileHashName, validateVideo } from '@/utils/file.ts'
|
||||
import { uploadFileApi } from '@/apis/file.ts'
|
||||
|
||||
const videoFileList = ref<UploadUserFile[]>([])
|
||||
|
||||
let baseUrl = ''
|
||||
const apiVideoUrl = ''
|
||||
|
||||
onMounted(async () => {
|
||||
videoFileList.value = []
|
||||
if (apiVideoUrl) {
|
||||
videoFileList.value.push({
|
||||
name: 'video',
|
||||
url: `${baseUrl}p/${apiVideoUrl}`
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
|
||||
const maxFileMax = 1024 * 1024 * 200
|
||||
const fileType = rawFile.type
|
||||
const fileName = rawFile.name
|
||||
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
|
||||
|
||||
if (!validateVideo(fileType, fileExtension)) {
|
||||
ElMessage.error('请上传视频')
|
||||
return false
|
||||
}
|
||||
|
||||
if (rawFile.size > maxFileMax) {
|
||||
ElMessage.error('请上传200M以内的文件!')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const httpRequest: UploadProps['httpRequest'] = async (options: UploadRequestOptions): Promise<any> => {
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '文件上传中 请等待',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
|
||||
try {
|
||||
const filePath = `${baseUrl}/${getFileHashName(options.file.name)}.${getFileExtension(options.file.name)}`
|
||||
|
||||
const data = new FormData()
|
||||
data.append('file', options.file)
|
||||
data.append('filePath', filePath)
|
||||
|
||||
await uploadFileApi(data)
|
||||
baseUrl = filePath
|
||||
ElMessage.success('上传成功')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
ElMessage.error('上传失败 请联系管理员')
|
||||
} finally {
|
||||
loading.close()
|
||||
}
|
||||
}
|
||||
|
||||
const handleExceed: UploadProps['onExceed'] = () => {
|
||||
ElMessage.warning('最多上传1个视频')
|
||||
}
|
||||
|
||||
const beforeRemove: UploadProps['beforeRemove'] = () => {
|
||||
return ElMessageBox.confirm('确认删除该文件?').then(() => true, () => false)
|
||||
}
|
||||
|
||||
const handleRemove: UploadProps['onRemove'] = () => {
|
||||
baseUrl = ''
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
# 三、工具类
|
||||
```ts
|
||||
import { hashSHA256 } from 'vue3-common/utils/cryptoUtil'
|
||||
|
||||
export const getFileHashName = (name: string) => {
|
||||
return hashSHA256(name)
|
||||
}
|
||||
|
||||
export const getFileExtension = (name: string) => {
|
||||
return name.substring(name.lastIndexOf('.') + 1).toLowerCase()
|
||||
}
|
||||
|
||||
export const validateExcel = (fileType: string, fileExtension: string): boolean => {
|
||||
// 常见的 Excel MIME 类型
|
||||
const excelMimeTypes = [
|
||||
'application/vnd.ms-excel', // .xls
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
|
||||
'application/vnd.ms-excel.sheet.macroEnabled.12', // .xlsm
|
||||
'text/csv' // CSV
|
||||
]
|
||||
|
||||
// 常见的 Excel 文件扩展名
|
||||
const excelExtensions = ['xls', 'xlsx', 'xlsm', 'csv']
|
||||
|
||||
return excelMimeTypes.includes(fileType) || excelExtensions.includes(fileExtension.toLowerCase())
|
||||
}
|
||||
|
||||
export const validateImage = (fileType: string, fileExtension: string): boolean => {
|
||||
// 常见的图片 MIME 类型
|
||||
const imageMimeTypes = [
|
||||
'image/jpeg', // JPEG
|
||||
'image/jpg', // JPG
|
||||
'image/png', // PNG
|
||||
'image/gif', // GIF
|
||||
'image/bmp', // BMP
|
||||
'image/webp', // WebP
|
||||
'image/svg+xml' // SVG
|
||||
]
|
||||
|
||||
// 常见的图片文件扩展名
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']
|
||||
|
||||
return imageMimeTypes.includes(fileType) || imageExtensions.includes(fileExtension.toLowerCase())
|
||||
}
|
||||
|
||||
export const validateVideo = (fileType: string, fileExtension: string): boolean => {
|
||||
// 常见的视频 MIME 类型
|
||||
const videoMimeTypes = [
|
||||
'video/mp4', // MP4
|
||||
'video/mpeg', // MPEG
|
||||
'video/quicktime', // MOV
|
||||
'video/x-msvideo', // AVI
|
||||
'video/x-matroska', // MKV
|
||||
'video/webm', // WebM
|
||||
'video/3gpp', // 3GP
|
||||
'video/3gpp2', // 3G2
|
||||
'video/x-flv', // FLV
|
||||
'video/mp2t', // TS (MPEG Transport Stream)
|
||||
'application/x-mpegURL', // M3U8 (HLS)
|
||||
'video/H264', // H264
|
||||
'video/H265' // H265/HEVC
|
||||
]
|
||||
|
||||
// 常见的视频文件扩展名
|
||||
const videoExtensions = [
|
||||
'mp4',
|
||||
'mpeg',
|
||||
'mpg',
|
||||
'mov',
|
||||
'avi',
|
||||
'mkv',
|
||||
'webm',
|
||||
'3gp',
|
||||
'3g2',
|
||||
'flv',
|
||||
'ts',
|
||||
'm3u8',
|
||||
'h264',
|
||||
'h265',
|
||||
'hevc'
|
||||
]
|
||||
|
||||
return videoMimeTypes.includes(fileType) || videoExtensions.includes(fileExtension.toLowerCase())
|
||||
}
|
||||
```
|
||||
1766
docs/Web/Vue/JavaScript-Guide.md
Normal file
1766
docs/Web/Vue/JavaScript-Guide.md
Normal file
File diff suppressed because it is too large
Load Diff
95
docs/Web/Vue/Package.md
Normal file
95
docs/Web/Vue/Package.md
Normal file
@@ -0,0 +1,95 @@
|
||||
---
|
||||
title: 前端包管理器
|
||||
date: 2025-12-13
|
||||
---
|
||||
|
||||
# npm
|
||||
  2010年随Node.js发布的npm,首次为JavaScript引入了标准的包管理系统。其核心创新是**package.json文件**,通过语义化版本规范定义了依赖声明标准。这一设计解决了手动管理依赖时的版本混乱问题,为模块化开发提供了基础设施。
|
||||
|
||||
## 第一阶段:嵌套依赖架构
|
||||
  早期版本采用树状嵌套的依赖结构。每个包将其依赖安装在自身的node_modules目录中,形成深层嵌套。这种设计保证了依赖隔离,但导致了几个严重问题:
|
||||
- 路径深度问题:依赖树深度可达十几层,触发Windows 260字符路径限制
|
||||
- 空间极度浪费:相同包在不同层级重复安装,项目体积指数级膨胀
|
||||
- 安装性能低下:大量递归操作导致I/O效率极低
|
||||
|
||||
## 第二阶段:扁平化依赖重构
|
||||
  2015年发布的npm v3引入扁平化依赖安装策略。该方案将**可共享的依赖提升到顶层node_modules**,减少嵌套深度。
|
||||
  主要解决方案为:
|
||||
- 依赖提升算法:将兼容版本的依赖包提升至顶层
|
||||
- 冲突处理:版本冲突时,低版本保持嵌套结构
|
||||
- 确定性牺牲:提升顺序影响最终结构
|
||||
|
||||
  但同时也引入了新问题:
|
||||
- 幻影依赖:未声明但被提升的包意外可用
|
||||
- 依赖不确定性:相同package.json产生不同目录结构
|
||||
- 模块解析复杂化:Node.js模块解析算法与扁平结构不匹配
|
||||
|
||||
# yarn
|
||||
  2016年发布的Yarn针对npm核心痛点提出了系统性解决方案:
|
||||
- 确定性版本锁定:yarn.lock文件记录精确版本和依赖树
|
||||
- 并行下载优化:多线程并发下载提升网络利用率
|
||||
- 离线缓存机制:全局缓存支持离线安装
|
||||
- 完整性校验:checksum验证确保包完整性
|
||||
|
||||
  Yarn的成功迫使npm进行重大改进。npm v5版本借鉴了Yarn的核心设计,增加了package-lock.json和缓存优化,形成了技术竞争的良性循环。
|
||||
|
||||
# pnpm
|
||||
  2017年发布的pnpm从存储层面重新设计了包管理模型。其核心是**基于内容寻址的全局存储与符号链接架构**。
|
||||
  实现原理:
|
||||
- 内容可寻址存储:包按内容哈希存储,全局唯一
|
||||
- 硬链接复用:相同包文件在所有项目间共享
|
||||
- 符号链接树:通过软链接构建符合Node.js解析规则的依赖树
|
||||
- 严格依赖隔离:每个包只能访问其声明的依赖
|
||||
|
||||
  npm/yarn的node_modules目录:
|
||||
```cmd
|
||||
项目A: node_modules/lodash@4.17.21
|
||||
项目B: node_modules/lodash@4.17.21 # 重复存储
|
||||
项目C: node_modules/lodash@4.17.21 # 重复存储
|
||||
```
|
||||
|
||||
  pnpm的node_modules目录:
|
||||
```cmd
|
||||
全局存储: .pnpm-store/lodash@4.17.21
|
||||
项目A: 硬链接 → 全局存储
|
||||
项目B: 硬链接 → 全局存储
|
||||
项目C: 硬链接 → 全局存储
|
||||
```
|
||||
|
||||
  npm/Yarn采用扁平化结构,包可访问非声明依赖,pnpm采用符号链接嵌套结构,包仅可访问声明依赖。
|
||||
  pnpm的符号链接架构在Monorepo场景中表现优异。跨包依赖通过本地文件系统链接实现,避免了重复安装。Workspaces功能通过优化符号链接策略,确保开发环境与生产环境一致性。
|
||||
|
||||
## 简单使用
|
||||
```cmd
|
||||
# 通过 npm 安装
|
||||
npm install -g pnpm
|
||||
|
||||
# 检查版本
|
||||
pnpm --version
|
||||
|
||||
# 安装所有依赖(根据 package.json)
|
||||
pnpm install
|
||||
|
||||
# 安装生产依赖
|
||||
pnpm add <package-name>
|
||||
|
||||
# 安装开发依赖
|
||||
pnpm add -D <package-name>
|
||||
|
||||
# 移除依赖
|
||||
pnpm remove <package-name>
|
||||
|
||||
# 查看所有配置
|
||||
pnpm config list
|
||||
|
||||
# 设置存储路径
|
||||
pnpm config set store-dir ~/.pnpm-store
|
||||
|
||||
# 使用淘宝镜像
|
||||
pnpm config set registry https://registry.npmmirror.com/
|
||||
```
|
||||
|
||||
::: tip
|
||||
npm确立了基本范式,Yarn解决了确定性问题,pnpm重构了存储模型
|
||||
:::
|
||||
|
||||
171
docs/Web/Vue/Vite-Principle.md
Normal file
171
docs/Web/Vue/Vite-Principle.md
Normal file
@@ -0,0 +1,171 @@
|
||||
---
|
||||
title: Vite核心原理
|
||||
date: 2025-12-13
|
||||
---
|
||||
|
||||
# 一、Vite简介
|
||||
  Vite是新一代的前端构建工具,在尤雨溪开发Vue3.0的时候诞生。类似于Webpack+ Webpack-dev-server。
|
||||
  其主要利用浏览器**ESModule特性**导入组织代码,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。生产中利用Rollup作为打包工具,号称下一代的前端构建工具。
|
||||
|
||||
# 二、核心原理
|
||||
## 2.1 ESModule
|
||||
  ESModule是JavaScript提出的官方标准化模块系统,不同于之前的CJS,AMD,CMD等等,ESModule提供了更原生以及更动态的模块加载方案,最重要的就是它是浏览器原生支持的,也就是说我们可以**直接在浏览器中去执行import**,动态引入我们需要的模块,而不是把所有模块打包在一起。
|
||||
  例如声明一个script标签:`<script type="module" src="/src/main.js"></script>`,当浏览器解析资源时,会往当前域名发起一个GET请求`main.js`文件:
|
||||
```javascript
|
||||
// main.js
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
createApp(App).mount('#app')
|
||||
```
|
||||
  请求到了main.js文件,会检测到内部含有import引入的包,又会import引用发起HTTP请求获取模块的内容文件,如App.vue文件。
|
||||
|
||||
## 2.2 基于ESModule的Dev Server
|
||||
  Vite其核心原理是利用浏览器现在已经支持ES6的import,碰见import就会发送一个HTTP请求去加载文件,**Vite启动一个koa服务器拦截这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再以ESM格式返回返回给浏览器**。
|
||||
  Vite整个过程中没有对文件进行打包编译,做到了真正的按需加载,所以其运行速度比原始的webpack开发编译速度快出许多。
|
||||
|
||||
## 2.3 基于ESModule的热更新
|
||||
  热更新主要思路是通过WebSocket创建浏览器和服务器的通信监听文件的改变,当文件被修改时,服务端发送消息通知客户端修改相应的代码,客户端对应不同的文件进行不同的操作的更新。
|
||||
  **Vite 通过 chokidar 来监听文件系统的变更,只用对发生变更的模块重新加载**,只需要精确的使相关模块与其临近的 HMR边界连接失效即可,这样HMR 更新速度就不会因为应用体积的增加而变慢而 Webpack 还要经历一次打包构建。所以 HMR 场景下,Vite 表现也要好于 Webpack。
|
||||
|
||||
## 2.4 基于Esbuild的开发环境编译
|
||||
  Esbuild是一个JavaScript Bundler 打包和压缩工具,它提供了与Webpack、Rollup等工具相似的资源打包能力。可以将JavaScript 和TypeScript代码打包分发在网页上运行。但其打包速度却是其他工具的10~100倍。
|
||||
  在开发环境中,Vite 使用 esbuild 进行快速的热重载和模块编译。
|
||||
|
||||
### 2.4.1 Esbuild高性能原理
|
||||
1. Go 语言实现:避开 JavaScript 运行时的性能瓶颈。
|
||||
2. 并行编译:利用多核 CPU 同时处理多个文件。
|
||||
3. 内存优化:在单个进程中完成解析、转换和生成,减少内存开销。
|
||||
|
||||
### 2.4.2 Esbuild关键特性
|
||||
1. 语法转换:TypeScript → JS(保留类型信息或移除)、现代 JS 语法降级(如 ?.、?? 转换)等。
|
||||
2. 压缩优化:变量重命名、死代码消除、支持 CSS 压缩(通过 esbuild-plugin-css-minify)等。
|
||||
|
||||
## 2.5 基于Rollup的生产环境打包
|
||||
  Rollup是基于ESM的JavaScript打包工具。相比于其他打包工具如Webpack,他总是能打出更小、更快的包。因为 Rollup 基于 ESM 模块,比 Webpack 和 Browserify 使用的 CommonJS模块机制更高效。Rollup的亮点在于同一个地方,一次性加载。能针对源码进行 Tree Shaking(去除那些已被定义但没被使用的代码),以及 Scope Hoisting 以减小输出文件大小提升运行性能。
|
||||
  在生产环境中,Vite 使用 Rollup 进行最终的打包和优化。
|
||||
|
||||
### 2.5.1 Rollup构建流程
|
||||
1. 解析入口文件:从 index.js 开始,递归分析所有 import 语句。
|
||||
2. 生成抽象语法树(AST):使用 acorn 解析 JS 文件为 AST。
|
||||
3. 应用插件转换:如 @rollup/plugin-typescript 处理 TypeScript。
|
||||
4. 生成依赖图:分析模块间的依赖关系。
|
||||
5. 输出优化:
|
||||
Tree-shaking:基于 ESM 静态分析,移除未使用的代码
|
||||
代码分割:识别动态导入(import())并拆分包。。
|
||||
|
||||
# 三、Mini-Vite
|
||||
## 3.1 初始化工程
|
||||
  初始化NodeJs工程,并添加Vue3依赖
|
||||
|
||||
## 3.2 Node Server
|
||||
```javascript
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const miniVueParse = require('./mini-vue-parser')
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
// 获取请求的路径
|
||||
const reqPath = req.url;
|
||||
// 构建本地文件路径
|
||||
const filePath = path.join(__dirname, reqPath);
|
||||
|
||||
console.log('filePath: ', filePath)
|
||||
|
||||
|
||||
// 检查文件是否存在
|
||||
fs.access(filePath, fs.constants.R_OK, (err) => {
|
||||
if (err) {
|
||||
console.error('文件不存在:', err);
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
res.end('文件不存在');
|
||||
} else {
|
||||
// 读取文件并发送给客户端
|
||||
fs.readFile(filePath, 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
console.error('读取文件出错:', err);
|
||||
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
||||
res.end('内部服务器错误');
|
||||
} else {
|
||||
// 根据文件类型设置合适的 Content-Type
|
||||
const extname = path.extname(filePath);
|
||||
let contentType = 'text/plain';
|
||||
switch (extname) {
|
||||
case '.html':
|
||||
contentType = 'text/html';
|
||||
break;
|
||||
case '.js':
|
||||
contentType = 'text/javascript';
|
||||
break;
|
||||
case '.vue':
|
||||
contentType = 'text/javascript';
|
||||
break;
|
||||
case '.css':
|
||||
contentType = 'text/css';
|
||||
break;
|
||||
// 添加其他需要支持的文件类型
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': contentType });
|
||||
|
||||
if (extname === '.js') {
|
||||
data = data.replace(/from ['"]vue['"]/g, "from './node_modules/vue/dist/vue.esm-browser.js'");
|
||||
}
|
||||
|
||||
if (extname === '.vue') {
|
||||
const fileName = path.basename(filePath);
|
||||
data = miniVueParse.parse(data, fileName)
|
||||
}
|
||||
|
||||
res.end(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const port = 3000;
|
||||
|
||||
server.listen(port, () => {
|
||||
console.log(`代理服务器正在监听端口 ${port}`);
|
||||
});
|
||||
```
|
||||
  在浏览器输入`http://localhost:3000/index.html`,会首先解析`<script type="module" src="main.js"></script>`,获取`main.js`,然后会把`import { createApp } from 'vue'`中的`Vue`换成替换成:`./node_modules/vue/dist/vue.esm-browser.js'`,在遇到`Vue`文件时,会利用`Vue`的模板编译器转成Javascript文件:
|
||||
  mini-vue-parser.js:
|
||||
```JavaScript
|
||||
const compilerSfc = require("@vue/compiler-sfc");
|
||||
const path = require('path');
|
||||
|
||||
function parse(vueFileString,fileName) {
|
||||
const sfc = compilerSfc.parse(
|
||||
vueFileString
|
||||
);
|
||||
const sfcTemplate = compilerSfc.compileTemplate({
|
||||
id: 'sfc',
|
||||
filename: fileName + '.template.vue',
|
||||
source: sfc.descriptor.template.content
|
||||
})
|
||||
|
||||
const sfcScript = compilerSfc.compileScript(sfc.descriptor, {
|
||||
filename: 'sfc.script.vue'
|
||||
})
|
||||
|
||||
let result = '';
|
||||
result += sfcTemplate.code.replace("export", "") + '\n';
|
||||
result += sfcScript.content.replace("export default ", "const __script = ") + '\n';
|
||||
result += `__script.render=render;\n`
|
||||
result += `export default __script;`
|
||||
result = result.replace(/from ['"]vue['"]/g, "from './node_modules/vue/dist/vue.esm-browser.js'");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parse
|
||||
}
|
||||
```
|
||||
|
||||
# 四、参考资料
|
||||
1. [深入浅出vite(核心原理 + 手撕mini-vite)](https://juejin.cn/post/7026285200766140453#heading-37)
|
||||
2. [深入理解Vite核心原理](https://juejin.cn/post/7064853960636989454)
|
||||
3. [step-vite](https://github.com/Llyonss/step-vite)
|
||||
Reference in New Issue
Block a user