This article is based on WebPack 4 and Babel 7, Mac OS, VS Code

Small program development status:

Small developer tools are not available, official support for NPM is limited, and support for common front-end toolchains such as Webpack and Babel is lacking.

Multi-end frameworks (Mpvue, Taro) are rising, but limiting the power of native applets.

After using the multi-terminal development framework for a period of time, our company decided to fall back to the native solution. In addition to the limitation of the multi-terminal framework on the native capability, the most important thing is that we only need a small program of wechat, and there is no need to cross the terminal.

The program is small, but it requires long-term maintenance and multiple people to maintain, so it is important to have standardized engineering practices. The first part of this two-part series covers Webpack, Babel, and environment configuration. The second part covers Typescript, EsLint, unit testing, and CI/CD.

In this article, you will learn how to use front-end engineering techniques to develop native applets:

  • Webpack basic configuration and advanced configuration

  • The WebPack build process, which is the basis for writing WebPack plug-ins

  • Write a WebPack plug-in with less than 50 lines of core source code, enabling small program development to support NPM

  • Show you the role of key code in webPack plug-ins, not just source code

  • Optimize WebPack configuration to eliminate unnecessary code and reduce package size

  • Supports CSS preprocessors such as SASS

Wechat mini program official support for NPM

Support NPM is the premise of small program engineering, wechat official claims to support NPM, but the actual operation is suffocating.

This is why the authors went to great lengths to learn how to write WebPack plug-ins that enable small programs to support NPM just like Web applications. I have to say, this is also an opportunity to learn how to write WebPack plug-ins.

Let’s make fun of the official support for NPM.

Open wechat Developer tool -> Project -> New Project, use the test number to create a small program project

Through the terminal, initialize the NPM

npm init --yes
Copy the code

As you can see, we have generated a package.json file in our project root directory

Now let’s introduce some dependencies through NPM, starting with the famous Moment and Lodash

npm i moment lodash
Copy the code

Click on the menu bar of wechat Developer Tools: Tools -> Build NPM

As you can see, in the root directory of our project, a directory called miniprogram_npm has been generated

Modify app.js and add the following content

// app.js
+ import moment from 'moment';
App({
  onLaunch: function () {
+ console.log('-----------------------------------------------x');
+ let sFromNowText = moment(new Date().getTime() - 360000).fromNow();
+ console.log(sFromNowText);}})Copy the code

And save, you can see the output of wechat Developer Tool console:

To test lodash again, modify app.js to add the following

// app.js
+ import { camelCase } from 'lodash';
App({
  onLaunch: function () {
+ console.log(camelCase('OnLaunch'));}})Copy the code

Save, and then it goes wrong

Then the author tried RXJS, which also failed. Read some information, said to RXJS source clone down compilation, and the compilation results to copy miniprogram_npm this folder. I tried it. It worked. But this way of using NPM is really too strange, too anti-human, not familiar with the taste.

After some research and experimentation, I found that using Webpack to match NPM was the way to go.

Create a Webpack-oriented applet project

First remove the new code from app.js

// app.js
- import moment from 'moment';
- import { camelCase } from 'lodash';
App({
  onLaunch: function () {
- console.log('-----------------------------------------------x');
- let sFromNowText = moment(new Date().getTime() - 360000).fromNow();
- console.log(sFromNowText);
- console.log(camelCase('OnLaunch'));}})Copy the code

Delete the miniprogram_npm folder, it’s an outlier.

Create a folder SRC and move the pages, utils, app.js, app.json, app.wxss, and sitemap.json files (folders) into it

Install WebPack and webpack-CLI

npm i --save-dev webpack webpack-cli copy-webpack-plugin clean-webpack-plugin
Copy the code

In the root directory, create a webpack.config.js file and add the following content

const { resolve } = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  context: resolve('src'),
  entry: './app.js'.output: {
    path: resolve('dist'),
    filename: '[name].js',},plugins: [
    new CleanWebpackPlugin({
      cleanStaleWebpackAssets: false,}).new CopyWebpackPlugin([
      {
        from: '* * / *'.to: '/',},]),],mode: 'none',}Copy the code

Modify the project.config.json file to indicate the entrance to the applet

// project.config.json {"description": "project profile ",+ "miniprogramRoot": "dist/",
}
Copy the code

Enter NPX Webpack at the terminal.

As you can see, in the applets developer tool emulator, our applets refresh and there are no errors on the console.

In the root of our project, we generated a folder called dist, which has exactly the same contents as in SRC, except for the main.js file.

Those of you who know anything about WebPack know that this is the classic structure for a Webpacked project

If you don’t know anything about WebPack, you should read the following documentation until you figure out why there is an extra main.js file.

start

Entry points

Entry and Context

Output (output)

loader

Plug-in (plugins)

In the example above, we simply copied the files from SRC into dist, intact, and let the wechat developer tool know that dist is the code we want to distribute.

This was an important step because we built a small Webpackable project.

We use NPM mainly to solve the dependency problem of js code, so the JS will be handled by Webpack, and other files such as.json,.wxml,.wxss will be copied directly, so things will be much easier.

If you already have a basic understanding of WebPack, then at this point you should understand that applets are multi-page applications that have multiple entries.

Next, let’s modify webpack.config.js to configure the entry

- entry: './app.js'
+ entry: {
+ 'app' : './app.js',
+ 'pages/index/index': './pages/index/index.js',
+ 'pages/logs/logs' : './pages/logs/logs.js'
+},
Copy the code

Webpack needs Babel to handle JS, hence Babel.

npm i  @babel/core @babel/preset-env babel-loader --save-dev
Copy the code

Create a.babelrc file in the root directory and add the following

// .babelrc
{
  "presets": ["@babel/env"]}Copy the code

Modify webpack.config.js to use babel-Loader to process js files

