preface

When writing libraries, we sometimes want to load some dependencies on demand, such as polyfills if the code is not running in an environment that supports some functionality. Webpack, currently the most popular packaging tool, has long supported dynamic loading. This article discusses an approach to webpack containing dynamically loaded libraries. Note that this article is written for library authors; if the reader is writing an application, there is no need to read further.

The sample library

// my-lib.js class MyLib { loadDeps() { return new Promise((resolve, reject) => { if (global.TextDecoder === undefined) { return Promise.all([ import('./deps/text-encoding'), import('./deps/other-dep.js'), import('./deps/another-dep.js'), ]).then(resolve).catch(reject); } else { resolve(); }}); } initialize() { this.decoder = new TextDecoder(); / /... }}Copy the code

The library has a loadDeps method that checks for the existence of TextDecoder based on the runtime environment, and if it doesn’t load the dependencies, which are in the local directory. When we publish the library to NPM, we use it like this:

// app.js
import MyLib from 'my-lib';
class App {
    constructor() {
        this.myLib = new MyLib();
    }
}

const app = new App();
app.myLib.loadDeps().then(() => {
    app.myLib.initialize();
    console.log(app.myLib.decoder);
});
Copy the code

In this way, our library can use TextDecoder in any environment.

But will it really go so well?

The problem

Before we publish to the NPM repository, we build the project with Webpack. At this point, Webpack analyzes the dependency paths in the file, generates the dependency files into output.path, and the import() method becomes a method inside Webpack. The dependent path also becomes output.publicPath dependent.

Suppose our WebPack configuration looks like this:

// webpack.config.js
module.exports = {
    output: {
        filename: 'index.js',
        chunkFilename: '[name].js',
        path: 'dist/',
        publicPath: '/',
        libraryTarget: 'umd'
    },
    // ...
};
Copy the code

It then outputs these files:

// webpack output
Built at: 02/19/2019 5:08:41 PM
  Asset      Size  Chunks             Chunk Names
   1.js  79 bytes       1  [emitted]
   2.js  79 bytes       2  [emitted]
   3.js  79 bytes       3  [emitted]
index.js   144 KiB       0  [emitted]  main
Entrypoint main = index.js
Copy the code

What is the path to loading dependencies when the application layer calls app.mylib.loaddeps ()? Correctly guessed, the browser will try to load these paths:

/1.js
/2.js
/3.js
Copy the code

But the result? 404, of course. Because the root directory does not have these files, unless you copy them to the root directory. Similarly, if publicPath were relative (assuming “”), the request would depend on the path relative to the current URL, so if the current URL is /topics/:uid, the request path would be /topics/:uid/1.js. Unless there are dependencies in the current directory, the result is 404. Js and **/1.js point to/ path/to/project/node_modules/my-lib/dist/1.js. For the application layer, changing the server configuration for a library is too cumbersome. For a class library, it should manage its dependencies and not let the application layer or even the server side cooperate.

The solution

We need to solve this path problem so that the results are correct and development is easy. Obviously, the problem is that webPack handles import() when it’s packaged, so if we don’t handle it at compile time, but at run time, wouldn’t that do the trick?

To prepare the dist/ directory structure, modify the webpack configuration:

const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
    output: { ... },
    plugins: [
       new CopyWebpackPlugin([{
           from: 'src/deps/',
           to: '.'
       }]) 
    ]
}
Copy the code

The CopyWebpackPlugin copies the files in SRC /deps/ to the dist/ directory, and dist/ becomes:

Dist ├ ─ ─ 1. Js ├ ─ ─ 2. Js ├ ─ ─ 3. Js ├ ─ ─ the text - encoding. Js ├ ─ ─ other - dep. Js ├ ─ ─ another - dep. Js └ ─ ─ index, jsCopy the code

The next step is to have Webpack not parse import(). There are two ways to do this:

Solution 1: The application layer processes the application layer

This solution is easy to implement by simply having the application layer call import().

// my-lib.js class MyLib { constructor(options) { this.onLoadDeps = options.onLoadDeps || null; } loadDeps() { return new Promise((resolve, reject) => { if (global.TextDecoder === undefined) { if (this.onLoadDeps) { this.onLoadDeps().then(resolve).catch(reject); } else { Promise.all([ import('./deps/text-encoding'), import('./deps/other-dep.js'), import('./deps/another-dep.js'), ]).then(resolve).catch(reject); } } else { resolve(); }}); } } // app.js import MyLib from 'my-lib'; class App { constructor() { this.myLib = new MyLib({ onLoadDeps: () => Promise.all([ import('my-lib/dist/text-encoding'), import('my-lib/dist/other-dep.js'), import('my-lib/dist/another-dep.js'), ]); }); }}Copy the code

This is very simple and does not require any processing in a development environment. The downside is that if more than one project references the library, then when the library adds new dependencies, all projects that reference the library will have to change the source code. This will be a tedious task. There’s actually a more convenient variant of this solution:

// my-lib.js class MyLib { constructor(options) { this.importPrefix = options.importPrefix || './deps'; } loadDeps() { return new Promise((resolve, reject) => { if (global.TextDecoder === undefined) { return Promise.all([ import(this.importPrefix + '/text-encoding'), import(this.importPrefix + '/other-dep.js'), import(this.importPrefix + '/another-dep.js'), ]).then(resolve).catch(reject); } else { resolve(); }}); } } // app.js import MyLib from 'my-lib'; class App { constructor() { this.myLib = new MyLib({ importPrefix: 'my-lib/dist' }); }}Copy the code

Critical Dependency: The request of a dependency is an expression

Scheme two: handled by the class library

This is a slightly more complicated scheme. If you want WebPack not to handle import(), you can’t tell WebPack to parse a file that contains import(), meaning you need to separate the parts that contain load dependencies into another file.

// runtime.js module.exports = { onLoadDeps: function() { return Promise.all([ import('my-lib/dist/text-encoding'), import('my-lib/dist/other-dep.js'), import('my-lib/dist/another-dep.js'), ]); }}Copy the code

Note: Since webPack does not parse this file and loader does not process this file, it is best to use node.js native syntax in this file.

// my-lib.js import RUNTIME from './runtime'; class MyLib { loadDeps() { return new Promise((resolve, reject) => { if (global.TextDecoder === undefined) { RUNTIME.onLoadDeps().then(resolve).catch(reject); } else { resolve(); }}); }}Copy the code

Then modify the WebPack configuration:

module.exports = {
    output: { ... },
    module: {
        noParse: /src\/runtime/,
    },
    plugins: [ ... ] 
}
Copy the code

This way, webpack loads runtime.js when it processes my-lib.js, but doesn’t parse it. So you get the following result:

// dist/index.js /******/ ([ /* 0 */ /***/ (function(module, exports) { module.exports = { onLoadDeps: function onLoadDeps() { return Promise.all([import('my-lib/dist/text-encoding'), import('my-lib/dist/other-dep.js'), import('my-lib/dist/another-dep.js')]); } };  /***/ }), /* 1 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { // ... var MyLib = /*#__PURE__*/ function () { function MyLib() {} var _proto = MyLib.prototype;  _proto.loadDeps = function loadDeps() { var _this = this;  return new Promise(function (resolve, reject) { if (global.TextDecoder === undefined) { _runtime__WEBPACK_IMPORTED_MODULE_0___default.a.onLoadDeps().then(resolve).catch(reject); } else { resolve(); } }); };  _proto.initialize = function initialize() { this.decoder = new TextDecoder(); // ... }; return MyLib; }(); // ...Copy the code

If the application layer references the class library, webPack handles the import() from the class library when packaging the application, just as the application layer does dynamic loading, and the above problem is solved. The last problem is that in the development environment, we also need to test runtime.js, but in this case it is import(‘my-lib/dist/ XXX ‘), which will report Error: Cannot find module. This time can be like project a, with the import (importPrefix + ‘/ text – encoding) way to solve, can also use NormalModuleReplacementPlugin to solve.

// webpack.dev.js
module.exports = {
    // ...
    plugins: [
        new webpack.NormalModuleReplacementPlugin(/my-lib\/dist\/(.*)/, function (resource) {
            resource.request = resource.request.replace(/my-lib\/dist/, '../src/deps')
        }),
    ]
}
Copy the code

My-lib /dist/* redirects resources to my-lib/dist/* / SRC/deps. More detailed usage, you can refer to the official documentation NormalModuleReplacementPlugin. Note: This plug-in is best used in a development environment only.

Although this solution is a bit cumbersome, the biggest advantage is that the dependency management can be left to the class library itself, without the intervention of the application layer. There is no need to let the application layer know if the library changes its packaging location (no longer dist/).

To the end.