基于Vuecli 开发vue组件库

1,818 阅读9分钟

组件库这个词在前端领域相信所有人都不会陌生。像 element-ui vant 等等... 都是社区比较优秀的组件库,那我们能不能自己也搞一个组件库呢?能!必须能!那今天咱们就来盘一盘。

前提

开始之前请先 clone element-ui,并自行使用 Vuecli 创建一个项目。

示例:

git clone https://github.com/ElemeFE/element.git

vue create vue-cmop-base

改造项目目录

1,去除脚手架叫原有的 srcassets components ,新增 src 平级的 packages 目录

2,复制 element-uibuttoncard 组件(element-ui 包文件位置 packages/);以及配套的样式,在 packages 中新增 theme 文件夹用于存放样式;

3,尽量保持样式层级不变,只复制有用到的样式(element-ui 样式文件位置 pockages/theme-chalk/src/) ;目前用到的样式包括

  • common/var.scss

  • fonts 目录下所有文件;

  • mixins 目录下所有文件;

  • theme-chalk/button.scsstheme-chalk/card.scsstheme-chalk/icon.scsstheme-chalk/index.scss

完成之后大概是下面这样子。

image.png

4,packages 中新增 index.js 作为所有组件的统一出口;

import Button from './button';
import Card from './card';

const components = [Button, Card];

const install = function (Vue) {
  // 批量注册
  components.forEach(component => {
    Vue.component(component.name, component);
  });
};

// 注入 Vue
if (typeof window.Vue !== 'undefined' && window.Vue) {
  install(window.Vue);
}

export default {
  // 导出 install Vue.use() 前提必须实现 install 方法;
  install,
  // 组件导出
  Button,
  Card
};

测试组件是否可用

  • src/main.js 中新增注册组件相关代码;
// ... ...
import testComp from '../packages';
import '../packages/theme/index.scss';

Vue.use(testComp);
// ... ...
  • src/App.vue 使用组件测试;此时启动服务,肯定会出现报错;解决方案在下一节;依赖安装
<template>
  <!--引入组件 -->
  <el-button size="mini" type="primary">test-btn</el-button>
</template>
<!-- ... ... -->

依赖安装

vuecli 创建的项目,默认是不能解析 scss 文件的,所以需要下载对应的依赖;

直接安装 sass-loader 可能会出现以下错误 Syntax Error: TypeError: this.getOptions is not a function;这是由于 sass-loader 版本过高导致的问题;

# 安装大版本为7的最新版本
npm intall sass-loader@7 -D

然后会再次出现错误 Syntax Error: Error: Cannot find module 'node-sass';此时 如果你直接安装 npm install node-sass -D 会出现以下错误;Syntax Error: Error: Node Sass version 7.0.1 is incompatible with ^4.0.0.;根据提示我们需要安装 4 版本的 node-sass

# 安装大版本为4的最新版本
npm install node-sass@4 -D

安装完成后重新运行项目会发现又出现一个问题 Syntax Error: $--group-option-flex: 0 0 math.div(1, 5) * 100% !default;; 此问题的出现方式主要 node-sass 中不支持 math 相关语法,而 element-ui 中使用的是 sass 从而不会出现此类问题;当然这也不是此次研究的重点; 解决方案:

  • 直接删除这一行代码,因为本示例中没有用到相关样式;theme/common/var.scss 中 搜索 $--group-option-flex 关键字进行注释;

  • 安装 sass 解决此问题;npm install sass -D ;当然我选择了后者

来吧展示! showtime

showTime.png

至此我们的组件库算是已经实现一半了;再来回头看整个项目;发现会有一些不合理的地方;

  • 为什么 src 要做为项目的演示目录;其实使用 examples 更合理;安排 ~~~
// vue.config.js
const path = require('path');

module.exports = {
  // 修改入口 同时 src 目录改名为 examples
  configureWebpack: {
    entry: path.join(__dirname, 'examples/main.js')
  }
};

项目打包

组件库已经完成,终归还是要打包供别人使用;Vuecli 中提供了一种库模式打包,当然我们也可以自己定义打包方式。

库模式打包

构建目标 首先在 package.jsonscripts 中新增一条命令;

npm run build:lib: "vue-cli-service build --target lib --dest lib --name test-comp-base packages/index.js"

vue-cli-service build 详解:

用法:vue-cli-service build [options] [entry|pattern]

