密集的导入语句不仅对视觉造成冲击,也是对代码组织结构的一次考验。

如何优雅地管理这些导入语句,避免“全屏占用”?本文将探讨生成大量导入语句的原因,可能带来的问题,以及如何从多个角度优化和管理导入语句。

拒绝使用模块重新导出

模块重新导出是一种常见技术,广泛应用于Twitter、字节跳动和谷歌等大公司的组件库中。

例如,在字节跳动的arco-design组件库中:https://github.com/arco-design/arco-design/blob/main/components/index.tsx

通过在components/index.tsx文件中重新导出所有组件,你可以只用一条导入语句来使用多个组件。

// 不要使用命名导入
import Modal from '@arco-design/web-react/es/Modal'
import Checkbox from '@arco-design/web-react/es/Checkbox'
import Message from '@arco-design/web-react/es/Message'
...

// 使用命名导入
import { Modal, Checkbox, Message } from '@arco-design/web-react'

image.png

重新导出通常用于整合同类型的模块,通常按文件夹组织,如 componentsroutesutilshooksstories 等,都通过各自的index.tsx文件进行暴露。这极大简化了导入路径,提高了代码的可读性和可维护性。

重新导出的几种形式:

直接重新导出:直接从另一个模块重新导出特定成员。

export { foo, bar } from './moduleA';

重命名和重新导出(包括默认导出):从另一个模块导入成员,可能重命名后再导出。默认导出也可以重命名和重新导出。

// 通过export导出
export { foo as newFoo, bar as newBar } from './moduleA';
// 通过export default导出
export { default as ModuleDDefault } from './moduleD';

重新导出整个模块(不包括默认导出):将另一个模块的所有导出成员重新导出为一个对象。(注意:重新导出不包括默认导出)

export * from './moduleA';

合并导入和重新导出:先导入模块中的成员,然后使用它们,最后重新导出它们。

import { foo, bar } from './moduleA';
export { foo, bar };

通过这些形式,我们可以灵活地组织和管理代码模块。每种形式都有其适用场景,选择合适的方式可以帮助我们构建更清晰和高效的代码结构。

使用 require.context

require.context是一个非常有用的功能,可以让我们在不显式地一个个导入的情况下动态导入一组模块。

只需一段代码,当你需要添加文件或组件时,它会自动收集并重新导入。

在固定场景如项目路由和状态管理中效果极佳(提高效率,避免添加一个配置需要修改多个文件的情况)。

尤其是在配置路由时,当需要生成大量导入时(你有多少页面就得导入多少页面 😅),require.context非常有用。

// 不要使用require.context
import A from '@/pages/A'
import B from '@/pages/B'
...

// 统一处理routes/index.ts文件
// 创建一个上下文来导入routes目录下的所有.ts文件
const routesContext = require.context('./routes', false, /.ts$/);
const routes = [];

// 遍历上下文中的每个模块
routesContext.keys().forEach(modulePath => {
  // 获取模块的导出
  const route = routesContext(modulePath);
  // 获取组件名称 [如果需要],例如:从"./Header.ts"中提取"Header"
  // const routeName = modulePath.replace(/^./(.*).\w+$/, '$1');
  // 将组件存储在组件对象中
  routes.push(route.default || route);
});

export default routes;

在拥有多个路由的大型项目中,使用require.context可以很好地处理路由导入。

使用动态导入

动态导入也可以实现与require.context类似的功能,动态打包模块。

对ProvidePlugin不感兴趣

webpack.ProvidePlugin是个好东西,但不应滥用。一旦配置好,项目中使用的变量/函数/库或工具可以在任何地方使用。

相信我——看完这个例子,如果你以前没用过,你会迫不及待地想试试 🤗

const webpack = require('webpack');

module.exports = {
  // 其他配置...
  plugins: [
    new webpack.ProvidePlugin({
      React: 'react',
      _: 'lodash',
      dayjs: 'dayjs',
      // 假设项目src目录中的自定义utils.js
      Utils: path.resolve(__dirname, 'src/utils.js')
    })
  ]
  // 其他配置...
};

在你可以在任何地方使用dayjs、lodash、Utils等,而无需导入它们。

webpack.ProvidePlugin是一个强大的工具,可以帮助我们减少重复的导入语句,使代码更简洁。然而,它并不能减少构建大小,因为这些库仍然会被包含在最终的捆绑文件中。正确使用这个插件可以提高开发效率,但应谨慎使用,以避免隐藏依赖,导致代码难以理解和维护。
对于需要按需加载的模块或组件,考虑使用动态import()语法,更有效地控制代码何时加载并减少捆绑大小。
谨慎使用ProvidePlugin,仅对在多个地方需要全局变量配置的模块使用,避免不必要的代码捆绑。
此外,如果是Vite项目,你可以使用vite-plugin-inject代替ProvidePlugin功能。

