【翻译】打包工具?Parcel!最新官方英文文档学习

1,382 阅读22分钟

Geting Started 开始

普通webapp


安装Parcel脚手架

Parcel CLI 构建在parcel包中。

你可以全局安装运行parcel,但更好的方式是将parcel作为开发依赖本地安装在你的项目中。

# yarn
yarn add -D parcel@next

# npm
npm install -D parcel@next

配置项目

example

为了让parcel更好的运行,需要在package.json中添加一些scripts。下面提供了一些建议。

package.json

{
  "name": "my-project",
  "scripts": {
    "start": "parcel serve ./src/index.html",
    "build": "parcel build ./src/index.html"
  },
  "dependencies": {
    "react": "^16.13.1",
    "react-dom": "^16.13.1"
  },
  "devDependencies": {
    "parcel": "next",
}

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My Parcel Project</title>
  </head>
  
  <body>
    <div id='root'></div>
    <script src='./index.tsx'></script>
  </body>
</html>

index.tsx

import React from 'react'
import { render } from 'react-dom'

render(<div>hello</div>, document.getElementById('root'))
parcel 指令解释

上面的例子中你可以看到两条指令:parcel serve ./src/index.html用于启动开发环境,parcel build ./src/index.html构建生产包。

注意指令中将./src/index.html这个html文件作为入口,而不是像其他打包工具中是一个JS文件。使用HTML文件作为入口能够使Parcel使用起来更便捷。不需要任何配置,直接从HTML文件中检测到依赖,并自动地将所有检测到的依赖打包到各自的包(bundle)中。

开发指令
parcel serve ./src/index.html开启了一个本地服务器服务于你的js,html,css文件以及其他项目资源。

除了资源托管,还启动了HMR(Hot Module Reload)服务。基于你的修改来重载脚本,样式,或者整个个页面(如果使用的是react,甚至还有内置的React Fast Refresh)。

同时确保了所有在用的库和框架以开发环境构建,表示如果他们提供了debug信息你也能够看到。parcel将process.env.NODE_ENV变量设置为development,生成sourcemap,并不做任何压缩。

生产指令

process.env.NODE_ENV设置为production

parcel为大多数资源运行了压缩器来确保代码体积尽可能小,并为所有js的包做了作用域提升确保无用代码尽可能少。

所有构建的包的命名都包含了基于这个包最终内容的hash值。以这种方式命名,所有非HTML资源都可以长时间安全地缓存在CDN上,不会出现用户获取到错误或过期的包的情况。


浏览器列表 Browserslist

Parcel默认是:> 0.25%,对大多数应用来说是较好的配置。

为什么配置browserslist
自定义的broswerslist确保你对你的应用要兼容哪些浏览器有一个完全的掌控。例如你要兼容IE11,那么你可以定义> 0.25%, ie 11。这样就确保了无论将来IE11拥有多少市场份额,他始终可以正常工作。

另一方面,自定义browserslist对减少包的体积大小也很有帮助,支持过多或者过时的浏览器会导致许多不必要的polyfills。如果你不需要支持IE11和Opera Mini,你可以使用> 0.25%, not ie 11, not op_mini all,如此即可减少包的体积,获得更快的加载,更好的用户体验。

如何配置browserslist
你可以通过browserlist键,定义在package.json文件中。或者在一个单独的配置文件里:browserslist或者.browserslistrc

你可以发现更多信息在Browserslist repo章节

在配置章节中解释了如何通过配置来设置(兼容的)目标。


差异化服务
Parcel支持差异化服务,表示你可以为单独现代浏览器服务一个不同的包。这可以为比较新的浏览器实现更快的加载速度,更小的包的体积。

你不需要为此添加任何配置,你只要做一件事就是在HTML文件中添加一个script标签。Parcel会自动关注兼容的目标浏览器,从你定义的browserslist中移除所有不支持es模块化的浏览器。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Differential Serving Example</title>
  </head>
  
  <body>
    <div id='root'></div>
    
    <!-- 这个标签将获取对你定义的目标浏览器兼容的包的引用 -->
    <script nomodule src='./index.tsx' ></script>
    <!-- 这个标签将获取es模块的包的引用 -->
    <script type='module' src='./index.tsx'></script>
  </body>
</html>
import React from 'react'
import { render } from 'react-dom'

render(<div>hello</div>, document.getElementById('root'))
{
  "name": "differential-serving-example",
  "scripts": {
    "start": "parcel serve ./src/index.html",
    "build": "parcel build ./src/index.html"
  },
  browserslist: "> 0.2%",
  dependencies: {
    "react": "^16.13.1",
    "react-deom": "^16.13.1"
  },
  "devdependencies": {
    "parcel": "next"
  }
}



普通JS库

如何开始构建一个js库

TODO
  • 多种输出格式main/module



(零)配置

你和零配置的距离有多远,如何去配置parcel

Targets 目标

Parcel运行的时候,可以同时以多种不同的方式去构建你的文件。这些叫做targets

例如,你可以设置一个modern的目标定位较新的浏览器,和一个lagacy目标对旧的浏览器。

每个入口都会根据设置的目标进行处理。


指定入口

入口是指包含了app的源代码的文件,在被Parcel编译之前。将会由以下几种形式被Parcel接受:

  1. $ parcel <entries>
  2. $ parcel <folder(s)> 或者使用 <folder>/package.json#source
  3. ./src/index.*
  4. ./index.*

这些文件所依赖的所有东西在Parcel中都会被视作"源(资源)"


设置出口路径

输出的包所放置的路径是可以被指定的。

默认的输出文件夹:

  • 通用的目标为path.dirname(package.json#${targetName})
  • 自定义目标为path.dirname(package.json#${targetName})或者~/dist/${targetName}/

不指定入口的情况下,默认目标输出文件路径为~/dist/

多入口的情况下,你必须使用显式的distDir作为顶层键target字段的值。因为Parcel并不知道哪一个包应该叫什么。

{
  "targets": {
    "app": {
      "distDir": "./www"
    }
  }
}

环境

环境告诉Parcel如何去转换并打包资源。他们告诉Parcel一个资源将运行在浏览器环境还是Node环境。

环境也告诉Parcel的插件应该输出的位置,通过指定你想构建哪一个目标版本的浏览器。

你可以通过targets#context 或 targets#enginesengines/browserslist来配置环境。


配置 Parcel

有三个地方可以配置Parcel。

  • 如果想配置cli,看cli章节
  • 如果想配置打包,将在package.json
  • 如果想配置你的文件,或者Parcel资源的进程,将在.parcelrc文件中



迁移




Features 特性

api代理

配置内置的开发服务器来将特定的路径转发到另外一台服务器

为了在开发期间更好地模拟生产环境,你可以指定请求路径代理到另一台服务器(例如你的真实api服务器或一台本地测试服务器)。通过.proxyrc或者.proxyrc.json或者.proxyrc.js文件。

在JSON文件中,你可以指定一个对象,键为匹配url所对应的字符,键值为http-proxy-middleware的配置项。


.proxyrc / .proxyrc.json
{
  "/api": {
    "target": "http://localhost:8000/",
    "pathRewrite": {
      "^/api": ""
    }
  }
}

这会将http://localhost:1234/api/endpoint代理到http://localhost:8000/endpoint


.proxyrc.js

可以处理更多复杂的配置,能够获取任何一个中间件,下面的例子,和.proxyrc的版本做了同样的事情。

const { craeteProxyMiddleware } = require('http-proxy-middleware')

module.exports = function(app) {
  app.use(
    createProxyMiddleware("/api", {
      target: "http://localhost:8000/",
      pathRewrite: {
        "^/api": "",
      }
    })
  )
}

(此功能由@parcel/reporter-dev-server提供)



脚手架CLI

parcel的指令

指令

所有指令中的入口(entries)可以是:

  1. 一个或多个文件
  2. 一个或多个通配符表达式
  3. 一个或多个目录(详见入口指定)
# ok
parcel './**/*.html'
parcel './img/**/*'

# not ok
parcel ./**/*.html
parcel ./img/**/*

parcel [serve] <entries>
开启一个本地服务,当你的文件发生变动将自动重建你的应用(由hot module replacement模块热替换支持)。你可以传入一个通配符的表达式或者一个列表。

parcel index.html
parcel a.html b.html
parcel './**/*.html'

如果你指定了多个HTML文件为入口,但没有一个入口文件的输出路径为/index.html,开发服务器将会返回404,因为Parcel不知道哪一个HTML包是首页。 这种情况下,你可以直接加载文件:http://localhost:1234/a.html或者http://localhost:1234/b.html

parcel [watch] <entries>
watch指令和serve类似,但仅仅启动了HMR,没有HTTP服务器。

parcel index.html

parcel [build] <entries>
编译你的项目一次,同时也启用了压缩,设置了环境变量NODE_ENVproduction。(详见生产)

parcel build index.html

相对于servewatch来说,build指令默认激活了作用域提升(scope hoisting)。(所以其他指令隐式地指定了--no-scope-hoist



代码拆分 code splitting

代码按需加载

Parcel支持0配置开箱即用的代码拆分。这允许你拆分你的应用代码为几个分开的包来按照要求加载。意味着更小的初始包体积,更快的加载速度。当用户浏览应用并且需要某些模块时,再按照要求加载子包。

代码拆分是通过动态引入(import())语法实现的,工作就如同普通的importrequire,但是返回一个Promise。这意味着模块可以异步加载。

使用动态引入
下面的例子展示了你可能会使用动态模块在你的应用中按照需求来加载一个子页面。
import('./pages/about').then(function(page) {
  // Render page
  page.render()
})

pages/about.js

export function render() {
  // Render the page
}
通过async/await使用动态引入

import()返回Promise,所以你可以使用async/await语法

asycn function load() {
  const page = await import('./pages/about')
  
  // Render page
  page.render()
}

包内部化
如果一个资源同时被同步和异步地引入,那么真实地创建一个异步的包是没有意义的(因为这个包已经被加载过了)。

这种情况下,Parcel会将import('foo')转为Promise.resolve(require("foo"))。所以,在一个较大的应用中,使用动态引入时你更应该去考虑的是“我不需要同步引入这个模块”,而不是“它会生成一个新的包”。


共享包

在许多情况下(例如当两个HTML入口通过js脚本引入同一个资源,或者两个动态引入具有相同的资源),Parcel会将他们拆分为单独的兄弟包(共享包),来减少重复代码。

可以在package.json中配置其参数。

{
  "name": "my-project",
  "dependencies": {
    ...
  },
  "@parcel/bundler-default": {
    "minBundles": 1,
    "minBundleSize": 3000,
    "maxParallelRequests": 20
  }
}

选项:

  • minBundles : 一个资源如果要被拆分,被引用的数量必须大于该参数数量。
  • minBundleSize : 如果要创建共享包,拆分出来的包体积大小至少需要满足该参数值。(压缩/摇树前)
  • maxParallelRequests: 为了防止太多并行请求使网络超载,该参数确保了一个包的兄弟包数量不能超过该参数值。
  • http : 这是一个设置上述默认值的简写,用来优化HTTP/1HTTP/2
http版本minBundlesminBundleSizemaxParallelRequests
11300006
2(deault)12000025



模块热替换

刷新你的应用,而不是重新加载整个页面

模块热替换(HMR)在浏览器运行过程中,自动更新模块而非整个页面,极大地提升了开发体验。这意味着如果只是进行了小的更改,应用的状态可以被保存下来。Parcel的HMR的实现同时支持JS资源和CSS资源。

默认情况下,当文件修改时Parcel会刷新整个页面。你可以通过下面的配置来激活HMR。这只会在开发模式下被应用,HMR在生产环境下会自动禁止。

if (module.hot) {
  module.hot.accept()
}

当你保存文件时,Parcel重新编译修改部分,并发送一个包含最新代码的更新给客户端。然后新的代码替换旧的版本,对所有父节点重新评估。你可以使用module.hotapi对这个进程进行hook(嵌入钩子),以此在当模块将要处理或者新版本更新的时候来确认你的代码。(对于React来说是自动的

if (module.hot) {
  module.hot.dispose(function (data) {
    // 模块姐昂要被替换
    // 你可以将新的资源中可访问的代码保存在`data`字段中
    data.updated = Date.now()
  })
  
  module.hot.accept(function (getParents) {
    let { updated } = module.hot.data
    // 模块或者其一依赖刚刚跟新
  })
}

safe write安全编写

某些编辑器和IDE有一个特性叫做safe write,为了防止数据丢失,当文件保存时创建文件的副本并进行重命名。

使用HMR时,该特性的会阻止Parcel自动检测文件的更新,可以通过以下方式禁用safe write

  • Sublime Text 3: 在user -> preferences 中添加 automic_save: "false"
  • IntelliJ: 在参数中搜索safe write并禁用
  • Vim: 在设置中修改backupcopy=yes
  • WebStorm: 在Preference > Apperance & Behavior > System Settings 中取消勾选Use safe write



模块解析

parcel如何解析依赖

Parcel的解析器实现了一个经过调整的版本的the node_modules resolution算法

  • 根目录: 指定给Parcel的入口文件的目录,或者如果指定了多个入口,即是共享的根目录(最近的公用父级目录)。
绝对路径

/foo会解析为相对于项目根目录的foo路径。


波浪号

~/foo会解析为相对于最近的node_modules目录的,package.json最近的,或者项目根目录的foo路径。(依次推算)


package.json的browser字段

如果package.json包括了browser字段(字符),Parcel会使用该字段,而不是main作为入口。

如果是一个对象,他的行为就像aliases,但是当target.context === 'browser'时,具有更高的优先权。


别名 aliases

支持别名,在package.json设置alias字段。

例中,将react设为preact,以及一些不在node_modules中的本地自定义模块。

运行时需要,他们也可以映射到全局变量中。对于使用CDN的版本替换一个依赖来说是非常有帮助的。

{
  "name": "some-package",
  "devDependencies": {
    "parcel-bundler": "^2.0.0-beta.1",
  },
  "alias": {
    "react": "preact/compat",
    "react-dom": "preact/compat",
    "local-module": "./custom/modules",
    "other-local-module": {
      "fileName": "./custom/other-module"
    },
    "lodash": { "global": "_" }
  }
}

别名中避免使用一些特殊字符,因为可能已经被Parcel,或者第三方工具使用。

  • ~被Parcel用作解析波浪路径
  • @被用于npm包的结构。

我们建议定义别名时,显式地指定文件的扩展名。而不是让Parcel猜。

多平台构建时,使用window(例如window.JQuery)或者全局this的前缀全局别名可能会失败,也不推荐。尽管文件别名可以是path/to/file而不是{ filename: "path/to/file' },但是全局必须使用这种格式:{ "global": "name" }


外部 externals
`externals`必须按照目标targets通过`includeNodeModules`键逐一配置。就像globals,`externals`不会被打包,但是在运行时会加载。
{
  "targets": {
    "app": {
      "includeNodeModules": {
        "react": false
      }
    }
  }
}

package 入口字段

当作用域提升(scope hoisting)激活时,裸指定(例如lodash)会按照以下的顺序解析。

  • package.json#source
  • package.json#browser
  • package.json#module
  • package.json#main
  • index.{js, json}

如果没有作用域提升,那么为了更好的体验,main会优先于module

  • package.json#source
  • package.json#browser
  • package.json#main
  • package.json#module
  • index.{js, json}

常见问题

javascript 命名导出

别名映射用于很多类型的资源,但是特定不支持映射javascript命名导出。如果你想映射命名导出,你可以在别名文件中将命名导出重导出。

{
  name: "some-package",
  alias: {
    ipcRenderer: "./electron-ipc.js", // 指定文件扩展名
  }
}
// electron-ipc.js
module.exports = require('electron').ipcRenderer

Flow中的绝对或波浪路径
当使用绝对路径或者波浪路径来进行模块解析,你必须使用module.name mapper特性对其配置。

现有一个结构如下展示的项目,src/index.html作为入口,入口文件的根目录是src/文件夹。

├── package.json
├── .flowconfig
└── src/
    ├── index.html
    ├── index.js
    └── components/
        ├── apple.js
        └── banana.js

然后,为了正确映射引入,Flow应该将前导/替换为src/,那么最终结果为src/components/apple。通过在.flowconfig文件中设置。

// index.js
import Apple from '/components/apple"

.flowconfig

module.name_mapper = '^\/\(.*\)$' -> '<PROJECT_ROOT>/src/\1'

注意:module.name_mapper可以有多个入口。除了本地模块别名支持外,还支持了绝对路径和波浪路径的解析。

Typescrip对的解析
Typescript需要知道你对使用的模块的解析或别名映射。详见Typescript模块解析文档

Monorepo 解析



Node模仿

一些特性最终模仿了Node.js的api

环境变量

Parcel使用了dotenv库来支持从.env文件加载环境变量

.env文件存储与package.json同级目录,也包含parcel-bundler依赖。

Parcel使用以下指定的NODE_ENV值读取.env文件:

可用的.env文件名NODE_ENV=*NODE_ENV=test
.env
.env.local×
.env.${NODE_ENV}
.env.${NODE_ENV}.local

值得注意的是,

  • NODE_ENV默认是`development
  • NODE_ENV=test.env.local不会加载,因为测试环境应该为所有用户生成相同的结果。
  • 有时创建了一个新的.env文件不会立刻生效,尝试删除.cache/目录。
  • 直接访问process.env对象是不支持的,但是可以访问特定的变量例如process.env.API_KEY,会如期提供正确的值。
  • 使用内置的Nodejs全局的process,即不要import process from 'process'。如果使用typescript,你可能会需要安装@types/node来编译。

polyfill,并且排除内置的Node模块

当你引入像cryptofsprocess这样的包,Parcel将自动使用列出polyfill的其中之一,否则会排除该模块。你可以在package.json中使用aliases字段。


内联的 fs.readFileSync

调用fs.readFileSync会被替换为文件的内容,如果文件路径在项目根目录中是静态可确定的。

  • fs.readFileSync(..., "utf8"): 将内容作为字符串(或其他可用的编码方式)
  • fs.readFileSync(...): 一个Buffer流对象(例如Buffer.from(...))以及可能需要的polyfill。
import fs from 'fs'
import path from 'path'

const data = fs.readFileSync(path.join(__dirname, "data.json"), "utf8")
console.log(data) // { "foo": "bar" }
{
  "foo": "bar"
}

禁用这些特性

内联的环境变量以及fs.readFileSync的调用可以被禁止,通过在package.json中设置@parcel/transformer-js键。

  "name": "my-project",
  "dependencies": {
    ...
  },
  "@parcel/transformer-js": {
    "inlineFS": false,
    "inlineEnvironment": false
  }
}

inlineEnvironment也可以是通配符字符数组 ···json { "name": "my-project"m "dependencies": { ... },, "@parcel/transoformer-js": { inlineEnvironment": ["SENTRY"] } }

`inlineFS`基于`readFileSync`函数调用,inlineEnviroment是`process.env.SOMETHING````json
{
  inlineFS: boolean,
  inlineEnvironment: boolean | Array<Glob>
}

这个功能由@parcel/transformer-js@parcel/resolver-default提供。



Parcel api

如何以编程的方式使用@parcel/core

一个简短的例子(parcel cli 做了什么)
import path from 'path'
import defaultConfigContents from '@/parcel/config-default'
import Parcel from '@parcel/core'

(async () => {
  let bundler = new Parcel({
    entries: path.join(__dirname, 'src/index.js'),
    defaultConfig: {
      ...defaultConfigContents,
      filePath: require.resolve('@parcel/config-default')
    },
    defaultEngines: {
      browsers: ["last 1 Chrome version"],
      node: "10"
  })
  
  await bundler.run()
})()

输出一个Memory中的文件系统
import path from 'path'
import Parcel, { createWorkerFarm } from '@parcel/core'
import defaultConfigContents from '@parce/config-default'
import { NodeFS, MemoryFS } from '@parcel/fs'

const DIST_DIR = '/dist'

(async () => {
  let workerFarm = createWorkerFarm()
  let inputFS = new NodeFS()
  let outputFS = new NodeFS()
  
  await outputFS.mkdirp(DIST_DIR)
  
  try {
    let b = new Parcel({
      entries: [path.join(__dirname, "src", "index.html")],
      defaultConfig: {
        ...defaultConfigContents,
        reporters: [],
        filePath: require.resolve("@parcel/config-default")
      },
      inputFS: inputFS,
      outputFS: outputFS,
      workerFarmm,
      defaultEngines: {
        browsers: ["last 1 Chrome version"],
        node: "8",
      },
      distDir: DIST_DIR,
      pathConsole: false
    })
    
    await b.run()
    
    for(let file of await outputFS.readdir(DIST_DIR)) {
      console.log("--------", file, "--------")
      console.log(await outputFS.readFile(path.join(DIST_DIR, file), "utf8"))
    }
  } catch (e) {
    console.error(e)
  } finally {
    await workerFarm.end()
  }
})()



生产

Parcel有内置的插件,一些工具来帮助分析包的体积大小

包分析器

要为每个包生成一个HTML文件,需设置PARCEL_BUNDLE_ANALYZER环境变量。

PARCEL_BUNLDE_ANALYZER=1 parcel build src/index.html

这将会在你的项目根目录中生成一个名为parcel-bundle-reports的文件夹,并会为每个目标target都生成HTML文件。


Bundle Buddy

设置环境变量BUNDLE_BUDDY

BUNDLE_BUDDY=1 parcel build src/index.html

并在Bundle Buddy中使用文件。



作用域提升

什么是作用域提升,他如何实现更小的构建体积和es模块化。

关于更小/更快的构建的建议

包装资源
在一些情况下,资源需要被包装,并转移到函数中。这样就否定了作用域提升的优势,因为将导出提升到顶层是我们的最初目标。

  • 如果使用了顶层的return表达式或者eval,又或者module变量自由被使用。我们不能将他加到顶层作用域(因为return停止整个打包的执行,而eval可能会使用被重命名的变量。)
  • 如果资源有条件地被引入(使用CommonJS的require,或者在try/catch,或者是条件函数中,总之不可能是ES模块语法),我们不能将他加入顶层作用域,因为他的内容只有当真实需要的时候才会被执行。

sideEffects: false

sideEffects: falsepackage.json中被指定,那么Parcel可以完全跳过处理某些资源,或者在输出包中根本不包含他们(因为这些资源仅仅是被透传了)。


作用域提升的动机和优势

很长一段时间内,许多打包工具(像Webpack、Browserify,Rollup除外)通过将资源包裹一个函数实现了真实的绑定,创建包含所有资源的映射,并提供CommonJS运行。一个非常简略的例子:

(function (modulesMap, entry) {
  // internal runtime
})(
  {
    "index.js": function(require, module, exports) {
      var { Foo } = require('./thing.js')
      var obj = new Foo()
      obj.run()
    },
    "thing.js": function(require, module, exports) {
      module.export.Foo = class Foo {
        run() {
          console.log("Hello!")
        }
      };
      module.eports.Bar = class Bar {
        run() {
          console.log("Unused!")
        }
      }
    }
  },
  "index.js"
)

这种机制有优点也有缺点:

√ 包可以很快的生成,资源只是简单的复制为字符串。

× 很难去优化,因为require函数使得很难静态地分析哪一个导出的模块被使用了(比如loadsh),并且无法判断一个资源是否只是透传,完全可以删除。

× 要生成一个实现es模块化的包,export声明不可以在函数中。


解决方法

提取每个资源并直接在顶层作用于连接他们。

// thing.js
var $thing$export$Foo = class {
  run() {
    console.log("Hello!")
  }
}
var $thing$export$Bar = class {
  run() {
    console.log("Unused!")
  }
}

// index.js
var $index$export$var$obj = new $thing$export$Foo()
$index$export$var$obj.run()

如你所见,来自资源的顶层作用域变量需要被重命名为一个全局唯一的名字。

现在,移除未使用的导出变得并不重要:变量$thing$export$Bar根本没有使用,所以我们可以安全地移除他(因为压缩器基本上都会自动移除)。这一步有时候被叫做摇树(tree shaking)。

唯一的缺点就是构建的时候名称会更长,比单纯包裹的方法占用更多的空间(因为每个单独的声明都需要被调整,并且打包期间,整个包都需要保存在内存中)。





Configuration 配置

Package.json

字段

这些是Parcel用于配置的字段:

main / module / browser

这也是其他工具会使用的公共字段。

{
  "main": "dist/main/index.js",
  "module": "dist/module/index.js",
  "browser": "dist/browser/index.js"
}

他们默认是库模式。(意思是他们不打包依赖)

  • main(以及module)是对于你的库来说是标准的入口,module默认是ES模块化输出。
  • browser是用于指定浏览器的构建。

如果指定了这些字段之一,Parcel会为他们创建一个target。

要想Parcel忽略他们中任一,在target.(main|browser|module)中设为false即可。

{
  "main": "unrelated.js",
  "targets": {
    "mian": false
  }
}

如果browser字段是一个对象,那么可以用package.json#browser[pkgName]

自定义 targets

创建你自己的target目标(而不是先前描述过的),添加一个顶层的字段并带有target名称和输出路径。你需要放入targets字段中,被Parcel识别。

{
  "app": "www/index.js",
  "targets": {
    "app": {}
  }
}

source

指定映射你的目标到源代码的入口,可以是字符,或者是数组。

{
  "source": "src/index.js"
}
{
  "source": ["src/index.js", "src/index.html"]
}

targets

目标是通过package.json#targets字段配置的

{
  "app": "dist/browser/index.js",
  "appModern": "dist/borwserModern/index.js",
  "targets": {
    "app": {
      "engines": {
        "browsers": "> 0.25%"
      }
    },
    "appModern": {
      "engines: {
        "browsers": "Chrome 70"
      }
    }
  }
}

每一个目标的名字都对应了package.json顶层的字段例如package.json#main或者package.json#app,为目标指定了主入口。

每个targets都包含了目标环境的配置(所有属性都是可选的):

选项可能的值描述
context见下方包应该在哪个环境下运行
distDirstring指定输出的文件夹(相对于输出文件)
enginespackage.json#engines优先权高于package.json#engines
includeNodeModules见下方是否打包所有/都不/部分node_modules的依赖
isLibraryboolean就像npm库中的库
minifyboolean是否启用压缩(具体的行为由插件决定),通过--no-minify设置
outputFormat'global' | 'esmodule' | 'commonjs'导入导出的类型
publicUrlstring包运行时的公共url
scopeHoistboolean是否启用作用域提升,outputFormat为'esmodule'或'commonjs'时必须为true,通过--no-scope-hoist设置
sourceMap见下方启用/禁用sourcemap和设置。可通过--no-source-maps覆盖

不过,许多构建库的通用配置已经默认地为你提供了:

target = {
  mian: {
    engines: {
      node: value("package.json#engines.node"),
      browsers: unless exist("package.json#browsers") then value("package.json#browserlist")
    },
    isLibrary: true
  },
  module: {
    engines: {
      node: value("package.json#engines.node"),
      browsers: unless exists("package.json#browser") then value("package.json#borwserlist")
    },
    isLibrary: true
  },
  borwser: {
   engines: {
     browsers: value("package.json#browserslist")
   },
   isLibrary: true
  },
  ...value("package.json#targets"
}

-context

可能的值为:node | browser | web-worker | service-worker | electron-main | 'electron-renderer'。

这些值可以被插件所使用。(例如服务端工作者的url不应该包含hash值,网页工作者可以使用importScript

对于通用的目标,可以从target中推出以下内容:

  • 如果有一个browser目标或者engines.node != null && engines.browsers == null,那么main目标的context属性应该是node,否则browser
  • 如果有一个browser目标,那么module目标的context值应该是node,否则是browser
  • browser目标的context值是browser

-includeNodeModules

isLibrarytrue的时候,这个字段的默认值为false。可能的值有:

  • false:不包含node_modules包
  • array:需要包括的包名或通配符的数组
  • objectincludeNodeModules[pckName] ?? true决定是否包括。(例如:{"lodash": false}

-sourceMap

可能是一个布尔值(禁用或启用sourcemap),或一个选项(部分启用)。

选项默认值描述
inlinefalse是否将sourcemap作为数据地址保存在包中
inlineSourcesfalsesourcemap是否包含资源的内容
sourceRootpath.relative(bundle, projectRoot)资源的公共地址

CLI指令参数--no-source-maps将上述默认值设置为false

engines / browserslist

这些顶层字段分别为target.*.engines.browserstarget.*.engines设置了默认值。

指定了环境。

{
  "browserslist": ["> 0.25%", "not dead"]
}
{
  "engines": {
    "node": ">=4.x",
    "electron": ">=2.x",
    "browsers": "> 0.25%"
  }
}

alias

详见模块解析


当指定了多个入口时,哪一个package.json会被使用

所有的路径都相对于/some/dir/my-monorepo

cwd入口使用的package.json
..packages/*/src/**/*.jspackage.json
.packages/*/src/**/*.jspackage.json
packages/packages/*/src/**/*.jspackage.json
packages/pkg-apackages/pkg-a/src/index.jspackages/pkg-a/package.json
packages/pkg-a/srcpackages/pkg-a/src/index.jspackages/pkg-a/package.json



插件配置

如何使用插件,创建命名管道

这里并不是关于配置个人的插件,而是告诉Parcel哪一个插件负责哪一种文件类型

Parcel的设计是非常模块化的,所以@parcel/core包本身并非特定用于打包js或者web页面。有很多不同的插件来指定实际的行为。

这里有一份来自parcel cli使用的默认配置的摘录。通常,这些插件类型大致有三类:

  • 只有一个插件负责整体打包(bundler)
  • 一个依次执行的插件列表(namers/resolvers/reporters)
  • 每种资源/包的类型都指定了插件(transformers/packagers/optimizers)
  • runtimes是例外,因为是他们根据每个context指定的

.parcelrc

{
  "bundler": "@parcel/bundler-default",
  "transformers": {
    "*.{js,jsx,ts,tsx}": [
      "@parcel/transformer-babel",
      "@parcel/transformer-js"
    ],
    "url:*": ["@parcel/transformer-raw"]
  },
  "namers": ["@parcel/namer-default"],
  "runtimes": {
    "browser": ["@parcel/runtime-js", "@parcel/runtime-browser-hmr"],
    "service-worker": ["@parcel/runtime-js"],
    "web-worker": ["@parcel/runtime-js"],
    "node": ["@parcel/runtime-js"]
  },
  "optimizers": {
    "*.js": ["@parcel/optimizer-terser"]
  },
  "packagers": {
    "*.html": "@parcel/packager-html",
    "*": "@parcel/packager-raw"
  },
  "resolvers": ["@parcel/resolver-default"],
  "reporters": ["@parcel/reporter-cli"]
}

文件的类型是由匹配完整路径的glob指定的(管道由声明的顺序匹配),所以你可以基于输入/输出的路径,使用不同的插件:

  • transformers的glob与资源(输入)路径匹配。
  • packagersoptimizers的glob与包(输出)的路径匹配。

扩展配置
常见的例子是扩展默认配置,因此`extends`字段可以使一个配置的包,或者一个配置包的数组。

.parcelrc

{
  "extends": "@parcel/config-default",
  "transformers": {
    "*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"]
  }
}

管道

细心地读者可能注意到最后的例子中没有包含@parcel/transformer-js,是@parcel/runtime-js@parcel/runtime-packager必需要的。

这是由管道解决的。一个TS资源先经由ts管道处理,一旦@parcel/transformer-typescript-tsc插件设置了资源类型(本质上就是文件扩展名)为js,Parcel将会重新评估资源后续应该如何处理。如此一来,就会进入被指定的@parcel/config-defaultjs管道。 所以@parcel/transformer-js仍然会被执行。

一旦一个转换器(transformer)将资源类型设置为一种当前管道并不处理的类型,那么这个资源要么被放进一个另一个不同的管道,要么转换就会结束。然后本应根据当前管道继续运行的转换器将不会再运行。

如果转换器没有改变资源的类型,但是你仍想要继续处理该资源。你可以在配置中加入...来继续转换。如果你想要不改变资源类型的情况下调整资源,并让一个已经定义好的管道处理翻译/依赖关系的注册,这样是很有帮助的。

.parcelrc

{
  "extends": "@parcel/config-default",
  "transformers": {
    "*.js": ["parcel-transformer-add-comment", "..."]
  }
}

命名管道

除了基于资源类型的管道,还有命名管道。能让你用不同的方式引入一个资源。

命名管道由一个类似于protocol的语法指定。(例如: import myLogo from 'url:./logo.png'

这里有一个例子,关于如何实现不创建新包但能以内联的数据url形式存在的的url依赖关系。

.parcelrc

{
  "extends": "@parcel/cofing-default",
  "transformers": {
    "data-url": ["@parcel/transformer-inline-string", "..."]
  },
  "optimizers": {
    "data-url": ["...", "@parcel/optimizer-data-url"]
  }
}

index.js

import x from './other.js'

new Worker("data-url:./worker.js")

如你所见,...现在被用来确保data-url:./worker.js仍会被js管道处理(命名管道的标识符只会被第一个管道匹配)

如果你很好奇在不和@parcel/packager-js集成的情况下如何实现:

@parcel/transformer-inline-string包会将资源标记为内联资源。然后@parcel/packager-js内联(以一个字符${contents})这个内联包。这个内联的包会先前被@parcl/optimizer-data-url处理(将js代码编译为数据地址)。

目前转换器(transformers)和优化器(optimizers)实现了命名管道。(命名管道继承自入口资源)


官方预定义的命名管道
  • data-url:看上面的例子,它并不会被新包的url地址替换,而是一个独立的数据地址。
  • url:当以url形式引入普通的资源例如一个媒体文件时,是需要的。
import logo from 'url:./logo.svg'

document.body.innerHTML = `<img src="${logo}">`

你可能会问为什么要用这种语法。原因是因为使用这种方式,添加一种新的资源类型给Parcel不是一个破坏性的改动。(过去使用import foo from './other.html这种方式,不返回URL而是HTML内容)

也可以通过修改parcel的配置来保持以前的行为

  • bundle-text:可以用来将CSS(或者Less等)文件引入js文件中。(一些框架需要)
@myColor: #143352

span {
  color: @myColor;
}
import style from 'bundle-text:./logo.less'

class MyTest extends HTMLElement {
  constructor() {
    super()
    
    let shadow = this.attachShadow({ mode: "open" })
    
    let style = ducument.createElement('style')
    style.textContent = style
    shadow.appendChild(style)
    
    let info = document.createElement('span')
    info.textContent = this.getAttribute('label')
    shadow.appendChild(info)
  }
}

customElements.define('my-test', MyTest)





Recipes 食谱

Debugging

由于Parcel默认会自动生成soucemaps,所以使用Parcel来设置debugging不需要花太多精力。


谷歌开发工具

如果启用了sourcemap,这里不需要任何额外的配置。

例如,假设你有一个结构如下所示

├── package.json
├── src
│   ├── index.html
│   └── index.ts
└── yarn.lock

src/index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Chrome Debugging Example</title>
  </head>
  
  <body>
    <h1 id='greeting'></h1>
    <script src="./index.ts"></script>
  </body>
</html>

src/index.ts

const variable: string = "Hello, World"

document.getElementById("greeting").innerHtml = variable

(package.json只安装了parcel-bundler

你可以运行parcel src/index.html,然后在源代码里设置断点如下:


VSCode

如果文件结构与上述相似,使用谷歌开发工具,下面的lanch.json可以在vscode中配置,并与Debugger for Chrome扩展插件一起使用。

{
  "version": "0.2.0",
  "configuration": [
    {
    
      "type": "chrome",
      "request": "launch",
      "name": "Launch Chrome against localhost",
      "url": "http://localhost:1234",
      "webRoot": "${workspaceFolder}",
      "breakOnload": true,
      "sourceMapPathOverrides": {
        "../*": "${webRoot}/"
      }
    }
  ]
}

然后启动Parcel开发服务。

parcel src/idnex.html

最后一步就是在debug页面点击绿色箭头来开启debugging进程。你可以再代码中打断。最后结果如图所示:



React

相比webpack,Parcel的规范是使用HTML文件作为入口

index.html

<!DOCTYPE html>
<div id='app'></div>
<script src='./index.js'></script>

index.js

import React from 'react'
import Reactdom from 'react-dom'

const App = () => <h1>Hello!</h1>

Reactdom.render(<App />, document.getElementById('app'))

HMR(Fast Refresh)
Parcel对React快速刷新有着一流的支持。(取代了`react-hot-loader`,一个支持将HMR添加到React的用烂的插件)它能够保留刷新前后的状态(甚至是在抛出错误以后)。

更多信息可以看一看官方文档

选择的局限性
类组件中的状态在刷新前后会被刷新。

类组件逐渐变得不推荐使用,他们的状态将不会保留。

  • 无法识别使用函数表达式默认导出的组件

编辑这个组件将会重置value,因为Fast Refresh的babel插件无法检测到默认导出的声明。 Component.js

import React, { useState } from 'react'

export default () => {
  const [value] = useState(Date.now())
  
  return <h1>Hellow {value}</h1>
}

index.js

import React from 'react'
import Reactdom from 'react-dom'
import Component from './Component.js'

const App = (
  <h1>
    <Component />
  </h1>
)

Reactdom.render(<App />, ...)
  • 导出的值不是组件,状态会重置

由于另一个导出不是组件,所以value的状态会被重置。

Component.js

import React, { useState } from 'react'

const Component = () => {
  const [value] = useState(Date.now())
  
  return <h1>Hello! {value}</h1>
}

export default Component

export function utility() {
  return Date.now()
}

index.js

import React from "react";
import ReactDom from "react-dom";
import Component, { utility } from "./Component.js";

console.log(utility());

const App = (
  <h1>
    <Component />
  </h1>
);

ReactDom.render(<App />, ...);
  • 修改的资源调用了Render函数,所有状态都会重置

很容易理解,修改了App会再次调用Reactdom.render

index.js

import React from 'react'
import Reactdom from 'react-dom'

const App = () => <h1>Hello</h1>

Reactdom.render(<App />, ...)

(HMR功能由@parcel/transformer-react-refresh-babel,@parcel/transformer-react-refresh-wrap@parcel/runtime-react-refresh提供)





插件系统

概述

下图是对整个插件系统的总览

Parcel 构建

即使没有做任何复杂的事情,但如果你打算经常用Parcel,花一些时间去了解Parcel如何工作是有意义的。

Parcel的生命周期

Parcel运行由这些阶段组成:

  • 解析 resolving
  • 转换 transforming
  • 捆绑 bundling
  • 打包 packaging
  • 优化 optimizing
  • (验证) validating

解析resolving和转换transforming阶段并行工作,共同创建你的资源图表。

资源图表在捆绑bundling阶段得到编译。

打包packaging阶段获取到编译后的包,并将他们合并到文件中,每个文件都包含一个完整的包。

最后在优化optimizing阶段,获取这些包文件进行优化转换。

资源图表

在解析和转换阶段间,Parcel在你的应用或程序中找到所有的资源。每个资源都有它们自己的依赖资源,Parcel会将他们引入。

代表所有这些资源的数据结构,以及他们互相间的依赖关系,被称为资源图表。

包图表

一旦Parcel构建完整个资源图表后,开始将他们转换为包bundles。这些包是被放进一个文件的若干组资源。包通常只包含一种语言的资源。

有些资源被视为你的应用入口,会被放在单独的包中。例如,如果你index.html文件链接向about.html文件,他们不会被合并。


完整的插件列表(正确的顺序)
  • Tranformer:转换资源(为另一种资源)
    例如:将ts文件转为js文件

  • Resolver:将依赖解析为绝对路径(或者排除它们)
    例如:添加你自己的引入语法(例如import '^/foo'

  • Bundler:资源图表转为包图表
    例如:创建一个捆绑器(拆分app和node_modules)

  • Namer:为包生成文件名
    例如:创建一个捆绑器(拆分app和node_modules)

  • Runtime:以编程的方式将资源插入到包中
    例如:添加分析信息到每个包中

  • Packager:将若干组资源(包)转为文件
    例如:将所有css文件合并到一个css包中

  • Optimizer:将修改应用到包中
    例如:运行压缩器,或者转换为内联使用的数据url

  • Validator:分析资源,抛出错误和警告
    例如:类型检查

  • Config:可重用的.parcelrc包 例如:为你的模板提供一个定制的config

  • Reporter:监听构建事件
    例如:生成打包报告,并启动开发服务器



创建插件

创建插件的时候应该注意什么

插件API

插件的类型有很多种,他们看起来都非常相似但都是独立的个体。所以我们可以指定严格的标准,每个人都可以实现。

每个插件都应该遵循一些规则:

  • 无状态 —— 避免任何类型的状态,这很可能会是bug的源头。例如,多个独立的工作可能会存在同一个转换器,但他们互相之间不允许通信,所以状态并不会如期地工作。
  • 纯净的(无副作用) —— 相同的输入,一个插件必须产出相同的输出,你不能具有任何可看到的副作用,或隐式的依赖。否则,Parcel捕获会停止工作。你从不应该告诉去用户清缓存。

插件的api应该遵循一个通用的格式:

import { NameOfPluginType } from '@parcel/plugin'

export default new NameOfPluginType({
  async methodName(opts: JSONObject): Promise<JSONObject> {
    return result
  }
})

他们由模块组成,这些模块由异步函数导出:

  • 接受一个严格验证的序列化JSON对象opts
  • 接受一个严格验证的序列化JSON对象vals

如果你需要一些opts没有的东西,请和Parcel团队进行讨论。避免尝试从其他地方获取你要的信息,尤其是fs系统。


命名

插件都应该遵循以下命名系统

名称${name}必须是具有描述性的,和包目的相关的。用户必须可以从包(.parcelrc或者package.json#devDependencies中)的名字中了解到这个包简单地做了什么。

parcel-transformer-posthtml
parcel-packager-wasm
parcel-reporter-graph-visualizer

如果你的插件添加了指定工具的支持,请使用该工具的名字

parcel-transformer-es6 (bad)
parcel-transformer-babel (good)

如果你的插件是一个现有插件的重新实现,尝试解释为什么它不同

parcel-transformer-better-typescript (bad)
parcel-transformer-typescript-server (good)

版本控制

你必须指定一个package.json#engines.parcel字段,为你插件支持的Parcel版本范围。

{
  "name": "parcel-transform-imagemin",
  "engines": {
    "parcel": "2.x"
  }
}

如果你不指定这个字段,Parcel会抛出一个警告

Warning: The plugin "parcel-transform-typescript" needs to specify a
`package.json#engines.parcel` field with the supported Parcel version range.

如果你制定了字段,但是用户使用了一个不兼容的版本,他们会看到一个错误信息:

Error: The plugin "parcel-transform-typescript" is not compatible with the
current version of Parcel. Requires "2.x" but the current version is "3.1.4"

Parcel使用node-semver匹配版本的范围。



Transformer

v2.parceljs.org/plugin-syst…


Resolver

v2.parceljs.org/plugin-syst…


Bundler

v2.parceljs.org/plugin-syst…


Namer

v2.parceljs.org/plugin-syst…


Runtime

v2.parceljs.org/plugin-syst…


Packager

v2.parceljs.org/plugin-syst…


Optimizer

v2.parceljs.org/plugin-syst…


Reporter

v2.parceljs.org/plugin-syst…


Validator

v2.parceljs.org/plugin-syst…


配置文件

v2.parceljs.org/plugin-syst…


数据结构

官网未完成...


日志

v2.parceljs.org/plugin-syst…


source map

v2.parceljs.org/plugin-syst…


API

phttps://v2.parceljs.org/plugin-system/api/






coming soon ...

Languages 语言