Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parcel 源码解读 #19

Open
tsy77 opened this issue Jan 30, 2019 · 0 comments
Open

Parcel 源码解读 #19

tsy77 opened this issue Jan 30, 2019 · 0 comments

Comments

@tsy77
Copy link
Owner

tsy77 commented Jan 30, 2019

Version
parcel-bundler: 1.11.0

Parcel中主要包含上述类:

  • Bundler,打包逻辑的入口
  • Parser,Asset的注册表,根据文件后缀查找并创建对应的Asset类
  • Asset,文件资源类,负责自身资源处理、依赖收集等操作,同时记录着原始资源、打包结果等信息;HTMLAsset、JSAsset等资源的Asset继承自此基类
  • Bundle,打包输出文件类,它由多个资源(Asset)组成,会根据当前Bundle类的类型查找对应的打包器(从PackagerRegistry中获取),调用打包器的package方法将自身包含的Asset打包进目的文件;bundle可以有子bundle,当动态从该bundle导入文件的时候,或者导入一个其他类型资源的文件的时候会产生childBundles
  • PackagerRegistry,Packager注册表,根据资源类型(基本上是Bundle在调用,所以基本上是Bundle的类型,也可以说是对应Asset的类型)注册、获取打包器(Packager)
  • Packager,打包组合类,用于将各个Asset产生的结果打包进目标文件,比如JSPackager将类型为JS的Asset产生的内容,打包以Bundle.name为名字的文件中
  • HMRServer,热更服务,其中包含启动ws服务,触发update等方法
  • FSCache,缓存
  • Resolver,资源路径解析类,如何对代码中引入的各种相对路径的资源路径进行解析,从而找到该模块的绝对路径

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

  • Bundler作为打包的入口,其中包含有Parser、Bundle、HMRServer、FSCache、Resolver等类
  • 构建的第一阶段,Bundler类调用Parser类获取文件对应的Asset,然后调用对应Asset的process等方法,取得Asset树
  • 构建的第二阶段,Bundler类中实例化根Bundle(初始空bundle),根据第一阶段中Asset的依赖信息,构建Bundle树
  • 构建的第三阶段,调用根Bundle类中的package方法,根据Bundle树进行文件写入等操作
  • Asset和Packager为基类,对应类型的(HTML、JS等)类继承自此基类

打包流程

打包的整体过程就在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();
      }
    }
  }

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

  • 准备工作,加载插件等
  • 根据入口文件及其依赖构建Asset Tree
  • 根据Asset Tree构建Bundle Tree
  • 根据Bundle Tree进行Package操作

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

准备工作

准备工作主要在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插件
  • 监听文件变化(可选)
  • 启动HMR服务(可选)

加载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);
    }
  }

加载插件步骤如下:

  • 读取根目录上的package.json
  • 循环遍历dependencies和devDependencies
    • 查找其中满足parcel-plugin-格式的依赖
    • 调用localRequire方法进行加载,localRequire获取到文件路径并缓存,然后做require操作(如果没有安装该npm包,则会调用npm / yarm install进行安装)。localRequire可以说是一个代理模式,代理了对文件的访问
    • 执行插件

注意,这里的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实例加入到buildQueue中
  • 执行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方法,也就是文件的文件资源的处理过程:

  • loadIfNeeded,加载文件内容
  • pretransform,预处理,比如js资源会用babel()进行转换
  • getDependencies, 这里主要对资源字符串进行解析,例如html字符串用posthtml-parser, js资源用babylon.parse来解析。然后收集依赖collectDependencies,具体操作稍后分析。
  • transform, 资源转换步骤接收 AST并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。
  • generate,产出一份处理后的文件内容,基本返回的数据格式是[this.type]: this.contents
  • generateHash,根据处理后的文件内容,产出对应hash值

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

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

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;
  }

主要做了如下两件事:

  • 利用Resolver类,获取到文件的绝对路径
  • 利用Parser类,根据文件的后缀获取到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);
    }
  }

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

  • this.farm.run(asset.name),其实就是调用了/src/pipeline.js中Pipeline类的processAsset方法,执行asset.process()对asset进行处理
  • 对该Asset的依赖执行resolveDepthis.loadAsset(assetDep),获取依赖的asset
  • 将所有依赖的asset放在asset.depAssets中进行记录

到此为止,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
  • 利用this.createBundleTree方法将所有的入口asset加入到根bundle中

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);
    }
  }

  ......
}
  • Bundle具有assets、childBundle等属性,同时拥有addAsset方法来注册asset,createChildBundle方法用来创建子bundle来构建bundle tree。
  • Bundle出了具有构建bundle tree能力外,还有package方法,可以递归的调用bundle tree中各个bundle的package方法,进行打包操作

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;
  }

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

  • 处理重复打包,如果重复则走另外一块逻辑,下面详细介绍
  • 如果bundle的类型在asset.generated中有对应项并且文件不是动态引入的,将asset加入到bundle的assets属性中
  • 如果文件是动态引入的或者是初始的根bundle(没有type),创建一个子bundle来容纳该asset,同时将当前bundle赋值为新创建的子bundle
  • 将asset.generated其他类型的产出加入到该bundle的兄弟bundle中
  • 遍历asset的依赖depAsset,递归的创建bundle tree,同时将当前bundle作为根bundle传入到Bundler.createBundleTree

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

 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;
 }
  • 如果一个资源的parentBundle已经存在但是不等于此次正在对它进行打包的bundle,那么将其转移到最近的公共父bundle中,避免一份代码重复的打包到了两份bundle中
  • 如果一个资源的parentBundle已经存在并且等于此次正在对它进行打包的bundle,说明他已经被打包过了,则直接跳过接下来的打包程序。

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;
  }

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

  • 获取bundle的hash值(利用bundle中包含的asset的hash值来获取),只有在旧的hash值不存在或者新的hash值不等于旧的hash值的时候,才进行package操作
  • 从根节点开始,递归的调用每个bundle的package方法进行打包操作
    • 根据bundle类型(打包文件类型)找到对应的打包资源处理类(Packager),然后调用Packager.addAsset(asset)方法将asset generate出的内容写入目标文件流
    • 每个bundle实例都会生成一个最终的打包文件

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();
  }
}

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

  • start,将预设的前端模块加载器(后面会详述)代码写入目标文件
  • addAsset,将asset.generated.js及其依赖模块的id按模块加载器所需格式写入目标文件
  • end,将hmr所需的客户端代码和sourceMaps url写入目标文件,对于动态引入的模块,需要把响应的loader注册代码写入文件。

周边技术点

如何收集依赖

我们在上述的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

  • modules为当前bandle中包含的所有模块,也就是上面提到的Bundle类中的assets,modules的类型为一个对象,key是模块名称,value是一个数组,数组第一项为包装过的模块内容,第二项是依赖的模块信息。比如如下内容
{"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"}]}
  • entry为该bundle的入口文件

下面我们来看下该立即执行函数的主要逻辑是便利入口文件,调用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对象具有以下属性:

  • id:当前模块的名称
  • bundle:newRequire方法
  • exports:当前模块暴露给外部的值

newRequire方法的逻辑如下:

  • 判断模块对象是否已被缓存
    • 如果是,直接return cache[name].exports
    • 如果没有,判断modules[name]是否存在
      • 如果存在,调用var module = cache[name] = new newRequire.Module(name); modules[name][0].call(module.exports, localRequire, module, module.exports, this);,缓存模块对象,并执行该模块
      • 如果不存在,则一次尝试调用其他bundle的parcelRequire(previousRequire)、node的require

在执行模块时,会将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

  • WorkerFarm是worker的入口,用来管理所有的子进程
  • Worker类用来管理单个子进程,具有fork、回调处理等能力
  • Child为子进程中执行的模块,在其中通过IPC 通信信道来接受父进程发送的命令,执行对应对应模块的方法,我们这里就是执行./worker.js中的对应方法,执行后通过信道将结果传递给父进程Worker

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

监听文件变化

监听文件变化同样是根据子进程对文件进行监听,但这里的子进程管理就比较简单了,创建一个子进程,然后发动命令就可以了,子进程中通过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重新执行模块。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant