动态修改webpack中的publicPath

·  阅读 832

需求背景

最近接到一个需求,比如项目本来的访问地址是 https://juejin.cn/pro/index.html, 现在要针对某个商户配置的访问地址是https://juejin.cn/pro/xxx/index.html, 多了一层上下文xxx, 代码都是同一套代码,只是后者会根据localtion.pathname做一个特定的主题配置,展示不同的风格。 所以问题就是怎么让https://juejin.cn/pro/xxx/index.html 指向 https://juejin.cn/pro/index.html

处理方法

  • 配置nginxlocation
  • 将打包好的文件夹直接复制到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属性的一个截图:

image.png

使用方式

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>


复制代码

image.png

双击xxx.html打开浏览器报错了,没跑起来,告诉我css引用失败了

image.png

经过调试发现boostrap文件提示是资源路径引用问题

image.png

在引用分包文件的css时,因为我们配置的publicPath., __webpack_require__.p就是'', 所以会从当前文件夹xxx找,但是xxx目录只有一个index.html, 找不到所以报错。我们希望引用的chunk也是从上层../查找,此时需要动态修正下__webpack_require__.p

动态修改webpack中的publicPath

webpack提供了一系列的hook,如mainTemplate就是可以调整不同模式下bootstrap函数生成,那么我们是否可以在bootstrap函数中新增一段逻辑,在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函数雀食干进来了

image.png

检查了一下页面和功能性的东西一切正常,没有任何报错了,这下子确实是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哦。

image.png

因为这个方式代码量更少,所有我也用了入口文件重置下__webpack_require__.p的方式去修改publicPath。

不得不说,webpack的文档鸡儿拉胯,晦涩难懂,直接把新手给劝退了

分类:
前端
标签:
分类:
前端
标签: