tsy77 / blog

78 stars 2 forks source link

Parcel 源码解读 #19

Open tsy77 opened 5 years ago

tsy77 commented 5 years ago

Version parcel-bundler: 1.11.0

Parcel中主要包含上述类:

它们直接的调用及继承关系如下:

打包流程

打包的整体过程就在Bundler.bundle()方法中,代码如下:

async bundle() {
    // If another bundle is already pending, wait for that one to finish and retry.
    if (this.pending) {
      return new Promise((resolve, reject) => {
        this.once('buildEnd', () => {
          this.bundle().then(resolve, reject);
        });
      });
    }

    ......

    logger.clear();
    logger.progress('Building...');

    try {
      // Start worker farm, watcher, etc. if needed
      await this.start();

      // Emit start event, after bundler is initialised
      this.emit('buildStart', this.entryFiles);

      // If this is the initial bundle, ensure the output directory exists, and resolve the main asset.
      if (isInitialBundle) {
        await fs.mkdirp(this.options.outDir);

        this.entryAssets = new Set();
        for (let entry of this.entryFiles) {
          try {
            let asset = await this.resolveAsset(entry);
            this.buildQueue.add(asset);
            this.entryAssets.add(asset);
          } catch (err) {
            throw new Error(
              `Cannot resolve entry "${entry}" from "${this.options.rootDir}"`
            );
          }
        }

        if (this.entryAssets.size === 0) {
          throw new Error('No entries found.');
        }

        initialised = true;
      }

      // Build the queued assets.
      let loadedAssets = await this.buildQueue.run();

      // The changed assets are any that don't have a parent bundle yet
      // plus the ones that were in the build queue.
      let changedAssets = [...this.findOrphanAssets(), ...loadedAssets];

      // Invalidate bundles
      for (let asset of this.loadedAssets.values()) {
        asset.invalidateBundle();
      }

      logger.progress(`Producing bundles...`);

      // Create a root bundle to hold all of the entry assets, and add them to the tree.
      this.mainBundle = new Bundle();
      for (let asset of this.entryAssets) {
        this.createBundleTree(asset, this.mainBundle);
      }

      // If there is only one child bundle, replace the root with that bundle.
      if (this.mainBundle.childBundles.size === 1) {
        this.mainBundle = Array.from(this.mainBundle.childBundles)[0];
      }

      // Generate the final bundle names, and replace references in the built assets.
      this.bundleNameMap = this.mainBundle.getBundleNameMap(
        this.options.contentHash
      );

      for (let asset of changedAssets) {
        asset.replaceBundleNames(this.bundleNameMap);
      }

      // Emit an HMR update if this is not the initial bundle.
      if (this.hmr && !isInitialBundle) {
        this.hmr.emitUpdate(changedAssets);
      }

      logger.progress(`Packaging...`);

      // Package everything up
      this.bundleHashes = await this.mainBundle.package(
        this,
        this.bundleHashes
      );

      ......

      return this.mainBundle;
    } catch (err) {

      ......

    } finally {
      this.pending = false;
      this.emit('buildEnd');

      // If not in watch mode, stop the worker farm so we don't keep the process running.
      if (!this.watcher && this.options.killWorkers) {
        await this.stop();
      }
    }
  }

这里主要做了如下几件事:

下面我们一步一步的讲解:

准备工作

准备工作主要在Bundler.start()中,代码如下:

async start() {
    if (this.farm) {
      return;
    }

    await this.loadPlugins();

    if (!this.options.env) {
      await loadEnv(Path.join(this.options.rootDir, 'index'));
      this.options.env = process.env;
    }

    this.options.extensions = Object.assign({}, this.parser.extensions);
    this.options.bundleLoaders = this.bundleLoaders;

    if (this.options.watch) {
      this.watcher = new Watcher();
      // Wait for ready event for reliable testing on watcher
      if (process.env.NODE_ENV === 'test' && !this.watcher.ready) {
        await new Promise(resolve => this.watcher.once('ready', resolve));
      }
      this.watcher.on('change', this.onChange.bind(this));
    }

    if (this.options.hmr) {
      this.hmr = new HMRServer();
      this.options.hmrPort = await this.hmr.start(this.options);
    }

    this.farm = await WorkerFarm.getShared(this.options, {
      workerPath: require.resolve('./worker.js')
    });
  }

这里主要做了如下几件事

加载Parcel插件

加载Parcel插件的代码如下:

async loadPlugins() {
    let relative = Path.join(this.options.rootDir, 'index');
    let pkg = await config.load(relative, ['package.json']);
    if (!pkg) {
      return;
    }

    try {
      let deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
      for (let dep in deps) {
        const pattern = /^(@.*\/)?parcel-plugin-.+/;
        if (pattern.test(dep)) {
          let plugin = await localRequire(dep, relative);
          await plugin(this);
        }
      }
    } catch (err) {
      logger.warn(err);
    }
  }

加载插件步骤如下:

注意,这里的localRequire就是一个代理模式,中间加入了缓存机制,控制了模块的访问。

监听文件变化和HMR后面会进行介绍。

构建Asset Tree

构建Asset Tree的主要逻辑在Bundler.Bundle()方法中,代码如下:

// If this is the initial bundle, ensure the output directory exists, and resolve the main asset.
      if (isInitialBundle) {
        await fs.mkdirp(this.options.outDir);

        this.entryAssets = new Set();
        for (let entry of this.entryFiles) {
          try {
            let asset = await this.resolveAsset(entry);
            this.buildQueue.add(asset);
            this.entryAssets.add(asset);
          } catch (err) {
            throw new Error(
              `Cannot resolve entry "${entry}" from "${this.options.rootDir}"`
            );
          }
        }

        if (this.entryAssets.size === 0) {
          throw new Error('No entries found.');
        }

        initialised = true;
      }

      // Build the queued assets.
      let loadedAssets = await this.buildQueue.run();

这里主要做了如下几件事:

Asset类

首先说明下Asset,Asset是文件资源类,与文件保持一对一的关系,Asset基类代码如下:

class Asset {
  constructor(name, options) {
    this.id = null;
    this.name = name;
    this.basename = path.basename(this.name);
    this.relativeName = path
      .relative(options.rootDir, this.name)
      .replace(/\\/g, '/');

    ......

    this.contents = options.rendition ? options.rendition.value : null;
    this.ast = null;
    this.generated = null;

    ......
  }

  shouldInvalidate() {
    return false;
  }

  async loadIfNeeded() {
    if (this.contents == null) {
      this.contents = await this.load();
    }
  }

  async parseIfNeeded() {
    await this.loadIfNeeded();
    if (!this.ast) {
      this.ast = await this.parse(this.contents);
    }
  }

  async getDependencies() {
    if (
      this.options.rendition &&
      this.options.rendition.hasDependencies === false
    ) {
      return;
    }

    await this.loadIfNeeded();

    if (this.contents && this.mightHaveDependencies()) {
      await this.parseIfNeeded();
      await this.collectDependencies();
    }
  }

  addDependency(name, opts) {
    this.dependencies.set(name, Object.assign({name}, opts));
  }

  addURLDependency(url, from = this.name, opts) {
    if (!url || isURL(url)) {
      return url;
    }

    if (typeof from === 'object') {
      opts = from;
      from = this.name;
    }

    const parsed = URL.parse(url);
    let depName;
    let resolved;
    let dir = path.dirname(from);
    const filename = decodeURIComponent(parsed.pathname);

    if (filename[0] === '~' || filename[0] === '/') {
      if (dir === '.') {
        dir = this.options.rootDir;
      }
      depName = resolved = this.resolver.resolveFilename(filename, dir);
    } else {
      resolved = path.resolve(dir, filename);
      depName = './' + path.relative(path.dirname(this.name), resolved);
    }

    this.addDependency(depName, Object.assign({dynamic: true, resolved}, opts));

    parsed.pathname = this.options.parser
      .getAsset(resolved, this.options)
      .generateBundleName();

    return URL.format(parsed);
  }

  ......

  parse() {
    // do nothing by default
  }

  collectDependencies() {
    // do nothing by default
  }

  async pretransform() {
    // do nothing by default
  }

  async transform() {
    // do nothing by default
  }

  async generate() {
    return {
      [this.type]: this.contents
    };
  }

  async process() {
    // Generate the id for this asset, unless it has already been set.
    // We do this here rather than in the constructor to avoid unnecessary work in the main process.
    // In development, the id is just the relative path to the file, for easy debugging and performance.
    // In production, we use a short hash of the relative path.
    if (!this.id) {
      this.id =
        this.options.production || this.options.scopeHoist
          ? md5(this.relativeName, 'base64').slice(0, 4)
          : this.relativeName;
    }

    if (!this.generated) {
      await this.loadIfNeeded();
      await this.pretransform();
      await this.getDependencies();
      await this.transform();
      this.generated = await this.generate();
    }

    return this.generated;
  }

  ......
}

这里主要关注下process方法,也就是文件的文件资源的处理过程:

注意,这里不同的子类会继承自此基类,实现基类暴露的接口,这其实就是针对接口编程的设计原则。

收集依赖的过程会在下面进行详细介绍。

Bundler.resolveAsset

根据文件后缀获取到入口文件对应的Asset实例的逻辑Bundler.resolveAsset中,代码如下:

  async resolveAsset(name, parent) {
    let {path} = await this.resolver.resolve(name, parent);
    return this.getLoadedAsset(path);
  }

  getLoadedAsset(path) {
    if (this.loadedAssets.has(path)) {
      return this.loadedAssets.get(path);
    }

    let asset = this.parser.getAsset(path, this.options);
    this.loadedAssets.set(path, asset);

    this.watch(path, asset);
    return asset;
  }

主要做了如下两件事:

这里简单说下Parser,Parser可以说是Asset的注册表,根据类型存储对应的Asset实例,parser.getAsset方法根据文件路径获取对应的Asset实例。

buildQueue.run

buildQueue是PromiseQueue的实例,PromiseQueue.run方法将对列中的内容一次通过process函数处理。PromiseQueue有兴趣大家可以去看下代码,这里不在赘述。

buildQueue的初始化代码在Bundler的constructor中,代码如下:

this.buildQueue = new PromiseQueue(this.processAsset.bind(this));

在我们上述的场景中,执行逻辑就是对所有的入口文件对应的Asset,执行Bundler.processAsset(Asset)

Bundler.processAsset()最终调用的是Bundler.loadAsset()方法,代码如下:

async loadAsset(asset) {
    ......

    if (!processed || asset.shouldInvalidate(processed.cacheData)) {
      processed = await this.farm.run(asset.name);
      cacheMiss = true;
    }

    ......

    // Call the delegate to get implicit dependencies
    let dependencies = processed.dependencies;
    if (this.delegate.getImplicitDependencies) {
      let implicitDeps = await this.delegate.getImplicitDependencies(asset);
      if (implicitDeps) {
        dependencies = dependencies.concat(implicitDeps);
      }
    }

    // Resolve and load asset dependencies
    let assetDeps = await Promise.all(
      dependencies.map(async dep => {
        if (dep.includedInParent) {
          // This dependency is already included in the parent's generated output,
          // so no need to load it. We map the name back to the parent asset so
          // that changing it triggers a recompile of the parent.
          this.watch(dep.name, asset);
        } else {
          dep.parent = asset.name;
          let assetDep = await this.resolveDep(asset, dep);
          if (assetDep) {
            await this.loadAsset(assetDep);
          }

          return assetDep;
        }
      })
    );

    // Store resolved assets in their original order
    dependencies.forEach((dep, i) => {
      asset.dependencies.set(dep.name, dep);
      let assetDep = assetDeps[i];
      if (assetDep) {
        asset.depAssets.set(dep, assetDep);
        dep.resolved = assetDep.name;
      }
    });

    logger.verbose(`Built ${asset.relativeName}...`);

    if (this.cache && cacheMiss) {
      this.cache.write(asset.name, processed);
    }
  }

这里主要做了如下几件事:

到此为止,Asset的树结构已经构建完成,构建的过程就是一个递归的操作,对本身进行process,然后递归的对其依赖进行process,最终形成asset tree。

注意,有一些细节点后面会进行详细介绍,比如上述this.farm是子进程管理的实例,可以利用多进程加快构建的速度;收集依赖的过程会根据文件类型的不同而不同。

this.farm也是一个代理模式的应用。

构建Bundle Tree

构建Bundle Tree的主要逻辑也在Bundler.Bundle()中,代码如下:

// Create a root bundle to hold all of the entry assets, and add them to the tree.
this.mainBundle = new Bundle();
for (let asset of this.entryAssets) {
this.createBundleTree(asset, this.mainBundle);
}

这里主要做了如下几件事:

Bundle类

Bundle类是文件束的类,每个Bundle表示一个大包后的文件,其中包含子assets、childBundle等属性,代码如下:

class Bundle {
  constructor(type, name, parent, options = {}) {
    this.type = type;
    this.name = name;
    this.parentBundle = parent;
    this.entryAsset = null;
    this.assets = new Set();
    this.childBundles = new Set();
    this.siblingBundles = new Set();
    this.siblingBundlesMap = new Map();

    ......

  }

  static createWithAsset(asset, parentBundle, options) {
    let bundle = new Bundle(
      asset.type,
      Path.join(asset.options.outDir, asset.generateBundleName()),
      parentBundle,
      options
    );

    bundle.entryAsset = asset;
    bundle.addAsset(asset);
    return bundle;
  }

  addAsset(asset) {
    asset.bundles.add(this);
    this.assets.add(asset);
  }

  ......

  getSiblingBundle(type) {
    if (!type || type === this.type) {
      return this;
    }

    if (!this.siblingBundlesMap.has(type)) {
      let bundle = new Bundle(
        type,
        Path.join(
          Path.dirname(this.name),
          // keep the original extension for source map files, so we have
          // .js.map instead of just .map
          type === 'map'
            ? Path.basename(this.name) + '.' + type
            : Path.basename(this.name, Path.extname(this.name)) + '.' + type
        ),
        this
      );

      this.childBundles.add(bundle);
      this.siblingBundles.add(bundle);
      this.siblingBundlesMap.set(type, bundle);
    }

    return this.siblingBundlesMap.get(type);
  }

  createChildBundle(entryAsset, options = {}) {
    let bundle = Bundle.createWithAsset(entryAsset, this, options);
    this.childBundles.add(bundle);
    return bundle;
  }

  createSiblingBundle(entryAsset, options = {}) {
    let bundle = this.createChildBundle(entryAsset, options);
    this.siblingBundles.add(bundle);
    return bundle;
  }

  ......

  async package(bundler, oldHashes, newHashes = new Map()) {
    let promises = [];
    let mappings = [];

    if (!this.isEmpty) {
      let hash = this.getHash();
      newHashes.set(this.name, hash);

      if (!oldHashes || oldHashes.get(this.name) !== hash) {
        promises.push(this._package(bundler));
      }
    }

    for (let bundle of this.childBundles.values()) {
      if (bundle.type === 'map') {
        mappings.push(bundle);
      } else {
        promises.push(bundle.package(bundler, oldHashes, newHashes));
      }
    }

    await Promise.all(promises);
    for (let bundle of mappings) {
      await bundle.package(bundler, oldHashes, newHashes);
    }
    return newHashes;
  }

  async _package(bundler) {
    let Packager = bundler.packagers.get(this.type);
    let packager = new Packager(this, bundler);

    let startTime = Date.now();
    await packager.setup();
    await packager.start();

    let included = new Set();
    for (let asset of this.assets) {
      await this._addDeps(asset, packager, included);
    }

    await packager.end();

    this.totalSize = packager.getSize();

    let assetArray = Array.from(this.assets);
    let assetStartTime =
      this.type === 'map'
        ? 0
        : assetArray.sort((a, b) => a.startTime - b.startTime)[0].startTime;
    let assetEndTime =
      this.type === 'map'
        ? 0
        : assetArray.sort((a, b) => b.endTime - a.endTime)[0].endTime;
    let packagingTime = Date.now() - startTime;
    this.bundleTime = assetEndTime - assetStartTime + packagingTime;
  }

  async _addDeps(asset, packager, included) {
    if (!this.assets.has(asset) || included.has(asset)) {
      return;
    }

    included.add(asset);

    for (let depAsset of asset.depAssets.values()) {
      await this._addDeps(depAsset, packager, included);
    }

    await packager.addAsset(asset);

    const assetSize = packager.getSize() - this.totalSize;
    if (assetSize > 0) {
      this.addAssetSize(asset, assetSize);
    }
  }

  ......
}

Bundler.createBundleTree

Bundler.createBundleTree()是创建Bundle tree的主要方法,其目的是将入口的asset加入到根bundle中,代码如下:

