探索 parcel(tree-shaking) 构建 vue 插件

本次我们要做的是编写一个 vue 的 Tag 组件,使用 parcel 结合 tree-shaking 进行打包。
本次要讲的重点是用 parcel 构建 vue,教大家如何去写一个可以供别的使用的 vue 组件,并发布到 npm 上。Tag 组件源码分析不是这篇文章的重点,重点是构建思路。

首先说一下为啥使用 parcel 打包而没用 webpack,出发点很简单,为了用 parcel 而用 parcel,哈哈~~~!其实 parcel 有一个优点就是零配置打包,这次使用 parcel 真的是一行配置代码没写。。。 而且 parcel-bundler 是安装到全局的,所以就是说在整个项目中都没有构建工具的痕迹(webpack 4也能做到。。。),parcel 会把你使用的工具都帮你配置好,不过得需要自己安装包。这是官网 https://zh.parceljs.org/getting_started.html,不过遗憾的是里面的 API 不全。。。很多新版本更新的 api 没有在文档里说明,需要自己去 Google,比如本次要用到的 tree-shaking 配置:--experimental-scope-hoisting,在 api 里就没有,在 https://medium.com/@devongovett/parcel-v1-9-0-tree-shaking-2x-faster-watcher-and-more-87f2e1a70f79 里面有详细说明。

下面是项目目录结构:

project
│   README.md
│   entry.js
│   package.json
│   .babelrc
│   .npmignore
│
└─── src
│   │   index.js
│   │   tag.vue
│   │
│   └─── fonts
│       │   iconfont.ttf
│       │   iconfont.woff
│   
└─── demo
│  │   App.vue
│  │   index.html
│  │   main.js
│   
└─── es
   │   index.js

因为组件比较简单,所以没有使用 vue-cli 创建,直接 npm init,然后安装需要的包
npm install vue -S
npm install vue-template-compiler babel-preset-env babel-plugin-syntax-jsx babel-plugin-transform-vue-jsx babel-helper-vue-jsx-merge-props -D

还有一些其他包,parcel 有一个 autoinstall 功能,是可以自动去 install 项目中引用但是没有下的包。
其中,vue-template-compiler 的作用是解析 .vue 文件的,babel-preset-env 解析 es6 的语法,babel-plugin-syntax-jsx babel-plugin-transform-vue-jsx babel-helper-vue-jsx-merge-props 这三个是解析 vue 中的 jsx 语法用的。

.babelrc

{
  "presets": [
    ["env", {
      "modules": false
    }]
  ],
  "plugins": ["transform-vue-jsx"]
}

entry.js:打包的入口文件,暴露出 Tag 组件

import Tag from './src';
export default Tag;

src 文件夹:存放源代码
src ---> index.js

import Tag from './tag';

function install (Vue) {
  if (install.installed) return;
  install.installed = true;
  Vue.component(Tag.name, Tag);
}

const SftcTag = {
  install,
  Tag
};

if (typeof window !== undefined && window.Vue) {
  window.Vue.use(SftcTag)
}

export default SftcTag

index.js 暴露出一个对象,包含有两个属性,Tag 是组件代码,install 为了使用 vue.use 注册 vue 插件。
重点:install 中有一个小技巧,就是这个方法接受了 Vue 参数来注册全局组件。没有在文件中直接使用 import Vue from 'vue'; 的原因是这种引用的 vue 其实是 node_modules 中的 vue/dist/vue.runtime.esm.js,而我们需要利用 tree-shaking 将 vue 抽离出去的时候使用的 vue 应该是 node_modules/vue/dist/vue.common.js,所以需要使用不同 Vue 进行注册决定了 install 方法需要使用接收的 Vue 对象来注册。细心的童鞋一定会对 vue/dist/vue.common.js 有印象,因为在使用 webpack 的 tree-shaking 的时候,使用 alias 配置 'vue$': vue/dist/vue.common.js,和这个道理是一样的,只是 parcel 帮我们做了这件事。

因为组件源码不是这篇文章的重点,所以 tag.vue 的源码放在最后贴上。