// 配置
import inject from 'vite-plugin-inject'; // 未提供测试,可更新为替代方案
...
plugins: [
  inject({
    // 键是你想提供的全局变量,值是你想提供的模块
    dayjs: 'dayjs', // 例如,这将全局提供'dayjs',可通过dayjs访问
    // 你可以继续添加需要全局提供的其他模块
  }),
]
...

如果使用TS,记得配置类型。

// globals.d.ts文件处理全局类型
import dayjs from 'dayjs';
declare global {
  const dayjs: typeof dayjs;
}

// 还要配置tsconfig.json文件
{
  "compilerOptions": {
    // 编译选项...
  },
  "include": ["src/**/*", "globals.d.ts" // 确保TypeScript包含此文件]
}

大量的TypeScript类型导入

在TS项目中,屏幕上会有大量的TypeScript导入。然而,通过适当的配置,可以显著减少导入数量。

这里介绍我在项目中最常用的方法:TS命名空间。使用它,不仅可以模块化类型,更重要的是可以直接使用类型而无需导入它们 😅。

类似于ProvidePlugin,它可以直接消除导入语句。

// accout.ts
declare namespace IAccount {
  type IList<T = IItem> = {
    count: number
    list: T[]
  }
  interface IUser {
    id: number;
    name: string;
    avatar: string;
  }
}

// 直接在任何文件中使用,无需导入。
const [list, setList] = useState<IAccount.IList | undefined>();
const [user, setUser] = useState<IAccount.IUser | undefined>();

注意 ⚠️ 可能需要配置eslint以启用命名空间的使用 🔛

充分利用Babel功能

React似乎也意识到了这个问题:在17版之前,由于JSX的特性,每个组件需要显式地从'react'导入React。然而之后,编译器自动转换,不再需要导入React。如果你使用的是React 17之前的版本,可以通过修改Babel来实现这一点。更多细节请参考React官方文档,提供了非常详细的解释。(还提供了自动删除导入的脚本。)

其他技巧

设置webpack和TypeScript别名,可以缩短导入路径,使其更具语义化。

resolve: {
  alias: {
    "@src": path.resolve(__dirname, 'src/'),
    "@components": path.resolve(__dirname, 'src/components/'),
    "@utils": path.resolve(__dirname, 'src/utils/')
  }
}

// 使用别名前
import MyComponent from '../../../../components/MyComponent';

// 使用别名后
import MyComponent from '@components/MyComponent';

设置格式化的 prettier.printWidth

将值设置得太小可能会导致频繁的换行,使其难以阅读。120是一个更合适的值(基于团队的实际使用)。

{
  "printWidth": 120,
  ...
}

根据条件全局动态加载组件

在入口文件中导入全局组件,使用require.ensureimport根据条件动态加载组件,便于维护,减少引用也减少性能开销。

// 异步加载全局弹窗以减少性能开销
Vue.component('IMessage', function (resolve) {
  // 在指定条件下全局加载,无需在具体页面中引用。
  if (/^\/pagea|pageb/.test(location.pathname)) {
    require.ensure(['./components/message/index.vue'], function() {
      resolve(require('./components/message/index.vue'));
    });
  }
});

使用babel-plugin-import

babel-plugin-import并不能直接减少导入数量,但通过优化导入语句来减少包大小并提高项目加载性能。这是对使用大型第三方库的项目非常有价值的优化技术。

以arco-design为例:

// .bablerc配置
{
  "plugins": [
    ["import", {
      "libraryName": "@arco-design/web-react",
      "libraryDirectory": "es", // 或"lib",取决于使用的具体模块系统
      "style": true // 加载CSS
    }, "@arco-design/web-react"]
  ]
}

此配置告诉babel-plugin-import自动将类似import { Button } from '@arco-design/web-react'; 的导入语句转换为按需导入,并加载相应的CSS文件。

结尾

有很多原因会导致屏幕充满导入语句。然而,如果没有诸如重新导入模块、require.context、动态导入、webpack.ProvidePlugin等方法,我们将不得不写满屏幕的导入语句 😂🤣😅😇。

交流

首发于公众号 大迁世界,欢迎关注。📝 每周一篇实用的前端文章 🛠️ 分享值得关注的开发工具 ❓ 有疑问?我来回答

本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试完整考点、资料以及我的系列文章。


王大冶
68.1k 声望105k 粉丝