從零開始-文件資源管理器-13-Next.js server action 創(chuàng)建文件夾、刪除、移動

這次主要實(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>
  )
}

效果

截屏2023-12-16 19.00.23.png
截屏2023-12-16 19.00.34.png
截屏2023-12-16 19.00.46.png
截屏2023-12-16 19.00.56.png
截屏2023-12-16 19.01.08.png

git-repo

yangWs29/share-explorer

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容