深切 Parcel--架构与流程篇

《深切 Parcel--架构与流程篇》

本篇文章是对 Parce 的源码剖析,代码基础架构与实行流程,带你相识打包东西的内部道理,在这之前你假如对 parcel 不熟悉能够先到 Parcel官网 相识

引见

下面是偷懒从官网抄下来的引见:

极速零设置Web运用打包东西

  • 极速打包

Parcel 运用 worker 历程去启用多核编译。同时有文件体系缓存,纵然在重启构建后也能疾速再编译。

  • 将你一切的资本打包

Parcel 具有开箱即用的对 JS, CSS, HTML, 文件 及更多的支撑,而且不须要插件。

  • 自动转换

如若有须要,Babel, PostCSS, 和 PostHTML 以至 node_modules 包会被用于自动转换代码.

  • 零设置代码分拆

运用动态 import() 语法, Parcel 将你的输出文件束(bundles)分拆,因而你只须要在首次加载时加载你所须要的代码。

  • 热模块替代

Parcel 无需设置,在开辟环境的时刻会自动在浏览器内跟着你的代码变动而去更新模块。

  • 友爱的毛病日记

当碰到毛病时,Parcel 会输出 语法高亮的代码片断,协助你定位题目。

打包东西时候
browserify22.98s
webpack20.71s
parcel9.98s
parcel – with cache2.64s

打包东西

我们经常使用的打包东西大抵功用:

  • 模块化(代码的拆分, 兼并, Tree-Shaking 等)
  • 编译(es6,7,8 sass typescript 等)
  • 紧缩 (js, css, html包括图片的紧缩)
  • HMR (热替代)

version

parcel-bundler 版本:

“version”: “1.11.0”

文件架构

|-- assets          资本目次 继续自 Asset.js
|-- builtins        用于终究构建
|-- packagers       打包
|-- scope-hoisting  作用域提拔 Tree-Shake
|-- transforms      转换代码为 AST
|-- utils           东西
|-- visitors        遍历 js AST树 网络依靠等

|-- Asset.js          资本
|-- Bundle.js         用于构建 bundle 树
|-- Bundler.js        主目次  
|-- FSCache.js        缓存
|-- HMRServer.js      HMR效劳器供应 WebSocket
|-- Parser.js         依据文件扩大名猎取对应 Asset
|-- Pipeline.js       多线程实行要领
|-- Resolver.js       剖析模块途径
|-- Server.js         静态资本效劳器
|-- SourceMap.js      SourceMap
|-- cli.js            cli进口 剖析敕令行参数
|-- worker.js         多线程进口

流程

申明

Parcel是面向资本的,JavaScript,CSS,HTML 这些都是资本,并非 webpackjs 是一等国民,Parcel 会自动的从进口文件最先剖析这些文件 和 模块中的依靠,然后构建一个 bundle 树,并对其举行打包输出到指定目次

一个简朴的例子

我们从一个简朴的例子最先相识 parcel 内部源码与流程

index.html
  |-- index.js
    |-- module1.js
    |-- module2.js

上面是我们例子的构造,进口为 index.html, 在 index.html 中我们用 script 标签援用了 src/index.js,在 index.js 中我们引入了2个子模块

实行

npx parcel index.html 或许 ./node_modules/.bin/parcel index.html,或许运用 npm script

cli

"bin": {
    "parcel": "bin/cli.js"
}

检察 parcel-bundlerpackage.json 找到 bin/cli.js,在cli.js里又指向 ../src/cli

const program = require('commander');

program
  .command('serve [input...]') // watch build
  ...
  .action(bundle);

program.parse(process.argv);

async function bundle(main, command) {
  const Bundler = require('./Bundler');

  const bundler = new Bundler(main, command);

  if (command.name() === 'serve' && command.target === 'browser') {
    const server = await bundler.serve();

    if (server && command.open) {...启动自动翻开浏览器}
  } else {
    bundler.bundle();
  }
}

cli.js 中应用 commander 剖析敕令行并挪用 bundle 要领
serve, watch, build 3个敕令来挪用 bundle 函数,实行 pracel index.html 默以为 serve,所以挪用的是 bundler.serve 要领

进入 Bundler.js

bundler.serve

async serve(port = 1234, https = false, host) {
    this.server = await Server.serve(this, port, host, https);
    try {
      await this.bundle();
    } catch (e) {}
    return this.server;
  }

