這次主要實(shí)現(xiàn) 創(chuàng)建文件夾、刪除、移動 這三項(xiàng)功能。
常規(guī)開發(fā)時(shí),需要現(xiàn)在服務(wù)端創(chuàng)建 http 接口,提供“創(chuàng)建文件夾、刪除、移動”功能??蛻舳送ㄟ^ fetch/xhr 鏈接到服務(wù)端。
Next.js@13 提供了一個(gè)叫 server action 的功能,可以省略創(chuàng)建 http 接口的過程,客戶端可以直接調(diào)用服務(wù)端的方法。
開發(fā)
這次的功能開發(fā)涉及的文件較多,就只針對“移動”功能做詳細(xì)解釋。其余的兩個(gè)可以在源碼中查看。
文件樹
explorer/src/components/move-modal/action.ts
explorer/src/components/move-modal/modal.tsx
explorer/src/components/move-modal/move-form.tsx
explorer/src/components/move-modal/move-path-context.tsx
explorer-manager/src/main.mjs
服務(wù)端:explorer-manager
新增一個(gè)移動方法,node 沒有提供遞歸移動目錄的方法。這里使用了一個(gè)外部依賴fs-extra.moveSync來來實(shí)現(xiàn)移動目錄的功能。
import fsExtra from 'fs-extra'
const { moveSync } = fsExtra
export const moveAction = async (src, dest) => {
return moveSync(formatPath(src), formatPath(dest), { overwrite: false })
}
客戶端:explorer
文件路徑:explorer/src/components/move-modal/move-path-context.tsx
創(chuàng)建移動文件彈窗上下文。
'use client'
import createCtx from '@/lib/create-ctx'
import React from 'react'
import MoveModal from '@/components/move-modal/modal'
export const MovePathContext = createCtx<string>()
export const useMovePathStore = MovePathContext.useStore
export const useMovePathDispatch = () => {
const changeMovePath = MovePathContext.useDispatch()
return (path: string) => changeMovePath(decodeURIComponent(path))
}
export const MovePathProvider: React.FC<React.PropsWithChildren> = ({ children }) => {
return (
<MovePathContext.ContextProvider value={''}>
{children}
<MoveModal />
</MovePathContext.ContextProvider>
)
}
文件路徑:explorer/src/components/move-modal/modal.tsx
移動文件彈窗組件
'use client'
import React from 'react'
import { Modal } from 'antd'
import { isEmpty } from 'lodash'
import MoveForm from '@/components/move-modal/move-form'
import { useMovePathDispatch, useMovePathStore } from '@/components/move-modal/move-path-context'
const MoveModal: React.FC = () => {
const move_path = useMovePathStore()
const changeMovePath = useMovePathDispatch()
return (
<Modal
title="移動"
open={!isEmpty(move_path)}
width={1000}
onCancel={() => changeMovePath('')}
footer={false}
destroyOnClose={true}
>
<MoveForm />
</Modal>
)
}
export default MoveModal
文件路徑:explorer/src/components/move-modal/move-form.tsx
移動文件 form 組件。基礎(chǔ)的 antd form 封裝。內(nèi)部提供一個(gè)只讀的來源目錄。一個(gè)移動目標(biāo)目錄。一個(gè)當(dāng)前文件名。
當(dāng)點(diǎn)擊“移動”時(shí)直接調(diào)用 moveAction 方法。
import React from 'react'
import { App, Flex, Form, Input, Space } from 'antd'
import SelectPathInput from '@/components/select-path-input'
import { useMovePathDispatch, useMovePathStore } from '@/components/move-modal/move-path-context'
import { moveAction } from '@/components/move-modal/action'
import SubmitBtn from '@/components/submit-btn'
import { useUpdateReaddirList } from '@/app/path/readdir-context'
import { parseDirPath } from '@/explorer-manager/src/parse-path.mjs'
const onFinish = (values: any) => {
console.log('Success:', values)
}
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo)
}
type FieldType = {
path?: string
new_path?: string
last?: string
}
const MoveForm: React.FC = () => {
const [form] = Form.useForm()
const move_path = useMovePathStore()
const { message } = App.useApp()
const changeMovePath = useMovePathDispatch()
const { update } = useUpdateReaddirList()
const { last, remain } = parseDirPath(move_path)
return (
<Form
form={form}
labelCol={{ span: 3 }}
initialValues={{ remember: true, path: move_path, new_path: remain, last: last }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
<Form.Item<FieldType> label="來源" name="path" rules={[{ required: true, message: '請輸入來源' }]}>
<Input disabled readOnly />
</Form.Item>
<SelectPathInput
onSelect={(path) => {
form.setFieldsValue({ new_path: '/' + path.join('/') + '/' })
}}
highlight_path={move_path}
>
<Form.Item<FieldType>
label="新位置"
name="new_path"
rules={[{ required: true, message: '請輸入移動置目錄' }]}
style={{ width: '100%' }}
>
<Input placeholder={`移動目錄`} />
</Form.Item>
</SelectPathInput>
<Form.Item<FieldType> label="名稱" name="last" rules={[{ required: true, message: '請輸入名稱' }]}>
<Input placeholder={`名稱`} />
</Form.Item>
<Form.Item>
<Flex justify="flex-end">
<Space>
<SubmitBtn
onClick={async () => {
const { path, new_path, last } = await form.validateFields()
return moveAction(path, [new_path, last].join('/')).then(() => {
changeMovePath('')
update()
message.success('移動成功').then()
})
}}
>
移動
</SubmitBtn>
</Space>
</Flex>
</Form.Item>
</Form>
)
}
export default MoveForm
文件路徑:explorer/src/components/move-modal/action.ts
文件頂部添加 ‘use server’,標(biāo)記為 server action 方法文件。
該方法返回一個(gè) Promise 對象,內(nèi)部直接調(diào)用了 node 的 explorer-manager 下的 moveAction 方法。
'use server'
import { moveAction as sysMoveAction } from '@/explorer-manager/src/main.mjs'
export const moveAction: (path: string, new_path: string) => Promise<{ message: string; status: string }> = async (
path,
new_path,
) => {
try {
await sysMoveAction(path, new_path)
return Promise.resolve({ status: 'ok', message: '移動成功' })
} catch (err: any) {
return Promise.resolve({ status: 'error', message: '移動失敗' })
}
}
觀察瀏覽器開發(fā)者工具的“網(wǎng)絡(luò)”信息時(shí),Next.js 自動將該方法生成一個(gè) API 接口,使用 fetch 的 post 的形式請求當(dāng)前頁面地址。
實(shí)際上還是客戶端使用 fetch 請求服務(wù)端,只是開發(fā)者可以省略創(chuàng)建一個(gè) API 路由的過程。
剩下的 “創(chuàng)建文件夾、刪除”也是一樣的流程,Node.js 分別創(chuàng)建 mkdirSync 創(chuàng)建文件夾、rmSync 刪除文件方法。
Next.js 創(chuàng)建兩個(gè)按鈕,當(dāng)點(diǎn)擊按鈕時(shí),直接調(diào)用該方法,傳入需要創(chuàng)建的文件名稱、刪除的文件路徑即可。
server action 的方法默認(rèn)為一個(gè) Promise 對象,方法內(nèi) return 的則為 Promise 的數(shù)據(jù)。
可以使用 await serverAction or serverAction.then() 來讀取 return 的數(shù)據(jù)。
刪除菜單按鈕
{
icon: <DeleteOutlined />,
label: '刪除',
key: 'delete',
onClick: () => {
modal.confirm({
title: `確認(rèn)刪除?`,
icon: <ExclamationCircleOutlined />,
content: item.name,
okText: '刪除',
cancelText: '取消',
onOk: async () => {
deleteAction(path).then(() => {
deleteItemAction(item.name)
})
},
})
},
}
創(chuàng)建文件,在 antd Form 的 onFinish 回調(diào)調(diào)用
const CreateFolderForm: React.FC = () => {
const { update } = useUpdateReaddirList()
const { message: appMessage } = App.useApp()
const { replace_pathname } = useReplacePathname()
return (
<Form
labelCol={{ span: 2 }}
initialValues={{ dir_name: '新建文件夾' }}
onFinish={(values) => {
const { dir_name } = values
createFolder([replace_pathname, dir_name].join('/'))
.then(({ status, message }) => {
if (status === 'error') {
return Promise.reject({ status, message })
}
update()
appMessage.success('新建文件夾成功').then()
})
.catch(({ message }) => {
appMessage.error(`新建文件夾失敗: ${message}`).then()
})
}}
onFinishFailed={onFinishFailed}
>
<Form.Item name="dir_name" rules={[{ required: true, message: '請輸入文件夾名稱' }]}>
<Input />
</Form.Item>
<Form.Item>
<Flex justify="flex-end">
<SubmitBtn>確定</SubmitBtn>
</Flex>
</Form.Item>
</Form>
)
}
效果




