为 带有副作用 的库提供 “按需加载” 功能。
pnpm add vite-plugin-demand-import -D
import { defineConfig } from 'vite'
import demandImport from 'vite-plugin-demand-import'
export default defineConfig({
plugins: [
demandImport({
lib: 'antd-mobile',
resolver: {
js({ name }) {
return `antd-mobile/es/components/${name}`
}
}
})
]
})
/////////// 编译结果 ////////////
import { Button } from 'antd-mobile'
↓ ↓ ↓ ↓ ↓ ↓
import Button from 'antd-mobile/es/components/button'
export type ResolverOptions = {
/**
* 被导入的模块标识,import { Button } from 'antd-mobile' 中 name 等于 'Button'
*/
name: string
/**
* 当前被解析文件的 id,一般是文件的绝对路径
*/
file: string
}
/**
* 返回 import xxx from 'yyy' 语句中的 yyy
*/
export type Resolver = (options: ResolverOptions) => string
export type DemandImportOptions = {
/**
* 类库的名称,用来判断当前 import 语句是否需要处理
*/
lib: string
/**
* 库的命名风格
*
* @default "kebab-case"
* @description "default" 将不做处理
*/
namingStyle?: 'kebab-case' | 'camelCase' | 'PascalCase' | 'default'
/**
* 路径解析器
*/
resolver: {
js?: Resolver // 返回 js 文件的导入路径
style?: Resolver // 返回样式文件的导入路径
}
}
在使用各种 “xxx-import” 插件之前我们需要先区分几个概念:
编译阶段
进行删减。编译阶段
自动插入导入语句。运行时
动态返回用户当前访问的资源。简单的代码说明:
// a.js
export const a1 = 1
export const a2 = 2
//b.js
export const b1 = 1
export const b2 = 2
// index.js
import { a1 } from './a'
import { b1 } from './b'
console.log(a1)
////////// 使用 rollup 打包 ////////////
// dist/index.js
const a1 = 1
console.log(a1)
a.js
& b.js
中没有用到的代码都被删减掉了。
import { Button } from 'antd'
///////// 使用 xxx-import 插件 /////////
import { Button } from 'antd'
// 自动插入了样式的导入语句
import 'antd/lib/style/button/index.less'
if (location.pathname.include('/login')) {
import('./login.js').then((res) => {
// do something
})
}
目前主要是路由中用的多,比如 vue-router
、react-router
中我们经常会配置:
export const routes = [
{
name: 'login',
path: '/login',
component: () => import('./Login/index')
}
]
需要注意的是 日常表述中 “按需加载” 在不同的场景下跟 “tree shaking” 或者 “自动导入” 是等价的。比如:
网上关于这个特性的文章很多了,这里只大概说一下我知道的几种方式:
sideEffects: false
便可以自动 tree shaking。@rollup/plugin-commonjs
插件进行 commonjs 到 esm 的转换。这个插件默认的行为是:如果模块使用 exports.xxx
导出会 tree shaking,而 modules.exports
导出则不会。import round from 'lodash/round'
代替 import { round } from 'lodash'
。关于这块可以使用 lodash
跟 lodash-es
进行测试,前者不会 tree shaking 而后者会。
一句话概括就是可以在运行时按需导入的就是“动态导入”,在编码时写死的就是“静态导入”:
import a from 'a' // 静态导入
if (flag) {
import 'a' // 动态导入,esm 不支持该行为
}
const a = require('a') // 静态导入
if (flag) {
require('a') // 动态导入,commonjs 支持该行为
}
xxx-import
插件antd 官方出品,感觉可以算这个领域最知名的类库吧,功能非常齐全。比如:
// .babelrc
{
"plugins": [["import", {
"libraryName": "antd"
}]]
}
// 效果
import { Button } from 'antd';
↓ ↓ ↓ ↓ ↓ ↓
var _button = require('antd/lib/button');
// .babelrc
{
"plugins": [["import", {
"libraryName": "antd",
style: true
}]]
}
// 效果
import { Button } from 'antd';
↓ ↓ ↓ ↓ ↓ ↓
var _button = require('antd/lib/button');
require('antd/lib/button/style'); // 自动插入了导入语句
这里只展示最简单的用法,更多复杂的配置可以查看官方文档,感觉基本没有解决不了的场景。
不过随着 esm 的普及以及在常规项目中 “全局导入” 比 “按需加载” 并不会大多少体积,所以作者推荐? 用 “全局导入” 代替 “按需加载”,这个插件也就不再需要了。
相关结论是 babel-plugin-import 某个 issue 中作者说的,一时找不到链接了 ~~~
库如其名,这个是 vite 框架的插件。
因为 vite 是基于 rollup 构建的,js 代码一般都可以自动 tree shaking,所以只需要处理样式文件的 “自动导入” 就好了:
import { ElButton } from 'element-plus';
↓ ↓ ↓ ↓ ↓ ↓
// dev
import { Button } from 'element-plus';
import 'element-plus/lib/theme-chalk/el-button.css`;
// prod
import Button from 'element-plus/lib/el-button';
import 'element-plus/lib/theme-chalk/el-button.css';
值得一提的是作者非常细心的区分了开发跟正式环境:
sideEffects
导致 tree shaking 不生效?上面扯了那么多终于到重点了,哈哈。
其实问题还是出在 UI 库构建时对样式的处理差异上,比如 antd 的组件代码跟样式是分离的:
button
|—— style/index.js
└—— index.js
这时候使用 vite-plugin-style-import 自动导入样式就好了。
但是 antd-mobile 构建出来的是:
button
|—— button.css
|—— button.js
└—— index.js
// index.js
import './button.css'
import { Button } from './button'
export default Button
官方为了“方便”用户使用而自动在组件入口引入了样式文件,对应的 sideEffects
配置:
"sideEffects": [
"**/*.css",
"**/*.less",
"./es/index.js",
"./src/index.ts",
"./es/global/index.js",
"./src/global/index.ts"
],
当我们在 vite 中使用时:
import { Button } from 'antd-mobile'
第一步,rollup 会去 _nodemodule/antd-mobile/package.json 中查看 module
或者 main
字段定义的入口文件。
第二步,找到 _nodemodule/antd-mobile/es/index.js 文件:
import './global'; export { setDefaultConfig } from './components/config-provider'; export { default as ActionSheet } from './components/action-sheet'; export { default as AutoCenter } from './components/auto-center'; export { default as Avatar } from './components/avatar'; export { default as Badge } from './components/badge'; export { default as Button } from './components/button'; export { default as Calendar } from './components/calendar'; export { default as CapsuleTabs } from './components/capsule-tabs'; export { default as Card } from './components/card'; export { default as CascadePicker } from './components/cascade-picker'; export { default as CascadePickerView } from './components/cascade-picker-view'; export { default as Cascader } from './components/cascader'; export { default as CascaderView } from './components/cascader-view'; export { default as CheckList } from './components/check-list'; export { default as Checkbox } from './components/checkbox'; export { default as Collapse } from './components/collapse'; export { default as ConfigProvider } from './components/config-provider'; export { default as DatePicker } from './components/date-picker'; export { default as DatePickerView } from './components/date-picker-view'; export { default as Dialog } from './components/dialog'; export { default as Divider } from './components/divider'; export { default as DotLoading } from './components/dot-loading'; export { default as Dropdown } from './components/dropdown'; export { default as Ellipsis } from './components/ellipsis'; export { default as Empty } from './components/empty'; export { default as ErrorBlock } from './components/error-block'; export { default as FloatingBubble } from './components/floating-bubble'; export { default as FloatingPanel } from './components/floating-panel'; export { default as Form } from './components/form'; export { default as Grid } from './components/grid'; export { default as Image } from './components/image'; export { default as ImageUploader } from './components/image-uploader'; export { default as ImageViewer } from './components/image-viewer'; export { default as IndexBar } from './components/index-bar'; export { default as InfiniteScroll } from './components/infinite-scroll'; export { default as Input } from './components/input'; export { default as JumboTabs } from './components/jumbo-tabs'; export { default as List } from './components/list'; export { default as Loading } from './components/loading'; export { default as Mask } from './components/mask'; export { default as Modal } from './components/modal'; export { default as NavBar } from './components/nav-bar'; export { default as NoticeBar } from './components/notice-bar'; export { default as NumberKeyboard } from './components/number-keyboard'; export { default as PageIndicator } from './components/page-indicator'; export { default as PasscodeInput } from './components/passcode-input'; export { default as Picker } from './components/picker'; export { default as PickerView } from './components/picker-view'; export { default as Popover } from './components/popover'; export { default as Popup } from './components/popup'; export { default as ProgressBar } from './components/progress-bar'; export { default as ProgressCircle } from './components/progress-circle'; export { default as PullToRefresh } from './components/pull-to-refresh'; export { default as Radio } from './components/radio'; export { default as Rate } from './components/rate'; export { default as Result } from './components/result'; export { default as SafeArea } from './components/safe-area'; export { default as ScrollMask } from './components/scroll-mask'; export { default as SearchBar } from './components/search-bar'; export { default as Selector } from './components/selector'; export { default as SideBar } from './components/side-bar'; export { default as Skeleton } from './components/skeleton'; export { default as Slider } from './components/slider'; export { default as Space } from './components/space'; export { default as SpinLoading } from './components/spin-loading'; export { default as Stepper } from './components//stepper'; export { default as Steps } from './components/steps'; export { default as SwipeAction } from './components/swipe-action'; export { default as Swiper } from './components/swiper'; export { default as Switch } from './components/switch'; export { default as TabBar } from './components/tab-bar'; export { default as Tabs } from './components/tabs'; export { default as Tag } from './components/tag'; export { default as TextArea } from './components/text-area'; export { default as Toast } from './components/toast'; export { default as TreeSelect } from './components/tree-select'; export { default as VirtualInput } from './components/virtual-input'; export { default as WaterMark } from './components/water-mark';
第三步,处理 index.js 文件中的导入。因为 button
被用到了肯定会加载,而其他组件因为没有用到会被 tree shaking 掉。但是这里存在一个问题:每个组件都引入了样式文件,而 css 类型是被定义成 “有副作用” 的(这个没错)。这就导致组件的 js 文件虽然不会导入,但这个组件所引用的样式会被导入,最后就是整个库的 css 全部被导入了。
众所皆知,vite 的性能由传统的 bundle 处理能力转向了浏览器处理请求的效率:
回到这个问题,解决方式有两个:
import { Button } from 'antd-mobile'
↓ ↓ ↓ ↓ ↓ ↓
import Button from 'antd-mobile/es/components/button'
emm,兜兜转转终于还是回到了原点,就问苍天饶过谁 😘
所以这个问题是可以用 babel-plugin-import
来解决的,而 vite 中也有 vite-plugin-importer 对应封装的插件。
但是我觉得在 rollup 中使用 babel 很不 “vite”,哈哈哈 ~~~