Geting Started 开始
普通webapp
Parcel CLI 构建在parcel包中。
你可以全局安装运行parcel,但更好的方式是将parcel作为开发依赖本地安装在你的项目中。
# yarn
yarn add -D parcel@next
# npm
npm install -D parcel@next
为了让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 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上,不会出现用户获取到错误或过期的包的情况。
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章节
在配置章节中解释了如何通过配置来设置(兼容的)目标。
你不需要为此添加任何配置,你只要做一件事就是在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库
- 多种输出格式
main/module
(零)配置
你和零配置的距离有多远,如何去配置parcel
Parcel运行的时候,可以同时以多种不同的方式去构建你的文件。这些叫做targets。
例如,你可以设置一个modern的目标定位较新的浏览器,和一个lagacy目标对旧的浏览器。
每个入口都会根据设置的目标进行处理。
入口是指包含了app的源代码的文件,在被Parcel编译之前。将会由以下几种形式被Parcel接受:
$ parcel <entries>$ parcel <folder(s)> 或者使用 <folder>/package.json#source./src/index.*./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#engines和engines/browserslist来配置环境。
有三个地方可以配置Parcel。
- 如果想配置cli,看cli章节
- 如果想配置打包,将在
package.json中 - 如果想配置你的文件,或者Parcel资源的进程,将在
.parcelrc文件中
迁移
略
Features 特性
api代理
配置内置的开发服务器来将特定的路径转发到另外一台服务器
为了在开发期间更好地模拟生产环境,你可以指定请求路径代理到另一台服务器(例如你的真实api服务器或一台本地测试服务器)。通过.proxyrc或者.proxyrc.json或者.proxyrc.js文件。
在JSON文件中,你可以指定一个对象,键为匹配url所对应的字符,键值为http-proxy-middleware的配置项。
{
"/api": {
"target": "http://localhost:8000/",
"pathRewrite": {
"^/api": ""
}
}
}
这会将http://localhost:1234/api/endpoint代理到http://localhost:8000/endpoint
可以处理更多复杂的配置,能够获取任何一个中间件,下面的例子,和.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)可以是:
- 一个或多个文件
- 一个或多个通配符表达式
- 一个或多个目录(详见入口指定)
# 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_ENV为production。(详见生产)
parcel build index.html
相对于serve和watch来说,build指令默认激活了作用域提升(scope hoisting)。(所以其他指令隐式地指定了--no-scope-hoist)
代码拆分 code splitting
代码按需加载
Parcel支持0配置开箱即用的代码拆分。这允许你拆分你的应用代码为几个分开的包来按照要求加载。意味着更小的初始包体积,更快的加载速度。当用户浏览应用并且需要某些模块时,再按照要求加载子包。
代码拆分是通过动态引入(import())语法实现的,工作就如同普通的import和require,但是返回一个Promise。这意味着模块可以异步加载。
import('./pages/about').then(function(page) {
// Render page
page.render()
})
pages/about.js
export function render() {
// Render the page
}
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/1或HTTP/2
| http版本 | minBundles | minBundleSize | maxParallelRequests |
|---|---|---|---|
| 1 | 1 | 30000 | 6 |
| 2(deault) | 1 | 20000 | 25 |
模块热替换
刷新你的应用,而不是重新加载整个页面
模块热替换(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
// 模块或者其一依赖刚刚跟新
})
}
某些编辑器和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字段(字符),Parcel会使用该字段,而不是main作为入口。
如果是一个对象,他的行为就像aliases,但是当target.context === 'browser'时,具有更高的优先权。
支持别名,在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" }
{
"targets": {
"app": {
"includeNodeModules": {
"react": false
}
}
}
}
当作用域提升(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来编译。
当你引入像crypto,fs,process这样的包,Parcel将自动使用列出polyfill的其中之一,否则会排除该模块。你可以在package.json中使用aliases字段。
调用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
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()
})()
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=1 parcel build src/index.html
并在Bundle Buddy中使用文件。
作用域提升
什么是作用域提升,他如何实现更小的构建体积和es模块化。
包装资源
在一些情况下,资源需要被包装,并转移到函数中。这样就否定了作用域提升的优势,因为将导出提升到顶层是我们的最初目标。
- 如果使用了顶层的
return表达式或者eval,又或者module变量自由被使用。我们不能将他加到顶层作用域(因为return停止整个打包的执行,而eval可能会使用被重命名的变量。) - 如果资源有条件地被引入(使用CommonJS的
require,或者在try/catch,或者是条件函数中,总之不可能是ES模块语法),我们不能将他加入顶层作用域,因为他的内容只有当真实需要的时候才会被执行。
sideEffects: false
当sideEffects: false在package.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 | 见下方 | 包应该在哪个环境下运行 |
| distDir | string | 指定输出的文件夹(相对于输出文件) |
| engines | package.json#engines | 优先权高于package.json#engines |
| includeNodeModules | 见下方 | 是否打包所有/都不/部分node_modules的依赖 |
| isLibrary | boolean | 就像npm库中的库 |
| minify | boolean | 是否启用压缩(具体的行为由插件决定),通过--no-minify设置 |
| outputFormat | 'global' | 'esmodule' | 'commonjs' | 导入导出的类型 |
| publicUrl | string | 包运行时的公共url |
| scopeHoist | boolean | 是否启用作用域提升,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
当isLibrary为true的时候,这个字段的默认值为false。可能的值有:
false:不包含node_modules包array:需要包括的包名或通配符的数组object:includeNodeModules[pckName] ?? true决定是否包括。(例如:{"lodash": false})
-sourceMap
可能是一个布尔值(禁用或启用sourcemap),或一个选项(部分启用)。
| 选项 | 默认值 | 描述 |
|---|---|---|
| inline | false | 是否将sourcemap作为数据地址保存在包中 |
| inlineSources | false | sourcemap是否包含资源的内容 |
| sourceRoot | path.relative(bundle, projectRoot) | 资源的公共地址 |
CLI指令参数--no-source-maps将上述默认值设置为false
engines / browserslist
这些顶层字段分别为target.*.engines.browsers和target.*.engines设置了默认值。
指定了环境。
{
"browserslist": ["> 0.25%", "not dead"]
}
{
"engines": {
"node": ">=4.x",
"electron": ">=2.x",
"browsers": "> 0.25%"
}
}
alias
详见模块解析
所有的路径都相对于/some/dir/my-monorepo
| cwd | 入口 | 使用的package.json |
|---|---|---|
| .. | packages/*/src/**/*.js | package.json |
| . | packages/*/src/**/*.js | package.json |
| packages/ | packages/*/src/**/*.js | package.json |
| packages/pkg-a | packages/pkg-a/src/index.js | packages/pkg-a/package.json |
| packages/pkg-a/src | packages/pkg-a/src/index.js | packages/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与资源(输入)路径匹配。packagers和optimizers的glob与包(输出)的路径匹配。
.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,然后在源代码里设置断点如下:
如果文件结构与上述相似,使用谷歌开发工具,下面的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'))
更多信息可以看一看官方文档
选择的局限性
类组件中的状态在刷新前后会被刷新。
类组件逐渐变得不推荐使用,他们的状态将不会保留。
- 无法识别使用函数表达式默认导出的组件
编辑这个组件将会重置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运行由这些阶段组成:
- 解析 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:监听构建事件
例如:生成打包报告,并启动开发服务器
创建插件
创建插件的时候应该注意什么
插件的类型有很多种,他们看起来都非常相似但都是独立的个体。所以我们可以指定严格的标准,每个人都可以实现。
每个插件都应该遵循一些规则:
- 无状态 —— 避免任何类型的状态,这很可能会是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
Resolver
Bundler
Namer
Runtime
Packager
Optimizer
Reporter
Validator
配置文件
数据结构
官网未完成...
日志
source map
API
phttps://v2.parceljs.org/plugin-system/api/
coming soon ...