bundler.serve 要领 挪用 serveStatic 起了一个静态效劳指向 终究打包的文件夹
下面就是主要的 bundle 要领

bundler.bundle

async bundle() {
    // 加载插件 设置env 启动多线程 watcher hmr
    await this.start();

    if (isInitialBundle) {
      // 建立 输出目次
      await fs.mkdirp(this.options.outDir);

      this.entryAssets = new Set();
      for (let entry of this.entryFiles) {
          let asset = await this.resolveAsset(entry);
          this.buildQueue.add(asset);
          this.entryAssets.add(asset);
      }
    }

    // 打包行列中的资本
    let loadedAssets = await this.buildQueue.run();

    // findOrphanAssets 猎取一切资本中自力的没有父Bundle的资本
    let changedAssets = [...this.findOrphanAssets(), ...loadedAssets];

    // 由于接下来要构建 Bundle 树,先对上一次的 Bundle树 举行 clear 操纵
    for (let asset of this.loadedAssets.values()) {
      asset.invalidateBundle();
    }

    // 构建 Bundle 树
    this.mainBundle = new Bundle();
    for (let asset of this.entryAssets) {
      this.createBundleTree(asset, this.mainBundle);
    }

    // 猎取新的终究打包文件的url
    this.bundleNameMap = this.mainBundle.getBundleNameMap(
      this.options.contentHash
    );
    // 将代码中的旧文件url替代为新的
    for (let asset of changedAssets) {
      asset.replaceBundleNames(this.bundleNameMap);
    }

    // 将转变的资本经由过程websocket发送到浏览器
    if (this.hmr && !isInitialBundle) {
      this.hmr.emitUpdate(changedAssets);
    }

    // 对资本打包
    this.bundleHashes = await this.mainBundle.package(
      this,
      this.bundleHashes
    );

    // 将自力的资本删除
    this.unloadOrphanedAssets();

    return this.mainBundle;
  }

我们一步步先从 this.start

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

if (this.options.watch) {
  this.watcher = new Watcher();
  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')
  });

start:

  • 开首的推断 防备屡次实行,也就是说 this.start 只会实行一次
  • loadPlugins 加载插件,找到 package.json 文件 dependencies, devDependenciesparcel-plugin-开首的插件举行挪用
  • loadEnv 加载环境变量,应用 dotenv, dotenv-expand 包将 env.development.local, .env.development, .env.local, .env 扩大至 process.env
  • watch 初始化监听文件并绑定 change 回调函数,内部 child_process.fork 起一个子历程,运用 chokidar 包来监听文件转变
  • hmr 起一个效劳,WebSocket 向浏览器发送变动的资本
  • farm 初始化多历程并指定 werker 事情文件,开启多个 child_process 去剖析编译资本

接下来回到 bundleisInitialBundle 是一个推断是不是是第一次构建
fs.mkdirp 建立输出文件夹
遍历进口文件,经由过程 resolveAsset,内部挪用 resolver 剖析途径,并 getAsset 猎取到对应的 asset(这里我们进口是 index.html,依据扩大名猎取到的是 HTMLAsset
asset 添加进行列
然后启动 this.buildQueue.run() 对资本从进口递归最先打包

PromiseQueue

这里 buildQueue 是一个 PromiseQueue 异步行列
PromiseQueue 在初始化的时刻传入一个回调函数 callback,内部保护一个参数行列 queueadd 往行列里 push 一个参数,run 的时刻while遍历行列 callback(...queue.shift()),行列悉数实行终了 Promise 置为完成(resolved)(能够将其理解为 Promise.all
这里定义的回调函数是 processAsset,参数就是进口文件 index.htmlHTMLAsset

async processAsset(asset, isRebuild) {
  if (isRebuild) {
    asset.invalidate();
    if (this.cache) {
      this.cache.invalidate(asset.name);
    }
  }

  await this.loadAsset(asset);
}

processAsset 函数内先推断是不是是 Rebuild ,是第一次构建,照样 watch 监听文件转变举行的重修,假如是重修则对资本的属性重置,并使其缓存失效
以后挪用 loadAsset 加载资本编译资本

loadAsset

async loadAsset(asset) {
    if (asset.processed) {
      return;
    }

    // Mark the asset processed so we don't load it twice
    asset.processed = true;

    // 先尝试读缓存,缓存没有在背景加载和编译
    asset.startTime = Date.now();
    let processed = this.cache && (await this.cache.read(asset.name));
    let cacheMiss = false;
    if (!processed || asset.shouldInvalidate(processed.cacheData)) {
      processed = await this.farm.run(asset.name);
      cacheMiss = true;
    }

    asset.endTime = Date.now();
    asset.buildTime = asset.endTime - asset.startTime;
    asset.id = processed.id;
    asset.generated = processed.generated;
    asset.hash = processed.hash;
    asset.cacheData = processed.cacheData;

    // 剖析和加载当前资本的依靠项
    let assetDeps = await Promise.all(
      dependencies.map(async dep => {
          dep.parent = asset.name;
          let assetDep = await this.resolveDep(asset, dep);
          if (assetDep) {
            await this.loadAsset(assetDep);
          }
          return assetDep;
      })
    );

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

loadAsset 在最先有个推断防备反复编译
以后去读缓存,读取失利就挪用 this.farm.run 在多历程里编译资本
编译完就去加载并编译依靠的文件
末了假如是新的资本没有用到缓存,就从新设置一下缓存
下面说一下这里吗触及的两个东西:缓存 FSCache 和 多历程 WorkerFarm

FSCache

read 读取缓存,并推断末了修正时候和缓存的修正时候
write 写入缓存

《深切 Parcel--架构与流程篇》

缓存目次为了加快读取,防止将一切的缓存文件放在一个文件夹里,parcel16进制 两位数的 256 种能够建立为文件夹,如许存取缓存文件的时刻,将目的文件途径 md5 加密转换为 16进制,然后截取前两位是目次,背面几位是文件名

WorkerFarm

在上面 start 里初始化 farm 的时刻,workerPath 指向了 worker.js 文件,worker.js 里有两个函数,init run
WorkerFarm.getShared 初始化的时刻会建立一个 new WorkerFarm ,挪用 worker.jsinit 要领,依据 cpu 猎取最大的 Worker 数,并启动一半的子历程
farm.run 会关照子历程实行 worker.jsrun 要领,假如历程数没有到达最大会再次开启一个新的子历程,子历程实行终了后将 Promise状况变动为完成
worker.run -> pipeline.process -> pipeline.processAsset -> asset.process
Asset.process 处置惩罚资本:

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

    return this.generated;
  }

将上面的代码内部扩大一下:

async process() {
  // 已经有就不须要编译
  if (!this.generated) {
    // 加载代码
    if (this.contents == null) {
      this.contents = await this.load();
    }
    // 可选。在网络依靠之前转换。
    await this.pretransform();
    // 将代码剖析为 AST 树
    if (!this.ast) {
      this.ast = await this.parse(this.contents);
    }
    // 网络依靠
    await this.collectDependencies();
    // 可选。在网络依靠以后转换。
    await this.transform();
    // 天生代码
    this.generated = await this.generate();
  }

  return this.generated;
}

// 末了处置惩罚代码
async postProcess(generated) {
  return generated
}

processAsset 中挪用 asset.process 天生 generated 这个generated 不一定是终究代码 ,像 html里内联的 script ,vuehtml, js, css,都邑举行二次或屡次递归处置惩罚,终究挪用 asset.postProcess 天生代码

Asset

下面说几个完成
HTMLAsset

  • pretransform 挪用 posthtmlhtml 剖析为 PostHTMLTree(假如没有设置posthtmlrc之类的不会走)
  • parse 挪用 posthtml-parserhtml 剖析为 PostHTMLTree
  • collectDependencies 用 walk 遍历 ast,找到 script, imgsrclinkhref 等的地点,将其到场到依靠
  • transform htmlnano 紧缩代码
  • generate 处置惩罚内联的 scriptcss
  • postProcess posthtml-render 天生 html 代码

JSAsset

  • pretransform 挪用 @babel/corejs 剖析为 AST,处置惩罚 process.env
  • parse 挪用 @babel/parserjs 剖析为 AST
  • collectDependencies 用 babylon-walk 遍历 ast, 如 ImportDeclarationimport xx from 'xx' 语法,CallExpression 找到 require挪用,import 被标记为 dynamic 动态导入,将这些模块到场到依靠
  • transform 处置惩罚 readFileSync__dirname, __filename, global等,假如没有设置scopeHoist 并存在 es6 module 就将代码转换为 commonjsterser 紧缩代码
  • generate @babel/generator 猎取 jssourceMap 代码

VueAsset

  • parse @vue/component-compiler-utilsvue-template-compiler.vue 文件举行剖析
  • generate 对 html, js, css 处置惩罚,就像上面说到会对其离别挪用 processAsset 举行二次剖析
  • postProcess component-compiler-utilscompileTemplate, compileStyle处置惩罚 html,cssvue-hot-reload-api HMR处置惩罚,紧缩代码

回到 bundle 要领:

let loadedAssets = await this.buildQueue.run() 就是上面说到的PromiseQueueWorkerFarm 结合起来:buildQueue.run —> processAsset -> loadAsset -> farm.run -> worker.run -> pipeline.process -> pipeline.processAsset -> asset.process,实行以后一切资本编译终了,并返回进口资本loadedAssets就是 index.html 对应的 HTMLAsset 资本

以后是 let changedAssets = [...this.findOrphanAssets(), ...loadedAssets] 猎取到转变的资本

findOrphanAssets 是从一切资本中查找没有 parentBundle 的资本,也就是自力的资本,这个 parentBundle 会在等会的构建 Bundle 树中被赋值,第一次构建都没有 parentBundle,所以这里会反复进口文件,这里的 findOrphanAssets 的作用是在第一次构建以后,文件change的时刻,在这个文件 import了新的一个文件,由于新文件没有被构建过 Bundle 树,所以没有 parentBundle,这个新文件也被标记物 change

invalidateBundle 由于接下来要构建新的树所以挪用重置一切资本上一次树的属性

createBundleTree 构建 Bundle 树:
起首一个进口资本会被建立成一个 bundle,然后动态的 import() 会被建立成子 bundle ,这引发了代码的拆分。

当差别范例的文件资本被引入,兄弟 bundle 就会被建立。比方你在 JavaScript 中引入了 CSS 文件,那它会被安排在一个与 JavaScript 文件对应的兄弟 bundle 中。

假如资本被多于一个 bundle 援用,它会被提拔到 bundle 树中近来的大众先人中,如许该资本就不会被屡次打包。

Bundle

  • type:它包括的资本范例 (比方:js, css, map, …)
  • name:bundle 的称号 (运用 entryAsset 的 Asset.generateBundleName() 天生)
  • parentBundle:父 bundle ,进口 bundle 的父 bundle 是 null
  • entryAsset:bundle 的进口,用于天生称号(name)和靠拢资本(assets)
  • assets:bundle 中一切资本的鸠合(Set)
  • childBundles:一切子 bundle 的鸠合(Set)
  • siblingBundles:一切兄弟 bundle 的鸠合(Set)
  • siblingBundlesMap:一切兄弟 bundle 的映照 Map<String(Type: js, css, map, …), Bundle>
  • offsets:一切 bundle 中资本位置的映照 Map<Asset, number(line number inside the bundle)> ,用于天生正确的 sourcemap 。

我们的例子会被构建成:

html            ( index.html )
  |-- js        ( index.js, module1.js, module2.js )
    |-- map     ( index.js, module1.js, module2.js )

module1.jsmodule2.js 被提到了与 index.js 同级,map 由于范例差别被放到了 子bundle

一个复杂点的树:

// 资本树
index.html
  |-- index.css
  |-- bg.png
  |-- index.js
    |-- module.js
// mainBundle
html            ( index.html )
  |-- js        ( index.js, module.js )
    |-- map     ( index.map, module.map )
  |-- css       ( index.css )
    |-- js      ( index.css, css-loader.js bundle-url.js )
    |-- map     ( css-loader.js, bundle-url.js )
  |-- png       ( bg.png )

由于要对 css 热更新,所以新增了 css-loader.js, bundle-url.js 两个 js

replaceBundleNames替代援用:天生树以后将代码中的文件援用替代为终究打包的文件名,假如是临盆环境会替代为 contentHash 依据内容天生 hash

hmr更新: 推断启用 hmr 而且不是第一次构建的状况,挪用 hmr.emitUpdate 将转变的资本发送给浏览器

Bundle.package 打包

unloadOrphanedAssets 将自力的资本删除

package

packagegenerated 写入到文件
有6种打包:
CSSPackagerHTMLPackagerSourceMapPackagerJSPackagerJSConcatPackagerRawPackager
当开启 scopeHoist 时用 JSConcatPackager 不然 JSPackager
图片等资本用 RawPackager

终究我们的例子被打包成 index.html, src.[hash].js, src.[hash].map 3个文件

index.html 里的 js 途径被替代建立终究打包的地点

我们看一下打包的 js:

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]) {
      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][4][x] || x;
    }
  }
  for (var i = 0; i < entry.length; i++) {
    newRequire(entry[i]);
  }
  // Override the current require with this new one
  return newRequire;
})({"src/module1.js":[function(require,module,exports) {
"use strict";

},{}],"src/module2.js":[function(require,module,exports) {
"use strict";

},{}],"src/index.js":[function(require,module,exports) {
"use strict";

var _module = require("./module");

var _module2 = require("./module1");

var _module3 = require("./module2");
console.log(_module.m);
},{"./module":"src/module.js","./module1":"src/module1.js","./module2":"src/module2.js","fs":"node_modules/parcel-bundler/src/builtins/_empty.js"}]
,{}]},{},["node_modules/parcel-bundler/src/builtins/hmr-runtime.js","src/index.js"], null)
//# sourceMappingURL=/src.a2b27638.map