module.exports = {
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ use: 'babel-loader'
+}
+]
+},
}
Copy the code

When copying files from SRC to dist, exclude js files

new CopyWebpackPlugin([
  {
    from: '**/*',
    to: './',
+ ignore: ['**/*.js']}])Copy the code

At this point, the webpack.config.js file looks like this:

const { resolve } = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  context: resolve('src'),
  entry: {
    app: './app.js'.'pages/index/index': './pages/index/index.js'.'pages/logs/logs': './pages/logs/logs.js',},output: {
    path: resolve('dist'),
    filename: '[name].js',},module: {
    rules: [{test: /\.js$/.use: 'babel-loader',}]},plugins: [
    new CleanWebpackPlugin({
      cleanStaleWebpackAssets: false,}).new CopyWebpackPlugin([
      {
        from: '* * / *'.to: '/'.ignore: ['**/*.js'],},])],mode: 'none',}Copy the code

Perform NPX webpack

As you can see, main.js is missing from the dist folder, along with the entire utils folder, because utils/util.js has been merged into the pages/logs/logs.js file that depends on it.

Why is main.js missing?

As you can see, in the applets developer tool emulator, our applets refresh and there are no errors on the console.

Add the following code back to the app.js file and see how it works.

// app.js
+ import moment from 'moment';
+ import { camelCase } from 'lodash';
App({
  onLaunch: function () {
+ console.log('-----------------------------------------------x');
+ let sFromNowText = moment(new Date().getTime() - 360000).fromNow();
+ console.log(sFromNowText);
+ console.log(camelCase('OnLaunch'));}})Copy the code

As you can see, both moment and Lodash work.

This is an important milestone, as we are finally able to use NPM properly.

At this point, we haven’t started writing the WebPack plug-in yet.

If you notice, when you execute the NPX webpack command, the terminal will output the following information

The generated app.js file is 1M in size. You should know that small programs have a 2M size limit. Don’t worry about this, we will optimize it later with webPack configuration.

Now, let’s start writing the WebPack plug-in.

The first WebPack plug-in

Previously, we configured the entrance to the applet in the following way,

entry: {
  'app': './app.js'.'pages/index/index': './pages/index/index.js'.'pages/logs/logs': './pages/logs/logs.js',},Copy the code

This is ugly, and it means that every page or Component needs to be configured, so let’s write a WebPack plug-in to handle this.

Start by installing a dependency that replaces the file extension

npm i --save-dev replace-ext
Copy the code

Create a folder called plugin in the project root directory and create a file called minaWebPackplugin.js with the following contents:

// plugin/MinaWebpackPlugin.js
const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin')
const MultiEntryPlugin = require('webpack/lib/MultiEntryPlugin')
const path = require('path')
const fs = require('fs')
const replaceExt = require('replace-ext')

function itemToPlugin(context, item, name) {
  if (Array.isArray(item)) {
    return new MultiEntryPlugin(context, item, name)
  }
  return new SingleEntryPlugin(context, item, name)
}

function _inflateEntries(entries = [], dirname, entry) {
  const configFile = replaceExt(entry, '.json')
  const content = fs.readFileSync(configFile, 'utf8')
  const config = JSON.parse(content) ; ['pages'.'usingComponents'].forEach(key= > {
    const items = config[key]
    if (typeof items === 'object') {
      Object.values(items).forEach(item= > inflateEntries(entries, dirname, item))
    }
  })
}

function inflateEntries(entries, dirname, entry) {
  entry = path.resolve(dirname, entry)
  if(entry ! =null && !entries.includes(entry)) {
    entries.push(entry)
    _inflateEntries(entries, path.dirname(entry), entry)
  }
}

class MinaWebpackPlugin {
  constructor() {
    this.entries = []
  }

  // Apply is the entry for each plugin
  apply(compiler) {
    const { context, entry } = compiler.options
    // Find all entry files and store them in Entries
    inflateEntries(this.entries, context, entry)

    // The compiler is subscribed to the entryOption event, which executes the code in the callback when the event occurs
    compiler.hooks.entryOption.tap('MinaWebpackPlugin', () = > {this.entries
        // Replace the file extension with js
        .map(item= > replaceExt(item, '.js'))
        // Convert the absolute path to the path relative to the context
        .map(item= > path.relative(context, item))
        // Apply each entry file as if it were configured manually
        // 'app' : './app.js',
        // 'pages/index/index': './pages/index/index.js',
        // 'pages/logs/logs' : './pages/logs/logs.js',
        .forEach(item= > itemToPlugin(context, '/' + item, replaceExt(item, ' ')).apply(compiler))
      // Return true to tell the webPack plugin not to process the entry file because it is already handled here
      return true}}})module.exports = MinaWebpackPlugin
Copy the code

The plug-in does exactly the same thing we did with the entry configuration manually, analyzing the.json file through the code, finding all the possible entry files, and adding them to WebPack.

Modify webpack.config.js to apply the plug-in

+ const MinaWebpackPlugin = require('./plugin/MinaWebpackPlugin');

module.exports = {
   context: resolve('src'),
- entry: {
- 'app' : './app.js',
- 'pages/index/index': './pages/index/index.js',
- 'pages/logs/logs' : './pages/logs/logs.js'
-},
+ entry: './app.js',

   plugins: [
+ new MinaWebpackPlugin()
   ],
   mode: 'none'
};
Copy the code

Execute NPX Webpack and pass!

Is the above plug-in code hard to read? Because we haven’t understood the webPack workflow yet.

The WebPack build process

Programming is a technology that processes input and output. Webpack is like a machine. Entry is raw material, which goes through several processes (Plugin, loader) to generate several intermediate products (dependency, Module, chunk, assets). Finally put the product into the Dist folder.

Before going through the WebPack workflow, read the official guide to writing a plug-in to get a sense of what a plug-in consists of, what types of event hooks are, how to touch them (subscribe to them), and how to invoke them (publish them).

