如何在 React 中实现基于设备的代码拆分

2025-06-07

如何在 React 中进行基于设备的代码拆分

📩

本文结合了各种论点、现实检验以及最后的代码解决方案。其重点在于:在 React 中实现基于设备(触控/桌面)驱动、无需后端的代码拆分。

通往实际实施的道路往往漫长而坎坷——优先级、设计、预算、持有不同观点的同事、使用不同语言交流。这些障碍极具挑战性,通常需要比仅仅编码付出更多的精力。因此,值得在此单独写一篇序言。

如果这是您要找的内容,请跳至代码部分,否则让我们继续。

如果你已经了解代码拆分是什么,这将会很有帮助。如果还不了解,React 文档中的“代码拆分”部分是一个不错的开始。


现实检验

如今,许多公司更愿意构建针对触摸设备和桌面设备的网络应用程序/网站,而不愿意投资单独的移动应用程序。

酋长们可能不会承认,但原因如下:

  1. 为浏览器构建既快速又便宜。
  2. 不需要后端介入。
  3. 重视“移动优先”,但实际上并不符合该原则。
  4. 将移动应用程序交付到商店存在技术障碍。
  5. 没有预算。

在浏览器中工作快速可靠。许多静态网站生成器(GatsbyNextjsDocusaurus)支持无需后端知识即可创建网站。Jamstack原理和工具使产品的生产部署比以往任何时候都更加轻松。这些工具能够将“移动优先”的概念变为现实,尽管这仍然只是美好的愿望。

与此同时,将独立的移动应用发布到某些应用商店可能会变成一场噩梦。阅读Hey saga fx 的相关内容。相比之下,JavaScript 开发人员可以借助 Chrome 工具快速构建移动版本,那么为什么要雇佣 iOS/Android 开发人员呢?

这些观点都很有道理,而且,作为前端专业人士,你通常没有机会影响最终决策(尤其是在大公司)。最终决策权应该由产品、市场或财务团队来决定。

原生应用程序或 Web 应用程序...让我们假设已经做出决定并且您别无选择 -必须交付 Web 应用程序(针对桌面和移动用户)


如果你必须进行代码拆分

如果必须在前端进行操作,则按触摸/桌面方式拆分反应应用程序可能会很棘手。

需要考虑的事项:

  • 1️⃣ 考虑触摸和桌面设备(何时为每个应用程序提供服务)
  • 2️⃣ 确定拆分起点(代码中的位置)
  • 3️⃣ 仅导入应用程序特定的组件(如何实现)

这三个问题的答案很重要,因为可维护性、时间、团队动力和其他方面都很大程度上取决于它。


当设备被视为触摸时

通常您会修改组件的 CSS 以适应移动设备。

或许以下

.TopBar {
  height: 60px;
  background-color: #fff;
  ...
}

