动态修改webpack中的publicPath
需求背景
处理方法
- 配置
nginx
的location
, - 将打包好的文件夹直接复制到
xxx
目录下 - 新增一个
webpack
插件, 只复制打包后的index.html
文件到xxx
目录
nginx的方式
nginx是最快捷、最简单的方式,只需要配置下location,转发或者定向即可。但是因为公司nginx变更流程的原因,没有通过这种方式去实现。
直接复制打包好的文件夹到子目录
比如打包好的文件结构如下
.
|____favicon.ico
|____index.html
|____css
| |____app.0c521dcc.css
|____js
| |____app.32c95ffe.js
| |____chunk-vendors.f9baef7c.js
|____img
| |____logo.82b9c7a5.png
通过node的fs模块在打包完成后直接对资源复制到xxx
文件夹
.
|____favicon.ico
|____index.html
|____css
| |____app.0c521dcc.css
|____js
| |____app.32c95ffe.js
| |____chunk-vendors.f9baef7c.js
|____img
| |____logo.82b9c7a5.png
******************复制开始****************
|____xxx
| |____favicon.ico
| |____index.html
| |____css
| | |____app.0c521dcc.css
| |____js
| | |____app.32c95ffe.js
| | |____chunk-vendors.f9baef7c.js
| |____img
| | |____logo.82b9c7a5.png
*****************复制结束*****************
上面的方式已经可以说完成了这个定制的需求,增加了一个子目录。
插件的方式
观察下打包后的index.html
文件
<!DOCTYPE html>
<html lang=en>
<head>
<link rel=icon href=favicon.ico>
<title>vue</title>
<link href=css/app.0c521dcc.css rel=preload as=style>
<link href=js/app.32c95ffe.js rel=preload as=script>
<link href=js/chunk-vendors.f9baef7c.js rel=preload as=script>
<link href=css/app.0c521dcc.css rel=stylesheet>
</head>
<body>
<div id=app></div>
<script src=js/chunk-vendors.f9baef7c.js></script>
<script src=js/app.32c95ffe.js></script>
</body>
</html>
之前的方法是通过暴力复制粘贴的方法,但是代码都是相同的,这大可不必,实际上xxx
目录只需要新增一个index.html
文件即可,然后修改下publicPath
。把资源中的引用路径改为../
, 这样就可以引用到外层的资源。这种对打包资源做修改,最好的方式是通过webpack
的插件方式完成。
新增一个名为extra-html-plugin的插件:
这个插件功能很简单:修改link、script标签中的publicPath
extra-html-plugin.js
const { relative } = require('path');
const LINK_RE = /(\<link[\w\W]+?href=")(?!https?:)([^"]+)([^>]+>)/g;
const SCRIPT_RE = /(\<script[\w\W]+?src=")(?!https?:)([^"]+)([^>]+>)/g;
// 一个同步任务的串联执行工具函数
function pipe(...taskpool) {
return (...args) => {
return taskpool.reduce((prev, curr) => {
return curr(prev(...args));
});
};
}
module.exports = class ExtraHtmlPlugin {
// 需要传入一个绝对路径,用于指定生成为路径
// 而不是直接写死路径'../'
constructor(options) {
this.outputDir = options.outputDir;
// 额外添加的index.html的资源路径
this.assetPath = '';
// index.html资源的前缀
this.publicPath = '';
}
apply(compiler) {
this.getRelativePath(compiler);
// 在文件emit之前,新增加额外的index.html
compiler.hooks.emit.tap('ExtraHtmlPlugin', stats => {
let indexHtmlContent = stats.assets['index.html'].source();
// 这里要先bind一下,不然pipe函数执行中会丢失this指向
const transformTask = pipe(
this.replaceLinkContent.bind(this),
this.replaceScriptContent.bind(this)
);
const source = transformTask(indexHtmlContent);
// 这里的stats在文档的命名叫做compliation对象,代表这次的构建对象
// assets属性是一个资源map,包含了这次编译中所有的资源文件
// type就是必须要包含source、和size方法
stats.assets[this.assetPath + '/index.html'] = {
source: () => source,
size: () => source.length
};
});
}
// 获取资源前缀、和index.html的路径
getRelativePath(compiler) {
const outputPath = compiler.options.output.path;
this.publicPath = relative(this.outputDir, outputPath) + '/';
this.assetPath = relative(outputPath, this.outputDir);
}
// 匹配正则中的资源做路径替换
replace(reg, content) {
return content.replace(reg, (_, $1, $2, $3) => {
return $1 + this.publicPath + $2 + $3;
});
}
replaceLinkContent(content) {
return this.replace(LINK_RE, content);
}
replaceScriptContent(content) {
return this.replace(SCRIPT_RE, content);
}
};
实现原理
- 通过传入的outputDir,计算出assetPath和publicPath
- 先获取本地编译原来的index.html, 通过正则替换里面的资源路径
- 在资源对象上直接新增
stats.assets[this.assetPath + '/index.html']
,返回指定格式的对象
这是assets
属性的一个截图:
使用方式
在vue.config.js
, 引用插件, 指定输入的路径
const ExtraHtmlPlugin = require('./script/extra-html-plugin');
module.exports = {
publicPath: '.',
productionSourceMap: false,
// .... 忽略的配置
chainWebpack(config) {
config.plugin('extra-html').use(ExtraHtmlPlugin, [
{
outputDir: resolve('dist/xxx')
}
]);
}
}
npm run build
后看下打包结果,确实是我们想要的打包结构。
<!DOCTYPE html>
<html lang=en>
<head>
<link rel=icon href=../favicon.ico>
<title>vue</title>
<link href=../css/app.0c521dcc.css rel=preload as=style>
<link href=../js/app.32c95ffe.js rel=preload as=script>
<link href=../js/chunk-vendors.f9baef7c.js rel=preload as=script>
<link href=../css/app.0c521dcc.css rel=stylesheet>
</head>
<body>
<div id=app></div>
<script src=../js/chunk-vendors.f9baef7c.js></script>
<script src=../js/app.32c95ffe.js></script>
</body>
</html>
双击xxx.html
打开浏览器报错了,没跑起来,告诉我css引用失败了
经过调试发现boostrap
文件提示是资源路径引用问题
在引用分包文件的css时,因为我们配置的publicPath
是.
, __webpack_require__.p
就是''
, 所以会从当前文件夹xxx
找,但是xxx
目录只有一个index.html
, 找不到所以报错。我们希望引用的chunk
也是从上层../
查找,此时需要动态修正下__webpack_require__.p
。
动态修改webpack中的publicPath
webpack
提供了一系列的hook,如mainTemplate
就是可以调整不同模式下bootstrap
函数生成,那么我们是否可以在bootstra
p函数中新增一段逻辑,在bootstrap
执行的时候动态修改下__webpack_require__.p
,看了下文档发现很多预置的hook可以完成这个操作,这里选了mainTemplate.hooks.requireExtensions
这个钩子塞进去我们的函数。
对之前写的extra-html-plugin坐下修改
extra-html-plugin
const { relative } = require('path');
const LINK_RE = /(\<link[\w\W]+?href=")(?!https?:)([^"]+)([^>]+>)/g;
const SCRIPT_RE = /(\<script[\w\W]+?src=")(?!https?:)([^"]+)([^>]+>)/g;
// 动态修改publicPath的一段函数
const asyncPublicPath = (r, p) => `
function getAsyncPublicPath () {
if (window.location.pathname.indexOf('${r}') > -1) {
__webpack_require__.p = "${p}";
window.__webpack_require__ = __webpack_require__;
}
};
getAsyncPublicPath();`;
function pipe(...taskpool) {
return (...args) => {
return taskpool.reduce((prev, curr) => {
return curr(prev(...args));
});
};
}
module.exports = class ExtraHtmlPlugin {
constructor(options) {
this.outputDir = options.outputDir;
this.assetPath = '';
this.publicPath = '';
}
apply(compiler) {
this.getRelativePath(compiler);
compiler.hooks.emit.tap('ExtraHtmlPlugin', stats => {
let indexHtmlContent = stats.assets['index.html'].source();
const transformTask = pipe(this.replaceLinkContent.bind(this), this.replaceScriptContent.bind(this));
const source = transformTask(indexHtmlContent);
stats.assets[this.assetPath + '/index.html'] = {
source: () => source,
size: () => source.length
};
});
compiler.hooks.compilation.tap('main', stats => {
// 在这个hook塞进去我们的函数片段
stats.mainTemplate.hooks.requireExtensions.tap('main', (source, chunk, hash) => {
const chunkMap = chunk.getChunkMaps();
// 这个片段只会包含在主包
if (Object.keys(chunkMap.hash).length) {
const buff = [source];
buff.push('\n\n// rewrite __webpack_public_path__');
buff.push(asyncPublicPath(this.assetPath, this.publicPath));
return buff.join('\n');
} else {
return source;
}
});
});
}
getRelativePath(compiler) {
const outputPath = compiler.options.output.path;
this.publicPath = relative(this.outputDir, outputPath) + '/';
this.assetPath = relative(outputPath, this.outputDir);
}
replace(reg, content) {
return content.replace(reg, (_, $1, $2, $3) => {
return $1 + this.publicPath + $2 + $3;
});
}
replaceLinkContent(content) {
return this.replace(LINK_RE, content);
}
replaceScriptContent(content) {
return this.replace(SCRIPT_RE, content);
}
};
重新npm run serve
, 查看下bootstrap
函数, 额外的asyncPublicPath
函数雀食干进来了
检查了一下页面和功能性的东西一切正常,没有任何报错了,这下子确实是ok了。
然鹅,第二天我翻了下文档,动态修改publicPath根本不用这么麻烦,只需要在入口文件顶一下__webpack_require__.p
这个变量值即可,webpack在做ast解析的时候会特殊处理这个__webpack_require__
这个变量,
在入口文件直接引入我们的函数即可
main.js
function getAsyncPublicPath () {
if (window.location.pathname.indexOf('xxx') > -1) {
__webpack_require__.p = "../";
window.__webpack_require__ = __webpack_require__;
}
};
getAsyncPublicPath();
new Vue({
render: h => <App />
})
看下了打包结果, 因为入口module
, webpack会传入自定义的__webpack_require__
的require函数, 它是个引用类型的对象,上面挂着了很多属性,开发者可以在代码中修改或新增它的属性,为了保证准确性,记得要在入口文件重置下__webpack_require__.p
哦。
因为这个方式代码量更少,所有我也用了入口文件重置下__webpack_require__.p
的方式去修改publicPath。
不得不说,webpack
的文档鸡儿拉胯,晦涩难懂,直接把新手给劝退了
转载自:https://juejin.cn/post/7004472324976017439