下面是最重要的部分:parcel 打包!
首先全局安装 parcel
sudo npm install -g parcel-bundler
不用任何配置文件!
在 package.json 中直接写 scripts:

"scripts": {
  "demo": "parcel demo/index.html -p 8090 -d lib",
  "build": "rm -rf lib && parcel build entry.js -d lib --out-file sftctag.min.js --experimental-scope-hoisting",
  "build:nominify": "rm -rf lib && parcel build entry.js -d lib --out-file sftctag.min.js --no-minify --no-source-maps --experimental-scope-hoisting --no-cache"
},

然后直接运行 npm run build,完事!就是这么快!

打包结果

如果不使用 tree-shaking 打包的话就会把整个的 vue 源代码也打进去,会增加 65KB 体积!加载一个包就会增加 65 KB,如果是 10 个这样的包,那性能损失真的是太大了。

下面说一个 build 命令里都干了哪些事:
rm -rf lib 每次打包删除 之前的打包目录
parcel build entry.js 执行打包命令
-d lib 输出目录文件夹名
--out-file sftctag.min.js 打包文件名
--experimental-scope-hoisting 开启 tree-shaking(parcel 1.9 版本新增特性)

build:nominify 用于查看打包出的源代码,新加了几个配置,也说明下:
--no-minify 不压缩代码
--no-source-maps 不使用 source-map
--no-cache 不使用 .cache

这样整个组件就写完了,parcel 针对这种很小的组件需求,确实是比 webpack 方便很多,但是对于大型项目,开始建议使用 webpack,原因是 parcel 毕竟是小型大包工具很多定制化构建都没有支持,另外现在parcel的相关资料确实不多也不深入。


demo 文件夹:引用 src 中的源码,做一个小 demo,一个是方便看源码的人通过 demo 理解源码。一个是在开发源码时自己做调试。

demo ---> index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>sftable</title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but sftable doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
  </body>
  <script src="./main.js"></script>
</html>

demo ---> main.js

import Vue from 'vue';
import App from './App.vue';
import Tag from '../src';

Vue.config.productionTip = false;

Vue.use(Tag);

new Vue({
  render: h => h(App),
}).$mount('#app');

demo ---> App.vue

<template>
  <div id="app">
    <div class="container">
      <sftc-tag>标签一</sftc-tag>
      <sftc-tag type="success">标签二</sftc-tag>
      <sftc-tag type="info">标签三</sftc-tag>
      <sftc-tag type="warning">标签四</sftc-tag>
      <sftc-tag type="danger">标签五</sftc-tag>
    </div>

    <div class="container">
      <sftc-tag closable>标签一</sftc-tag>
      <sftc-tag closable type="success" @close="handleClose">标签二</sftc-tag>
      <sftc-tag closable type="info">标签三</sftc-tag>
      <sftc-tag closable type="warning">标签四</sftc-tag>
      <sftc-tag closable type="danger">标签五</sftc-tag>
    </div>

    <div class="container">
      <sftc-tag closable>默认标签</sftc-tag>
      <sftc-tag size="medium" closable>中等标签</sftc-tag>
      <sftc-tag size="small" closable>小型标签</sftc-tag>
      <sftc-tag size="mini" closable>超小标签</sftc-tag>
    </div>

    <div class="container">
      <sftc-tag v-for="item in list" :key="item" closable type="success" @close="handleClose">标签{{ item }}</sftc-tag>
    </div>
  </div>
</template>

<script>

export default {
  name: 'App',
  data () {
    return {
      list: [1, 2, 3, 4, 5]
    }
  },
  methods: {
    handleClose() {
      this.list.pop();
    },
  },
}
</script>

<style>
.container {
  margin-bottom: 30px;
}
</style>

下一步是发布 npm 包,具体发布 npm 包的方法我就不多介绍了,网上配置文章很多,下面主要说一下 package.json 中的 module 字段配置,Tag 组件的 module 配置是: es/index.js,下面是 index.js 的代码:

export { default } from '../lib/sftctag.min.js';
export * from '../lib/sftctag.min.js'
import '../lib/sftctag.min.css'

很简单就是暴露出了 js 源码并引入需要的 css 文件
package.json 中引入 module 是为了拥抱 ES2015 中的 ES Module,所以也可以享受 tree-shaking 的特性,之前 package.json 一直用 main 字段做入口文件。具体解释可以参考这篇文章:《package.json 中的 Module 字段是干嘛的》


使用 Tag 组件方法:

import Vue from 'vue'
import SftcTag from 'sftctag'
Vue.use(SftcTag)
<sftc-tag>标签</sftc-tag>

因为 Tag 组件依赖使用者项目的 vue ,所以使用 Tag 组件前必须先引入 vue ,否则会报错。

使用方法和相关 API,最好都写到 README.md 中,方便使用者阅读。


总结:现在 Google 上 parcel 的资源并不多,更别说度娘了。。。但是其实 parcel 的构建思想和 webpack 很多都是归一的,所以可以对比着去探索,写完 Tag 组件之后对 parcel 有了一个大致了解,并对如果写一个 vue 插件需要注意哪些点也有了清晰的思路。学会了 Tag 组件的构建过程之后,下一步就是写其他的业务组件了,比如 Table、Form 等等。


推荐一个 parcel 的可视化包内容占用比的插件:parcel-plugin-bundle-visualiser
和 webpack 的 webpack-bundle-analyzer 很相似

parcel-plugin-bundle-visualiser


最后贴在 Tag 组件源码
src ---> tag.vue

<script>
  export default {
    name: 'sftcTag',
    props: {
      text: String,
      closable: Boolean,
      type: String,
      hit: Boolean,
      disableTransitions: Boolean,
      color: Boolean,
      size: String
    },
    methods: {
      handleClose(event) {
        event.stopPropagation();
        this.$emit('close', event);
      }
    },
    computed: {
      tagSize() {
        return this.size;
      }
    },
    render(h) {
      const classes = [ 'sftc-tag', this.type ? `sftc-tag--${this.type}` : '',
        this.tagSize ? `sftc-tag--${this.tagSize}` : '',
        {'is-hit' : this.hit}
      ];
      const tagEl = (<span class={classes} style={{backgroundColor: this.color}}>
        { this.$slots.default }
        {
          this.closable && <i class="sftc-tag__close sftc-icon-close" on-click={this.handleClose}></i>
        }
      </span>);

      return this.disableTransitions ? tagEl : <transition name="sftc-zoom-in-center">{ tagEl }</transition>;
    },
  };
</script>