/* Mobile */
@media (max-width: 768px) {
  .TopBar {
    height: 100px;
    background-color: #ccc;
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

大多数情况下效果都很好。相同的组件,但根据浏览器宽度显示不同的外观。这种方法没有问题,而且通常就足够了。现在有人可能会说,这max-width: 768px足以正确判断用户是否在使用移动设备。可能并非如此。也许这样更准确:

@media (pointer: coarse) and (hover: none) {
  ...
}
Enter fullscreen mode Exit fullscreen mode

您可以阅读更多关于交互媒体功能及其对设备性能影响的潜力的文章。在确定移动 Web 应用的服务标准时,请考虑这些内容。


当你的公司开始更加重视移动用户(“移动优先”)时,挑战就会随之而来。这可能是因为公司正在组建一个独立的、强大的设计/用户体验和产品团队。在这种情况下,你的桌面版和移动网站/应用最终可能会截然不同。业务逻辑、页面、交互和整体外观现在都不一样了。就像是同一软件的两个独立版本。

这在 React 语言中该如何翻译?

当然,您无法在两个应用(触屏和桌面)中复用每个组件。相同的组件/页面需要不同的数据集,并且行为方式(JavaScript 逻辑)也不同。其他组件/页面在每个应用中将完全不同。在这种情况下,像上面那样进行 CSS 调整可能不再有效。交互和数据(JavaScript)需要与样式(CSS)一起考虑。

这是必须在前端进行适当拆分的地方,并且它不能.css单独驻留在您的文件中。


在哪里拆分应用程序 2️⃣

这真的要看情况。考虑到需求和设计,你有几个选择。一种是将应用拆分到根目录。也许你已经根据 URL 路径渲染了页面组件PageRouter.js,或者只是渲染了页面组件。第二种选择是拆分单个组件。如果移动端和桌面端的页面相同(或非常相似),但某些子组件不同,那么拆分组件是一个不错的选择。你也可以选择第三种方案,即在 CSS 中使用媒体查询。App.js

在应用程序的根目录中拆分

如果您的移动应用程序和桌面应用程序非常不同,则这种方法很有意义 - 组件中单独的页面、行为、数据和业务逻辑。

假设<ProductDetails />您的桌面网站中不存在触屏版产品详情页面 ( )。该页面会显示详细的产品信息,这些信息<Products />在电脑上查看时会显示出来。然而,在手机上,在一个页面上显示如此多的数据可能会显得过于“杂乱”。

-- src
   |-- components
   |-- pages
   |   |-- touch
   |   |   |-- Products.js
   |   |   |-- ProductDetails.js
   |   |-- desktop
   |   |   |-- Products.js
   |   |-- common
   |       |-- Checkout.js
   |-- App.js
Enter fullscreen mode Exit fullscreen mode

查看Codesandbox 中的工作示例。

为什么这个结构是可以的?

  • 更多控制

您可以将文件/touch/desktop视为两个独立的应用程序,从而完全控制其内容。

  • 维护更轻松

您的应用程序中的大多数页面都是通用的 - 组件名称相同,但实现特定于应用程序的逻辑,这有利于维护。

  • 隔离修复错误

触摸产品页面出现错误,说明问题可能出在touch/Products.js。修复该错误可以确保您的桌面页面不受影响。

  • 副作用较少

移动端需要多几个按钮,或者桌面端需要多一个下拉菜单?下次实现类似的功能请求时,你就能更安心了。

  • 充分的团队协作

实现产品页面意味着你必须为每个应用(两个组件)都实现。有了上面的文件夹拆分,团队内部的工作就更容易划分,不会互相干扰。

按组件级别拆分

根级代码拆分通常会通过/components类似方式拆分文件夹来补充。另一方面,有时您的桌面应用和移动应用并不会有很大差异。只有树深处的少数组件可能具有不同的数据模型或行为。如果您遇到任何这些情况,按组件进行拆分可能会有所帮助

-- src
   |-- components
   |   |-- touch
   |   |   |-- TopBar.js
   |   |   |-- TopBar.css
   |   |-- desktop
   |   |   |-- TopBar.js
   |   |   |-- TopBar.css
   |   |-- common
   |       |-- Footer.js
   |       |-- Footer.css
   |-- pages
   |-- App.js
Enter fullscreen mode Exit fullscreen mode

<TopBar />组件有一些数据/行为差异,需要您为每个应用单独实现。同时,/common文件夹仍然包含所有共享组件。

/components您可以在产品页面示例中看到如何实现这一点

为什么这个结构是可以的?

除了上一节的优点之外,由于只需要拆分少数组件,您需要维护的代码将更少。应用程序特定组件和共享组件的复用也将变得非常简单。

import ProductDescription from "../../components/desktop/ProductDescription";

export default function Products() {
  ...
}
Enter fullscreen mode Exit fullscreen mode

pages/desktop/Products仅从 导入组件components/desktop

具有样式差异的组件

如果组件包含相同的逻辑,但样式不同,是否应该创建两个副本?看起来应该将它们放在同一个/common文件夹中,但同时它的 CSS 需要使用传统的媒体查询方法。

@media (max-width: 768px) { ... }

/* OR */

@media (pointer: coarse) and (hover: none) { ... }
Enter fullscreen mode Exit fullscreen mode

看起来还不错。但这真的是你能做的最好的事情吗?如果检测移动功能的逻辑发生了变化怎么办?你需要在所有地方都进行修改吗?这显然不是最佳方案。

好的,该怎么办?

理想情况下,检测触摸设备的逻辑应该是应用程序的核心。让桌面或移动组件渲染只需简单调整一个 prop 即可。

想象一下这个结构:

-- src
   |-- components
   |   |-- touch
   |   |   |-- TopBar.js
   |   |   |-- TopBar.css
   |   |-- desktop
   |   |   |-- TopBar.js
   |   |   |-- TopBar.css
   |   |-- common
   |       |-- TopBarLinks.js
   |       |-- TopBarLinks.css
   |-- pages
   |-- App.js
Enter fullscreen mode Exit fullscreen mode

<TopBarLinks />是一个共享组件,可能存在一些视觉差异。其 CSS 代码中通过一个类来解决这个问题。

.TopBarLinks { ... }         /* Desktop */
.TopBarLinks.touch { ... }   /* Mobile */
Enter fullscreen mode Exit fullscreen mode

desktop/TopBar然后它在和中都被使用touch/TopBar

// desktop/TopBar.js
export const TopBar = () => (
  <div className="TopBar">
    <img alt="Logo" src="../../assets/logo.png" />
    <TopBarLinks />
  </div>
);
Enter fullscreen mode Exit fullscreen mode


// touch/TopBar.js
export const TopBar = () => (
  <div className="TopBar">
    <img alt="Logo" src="../../assets/logo.png" />
    <TopBarLinks touch />
  </div>
);
Enter fullscreen mode Exit fullscreen mode

就是这样。这就是你如何通过可视化差异渲染共享组件的方法。这样一来,CSS 文件会更简洁,并且不受设备检测逻辑的影响。

关于组织代码库的可能性已经说得够多了。现在,谈谈如何将所有内容整合在一起。


按需加载组件 3️⃣

无论拆分位于何处——应用程序根目录、单个组件,或者两者兼而有之——其实现都将相同。最终,所有先前示例中的页面也都是组件。

任务是在浏览器中仅加载桌面触控相关的代码。加载整个 bundle(所有组件),但仅使用(渲染)特定于设备的切片可能有效,但并非最佳方案。正确的实现需要使用动态 import()

React 文档告诉你,Suspense底层依赖于该原则,并且很可能能够完成这项工作。你也可以基于loadable-components库来构建你的解决方案。为了简单起见,并涵盖基于触摸/桌面分离的具体用例,我们进一步关注一个简​​单的解决方案。

有条件地导入和渲染组件

我个人设想在应用程序根目录中有以下内容(App.js):

import Import from "./Import";

function App() {
  return (
    <div className="App">
      <h1>Product page</h1>
      <Import
        touch={() => import("./touch/Products")}
        desktop={() => import("./desktop/Products")}
      >
        {Product => <Product />}
      </Import>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

在示例 Codesandbox 应用程序中查看它

<Import />组件(您可以将其命名为其他名称)接受两个 props -desktoptouch。它们期望一个返回动态导入调用的函数。在上面的示例中,有两个独立的<Product />页面组件,您可能需要有条件地导入/渲染它们。

第三个 prop 是一个children执行实际渲染的函数。在这里使用 render prop 函数的一个明显好处是,可以根据需要显式地将任何 props 传递给组件。

{Product =>
  <Product
    title={product.title}
    description={product.description}
  />
}
Enter fullscreen mode Exit fullscreen mode

实现细节

内部将Import执行的操作是:评估要加载哪个组件并将其作为参数传递给渲染 prop 函数。

基本实现可能如下所示:

// Detect touch enabled devices based on interaction media features
// Not supported in IE11, in which case isMobile will be 'false'
const isMobile =
  window.matchMedia("(pointer: coarse) and (hover: none)").matches;

export function Import({ touch, desktop, children }) {
  const [Component, setComponent] = useState(null);

  useEffect(() => {
    // Assign a callback with an import() call
    const importCallback = isMobile ? touch : desktop;

    // Executes the 'import()' call that returns a promise with
    // component details passed as an argument
    importCallback().then(componentDetails => {
      // Set the import data in the local state
      setComponent(componentDetails);
    });
  }, [desktop, touch]);

  // The actual component is assigned to the 'default' prop
  return children(Component ? Component.default : () => null);
}
Enter fullscreen mode Exit fullscreen mode

有关导入及其用法的更多信息 - 检查应用程序上下文

一些注意事项:

  1. window.matchMedia("(pointer: coarse) and (hover: none)")- 您可以在此处使用任何其他机制来检测触摸功能。更进一步,isMobile可以改为使用 store 来检测(如果您使用 redux、mobx 或其他全局状态管理机制)。

  2. importCallback().then(componentDetails)- 实际组件已设置componentDetails.default,您必须使用默认导出(export default function Products())将其导出。

  3. 最后,将导入的数据设置为本地状态,并将组件传递给子函数进行渲染。

使用此功能import()需要满足一些先决条件,以便能够正确解析并将最终的 bundle 拆分成多个部分。您可能需要额外进行设置。

Webpack 配置

为了使拆分功能正常工作,需要对 webpack 配置文件进行一些调整。Dan Abramov编写的示例配置可以在 github 上找到。如果您使用的是Create React App,则默认已完成此操作。

module.exports = {
  entry: {
    main: './src/App.js',
  },
  output: {
    filename: "bundle.js",
    chunkFilename: "chunk.[id].js",
    path: './dist',
    publicPath: 'dist/'
  }
};
Enter fullscreen mode Exit fullscreen mode

Babel 插件

如果您使用 Babel,则需要@babel/plugin-syntax-dynamic-import插件才能正确解析动态导入。

Eslint 配置

eslint-plugin-import也需要支持 export/import 语法。别忘了更新你的 eslint 配置文件:

{
  parser: "babel-eslint",
  plugins: ["import"]
  ...
}
Enter fullscreen mode Exit fullscreen mode

再次, Create React App默认支持代码拆分,在这种情况下您可以跳过配置步骤。


最后的话

有关基于设备的代码拆分的详细信息,请查看Codesandbox 中的完整代码实现。

最后,我想分享一下我自己想要构建上述应用结构的动机。你的情况可能并非如此,但我的观察表明,尤其是在产品、后端和前端明确划分的大型公司中,这种思维模式很普遍。

实际上,通过技术解决方案来克服流程问题比试图改变人要容易得多(而且往往是唯一能做的事情)。

举个例子:你知道后端会在一周内交付 API,但你也知道 UI 可以今天交付。要等一周才能交付后端?后端交付缓慢可能是由于组织问题。在这种情况下,技术解决方案是模拟负载并尽早交付给 QA 和产品团队。

当尝试通过仔细分割应用程序代码来避免后端时,同样的动机也发挥着作用。

仅前端应用程序拆分将允许:

  • 较少的后端依赖带来更快的开发速度
  • 请求变更时的灵活性

这也意味着您不必与同事和管理层对抗,从而减少麻烦,并且由于您仍然留在 JavaScript 领域(您熟悉的专业领域),因此信心更高。

📩

如果您遇到 Google 搜索无法解决的流程或代码难题,欢迎加入我的读者群。我每月都会更新类似的帖子。


资源

文章来源:https://dev.to/moubi/thoughts-on-device-based-code-split-in-react-4n09
PREV
在 JavaScript 中使用 Promises 时最常见的 3 个错误
NEXT
遗憾的是,我必须与 Leaf(我的编程语言)告别