As we go through the WebPack process, we will explain in detail where it is helpful to understand the small webPack plug-in that we are going to write. Other areas will be brief. If you want to have a deeper understanding of the WebPack process, you will need to read other materials and the source code.

Initialization phase

When we execute a command like NPX webpack, WebPack parses the webpack.config.js file and the command-line arguments, combines the configuration and arguments into an options object, and then calls the Webpack function

// webpack.js
const webpack = (options, callback) = > {
  let compiler

  // Complete the default configuration
  options = new WebpackOptionsDefaulter().process(options)

  // Create the compiler object
  compiler = new Compiler(options.context)
  compiler.options = options

  // Apply the plug-in that the user configures or passes as a command-line argument via webpack.config.js
  if (options.plugins && Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      plugin.apply(compiler)
    }
  }

  // According to the configuration, apply the webPack built-in plug-in
  compiler.options = new WebpackOptionsApply().process(options, compiler)

  / / start compiler
  compiler.run(callback)
  return compiler
}
Copy the code

In this function, the Compiler object is created, the complete configuration parameters options are saved to the compiler object, and the run method of the compiler is called.

The Compiler object represents the complete Configuration of the WebPack environment. This object is created once when webpack is started and all operational Settings, including options, Loader and plugin, are configured. You can use compiler to access webPack’s main environment.

As you can see from the above source code, the user-configured plugin is applied before the built-in plugin.

WebpackOptionsApply. The registration process for quite a few plug-ins, one of them:

// WebpackOptionsApply.js
class WebpackOptionsApply extends OptionsApply {
  process(options, compiler) {
    new EntryOptionPlugin().apply(compiler)
    compiler.hooks.entryOption.call(options.context, options.entry)
  }
}
Copy the code

WebpackOptionsApply applies the EntryOptionPlugin plugin and immediately triggers the Compiler’s entryOption event hook,

The EntryOptionPlugin internally registers an entryOption event hook to listen to.

EntryOption is a SyncBailHook, meaning that as long as one plug-in returns true, subsequent plug-in code registered on that hook will not be called. We use this feature when we write small program plug-ins.

// EntryOptionPlugin.js
const itemToPlugin = (context, item, name) = > {
  if (Array.isArray(item)) {
    return new MultiEntryPlugin(context, item, name)
  }
  return new SingleEntryPlugin(context, item, name)
}

module.exports = class EntryOptionPlugin {
  apply(compiler) {
    compiler.hooks.entryOption.tap('EntryOptionPlugin', (context, entry) => {
      if (typeof entry === 'string' || Array.isArray(entry)) {
        // If the name of the entry is not specified, the default is main
        itemToPlugin(context, entry, 'main').apply(compiler)
      } else if (typeof entry === 'object') {
        for (const name of Object.keys(entry)) {
          itemToPlugin(context, entry[name], name).apply(compiler)
        }
      } else if (typeof entry === 'function') {
        new DynamicEntryPlugin(context, entry).apply(compiler)
      }
      // Note that this returns true,
      return true}}})Copy the code

The code in the EntryOptionPlugin is very simple. It delegates work to SingleEntryPlugin, MultiEntryPlugin, and DynamicEntryPlugin, depending on the type of entry.

The code of these three plug-ins is not complicated, the logic is roughly the same, and the ultimate goal is to call compilation.addentry. Let’s take a look at the source code of SingleEntryPlugin

// SingleEntryPlugin.js
class SingleEntryPlugin {
  constructor(context, entry, name) {
    this.context = context
    this.entry = entry
    this.name = name
  }

  apply(compiler) {
    // Compiler calls the compile method in the run method, which creates the Compilation object
    compiler.hooks.compilation.tap('SingleEntryPlugin', (compilation, { normalModuleFactory }) => {
      // Set the mapping between the dependency and module factory
      compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory)
    })

    // Compiler After the Compilation object is created, the make event is triggered
    compiler.hooks.make.tapAsync('SingleEntryPlugin', (compilation, callback) => {
      const { entry, name, context } = this

      // Create a Dependency object based on the entry file and name
      const dep = SingleEntryPlugin.createDependency(entry, name)

      // When this method is called, the compile process starts
      compilation.addEntry(context, dep, name, callback)
    })
  }

  static createDependency(entry, name) {
    const dep = new SingleEntryDependency(entry)
    dep.loc = { name }
    return dep
  }
}
Copy the code

So how is the make event triggered? When WebpackOptionsApply. After the process execution, the compiler will call the run method, and run method calls the compile method, triggered the inside make event hooks, as shown in the following code:

// webpack.js
const webpack = (options, callback) = > {
  // Based on the configuration, the webPack built-in plugins are applied, including the EntryOptionPlugin, and the Compiler's entryOption event is triggered
  // The EntryOptionPlugin listens for this event and applies the SingleEntryPlugin
  // The SingleEntryPlugin listens to the Make event of the Compiler and calls the addEntry method of the Compilation object to start the compilation process
  compiler.options = new WebpackOptionsApply().process(options, compiler)

  // This method calls the compile method, which triggers the make event, transferring control to Compilation
  compiler.run(callback)
}
Copy the code
// Compiler.js
class Compiler extends Tapable {
  run(callback) {
    const onCompiled = (err, compilation) = > {
      // ...
    }
    // Call the compile method
    this.compile(onCompiled)
  }

  compile(callback) {
    const params = this.newCompilationParams()
    this.hooks.compile.call(params)
    // Create the Compilation object
    const compilation = this.newCompilation(params)
    // Trigger the Make event hook to transfer control to the Compilation process
    this.hooks.make.callAsync(compilation, err => {
      // ...
    })
  }