选项:

  --mode        指定环境模式 (默认值:production)
  --dest        指定输出目录 (默认值:dist)
  --modern      面向现代浏览器带自动回退地构建应用
  --target      app | lib | wc | wc-async (默认值:app)
  --name        库或 Web Components 模式下的名字 (默认值:package.json 中的 "name" 字段或入口文件名)
  --no-clean    在构建项目之前不清除目标目录
  --report      生成 report.html 以帮助分析包内容
  --report-json 生成 report.json 以帮助分析包内容
  --watch       监听文件变化

执行完以上命令,不出意外项目中会出现 lib 文件夹;这个就是我们组件库的源码了;

build.png

此时问题出现了;我们发现打包后的代码没有包含我们想要的 css 也就是说样式丢了?库打包其实他是会生成 css 文件的;只是这个 css 在我们的工程中没有引入

我不确定这个是不是 VueCli 官方认定的最佳实践。还是我得理解有问题;就目前我的理解此种方式打包应该是不能实现 按需引入 组件功能;不过既然支线任务出来了还是要做的。

  • packages/index.js 中导入 theme/index.scss 文件。

  • 再次执行打包,可以看出这里打包之后的包就已经包含了 css 文件。之后可以走 托管到 npm 流程

build1.png

  • 在 新的 Vue 项目 安装组件并且在 src/main.js 中注册组件和 src/App.vue 中使用组件;
// src/main.js
import testComp from 'test-comp-base';
import 'test-comp-base/lib/test-comp-base.css';

Vue.use(testComp);
// ... ...

// src/App.vue
<template>
  <el-button size="mini" type="primary">
    test-btn
  </el-button>
</template>;

vue.config.js 构建打包

此种打包方式总的来说就是 规定多个入口逐个打包,最后生成不同的文件方便我们使用 babel 对其做按需加载;

1,根目录新建 components.json 文件:作为 打包的入口配置。

// 后续每次的新增都需要再次配置。
{
  "button": "./packages/button/index.js",
  "card": "./packages/card/index.js",
  "index": "./packages/index.js" // 主文件
}

2,删除之前 vue.config.js 中所有代码,新增以下代码;(不用删除也可,后续可以根据环境判断使用什么配置,我是图个方便。)

const path = require('path');
const components = require('./components.json');

// 获取每个组件的绝对路径
const getComponentEntries = () => {
  const entryKeys = Object.keys(components);

  entryKeys.forEach(key => {
    components[key] = path.join(__dirname, components[key]);
  });

  return components;
};

// { button: '/Users/xxx/packages/button/index.js' }
const componentEntries = getComponentEntries();

module.exports = {
  outputDir: path.join(__dirname, 'lib'),
  configureWebpack: {
    entry: componentEntries,
    output: {
      //  文件名称
      filename: '[name].js',
      //  构建依赖类型
      libraryTarget: 'umd',
      //  库中被导出的项
      libraryExport: 'default',
      //  引用时的依赖名
      library: 'testComp'
    }
  },
  css: {
    // 独立构建 css
    extract: {
      filename: 'theme/[name].css' //在lib文件夹中建立 theme 文件夹中,生成对应的css文件。
    }
  },
  /**
   * 删除splitChunks,因为每个组件是独立打包,不需要抽离每个组件的公共js出来。
   * 删除copy,不要复制public文件夹内容到lib文件夹中。
   * 删除html,只打包组件,不生成html页面。
   * 删除preload以及prefetch,因为不生成html页面,所以这两个也没用。
   * 删除hmr,删除热更新。
   * 删除自动加上的入口App。
   */
  chainWebpack: config => {
    config.optimization.delete('splitChunks');
    config.plugins.delete('copy');
    config.plugins.delete('html');
    config.plugins.delete('preload');
    config.plugins.delete('prefetch');
    config.plugins.delete('hmr');
    config.entryPoints.delete('app');
  }
};

3,然后直接执行我们原有的 npm run build。不出意外可能只有一个 index.css 文件。

build2.png

出现这个问题的原因很简单:之前我们引入了 index.scss 文件中;现在已经修改成多入口了,我们需要在每个文件中再做一次引入即可。

解决方案:

  • 每个组件在导入一次文件;
<!-- packages/button/src/button.vue -->
<style lang="scss" src="../../theme/button.scss"></style>
  • css 文件单独打包;参考 element-ui 使用 gulp 再分开打包

element-ui.png

4,如果你完成以上操作之后,再次进行打包出现类似于下面这种文件结构;恭喜你可以进入下一步 托管到 npm,随后我们可以在另一个项目研究按需加载了;

image.png

babel-plugin-component 按需引入