createBundleTree(asset, bundle, dep, parentBundles = new Set()) {
    if (dep) {
      asset.parentDeps.add(dep);
    }

    if (asset.parentBundle && !bundle.isolated) {
      // If the asset is already in a bundle, it is shared. Move it to the lowest common ancestor.
      if (asset.parentBundle !== bundle) {
        let commonBundle = bundle.findCommonAncestor(asset.parentBundle);

        // If the common bundle's type matches the asset's, move the asset to the common bundle.
        // Otherwise, proceed with adding the asset to the new bundle below.
        if (asset.parentBundle.type === commonBundle.type) {
          this.moveAssetToBundle(asset, commonBundle);
          return;
        }
      } else {
        return;
      }

      // Detect circular bundles
      if (parentBundles.has(asset.parentBundle)) {
        return;
      }
    }

    ......

    // If the asset generated a representation for the parent bundle type, and this
    // is not an async import, add it to the current bundle
    if (bundle.type && asset.generated[bundle.type] != null && !dep.dynamic) {
      bundle.addAsset(asset);
    }

    if ((dep && dep.dynamic) || !bundle.type) {
      // If the asset is already the entry asset of a bundle, don't create a duplicate.
      if (isEntryAsset) {
        return;
      }

      // Create a new bundle for dynamic imports
      bundle = bundle.createChildBundle(asset, dep);
    } else if (
      asset.type &&
      !this.packagers.get(asset.type).shouldAddAsset(bundle, asset)
    ) {
      // If the asset is already the entry asset of a bundle, don't create a duplicate.
      if (isEntryAsset) {
        return;
      }

      // No packager is available for this asset type, or the packager doesn't support
      // combining this asset into the bundle. Create a new bundle with only this asset.
      bundle = bundle.createSiblingBundle(asset, dep);
    } else {
      // Add the asset to the common bundle of the asset's type
      bundle.getSiblingBundle(asset.type).addAsset(asset);
    }

    // Add the asset to sibling bundles for each generated type
    if (asset.type && asset.generated[asset.type]) {
      for (let t in asset.generated) {
        if (asset.generated[t]) {
          bundle.getSiblingBundle(t).addAsset(asset);
        }
      }
    }

    asset.parentBundle = bundle;
    parentBundles.add(bundle);

    for (let [dep, assetDep] of asset.depAssets) {
      this.createBundleTree(assetDep, bundle, dep, parentBundles);
    }

    parentBundles.delete(bundle);
    return bundle;
  }

这里主要做了如下几件事:

这里需要注意的是如何判断是否重复打包呢?

 if (asset.parentBundle) {
   // If the asset is already in a bundle, it is shared. Move it to the lowest common ancestor.
   if (asset.parentBundle !== bundle) {
     let commonBundle = bundle.findCommonAncestor(asset.parentBundle);
     if (
       asset.parentBundle !== commonBundle &&
       asset.parentBundle.type === commonBundle.type
     ) {
       this.moveAssetToBundle(asset, commonBundle);
       return;
     }
   } else return;
 }

Package

打包(package)的入口逻辑Bundler.Bundle()中,代码如下:

// Package everything up
this.bundleHashes = await this.mainBundle.package(
    this,
    this.bundleHashes
);

这段代码就是调用了mainBundle.package方法,从根bundle开始进行打包

bundle.package

构建好bundle tree之后,从根bundle开始,递归的调用每个bundle的package方法,进行打包操作,Bundle.package()的代码如下:

async package(bundler, oldHashes, newHashes = new Map()) {
    let promises = [];
    let mappings = [];

    if (!this.isEmpty) {
      let hash = this.getHash();
      newHashes.set(this.name, hash);

      if (!oldHashes || oldHashes.get(this.name) !== hash) {
        promises.push(this._package(bundler));
      }
    }

    for (let bundle of this.childBundles.values()) {
      if (bundle.type === 'map') {
        mappings.push(bundle);
      } else {
        promises.push(bundle.package(bundler, oldHashes, newHashes));
      }
    }

    await Promise.all(promises);
    for (let bundle of mappings) {
      await bundle.package(bundler, oldHashes, newHashes);
    }
    return newHashes;
  }

这里主要做了如下几件事:

Packager

Packager根据bundle类型不同而有不同的Packager子类,使用者通过PackagerRegistry进行注册和获取某个类型的Packager。

基类代码如下:

class Packager {
  constructor(bundle, bundler) {
    this.bundle = bundle;
    this.bundler = bundler;
    this.options = bundler.options;
  }

  static shouldAddAsset() {
    return true;
  }

  async setup() {
    // Create sub-directories if needed
    if (this.bundle.name.includes(path.sep)) {
      await mkdirp(path.dirname(this.bundle.name));
    }

    this.dest = fs.createWriteStream(this.bundle.name);
    this.dest.write = promisify(this.dest.write.bind(this.dest));
    this.dest.end = promisify(this.dest.end.bind(this.dest));
  }

  async write(string) {
    await this.dest.write(string);
  }

  ......
}

我们主要关注其setupwrite方法即可,两个方法分别是创建文件写流、向文件中写入字符串。

子类的话我们以JSPackager为例,代码如下:

class JSPackager extends Packager {
  async start() {
    this.first = true;
    this.dedupe = new Map();
    this.bundleLoaders = new Set();
    this.externalModules = new Set();

    let preludeCode = this.options.minify ? prelude.minified : prelude.source;
    if (this.options.target === 'electron') {
      preludeCode =
        `process.env.HMR_PORT=${
          this.options.hmrPort
        };process.env.HMR_HOSTNAME=${JSON.stringify(
          this.options.hmrHostname
        )};` + preludeCode;
    }
    await this.write(preludeCode + '({');
    this.lineOffset = lineCounter(preludeCode);
  }

  async addAsset(asset) {
    // If this module is referenced by another JS bundle, it needs to be exposed externally.
    // In that case, don't dedupe the asset as it would affect the module ids that are referenced by other bundles.
    let isExposed = !Array.from(asset.parentDeps).every(dep => {
      let depAsset = this.bundler.loadedAssets.get(dep.parent);
      return this.bundle.assets.has(depAsset) || depAsset.type !== 'js';
    });

    if (!isExposed) {
      let key = this.dedupeKey(asset);
      if (this.dedupe.has(key)) {
        return;
      }

      // Don't dedupe when HMR is turned on since it messes with the asset ids
      if (!this.options.hmr) {
        this.dedupe.set(key, asset.id);
      }
    }

    ......

    this.bundle.addOffset(asset, this.lineOffset);
    await this.writeModule(
      asset.id,
      asset.generated.js,
      deps,
      asset.generated.map
    );
  }

  ......