  newCompilation(params) {
    const compilation = this.createCompilation()
    // When the Compilation object is created, the Compilation event hook is triggered
    // This is a good time to listen to compilation events
    this.hooks.compilation.call(compilation, params)
    return compilation
  }
}
Copy the code

The WebPack function creates the Compiler object, which in turn creates the Compilation object. The Compiler object represents the complete configuration of the WebPack environment, while the compilatoin object is responsible for the entire packaging process, storing the intermediate products of the packaging process. After the compiler object triggers the Make event, control is transferred to compilation, which starts the main compilation and build process by calling the addEntry method.

At this point, we have enough knowledge to understand the WebPack plug-in we wrote earlier

// MinaWebpackPlugin.js
class MinaWebpackPlugin {
  constructor() {
    this.entries = []
  }

  apply(compiler) {
    const { context, entry } = compiler.options
    inflateEntries(this.entries, context, entry)

    // As with the EntryOptionPlugin, listen for entryOption events
    // This event is triggered in WebpackOptionsApply
    compiler.hooks.entryOption.tap(pluginName, () => {
      this.entries
        .map(item= > replaceExt(item, '.js'))
        .map(item= > path.relative(context, item))
        // Use SingleEntryPlugin for each entry, as with EntryOptionPlugin
        .forEach(item= > itemToPlugin(context, '/' + item, replaceExt(item, ' ')).apply(compiler))

      // As with EntryOptionPlugin, return true. Because entryOption is a SyncBailHook,
      // The custom plugin executes before the built-in plugin, so the code in the EntryOptionPlugin callback is no longer executed.
      return true}}})Copy the code

To dynamically register entries, in addition to listening for the entryOption hook, we can also listen for the make hook for the same purpose.

Module construction phase

AddEntry calls the private method _addModuleChain, which does two main things. One is to obtain the corresponding module factory and create the module according to the module type; the other is to build the module.

This stage is mainly the loader stage.

class Compilation extends Tapable {
  // If you've looked at the SingleEntryPlugin source code, you know that the entry is not a string, but a Dependency object
  addEntry(context, entry, name, callback) {
    this._addModuleChain(context, entry, onModule, callbak)
  }

  _addModuleChain(context, dependency, onModule, callback) {
    const Dep = dependency.constructor
    // Get the factory for the module, which is set in the SingleEntryPlugin
    const moduleFactory = this.dependencyFactories.get(Dep)
    // Create the module through the factory
    moduleFactory.create(/* In the callback to this method, call this.buildModule to build the module. After the completion of the building, call this. ProcessModuleDependencies dependence to processing module This is a loop and recursive process, by relying on its corresponding module factory to build the module, until all the sub module build complete * /)
  }

  buildModule(module, optional, origin, dependencies, thisCallback) {
    // Build module
    module.build(/* As the most time-consuming step, building the module can be refined into three steps: 1. Call each loader to process the dependency between modules 2. Parse the source file processed by loader to generate an abstract syntax tree AST 3. Walk through the AST to get the dependencies of the Module. The result is stored in the Module dependencies attribute */)}}Copy the code

Chunk generation phase

After all modules are built, WebPack calls the compilation.seal method to start generating chunks.

// https://github.com/webpack/webpack/blob/master/lib/Compiler.js#L625
class Compiler extends Tapable {
  compile(callback) {
    const params = this.newCompilationParams()
    this.hooks.compile.call(params)
    // Create the Compilation object
    const compilation = this.newCompilation(params)
    // Trigger the Make event hook to transfer control to the Compilation process
    this.hooks.make.callAsync(compilation, err => {
      // After the compile and build process is finished, go back here,
      compilation.seal(err= >{})}}}Copy the code

Each entry point, public dependency, dynamic import, and Runtime extract generates a chunk.

The SEAL method includes tuning, chunking, hashing, and compiling to stop receiving new modules and start generating chunks. This phase relies on webPack internal plug-ins to optimize the module, hash the chunk generated by the build, and so on.

// https://github.com/webpack/webpack/blob/master/lib/Compilation.js#L1188
class Compilation extends Tapable {
  seal(callback) {
    // _preparedEntrypoints are populated in the addEntry method, which holds the entry name and corresponding entry Module
    // Generate a new chunk for each module in the entry
    for (const preparedEntrypoint of this._preparedEntrypoints) {
      const module = preparedEntrypoint.module
      const name = preparedEntrypoint.name
      / / create the chunk
      const chunk = this.addChunk(name)
      // Entrypoint inherits from ChunkGroup
      const entrypoint = new Entrypoint(name)
      entrypoint.setRuntimeChunk(chunk)
      entrypoint.addOrigin(null, name, preparedEntrypoint.request)

      this.namedChunkGroups.set(name, entrypoint)
      this.entrypoints.set(name, entrypoint)
      this.chunkGroups.push(entrypoint)

      // Establish the relationship between chunk and chunkGroup
      GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk)
      // Establish the relationship between chunks and modules
      GraphHelpers.connectChunkAndModule(chunk, module)

      // Indicates that the chunk is generated from an entry
      chunk.entryModule = module
      chunk.name = name

      this.assignDepth(module)}// Iterate over the dependency list of modules and add them to the chunk
    this.processDependenciesBlocksForChunkGroups(this.chunkGroups.slice())

    / / optimization
    // SplitChunksPlugin will listen for the optimizeChunksAdvanced event, extract the common module, and form a new chunk
    // RuntimeChunkPlugin will listen for the optimizeChunksAdvanced event and extract the Runtime chunk
    while (
      this.hooks.optimizeChunksBasic.call(this.chunks, this.chunkGroups) ||
      this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups) ||
      this.hooks.optimizeChunksAdvanced.call(this.chunks, this.chunkGroups)
    ) {
      /* empty */
    }
    this.hooks.afterOptimizeChunks.call(this.chunks, this.chunkGroups)

    / / hash
    this.createHash()