能够看到代码被拼接成了对象的情势,吸收参数 module, require 用来模块导入导出,完成了 commonjs 的模块加载机制,一个越发简化版:

parcelRequire = (function (modules, cache, entry, globalName) {
  function newRequire(id){
    if(!cache[id]){
      let module = cache[id] = { exports: {} }
      modules[id][0].call(module.exports, newRequire, module, module.exports, this);
    }
    return cache[id]
  }
  for (var i = 0; i < entry.length; i++) {
    newRequire(entry[i]);
  }
  return newRequire;
})()

代码被拼接起来:

`(function(modules){
  //...newRequire
})({` +
  asset.id +
    ':[function(require,module,exports) {\n' +
        asset.generated.js +
      '\n},' +
'})'
(function(modules){
  //...newRequire
})({
  "src/index.js":[function(require,module,exports){
    // code
  }]
})

hmr-runtime

上面打包的 js 中另有个 hmr-runtime.js 太长被我省略了
hmr-runtime.js 建立一个 WebSocket 监听效劳端音讯
修正文件触发 onChange 要领,onChange 将转变的资本 buildQueue.add 到场构建行列,从新挪用 bundle 要领,打包资本,并挪用 emitUpdate 关照浏览器更新
当浏览器吸收到效劳端有新资本更新音讯时
新的资本就会设置或掩盖之前的模块
modules[asset.id] = new Function('require', 'module', 'exports', asset.generated.js)
对模块举行更新:

function hmrAccept(id){
  // dispose 回调
  cached.hot._disposeCallbacks.forEach(function (cb) {
    cb(bundle.hotData);
  });

  delete bundle.cache[id]; // 删除之前缓存
  newRequire(id); // 从新此加载

  // accept 回调
  cached.hot._acceptCallbacks.forEach(function (cb) {
    cb();
  });

  // 递归父模块 举行更新
  getParents(global.parcelRequire, id).some(function (id) {
    return hmrAccept(global.parcelRequire, id);
  });
}

至此全部打包流程完毕

总结

parcle index.html
进入 cli,启动Server挪用 bundle,初始化设置(Plugins, env, HMRServer, Watcher, WorkerFarm),从进口资本最先,递归编译(babel, posthtml, postcss, vue-template-compiler等),编译完设置缓存,构建 Bundle 树,举行打包
假如没有 watch 监听,完毕封闭 Watcher, Worker, HMR
watch 监听:
文件修正,触发 onChange,将修正的资本到场构建行列,递归编译,查找缓存(这一步缓存的作用就提示出来了),编译完设置新缓存,构建 Bundle 树,举行打包,将 change 的资本发送给浏览器,浏览器吸收 hmr 更新资本

末了

经由过程此文章愿望你对 parcel 的大抵流程,打包东西道理有更深的相识
相识更多请关注专栏,后续 深切Parcel 同系列文章,对 AssetPackagerWorkerHMRscopeHoistFSCacheSourceMapimport 越发 细致解说代码完成

    原文作者:李十三
    原文地址: https://segmentfault.com/a/1190000018550209
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