  async end() {
    let entry = [];

    // Add the HMR runtime if needed.
    if (this.options.hmr) {
      let asset = await this.bundler.getAsset(
        require.resolve('../builtins/hmr-runtime')
      );
      await this.addAssetToBundle(asset);
      entry.push(asset.id);
    }

    if (await this.writeBundleLoaders()) {
      entry.push(0);
    }

    if (this.bundle.entryAsset && this.externalModules.size === 0) {
      entry.push(this.bundle.entryAsset.id);
    }

    await this.write(
      '},{},' +
        JSON.stringify(entry) +
        ', ' +
        JSON.stringify(this.options.global || null) +
        ')'
    );
    if (this.options.sourceMaps) {
      // Add source map url if a map bundle exists
      let mapBundle = this.bundle.siblingBundlesMap.get('map');
      if (mapBundle) {
        let mapUrl = urlJoin(
          this.options.publicURL,
          path.basename(mapBundle.name)
        );
        await this.write(`\n//# sourceMappingURL=${mapUrl}`);
      }
    }
    await super.end();
  }
}

这里主要关注上述几个方法:

周边技术点

如何收集依赖

我们在上述的Asset处理时,有一个步骤是收集依赖(collectDependencies),这个步骤根据不同的文件类型处理方式会有不同,我们下面以JSAsset为例讲解一下。

  1. 首先在pretransform阶段中,JSAsset利用@babel/core生成ast,代码在/transforms/babel/babel7.js中,
  let res;
  if (asset.ast) {
    res = babel.transformFromAst(asset.ast, asset.contents, config);
  } else {
    res = babel.transformSync(asset.contents, config);
  }

  if (res.ast) {
    asset.ast = res.ast;
    asset.isAstDirty = true;
  }
  1. 遍历AST中的每个节点,收集依赖

遍历AST的过程由babylon-walk进行控制,代码如下:

const walk = require('babylon-walk');

collectDependencies() {
    walk.ancestor(this.ast, collectDependencies, this);
}

其中collectDependencies对应的是babel visitors,简单来说,在遇到某类型的节点时,就会触发某类型的visitors,我们可以控制进入节点或退出节点的处理逻辑。

在看用于收集依赖的visitor之前,先了解下ES6 module和nodejs的模块系统的几种导入导出方式以及对应在抽象语法树中代表的declaration类型:

// ImportDeclaration
import { stat, exists, readFile } from 'fs';

// ExportNamedDeclaration with node.source = null;
export var year = 1958;

// ExportNamedDeclaration with node.source = null;
export default function () {
  console.log('foo');
}

// ExportNamedDeclaration with node.source.value = 'my_module';
export { foo, bar } from 'my_module';

// CallExpression with node.Callee.name is require;
// CallExpression with node.Callee.arguments[0] is the 'react';
import('react').then(...)

// CallExpression with node.Callee.name is require;
// CallExpression with node.Callee.arguments[0] is the 'react';
var react = require('react');

除了上述这些依赖引入方式之外,还有两种比较特殊的方式:

// web Worker
new Worker('sw.js')

// service worker
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw-test/sw.js', { scope: '/sw-test/' }).then(function(reg) {
    // registration worked
    console.log('Registration succeeded. Scope is ' + reg.scope);
  }).catch(function(error) {
    // registration failed
    console.log('Registration failed with ' + error);
  });
}

下面我们正式来看collectDependencies对应的viditors,代码如下:

module.exports = {
  ImportDeclaration(node, asset) {
    asset.isES6Module = true;
    addDependency(asset, node.source);
  },

  ExportNamedDeclaration(node, asset) {
    asset.isES6Module = true;
    if (node.source) {
      addDependency(asset, node.source);
    }
  },

  ExportAllDeclaration(node, asset) {
    asset.isES6Module = true;
    addDependency(asset, node.source);
  },

  ExportDefaultDeclaration(node, asset) {
    asset.isES6Module = true;
  },

  CallExpression(node, asset) {
    let {callee, arguments: args} = node;

    let isRequire =
      types.isIdentifier(callee) &&
      callee.name === 'require' &&
      args.length === 1 &&
      types.isStringLiteral(args[0]);

    if (isRequire) {
      addDependency(asset, args[0]);
      return;
    }

    let isDynamicImport =
      callee.type === 'Import' &&
      args.length === 1 &&
      types.isStringLiteral(args[0]);

    if (isDynamicImport) {
      asset.addDependency('_bundle_loader');
      addDependency(asset, args[0], {dynamic: true});

      node.callee = requireTemplate().expression;
      node.arguments[0] = argTemplate({MODULE: args[0]}).expression;
      asset.isAstDirty = true;
      return;
    }

    const isRegisterServiceWorker =
      types.isStringLiteral(args[0]) &&
      matchesPattern(callee, serviceWorkerPattern);

    if (isRegisterServiceWorker) {
      addURLDependency(asset, args[0]);
      return;
    }
  },

  NewExpression(node, asset) {
    const {callee, arguments: args} = node;

    const isWebWorker =
      callee.type === 'Identifier' &&
      callee.name === 'Worker' &&
      args.length === 1 &&
      types.isStringLiteral(args[0]);

    if (isWebWorker) {
      addURLDependency(asset, args[0]);
      return;
    }
  }
};

我们可以看到,每次遇到引入模块,就会调用addDependency,这里对动态引入(import())的处理稍微特殊一点,我们下面会详细介绍。

前端模块加载器

我们先来看一下构建好的js bundle的内容:

// modules are defined as an array
// [ module function, map of requires ]
//
// map of requires is short require name -> numeric require
//
// anything defined in a previous bundle is accessed via the
// orig method which is the require for previous bundles

// eslint-disable-next-line no-global-assign
parcelRequire = (function (modules, cache, entry, globalName) {
  // Save the require from previous bundle to this closure if any
  var previousRequire = typeof parcelRequire === 'function' && parcelRequire;
  var nodeRequire = typeof require === 'function' && require;

  function newRequire(name, jumped) {
    if (!cache[name]) {
      if (!modules[name]) {
        // if we cannot find the module within our internal map or
        // cache jump to the current global require ie. the last bundle
        // that was added to the page.
        var currentRequire = typeof parcelRequire === 'function' && parcelRequire;
        if (!jumped && currentRequire) {
          return currentRequire(name, true);
        }

        // If there are other bundles on this page the require from the
        // previous one is saved to 'previousRequire'. Repeat this as
        // many times as there are bundles until the module is found or
        // we exhaust the require chain.
        if (previousRequire) {
          return previousRequire(name, true);
        }

        // Try the node require function if it exists.
        if (nodeRequire && typeof name === 'string') {
          return nodeRequire(name);
        }

        var err = new Error('Cannot find module \'' + name + '\'');
        err.code = 'MODULE_NOT_FOUND';
        throw err;
      }

      localRequire.resolve = resolve;
      localRequire.cache = {};

      var module = cache[name] = new newRequire.Module(name);

      modules[name][0].call(module.exports, localRequire, module, module.exports, this);
    }

    return cache[name].exports;

    function localRequire(x){
      return newRequire(localRequire.resolve(x));
    }

    function resolve(x){
      return modules[name][1][x] || x;
    }
  }

  function Module(moduleName) {
    this.id = moduleName;
    this.bundle = newRequire;
    this.exports = {};
  }

  newRequire.isParcelRequire = true;
  newRequire.Module = Module;
  newRequire.modules = modules;
  newRequire.cache = cache;
  newRequire.parent = previousRequire;
  newRequire.register = function (id, exports) {
    modules[id] = [function (require, module) {
      module.exports = exports;
    }, {}];
  };

  for (var i = 0; i < entry.length; i++) {
    newRequire(entry[i]);
  }

  if (entry.length) {
    // Expose entry point to Node, AMD or browser globals
    // Based on https://github.com/ForbesLindesay/umd/blob/master/template.js
    var mainExports = newRequire(entry[entry.length - 1]);

    // CommonJS
    if (typeof exports === "object" && typeof module !== "undefined") {
      module.exports = mainExports;

    // RequireJS
    } else if (typeof define === "function" && define.amd) {
     define(function () {
       return mainExports;
     });

    // <script>
    } else if (globalName) {
      this[globalName] = mainExports;
    }
  }

  // Override the current require with this new one
  return newRequire;
})({"a.js":[function(require,module,exports) {
var name = 'tsy'; // console.log(Buffer);

module.exports = name;
},{}],"index.js":[function(require,module,exports) {
var a = require('./a.js');

console.log(a);
},{"./a.js":"a.js"}]},{},["index.js"], null)
//# sourceMappingURL=/parcel-demo.e31bb0bc.js.map

我们可以看到这是一个立即执行的函数,参数有modulescacheentryglobalName

{"a.js":[function(require,module,exports) {
var name = 'tsy'; // console.log(Buffer);

module.exports = name;
},{}],"index.js":[function(require,module,exports) {
var a = require('./a.js');

console.log(a);
},{"./a.js":"a.js"}]}

下面我们来看下该立即执行函数的主要逻辑是便利入口文件,调用newRequire方法:

function newRequire(name, jumped) {
    if (!cache[name]) {
      if (!modules[name]) {
        // if we cannot find the module within our internal map or
        // cache jump to the current global require ie. the last bundle
        // that was added to the page.
        var currentRequire = typeof parcelRequire === 'function' && parcelRequire;
        if (!jumped && currentRequire) {
          return currentRequire(name, true);
        }

        // If there are other bundles on this page the require from the
        // previous one is saved to 'previousRequire'. Repeat this as
        // many times as there are bundles until the module is found or
        // we exhaust the require chain.
        if (previousRequire) {
          return previousRequire(name, true);
        }

        // Try the node require function if it exists.
        if (nodeRequire && typeof name === 'string') {
          return nodeRequire(name);
        }

        var err = new Error('Cannot find module \'' + name + '\'');
        err.code = 'MODULE_NOT_FOUND';
        throw err;
      }

      localRequire.resolve = resolve;
      localRequire.cache = {};

      var module = cache[name] = new newRequire.Module(name);

      modules[name][0].call(module.exports, localRequire, module, module.exports, this);
    }

    return cache[name].exports;

    function localRequire(x){
      return newRequire(localRequire.resolve(x));
    }

    function resolve(x){
      return modules[name][1][x] || x;
    }
}

function Module(moduleName) {
    this.id = moduleName;
    this.bundle = newRequire;
    this.exports = {};
}

每一个文件就是一个模块,在每个模块中,都会有一个module对象,这个对象就指向当前的模块。Parcel中的module对象具有以下属性:

newRequire方法的逻辑如下:

在执行模块时,会将localRequire, module, module.exports作为形参,我们在模块中可以直接使用的requiremoduleexports即为执行该模块时传入的对应参数。

总结一下,我们利用函数把一个个模块封装起来,并给其提供 require和exports 的接口和一套模块规范,这样在不支持模块机制的浏览器环境中,我们也能够不去污染全局变量,体验到模块化带来的优势。

动态引入

我们接着来看动态引入,在上面JSAsset的collectDependencies中,已经有所提及。

我们首先看下在js遍历节点的过程中,遇到动态引入的情况如何处理:

if (isDynamicImport) {
  asset.addDependency('_bundle_loader');

  addDependency(asset, args[0], {dynamic: true});

  node.callee = requireTemplate().expression;
  node.arguments[0] = argTemplate({MODULE: args[0]}).expression;
  asset.isAstDirty = true;
  return;
}

这里我们可以看出,如果碰到Import()导入的资源, 直接将_bundle_loader加入其依赖列表,同时对表达式进行处理。根据上面代码,在ast中如果遇到import('./a.js')这段动态引入的代码, 会被直接替换为require('_bundle_loader')(require.resolve('./a.js'))

