vue3+ts实现monorepo 组件库

vue-ts-ui

vue3+ts实现一个简单组件库

目的

主要是使用momorepo的方式来写一个vue3组件库,再把ts加进去,不考虑完善组件的功能

工具

  • vue3
  • webpack
  • lerna(实现monorepo)
  • ts
  • gulp (打包静态资源,处理css 和图片等)
  • rollup (生成iife,cjs,amd,esm,umd)多格式文件
  • yarn(包管理,产生软链接)

    1.monorepo项目初始化

    yarn global add lerna
    lerna init

    这个时候会生成
    lerna.json 和package.json

    {
    "packages": ["packages/*"],
    "npmClient": "yarn",
    "version": "0.0.0",
    "useWorkspaces": true // 使用workspace,需要配置package.json,实现多个组件库共用相同依赖
    }
    {
    "name": "root",
    "private": true,
    "workspaces": [
        "packages/*"
    ],
    "devDependencies": {
    "lerna": "^4.0.0"
    }
    }

    2.初始化组件

    lerna create @h-ui/button
    lerna create @h-ui/icon

    在填写组件名称时最好是 @组件库名称/组件名称 例如:@h-ui/button
    然后在生成的单个组件目录下新增src目录和index.ts入口
    就像:

    ├─button
    │  │  package.json
    │  │  README.md
    │  ├─src
    |  ├─  button.vue
    │  ├─index.ts # 组件入口
    │  └─__tests__ # 测试相关
    └─icon
    │  package.json
    │  README.md
    ├─src
    ├─  icon.vue
    ├─index.ts # 组件入口
    └─__tests__ 

    3.使用ts

    yarn add typescript 
    npx tsc --init

    然后更改tsconfig.json如下

    {
    "compilerOptions": {
    "target": "ESNext", // 打包的目标语法
    "module": "ESNext", // 模块转化后的格式
    "esModuleInterop": true, // 支持模块转化
    "skipLibCheck": true, // 跳过类库检测
    "forceConsistentCasingInFileNames": true, // 强制区分大小写
    "moduleResolution": "node", // 模块解析方式
    "jsx": "preserve", // 不转化jsx
    "declaration": true, // 生成声明文件
    "sourceMap": true // 生成映射文件
    }
    }

    二.组件初始化

    在button.vue编写源代码

    <template>
    <button :class="classs" @click="handleClick">
    <i :class="icon"></i>
    <span><slot></slot></span>
    </button>
    </template>
    <script lang="ts">
    import { computed, defineComponent, PropType } from "vue";
    type ButtonType =
    | "primary"
    | "wraning"
    | "danger"
    | "default"
    | "info"
    | "sucess";
    export default defineComponent({
    props: {
    type: {
      type: String as PropType<ButtonType>,
      default: "primary",
      vaildator: (val: string) => {
        return [
          "primary",
          "wraning",
          "danger",
          "default",
          "info",
          "sucess",
        ].includes(val);
      },
    },
    icon: {
      type: String,
      default: "",
    },
    disabled: Boolean,
    loading: Boolean,
    round: Boolean,
    },
    emits: ["click"],
    name: "HButton",
    setup(props, ctx) {
    const classs = computed(() => [
      "h-button",
      "h-button--" + props.type,
      {
        "is-disabled": props.disabled,
        "is-loading": props.loading,
        "is-round": props.round,
      },
    ]);
    console.log(classs);
    const handleClick = (e) => {
      ctx.emit("click", e);
    };
    return {
      classs,
      handleClick,
    };
    },
    });
    </script>

    组件入口index.ts声明install方法并导出组件

    import { App } from "@vue/runtime-core";
    import Button from "./src/button.vue";
    Button.install = (app: App): void => {
    app.component(Button.name, Button);
    };
    type WithInstall<T> = T & { install(app: App): void };
    const _Button: WithInstall<typeof Button> = Button;
    export default _Button;

    同时.vue文件后缀的文件默认无法解析,增加typings
    根目录下新建typings/vue-shim.d.ts

    declare module "*.vue" {
    import { defineComponent } from "@vue/runtime-core";
    const component: ReturnType<typeof defineComponent> & {
    install(app: APP): void;
    };
    export default component;
    }

    2.整合所有组件

    lerna create h-ui
    在h-ui/index.ts里

    import Button from "@h-ui/button";
    import Icon from "@h-ui/icon";
    import { App } from "@vue/runtime-core";
    import ButtonGroup from "../button-group";
    const components = [Button, Icon, ButtonGroup];
    const install = (app: App): void => {
    components.forEach((component) => {
    console.log(component.name);
    //注册组件
    app.component(component.name, component);
    });
    };
    export default {
    install,
    };

    三.搭建文档环境

    yarn add webpack webpack-cli webpack-dev-server vue-loader@next @vue/compiler-sfc -WD
    yarn add babel-loader @babel/core @babel/preset-env @babel/preset-typescript babel-plugin-module-resolver url-loader file-loader html-webpack-plugin css-loader sass-loader style-loader sass -WD

    并且根目录新建babel.config.js

    module.exports = {
    presets: ["@babel/preset-env", "@babel/preset-typescript"],
    overrides: [
    {
      test: /\.vue$/,
      plugins: ["@babel/transform-typescript"],
    },
    ],
    env: {
    utils: {
      plugins: [
        [
          "babel-plugin-modu-resolver",
          {
            root: "h-ui",
          },
        ],
      ],
    },
    },
    };

    根目录下新建website文件夹用于起个服务预览组件效果,在这个目录下新建
    App.vue main.ts template.html webpack.config.js
    webpack.config.js配置如下

    const HtmlWebpackPlugin = require("html-webpack-plugin");
    const path = require("path");
    const { VueLoaderPlugin } = require("vue-loader");
    module.exports = {
    mode: "development",
    devtool: "source-map",
    entry: path.resolve(__dirname, "main.ts"),
    output: {
    path: path.resolve(__dirname, "../website-dist"),
    filename: "bundles.js",
    },
    resolve: {
    //解析模块
    extensions: [".ts", ".tsx", ".js", ".vue"],
    },
    module: {
    rules: [
      {
        test: /\.(ts|js)x?$/,
        exclude: /node_modules/,
        loader: "babel-loader",
      },
      {
        test: /\.vue$/,
        loader: "vue-loader",
      },
      {
        // 识别图标...
        test: /\.(svg|otf|ttf|woff|eot|gif|png)$/,
        loader: "url-loader",
      },
      {
        // 识别样式
        test: /\.(scss|css)$/,
        use: ["style-loader", "css-loader", "sass-loader"],
      },
    ],
    },
    plugins: [
    new VueLoaderPlugin(),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "template.html"),
    }),
    ],
    };

    main.ts 如下

    import App from "./App.vue";
    import { createApp } from "@vue/runtime-dom";
    import HUI from "h-ui";
    // import HUI from "../lib/index.esm.js";
    // import Icon from "../lib/icon/index";
    import "theme-chalk/src/index.scss";
    //创建应用,并使用组件库
    createApp(App).use(HUI).mount("#app");

    app.vue 和tempate.html就和正常的项目写法一样,在app.vue 直接使用组件,预览效果
    根目录下package.json配置运行命令
    "scripts": { "website-dev": "webpack serve --config ./website/webpack.config.js" }

四.组件库打包

1.打包umd格式组件库(兼容 IIFE, AMD, CJS 三种模块规范)

根目录下新建builds/webpack.config.js,配置如下

const path = require("path");
const { VueLoaderPlugin } = require("vue-loader");
module.exports = {
  mode: "development",
  entry: path.resolve(__dirname, "../packages/h-ui/index.ts"),
  output: {
    path: path.resolve(__dirname, "../lib"),
    filename: "index.js",
    libraryTarget: "umd",
    library: "h-ui",
  },
  externals: {
    vue: {
      // 忽略组件引用的vue变量
      root: "Vue",
      commonjs: "vue",
      commonjs2: "vue",
    },
  },
  resolve: {
    //解析模块
    extensions: [".ts", ".tsx", ".js", ".vue"],
  },
  module: {
    rules: [
      {
        test: /\.(ts|js)x?$/,
        exclude: /node_modules/,
        loader: "babel-loader",
      },
      {
        test: /\.vue$/,
        loader: "vue-loader",
      },
    ],
  },
  plugins: [new VueLoaderPlugin()],
};

根目录下的package.json新增脚本运行

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

打包esModule格式组件库

使用rollup进行打包,安装所需依赖

yarn add rollup rollup-plugin-typescript2 @rollup/plugin-node-resolve rollup-plugin-vue -WD

打包分为两种,单个组件打包和全量打包

全量打包

新增builds/rollup.config.bundle.js
配置如下

import typescript from 'rollup-plugin-typescript2';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import path from 'path';
import vue from 'rollup-plugin-vue'

export default {
    input: path.resolve(__dirname, <code>../packages/h-ui/index.ts</code>),
    output: {
        format: 'es',
        file: <code>lib/index.esm.js</code>,
    },
    plugins: [
        nodeResolve(),
        vue({
            target: 'browser'
        }),
        typescript({ // 默认调用tsconfig.json  帮我们生成声明文件
            tsconfigOverride:{
                exclude: [
                    'node_modules',
                    'website'
                ]
            }
        })
    ],
    external(id) { // 排除vue本身
        return /^vue/.test(id)
    },
}

单个组件打包

新建builds/rollup.config.js,配置如下

import typescript from "rollup-plugin-typescript2";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import path from "path";
import { getPackagesSync } from "@lerna/project";
import vue from "rollup-plugin-vue";

// 获取package.json 找到名字 以@h-ui 开头的
const inputs = getPackagesSync()
  .map((pck) => pck.name)
  .filter((name) => name.includes("@h-ui"));
export default inputs.map((name) => {
  const pckName = name.split("@h-ui")[1]; // button icon
  return {
    input: path.resolve(__dirname, <code>../packages/${pckName}/index.ts</code>),
    output: {
      format: "es",
      file: <code>lib/${pckName}/index.js</code>,
    },
    plugins: [
      nodeResolve(),
      vue({
        target: "browser",
      }),
      typescript({
        tsconfigOverride: {
          compilerOptions: {
            // 打包单个组件的时候不生成ts声明文件
            declaration: false,
          },
          exclude: ["node_modules"],
        },
      }),
    ],
    external(id) {
      // 对vue本身 和 自己写的包 都排除掉不打包
      return /^vue/.test(id) || /^@h-ui/.test(id);
    },
  };
});

同时新增两个脚本运行命令

 "scripts": {
    "build": "webpack --config builds/webpack.config.js",
    "build:esm-bundle": "rollup -c ./builds/rollup.config.bundle.js",
    "build:esm": "rollup -c ./builds/rollup.config.js"
  }

组件样式打包

使用gulp打包样式
安装gulp
yarn add gulp gulp-autoprefixer gulp-cssmin gulp-dart-sass gulp-rename -D

样式命名规范采用bem规范 https://baike.baidu.com/item/BEM/23772830

scss的复杂使用比如方法参考 https://www.sass.hk/docs/

lerna create theme-chalk 之后内容如下

src--│ button.scss
     │ icon.scss
     ├─common
     │    var.scss # 提供scss变量
     ├─fonts # 字体
     └─mixins
          config.scss # 提供名字
          mixins.scss # 提供mixin方法
       index.scss  # 整合所有scss
gulpfile.js

gulpfile.js配置如下

const { series, src, dest } = require('gulp')
const sass = require('gulp-dart-sass')
const autoprefixer = require('gulp-autoprefixer')
const cssmin = require('gulp-cssmin')

function compile() { // 处理scss文件
    return src('./src/*.scss')
    .pipe(sass.sync())
    .pipe(autoprefixer({}))
    .pipe(cssmin())
    .pipe(dest('./lib'))
}
function copyfont(){ // 拷贝字体样式
    return src('./src/fonts/**').pipe(cssmin()).pipe(dest('./lib/fonts'))
}

exports.build = series(compile,copyfont)

完整代码:https://github.com/dz333333/vue-ts-ui

发表评论

您的电子邮箱地址不会被公开。