    // This hook allows you to modify chunks before generating assets, which we will use later
    this.hooks.beforeChunkAssets.call()

    // Generate assets by chunks
    this.createChunkAssets()
  }
}
Copy the code

Assets Rendering phase

When Compilation is instantiated, you instantiate three objects at the same time: MainTemplate, ChunkTemplate, and ModuleTemplate. These three objects are used as templates to render the chunk objects and get the final code.

class Compilation extends Tapable {
  // https://github.com/webpack/webpack/blob/master/lib/Compilation.js#L2373
  createChunkAssets() {
    // Each chunk is rendered as an asset
    for (let i = 0; i < this.chunks.length; i++) {
      // Render with mainTemplate if chunk contains WebPack Runtime code, otherwise with chunkTemplate
      // What is WebPack Runtime
      const template = chunk.hasRuntime() ? this.mainTemplate : this.chunkTemplate
    }
  }
}
Copy the code

Let’s look at how MainTemplate renders chunk.

// https://github.com/webpack/webpack/blob/master/lib/MainTemplate.js
class MainTemplate extends Tapable {
  constructor(outputOptions) {
    super(a)this.hooks = {
      bootstrap: new SyncWaterfallHook(['source'.'chunk'.'hash'.'moduleTemplate'.'dependencyTemplates']),
      render: new SyncWaterfallHook(['source'.'chunk'.'hash'.'moduleTemplate'.'dependencyTemplates']),
      renderWithEntry: new SyncWaterfallHook(['source'.'chunk'.'hash']),}// It listens to the render event
    this.hooks.render.tap('MainTemplate', (bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) => {
      const source = new ConcatSource()
      // Splice the runtime source code
      source.add(new PrefixSource('/ * * * * * * /, bootstrapSource))

      MainTemplate delegates the responsibility of rendering the module code to moduleTemplate, and only generates the Runtime code itself
      source.add(this.hooks.modules.call(new RawSource(' '), chunk, hash, moduleTemplate, dependencyTemplates))

      return source
    })
  }

  // This is the entry method for Template
  render(hash, chunk, moduleTemplate, dependencyTemplates) {
    // Generate runtime code
    const buf = this.renderBootstrap(hash, chunk, moduleTemplate, dependencyTemplates)

    MainTemplate itself listens for this event in the constructor, completing the concatenation of the Runtime code and the Module code
    let source = this.hooks.render.call(
      // Pass the Runtime code
      new OriginalSource(Template.prefix(buf, ' \t') + '\n'.'webpack/bootstrap'),
      chunk,
      hash,
      moduleTemplate,
      dependencyTemplates,
    )

    // For each entry module, that is, the module added via compilation.addEntry
    if (chunk.hasEntryModule()) {
      // Fires the renderWithEntry event, giving us a chance to modify the generated code
      source = this.hooks.renderWithEntry.call(source, chunk, hash)
    }
    return new ConcatSource(source, '; ')
  }

  renderBootstrap(hash, chunk, moduleTemplate, dependencyTemplates) {
    const buf = []
    // With the bootstrap hook, users can add their own Runtime code
    buf.push(this.hooks.bootstrap.call(' ', chunk, hash, moduleTemplate, dependencyTemplates))
    return buf
  }
}
Copy the code

Rendering is the process of generating code, code is strings, rendering is the process of concatenating and replacing strings.

The final rendered code is stored in the Assets property of the Compilation.

Output file stage

Finally, WebPack calls the Compiler’s emitAssets method and outputs the file to the corresponding path according to the configuration item in the output, thus ending the packing process.

// https://github.com/webpack/webpack/blob/master/lib/Compiler.js
class Compiler extends Tapable {
  run(callback) {
    const onCompiled = (err, compilation) = > {
      // Output file
      this.emitAssets(compilation, err => {})
    }
    // Call the compile method
    this.compile(onCompiled)
  }

  emitAssets(compilation, callback) {
    const emitFiles = err= > {}
    // Emit the emit event before entering the file. This is the last chance to modify the assets
    this.hooks.emit.callAsync(compilation, err => {
      outputPath = compilation.getPath(this.outputPath)
      this.outputFileSystem.mkdirp(outputPath, emitFiles)
    })
  }

  compile(onCompiled) {
    const params = this.newCompilationParams()
    this.hooks.compile.call(params)
    // Create the Compilation object
    const compilation = this.newCompilation(params)
    // Trigger the Make event hook, transfer control to compilation, and start compiling the Module
    this.hooks.make.callAsync(compilation, err => {
      // After the module is compiled and built, start generating chunks and assets
      compilation.seal(err= > {
        // Call emitAssets after the chunks and assets are generated
        return onCompiled(null, compilation)
      })
    })
  }
}
Copy the code

The separation of the Runtime

Now, back to our applet project, make sure that app.js has removed the following code

// app.js
- import moment from 'moment';
- import { camelCase } from 'lodash';
App({
  onLaunch: function () {
- console.log('-----------------------------------------------x');
- let sFromNowText = moment(new Date().getTime() - 360000).fromNow();
- console.log(sFromNowText);
- console.log(camelCase('OnLaunch'));}})Copy the code

Execute NPX Webpack and observe the generated code

The webpackBootstrap code generated by the mainTemplate is the WebPack Runtime code and is the starting point for execution of the entire application. ModuleTemplate wraps our code in a module wrapper function.

The code line with /******/ prefix means that the line code is generated by mainTemplate, and the code with /***/ prefix means that the line code is generated by moduleTemplate. The code without prefix is the module code we wrote after being processed by loader.

Let’s look at the code for dist/logs/logs.js again

You can see

  • The webPack Runtime code is also generated,

  • The code in utils/util.js is merged into dist/logs/logs.js

  • The code in logs.js and util.js is wrapped in module wrappers, respectively

What do the numbers mean? They represent the id of the module.

As you can see from the above code, logs.js imports the module with ID 3 through __webpack_require__(3), which is util.js.

Instead of generating The Runtime code for each entry file, we want to extract it into a separate file to reduce the size of the app. We do this by configuring runtimeChunk.

Modify the webpack.config.js file and add the following configuration

module.exports = {
+ optimization: {
+ runtimeChunk: {
+ name: 'runtime'
+}
+},
  mode: 'none'
}
Copy the code

Do NPX webpack,

As you can see, in the dist directory, a file named Runtime.js is generated

This is an IIFE.

Now let’s look at dist/app.js

This seems to store the app.js module in the global object Window, but there is no window object in the applet, only wx. In webpack.config.js, we configure the global object to wx

module.exports = {
  output: {
    path: resolve('dist'),
- filename: '[name].js'
+ filename: '[name].js',
+ globalObject: 'wx'}},Copy the code

However, there is a problem. Our little program has stopped running

This is because applets are different from Web applications, which can refer to runtime.js via

We have to make other modules aware of runtime.js because runtime.js is an immediate function expression, so we just need to import it.

As we mentioned in the Assets rendering phase:

// For each entry module, that is, the module added via compilation.addEntry
if (chunk.hasEntryModule()) {
  // Fires the renderWithEntry event, giving us a chance to modify the generated code
  source = this.hooks.renderWithEntry.call(source, chunk, hash)
}
Copy the code

The second WebPack plug-in

The previous MinaWebpackPlugin was used to handle entries. Here we follow the single responsibility principle and write another plug-in to handle Runtime.

Fortunately, someone has already written such a plugin that we can use directly with the MinaRuntimePlugin.

For your convenience and learning, we will revised the code slightly, copied to the plugin/MinaRuntimePlugin js

// plugin/MinaRuntimePlugin.js
/* * copied from https://github.com/tinajs/mina-webpack/blob/master/packages/mina-runtime-webpack-plugin/index.js */
const path = require('path')
const ensurePosix = require('ensure-posix-path')
const { ConcatSource } = require('webpack-sources')
const requiredPath = require('required-path')

function isRuntimeExtracted(compilation) {
  return compilation.chunks.some(chunk= >chunk.isOnlyInitial() && chunk.hasRuntime() && ! chunk.hasEntryModule()) }function script({ dependencies }) {
  return '; ' + dependencies.map(file= > `require('${requiredPath(file)}'); `).join(' ')}module.exports = class MinaRuntimeWebpackPlugin {
  constructor(options = {}) {
    this.runtime = options.runtime || ' '
  }

  apply(compiler) {
    compiler.hooks.compilation.tap('MinaRuntimePlugin', compilation => {
      for (let template of [compilation.mainTemplate, compilation.chunkTemplate]) {
        // Listen for the Template event renderWithEntry
        template.hooks.renderWithEntry.tap('MinaRuntimePlugin', (source, entry) => {
          if(! isRuntimeExtracted(compilation)) {throw new Error(['Please reuse the runtime chunk to avoid duplicate loading of javascript files.'."Simple solution: set `optimization.runtimeChunk` to `{ name: 'runtime.js' }` .".'Detail of `optimization.runtimeChunk`: https://webpack.js.org/configuration/optimization/#optimization-runtimechunk .',
              ].join('\n'),)}// If it is not an entry chunk, that is, a chunk generated by a module added through compilation.addEntry, leave it alone
          if(! entry.hasEntryModule()) {return source
          }

          let dependencies = []
          // Find all other chunks that the entry chunk depends on
          entry.groupsIterable.forEach(group= > {
            group.chunks.forEach(chunk= > {
              /** * assume output.filename is chunk.name here */
              let filename = ensurePosix(path.relative(path.dirname(entry.name), chunk.name))

              if (chunk === entry || ~dependencies.indexOf(filename)) {
                return
              }
              dependencies.push(filename)
            })
          })

          // Add runtime and common code dependencies in front of the source code
          source = new ConcatSource(script({ dependencies }), source)
          return source
        })
      }
    })
  }
}
Copy the code

Modify webpack.config.js to apply the plug-in

  const MinaWebpackPlugin = require('./plugin/MinaWebpackPlugin');
+ const MinaRuntimePlugin = require('./plugin/MinaRuntimePlugin');
  module.exports = {
    plugins: [
      new MinaWebpackPlugin(),
+ new MinaRuntimePlugin()],}Copy the code

Run NPX webpack and our applet should now run normally.

View the dist/app. Js, dist/pages/index/index. The js file, such as their first row to add a similar; require(‘./.. /.. /runtime’); The code.

Watch mode

So far, we have executed NPX Webpack every time we change the code, which is a bit cumbersome. Can we have WebPack detect changes in the file and automatically refresh it? The answer is yes.

Webpack can be run as either run or watchRun

// https://github.com/webpack/webpack/blob/master/lib/webpack.js#L62
const webpack = (options, callback) = > {
  if (options.watch === true| | -Array.isArray(options) && options.some(o= > o.watch))) {
    const watchOptions = Array.isArray(options) ? options.map(o= > o.watchOptions || {}) : options.watchOptions || {}
    // If watch is executed, run will not be executed
    return compiler.watch(watchOptions, callback)
  }
  compiler.run(callback)
  return compiler
}
Copy the code

Modify the plugin/MinaWebpackPlugin. Js file

class MinaWebpackPlugin {
  constructor() {
    this.entries = []
  }

