vue3+rollup从0创建简版组建库

57 阅读4分钟

组件库创建是一个复杂繁琐有很多规范化的事情,这样在学习组件库时会增加很多额外负担。本文采用最简单最基础的创建方式,演示出一个简单版本的UI库,能够让你在一小时内了解到UI组件库的运行原理和创建流程。理解了这些在带着问题查看有名的组件库源码,会让你事半功倍。

创建目录结构

# 创建 coolui目录
$ mkdir coolui
# 初始化npm环境,创建出package.json文件
$ npm init -y

设置package.json

{
  "name": "coolui",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

创建出来的package.json文件,并没有显示type,但是默认为commonjs模式,项目中我们使用es开发,所以需要修改添加上"type": "module",

设置rollup配置

创建 rollup.config.js ,配置rollup打包所需要的参数。

import resolve from "rollup-plugin-node-resolve";
import vue from "rollup-plugin-vue";
import babel from "@rollup/plugin-babel";
import commonjs from "@rollup/plugin-commonjs";
import scss from "rollup-plugin-scss";
import json from "@rollup/plugin-json";

const formatName = "CoolUI";
const config = {
  input: "./main.js",
  output: [
    {
      file: "./lib/bundle.cjs.js",
      format: "cjs",
      name: formatName,
      exports: "named",
    },
    {
      file: "./lib/bundle.umd.js",
      format: "umd",
      name: formatName,
      globals: {
        vue: "Vue",
      },
      exports: "named",
    },
    {
      file: "./lib/bundle.es.js",
      format: "es",
      name: formatName,
      exports: "named",
    },
    {
      file: "./lib/bundle.js",
      format: "iife",
      name: formatName,
      globals: {
        vue: "Vue",
      },
      exports: "named",
    },
  ],
  external: ["vue"],
  plugins: [
    json(),
    resolve(),
    vue({
      css: true,
      compileTemplate: true,
    }),
    babel({
      exclude: "**/node_modules/**",
    }),
    commonjs(),
    scss(),
  ],
};
export default config;

rollup打包出来的4种格式:

  • cjs:commonjs模式,node后端使用
  • es:esModel规范,包含新规范的es6,import和export
  • umd:通用的模式,兼容了amd和commonjs
  • iife:立即执行函数,可以把导出的对象挂到window全局对象上。

创建入口文件main.js

main.js文件用来注册所有组件,并导出所有组件。

package.json完整内容

{
  "name": "coolui",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "build": "rollup -c ./rollup.config.js"
  },
  "keywords": ["rollup", "vue3", "UI"],
  "author": "shenshuai89@qq.com",
  "license": "ISC",
  "dependencies": {
    "@rollup/plugin-babel": "^6.0.3",
    "@rollup/plugin-commonjs": "^24.0.1",
    "@rollup/plugin-json": "^6.0.0",
    "rollup-plugin-node-resolve": "^5.2.0",
    "rollup-plugin-scss": "^4.0.0",
    "rollup-plugin-vue": "^6.0.0"
  }
}

创建2个基础组件

在src下创建components目录

src
└── components
    ├── CoolButton
    │   ├── index.js
    │   ├── index.scss
    │   └── index.vue
    └── CoolInput
        ├── index.js
        ├── index.scss
        └── index.vue

CoolButton组件

vue内容

<template>
  <button class="btn">CoolButton</button>
</template>

<script>
import { defineComponent } from 'vue'
export default defineComponent({
  name: "CoolButton",
});
</script>
<style lang="scss" scoped></style>

js内容

import CoolButton from "./index.vue";
// 给组件设置install方法,这样在main中才可以使用use,或者在引入组件时,可以使用use调用组件
CoolButton.install = function (app) {
  app.component(CoolButton.name, CoolButton);
};
export { CoolButton };

scss内容

.btn{
  color: red;
}

CoolInput组件

vue内容

<template>
  <div class="ipt">CoolInput</div>
</template>

<script>
import { defineComponent } from 'vue'
export default defineComponent({
  name: "CoolInput",
});
</script>
<style lang="scss" scoped></style>

js内容

import CoolInput from "./index.vue";

CoolInput.install = function (app) {
  app.component(CoolInput.name, CoolInput);
};
export { CoolInput };

scss内容

.ipt{
    font-size: 24px;
}

在main.js中注册组件

import { CoolButton } from "./src/components/CoolButton/index.js";
import { CoolInput } from "./src/components/CoolInput/index.js";
import { version } from "./package.json";

import "./src/components/CoolButton/index.scss";
import "./src/components/CoolInput/index.scss";

const components = [CoolButton, CoolInput];

// vue3中通过use注册组件
const install = function (app) {
  components.forEach((component) => {
    //这里可以使用use,也可以使用component。
    // use内部调用了install方法,定义的所有component组件都要实现下这个install方法
    app.use(component);
    // app.component是通过name和组件直接定义的全局组件
    // app.component(component.name, component);
  });
};

export { CoolButton, CoolInput, install };
export default { version, install };

安装需要的依赖:

yarn add @babel/core @rollup/plugin-babel @rollup/plugin-commonjs@21.0.0 @rollup/plugin-json 
yarn add vue node-sass rollup-plugin-node-resolve rollup-plugin-scss rollup-plugin-vue

打包编译出库js文件

执行打包命令 yarn run build,可以看到输出的目录lib生成了4个打包出来的文件。

├── assets
│   └── output-96704ed1.css
├── bundle.cjs.js
├── bundle.es.js
├── bundle.js
└── bundle.umd.js

在脚手架项目中安装使用

使用vite创建出vue项目,在mian中引入CoolUI。

import { createApp } from 'vue'
import App from './App.vue'
import CoolUI from "./lib/bundle.es.js";

createApp(App).use(CoolUI).mount('#app')

接下来就可以在全局中使用CoolUI中的组件。

<template>
  <div>
    <Cool-Button></Cool-Button>
    <Cool-Input></Cool-Input>
  </div>
</template>

通过src路径引入使用

在html通过srcipt标签的src链接引入。引入的格式应为umd或iife。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>test ui</title>
    <script src="https://unpkg.com/vue@3.2.47/dist/vue.global.js"></script>
    <script src="./lib/bundle.js"></script>
  </head>
  <body>
    <div id="app">
    	<Cool-Button></Cool-Button>
    	<Cool-Input></Cool-Input>
    </div>

    <script>
      const { createApp } = Vue;
      const app = createApp().use(CoolUI).mount("#app");
    </script>
  </body>
</html>

打包编译样式文件

到目前为止,打包出来的组件并没有看到样式生效,在上面示例中并没有引入css,也没有单独处理css。接下来就出来样式文件。样式文件可以单独打包出来一个库,给全局组件使用,也可以每个组件打包出来一个样式文件,给单独使用组件时添加样式。

在src下创建theme目录

初始化环境 npm init -y,新增出package.json 创建gulpfile.js文件,执行打包

{
  "name": "theme",
  "version": "1.0.0",
  "description": "",
  "main": "gulpfile.js",
  "scripts": {
    "build": "gulp build"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "gulp": "^4.0.2",
    "gulp-autoprefixer": "^8.0.0",
    "gulp-cssmin": "^0.2.0",
    "gulp-sass": "^5.1.0",
    "sass": "^1.58.0"
  }
}
const { series, src, dest } = require("gulp");
const sass = require("gulp-sass")(require('sass'));
const autoprefixer = require("gulp-autoprefixer");
const cssmin = require("gulp-cssmin");

//编译scss文件,并拷贝到指定目录下
function compile() {
  return src("./src/components/*.scss")
    .pipe(sass.sync())
    .pipe(
      autoprefixer({
        overrideBrowserslist: ["ie > 9", "last 2 versions"],
        cascade: false,
      })
    )
    .pipe(cssmin())
    .pipe(dest("./lib/style"));
}
// 拷贝字体文件
function copyfont() {
  return src("./src/fonts/**").pipe(cssmin()).pipe(dest("./lib/fonts"));
}

