大前端进阶~如何构建组件库

前言

在日常开发过程中,构建组件库是必不可少的一环,此篇文章就是描述如何搭建一个完整的组件库,解决组件库开发发布过程中的如下问题:

1.如何在最少的依赖下快速开发一个vue组件。
2.如何将所有的包放置在一个git仓库内。
3.如何将git仓库内的所有包一键发布。
4.如何管理所有包的依赖,减少包的体积。
5.如何快速创建组件示例。
6.如何打包组件,webpack?

快速原型开发

开发组件和开发项目是不一样的,在开发组件的时候,我们希望能够有一种工具能够快速针对某个vue文件搭建开发环境,并且在发布的时候能够对其进行打包编译,此时我们可以使用@vue/cli-service-global。

  • 全局安装
    此包必须全局安装:

npm install -g @vue/cli-service-global

  • 创建vue文件
    在根目录下创建App.vue文件:
<template>
  <h1>Hello!</h1>
</template>
  • 启动开发服务器
    在命令行中执行:

vue serve

入口可以是 main.js、index.js、App.vue 或 app.vue 中的一个。你也可以显式地指定入口文件:

vue serve App.vue

  • 执行打包
    vue build

打包完成后,打包结果会放到dist目录下, 默认情况下,会打包生成一个应用,该应用包含html和资源文件,可以直接部署为静态站点。但是通常情况下,我们需要将组件打包成一个库,以便发布后供项目使用。

打包成库需要指定构建目标:

vue build --target lib

添加构建目标后,执行打包,dist目录中包含各种规范的js文件和一个demo示例html。

目前为止,快速开发vue组件已经完成,我们可以快乐的开发各种组件,但是,当所需开发的组件慢慢变多之后,文件的组织方式成为我们需要考虑的事情。
可以想到有以下三种方式组织文件结构:

1.每一个组件都是一个单独的仓库。
2.一个仓库中包含多个组件vue文件,作为一个包发布。
3.一个仓库中包含多个组件包,每个组件包单独发布。

第一种方式,每一个组件都是一个单独仓库,虽然有利于组件开发,但是组件维护起来比较麻烦。组件越多,需要维护的仓库也就越多,当其中部分组件依赖的如lodash需要升级时,我们需要一个个进行升级,比较麻烦。

第二种方式,将所有的组件作为一个包发布,虽然维护比较方便,但是发布后,别人只想使用其中的一个组件时,会需要把整个组件库引入,如果不提供按需加载,那么会造成项目中引入很多不必要的代码。

第三种方式可参考下文。

monorepo

当我们查看 vue3 源码时,可以看到,仓储结构如下:

packages
├── compiler-core
    ├──_tests_ #单元测试
    ├──src #源文件目录
    ├──package.json
├── compiler-dom
    ├──_tests_ #单元测试
    ├──src #源文件目录
    ├──package.json
package.json

这个就是典型的monorepo ,monorepo是项目代码的一种管理方式,指在一个仓库中管理多个模块/包。

monorepo追求的是在一个仓库中管理多个模块,每个模块有独立的package.json管理各自依赖,同时在项目根目录下可以通过命令安装或升级模块依赖,并提供了一个模块共享的node_modules。

yarn workspace

yarn workspace 是实现monorepo的一种方式。

使用yarn workspace要求在根目录的package.json中添加如下属性:

{
    "private": true,
    "workspaces": ["packages/*"]
}

private属性指定根目录是私有的,不会被发布工具发布到npm上。

workspace属性指定组件所在文件夹,支持通配符。

修改完package.json之后,按照vue-next的项目结构在packages文件夹下创建input测试组件。

假设,自定义的input组件依赖dayjs包,可以在根目录下执行如下命令安装:

yarn workspace m-input add dayjs

其中m-input并不是packages下组件文件夹的名称,而是组件文件夹下package.json中的name属性值。

安装完成后,dayjs会自动添加到input组件的package.json下,但是包下载到了根目录下的node_modules文件夹中,这样做可以更好的管理多组件包的依赖。如果当前组件依赖的包版本和其他组件依赖的包版本不一样,如其他组件依赖lodash@4,当前组件依赖lodash@3, 此时依赖包会被下载到当前组件文件夹下的node_modules中。

