Shall not be reproduced without authorization. For reprint, please contact the author. Original link: github.com/axuebin/art…

Writing in the front

Rax is a cross – end solution for Amoy.

If target is set to [‘web’, ‘weex’], the build product build directory will have two subdirectories: Web and WEEx, which will be consumed on the Web side and weex side respectively. And through observation, you can see that the contents of the two directories are not the same, and the code has been split according to the different environment. When the business logic is complex, the code volume is large, and the ability to split the code by side is a must.

However, there is no distinction between the Web and WEEx directories in the SRC directory. The code is written together and judged by environmental variables such as isWeex or isWeb.

Three questions to consider:

  1. How can one piece of code create two different environments (webweex)?
  2. In the construction phase, how to enablewebweexThere is no redundant code on the other side of the code?
  3. Why you need to useuniversal-envThe exportedisWeexisWebVariables that cannot be used directly in a projecttypeof WXEnvironmentTo judge?

This section uses weex as an example. Other terminals perform the sametarget: miniappThere will be an additional miniApp directory in the build product.

With these three questions in mind, let’s see if we can find the answers in the Rax source code.

Take a chestnut

If you haven’t written Rax before, the content in the foreword probably doesn’t feel like much. It doesn’t matter, just look at an example.

The code logic is very simple, the web to show Hello Web, weex to show Hello Weex:

import { createElement } from 'rax';
import View from 'rax-view';
import Text from 'rax-text';
import { isWeex } from 'universal-env';

export default function Home() {
  return (
    <View className="home">
      <Text className="title">{ isWeex ? 'hello weex' : 'hello web' }</Text>
    </View>
  );
}
Copy the code

Take a look at the build product:

├ ─ ─ build │ ├ ─ ─ web │ │ ├ ─ ─ index. The CSS │ │ ├ ─ ─ index. The HTML │ │ ├ ─ ─ index. The js │ │ ├ ─ ─ pages_home_index. The chunk. The CSS │ │ └ ─ ─ Pages_home_index.scient.js │ ├ ─ garbage ─ scient.jsCopy the code

weex/index.js

  function c() {
    return Object(r.createElement)(i.a, {
      className: "home"
    }, Object(r.createElement)(u.a, {
      className: "title"
    }, "hello weex"))}Copy the code

web/pages_home_index.chunk.js

function c() {
  return Object(n.createElement)(o.a, {
    className: "home"
  }, Object(n.createElement)(s.a, {
    className: "title"
  }, "hello web"))}Copy the code

Only Hello Weex and Hello Web were among the weex and Web builds, as we expected.

Source code analysis

Rax is also built based on the build-scripts architecture. If you don’t know about build-scripts, take a look at build-scripts

The basic webpack configuration inside build-scripts is generated through webpack-chain, which configudes the API for chain operations through Webpack, and can define specific Loader rules and the name of the WebPack plug-in. Allows developers to modify the WebPack configuration in a more granular manner.

After learning about build-scripts and its plugin architecture, let’s take a look at the core plugin of Rax App, build-plugin-rax-app, to solve the above three problems one by one. The code is here: build-plugin-rax-app, if you are interested, you can also take a look.

How does build-scripts build multiple artifacts

SRC = “build.js” SRC = “build.js” SRC = “build.js

// Only the code for building multiple artifacts is reserved below
module.exports = ({ onGetWebpackConfig, registerTask, context, onHook }, options = {}) = > {
  const { targets = [], type = 'spa' } = options;
  targets.forEach(async(target) => {
    if ([WEB, WEEX, KRAKEN].includes(target)) {
      const getBase = require(`./config/${target}/getBase`); registerTask(target, getBase(context, target, options)); }}); };Copy the code

The plug-in traversesbuild.jsonThe incomingtargetsFields that register multiple named nameswebpack Task, the default configuration is stored inconfigIn the corresponding directory.contrastweb/getBase.js 和 weex/getBase.jsThey all have Settingsoutput.outputConfiguration item controlwebpackHow to outputbundles.

It looks like registerTask just registers multiple copies of WebPack Config. How does it get consumed by WebPack? That comes down to the code in build-scripts.

/ / define registerTask
// Every time registerTask is run, a config is pushed into the configArr
this.registerTask = (name, chainConfig) = > {
  const exist = this.configArr.find((v) = > v.name === name);
  if(! exist) {this.
      .push({
      name,
      chainConfig,
      modifyFunctions: [],}); }else {
    throw new Error(`[Error] config '${name}' already exists! `); }};// Webpack uses config
const webpackConfig = configArr.map(v= > v.chainConfig.toConfig());
await applyHook(`before.${command}.run`, { args, config: webpackConfig });
let compiler;
try {
  // Pass webpack an array of configuration items
  compiler = webpackInstance(webpackConfig);
}
Copy the code

