Dear,大家好,我是“前端小鑫同学”,长期从事前端开发,安卓开发,热衷技术,在编程路上越走越远~


写作背景:

     现在其实做得不错的开源 UI 库有很多,我还没有真正的实践过多造一个轮子也没太大必要,但是学习编写的思路和过程还是很有必要的,正好看到慕课的一个视频就顺便总结一下组件库开发的流程,顺便熟悉一个打包的配置和流程。

搭建基础结构:使用VueCli创建默认模板:
  1. 创建名为it200-ui的项目:vue create it200-ui;
  2. 使用默认Vue2模板即可,我们只考虑搭建UI库的思路不考虑版本的选择;
  3. 按提示命令进入项目cd it200-ui,并启动yarn serve;
调整目录使适合UI库开发:
  1. 调整src/components层级到根目录;
  2. 调整src为组件渲染示例examples;
  3. 通过在 vue.config.js 配置pages节点来更改入口;
创建第一个演示组件:

     目录结构如下,需按要求安装开发依赖sass-loader,为了避免与 node-sass 的版本冲突造成的更多问题,我们不再安装它而去添加一个名为sass包;

components ├─lib | ├─demo | | └index.vue ├─css | └demo.scss

在示例文件夹的 main.js 中导入并申明组件:

import "../components/css/demo.scss"; import Demo from "../components/lib/demo/index.vue"; Vue.component("name", Demo);

创建组件安装脚本:

     通常在使用开源 UI 库时并没有使用 component 不是导入组件,而是使用的 use 进行安装,所以我们在组件的同目录创建一个组件的安装脚本:

import Demo from "./index.vue"; Demo.install = function (Vue) { Vue.component(Demo.name, Demo); }; export default Demo;

使用组件安装脚本注册组件:

import Demo from "../components/lib/demo/index.js"; Vue.use(Demo);

开发一个组件的生命周期:设计组件:

     组件的设计一定是为了满足多处的复用而提出来的,站在各自的角度也可能都会有不一样的答案,所以我们这里找了 Elementcard 组件中的一块内容来充当我们今天待设计组件的需求:

组件设计稿:

卡片组件需要满足以下几点要求,其他的要求暂不考虑:

  1. 支持通过 body-style 属性来覆盖默认的 body 区域属性;
  2. 支持通过 shadow 属性来设置阴影出现的时机;

从头开始开发组件库(如何搭建组件库的最小原型)(1)

image.png

组件提供的属性:

参数 说明 类型 可选值 默认值 body-style 设置 body 的样式 object — { padding: '20px' } shadow 设置阴影显示时机 string always / hover / never always

编写组件模板:创建card 组件的结构:

components/ ├─lib | ├─card | | ├─index.js | | └index.vue ├─css | └card.scss

注册并在 App.vue 中使用组件:

import "../components/css/card.scss"; import Card from "../components/lib/card/index.js"; Vue.use(Card);

按设计要求为组件添加属性:

通过 props 提供组件的上述基础属性。

export default { name: "it-card", props: { bodyStyle: { type: Object, default: () => { return { padding: "20px" }; }, }, shadow: { type: String, default: "always", }, }, };

编写组件模板的框架:

     组件的大致结构如下,通过三层 div 来设置卡片组件容器、阴影、内容区的样式,并提供默认插槽来设置具体内容。

<template> <div class="it-card"> <div :class="`is-${shadow}-shadow`"></div> <div class="it-card__body" :style="bodyStyle"> <slot></slot> </div> </div> </template>

编写组件的样式:

