从零开始-文件资源管理器-13-Next.js server action 创建文件夹、删除、移动

这次主要实现 创建文件夹、删除、移动 这三项功能。

常规开发时,需要现在服务端创建 http 接口,提供"创建文件夹、删除、移动"功能。客户端通过 fetch/xhr 链接到服务端。

Next.js@13 提供了一个叫 server action 的功能,可以省略创建 http 接口的过程,客户端可以直接调用服务端的方法。

开发

这次的功能开发涉及的文件较多,就只针对"移动"功能做详细解释。其余的两个可以在源码中查看。

文件树

scss 复制代码
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

服务端:explorer-manager

新增一个移动方法,node 没有提供递归移动目录的方法。这里使用了一个外部依赖fs-extra.moveSync来来实现移动目录的功能。

javascript 复制代码
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

创建移动文件弹窗上下文。

typescript 复制代码
'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

移动文件弹窗组件

javascript 复制代码
'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 组件。基础的 antd form 封装。内部提供一个只读的来源目录。一个移动目标目录。一个当前文件名。

当点击"移动"时直接调用 moveAction 方法。

ini 复制代码
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',标记为 server action 方法文件。

该方法返回一个 Promise 对象,内部直接调用了 node 的 explorer-manager 下的 moveAction 方法。

typescript 复制代码
'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: '移动失败' })
  }
}

观察浏览器开发者工具的"网络"信息时,Next.js 自动将该方法生成一个 API 接口,使用 fetch 的 post 的形式请求当前页面地址。

实际上还是客户端使用 fetch 请求服务端,只是开发者可以省略创建一个 API 路由的过程。

剩下的 "创建文件夹、删除"也是一样的流程,Node.js 分别创建 mkdirSync 创建文件夹、rmSync 删除文件方法。

Next.js 创建两个按钮,当点击按钮时,直接调用该方法,传入需要创建的文件名称、删除的文件路径即可。

server action 的方法默认为一个 Promise 对象,方法内 return 的则为 Promise 的数据。

可以使用 await serverAction or serverAction.then() 来读取 return 的数据。

删除菜单按钮

less 复制代码
      {
        icon: <DeleteOutlined />,
        label: '删除',
        key: 'delete',
        onClick: () => {
          modal.confirm({
            title: `确认删除?`,
            icon: <ExclamationCircleOutlined />,
            content: item.name,
            okText: '删除',
            cancelText: '取消',
            onOk: async () => {
              deleteAction(path).then(() => {
                deleteItemAction(item.name)
              })
            },
          })
        },
      }

创建文件,在 antd Form 的 onFinish 回调调用

ini 复制代码
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>
  )
}

效果

git-repo

yangWs29/share-explorer

相关推荐
敲代码的玉米C4 小时前
我修的那个 bug,制造了另一个 bug
前端·人工智能·架构
gis开发之家4 小时前
《Vue3 从入门到大神50篇》Vue3 源码详解(二十):生命周期钩子源码解析 —— onMounted / onUpdated 如何实现?
前端·javascript·前端框架·vue3·vue3源码
Narrastory4 小时前
我用 Claude Code 写代码不到 2 小时,却花了 3 天做完这个软件—Vibe Coding 的正确姿势,是设计不是生成
前端·人工智能·github
Enaium4 小时前
我实现了KMP的SDL绑定并编写了高性能软光追
前端·kotlin
古夕4 小时前
本地正常线上白屏:一次路由切换后刷新恢复问题的排查与修复
前端·ai编程
栀鸢ouo5 小时前
解决Element Plus表格展开行横向溢出、滚动截断问题(项目实战方案)
前端·vue.js
用户39051332192885 小时前
写了5年JS,才发现这10个方法能少写一半代码
前端
平凡的阿泽5 小时前
我用TRAE Work手搓了一个「谁是卧底」小游戏
前端·javascript
一心只读圣贤书5 小时前
AI 辅助前端视觉回归治理:从截图基线到变更解释
前端·人工智能
嘟嘟07175 小时前
用单例模式管理弹窗:从一段原生 JS 理解 Singlet
前端·javascript·代码规范