通过yarn workspace可以执行某个组件下的npm scripts,如给input组件添加一个build命令,可以在根目录下通过如下命令执行build:

yarn workspace m-input run build

对于build这种命令,几乎所有组件都需要,那么yarn workspace提供了一个快捷命令,可以一键执行所有组件包的build命令:

yarn workspaces run build

storybook

目前为止,仓库的整体文件结构和组件库的依赖包管理都已经完成了,可以愉快的开发组件了,当组件开发完成后,一般开发人员都会编写相应的使用文档,文档中包含相应的使用示例.

storybook是可视化的组件管理展示平台,支持在隔离的开发环境中,以交互式的方式展示组件,支持vue,react等。

安装使用:

npx -p @storybook/cli sb init --type vue

yarn add vue -W

yarn add vue-loader vue-template-compiler --dev -W

修改配置:

安装完成之后,在根目录的.storybook文件夹下存放着storybook使用的所有配置文件,修改main.js中stories属性,将其指向packages所有组件下的.stories.js文件。

"stories": [
    "../packages/**/*.stories.mdx",
    "../packages/**/*.stories.@(js|jsx|ts|tsx)"
]

添加组件示例:

在input组件包中添加 Input.stories.js 文件:

import MInput from './index'
export default {
    title: 'MInput',
    component: MInput
};

export const Text = () => ({
    components: { MInput },
    template: '<m-input />',
});

export const Password = () => ({
    components: { MInput },
    template: '<m-input type="password" placeholder="请输入密码"/>',
});

其中默认导出是storybook页面左侧导航栏,每一个具名导出都是一个样例。

最终执行yarn storybook,打开站点:


lerna

lerna 是babel团队开源的用于管理多包仓库的工具,也可以用于实现monorepo。

安装lerna:

npm install lerna -g

初始化lerna:

lerna init

会在项目根目录下添加lerna.json配置文件。

可以使用lerna管理项目依赖:

如果当前form自定义组件依赖input自定义组件,可以使用:

lerna add input --scope=form

还可以使用import命令导入本地包:

lerna import <path-to-external-repository>

通过exec和run执行包里面的相关命令

lerna run --scope my-component test

通过clean命令一键清除所有包的node_modules目录:

lerna clean

learn最主要的功能是一键发布所有包的npm上:

lerna publish

发布包到npm需要登录,可以通过 npm whoami 查看当前登录用户,通过 npm login 进行登录。

单元测试

单元测试是组件化开发中必不可少的部分

安装依赖:

npm i jest @vue/test-utils vue-jest babel-jest -D

1.添加jest配置文件jest.config.js

module.exports = {
    "testMatch": ["**/_tests_/**/*.[jt]s?(x)"],
    "moduleFileExtensions": [
        "js",
        "json",
        // 告诉 Jest 处理 `*.vue` 文件
        "vue"
    ],
    "transform": {
        // 用 `vue-jest` 处理 `*.vue` 文件
        ".*\\.(vue)$": "vue-jest",
        // 用 `babel-jest` 处理 js
        ".*\\.(js)$": "babel-jest"
    }
}

1.添加babel配置文件babel.config.js

module.exports = {
  presets: [
    [
      '@babel/preset-env'
    ]
  ]
}

1.添加测试命令
"test": "jest"

1.添加测试文件
在组件包的tests文件夹下添加相关js文件,如input包下面添加input.test.js

import input from '../src/index.js'
import { mount } from '@vue/test-utils'

describe('m-input', () => {
  test('input-text', () => {
    const wrapper = mount(input)
    expect(wrapper.html()).toContain('input type="text"')
  })
})

1.执行测试命令
yarn test

测试可以在命令行中看到单元测试执行结果:


rollup打包

rollup是一个基于ESM的模块打包工具,和webpack相比,其打包结果更小,因此适合打包框架或者组件库。

安装必须的依赖:

npm i rollup rollup-plugin-terser rollup-plugin-vue@5.1.9 vue-template-compiler -D

需要注意的是安装vue时需要指定版本,否则会安装vue3。

  • 单组件打包
    1.添加配置文件
    在组件中添加rollup.config.js文件,该文件是rollup打包的配置文件,指定起始文件,输出文件位置及格式,插件。
import { terser } from 'rollup-plugin-terser'
import vue from 'rollup-plugin-vue'

module.exports = {
    input: 'src/index.js',
    output: [
        {
            file: 'dist/index.js',
            format: 'es'
        }
    ],
    plugins: [
        vue({
            css: true,
            compileTemplate: true
        }),
        terser()
    ]
}

1.添加可执行命令
在package.json文件的scripts属性下添加打包命令:

"build": "rollup -c"

-c指的是使用当前项目目录下的配置文件rollup.config.js

1.执行命令
yarn build

执行完毕之后,可以看到打包结果。

  • 多组件打包
    虽然可以用上述单组件打包的方式为每一个组件打包,但是这样比较麻烦,可以在项目根目录下通过一个配置文件打包所有组件。

此时需要添加额外依赖:

npm i @rollup/plugin-json rollup-plugin-postcss @rollup/plugin-node-resolve cross-env -D

1.为组件指定入口文件
在每个包下的package.json文件中添加main和module属性:

"main": "dist/cjs/index.js",

"module": "dist/es/index.js",

1.设置环境变量
利用cross-env设置环境变量,区分开发环境和生产环境:

"build:prod": "cross-env NODE_ENV=production rollup -c",

"build:dev": "cross-env NODE_ENV=development rollup -c"

1.添加配置文件
在项目的根目录下添加rollup.config.js文件,该文件会遍历packages文件夹下的所有文件夹并打包:

import fs from 'fs'
import path from 'path'
import json from '@rollup/plugin-json'
import vue from 'rollup-plugin-vue'
import { terser } from 'rollup-plugin-terser'
import postcss from 'rollup-plugin-postcss'
import { nodeResolve } from '@rollup/plugin-node-resolve'

const isDev = process.env.NODE_ENV !== 'production'

// 公共插件配置
const plugins = [
    vue({
        css: true,
        compileTemplate: true
    }),
    json(),
    nodeResolve(),
    postcss({
        // 把 css 插入到 style 中
        // inject: true,
        // 把 css 放到和js同一目录
        extract: true
    })
]

// 如果不是开发环境,开启压缩
isDev || plugins.push(terser())
// packages 文件夹路径
const root = path.resolve(__dirname, 'packages')


module.exports = fs.readdirSync(root)
    .filter(item => fs.statSync(path.resolve(root, item)).isDirectory())
    .map(item => {
        // 获取每个包的配置文件
        const pkg = require(path.resolve(root, item, 'package.json'))
        return {
            input: path.resolve(root, item, 'src/index.js'),
            output: [
                {
                    exports: 'auto',
                    file: path.resolve(root, item, pkg.main),
                    format: 'cjs'
                },
                {
                    exports: 'auto',
                    file: path.join(root, item, pkg.module),
                    format: 'es'
                },
            ],
            plugins: plugins
        }
    })

此时执行打包命令,可以一次性为所有组件包打包。

现在有个问题,每次打包的时候需要删除上次打包结果,因此需要添加删除命令:

安装依赖包:

npm i -D rimraf

为每个组件包添加del命令:

"del": "rimraf dist"

在根目录下添加clean命令:

"clean": "yarn workspaces run del"

此时执行yarn clean 就可以清除所有包的dist目录。

plop模版

截止到目前为止,项目的整体结构已经完成,接下来就是无休止的添加组件了,但是考虑到每个组件的初始化有很多相同的工作需要手动完成,此时可以通过plop将这部分工作交给机器。

安装依赖:

npm i plop -D

1.创建模版文件
在项目中添加plop-template/component文件夹,此文件夹下放置创建组件用的所有模版文件。

1.添加plopfile.js
该文件是plop插件执行的入口文件:

module.exports = plop => {
    plop.setGenerator('component', {
      description: 'create a custom component',
      prompts: [
        {
          type: 'input',
          name: 'name',
          message: 'component name',
          default: 'MyComponent'
        }
      ],
      actions: [
        {
          type: 'add',
          path: 'packages/{{name}}/src/{{name}}.vue',
          templateFile: 'plop-template/component/src/component.hbs'
        }
      ]
    })
}

为plop添加一个可执行的命令,该命令会询问用户组件的名称,然后将模版中所有的文件拷贝到packages相关文件夹内。

1.添加scripts命令
"plop": "plop"

此时在命令行中执行yarn plop component就可以创建组件了。


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