大家好,我是小鑫同学。一位从事过 Android 开发、混合开发,现在长期从事前端开发的编程爱好者,我觉得在编程之路上最重要的是知识的分享,所谓三人行必有我师。所以我开始在社区持续输出我所了解到、学习到、工作中遇到的各种编程知识,欢迎有想法、有同感的伙伴加我fe-xiaoxin微信交流~
写作背景:
现在其实做的不错的开源 UI 库有很多,我还没有真正的实践过多造一个轮子也没太大必要,但是学习编写的思路和过程还是很有必要的,正好看到慕课的一个视频就顺便总结一下组件库开发的流程,顺便熟悉一个打包的配置和流程。
搭建基础结构:
使用 VueCli 创建默认模板:
创建名为 it200-ui 的项目:vue create it200-ui
;
使用默认 Vue2 模板即可,我们只考虑搭建 UI 库的思路不考虑版本的选择;
按提示命令进入项目cd it200-ui
,并启动yarn serve
;
调整目录使适合 UI 库开发:
调整src/components
层级到根目录;
调整src
为组件渲染示例examples
;
通过在 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);
复制代码
开发一个组件的生命周期:
设计组件:
组件的设计一定是为了满足多处的复用而提出来的,站在各自的角度也可能都会有不一样的答案,所以我们这里找了 Element 的 card 组件中的一块内容来充当我们今天待设计组件的需求:
组件设计稿:
卡片组件需要满足以下几点要求,其他的要求暂不考虑:
支持通过 body-style 属性来覆盖默认的 body 区域属性;
支持通过 shadow 属性来设置阴影出现的时机;
组件提供的属性:
编写组件模板:
创建 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:
文件作用域:每个文件即为一个单独的模块,模块中的内容未主动暴露则对外不可见;
缓存:模块的加载只发生在第一次导入,在之后的导入会优先读取缓存;
同步加载:同步加载能保证在使用是必定存在该模块,但是并不适用于浏览器端,当同步加载慢的时候可能造成浏览器假死的状态发生。
结论:CommonJs 的模块更适用于服务端应用。
AMD:
文件作用域:同 CommonJs,也是模块化的主要产物;
异步加载:异步加载更好的适用于浏览器端,可以在异步加载后通过回调来执行后续的脚本。
结论:AMD 的模块更适用于浏览器端应用。
UMD 通用模块:
同时满足适用于浏览器和服务端的模块化解决方案;
通过判断是否包含 exports
来确认是否支持 Node.js 模块;
通过判断是否包含 define
来确认是否支持 AMD 模块;
上述两个特点均不存在则将模块挂在到全局(window
或 global
)。
使用 Webpack 来打包组件逻辑代码:
定义 webpack 打包配置文件webpack.components.js
:
组件的打包我们使用多入口的方式分别处理,所以我们首先处理入口,通过遍历组件 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);
复制代码
接下来我们处理出口的配置:
输出文件名称:使用入口的 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",
},
};
复制代码
最关键的是我们的 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",
},
],
},
],
},
};
复制代码
为了方便调用我们还是配置一下打包命令:
"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 文件。
需要用到的模块如下:
gulp-sass,因版本问题需要额外导入 sass 模块。
gulp-minify-css:主要用来对 css 文件进行压缩。
完整的打包配置如下:
配置文件指明了操作的文件入口为 css 目录下的 scss 结尾的文件;
文件输出到 dist/css
目录下;
方便执行我们配一下打包命令:"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:
注册 npm 用户;
调整 package.json ;
调整 package:
移除私有配置:private;
添加组件库描述信息:description;
添加组件入口文件:main;
添加组件相关的关键词:keywords;
添加作者名字:author;
添加组件库发布的内容:files;
完成新增内容如下:
{
"description": "IT200 组件库,最小原型演示",
"main": "dist/index.umd.js",
"keywords": [
"it200",
"ui",
"组件库"
],
"author": "fe-xiaoxin",
"files": [
"dist",
"components"
]
}
复制代码
调整组件库的说明文档:
包含组件库的安装方式;
包含组件库的引用方式;
快速开始
如何安装
如何引入
// 全部引入
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);
复制代码
正式开始发布:
确认 NPM 源为修改成其他镜像地址,我这里使用 nrm
包进行源的管理,可以通过 nrm ls
查询和 nrm use
进行切换;
执行 npm login
开始登陆,分别输入用户名、密码、邮箱,开通动态验证的话还需要输入动态验证码,开通的方式可以翻我以前的文章;
执行 npm publish
开始发布,开通动态验证码的话需要再次验证动态验证码;
写到最后:
整个组件库的开发我们省略了最后一步,因为版本的问题导致 vuepress 没有成功的配置,在开发组件库的过程中使用到的技术栈可以是五花八门但是通过本次总结到的我们开发组件库的生命周期大致统一应该是搭建结构、设计组件、编写组件、验证组件、打包构建、发布为主线,构建组件库文档站点、编写使用手册、自动化构建发布为支线同步进行。
评论