  applyEntry(compiler, done) {
    const { context } = compiler.options
    this.entries
      .map(item= > replaceExt(item, '.js'))
      .map(item= > path.relative(context, item))
      .forEach(item= > itemToPlugin(context, '/' + item, replaceExt(item, ' ')).apply(compiler))
    if (done) {
      done()
    }
  }

  apply(compiler) {
    const { context, entry } = compiler.options
    inflateEntries(this.entries, context, entry)

    compiler.hooks.entryOption.tap('MinaWebpackPlugin', () = > {this.applyEntry(compiler)
      return true
    })

    // Listen for the watchRun event
    compiler.hooks.watchRun.tap('MinaWebpackPlugin', (compiler, done) => {
      this.applyEntry(compiler, done)
    })
  }
}
Copy the code

Execute NPX webpack –watch — Progress to open watch mode, modify source code and save, and dist will be regenerated.

Webpack configuration optimization

Webpack can help us turn ES6 to ES5, compress and obfuscate code, so these things do not need wechat developer tools to do for us. Click the details button in the upper right corner of wechat developer tool. In project Settings, deselect ES6 to ES5, automatic compression and confusion when uploading code, as shown in the picture:

Extract common code

SRC /pages/index/index.js

+ const util = require('.. /.. /utils/util.js');
+ console.log(util.formatTime(new Date()));