这里插一段背景,这种动态资源由于设置了dynamic: true,在后见bundle tree的时候,会单独生成一个bundle作为当前bundle的child bundle,同时在当前bundle中记录动态资源的信息。最后在当前的bundle中得到的打包资源数组,比如[md5(dynamicAsset).js, md5(cssWithDynamicAsset).css, ..., assetId], 由打包之后的文件名和该模块的id所组成.

根据上述前端模块加载器部分的介绍,require.resolve('./a.js')实际上获取的是./a.js模块的id,代码如下:

function resolve(x){
   return modules[name][1][x] || x;
}

_bundle_loader是Parcel-bundler的内置模块,位于/src/builtins/bundle-loader.js中,代码如下:

var getBundleURL = require('./bundle-url').getBundleURL;

function loadBundlesLazy(bundles) {
  if (!Array.isArray(bundles)) {
    bundles = [bundles]
  }

  var id = bundles[bundles.length - 1];

  try {
    return Promise.resolve(require(id));
  } catch (err) {
    if (err.code === 'MODULE_NOT_FOUND') {
      return new LazyPromise(function (resolve, reject) {
        loadBundles(bundles.slice(0, -1))
          .then(function () {
            return require(id);
          })
          .then(resolve, reject);
      });
    }

    throw err;
  }
}

function loadBundles(bundles) {
  return Promise.all(bundles.map(loadBundle));
}

var bundleLoaders = {};
function registerBundleLoader(type, loader) {
  bundleLoaders[type] = loader;
}

module.exports = exports = loadBundlesLazy;
exports.load = loadBundles;
exports.register = registerBundleLoader;

var bundles = {};
function loadBundle(bundle) {
  var id;
  if (Array.isArray(bundle)) {
    id = bundle[1];
    bundle = bundle[0];
  }

  if (bundles[bundle]) {
    return bundles[bundle];
  }

  var type = (bundle.substring(bundle.lastIndexOf('.') + 1, bundle.length) || bundle).toLowerCase();
  var bundleLoader = bundleLoaders[type];
  if (bundleLoader) {
    return bundles[bundle] = bundleLoader(getBundleURL() + bundle)
      .then(function (resolved) {
        if (resolved) {
          module.bundle.register(id, resolved);
        }

        return resolved;
      }).catch(function(e) {
        delete bundles[bundle];

        throw e;
      });
  }
}

function LazyPromise(executor) {
  this.executor = executor;
  this.promise = null;
}

LazyPromise.prototype.then = function (onSuccess, onError) {
  if (this.promise === null) this.promise = new Promise(this.executor)
  return this.promise.then(onSuccess, onError)
};

LazyPromise.prototype.catch = function (onError) {
  if (this.promise === null) this.promise = new Promise(this.executor)
  return this.promise.catch(onError)
};

其中loadBundlesLazy方法首先直接去require模块,如果没有的话,调用loadBundles加载后再去require。

loadBundles方法对每个模块调用loadBundle方法,loadBundle根据bundle类型获取相应的loader动态加载对应的bundle(被动态引入的模块会作为一个新的bundle),加载完成后注册到该bundle的modules中,这样后面的require就可以利用modules[name]获取到该模块了。

bundler loader在上述bundle的package.end()中将注册bundler loader的逻辑写入bundle,代码如下(JSPackager为例):

// Generate a module to register the bundle loaders that are needed
    let loads = 'var b=require(' + JSON.stringify(bundleLoader.id) + ');';
    for (let bundleType of this.bundleLoaders) {
      let loader = this.options.bundleLoaders[bundleType];
      if (loader) {
        let target = this.options.target === 'node' ? 'node' : 'browser';
        let asset = await this.bundler.getAsset(loader[target]);
        await this.addAssetToBundle(asset);
        loads +=
          'b.register(' +
          JSON.stringify(bundleType) +
          ',require(' +
          JSON.stringify(asset.id) +
          '));';
      }
    }

这段代码最终会在在modules中加入:

0:[function(require,module,exports) {
var b=require("../parcel/packages/core/parcel-bundler/src/builtins/bundle-loader.js");b.register("js",require("../parcel/packages/core/parcel-bundler/src/builtins/loaders/browser/js-loader.js"));
},{}]

同时将0这个模块加入到bundle的入口中(开始就会执行),这样在loadBundle就可以获取到对应的loader用于动态加载模块,以js-loader为例:

module.exports = function loadJSBundle(bundle) {
  return new Promise(function (resolve, reject) {
    var script = document.createElement('script');
    script.async = true;
    script.type = 'text/javascript';
    script.charset = 'utf-8';
    script.src = bundle;
    script.onerror = function (e) {
      script.onerror = script.onload = null;
      reject(e);
    };

    script.onload = function () {
      script.onerror = script.onload = null;
      resolve();
    };

    document.getElementsByTagName('head')[0].appendChild(script);
  });
};

在加载完资源后,我们又利用了module.bundle.register(id, resolved);注册到当前bundle的modules中,注册的代码在前端模块加载那里已经提及,代码如下:

newRequire.register = function (id, exports) {
    modules[id] = [function (require, module) {
      module.exports = exports;
    }, {}];
};

这样,我们利用require就可以直接获取到动态加载的资源了。

Worker

Parcel利用子进程来加快构建Asset Tree的速度,特别是编译生成AST的阶段。其最终调用的是node的child_process,但前面还有一些进程管理的工作,我们下面来探究一下。

worker在/src/bundler.js中load asset(this.farm.run())时使用,在start中被定义,我们来看下如何定义:

this.farm = await WorkerFarm.getShared(this.options, {
      workerPath: require.resolve('./worker.js')
});

这里传入了一些配置参数和workerPath,workerPath对应的模块中实现了initrun接口,后面在worker中会被使用,这也是面向接口编程的体现。

worker主要的代码在@parcel/workers中,worker中重要有三个类,WorkerFarmWorkerChild

