Demand background

I recently received a request, Project was supposed to visit https://juejin.cn/pro/index.html, for example, now want to visit https://juejin.cn/pro/xxx/index.html for a merchant configuration, more than a layer of context XXX, The code is the same, except that the latter does a specific theme configuration based on localtion.pathName, showing different styles. So the question is how to make https://juejin.cn/pro/xxx/index.html to https://juejin.cn/pro/index.html.

Processing method

  • configurationnginxthelocation.
  • Copy the packaged folder directly toxxxdirectory
  • A newwebpackPlugins that copy only packaged onesindex.htmlThe file toxxxdirectory

Nginx way

Nginx is the fastest and easiest way to do this. You only need to configure location, forwarding or direction. However, due to the company’s Nginx change process, it was not implemented in this way.

Copy packaged folders directly to subdirectories

For example, the structure of the packaged file is as follows

.
|____favicon.ico
|____index.html
|____css
| |____app.0c521dcc.css
|____js
| |____app.32c95ffe.js
| |____chunk-vendors.f9baef7c.js
|____img
| |____logo.82b9c7a5.png
Copy the code

The fs module of Node copies the resources directly to the XXX folder after packaging

. |____favicon.ico |____index.html |____css | |____app.0c521dcc.css |____js | |____app.32c95ffe.js | | ____chunk - vendors. F9baef7c. Js | ____img | | ____logo. 82 b9c7a5. PNG * * * * * * * * * * * * * * * * * * copy start * * * * * * * * * * * * * * * * | ____xxx | |____favicon.ico | |____index.html | |____css | | |____app.0c521dcc.css | |____js | | |____app.32c95ffe.js | | | ____chunk - vendors. F9baef7c. Js | | ____img | | | ____logo. 82 b9c7a5. PNG * * * * * * * * * * * * * * * * * copy over * * * * * * * * * * * * * * * * *Copy the code

The above approach can be said to fulfill this custom requirements, adding a subdirectory.

Plug-in mode

Take a look at the packaged index.html file

<! DOCTYPEhtml>
<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>

Copy the code

The previous method was to copy and paste by brute force, but the code is the same, this is not necessary, in fact, XXX directory just need to add an index. HTML file, and then modify publicPath. Change the reference path in the resource to.. /, so you can refer to the outer resources. The best way to do this is through webPack plug-ins.

Add a plugin called extra-html-plugin:

The plugin is simple: modify publicPath in link, script tags

extra-html-plugin.js

const { relative } = require('path');
const LINK_RE = /(\
      
       ]+>)/g
      [\w\w]+?>;
const SCRIPT_RE = /(\<script src=")([^"]+)([^>]+>)/g;

// a serial execution utility function for a synchronization task
function pipe(. taskpool) {
   return (. args) = > {
       return taskpool.reduce((prev, curr) = > {
           returncurr(prev(... args)); }); }; }module.exports = class ExtraHtmlPlugin {
   // You need to pass in an absolute path to specify the generation path
   // Instead of writing dead path '.. / '
   constructor(options) {
       this.outputDir = options.outputDir;
       // The additional resource path for index.html
       this.assetPath = ' ';
       // the prefix of the index.html resource
       this.publicPath = ' ';
   }

   apply(compiler) {
       this.getRelativePath(compiler);

       // Add extra index.html before the file emit
       compiler.hooks.emit.tap('ExtraHtmlPlugin'.stats= > {
           let indexHtmlContent = stats.assets['index.html'].source();
           // Bind the pipe function, otherwise the "this" reference will be lost
           const transformTask = pipe(
               this.replaceLinkContent.bind(this), 
               this.replaceScriptContent.bind(this));const source = transformTask(indexHtmlContent);
           // The stats in the document is named compliation object, which stands for the build object
           The // assets property is a resource map that contains all the resource files in this compilation
           // type must include the source and size methods
           stats.assets[this.assetPath + '/index.html'] = {
               source: () = > source,
               size: () = > source.length
           };
       });
   }
   // Get the path to the resource prefix, and index.html
   getRelativePath(compiler) {
       const outputPath = compiler.options.output.path;
       this.publicPath = relative(this.outputDir, outputPath) + '/';
       this.assetPath = relative(outputPath, this.outputDir);
   }

   // Match the resources in the re for path replacement
   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); }};Copy the code

Realize the principle of

  • Calculate the assetPath and publicPath from the outputDir passed in
  • First get local compilation of the original index.html, through the re to replace the resource path
  • Add directly to the resource objectstats.assets[this.assetPath + '/index.html']Returns an object of the specified format

Here is a screenshot of the assets property:

use

In vue.config.js, reference the plug-in and specify the path to input

const ExtraHtmlPlugin = require('./script/extra-html-plugin');
module.exports = {
    publicPath: '. '.productionSourceMap: false./ /... Ignored configurationChainWebpack (config) {config.plugin('extra-html').use(ExtraHtmlPlugin, [
            {
                outputDir: resolve('dist/xxx')}]); }}Copy the code

After the NPM run build, look at the package result. It is exactly the package structure we want.

<! DOCTYPEhtml>
<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>


Copy the code

Double-click xxx.html to open the browser error, not running, telling me that the CSS reference failed

After debugging, the Boostrap file displays a resource path reference problem

When referring to the CSS of the subcontract file, because we configured publicPath is., __webpack_require__.p is “”, we will search from the current folder XXX, but XXX directory only has one index.html, so we can’t find it. We want the chunk referenced from the top as well.. __webpack_require__.p needs to be dynamically corrected.

Dynamically modify the publicPath in WebPack

Webpack provides a series of hooks. For example, mainTemplate can adjust the generation of bootstrap function in different modes. Can we add a section of logic to the bootstrap function? At the time of the bootstrap to perform dynamic modifying __webpack_require__. P, looked at the documents found lots of preset hook can do this, here chose mainTemplate. Hooks. RequireExtensions this hook in our function.

Modify extra-html-plugin to the extra-html-plugin you wrote earlier

const { relative } = require('path');
const LINK_RE = /(\
      
       ]+>)/g
      [\w\w]+?>;
const SCRIPT_RE = /(\<script src=")([^"]+)([^>]+>)/g;

// Dynamically modifies a function of 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) = > {
            returncurr(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= > {
            // Insert our function fragment in this hook
            stats.mainTemplate.hooks.requireExtensions.tap('main'.(source, chunk, hash) = > {
                const chunkMap = chunk.getChunkMaps();
                // This fragment will only be included in the main package
                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 {
                    returnsource; }}); }); }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); }};Copy the code

Rerun NPM run serve and look at the bootstrap function. Additional asyncPublicPath functions are coming in

Check the page and functional things everything is normal, there is no error, this is really OK.

The next day I checked the documentation and found that changing publicPath dynamically didn’t have to be that much trouble at all. All I had to do was add the __webpack_require__.p variable to the entry file. Webpack specifically handles the __webpack_require__ variable when doing ast parsing,

Just import our function in the entry file

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 />
})
Copy the code

Since the entry module, webpack passes in a custom __webpack_require__ function called require, which is a reference object with properties that developers can modify or add in their code. Remember to reset __webpack_require__.p in the entry file.

Since this method requires less code, I also modified publicPath by resetting __webpack_require__.p in the entry file.

I have to say that Webpack’s documentation is so convoluted and hard to understand that it discourages newbies