  const app = getApp();
Copy the code

Perform NPX webpack

As you can see, the generated dist/pages/index/index, js and dist/pages/logs/logs. The js files have the same code

; (function(module, exports) {
  // util.js
  var formatTime = function formatTime(date) {
    var year = date.getFullYear()
    var month = date.getMonth() + 1
    var day = date.getDate()
    var hour = date.getHours()
    var minute = date.getMinutes()
    var second = date.getSeconds()
    return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':')}var formatNumber = function formatNumber(n) {
    n = n.toString()
    return n[1]? n :'0' + n
  }

  module.exports = {
    formatTime: formatTime,
  }
})
Copy the code

This is not what we want. We need to separate the common code into a separate file. See the documentation here.

Modify the webpack.config.js file

  optimization: {
+ splitChunks: {
+ chunks: 'all',
+ name: 'common',
+ minChunks: 2,
+ minSize: 0,
+},
    runtimeChunk: {
      name: 'runtime',
    },
  },
Copy the code

Perform NPX webpack

You can see that a common.js file is generated in the dist directory with the code for util.js, And dist/pages/index/index. Js and dist/pages/logs/logs. The first line of a js code is imported the common file:; require(‘./.. /.. /runtime’); require(‘./.. /.. /common’);

Tree Shaking

At present, the code we generate through NPX Webpack is uncompressed and optimized. If you don’t pay attention to it, it will exceed the 2M size limit of wechat.

Tree Shaking is a way to remove methods that we have never used before when generating dist code. To shake a dead branch off a tree is to remove useless code.

Configure the configuration according to the document. Do not expand the configuration here.

NPX webpack –mode=production

You can see that the generated app.js file is less than 1KB in size

Now, let’s introduce a big file

Modify the SRC /app.js file to reintroduce lodash

// app.js
+ import { camelCase } from 'lodash';
App({
  onLaunch: function () {
+ console.log('-----------------------------------------------x');
+ console.log(camelCase('OnLaunch'));}})Copy the code

NPX webpack –mode=production and you can see that the app.js file is nearly 70 KB in size, which is too expensive to use lodash. Fortunately, there is an optimization:

Start by installing the following two dependencies

npm i --save-dev babel-plugin-lodash lodash-webpack-plugin
Copy the code

Modify the webpack.config.js file

  const MinaRuntimePlugin = require('./plugin/MinaRuntimePlugin');
+ const LodashWebpackPlugin = require('lodash-webpack-plugin');

  new MinaRuntimePlugin(),
+ new LodashWebpackPlugin()
Copy the code

Modify the.babelrc file

{
  "presets": ["@babel/env"],
+ "plugins": ["lodash"]
}
Copy the code

Run NPX webpack –mode=production again and you can see that app.js is less than 4K in size, so we can happily use Lodash.

As for Moment, it’s still around 70K after optimization, which can be hard to swallow for a small program. There’s a project on Github called You Dont Need Moment.

Multi-environment configuration

The environment here is the server address of the applet, and our applet, when it’s developing, when it’s testing, when it’s publishing, the server address that needs to be accessed is different. We usually distinguish between development environments, test environments, pre-release environments, production environments, and so on.

Now let’s talk about mode, which is usually thought to be associated with multiple environment configurations.

We learned a little about mode in the Tree Shaking section.

Mode has three possible values, production, development, and None. Small programs cannot use development, so production and None are the only values.

When we see words like production and development, it’s easy to associate them with production environment and development environment, which can be misleading.

In addition to distinguishing between environments, we actually need to distinguish between build types (release, debug).

We should think of Mode as a build type configuration, not an environment configuration.

Build types and environments can be combined, such as debug packages for development, debug packages for production, release packages for production, and so on.

So the best practice is to use mode to decide whether to use compressed and optimized packages, and use EnvironmentPlugin to configure multiple environments.

Modify the webpack.config.js file

+ const webpack = require('webpack');
+ const debuggable = process.env.BUILD_TYPE ! == 'release'
module.exports = {
  plugins: [
+ new webpack.EnvironmentPlugin({
+ NODE_ENV: JSON.stringify(process.env.NODE_ENV) || 'development',
+ BUILD_TYPE: JSON.stringify(process.env.BUILD_TYPE) || 'debug',
+}).].- mode: 'none',
+ mode: debuggable ? 'none' : 'production',
}
Copy the code

By default, Webpack sets the value of process.env.node_env to the value of mode

It is customary to use NODE_ENV to distinguish environment types, as shown here

In our code, we can read these environment variables in the following ways

console.log(` environment:${process.env.NODE_ENV}Build type:${process.env.BUILD_TYPE}`)
Copy the code

npm scripts

How do we inject values for variables like NODE_ENV? We do this with NPM Scripts. The webPack documentation also provides an introduction to NPM Scripts, which you are advised to read.

First installation

npm i --save-dev cross-env
Copy the code

Modify the package.json file and add scripts

{
  "scripts": {
    "start": "webpack --watch --progress"."build": "cross-env NODE_ENV=production BUILD_TYPE=release webpack"}}Copy the code

Instead of using the NPX webpack –mode=production command, use the NPM run build command.

Use NPM start instead of the NPX webpack –watch –progress command we used earlier.

source mapping

Source mapping is used in development, to facilitate debugging, and in online, to locate which lines of code have problems.

Wechat mini program official description of source Map, click here to view.

Modify the webpack.config.js file

  mode: debuggable ? 'none' : 'production',
+ devtool: debuggable ? 'inline-source-map' : 'source-map',
Copy the code

Support Sass

Sass is a CSS preprocessor, but other CSS preprocessors can be used as well, so it’s easy to use sass as an example.

Installation-related Dependencies

npm i --save-dev sass-loader node-sass file-loader
Copy the code

Modify the webpack.config.js file

module.exports = {
  module: {
    rules: [
+ {
+ test: /\.(scss)$/,
+ include: /src/,
+ use: [
+ {
+ loader: 'file-loader',
+ options: {
+ useRelativePath: true,
+ name: '[path][name].wxss',
+ context: resolve('src'),
+},
+},
+ {
+ loader: 'sass-loader',
+ options: {
+ includePaths: [resolve('src', 'styles'), resolve('src')],
+},
+},
+],
+},
    ],
  },
  plugins: [
    new CopyWebpackPlugin([
      {
        from: '**/*',
        to: './',
- ignore: ['**/*.js', ],
+ ignore: ['**/*.js', '**/*.scss'],
      },
    ]),
    new MinaWebpackPlugin({
+ scriptExtensions: ['.js'],
+ assetExtensions: ['.scss'],})],}Copy the code

In the above configuration, we use file-Loader, which is a loader that outputs files directly to dist.

As mentioned in our analysis of the WebPack workflow, loader works primarily during the Module build phase. That is, we still need to add the.scss file as the entry so that the loader can have a chance to parse it and print the final result.

Each entry corresponds to a chunk, and each entry chunk outputs a file. Since the file-loader already helps us output the final result we want, we need to prevent this behavior.

Modify the plugin/MinaWebpackPlugin js file, the following is a modified

// plugin/MinaWebpackPlugin.js
const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin')
const MultiEntryPlugin = require('webpack/lib/MultiEntryPlugin')
const path = require('path')
const fs = require('fs')
const replaceExt = require('replace-ext')

const assetsChunkName = '__assets_chunk_name__'

function itemToPlugin(context, item, name) {
  if (Array.isArray(item)) {
    return new MultiEntryPlugin(context, item, name)
  }
  return new SingleEntryPlugin(context, item, name)
}

function _inflateEntries(entries = [], dirname, entry) {
  const configFile = replaceExt(entry, '.json')
  const content = fs.readFileSync(configFile, 'utf8')
  const config = JSON.parse(content) ; ['pages'.'usingComponents'].forEach(key= > {
    const items = config[key]
    if (typeof items === 'object') {
      Object.values(items).forEach(item= > inflateEntries(entries, dirname, item))
    }
  })
}

function inflateEntries(entries, dirname, entry) {
  entry = path.resolve(dirname, entry)
  if(entry ! =null && !entries.includes(entry)) {
    entries.push(entry)
    _inflateEntries(entries, path.dirname(entry), entry)
  }
}

function first(entry, extensions) {
  for (const ext of extensions) {
    const file = replaceExt(entry, ext)
    if (fs.existsSync(file)) {
      return file
    }
  }
  return null
}

function all(entry, extensions) {
  const items = []
  for (const ext of extensions) {
    const file = replaceExt(entry, ext)
    if (fs.existsSync(file)) {
      items.push(file)
    }
  }
  return items
}

class MinaWebpackPlugin {
  constructor(options = {}) {
    this.scriptExtensions = options.scriptExtensions || ['.ts'.'.js']
    this.assetExtensions = options.assetExtensions || []
    this.entries = []
  }

  applyEntry(compiler, done) {
    const { context } = compiler.options

    this.entries
      .map(item= > first(item, this.scriptExtensions))
      .map(item= > path.relative(context, item))
      .forEach(item= > itemToPlugin(context, '/' + item, replaceExt(item, ' ')).apply(compiler))

    // Put all the non-JS files into one entry and give it to the MultiEntryPlugin
    const assets = this.entries
      .reduce((items, item) = > [...items, ...all(item, this.assetExtensions)], [])
      .map(item= > '/' + path.relative(context, item))
    itemToPlugin(context, assets, assetsChunkName).apply(compiler)

    if (done) {
      done()
    }
  }

  apply(compiler) {
    const { context, entry } = compiler.options
    inflateEntries(this.entries, context, entry)

    compiler.hooks.entryOption.tap('MinaWebpackPlugin', () = > {this.applyEntry(compiler)
      return true
    })

    compiler.hooks.watchRun.tap('MinaWebpackPlugin', (compiler, done) => {
      this.applyEntry(compiler, done)
    })

    compiler.hooks.compilation.tap('MinaWebpackPlugin', compilation => {
      / / beforeChunkAssets events in compilation. CreateChunkAssets method before be triggered
      compilation.hooks.beforeChunkAssets.tap('MinaWebpackPlugin', () = > {const assetsChunkIndex = compilation.chunks.findIndex(({ name }) = > name === assetsChunkName)
        if (assetsChunkIndex > - 1) {
          // Remove the chunk so that it does not generate the corresponding asset and therefore does not output files
          // Without this step, an __assets_chunk_name__.js file will eventually be generated
          compilation.chunks.splice(assetsChunkIndex, 1)}})})}module.exports = MinaWebpackPlugin
Copy the code

Thanks for the following items and articles

Discuss webPack’s process section

mina-webpack

wxapp-webpack-plugin