The webpack function is passed an array of configuration items that correspond to multiple registerTask-registered configuration items.

You must be wondering, how is this array of configuration items executed in WebPack? Is it a compilation process or a multi-compilation process? Is it executed serially or in parallel? These are all things you need to know if you want to care about build speed. I just didn’t know about it before, so LET’s take a look.

// webpack/lib/webpack.js
const webpack = (options, callback) = > {
	let compiler;
	if (Array.isArray(options)) {
		compiler = new MultiCompiler(
			Array.from(options).map(options= > webpack(options))
		);
	} else if (typeof options === "object") {
    / / options
  }
  / / callback
  if (callback) {
		compiler.run(callback);
	}
	return compiler;
}
Copy the code

The code looks as if multiple compilations will be performed, and the callback will be called after the multiple compilations. Are multiple compilations sequential or parallel?

Before jumping to conclusions, a sixth sense tells me to see what the MultiCompiler does first.

The MultiCompiler compiles multiple configurations

The following content involves the source of Webpack, to tell the truth, or the first time to see, if there is something wrong, trouble we must point out, thank you.

The  MultiCompiler module allows webpack to run multiple configurations in separate compilers. If the options parameter in the webpack’s NodeJS api is an array of options, webpack applies separate compilers and calls the callback after all compilers have been executed.

Note: Separate compilers are compiled separately and callback is only called when all compilers executed.

By the way, make fun of the Chinese documentation of Webpack. We still read more English documents, so as not to be misled.

This is wrong ❌

Is the MultiCompiler serial or parallel? The official website describes it as follows:

Multiple configurations will not be run in parallel. Each configuration is only processed after the previous one has finished processing.

Well, sequentially, each compilation is executed after the last compilation. The compilation of multiple Configs can be handled in parallel via parallel-webapck.

When using the rax-app build, you can see that the progress bars on both sides of the console are parallel.

This is how I understand it. The MultiCompiler mentioned above is serial, but Webpack is based on Tapable, and it executes the loader/ Plugin process asynchronously, which is equivalent to MultiCompiler only registering the compiler task. Internal processes run asynchronously at the same time. Take a look at the source code to verify:

Each compiler in MultiCompiler taps (registers) the MultiCompiler event, and when it’s done, the callback is executed.

As for Tapable, I won’t expand on it here (I don’t know much about it, dare not say), but if you are interested, you can have a look at Webpack/Tapable.

At this point, it’s clear how build-scripts builds multiple artifacts.

Summary: If we need to build more artifacts, we only need to pass them inside the plug-inregisterTaskRegister a new task and pass in the correspondingwepack configCan.

How does Rax App split code

As we know from the example above, the Code we need to remove is the Code (including unused modules) that will not be executed based on the end, that is, all Dead Code. Dead Code can be deleted in the construction phase in the following ways:

  1. babel-plugin-minify-dead-code-elimination:babel plugin, delete thedead code
  2. terser-webpack-plugin:webpack plugin, delete thedead code,console, comments etc.

DefinePlugin removes the code

A common approach is to define global variables via DefinePlugin, with Dead Code removed when Webpack is compressed.

Directly to the code, the most physical. Initialize a minimal Webpack demo.

Index. Js:

let hello = 'hello world';

if (isWeex) {
  hello = 'hello weex';
} else {
  hello = 'hello web';
}

export default hello;
Copy the code

webpack.config.js

const webpack = require('webpack');

module.exports = {
  mode: 'production'.optimization: {
    minimize: false
  },
  plugins: [
    new webpack.DefinePlugin({
      isWeex: true}})]Copy the code

Run webpack index.js –config webpack.config.js to get bundle.js:

([
  /* 0 */
  / * * * /
  (function (module, __webpack_exports__, __webpack_require__) {

    "use strict";
    __webpack_require__.r(__webpack_exports__);
    let hello = 'hello world';

    if (true) {
      console.log('aaa');
    } else {}

    /* harmony default export */
    __webpack_exports__["default"] = (hello);

    / * * * /
  })
  / * * * * * * /
]);
Copy the code

The global variable isWeex: true was defined in config via DefinePlugin, and we expected webPack to replace isWeex with the constant true in SRC /index.js code at build time. Bundle.js meets expectations.

With compress: true on, Webpack will remove dead code:

// The code has been formatted for easy reading
([function (e, t, r) {
  "use strict";
  r.r(t);
  let n = "hello world";
  n = "hello weex", t.default = n
}]);
Copy the code

So, we can accomplish end-to-end code splitting via build-scripts multi-product build capability + webpack.defineplugin, which might look something like this:

function getWebpackBase() {
	// Return to base config
}