<style>
  /* font */
  @font-face {
    font-family: sftc-icons;
    src: url(fonts/iconfont.woff?t=1545101934831) format("woff"), url(fonts/iconfont.ttf?t=1545101934831) format("truetype");
    font-weight: 400;
    font-style: normal
  }

  [class*=" sftc-icon-"],
  [class^=sftc-icon-] {
    font-family: sftc-icons !important;
    speak: none;
    font-style: normal;
    font-weight: 400;
    font-variant: normal;
    text-transform: none;
    line-height: 1;
    vertical-align: baseline;
    display: inline-block;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale
  }

  /* tag */
  .sftc-icon-close:before {
    content: "\e601"
  }

  .sftc-tag {
    background-color: rgba(64, 158, 255, .1);
    display: inline-block;
    padding: 0 10px;
    height: 32px;
    line-height: 30px;
    font-size: 12px;
    color: #409EFF;
    border-radius: 4px;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
    border: 1px solid rgba(64, 158, 255, .2);
    white-space: nowrap
  }

  .sftc-tag .sftc-icon-close {
    border-radius: 50%;
    text-align: center;
    position: relative;
    cursor: pointer;
    font-size: 12px;
    height: 16px;
    width: 16px;
    line-height: 16px;
    vertical-align: middle;
    top: -1px;
    right: -5px;
    color: #409EFF
  }

  .sftc-tag .sftc-icon-close::before {
    display: block
  }

  .sftc-tag .sftc-icon-close:hover {
    background-color: #409EFF;
    color: #fff
  }

  .sftc-tag--info,
  .sftc-tag--info .sftc-tag__close {
    color: #909399
  }

  .sftc-tag--info {
    background-color: rgba(144, 147, 153, .1);
    border-color: rgba(144, 147, 153, .2)
  }

  .sftc-tag--info.is-hit {
    border-color: #909399
  }

  .sftc-tag--info .sftc-tag__close:hover {
    background-color: #909399;
    color: #fff
  }

  .sftc-tag--success {
    background-color: rgba(103, 194, 58, .1);
    border-color: rgba(103, 194, 58, .2);
    color: #67c23a
  }

  .sftc-tag--success.is-hit {
    border-color: #67c23a
  }

  .sftc-tag--success .sftc-tag__close {
    color: #67c23a
  }

  .sftc-tag--success .sftc-tag__close:hover {
    background-color: #67c23a;
    color: #fff
  }

  .sftc-tag--warning {
    background-color: rgba(230, 162, 60, .1);
    border-color: rgba(230, 162, 60, .2);
    color: #e6a23c
  }

  .sftc-tag--warning.is-hit {
    border-color: #e6a23c
  }

  .sftc-tag--warning .sftc-tag__close {
    color: #e6a23c
  }

  .sftc-tag--warning .sftc-tag__close:hover {
    background-color: #e6a23c;
    color: #fff
  }

  .sftc-tag--danger {
    background-color: rgba(245, 108, 108, .1);
    border-color: rgba(245, 108, 108, .2);
    color: #f56c6c
  }

  .sftc-tag--danger.is-hit {
    border-color: #f56c6c
  }

  .sftc-tag--danger .sftc-tag__close {
    color: #f56c6c
  }

  .sftc-tag--danger .sftc-tag__close:hover {
    background-color: #f56c6c;
    color: #fff
  }

  .sftc-tag--medium {
    height: 28px;
    line-height: 26px
  }

  .sftc-tag--medium .sftc-icon-close {
    -webkit-transform: scale(.8);
    transform: scale(.8)
  }

  .sftc-tag--small {
    height: 24px;
    padding: 0 8px;
    line-height: 22px
  }

  .sftc-tag--small .sftc-icon-close {
    -webkit-transform: scale(.8);
    transform: scale(.8)
  }

  .sftc-tag--mini {
    height: 20px;
    padding: 0 5px;
    line-height: 19px
  }

  .sftc-tag--mini .sftc-icon-close {
    margin-left: -3px;
    -webkit-transform: scale(.7);
    transform: scale(.7)
  }

  /* transition */
  .sftc-zoom-in-center-enter-active,
  .sftc-zoom-in-center-leave-active {
    transition: all .3s cubic-bezier(.55,0,.1,1);
  }
  .sftc-zoom-in-center-enter,
  .sftc-zoom-in-center-leave-active {
    opacity: 0;
    transform: scaleX(0);
  }

</style>

这块就不做多解释了,按照 element 的 Tag 写的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容

  • 1 Webpack 1.1 概念简介 1.1.1 WebPack是什么 1、一个打包工具 2、一个模块加载工具 3...
    Kevin_Junbaozi阅读 6,638评论 0 16
  • 第497天锻炼,今天上班,看彭小六《洋葱阅读法》。 ——每天一荐:“《南方周末》官方公众号:‘南方周末’。”
  • 一个人的快乐,不是因为他拥有的多,而是因为他计较的少。
    舞云端阅读 231评论 0 0
  • 放暑假了,我和妈妈去市场买菜。市场里可热闹了,有的在大声吆喝,有的在细心挑选自己喜欢的菜,还有的在讨价还价。...
    能耐我何阅读 158评论 0 0
  • 游俞源村感怀 周光华 人在苍天星象中, 神奇布局伯温公。 树摇古井波光滟, 日映长塘云影空。 耕读诗书勤不断, 渔...
    剑南123阅读 264评论 0 0