SPA的模式下,宿主页是首先加载的页面,会需要一些主要的组件,如element-plus,easyui,devextreme,ant-design等,这些组件及其依赖组件,文件多,代码量大,可能导致首页加载很慢,超过3-8秒,必须优化。文件多,浏览器与服务器之间的交互次数多,网络来回多,浪费了大量时间,如果能一次打包返回,可以提高效率。
但是SPA,不像MPA,有很多差异:
(1)SPA后续加载的作业组件,对公共组件都是内存共用的。而MPA每个页面可以不考虑共用,单独打包或tree-shaking精简。因此SPA下不能使用webpack打包模式来解决这个问题。
(2)组件的引用方式必须统一。要么大家都是引用其打包文件(如:/lodash/lodash.js),要么大家都引用零散文件(如:/lodash/目录下640个文件)。如果混用,代码在内存就不是一致的,可能引起其逻辑错误。
第三方组件,很多小文件:
- devextreme,devextreme-vue,700+
- lodash-es,640+
- element-plus,360+
- ant-design-vue,530+
在es6模式下,这些第三方组件一般没有提供一个打包后的大文件,一般官方建议分散使用。但它们通常又提供了一个index.mjs文件,在里面一网打尽的引用了全部小文件。如element-plus/es/index.mjs
import installer from './defaults.mjs';
export { default } from './defaults.mjs';
import './components/index.mjs';
import './constants/index.mjs';
import './directives/index.mjs';
import './hooks/index.mjs';
export { makeInstaller } from './make-installer.mjs';
export { default as dayjs } from 'dayjs';
export { affixEmits, affixProps } from './components/affix/src/affix.mjs';
export { ElAffix } from './components/affix/index.mjs';
export { alertEffects, alertEmits, alertProps } from './components/alert/src/alert.mjs';
export { ElAlert } from './components/alert/index.mjs';
export { autocompleteEmits, autocompleteProps } from './components/autocomplete/src/autocomplete.mjs';
export { ElAutocomplete } from './components/autocomplete/index.mjs';
export { avatarEmits, avatarProps } from './components/avatar/src/avatar.mjs';
export { ElAvatar } from './components/avatar/index.mjs';
export { backtopEmits, backtopProps } from './components/backtop/src/backtop.mjs';
export { ElBacktop } from './components/backtop/index.mjs';
export { badgeProps } from './components/badge/src/badge.mjs';
export { ElBadge } from './components/badge/index.mjs';
export { breadcrumbProps } from './components/breadcrumb/src/breadcrumb.mjs';
export { breadcrumbItemProps } from './components/breadcrumb/src/breadcrumb-item.mjs';
export { breadcrumbKey } from './components/breadcrumb/src/constants.mjs';
export { ElBreadcrumb, ElBreadcrumbItem } from './components/breadcrumb/index.mjs';
export { buttonEmits, buttonNativeTypes, buttonProps, buttonTypes } from './components/button/src/button.mjs';
export { buttonGroupContextKey } from './components/button/src/constants.mjs';
export { ElButton, ElButtonGroup } from './components/button/index.mjs';
export { calendarEmits, calendarProps } from './components/calendar/src/calendar.mjs';
export { ElCalendar } from './components/calendar/index.mjs';
export { cardProps } from './components/card/src/card.mjs';
export { ElCard } from './components/card/index.mjs';
export { carouselEmits, carouselProps } from './components/carousel/src/carousel.mjs';
export { carouselItemProps } from './components/carousel/src/carousel-item.mjs';
export { CAROUSEL_ITEM_NAME, carouselContextKey } from './components/carousel/src/constants.mjs';
export { ElCarousel, ElCarouselItem } from './components/carousel/index.mjs';
export { cascaderEmits, cascaderProps } from './components/cascader/src/cascader.mjs';
export { ElCascader } from './components/cascader/index.mjs';
export { CASCADER_PANEL_INJECTION_KEY } from './components/cascader-panel/src/types.mjs';
export { CommonProps, DefaultProps, useCascaderConfig } from './components/cascader-panel/src/config.mjs';
export { ElCascaderPanel } from './components/cascader-panel/index.mjs';
export { checkTagEmits, checkTagProps } from './components/check-tag/src/check-tag.mjs';
export { ElCheckTag } from './components/check-tag/index.mjs';
export { checkboxGroupEmits, checkboxGroupProps } from './components/checkbox/src/checkbox-group.mjs';
export { checkboxEmits, checkboxProps } from './components/checkbox/src/checkbox.mjs';
export { checkboxGroupContextKey } from './components/checkbox/src/constants.mjs';
export { ElCheckbox, ElCheckboxButton, ElCheckboxGroup } from './components/checkbox/index.mjs';
export { colProps } from './components/col/src/col.mjs';
export { ElCol } from './components/col/index.mjs';
export { collapseEmits, collapseProps, emitChangeFn } from './components/collapse/src/collapse.mjs';
export { collapseItemProps } from './components/collapse/src/collapse-item.mjs';
export { collapseContextKey } from './components/collapse/src/constants.mjs';
export { ElCollapse, ElCollapseItem } from './components/collapse/index.mjs';
export { ElCollapseTransition } from './components/collapse-transition/index.mjs';
export { colorPickerContextKey, colorPickerEmits, colorPickerProps } from './components/color-picker/src/color-picker.mjs';
export { ElColorPicker } from './components/color-picker/index.mjs';
export { messageConfig } from './components/config-provider/src/config-provider.mjs';
export { configProviderProps } from './components/config-provider/src/config-provider-props.mjs';
export { configProviderContextKey } from './components/config-provider/src/constants.mjs';
export { provideGlobalConfig, useGlobalComponentSettings, useGlobalConfig } from './components/config-provider/src/hooks/use-global-config.mjs';
export { ElConfigProvider } from './components/config-provider/index.mjs';
export { ElAside, ElContainer, ElFooter, ElHeader, ElMain } from './components/container/index.mjs';
export { countdownEmits, countdownProps } from './components/countdown/src/countdown.mjs';
export { ElCountdown } from './components/countdown/index.mjs';
export { ROOT_PICKER_INJECTION_KEY } from './components/date-picker/src/constants.mjs';
export { datePickerProps } from './components/date-picker/src/props/date-picker.mjs';
export { ElDatePicker } from './components/date-picker/index.mjs';
export { descriptionProps } from './components/descriptions/src/description.mjs';
export { descriptionItemProps } from './components/descriptions/src/description-item.mjs';
export { ElDescriptions, ElDescriptionsItem } from './components/descriptions/index.mjs';
export { useDialog } from './components/dialog/src/use-dialog.mjs';
export { dialogEmits, dialogProps } from './components/dialog/src/dialog.mjs';
export { dialogInjectionKey } from './components/dialog/src/constants.mjs';
export { ElDialog } from './components/dialog/index.mjs';
export { dividerProps } from './components/divider/src/divider.mjs';
export { ElDivider } from './components/divider/index.mjs';
export { drawerEmits, drawerProps } from './components/drawer/src/drawer.mjs';
export { ElDrawer } from './components/drawer/index.mjs';
export { DROPDOWN_COLLECTION_INJECTION_KEY, DROPDOWN_COLLECTION_ITEM_INJECTION_KEY, ElCollection, ElCollectionItem, FIRST_KEYS, FIRST_LAST_KEYS, LAST_KEYS, dropdownItemProps, dropdownMenuProps, dropdownProps } from './components/dropdown/src/dropdown.mjs';
export { DROPDOWN_INJECTION_KEY } from './components/dropdown/src/tokens.mjs';
export { ElDropdown, ElDropdownItem, ElDropdownMenu } from './components/dropdown/index.mjs';
export { emptyProps } from './components/empty/src/empty.mjs';
export { ElEmpty } from './components/empty/index.mjs';
export { formEmits, formMetaProps, formProps } from './components/form/src/form.mjs';
export { formItemProps, formItemValidateStates } from './components/form/src/form-item.mjs';
export { formContextKey, formItemContextKey } from './components/form/src/constants.mjs';
export { useDisabled, useFormDisabled, useFormSize, useSize } from './components/form/src/hooks/use-form-common-props.mjs';
export { useFormItem, useFormItemInputId } from './components/form/src/hooks/use-form-item.mjs';
export { ElForm, ElFormItem } from './components/form/index.mjs';
export { iconProps } from './components/icon/src/icon.mjs';
export { ElIcon } from './components/icon/index.mjs';
export { imageEmits, imageProps } from './components/image/src/image.mjs';
export { ElImage } from './components/image/index.mjs';
export { imageViewerEmits, imageViewerProps } from './components/image-viewer/src/image-viewer.mjs';
export { ElImageViewer } from './components/image-viewer/index.mjs';
export { inputEmits, inputProps } from './components/input/src/input.mjs';
export { ElInput } from './components/input/index.mjs';
export { inputNumberEmits, inputNumberProps } from './components/input-number/src/input-number.mjs';
export { ElInputNumber } from './components/input-number/index.mjs';
export { linkEmits, linkProps } from './components/link/src/link.mjs';
export { ElLink } from './components/link/index.mjs';
export { menuEmits, menuProps } from './components/menu/src/menu.mjs';
export { menuItemEmits, menuItemProps } from './components/menu/src/menu-item.mjs';
export { menuItemGroupProps } from './components/menu/src/menu-item-group.mjs';
export { subMenuProps } from './components/menu/src/sub-menu.mjs';
export { ElMenu, ElMenuItem, ElMenuItemGroup, ElSubMenu } from './components/menu/index.mjs';
export { overlayEmits, overlayProps } from './components/overlay/src/overlay.mjs';
export { ElOverlay } from './components/overlay/index.mjs';
export { pageHeaderEmits, pageHeaderProps } from './components/page-header/src/page-header.mjs';
export { ElPageHeader } from './components/page-header/index.mjs';
export { paginationEmits, paginationProps } from './components/pagination/src/pagination.mjs';
export { elPaginationKey } from './components/pagination/src/constants.mjs';
export { ElPagination } from './components/pagination/index.mjs';
export { popconfirmEmits, popconfirmProps } from './components/popconfirm/src/popconfirm.mjs';
export { ElPopconfirm } from './components/popconfirm/index.mjs';
export { Effect, popperProps, roleTypes, usePopperProps } from './components/popper/src/popper.mjs';
export { popperTriggerProps, usePopperTriggerProps } from './components/popper/src/trigger.mjs';
export { popperContentEmits, popperContentProps, popperCoreConfigProps, usePopperContentEmits, usePopperContentProps, usePopperCoreConfigProps } from './components/popper/src/content.mjs';
export { popperArrowProps, usePopperArrowProps } from './components/popper/src/arrow.mjs';
export { POPPER_CONTENT_INJECTION_KEY, POPPER_INJECTION_KEY } from './components/popper/src/constants.mjs';
export { default as ElPopperArrow } from './components/popper/src/arrow2.mjs';
export { default as ElPopperTrigger } from './components/popper/src/trigger2.mjs';
export { default as ElPopperContent } from './components/popper/src/content2.mjs';
export { ElPopper } from './components/popper/index.mjs';
export { progressProps } from './components/progress/src/progress.mjs';
export { ElProgress } from './components/progress/index.mjs';
export { radioEmits, radioProps, radioPropsBase } from './components/radio/src/radio.mjs';
export { radioGroupEmits, radioGroupProps } from './components/radio/src/radio-group.mjs';
export { radioButtonProps } from './components/radio/src/radio-button.mjs';
export { radioGroupKey } from './components/radio/src/constants.mjs';
export { ElRadio, ElRadioButton, ElRadioGroup } from './components/radio/index.mjs';
export { rateEmits, rateProps } from './components/rate/src/rate.mjs';
export { ElRate } from './components/rate/index.mjs';
export { IconComponentMap, IconMap, resultProps } from './components/result/src/result.mjs';
export { ElResult } from './components/result/index.mjs';
export { RowAlign, RowJustify, rowProps } from './components/row/src/row.mjs';
export { rowContextKey } from './components/row/src/constants.mjs';
export { ElRow } from './components/row/index.mjs';
export { BAR_MAP, GAP, renderThumbStyle } from './components/scrollbar/src/util.mjs';
export { scrollbarEmits, scrollbarProps } from './components/scrollbar/src/scrollbar.mjs';
export { thumbProps } from './components/scrollbar/src/thumb.mjs';
export { scrollbarContextKey } from './components/scrollbar/src/constants.mjs';
export { ElScrollbar } from './components/scrollbar/index.mjs';
export { selectGroupKey, selectKey } from './components/select/src/token.mjs';
export { ElOption, ElOptionGroup, ElSelect } from './components/select/index.mjs';
export { selectV2InjectionKey } from './components/select-v2/src/token.mjs';
export { ElSelectV2 } from './components/select-v2/index.mjs';
export { skeletonProps } from './components/skeleton/src/skeleton.mjs';
export { skeletonItemProps } from './components/skeleton/src/skeleton-item.mjs';
export { ElSkeleton, ElSkeletonItem } from './components/skeleton/index.mjs';
export { sliderEmits, sliderProps } from './components/slider/src/slider.mjs';
export { sliderContextKey } from './components/slider/src/constants.mjs';
export { ElSlider } from './components/slider/index.mjs';
export { spaceProps } from './components/space/src/space.mjs';
export { spaceItemProps } from './components/space/src/item.mjs';
export { useSpace } from './components/space/src/use-space.mjs';
export { ElSpace } from './components/space/index.mjs';
export { statisticProps } from './components/statistic/src/statistic.mjs';
export { ElStatistic } from './components/statistic/index.mjs';
export { stepProps } from './components/steps/src/item.mjs';
export { stepsEmits, stepsProps } from './components/steps/src/steps.mjs';
export { ElStep, ElSteps } from './components/steps/index.mjs';
export { switchEmits, switchProps } from './components/switch/src/switch.mjs';
export { ElSwitch } from './components/switch/index.mjs';
export { ElTable, ElTableColumn } from './components/table/index.mjs';
export { Alignment as TableV2Alignment, FixedDir as TableV2FixedDir, SortOrder as TableV2SortOrder } from './components/table-v2/src/constants.mjs';
export { default as TableV2 } from './components/table-v2/src/table-v2.mjs';
export { placeholderSign as TableV2Placeholder } from './components/table-v2/src/private.mjs';
export { autoResizerProps } from './components/table-v2/src/auto-resizer.mjs';
export { tableV2Props } from './components/table-v2/src/table.mjs';
export { tableV2RowProps } from './components/table-v2/src/row.mjs';
export { ElAutoResizer, ElTableV2 } from './components/table-v2/index.mjs';
export { tabsEmits, tabsProps } from './components/tabs/src/tabs.mjs';
export { tabBarProps } from './components/tabs/src/tab-bar.mjs';
export { tabNavEmits, tabNavProps } from './components/tabs/src/tab-nav.mjs';
export { tabPaneProps } from './components/tabs/src/tab-pane.mjs';
export { tabsRootContextKey } from './components/tabs/src/constants.mjs';
export { ElTabPane, ElTabs } from './components/tabs/index.mjs';
export { tagEmits, tagProps } from './components/tag/src/tag.mjs';
export { ElTag } from './components/tag/index.mjs';
export { textProps } from './components/text/src/text.mjs';
export { ElText } from './components/text/index.mjs';
export { buildTimeList, dateEquals, extractDateFormat, extractTimeFormat, formatter, makeList, parseDate, rangeArr, valueEquals } from './components/time-picker/src/utils.mjs';
export { DEFAULT_FORMATS_DATE, DEFAULT_FORMATS_DATEPICKER, DEFAULT_FORMATS_TIME, timeUnits } from './components/time-picker/src/constants.mjs';
export { timePickerDefaultProps } from './components/time-picker/src/common/props.mjs';
export { ElTimePicker } from './components/time-picker/index.mjs';
export { default as CommonPicker } from './components/time-picker/src/common/picker.mjs';
export { default as TimePickPanel } from './components/time-picker/src/time-picker-com/panel-time-pick.mjs';
export { timeSelectProps } from './components/time-select/src/time-select.mjs';
export { ElTimeSelect } from './components/time-select/index.mjs';
export { timelineItemProps } from './components/timeline/src/timeline-item.mjs';
export { ElTimeline, ElTimelineItem } from './components/timeline/index.mjs';
export { tooltipEmits, useTooltipModelToggle, useTooltipModelToggleEmits, useTooltipModelToggleProps, useTooltipProps } from './components/tooltip/src/tooltip.mjs';
export { useTooltipTriggerProps } from './components/tooltip/src/trigger.mjs';
export { useTooltipContentProps } from './components/tooltip/src/content.mjs';
export { TOOLTIP_INJECTION_KEY } from './components/tooltip/src/constants.mjs';
export { ElTooltip } from './components/tooltip/index.mjs';
export { LEFT_CHECK_CHANGE_EVENT, RIGHT_CHECK_CHANGE_EVENT, transferCheckedChangeFn, transferEmits, transferProps } from './components/transfer/src/transfer.mjs';
export { ElTransfer } from './components/transfer/index.mjs';
export { ElTree } from './components/tree/index.mjs';
export { ElTreeSelect } from './components/tree-select/index.mjs';
export { ElTreeV2 } from './components/tree-v2/index.mjs';
export { genFileId, uploadBaseProps, uploadListTypes, uploadProps } from './components/upload/src/upload.mjs';
export { uploadContentProps } from './components/upload/src/upload-content.mjs';
export { uploadListEmits, uploadListProps } from './components/upload/src/upload-list.mjs';
export { uploadDraggerEmits, uploadDraggerProps } from './components/upload/src/upload-dragger.mjs';
export { uploadContextKey } from './components/upload/src/constants.mjs';
export { ElUpload } from './components/upload/index.mjs';
export { default as FixedSizeList } from './components/virtual-list/src/components/fixed-size-list.mjs';
export { default as DynamicSizeList } from './components/virtual-list/src/components/dynamic-size-list.mjs';
export { default as FixedSizeGrid } from './components/virtual-list/src/components/fixed-size-grid.mjs';
export { default as DynamicSizeGrid } from './components/virtual-list/src/components/dynamic-size-grid.mjs';
export { virtualizedGridProps, virtualizedListProps, virtualizedProps, virtualizedScrollbarProps } from './components/virtual-list/src/props.mjs';
export { watermarkProps } from './components/watermark/src/watermark.mjs';
export { ElWatermark } from './components/watermark/index.mjs';
export { tourEmits, tourProps } from './components/tour/src/tour.mjs';
export { tourStepEmits, tourStepProps } from './components/tour/src/step.mjs';
export { tourContentEmits, tourContentProps, tourPlacements, tourStrategies } from './components/tour/src/content.mjs';
export { ElTour, ElTourStep } from './components/tour/index.mjs';
export { anchorEmits, anchorProps } from './components/anchor/src/anchor.mjs';
export { ElAnchor, ElAnchorLink } from './components/anchor/index.mjs';
export { segmentedEmits, segmentedProps } from './components/segmented/src/segmented.mjs';
export { ElSegmented } from './components/segmented/index.mjs';
export { mentionEmits, mentionProps } from './components/mention/src/mention.mjs';
export { ElMention } from './components/mention/index.mjs';
export { ElInfiniteScroll } from './components/infinite-scroll/index.mjs';
export { ElLoading } from './components/loading/index.mjs';
export { vLoading as ElLoadingDirective, vLoading } from './components/loading/src/directive.mjs';
export { Loading as ElLoadingService } from './components/loading/src/service.mjs';
export { messageDefaults, messageEmits, messageProps, messageTypes } from './components/message/src/message.mjs';
export { ElMessage } from './components/message/index.mjs';
export { ElMessageBox } from './components/message-box/index.mjs';
export { notificationEmits, notificationProps, notificationTypes } from './components/notification/src/notification.mjs';
export { ElNotification } from './components/notification/index.mjs';
export { popoverEmits, popoverProps } from './components/popover/src/popover.mjs';
export { ElPopover, ElPopoverDirective } from './components/popover/index.mjs';
export { EVENT_CODE } from './constants/aria.mjs';
export { WEEK_DAYS, datePickTypes } from './constants/date.mjs';
export { CHANGE_EVENT, INPUT_EVENT, UPDATE_MODEL_EVENT } from './constants/event.mjs';
export { INSTALLED_KEY } from './constants/key.mjs';
export { componentSizeMap, componentSizes } from './constants/size.mjs';
export { default as ClickOutside } from './directives/click-outside/index.mjs';
export { vRepeatClick } from './directives/repeat-click/index.mjs';
export { default as TrapFocus } from './directives/trap-focus/index.mjs';
export { default as Mousewheel } from './directives/mousewheel/index.mjs';
export { useAttrs } from './hooks/use-attrs/index.mjs';
export { useDeprecated } from './hooks/use-deprecated/index.mjs';
export { useDraggable } from './hooks/use-draggable/index.mjs';
export { useFocus } from './hooks/use-focus/index.mjs';
export { buildLocaleContext, buildTranslator, localeContextKey, translate, useLocale } from './hooks/use-locale/index.mjs';
export { useLockscreen } from './hooks/use-lockscreen/index.mjs';
export { useModal } from './hooks/use-modal/index.mjs';
export { createModelToggleComposable, useModelToggle, useModelToggleEmits, useModelToggleProps } from './hooks/use-model-toggle/index.mjs';
export { usePreventGlobal } from './hooks/use-prevent-global/index.mjs';
export { useProp } from './hooks/use-prop/index.mjs';
export { usePopper } from './hooks/use-popper/index.mjs';
export { useSameTarget } from './hooks/use-same-target/index.mjs';
export { useTeleport } from './hooks/use-teleport/index.mjs';
export { useThrottleRender } from './hooks/use-throttle-render/index.mjs';
export { useTimeout } from './hooks/use-timeout/index.mjs';
export { useTransitionFallthrough, useTransitionFallthroughEmits } from './hooks/use-transition-fallthrough/index.mjs';
export { ID_INJECTION_KEY, useId, useIdInjection } from './hooks/use-id/index.mjs';
export { useEscapeKeydown } from './hooks/use-escape-keydown/index.mjs';
export { usePopperContainer, usePopperContainerId } from './hooks/use-popper-container/index.mjs';
export { useDelayedRender } from './hooks/use-intermediate-render/index.mjs';
export { useDelayedToggle, useDelayedToggleProps } from './hooks/use-delayed-toggle/index.mjs';
export { FORWARD_REF_INJECTION_KEY, useForwardRef, useForwardRefDirective } from './hooks/use-forward-ref/index.mjs';
export { defaultNamespace, namespaceContextKey, useGetDerivedNamespace, useNamespace } from './hooks/use-namespace/index.mjs';
export { ZINDEX_INJECTION_KEY, defaultInitialZIndex, useZIndex, zIndexContextKey } from './hooks/use-z-index/index.mjs';
export { arrowMiddleware, getPositionDataWithUnit, useFloating, useFloatingProps } from './hooks/use-floating/index.mjs';
export { useCursor } from './hooks/use-cursor/index.mjs';
export { useOrderedChildren } from './hooks/use-ordered-children/index.mjs';
export { SIZE_INJECTION_KEY, useGlobalSize, useSizeProp, useSizeProps } from './hooks/use-size/index.mjs';
export { useFocusController } from './hooks/use-focus-controller/index.mjs';
export { useComposition } from './hooks/use-composition/index.mjs';
export { DEFAULT_EMPTY_VALUES, DEFAULT_VALUE_ON_CLEAR, SCOPE, emptyValuesContextKey, useEmptyValues, useEmptyValuesProps } from './hooks/use-empty-values/index.mjs';
export { ariaProps, useAriaProps } from './hooks/use-aria/index.mjs';
const install = installer.install;
const version = installer.version;
export { install, version };
//# sourceMappingURL=index.mjs.map
注意,这个index.mjs并不是打包后文件。因此这时引用混用是没有问题的。
import {ElDatePicker} from 'element-plus';
import {ElDatePicker} from 'element-plus/components/date-picker';
组件打包后,前端如何使用?关键是要能把一个个文件代码再拆开,否则无法兼容分散引用。因此,不能用webpack的模式打包,因为无法拆开。只能自己建立一个打包格式,如:
{
modules:[
{
path:'/npm/element-plus/es/components/date-picker
code:......
},
{
path:'/npm/element-plus/es/components/card
code:......
},
]
}
浏览器前端拿到这个打包结果后,需要一一拆开。在es5模式下,因为require函数是你自己实现的,你的require函数可以支持内联加载(inline module import),即直接加载code而不是url,不赘述。
但是es6下,import和import()是浏览器内置的,你无法修改。特别的,nodejs的import有实现这种模式:Modules: ECMAScript modules | Node.js v24.0.2 Documentation
那如何办呢?
(1)等待新的es标准,各大浏览器支持内联加载(inline module import)
(2)在service worker中,拦截fetch
本文主要讲service worker中如何处理打包拆包。
拆包后代码可以放在浏览器cache中,也可以放在内存中。放在cache中比较慢,好处是浏览器关闭后,下次打开可以直接用。
如:拆到内存中:
/**
* 把小模组存储到内存中
*/
const memoryCache={};
const cacheHeaders={};
let foil={
onActivate(){
return clients.claim();
},
getModule(req,cb){
let u=new URL(req.url);
let url=u.pathname+u.search;
// console.log(prefix,url);
let m=memoryCache[url];
let res;
if (m){
res=new Response(m.code,{
status:m.status,
headers:{
'Content-Type': 'application/javascript',
'Location':m.location,
}
});
}
cb(null,res);
},
async parseBundleResult(bundleObj,cacheHeaders){
//console.log(prefix,''bundle result:',bundleObj);
for(let i=0;i<bundleObj.modules.length;i++){
let m=bundleObj.modules[i];
//console.log(prefix,m.path,m.location);
/**
* 作业打包时,可能只返回了依赖列表而没有代码,没有代码就不加入cache中。
*/
if (!m.code) continue;
if (m.location){
let originDir=m.path.slice(0, m.path.lastIndexOf("/") + 1);
let realDir=m.location.slice(0, m.location.lastIndexOf("/") + 1);
if (originDir!=realDir){
/**
* 重定向的路径变了,会影响相对路径的import/require加载,
* 增加一个单独的路由。
*/
memoryCache[m.location]={status:200,code:m.code};
memoryCache[m.path]={status:301,location:m.location};
}
else memoryCache[m.path]={status:200,code:m.code};
}
else memoryCache[m.path]={status:200,code:m.code};
cacheHeaders[m.path]=m.cacheHeaders['last-modified'];
if (m.location) cacheHeaders[m.location]=m.cacheHeaders['last-modified'];
}
},
getCacheHeaders(){
return cacheHeaders;
}
}
如:拆到cache中:
/**
* 把小模组存储到浏览器缓存中
*/
const CACHE_NAME = "foil-spa-vue-v1";
const key_cacheHeaders='/_cache_meta_info';
function module2Response(m){
let res1,res2;
let status=200;
let code=m.code;
let headers={
'Content-Type': 'application/javascript',
'Content-Length':new Blob([m.code]).size.toString(),
}
if (m.location){
//console.log(prefix,m.path,m.location);
let originDir=m.path.slice(0, m.path.lastIndexOf("/") + 1);
let realDir=m.location.slice(0, m.location.lastIndexOf("/") + 1);
if (originDir!=realDir){
/**
* 重定向的路径变了,会影响相对路径的import/require加载,
* 增加一个单独的路由。
*/
res2={
path:m.location,
res:new Response(m.code,{status:200,headers})
};
status=301;
code=undefined;
headers={
'Content-Type': 'application/javascript',
'Location':m.location,
'Content-Length':0
}
}
}
res1={
path:m.path,
res:new Response(code,{status,headers})
};
return [res1,res2];
}
let foil={
onActivate(){
let t1=new Date().getTime();
let task=new Promise(async function(resolve,reject){
try{
let cacheNames=await caches.keys();
await cacheNames
.filter(function (cacheName) {
// 过滤掉不需要的缓存
return (
cacheName.startsWith("foil-spa-vue-") && cacheName !== CACHE_NAME
);
})
.map(function (cacheName) {
// 删除旧的缓存
return caches.delete(cacheName);
});
// 立即接管所有客户端
await clients.claim();
console.log(prefix,"activate clear cache,",new Date().getTime()-t1,'ms');
resolve();
}
catch(err){
reject(err);
}
});
return task;
},
getModule(req,cb){
caches.match(req).then(function (res) {
cb(null,res);
return res;
},cb);
},
async parseBundleResult(bundleObj,cacheHeaders){
let cache=await caches.open(CACHE_NAME);
let tasks=[];
for(let i=0;i<bundleObj.modules.length;i++){
let m=bundleObj.modules[i];
if (!m.code) continue;
let [res1,res2]=module2Response(m);
tasks.push(cache.put(res1.path,res1.res));
if (res2) tasks.push(cache.put(res2.path,res2.res));
cacheHeaders[m.path]=m.cacheHeaders['last-modified'];
if (m.location) cacheHeaders[m.location]=m.cacheHeaders['last-modified'];
}
let cacheHeadersJSON=JSON.stringify(cacheHeaders);
tasks.push(cache.put(key_cacheHeaders,new Response(cacheHeadersJSON,{
headers:{
'Content-Length':new Blob([cacheHeadersJSON]).size.toString()
}
})));
await Promise.all(tasks);
},
async getCacheHeaders(){
let cacheHeaders={};
let cache=await caches.open(CACHE_NAME);
let cacheRes=await cache.match(key_cacheHeaders);
if (cacheRes){
cacheHeaders=await cacheRes.json();
}
return cacheHeaders;
}
}
在service worker中拦截fetch调用,注意要处理post的body。
self.addEventListener("fetch", function (event) {
//console.log(prefix,'fetch',event.request.url);
let task = new Promise(function (resolve, reject) {
foil.getModule(event.request, function (err, res) {
if (err) {
reject(err);
} else if (res) resolve(res);
else {
/**
* 看到event.request.destination是'script',但是后台收到的req.headers['sec-fetch-dest']是'empty'
* 这导致es6下,后台对/npm/react/umd/react.development.js这个es5文件不能转码。
* 增加'sec-fetch-dest'无效,只能增加'tm-sec-fetch-dest'。
* es6下为了处理import ".";这种非法语句,需要把原始referer传递到后台。
* 注意:req.referrer单词与referer不同,多一个r字母。
*
* Request 对象的 headers 是只读的,没办法直接进行修改。当你使用 req.headers.set() 时,浏览器会报错,提示 Headers are immutable(头部不可变)。
*/
let req = event.request;
let bodyPromise;
if (req.method === "POST" || req.method === "PUT") {
const contentType = req.headers.get("Content-Type");
if (contentType && contentType.includes("application/json")) {
bodyPromise = req.json().then(function(json){return JSON.stringify(json);});
} else if (contentType && contentType.includes("form")) {
bodyPromise = req.formData();
} else {
bodyPromise = req.text();
}
} else {
bodyPromise = Promise.resolve(null);
}
bodyPromise.then(function (body) {
const newHeaders = {
"tm-referer": req.referrer,
"tm-sec-fetch-dest": req.destination,
};
for (let [key, value] of req.headers.entries()) {
newHeaders[key] = value;
}
let mode = req.mode;
if (mode === "navigate") {
/**
* 修正 mode 属性,避免使用 'navigate'
* 对于导航请求,降级为 cors 或 same-origin
* 否则报错:Uncaught (in promise) TypeError: Failed to construct 'Request': Cannot construct a Request with a RequestInit whose mode member is set as 'navigate'.
*/
mode = req.url.startsWith(self.origin) ? "same-origin" : "cors";
}
req = new Request(req, {
method: req.method,
headers: newHeaders,
body: body,
mode: mode,
credentials: req.credentials,
cache: req.cache,
redirect: req.redirect,
referrer: req.referrer,
});
res = fetch(req).then(
function (data) {
return data;
},
function (err) {
/**增加then后,不在浏览器显示重复的错误信息*/
}
);
resolve(res);
return body;
}, reject);
}
});
});
event.respondWith(task);
});
注意:
(1)每次后台打包,可能要花2-3秒,最好把打包结果保存到文件,下次直接读取。或者服务器启动时,预热打包一次。
(2)前端如何更新包内个别文件。前端记录每个文件的last-modified,打包时全部传到后台,文件多时可能有几十K。
(3)service worker的使用限制,必须是https有合法ca证书,且不能是https://localhost。
如此,SPA宿主页加载速度可以提高40-50%。