打包神器之Parcel使用指南

簡介

Blazing fast, zero configuration web application bundler.官網 github

快速,零配置的 Web 應用程序打包器。

主要有以下特點:

  • 快速打包: 利用cpu多核,和緩存機制打包速度更快
  • 自動轉換: 支持目前絕大部份資源文件的轉換 支持文件類型
    • postCss,postHtml 以及各種對應插件與配置
    • babel,typescript,coffeeScript,等JS預編譯
    • sass,less,stylus 等CSS預編譯語言
    • 支持 vue,react,angular 主流前端框架
    • yaml,json,以及各種圖片資源
  • 打包所有資源: 支持 brower| node | electron 環境,項目內所引用的資源文件會全部編譯打包
  • 零配置代碼拆分: 基本無任何配置即可以開發,支持代碼動態加載,自動拆分打包文件
  • 模塊熱替換: 支持代碼改動支持熱更新,socket實時無刷新更新web頁面
  • 友好的錯誤記錄: 支持不同級別日志顯示,默認只會顯示錯誤信息

其他打包工具對比

  • grunt
    node老牌的主流的構建工具,需要使用gruntfile編寫構建任務,然后運行grunt-cli命令執行任務,功能強大,擁有大量的可用插件。但是編寫構建任務復雜、繁重,會生成構建中間文件,學習成本高。
  • fis3
    是百度公司內部使用的構建工具,后來開源了,文檔相當齊全,也需要fisconfig配置文件。由于百度團隊集成了常用的一些前端構建任務的插件,配置簡單方便。但是現在基本不維護了,對于node新版本支持也不是非常好。
  • gulp
    node基于流的自動化構建工具。需要使用gulpfile編寫構建任務,然后運行gulp命令執行任務。相比grunt易于使用,構建快速,易于學習,插件高質,不產生中間文件。
  • webpack
    webpack是一個現代 JavaScript 應用程序的靜態模塊打包器,也是目前流行的打包工具,是的高度可配置的。從 webpack v4.0.0 開始,可以不用引入一個配置文件。配置內容相當齊全,性能高,也集成了各種個樣的插件?;緷M足了前端開發絕大部分需求。缺點就是配置相當的多,不適合新手使用。
  • roadhog
    是webpack上層封裝,本質上是簡化了一系列webpack的配置,生成最終的wepack配置文件,是antd-pro的官方的cli工具。它效果與parcel很像,適用范圍窄,打包效率低。
  • create-react-app
    集成了react開發的一系列資源,還有命令行工具,對于創建的新的react項目非常好,但是限制也是相當明顯的。只適用react

快速使用

  1. 下載全局依賴
npm install -g parcel-bundler
  1. 新建html,css,js文件
// index.html

<html>
  <body>
    <p>hello world</p>
    <script src="./index.js"></script>
  </body>
</html>

// index.js

import './index.css';
alert(1);

// index.css

body {
  background: red;
}
  1. 本地開發
parcel serve index.html
  1. 打包應用
parcel build index.html 

命令行選項介紹

  1. parcel主要命令
parcel -h # 查看幫助信息
parcel serve -h # 本地開發模式,會啟動一個默認1234端口的本地服務器,代理到配置的目標文件,前端頁面開發為主
parcel watch -h # 監聽文件改動模式,不會啟動默認本地服務器,服務端開發為主
parcel build -h # 編譯源文件并打包到目標?文件夾
  1. parcel重要選項介紹
  • server、watch 命令一致
  -p, --port <port> # ?本地服務器啟動端口
  --open # 是否打開默認瀏覽器
  --public-url # 靜態資源文件夾路徑
  --no-hmr # false 是否開啟代碼熱更新
  --no-cache  # false 是否啟用緩存機制,用于緩存引起的代碼更新不及時的問題排查
  --no-source-maps # false 是否啟用source-maps文件,主要用于錯誤排查與定位
  --no-autoinstall # false 很多資源文件的loader包是自動下載的,代碼會將沒有路徑的包當成npm包自動下載,發現代碼自動下載不需要的npm包是,需要開啟這個屬性
  -t, --target [target] # browser 代碼目標環境 node | browser | electron
  --log-level <level> # 1 日志輸入級別 0 | 1 | 2 | 3 對應 不輸出日志 | 僅錯誤日志 | 警告與錯誤日志 | 所有日志 
  • build 命令
-d, --out-dir <path> # 打包目標文件夾 默認 "dist"
-o, --out-file <filename> # 項目入口文件的文件名,默認與 --public-url 一致
--no-minify # 打包時不壓縮源文件
--detailed-report # 打印詳細打包資源報告

主要使用場景實例--- 所有demo無需手動寫依賴與配置

由于parcel可以自動加載 vue,react...等框架的依賴包,所以不需要自己特地去將依賴加到package.json中,直接寫vue,react組件即可。

vue demo

  1. 編寫src/app.vue
<template lang="html">
    <div id="app">
      <h1>Hello Parcel vue app e?“| e??€</h1>
    </div>
  </template>
  
