如何打包一个Vue组件库

前言

大多数的教程写都是使用最直接的方式,即使用webpack的lib模式进行打包,一些教程的区别可能在于使用Vue-cli2/vue-cli3的形式而已,这里罗列一下简单的组件库模板是怎么生成的,本教程以vue-cli3为例。

最直接的方式

使用vue-cli创建一个模板(随意),以生成的目录来改造,需要做的改动如下:

  1. 将原有的src目录改为examples,用于存放示例代码,改的目的是为了命名上更好的区分功能模块
  2. 在根目录下新建一个packages目录,用于存放组件库代码
  3. 接着修改vue.config.js文件(根目录下,没有则创建一个)
module.exports = {
    // 修改src目录为examples目录
    pages: {
        index: {
            entry: 'examples/main.js',
            template: 'public/index.html',
            filename: 'index.html'
        }
    },
    // 扩展webpack配置,使packages加入编译
    chainWebpack: config => {
        config.module
            .rule('js')
            .include
                .add('/packages')
                .end()
            .use('babel')
                .loader('babel-loader')
                .tap(options => options);
    }
}
  1. 创建组件库文件,首先在packages下创建一个index.js作为组件库入口,对整个组件库进行导出
import BaseComp from './packages/BaseComp.vue';

const install = function (Vue) {
    // 全局注册组件
    Vue.component('BaseComp', BaseComp);
}

export {
    install,
    BaseComp
}

export default {
    install
}
  1. 改造package.json
    • 调整打包脚本,scripts新增打包命令
    • 调整main字段,指向打包后的lib入口:lib/your-lib-name.umd.min.js
{
    "main": "lib/your-lib-name.umd.min.js",
    "scripts": {
        "build:lib": "vue-cli-service build --target lib --name your-lib-name --dest lib packages/index.js"
    }
}
  1. 发布到npm(非必须的步骤,也可以用git仓库),业务系统直接npm i引入即可

经过上述步骤,一个基本的组件库模板就出来了。那么问题来了,build:lib命令各个参数是什么意思呢?以下来拆段分析:

  • vue-cli-service build: 打包命令,不解释~
  • --target lib: 构建目标为文件
  • --name your-lib-name: 库的名字为your-lib-name
  • --dest lib: 编译后的库文件输出目录,指定为lib
  • packages/index.js: 编译入口

运行打包命令后,就会输出打包后的文件,编译后的文件umd模块和commonjs模块都有,这里我们选择umd模块,作为库的入口

折腾

如果对组件库没有太多要求的话,直接方式打包就已经够用了,但作为一个开发就喜欢多折腾,折腾什么呢?想要让组件库进行按需加载。为了达成这个目标,就不能使用直接打包的方式一把梭搞定了,需要做以下调整:

  • 多组件入口打包
  • 导出ES Module

ok,知道了以上的目标,接下来就好调整了

多组件入口打包

在一开始的库项目中,我们是把所有组件聚合到一起导出的,即打包的入口packages/index.js。如果对文章前面的内容还有印象的话,可以知道导出语句写了两种:export && export default,那问题来了,有export语句,不是可以通过import { SomeComp } from YourComps来局部导入么?嗯,写是可以这么写,但按照我们之前的打包方式,导出的是一整个打包好的comp.umd.js文件,webpack的tree shaking是无法将不引入的其它组件剔除掉的。所以,局部导入也就是写法上的一种方便罢了,没有多少实际上的意义。实现按需加载的目标,需要先将项目目录结构进行如下转换

- packages
-- some-comp
--- index.js
--- SomeComp.js
--- index.scss
-- some-comp2
...

导出ES Module

上一步的操作,实际上是为导出ES Module做准备的。我们知道,JavaScript在ES6前是没有原生模块系统的,社区给出了一些解决方案,如AMD/CMD/UMD,ES6开始JavaScript就有了ES Module,原生的模块系统采用的是静态编译,得益于静态编译可以做到在编译阶段就可以确定哪些模块是真正被使用,哪些模块是绝对不会用到的,也就是可以做tree shaking。在vue-cli中打包成的target只支持umd/common.js,而不支持ES Module的形式,咋整呢?如果让业务系统直接引用到库中的单个组件,交由业务系统本身去编译,这事其实就算完成了。

编写转换脚本,使用babel转换一次,导出成ES Module

/* 文件所在路径 build/build-component.js */
/* 编译组件 */
const fs = require('fs-extra');
const babel = require('@babel/core');
const path = require('path');

const esDir = path.join(__dirname, '../es');
const srcDir = path.join(__dirname, '../packages');
const babelConfig = {
    configFile: path.join(__dirname, '../babel.config.js')
};

const scriptRegExp = /\.(js|ts|tsx)$/;
const isScript = path => scriptRegExp.test(path);

/**
 * 是否是文件目录
 * @param {string} dir 
 */
const isDir = dir => fs.lstatSync(dir).isDirectory();

/**
 * 编译指定目录
 * @param {string} dir 
 */
function compile(dir) {
    const files = fs.readdirSync(dir);

    files.forEach(file => {
        const filePath = path.join(dir, file);

        // scan dir
        if (isDir(filePath)) {
            return compile(filePath);
        }

        // compile js or ts
        if (isScript(file)) {
            const { code } = babel.transformFileSync(filePath, babelConfig);
            fs.removeSync(filePath);
            fs.outputFileSync(filePath.replace(scriptRegExp, '.js'), code);
        }
    });
}

// 清除目录
fs.emptyDirSync(esDir);
// 编译目录
fs.copySync(srcDir, esDir)
compile(esDir);

在package.json新增一个命令:

"build:es": "node build/build-component.js"

运行build:es即可成功导出es模块

按需引入

按需引入针对的是业务系统,当前有两个流行的方案,分别是

  • babel-plugin-import
  • babel-plugin-component

两种方案的本质,都是缩短了业务系统引入库文件时的路径罢了,以babel-plugin-import为例,使用的对比如下:

/* 不使用babel-plugin-import */
import SomeComp from 'YourComp/es/some-comp/index.js' 

/* 使用babel-plugin-import */
import { SomeComp } from 'YourComp'

可以的话肯定选择下面那种导入形式对吧,可以不用知道要导入的组件究竟在哪个目录下,要达到这个目的需要在业务系统中的babel.config.js进行如下配置

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

推荐阅读更多精彩内容