// Get the config for each side of webpack
function getBase(target) {
  const config = getWebpackBase(target);
  let options = { isWeb: true.isWeex: false };
  if (target === 'weex') {
  	options = { isWeb: false.isWeex: true };
  }
	config
    .plugin('DefinePlugin')
    .use(webpack.DefinePlugin, [{
    	...options,
    }]);
}

/ / build - scripts plug-in
module.exports = ({ registerTask }) = > {
  const targets = ['weex'.'web'];
  targets.forEach(async(target) => {
    registerTask(target, getBase(target));
  });
};
Copy the code

Conclusion: YesDefinePluginRemove extraneous code at build time. According to the build-scripts multi-artifact build capability above, we can build products at build stage that remove each end of extraneous code.

However, as you can see from the above code, the disadvantage of defining global variables via DefinePlugin is that variable names such as isWeex and isWeb are defined directly in the WebPack configuration, which does not scale well and also needs to be used in actual business code, which seems too restrictive. This kind of convention, with the passage of time, may be slowly forgotten. So this plan is not very friendly in the long run.

Universal -env is required

The universal env package is a simple export of variables for each environment, as follows:

But it’s more than that, it’s a vital part of the whole cross-end system. As seen in the previous examples, the usual way to determine the environment in business code is to import {isWeex} from ‘univeral-env’. This approach is more scalable and maintainable than defining global variables in WebPack Config.

The problem now is how to split the code after exporting the environment variables using univeral-env.

Babel removes code

Since we can’t use webPack, the only way to change the code is through Babel to change the AST. Of course, today’s hero Rax does the same, using a platformLoader to replace variables such as isWeex with constants during the build phase.

This article focuses only on the core code (variables -> constants), but those who are interested can read the full code: platformLoader.

A mapping table is defined where the target variable is set to true.

  const platformMap = {
    weex: ['isWeex'].web: ['isWeb'].kraken: ['isKraken'.'isWeb'].node: ['isNode'].miniapp: ['isMiniApp'].'wechat-miniprogram': ['isWeChatMiniProgram']};Copy the code

The traverseImport code for manipulating AST is the same as it used to be with Babel.

  1. findImportDeclarationDetermine whetherimportuniversal-env
    • If yes, check for importimported.nameWhether or not it’s defined abovemap
      • If yes, create oneVariableDeclarationAdded to theASTAnd removeImportDeclaration

In addition to the general operation, here are two interesting points to mention:

Compatible with CommonJS code

If the code has been compiled, the CommonJS specification might already look like this:

var _universalEnv = require("universal-env");
if (_universalEnv.isWeex) {
  console.log('weex');
} else {
  console.log('web');
}
Copy the code

PlatformLoader handles this:

  1. Handling reference relationships: findCallExpression, replace it withobjectExpression
    • Replace before:var _universalEnv = require("universal-env")
    • After the replacement:var _universalEnv = {isWeex: false} 
  2. Processing value: foundMemberExpressionTo determineobject.nameWhether it is_universalEnv
    • If so, judgeproperty.nameWhether or not it’s defined abovemap
      • If yes, set totrueOtherwise, set tofalse
Compatible deconstruction alias

What if a variable is aliased when using universal-env?

import { isWeex as isWeexPlatform } from 'univeral-env';

if (isWeexPlatform) {}
Copy the code

PlatformLoader also does the following:

if(specObj.imported ! == specObj.local) { newNode = variableDeclarationMethod( specObj.local, newNodeInit, ); path.insertAfter(newNode); }Copy the code

In summary, platformLoader implements the two functions we need:

  1. removeuniversal-envRely on
  2. Replace environment variables with constants

Finally, the ability to delete Dead Code during Webpack compression can delete if(false) and other codes that will not be executed to achieve the function of splitting the code.

So, if we have a cross-end component that wants to be in purewebThe project can also be used throughplatformLoaderDo not worry about introducing redundant code to make cross-use of the underlying components.

conclusion

Read the above source analysis, the above three questions have been answered?

To summarize

  1. build-scriptsSupport for multipleconfigBuild capabilities throughregisterTaskYou can register multiplewebpack compilerThey are themselves a serial process.compilerAfter registration, it’s internalloaderEtc process compared to registrationcompilerIs asynchronous
  2. universal-envProvides unified environmental judgment variables inRaxIt plays a crucial role in the cross-end engineering system, and the variables derived from it must be used to judge the environment in the business code
  3. RaxIs provided through itplatformLoader + webpackThe ability to compress code to achieve end splitting can be leveraged in nonRaxProject to achieve cross-end components on demand packaging

recruitment

Ali international team infrastructure group recruitment front-end P6/P7, Base Hangzhou, infrastructure construction, business enabling… Lots of things to do.

Familiar with engineering/Node/ React… You can send your resume directly to [email protected], or add me to wechat for details on XB9207.