总体来说目前主流的组件库按需引入主要 根据 babel-plugin-import babel-plugin-component 去完成的,下面的示例将会根据目前社区比较火的 element-ui Vant ,前者只用的是 babel-plugin-component 后者则是 babel-plugin-import

由于咱们的组件库基本仿造 element-ui 打包后结构;所以咱们也会使用。babel-plugin-component

1,在新项目中安装插件,并且下载 babel-plugin-component

npm install test-comp-base
npm install babel-plugin-component

2,参考 element-ui 中的使用方式。注意: 我打包的样式目录是 theme 不是 theme-chalk 需要修改一下。libraryName 则是库名 例如:test-comp-base

babel.png

3,新项目的 src/main.js 中按需引入组件。

// ... ...
import { Button as ElButton } from 'test-comp-base';
import { Card as ElCard } from 'test-comp-base';

Vue.use(ElButton);
Vue.use(ElCard);

4,新项目的 src/App.vue 中写入测试代码

<el-card>
  <div slot="header" class="clearfix">
    <span>卡片名称</span>
    <el-button type="primary" size="mini">test-btn</el-button>
  </div>
  <div v-for="o in 4" :key="o">
    {{ '列表内容 ' + o }}
  </div>
</el-card>

5,重新启动项目查看结果(非 src 下的文件修改后尽量都重启一下服务)。启动服务后大家可能会遇到一个错误。

babel2.png

问题原因:它会默认去查找一个 base.css 的文件,为什么 element-ui 没问题。(因为人家有这个文件)。

解决方案:这个默认配置项是可以被更改的。我们修改一下配置文件即可。

plugins: [
  [
    'component',
    {
      libraryName: 'test-comp-base',
      // 不让他去查找 base
      styleLibrary: {
        name: 'theme',
        base: false
      }
    }
  ]
]

为什么不是 babel-plugin-import

如果你去他们的官网看下就明白大概了,不过我还是演示一下水一水经验吧。

首先我们把之前已经改好的 babel.config.js 中关于 babel-plugin-component 中的 styleLibrary 中的 name 选项还改成 theme-chalk 再次重新启动服务。

我们可以通过 在 node_modules 中查看这个包结构以及 console 中的错误信息结合着看着端倪。 仔细看图

babel3.png

其实当我们配置好 babel 之后,在运行的时候语法会经历以下的转换。

import { Button } from 'test-comp-base'

// babel-transform

var button = require('test-comp-base/lib/button')
// 当然这个 theme/ 是看你的 babel 如何配置。
require('test-comp-base/lib/theme/button.css')

问题结束,我们在新项目中安装一个 vant 来看下 babel-plugin-import 是如何查找文件的。

npm install vant
npm install babel-plugin-import -D

babel4.png

此时我们让他故意出点错误。比如把配置中的 libraryDirectory 更改为 xxx。再次运行项目。 仔细看图

babel5.png

可以看出本来是要在 es 里面查找对应的组件中的 style,当我们把 es 修改之后就找不到了。而且还有一个需要注意的点,他只是找到 style 这层目录,并不是某一个 .css 文件

这就比较重要了。众所周知如果只导入到目录结构,会默认导入该目录下的 index.js so 他的样式引用就放到了 style/index.js 中。

按照我们上面的推断,当你配置好 babel 时,在运行的时候语法会经历以下的转换。

import { Button } from 'vant'

// transform

// 可以看出省略了 index.js 
var _button = require('vant/lib/es/button');
require('vant/lib/es/button/style');

总结: 并不是你想用哪个就用哪个,提前设计好打包结构,根据需要合适才是最重要的。

托管到 npm

关于 npm 发布后续会考虑再开一篇说;这里就不细究了;请按照以下步骤操作:

1, package.json 内容补充,以下内容 在 Cli 创建的项目中会有部分缺失;自行补齐。

  • private: 选项改为 false;当 private 为 true 时,是不允许发布到 npm 的。

  • name: 选项就是你当前应用的名称;换句话说就是库名称。

  • version: 原则上每次发布版本,版本号都应该是递增的。

  • main:项目入口文件。例如:./lib/test-comp-base.umd.min.js

  • author: 作者名称。

  • license:使用哪种开源协议。

2, 新增 .npmignore 文件;过滤不需要上传的文件;文件内容规则参考 .gitignore

3, 注册 npm 账号 -> 登录 npm 账号 -> 发布 npm 包 参考

npm.png

4, 使用 cli 新建项目;下载并且下载咱们自己的组件库进行测试。

vue create comp-demo
npm install test-comp-base

源码地址

源码 欢迎 star ~