<script>
    export default {
        name: 'app'
    }
</script>

<style lang="css">
    #app {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100%;
    }
    h1 {
        font-weight: 300;
    }
</style>
  1. 編寫src/main.js
import Vue from 'vue'
import App from './app.vue'

new Vue({
  el: '#app',
  render: h => h(App)
});
  1. 編寫index.html
<html>
<head>
    <title>Welcome to Vue</title>
</head>
<body>
    <div id="app"></div>
    <script src="src/main.js"></script>
</body>
</html>
  1. 運行與打包
# 運行
parcel serve pathto/index.html --no-cache
# 打包
parcel build pathto/index.html --public-url . --no-source-maps --no-cache --detailed-report

react-typescript demo

  1. 編寫components/App.tsx組件
import * as React from 'react'

export default class App extends React.Component<any, any> {
    render() {
        return (
            <div className="container">
                <h1>typescript react component</h1>
            </div>
        )
    }
}
  1. 編寫index.tsx入口
import * as React from 'react'
import { render } from 'react-dom'

import App from './components/App'

render(<App />, document.getElementById('root'))
  1. 編寫index.html
<html lang="en">
<head>
  <title>Parcel with Typescript</title>
</head>
<body>
  <div id="root"></div>
  <script src="./index.tsx"></script>
</body>
</html>
  1. 開發與打包
parcel serve pathto/index.html --no-cache
# 打包
parcel build pathto/index.html --public-url . --no-source-maps --no-cache --detailed-report

多頁面應用

  1. 建立pages文件夾放html文件
// index.html
<html lang="en">
<head>
    <title>index</title>
</head>
<body>
    <nav>
        <ul>
            <li><a href="./page1.html">第一頁</a></li>
            <li><a href="./page2.html">第二頁</a></li>
            <li><a href="./page3.html">第三頁</a></li>
        </ul>
    </nav>
    <h1>這是首頁</h1>
</body>
</html>

// page1.html
<html lang="en">
<head>
    <title>Page 1</title>
</head>
<body>
    <h1>第一頁</h1>
    <a href="./index.html">返回首頁</a>
    <script src="../js/page1.js"></script>
</body>
</html>

// page2.html
<html lang="en">
<head>
    <title>Page 2</title>
</head>
<body>
    <h1>第二頁</h1>
    <a href="./index.html">返回首頁</a>
    <script src="../js/page2.js"></script>
</body>
</html>

// page3.html
<html lang="en">
<head>
    <title>Page 3</title>
</head>
<body>
    <h1>第三頁</h1>
    <a href="./index.html">返回首頁</a>
    <script src="../js/page3.js"></script>
</body>
</html>
  1. 建立css文件夾放less文件
// base.less
body {
    background: grey;
    color: #ffffff;
}

// page1.less
body {
    background: red !important;
}

// page2.less
body {
    background: black !important;
}

// page3.less
body {
    background: green !important;
}
  1. 建立js文件夾放js文件
// base.js
import '../css/base.less';

export const baseFunc = (text) => {
    alert(`baseFunc --- by ${text}`);
}

// page1.js
import '../css/page1.less'
import { baseFunc } from './base'

baseFunc('page1');

// page2.js
import '../css/page2.less'
import { baseFunc } from './base'

baseFunc('page2');

// page3.js
import '../css/page3.less'
import { baseFunc } from './base'

baseFunc('page3');
  1. 開發與打包 注意這里使用 * 號匹配html路徑