这里的父进程向子进程发送命令,应用了设计模式中的命令模式

监听文件变化

监听文件变化同样是根据子进程对文件进行监听,但这里的子进程管理就比较简单了,创建一个子进程,然后发动命令就可以了,子进程中通过chokidar对文件进行监听,如果发现文件变化,发送消息给父进程,父进程出发相应的事件。

handleEmit(event, data) {
    if (event === 'watcherError') {
      data = errorUtils.jsonToError(data);
    }

    this.emit(event, data);
}

HMR

HMR通过WebSocket来实现,具有服务端和客户端两部分逻辑。

服务端逻辑(/src/HMRServer.js):

class HMRServer {
  async start(options = {}) {
    await new Promise(async resolve => {
      if (!options.https) {
        this.server = http.createServer();
      } else if (typeof options.https === 'boolean') {
        this.server = https.createServer(generateCertificate(options));
      } else {
        this.server = https.createServer(await getCertificate(options.https));
      }

      let websocketOptions = {
        server: this.server
      };

      if (options.hmrHostname) {
        websocketOptions.origin = `${options.https ? 'https' : 'http'}://${
          options.hmrHostname
        }`;
      }

      this.wss = new WebSocket.Server(websocketOptions);
      this.server.listen(options.hmrPort, resolve);
    });

    this.wss.on('connection', ws => {
      ws.onerror = this.handleSocketError;
      if (this.unresolvedError) {
        ws.send(JSON.stringify(this.unresolvedError));
      }
    });

    this.wss.on('error', this.handleSocketError);

    return this.wss._server.address().port;
  }

  ......

  emitUpdate(assets) {
    if (this.unresolvedError) {
      this.unresolvedError = null;
      this.broadcast({
        type: 'error-resolved'
      });
    }

    const shouldReload = assets.some(asset => asset.hmrPageReload);
    if (shouldReload) {
      this.broadcast({
        type: 'reload'
      });
    } else {
      this.broadcast({
        type: 'update',
        assets: assets.map(asset => {
          let deps = {};
          for (let [dep, depAsset] of asset.depAssets) {
            deps[dep.name] = depAsset.id;
          }

          return {
            id: asset.id,
            generated: asset.generated,
            deps: deps
          };
        })
      });
    }
  }

  ......

  broadcast(msg) {
    const json = JSON.stringify(msg);
    for (let ws of this.wss.clients) {
      ws.send(json);
    }
  }
}

这里的start方法用来创建WebSocket server,当有asset更新时,触发emitUpdate将asset id、asset 内容发送给客户端。

客户端逻辑:

var OVERLAY_ID = '__parcel__error__overlay__';

var OldModule = module.bundle.Module;

function Module(moduleName) {
  OldModule.call(this, moduleName);
  this.hot = {
    data: module.bundle.hotData,
    _acceptCallbacks: [],
    _disposeCallbacks: [],
    accept: function (fn) {
      this._acceptCallbacks.push(fn || function () {});
    },
    dispose: function (fn) {
      this._disposeCallbacks.push(fn);
    }
  };

  module.bundle.hotData = null;
}

module.bundle.Module = Module;

var parent = module.bundle.parent;
if ((!parent || !parent.isParcelRequire) && typeof WebSocket !== 'undefined') {
  var hostname = process.env.HMR_HOSTNAME || location.hostname;
  var protocol = location.protocol === 'https:' ? 'wss' : 'ws';
  var ws = new WebSocket(protocol + '://' + hostname + ':' + process.env.HMR_PORT + '/');
  ws.onmessage = function(event) {
    var data = JSON.parse(event.data);

    if (data.type === 'update') {
      console.clear();

      data.assets.forEach(function (asset) {
        hmrApply(global.parcelRequire, asset);
      });

      data.assets.forEach(function (asset) {
        if (!asset.isNew) {
          hmrAccept(global.parcelRequire, asset.id);
        }
      });
    }

    if (data.type === 'reload') {
      ws.close();
      ws.onclose = function () {
        location.reload();
      }
    }

    if (data.type === 'error-resolved') {
      console.log('[parcel] ✨ Error resolved');

      removeErrorOverlay();
    }

    if (data.type === 'error') {
      console.error('[parcel] 🚨  ' + data.error.message + '\n' + data.error.stack);

      removeErrorOverlay();

      var overlay = createErrorOverlay(data);
      document.body.appendChild(overlay);
    }
  };
}

......

function hmrApply(bundle, asset) {
  var modules = bundle.modules;
  if (!modules) {
    return;
  }

  if (modules[asset.id] || !bundle.parent) {
    var fn = new Function('require', 'module', 'exports', asset.generated.js);
    asset.isNew = !modules[asset.id];
    modules[asset.id] = [fn, asset.deps];
  } else if (bundle.parent) {
    hmrApply(bundle.parent, asset);
  }
}

function hmrAccept(bundle, id) {
  var modules = bundle.modules;
  if (!modules) {
    return;
  }

  if (!modules[id] && bundle.parent) {
    return hmrAccept(bundle.parent, id);
  }

  var cached = bundle.cache[id];
  bundle.hotData = {};
  if (cached) {
    cached.hot.data = bundle.hotData;
  }

  if (cached && cached.hot && cached.hot._disposeCallbacks.length) {
    cached.hot._disposeCallbacks.forEach(function (cb) {
      cb(bundle.hotData);
    });
  }

  delete bundle.cache[id];
  bundle(id);

  cached = bundle.cache[id];
  if (cached && cached.hot && cached.hot._acceptCallbacks.length) {
    cached.hot._acceptCallbacks.forEach(function (cb) {
      cb();
    });
    return true;
  }

  return getParents(global.parcelRequire, id).some(function (id) {
    return hmrAccept(global.parcelRequire, id)
  });
}

这里主要创建了Websocket Client,监听update消息,如果有,则替换modules中的对应内容,同时利用global.parcelRequire重新执行模块。