.it-card { border-radius: 4px; border: 1px solid #ebeef5; background-color: #fff; overflow: hidden; color: #303133; transition: 0.3s; .it-card__body { padding: 20px; } .is-always-shadow { box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%); } .is-hover-shadow:hover { box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%); } .is-never-shadow { box-shadow: none; } }

在 App.vue 中完善卡片组件:

在 app.vue 中完善卡片组件,并对比组件设计稿。

<template> <div id="app"> <h3>Card组件</h3> <it-card style="width: 300px" :body-style="{ padding: '0px' }"> <img src="https://shadow.elemecdn.com/app/element/hamburger.9cf7b091-55e9-11e9-a976-7f4d0b07eef6.png" class="image" /> <div style="padding: 14px"> <span>好吃的汉堡</span> <div class="bottom"> <time class="time">"2022-05-03T16:21:26.010Z"</time> </div> </div> </it-card> </div> </template> <script> export default { name: "App", }; </script> <style> // 这里的样式使用 element card 使用的样式 </style>

构建 UMD 模块:

在前端模块化的进程中,经过了全局函数、命名空间,匿名函数自调,文件模块化方案,尤为常见的文件模块化方案就是 CommonJs,ADM,UMD 了,下面来介绍一下各自的特点;

CommonJs:
  1. 文件作用域:每个文件即为一个单独的模块,模块中的内容未主动暴露则对外不可见;
  2. 缓存:模块的加载只发生在第一次导入,在之后的导入会优先读取缓存;
  3. 同步加载:同步加载能保证在使用时必定存在该模块,但是并不适用于浏览器端,当同步加载慢的时候可能造成浏览器假死的状态发生。

结论:CommonJs 的模块更适用于服务端应用。

AMD:
  1. 文件作用域:同 CommonJs,也是模块化的主要产物;
  2. 异步加载:异步加载更好地适用于浏览器端,可以在异步加载后通过回调来执行后续的脚本。

结论:AMD 的模块更适用于浏览器端应用。

UMD通用模块:
  1. 同时满足适用于浏览器和服务端的模块化解决方案;
  2. 通过判断是否包含毒素 exports 来确认是否支持 Node.js 模块;
  3. 通过判断是否包含 define 来确认是否支持 AMD 模块;
  4. 上述两个特点均不存在则将模块挂载到全局(window 或 global)。
使用 Webpack 来打包组件逻辑代码:定义 webpack 打包配置文件webpack.components.js:
  1. 组件的打包我们使用多入口的方式分别处理,所以我们首先处理入口,通过遍历组件 lib 目录来得到一个以组件名和组件路径组成的键值对。

const glob = require("glob"); let entrys = {}; async function makrList(dirPath, list) { const files = glob.sync(`${dirPath}/**/index.js`); for (let file of files) { const component = file.split(/[/.]/)[2]; list[component] = `./${file}`; } } makrList("components/lib", entrys);

  1. 接下来我们处理出口的配置: 输出文件名称:使用入口的 key 来区分各个组件,并使用通过的 umd 作为组件输出产物的标识;输出目录:这里需要注意使用绝对路径来指定输出文件的位置;libraryTarget和library有相互依赖的关系,主要用来指定模块的暴露方式和模块的别名,这一块的描述我觉得 Rollup 中的描述将更清晰。

const path = require("path"); module.exports = { entry: entrys, output: { filename: "[name].umd.js", path: path.resolve(__dirname, "dist"), library: "it200", libraryTarget: "umd", }, };

  1. 最关键的是我们的 webpack 默认不认识.vue 的文件我们需要使用对应的loader来处理,Vue 文件对应的就是vue-loader,需要注意的是我们目前基于 Vue2 来构建的项目,所以最新的vue-loader并不是特别适合我们可以降级到 **15** 版本来让构建正常进行。

const { VueLoaderPlugin } = require("vue-loader"); module.exports = { plugins: [new VueLoaderPlugin()], module: { rules: [ { test: /\.vue$/, use: [ { loader: "vue-loader", }, ], }, ], }, };

  1. 为了方便调用我们还是配置一下打包命令:

"build:js": "webpack --config ./webpack.components.js"

为了满足全部导入的要求,我们还需要将组件整合:

在 lib 目录下新建一个index.js 文件将我们的组件统一导入后统一执行组件挂载。

import Demo from "./demo"; import Card from "./card"; const components = { Demo, Card, }; const install = function (Vue) { if (install.installed) return; Object.keys(components).forEach((key) => { Vue.component(components[key].name, components[key]); }); }; export default { install, };

使用Gulp 来打包组件的样式代码:

gulp 主要通过定义任务并使用流式的处理方式使用不同的管道依次进行,我们主要处理 scss 文件内容为 css 文件。

需要用到的模块如下:
  1. gulp-sass,因版本问题需要额外导入 sass 模块。
  2. gulp-minify-css:主要用来对 css 文件进行压缩。
完整的打包配置如下:
  1. 配置文件指明了操作的文件入口为css 目录下的 scss 结尾的文件;
  2. 文件输出到 dist/css 目录下;
  3. 方便执行我们配一下打包命令:"build:css": "npx gulp sass"。

const gulp = require("gulp"); const sass = require("gulp-sass")(require("sass")); const minifyCSS = require("gulp-minify-css"); gulp.task("sass", async function () { return gulp .src("components/css/**/*.scss") .pipe(sass()) .pipe(minifyCSS()) .pipe(gulp.dest("dist/css")); });

将模块化的 scss 文件整合到一起,方便全部加载:

在 css 目录新建 index.scss 文件,并将各个组件需要的 scss 文件导入到此文件。

@import "./card.scss"; @import "./demo.scss";

按需引入和全部引入:

import "../dist/css/index.css"; import IT200UI from "../dist/index.umd"; Vue.use(IT200UI);

import "../dist/css/card.css"; import Card from "../dist/card.umd"; Vue.use(Card); import "../dist/css/demo.css"; import Demo from "../dist/demo.umd"; Vue.use(Demo);

发布组件库到 NPM:
  1. 注册 npm 用户;
  2. 调整 package.json ;
调整 package:
  1. 移除私有配置:private;
  2. 添加组件库描述信息:description;
  3. 添加组件入口文件:main;
  4. 添加组件相关的关键词:keywords;
  5. 添加作者名字:author;
  6. 添加组件库发布的内容:files;

完成新增内容如下:

{ "description": "IT200 组件库,最小原型演示", "main": "dist/index.umd.js", "keywords": [ "it200", "ui", "组件库" ], "author": "fe-xiaoxin", "files": [ "dist", "components" ] }

调整组件库的说明文档:
  1. 包含组件库的安装方式;
  2. 包含组件库的引用方式;
快速开始如何安装

npm i it200-ui

如何引入

// 全部引入 import 'it200-ui/dist/css/index.css'; import IT200UI from 'it200-ui'; Vue.use(IT200UI); // 按需引入 import 'it200-ui/dist/css/cart.css'; import { Card } from 'it200-ui'; Vue.use(Card);

正式开始发布:
  1. 确认 NPM 源为修改成其他镜像地址,我这里使用 nrm 包进行源的管理,可以通过 nrm ls查询和 nrm use 进行切换;
  2. 执行 npm login 开始登陆,分别输入用户名、密码、邮箱,开通动态验证的话还需要输入动态验证码,开通的方式可以翻我以前的文章;
  3. 执行 npm publish 开始发布,开通动态验证码的话需要再次验证动态验证码;

从头开始开发组件库(如何搭建组件库的最小原型)(2)

image.png

写到最后:

     整个组件库的开发我们省略了最后一步,因为版本的问题导致 vuepress 没有成功的配置,在开发组件库的过程中使用到的技术栈可以是五花八门但是通过本次总结到的我们开发组件库的生命周期大致统一应该是搭建结构、设计组件、编写组件、验证组件、打包构建、发布为主线,构建组件库文档站点、编写使用手册、自动化构建发布为支线同步进行。

,