在 webpack 上,如何在不评估脚本的情况下导入脚本?
On webpack how can I import a script without evaluate it?
我最近在做一些网站优化工作,我开始在 webpack 中使用代码拆分,通过这样的导入语句:
import(/* webpackChunkName: 'pageB-chunk' */ './pageB')
正确创建 pageB-chunk.js,现在假设我想 prefetch pageA 中的这个块,我可以通过在 pageA 中添加此语句来完成:
import(/* webpackChunkName: 'pageB-chunk' */ /* webpackPrefetch: true */ './pageB')
这将导致
<link rel="prefetch" href="pageB-chunk.js">
被附加到HTML的头部,然后浏览器将预取它,到目前为止一切顺利。
问题是我在这里使用的 import 语句不仅预取 js 文件,而且还评估 js 文件,意味着该 js 文件的代码被解析并编译为字节码,执行该JS的顶层代码。
这在移动设备上是一个非常耗时的操作,我想优化它,我只想要 prefetch 部分,我不需要 evaluate & execute 部分,因为稍后当一些用户交互发生时,我会触发 parsing & evaluate myself
↑↑↑↑↑↑↑↑↑我只想触发前两步,图片来自https://calendar.perfplanet.com/2011/lazy-evaluation-of-commonjs-modules/↑↑↑↑↑↑↑↑↑
当然我可以通过自己添加预取 link 来做到这一点,但这意味着我需要知道我应该把哪个 URL 放在预取 link 中,webpack 肯定知道这个URL,如何从webpack获取?
webpack有什么简单的方法可以实现吗?
更新
您可以使用 preload-webpack-plugin with html-webpack-plugin 它会让您定义要在配置中预加载的内容,它会自动插入标签来预加载您的块
请注意,如果您现在使用的是 webpack v4,则必须使用 preload-webpack-plugin@next
安装此插件
示例
plugins: [
new HtmlWebpackPlugin(),
new PreloadWebpackPlugin({
rel: 'preload',
include: 'asyncChunks'
})
]
For a project generating two async scripts with dynamically generated
names, such as chunk.31132ae6680e598f8879.js
and
chunk.d15e7fdfc91b34bb78c4.js
, the following preloads will be injected
into the document head
<link rel="preload" as="script" href="chunk.31132ae6680e598f8879.js">
<link rel="preload" as="script" href="chunk.d15e7fdfc91b34bb78c4.js">
更新 2
如果你不想预加载所有异步块但只预加载一次你也可以这样做
您可以使用 或与 preload-webpack-plugin
一起使用
首先,您必须借助 webpack
magic comment
示例
命名该异步块
import(/* webpackChunkName: 'myAsyncPreloadChunk' */ './path/to/file')
然后在插件配置中使用该名称,如
plugins: [
new HtmlWebpackPlugin(),
new PreloadWebpackPlugin({
rel: 'preload',
include: ['myAsyncPreloadChunk']
})
]
首先让我们看看当我们指定script
标签或link
标签加载脚本时浏览器的行为
- 只要浏览器遇到
script
标签,它就会加载并解析它
并立即执行
- 您只能借助
async
和
defer
标记 仅在 DOMContentLoaded
事件 之前
- 如果不插入脚本标签(只用
link
预加载)可以延迟执行(评估)
现在有一些其他的不推荐 hackkey 方法是你发送整个脚本和 string
或 comment
(因为评论或字符串的评估时间几乎可以忽略不计),当你需要执行时,你可以使用 Function() constructor
或 eval
两者都不推荐
另一种方法服务工作者:(这将在页面重新加载后保留您的缓存事件,或者在加载缓存后用户离线)
在现代浏览器中,您可以使用 service worker
来获取和缓存资源(JavaScript、图像、css 任何东西),当主线程请求该资源时,您可以拦截它通过这种方式请求和 return 来自缓存的资源,当您将脚本加载到缓存中时,您不会解析和评估脚本
阅读更多关于服务人员的信息 here
例子
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('v1').then(function(cache) {
return cache.addAll([
'/sw-test/',
'/sw-test/index.html',
'/sw-test/style.css',
'/sw-test/app.js',
'/sw-test/image-list.js',
'/sw-test/star-wars-logo.jpg',
'/sw-test/gallery/bountyHunters.jpg',
'/sw-test/gallery/myLittleVader.jpg',
'/sw-test/gallery/snowTroopers.jpg'
]);
})
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(caches.match(event.request).then(function(response) {
// caches.match() always resolves
// but in case of success response will have value
if (response !== undefined) {
return response;
} else {
return fetch(event.request).then(function (response) {
// response may be used only once
// we need to save clone to put one copy in cache
// and serve second one
let responseClone = response.clone();
caches.open('v1').then(function (cache) {
cache.put(event.request, responseClone);
});
return response;
}).catch(function () {
// any fallback code here
});
}
}));
});
如你所见,这不是 webpack 依赖的 这超出了 webpack 的范围,但是在 webpack 的帮助下,你可以拆分你的包,这将有助于更好地利用 service worker
假设我理解您要实现的目标,您希望在给定事件(例如单击按钮)后解析和执行模块。您可以简单地将 import 语句放在该事件中:
element.addEventListener('click', async () => {
const module = await import("...");
});
更新:
我将所有东西都包含在一个 npm 包中,检查一下!
https://www.npmjs.com/package/webpack-prefetcher
经过几天的研究,我最终写了一个自定义的 babel 插件...
简而言之,插件是这样工作的:
- 收集代码中的所有 import(args) 语句
- 如果 import(args) 包含 /* prefetch: true */ comment
- 从import()[=中找到chunkId 62=] 语句
- 替换为Prefetcher.fetch(chunkId)
Prefetcher 是一个包含 webpack 输出清单的助手 class,可以帮助我们插入预取 link:
export class Prefetcher {
static manifest = {
"pageA.js": "/pageA.hash.js",
"app.js": "/app.hash.js",
"index.html": "/index.html"
}
static function fetch(chunkId) {
const link = document.createElement('link')
link.rel = "prefetch"
link.as = "script"
link.href = Prefetcher.manifest[chunkId + '.js']
document.head.appendChild(link)
}
}
用法示例:
const pageAImporter = {
prefetch: () => import(/* prefetch: true */ './pageA.js')
load: () => import(/* webpackChunkName: 'pageA' */ './pageA.js')
}
a.onmousehover = () => pageAImporter.prefetch()
a.onclick = () => pageAImporter.load().then(...)
这个插件的详细信息可以在这里找到:
Prefetch - Take control from webpack
再次强调,这是一个非常 hacky 的方式,我不喜欢它,如果你想让 webpack 团队实现这个,请在这里投票:
我最近在做一些网站优化工作,我开始在 webpack 中使用代码拆分,通过这样的导入语句:
import(/* webpackChunkName: 'pageB-chunk' */ './pageB')
正确创建 pageB-chunk.js,现在假设我想 prefetch pageA 中的这个块,我可以通过在 pageA 中添加此语句来完成:
import(/* webpackChunkName: 'pageB-chunk' */ /* webpackPrefetch: true */ './pageB')
这将导致
<link rel="prefetch" href="pageB-chunk.js">
被附加到HTML的头部,然后浏览器将预取它,到目前为止一切顺利。
问题是我在这里使用的 import 语句不仅预取 js 文件,而且还评估 js 文件,意味着该 js 文件的代码被解析并编译为字节码,执行该JS的顶层代码。
这在移动设备上是一个非常耗时的操作,我想优化它,我只想要 prefetch 部分,我不需要 evaluate & execute 部分,因为稍后当一些用户交互发生时,我会触发 parsing & evaluate myself
↑↑↑↑↑↑↑↑↑我只想触发前两步,图片来自https://calendar.perfplanet.com/2011/lazy-evaluation-of-commonjs-modules/↑↑↑↑↑↑↑↑↑
当然我可以通过自己添加预取 link 来做到这一点,但这意味着我需要知道我应该把哪个 URL 放在预取 link 中,webpack 肯定知道这个URL,如何从webpack获取?
webpack有什么简单的方法可以实现吗?
更新
您可以使用 preload-webpack-plugin with html-webpack-plugin 它会让您定义要在配置中预加载的内容,它会自动插入标签来预加载您的块
请注意,如果您现在使用的是 webpack v4,则必须使用 preload-webpack-plugin@next
示例
plugins: [
new HtmlWebpackPlugin(),
new PreloadWebpackPlugin({
rel: 'preload',
include: 'asyncChunks'
})
]
For a project generating two async scripts with dynamically generated names, such as
chunk.31132ae6680e598f8879.js
andchunk.d15e7fdfc91b34bb78c4.js
, the following preloads will be injected into the documenthead
<link rel="preload" as="script" href="chunk.31132ae6680e598f8879.js">
<link rel="preload" as="script" href="chunk.d15e7fdfc91b34bb78c4.js">
更新 2
如果你不想预加载所有异步块但只预加载一次你也可以这样做
您可以使用 preload-webpack-plugin
一起使用
首先,您必须借助
命名该异步块webpack magic comment
示例import(/* webpackChunkName: 'myAsyncPreloadChunk' */ './path/to/file')
然后在插件配置中使用该名称,如
plugins: [ new HtmlWebpackPlugin(), new PreloadWebpackPlugin({ rel: 'preload', include: ['myAsyncPreloadChunk'] }) ]
首先让我们看看当我们指定script
标签或link
标签加载脚本时浏览器的行为
- 只要浏览器遇到
script
标签,它就会加载并解析它 并立即执行 - 您只能借助
async
和defer
标记 仅在DOMContentLoaded
事件 之前
- 如果不插入脚本标签(只用
link
预加载)可以延迟执行(评估)
现在有一些其他的不推荐 hackkey 方法是你发送整个脚本和 string
或 comment
(因为评论或字符串的评估时间几乎可以忽略不计),当你需要执行时,你可以使用 Function() constructor
或 eval
两者都不推荐
另一种方法服务工作者:(这将在页面重新加载后保留您的缓存事件,或者在加载缓存后用户离线)
在现代浏览器中,您可以使用 service worker
来获取和缓存资源(JavaScript、图像、css 任何东西),当主线程请求该资源时,您可以拦截它通过这种方式请求和 return 来自缓存的资源,当您将脚本加载到缓存中时,您不会解析和评估脚本
阅读更多关于服务人员的信息 here
例子
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('v1').then(function(cache) {
return cache.addAll([
'/sw-test/',
'/sw-test/index.html',
'/sw-test/style.css',
'/sw-test/app.js',
'/sw-test/image-list.js',
'/sw-test/star-wars-logo.jpg',
'/sw-test/gallery/bountyHunters.jpg',
'/sw-test/gallery/myLittleVader.jpg',
'/sw-test/gallery/snowTroopers.jpg'
]);
})
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(caches.match(event.request).then(function(response) {
// caches.match() always resolves
// but in case of success response will have value
if (response !== undefined) {
return response;
} else {
return fetch(event.request).then(function (response) {
// response may be used only once
// we need to save clone to put one copy in cache
// and serve second one
let responseClone = response.clone();
caches.open('v1').then(function (cache) {
cache.put(event.request, responseClone);
});
return response;
}).catch(function () {
// any fallback code here
});
}
}));
});
如你所见,这不是 webpack 依赖的 这超出了 webpack 的范围,但是在 webpack 的帮助下,你可以拆分你的包,这将有助于更好地利用 service worker
假设我理解您要实现的目标,您希望在给定事件(例如单击按钮)后解析和执行模块。您可以简单地将 import 语句放在该事件中:
element.addEventListener('click', async () => {
const module = await import("...");
});
更新: 我将所有东西都包含在一个 npm 包中,检查一下! https://www.npmjs.com/package/webpack-prefetcher
经过几天的研究,我最终写了一个自定义的 babel 插件...
简而言之,插件是这样工作的:
- 收集代码中的所有 import(args) 语句
- 如果 import(args) 包含 /* prefetch: true */ comment
- 从import()[=中找到chunkId 62=] 语句
- 替换为Prefetcher.fetch(chunkId)
Prefetcher 是一个包含 webpack 输出清单的助手 class,可以帮助我们插入预取 link:
export class Prefetcher {
static manifest = {
"pageA.js": "/pageA.hash.js",
"app.js": "/app.hash.js",
"index.html": "/index.html"
}
static function fetch(chunkId) {
const link = document.createElement('link')
link.rel = "prefetch"
link.as = "script"
link.href = Prefetcher.manifest[chunkId + '.js']
document.head.appendChild(link)
}
}
用法示例:
const pageAImporter = {
prefetch: () => import(/* prefetch: true */ './pageA.js')
load: () => import(/* webpackChunkName: 'pageA' */ './pageA.js')
}
a.onmousehover = () => pageAImporter.prefetch()
a.onclick = () => pageAImporter.load().then(...)
这个插件的详细信息可以在这里找到:
Prefetch - Take control from webpack
再次强调,这是一个非常 hacky 的方式,我不喜欢它,如果你想让 webpack 团队实现这个,请在这里投票: