年前开始负责新项目开发,是一个h5内嵌到企业微信。技术栈是 vite2.x + vue3.x。随着业务的开展,版本迭代,页面越来越多,第三方依赖也越来越多,打出来的包也越来越大。针对这个问题,很容易就会想到分包这个解决方案。根据 vite 官方文档 提示,做了 vendor 分包之外,还对路由引用的组件做了异步加载处理,也会产生独立分包。这种配置在某个阶段是没问题的。
vite 配置文件,通过 build.rollupOptions.output.manualChunks 配合手动分包策略之后,vite 不会自动生成 vendor包chunk碎片,如几个页面公用的文件 api.js sdkUtils.js http.js 等,这些独立的分包大小都很小,加起来 gzip 之后都不到 1kb,增加了网络请求。经过阅读源码,以及官方文档,分析了vite和rollup的分包策略,最后得出这个解决方案:
rollupOptions: {
output: {
manualChunks(id: any, { getModuleInfo }) {
const cssLangs = `\\.(css|less|sass|scss|styl|stylus|pcss|postcss)($|\\?)`;
const cssLangRE = new RegExp(cssLangs);
const isCSSRequest = (request: string): boolean =>
cssLangRE.test(request);
// 分vendor包
if (
id.includes('node_modules') &&
!isCSSRequest(id) &&
staticImportedByEntry(id, getModuleInfo, cache.cache)
) {
return 'vendor';
} else if (
// 分manifest包,解决chunk碎片问题
((getModuleInfo(id).importers.length + getModuleInfo(id).dynamicImporters.length) > 1) &&
id.includes('src')
) {
return 'manifest';
}
}
}
}
export class SplitVendorChunkCache {
constructor() {
this.cache = new Map();
}
reset() {
this.cache = new Map();
}
}
export function staticImportedByEntry(
id,
getModuleInfo,
cache,
importStack = []
) {
if (cache.has(id)) {
return !!cache.get(id);
}
if (importStack.includes(id)) {
cache.set(id, false);
return false;
}
const mod = getModuleInfo(id);
if (!mod) {
cache.set(id, false);
return false;
}
if (mod.isEntry) {
cache.set(id, true);
return true;
}
const someImporterIs = mod.importers.some((importer) =>
staticImportedByEntry(
importer,
getModuleInfo,
cache,
importStack.concat(id)
)
);
cache.set(id, someImporterIs);
return someImporterIs;
}
下面来看看当时是如何分析,以及一步一步来揭开默认分包策略的神秘面纱。
vite2.x 什么情况下可以触发 vendor 包的生成?经过测试,在 vite 配置文件,通过 build.rollupOptions.output.manualChunks 配合手动分包策略之后,不会自动生成vendor包。想要知道更清晰 vite 在什么情况会分 vendor 包,什么时候不会分 vendor 包,需要打开源码看清楚。
// vite
// packages\vite\src\node\plugins\splitVendorChunk.ts
return {
name: 'vite:split-vendor-chunk',
config(config) {
let outputs = config?.build?.rollupOptions?.output
if (outputs) {
outputs = Array.isArray(outputs) ? outputs : [outputs]
for (const output of outputs) {
const viteManualChunks = createSplitVendorChunk(output, config)
if (viteManualChunks) {
if (output.manualChunks) {
if (typeof output.manualChunks === 'function') {
const userManualChunks = output.manualChunks
output.manualChunks = (id: string, api: GetManualChunkApi) => {
return userManualChunks(id, api) ?? viteManualChunks(id, api)
}
}
// else, leave the object form of manualChunks untouched, as
// we can't safely replicate rollup handling.
} else {
output.manualChunks = viteManualChunks
}
}
}
} else {
return {
build: {
rollupOptions: {
output: {
manualChunks: createSplitVendorChunk({}, config)
}
}
}
}
}
},
buildStart() {
caches.forEach((cache) => cache.reset())
}
}
build.rollupOptions.output,使用插件之后就可以分出 vendor 包build.rollupOptions.output,但没有配置 manualChunks,使用插件之后就可以分出 vendor 包build.rollupOptions.output,且有配置 manualChunks,就会以手动分包配置为准,不会生成 vendor 包vendor 包分包的策略是:模块id名是包含 'node_modules' 的,表示该模块是在node_modules下的,且这个模块不是 css 模块,且这个模块是被静态入口点模块(单页应用的index.html,多页应用下可以有多个)导入的小结:用户配置了手动分包,就会忽略 vite 提供的 vendor 分包逻辑。
那如果希望在手动分包的基础上还需要 vendor 分包,那么就需要把 vendor 分包的逻辑抄过去就可以了。
备注:
vite2.x 在 2.8 版本之后把默认分 vendor 包的逻辑取消了,改为了插件式使用。vite2.x 底层是通过 rollup 打包的。rollup 默认分包策略的 chunk碎片 是如何产生的?为什么会产生 chunk碎片?参考对 webpack 分包的理解,除了入口点(静态入口点、动态入口点)单独生成一个chunk之外,当一个模块被两个或以上的 chunk 引用,这个模块需要单独生成一个 chunk。
下面从源码的角度看看 rollup 是如何生成这些 chunk碎片的。
// rollup
// src\Bundle.ts
private async generateChunks(): Promise<Chunk[]> {
const { manualChunks } = this.outputOptions;
const manualChunkAliasByEntry =
typeof manualChunks === 'object'
? await this.addManualChunks(manualChunks)
: this.assignManualChunks(manualChunks);
const chunks: Chunk[] = [];
const chunkByModule = new Map<Module, Chunk>();
for (const { alias, modules } of this.outputOptions.inlineDynamicImports
? [{ alias: null, modules: getIncludedModules(this.graph.modulesById) }]
: this.outputOptions.preserveModules
? getIncludedModules(this.graph.modulesById).map(module => ({
alias: null,
modules: [module]
}))
: getChunkAssignments(this.graph.entryModules, manualChunkAliasByEntry)) {
sortByExecutionOrder(modules);
const chunk = new Chunk(
modules,
this.inputOptions,
this.outputOptions,
this.unsetOptions,
this.pluginDriver,
this.graph.modulesById,
chunkByModule,
this.facadeChunkByModule,
this.includedNamespaces,
alias
);
chunks.push(chunk);
for (const module of modules) {
chunkByModule.set(module, chunk);
}
}
for (const chunk of chunks) {
chunk.link();
}
const facades: Chunk[] = [];
for (const chunk of chunks) {
facades.push(...chunk.generateFacades());
}
return [...chunks, ...facades];
}
manualChunkAliasByEntry(map数据类型,key 为当前模块的模块信息,是一个对象数据类型,value 为该模块的入口,是一个string,也就是手动分包的名字,如最终答案中的 vendor 、manifest)inlineDynamicImports 和 preserveModules 而改变,这次不进行分析。默认是会通过 getChunkAssignments 返回的 chunk定义数据,然后生成chunk。下面来看看 getChunkAssignments 做了什么。
function getChunkAssignments(entryModules, manualChunkAliasByEntry) {
const chunkDefinitions = [];
debugger;
const modulesInManualChunks = new Set(manualChunkAliasByEntry.keys());
const manualChunkModulesByAlias = Object.create(null);
for (const [entry, alias] of manualChunkAliasByEntry) {
const chunkModules = (manualChunkModulesByAlias[alias] =
manualChunkModulesByAlias[alias] || []);
addStaticDependenciesToManualChunk(entry, chunkModules, modulesInManualChunks);
}
for (const [alias, modules] of Object.entries(manualChunkModulesByAlias)) {
chunkDefinitions.push({ alias, modules });
}
const assignedEntryPointsByModule = new Map();
const { dependentEntryPointsByModule, dynamicEntryModules } = analyzeModuleGraph(entryModules);
const dynamicallyDependentEntryPointsByDynamicEntry = getDynamicDependentEntryPoints(dependentEntryPointsByModule, dynamicEntryModules);
const staticEntries = new Set(entryModules);
function assignEntryToStaticDependencies(entry, dynamicDependentEntryPoints) {
const modulesToHandle = new Set([entry]);
for (const module of modulesToHandle) {
const assignedEntryPoints = getOrCreate(assignedEntryPointsByModule, module, () => new Set());
if (dynamicDependentEntryPoints &&
areEntryPointsContainedOrDynamicallyDependent(dynamicDependentEntryPoints, dependentEntryPointsByModule.get(module))) {
continue;
}
else {
assignedEntryPoints.add(entry);
}
for (const dependency of module.getDependenciesToBeIncluded()) {
if (!(dependency instanceof ExternalModule || modulesInManualChunks.has(dependency))) {
modulesToHandle.add(dependency);
}
}
}
}
function areEntryPointsContainedOrDynamicallyDependent(entryPoints, containedIn) {
const entriesToCheck = new Set(entryPoints);
for (const entry of entriesToCheck) {
if (!containedIn.has(entry)) {
if (staticEntries.has(entry))
return false;
const dynamicallyDependentEntryPoints = dynamicallyDependentEntryPointsByDynamicEntry.get(entry);
for (const dependentEntry of dynamicallyDependentEntryPoints) {
entriesToCheck.add(dependentEntry);
}
}
}
return true;
}
for (const entry of entryModules) {
if (!modulesInManualChunks.has(entry)) {
assignEntryToStaticDependencies(entry, null);
}
}
for (const entry of dynamicEntryModules) {
if (!modulesInManualChunks.has(entry)) {
assignEntryToStaticDependencies(entry, dynamicallyDependentEntryPointsByDynamicEntry.get(entry));
}
}
chunkDefinitions.push(...createChunks([...entryModules, ...dynamicEntryModules], assignedEntryPointsByModule));
return chunkDefinitions;
}
entryModules 是一个数组,存放了入口模块的信息。因为是单页应用,入口只有一个,这里的 entryModules 只有一个元素,就是 id 为 "项目路径/index.html" 的模块。如果是多页面应用就会有多个入口。manualChunkAliasByEntry(map数据类型,key为模块信息,value为分包名字)转换成 manualChunkModulesByAlias(对象数据类型,key为分包名字,value为以key为入口的模块数组),顺便记录modulesInManualChunks(所有手动分包包括的模块),方便后续默认分包的使用。把 manualChunkModulesByAlias 包装成 { alias, modules } 放到 chunkDefinitions 数组。entryModules 分析出 dynamicEntryModules(动态入口模块,即用import导入的组件/页面)、dependentEntryPointsByModule (map数据类型,key为模块信息,value为该模块的入口点,入口点可以有多个,可以是静态入口点,也可以是动态入口点)、dynamicallyDependentEntryPointsByDynamicEntry(map数据类型,key为动态入口模块,value为动态入口被动态导入的模块的入口点数组,有点拗口,这个数据就是)assignEntryToStaticDependencies 方法构造 assignedEntryPointsByModule(map数据类型,key为模块信息,value为该模块的入口模块)createChunks 方法把静态入口模块和动态入口模块转换成chunk定义信息,然后推到chunkDefinitions 数组。小结:
下面看看通过 createChunks 是如何进行默认分包的
function createChunks(allEntryPoints, assignedEntryPointsByModule) {
const chunkModules = Object.create(null);
for (const [module, assignedEntryPoints] of assignedEntryPointsByModule) {
let chunkSignature = '';
for (const entry of allEntryPoints) {
chunkSignature += assignedEntryPoints.has(entry) ? 'X' : '_';
}
const chunk = chunkModules[chunkSignature];
if (chunk) {
chunk.push(module);
}
else {
chunkModules[chunkSignature] = [module];
}
}
return Object.values(chunkModules).map(modules => ({
alias: null,
modules
}));
}
allEntryPoints 包括了 静态入口模块 和 动态入口模块
chunk签名。chunk签名 是由 X 和 _ 组成的,总长度为入口模块 allEntryPoints 的数量。遍历 allEntryPoints ,如果当前模块的入口点中有 allEntryPoints 其中的一个,则记作 X 否则记作 _
{alias, modules} 返回chunk定义信息对于生成 chunk签名,举个具体点的例子,allEntryPoints 包括一个静态入口点 index.html,两个动态入口点: Hello.vue 和 World.vue。有一个模块 sdkUtils.js 的入口点为 Hello.vue(即被 Hello.vue 导入);有一个模块 api.js 的入口点为 Hello.vue 以及 World.vue;有一个模块 log.js 依赖了 Hello.vue、World.vue和 index.html。
index.html,遍历 allEntryPoints,入口点有index.html,则 chunk签名 为 X;入口点没有 Hello.vue,则 chunk签名 为 X_;入口点没有 World.vue,则 chunk签名 为 X__。拿到签名之后,用变量chunkModules 存放不同 chunk签名 的模块,以 chunk签名 为key,value为数组,把当前模块push进去。Hello.vue,遍历 allEntryPoints,入口点没有index.html,则 chunk签名 为 _;入口点有 Hello.vue,则 chunk签名 为 _X;入口点没有 World.vue,则 chunk签名 为 _X_。拿到签名之后,用变量chunkModules 存放不同 chunk签名 的模块,以 chunk签名 为key,value为数组,把当前模块push进去。World.vue,同理,chunk签名 为 __X
sdkUtils.js,则 chunk签名 为 _X_。api.js,则 chunk签名 为 _XX。log.js,则 chunk签名 为 XXX。所以,这个例子中,会产生5个chunk,且 api.js对应的 chunk 和 log.js 对应的chunk 就是额外多出来的chunk。
小结:
chunk签名 来标识模块和所有入口点之间依赖关系。有同样 chunk签名 的模块会分到同一个 chunk。从而巧妙地实现一个模块被多个入口点引用,生成一个新的chunk。以及每个入口点都会生成一个chunk。chunk签名 是依赖所有入口点来生成,当动态入口点过多(如页面过多,所有页面都动态导入),可复用的 chunk签名 少,所以分出来的 chunk 就多,且这些被多个入口点引入的模块所生成的 chunk 所包含的模块少,所以会产生 chunk碎片
chunk碎片 问题?chunk碎片的生成原理,在手动分包时,不方便获取动态入口点。尽管可以参考 analyzeModuleGraph ,通过静态入口点来获取,因为代码量多,不好照搬。chunk碎片,它们的特征就是当前模块被2个或以上的模块引用。而模块信息,有两个字段可以利用,一个是 importers,一个是 dynamicImporters,对应着当前模块被静态引入的模块,以及被动态引入的模块。importers.length + dynamicImporters.length > 1,就可以把它放到 manifest chunk中src 目录下,否则会影响 node_modules 下的一些包的依赖关系。所以只需要添加约束条件:id.includes('src')
vite2.x 中,当用户配置了手动分包,就会覆盖 vite 提供的 vendor 分包逻辑。如果想在手动分包中保留 vendor 逻辑,只需把代码拷贝到手动分包;rollup 的默认分包机制,使用 chunk签名 来实现分包,除了入口点(静态入口点如index.html、动态入口点如路由使用 import 导入页面)单独作为一个chunk,那些有多个入口点且 chunk签名 一致的模块会打包到同一个 chunk
importers 和 dynamicImporters 来获取静态和动态导入当前模块的模块。当importers.length + dynamicImporters.length > 1,就把它放进命名为 manifest 的 chunk
我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po
尝试通过RVM将RubyGems升级到版本1.8.10并出现此错误:$rvmrubygemslatestRemovingoldRubygemsfiles...Installingrubygems-1.8.10forruby-1.9.2-p180...ERROR:Errorrunning'GEM_PATH="/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/ruby-1.9.2-p180@global:/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/rub
我的最终目标是安装当前版本的RubyonRails。我在OSXMountainLion上运行。到目前为止,这是我的过程:已安装的RVM$\curl-Lhttps://get.rvm.io|bash-sstable检查已知(我假设已批准)安装$rvmlistknown我看到当前的稳定版本可用[ruby-]2.0.0[-p247]输入命令安装$rvminstall2.0.0-p247注意:我也试过这些安装命令$rvminstallruby-2.0.0-p247$rvminstallruby=2.0.0-p247我很快就无处可去了。结果:$rvminstall2.0.0-p247Search
由于fast-stemmer的问题,我很难安装我想要的任何rubygem。我把我得到的错误放在下面。Buildingnativeextensions.Thiscouldtakeawhile...ERROR:Errorinstallingfast-stemmer:ERROR:Failedtobuildgemnativeextension./System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/rubyextconf.rbcreatingMakefilemake"DESTDIR="cleanmake"DESTDIR=
当我尝试安装Ruby时遇到此错误。我试过查看this和this但无济于事➜~brewinstallrubyWarning:YouareusingOSX10.12.Wedonotprovidesupportforthispre-releaseversion.Youmayencounterbuildfailuresorotherbreakages.Pleasecreatepull-requestsinsteadoffilingissues.==>Installingdependenciesforruby:readline,libyaml,makedepend==>Installingrub
我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www
我意识到这可能是一个非常基本的问题,但我现在已经花了几天时间回过头来解决这个问题,但出于某种原因,Google就是没有帮助我。(我认为部分问题在于我是一个初学者,我不知道该问什么......)我也看过O'Reilly的RubyCookbook和RailsAPI,但我仍然停留在这个问题上.我找到了一些关于多态关系的信息,但它似乎不是我需要的(尽管如果我错了请告诉我)。我正在尝试调整MichaelHartl'stutorial创建一个包含用户、文章和评论的博客应用程序(不使用脚手架)。我希望评论既属于用户又属于文章。我的主要问题是:我不知道如何将当前文章的ID放入评论Controller。
相信很多人在录制视频的时候都会遇到各种各样的问题,比如录制的视频没有声音。屏幕录制为什么没声音?今天小编就和大家分享一下如何录制音画同步视频的具体操作方法。如果你有录制的视频没有声音,你可以试试这个方法。 一、检查是否打开电脑系统声音相信很多小伙伴在录制视频后会发现录制的视频没有声音,屏幕录制为什么没声音?如果当时没有打开音频录制,则录制好的视频是没有声音的。因此,建议在录制前进行检查。屏幕上没有声音,很可能是因为你的电脑系统的声音被禁止了。您只需打开电脑系统的声音,即可录制音频和图画同步视频。操作方法:步骤1:点击电脑屏幕右下侧的“小喇叭”图案,在上方的选项中,选择“声音”。 步骤2:在“声
首先回顾一下拉格朗日定理的内容:函数f(x)是在闭区间[a,b]上连续、开区间(a,b)上可导的函数,那么至少存在一个,使得:通过这个表达式我们可以知道,f(x)是函数的主体,a和b可以看作是主体函数f(x)中所取的两个值。那么可以有, 也就意味着我们可以用来替换 这种替换可以用在求某些多项式差的极限中。方法: 外层函数f(x)是一致的,并且h(x)和g(x)是等价无穷小。此时,利用拉格朗日定理,将原式替换为 ,再进行求解,往往会省去复合函数求极限的很多麻烦。使用要注意:1.要先找到主体函数f(x),即外层函数必须相同。2.f(x)找到后,复合部分是等价无穷小。3.要满足作差的形式。如果是加
SPI接收数据左移一位问题目录SPI接收数据左移一位问题一、问题描述二、问题分析三、探究原理四、经验总结最近在工作在学习调试SPI的过程中遇到一个问题——接收数据整体向左移了一位(1bit)。SPI数据收发是数据交换,因此接收数据时从第二个字节开始才是有效数据,也就是数据整体向右移一个字节(1byte)。请教前辈之后也没有得到解决,通过在网上查阅前人经验终于解决问题,所以写一个避坑经验总结。实际背景:MCU与一款芯片使用spi通信,MCU作为主机,芯片作为从机。这款芯片采用的是它规定的六线SPI,多了两根线:RDY和INT,这样从机就可以主动请求主机给主机发送数据了。一、问题描述根据从机芯片手