# 開發 
parcel serve pathto/pages/*.html --no-cache
# 打包
parcel build pathto/pages/*.html --public-url ./ --no-source-maps --no-cache --detailed-report

parcel原理簡介

parcel flow.jpg

寫一個parcel 識別 **.json2文件parcel 插件

  1. 寫一個 Asset 實現類 myAsset.js
const path = require('path');
const json5 = require('json5');
const {minify} = require('terser');
const {Asset} = require('parcel-bundler');

class MyAsset extends Asset {
  constructor(name, options) {
    super(name, options);
    this.type = 'js'; // set the main output type.
  }

  async parse(code) {
    // parse code to an AST
    return path.extname(this.name) === '.json5' ? json5.parse(code) : null;
  }

  // async pretransform() { // 轉換前
  //   // optional. transform prior to collecting dependencies.
  // }

  // collectDependencies() { // 分析依賴
  //   // analyze dependencies
  // }

  // async transform() { // 轉換
  //   // optional. transform after collecting dependencies.
  // }

  async generate() { // 生成代碼
    // code generate. you can return multiple renditions if needed.
    // results are passed to the appropriate packagers to generate final bundles.
    let code = `module.exports = ${
        this.ast ? JSON.stringify(this.ast, null, 2) : this.contents
    };`;
  
    if (this.options.minify && !this.options.scopeHoist) {
      let minified = minify(code);
      if (minified.error) {
          throw minified.error;
      }
      code = minified.code;
    }

    return [{
      type: 'json2',
      value: this.contents
    }, {
      type: 'js',
      value: code
    }];
  }

  // async postProcess(generated) { // 生成代碼完成之后操作
  //   // Process after all code generating has been done
  //   // Can be used for combining multiple asset types
  // }
}

module.exports = MyAsset;
  1. 寫一個 Packager 實現類 myPackager.js
const {Packager} = require('parcel-bundler');

class MyPackager extends Packager {
  async start() { // 文件頭之前的內容
    // optional. write file header if needed.
    await this.dest.write(`\n123-before\n`);
  }

  async addAsset(asset) { // 文件內容
    // required. write the asset to the output file.
    await this.dest.write(`\n${asset.generated.json2}\n`);
  }

  async end() { // 寫在文件尾 的內容
    // optional. write file trailer if needed.
    await this.dest.end(`\nabc-after\n`);
  }
}

module.exports = MyPackager;
  1. 編寫插件方法myPlugin.js
module.exports = function (bundler) {
    bundler.addAssetType('.josn2', require.resolve('./MyAsset'));
    bundler.addPackager('json2', require.resolve('./MyPackager'));
};
  1. 發布到npm中
    將這個包發不到npm時,需要加parcel-plugin-前綴,然后它就會被parcel自動識別。
  2. 使用插件兩種方式
  • 只需要將parcel-plugin-前綴的包,加入到package.json中,pacel在初始化的時候就會自動加載這些插件。
  • 通過parcel類使用
const path = require('path');
const Bundler = require('parcel-bundler');
const bundler = new Bundler(file, options);

// 獲取node命令行的參數
const args = process.argv.splice(2);

// Entrypoint file location
const file = path.join(__dirname, './src/index.html');
// Bundler options
const options = {
  outDir: './demo_custom/dist', // The out directory to put the build files in, defaults to dist
  //   outFile: './demo_custom/dist/index.html', // The name of the outputFile
  //   publicUrl: './demo_custom/dist', // The url to server on, defaults to dist
  watch: true, // whether to watch the files and rebuild them on change, defaults to process.env.NODE_ENV !== 'production'
  cache: false, // Enabled or disables caching, defaults to true
  cacheDir: '.cache', // The directory cache gets put in, defaults to .cache
  minify: true, // Minify files, enabled if process.env.NODE_ENV === 'production'
  target: 'browser', // browser/node/electron, defaults to browser
  https: false, // Serve files over https or http, defaults to false
  logLevel: 3, // 3 = log everything, 2 = log warnings & errors, 1 = log errors
  hmrPort: 0, // The port the HMR socket runs on, defaults to a random free port (0 in node.js resolves to a random free port)
  sourceMaps: args[0] !== 'build', // Enable or disable sourcemaps, defaults to enabled (not supported in minified builds yet)
  hmrHostname: '', // A hostname for hot module reload, default to ''
  detailedReport: args[0] === 'build', // Prints a detailed report of the bundles, assets, filesizes and times, defaults to false, reports are only printed if watch is disabled
  open: true,
  port: 1234,
  production: args[0] === 'build'
};

const runBundle = async () => {
  // Initializes a bundler using the entrypoint location and options provided
  const bundler = new Bundler(file, options);
  bundler.addAssetType('.json2', require.resolve('./myAsset')); // 引入剛剛寫好的資源識別類 【識別xx.json2類型文件】
  bundler.addPackager('json2', require.resolve('./myPackager')); // 引入剛剛寫好的打包類【打包 xx.json2 類型文件】
  if (cli === 'serve' && options.open) {
    const server = await bundler.serve(options.port);
    if (server) {
        await require('parcel-bundler/src/utils/openInBrowser')(`http://localhost:${options.port}`, true)
    }
  } else {
    childProcess.exec(`rm -rf ${path.join(__dirname, './dist')}`);
    bundler.bundle();
  }
};

parcel實踐應用show-pages

該項目簡單二次應用了parcel 工具,使用了postcss,posthtml,以及一些模版替換的,純前端項目,已可以實現基本功能。該項目現已稍微擱置,沒有進步繼續編寫。后邊有時間,會繼續完善這個項目,代碼僅供參考。非常希望有小伙伴與我一起該貢獻

總結

  • parcel優點
    • 簡單、簡單、簡單 (重要的事三遍)
    • 零配置
  • parcel缺點
    • 不能使用alias路徑別名
    • 不能按照自己意愿構建資源文件目錄,都打包到目標的根目錄
    • 會自動加載很多依賴包,在開發時這個不是很影響,對于新手也不影響
    • 對于大項目來說,自定義需求不太容易實現,但是webpack可實現
    • 遇到問題只能 github issue, 瀏覽源碼
  • 簡單結論
    • 代碼思想非常好,純JS的面向對象編寫,可以學習源碼
    • 適用于前端新手或非前端人員
    • 適合于新框架、新編譯語言的學習
    • 適用于簡單前端項目
    • 使用于二次開發,個性化的打包工具

番外篇【源碼解讀,待更新...】


引用博文及項目鏈接

本文僅是自己個人觀點,如果紕漏與錯誤,歡迎評論指正

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容