exports.build = series(compile, copyfont);

安装依赖:yarn add gulp gulp-autoprefixer gulp-cssmin gulp-sass sass 然后执行打包命令 yarn run build,实际上执行的是gulp build

打包结果lib目录

└── style
    ├── button.css
    ├── index.css
    └── input.css

可以将index.css引入到项目中

在vite项目中的main.js

// 引入样式
import "./lib/style/index.css";

在html中通过link引入

<link rel="stylesheet" href="./src/lib/style/index.css">

创建函数调用全局组件 CoolMsg

在创建组件库时,有一类组件是通过js全局引入的,比如$message,并将组件挂载到body上。该类组件主要是通过 createVNode 和 render 方法创建,然后添加并渲染到body

主要是要 createVNode 和 render 方法

新建CoolMsg目录,创建index.js和index.vue

<template>
  <div class="msg" v-if="visible">
    message
    <button @click="close">close</button>
  </div>
</template>
<script>
export default {
  name: "CoolMsg",
};
</script>
<script setup>
import { onMounted, ref } from "vue";
const visible = ref(false);
onMounted(() => {
  visible.value = true;
});
function close() {
  visible.value = false;
}
defineExpose({
  visible,
  close,
});
</script>
<style lang="scss" scoped></style>
import { render, createVNode } from "vue";
import CoolMsgCom from "./index.vue";
// 消息实例数组
const instances = [];
const removeContainer = (ele) => {
  const index = instances.findIndex((item) => item === ele);
  if (~index) {
    instances.splice(index, 1);
  }
};
const addContainer = (ele) => {
  instances.push(ele);
};

const createMount = (opts) => {
  const { duration } = opts;
  // 创建一个 div 容器
  const container = document.createElement("div");
  // 创建 CoolMsgCom 实例,createVNode 的性能比 h 更好
  const vm = createVNode(CoolMsgCom, opts);
  // 把实例 render 到容器上
  render(vm, container);
  addContainer(container);
  // 把容器渲染到 body 上
  document.body.appendChild(container);
  const destory = () => {
    const timer = setTimeout(() => {
      render(null, container);
      removeContainer(container);
      document.body.removeChild(container);
      clearTimeout(timer);
    }, 500); // 500 为动画结束时间,根据情况修改
  };

  const timer = setTimeout(() => {
    destory();
    clearTimeout(timer);
  }, 2000);

  return { destory };
};
function CoolMsg(options) {
  if (typeof options === "string") {
    // eslint-disable-next-line
    options = {
      message: options || "",
    };
  } else {
    // eslint-disable-next-line
    options = {
      ...options,
    };
  }
  return createMount(options);
}

CoolMsg.install = function (app) {
  app.component("CoolMsg", CoolMsgCom);
};

export { CoolMsg };

在main.js中引入CoolMsg

import { CoolButton } from "./src/components/CoolButton/index.js";
import { CoolInput } from "./src/components/CoolInput/index.js";
import { CoolMsg } from "./src/components/CoolMsg/index.js";
import { version } from "./package.json";

const components = [CoolButton, CoolInput, CoolMsg];

// vue3中通过use注册组件
const install = function (app) {
  components.forEach((component) => {
    //这里可以使用use,也可以使用component。
    // use内部调用了install方法,定义的所有component组件都要实现下这个install方法
    app.use(component);
    // app.component是通过name和组件直接定义的全局组件
    // app.component(component.name, component);

    applyOptions(app);
  });
};
// 用来挂载 CoolMsg 这样用 js 来调用的组件, this.$CoolMsg()
const applyOptions = (app) => {
  app.config.globalProperties.$CoolMsg = CoolMsg;
};

export { CoolButton, CoolInput, CoolMsg, install };
export default { version, install };

创建函数组件,vue2和vue3区别

  • vue2中通过vue.extend进行创建
  • vue3中通过createVNode,然后render到container挂载点上。

项目代码:gitee.com/shenshuai89…