密集的导入语句不仅对视觉造成冲击,也是对代码组织结构的一次考验。
如何优雅地管理这些导入语句,避免“全屏占用”?本文将探讨生成大量导入语句的原因,可能带来的问题,以及如何从多个角度优化和管理导入语句。
拒绝使用模块重新导出
模块重新导出是一种常见技术,广泛应用于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'
重新导出通常用于整合同类型的模块,通常按文件夹组织,如 components
、routes
、utils
、hooks
、stories
等,都通过各自的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.ensure
或import
根据条件动态加载组件,便于维护,减少引用也减少性能开销。
// 异步加载全局弹窗以减少性能开销
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等方法,我们将不得不写满屏幕的导入语句 😂🤣😅😇。