Node.js
安装依赖
$ cd /explorer-manager
$ pnpm i @types/node@20.10.4 -D
安装 node types 类型描述,20+,不影响实际运行,提升开发体验。
获取指定路径下文件夹、文件方法
使用 Node.js 的 fs.readdirSync 同步方法读取指定路径下的内容
功能实现
文件树
explorer-manager
└── src
├── format-path.mjs
├── main.mjs
└── type.ts
文件路径:/explorer-manager/src/format-path.mjs
import sys_path from 'path'
export const BASE_EXPLORER_PATH = process.env.EXPLORER_PATH || process.env.HOME
export const formatPath = (...path) => {
return sys_path
.join(BASE_EXPLORER_PATH, ...path)
.split('/')
.map((text) => {
try {
return decodeURIComponent(text)
} catch (e) {
return text
}
})
.join('/')
}
export const resetPath = (path) => {
return path.replace(BASE_EXPLORER_PATH, '')
}
formatPath
用于组合路径,可通过环境变量 EXPLORER_PATH
设置起始位置。默认为当前用户文件夹。
resetPath
用于还原原始路径。
文件路径:/explorer-manager/src/main.mjs
import fs from 'fs'
import { formatPath } from './format-path.mjs'
const checkIsHideExp = /^\./
/**
* @param path {string}
* @param only_dir {only_dir:'0'|'1'}
* @param only_file {only_file:'0'|'1'}
* @param show_hide {show_hide:'0'|'1'}
* @param has_file_stat {has_file_stat:'0'|'1'}
* @returns {import('./type').ReaddirListType}
*/
export const readdir = (path = '.', { only_dir = '0', only_file = '0', show_hide = '0', has_file_stat = '0' } = {}) => {
return fs.readdirSync(formatPath(path), { withFileTypes: true }).reduce((dir_list, Dirent) => {
const is_directory = !Dirent.isFile()
const name = Dirent.name
let stat = {}
if (has_file_stat === '1') {
stat = fs.statSync(formatPath(path, name))
}
if (show_hide === '0' && checkIsHideExp.test(name)) {
return dir_list
}
if (only_dir === '1' && !is_directory) {
return dir_list
}
if (only_file === '1') {
if (is_directory) {
return dir_list
} else if (has_file_stat !== '1') {
stat = fs.statSync(formatPath(path, name))
}
}
dir_list.push({
name,
is_directory,
stat: { ...stat },
})
return dir_list
}, [])
}
使用 .mjs 作为后缀名(Node.js 13.2.0 版本后可以识别运行 ES Modules)。方便使用 node *.mjs 进行测试。
可在 Next.js 内 import *.mjs 文件
类型描述文件
文件路径:/explorer-manager/src/type.ts
import { Stats } from 'fs'
export type ReaddirItemType = { is_directory: boolean; name: string; stat?: Stats }
export type ReaddirListType = ReaddirItemType[]
Next.js
页面路由设计
使用 Next.js 动态路由进行跳转,格式为
http[s]://domina/path/[[…path]]
Next.js 不可在根目录进行动态路由配置,必须要添加一级/path/
目录、路径。
[[…path]]
可动态识别例如
domian/path/media/video-1
dir
将是 { path: ['media', 'video-1'] }
参考链接 - Optional Catch-all Segments
使用 Next.js 的 <Link/>
标签进行跳转。会使用 React 的流式传输改变页面内容。
功能实现
首先将 explorer-manager monorepo 加入 Next.js 别名引用中,方便之后引用。
/explorer/tsconfig.json 添加下面内容
compilerOptions.paths['@/explorer-manager/*'] = ["../explorer-manager/*"]
{
...
"compilerOptions": {
...
"paths": {
"@/*": ["./src/*"],
"@/explorer-manager/*": ["../explorer-manager/*"]
}
}
...
}
文件树
在 /explorer/src/app/path/ 目录下创建下面文件夹与文件
├── [[...path]]
│ ├── card-display.tsx
│ ├── change-display-type.tsx
│ ├── layout.tsx
│ ├── page.tsx
│ └── tale-display.tsx
├── context.tsx
└── loading.tsx
- loading.tsx:用于 <Link /> 标签跳转时 loading 效果
- context.tsx:react context 组件,用于跨组件快速访问数据
- [[...path]]/layout.tsx:根据 url 内 pathname 读取指定目录下内容
- [[...path]]/page.tsx:页面组件
- [[...path]]/card-display.tsx:卡片呈现
- [[...path]]/tale-display.tsx:表格呈现
- [[...path]]/change-display-type.tsx:在 card 与 table 之间切换呈现样式
文件路径:path/loading.tsx
import Loading from '@/components/loading'
export default Loading
loading 组件很简单,直接引用公共组件内的 loading
文件路径:path/context.tsx
'use client'
import React, { createContext, useContext, useState } from 'react'
import { ReaddirListType } from '@/explorer-manager/src/type'
type PathContextType = {
readdir: ReaddirListType
display_type: 'card' | 'table'
changeDisplayType: React.Dispatch<React.SetStateAction<PathContextType['display_type']>>
}
const PathContext = createContext<PathContextType>(null!)
export const usePathContext = () => {
return useContext(PathContext)
}
export const PathContextProvider: React.FC<React.ProviderProps<ReaddirListType>> = ({ value, children }) => {
const [display_type, changeDisplayType] = useState<'card' | 'table'>('card')
return (
<PathContext.Provider value={{ readdir: value, display_type: display_type, changeDisplayType: changeDisplayType }}>
{children}
</PathContext.Provider>
)
}
context 中注册
- readdir:当前目标目录文件、文件夹信息
- display_type:显示类型
- changeDisplayType:改变现实类型
将显示类型与改变显示类型也放入上下文中,方便在更深组件上改变呈现样式
会出现只需要 readdir 数据的组件会被 display_type 改变时触发不必要的 rerender,会影响页面的性能。后续会有相关解决方案的文章
文件路径:path/[[...path]]/layout.tsx
import React from 'react'
import { readdir } from '@/explorer-manager/src/main.mjs'
import { PathContextProvider } from '@/app/path/context'
const Layout: React.FC<React.PropsWithChildren & { params: { path: string[] } }> = ({
children,
params: { path = [] },
}) => {
const readdirList = readdir(path.join('/'), { only_dir: '0', only_file: '0', has_file_stat: '1' })
return <PathContextProvider value={readdirList}>{children}</PathContextProvider>
}
export default Layout
- 这里使用了 Optional Catch-all Segments 路由,组件会多接受一个 params 的入参,内容为当前目录名 [[…path]] 内的 path。数据内容为一个将 URL 以 ’/‘ 分割的的数组字符串
- 将 readdir 读取到的目标目录内容加入 PathContext 上下文中,方便组件消费数据
文件路径:path/[[...path]]/page.tsx
'use client'
import React from 'react'
import { Card } from 'antd'
import CardDisplay from '@/app/path/[[...path]]/card-display'
import TableDisplay from '@/app/path/[[...path]]/tale-display'
import ChangeDisplayType from '@/app/path/[[...path]]/change-display-type'
import { usePathContext } from '@/app/path/context'
const Page: React.FC = () => {
const { display_type } = usePathContext()
return <Card extra={<ChangeDisplayType />}>{display_type === 'table' ? <TableDisplay /> : <CardDisplay />}</Card>
}
export default Page
读取 display_type ,分别呈现对应的样式
文件路径:path/[[...path]]/card-display.tsx
'use client'
import React from 'react'
import { usePathContext } from '@/app/path/context'
import { Card, Flex, List } from 'antd'
import { FileOutlined, FolderOutlined } from '@ant-design/icons'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
const CardDisplay: React.FC = () => {
const pathname = usePathname()
const { readdir } = usePathContext()
return (
<List
grid={{ gutter: 0, xs: 3, md: 4, lg: 5, xl: 6, xxl: 7 }}
dataSource={readdir}
renderItem={(item) => {
return (
<List.Item style={{ padding: '0 8px' }}>
<Card title={item.name}>
<Link
href={item.is_directory ? `${pathname}/${encodeURIComponent(item.name)}` : `${pathname}`}
prefetch={false}
>
<Flex
justify={'center'}
align={'center'}
style={{ fontSize: '3em', padding: `${61.8 / 2}% 0`, position: 'relative' }}
>
<span style={{ position: 'absolute' }}>
{item.is_directory ? <FolderOutlined /> : <FileOutlined />}
</span>
</Flex>
</Link>
</Card>
</List.Item>
)
}}
/>
)
}
export default CardDisplay
使用 List 组件封装,用于不同屏幕大小控制不同的列数。
内部为 Card 组件
标题为当前文件、文件夹名字
内容如果是文件夹将使用文件夹的📂,文件则为文件📃。并使用上下边距将内容撑开,维持一个 1:0.618 的比例,并将内容上下垂直居中。并设置为 absolute 状态,避免破坏长宽比。
文件路径:path/[[...path]]/tale-display.tsx
'use client'
import React from 'react'
import { usePathContext } from '@/app/path/context'
import { Space, Table } from 'antd'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { FileOutlined, FolderOutlined } from '@ant-design/icons'
const TableDisplay: React.FC = () => {
const pathname = usePathname()
const { readdir } = usePathContext()
return (
<Table
rowKey={'name'}
columns={[
{
title: '文件名',
dataIndex: 'name',
key: 'name',
render: (name, item) => {
return (
<Space>
{item.is_directory ? <FolderOutlined /> : <FileOutlined />}
<Link
href={item.is_directory ? `${pathname}/${encodeURIComponent(name)}` : `${pathname}`}
prefetch={false}
>
{name}
</Link>
</Space>
)
},
},
{
title: '大小',
dataIndex: ['stat', 'size'],
width: '160px',
sorter: (a, b) => (a?.stat?.size ?? 0) - (b?.stat?.size ?? 0),
render: (size) => {
return size
},
},
{
title: '修改时间',
dataIndex: ['stat', 'mtimeMs'],
width: '160px',
sorter: (a, b) => (a?.stat?.mtimeMs ?? 0) - (b?.stat?.mtimeMs ?? 0),
render: (time) => {
return time
},
},
]}
dataSource={readdir}
/>
)
}
export default TableDisplay
表格视图,分为 标题、大小、修改时间 三列
其中标题部分会根据当前文件夹 OR 文件使用不同的 ICON 区分
文件路径:[[...path]]/change-display-type.tsx
import React from 'react'
import { Button } from 'antd'
import { usePathContext } from '@/app/path/context'
const ChangeDisplayType: React.FC = () => {
const { changeDisplayType, display_type } = usePathContext()
return (
<Button
onClick={() => {
changeDisplayType(display_type === 'table' ? 'card' : 'table')
}}
>
display
</Button>
)
}
export default ChangeDisplayType
使用 PathContext 上下文中的方法,在 card 与 table 之间切换
结果
卡片视图
表格视图