Vue3+Typescript 搭建PC端组件库

前言


本项目技术栈为vue3+typescript,PC端组件库。 项目gitee地址

本文将从环境搭建开始,一步步完成组件库的打包工作。组件库内组件不定期更新。

1. 代码管理方式

组件库采用monorepo的代码管理方式。

1)概念

monorepo 是管理项目代码的一种方式,指在一个项目仓库 (repo) 中管理多个模块/包 (package),不同于常见的每个模块建一个repo。

2)优劣势

monorepo最主要的优势是统一的工作流Code Sharing。一套代码管理多个package包的构建和发布。劣势是如果每个package都有自己的独立的依赖,频繁的install包,node_modules会变得很大。

2. monorepo的解决方案

本项目使用lernayarnworkspaces特性,来管理monorepo

项目中lerna.json声明packages后,lerna为项目提供统一的依赖安装 (lerna bootstrap)统一的执行package scripts(lerna run)统一的npm发版 (lerna publish)等特性。

使用yarn作为包管理器,在package.json中用workspaces字段声明packagesyarn就会以monorepo的方式管理packages

相比lernayarn突出的是对依赖的管理,包括packages的相互依赖、packages对第三方的依赖,yarn会以semver约定来分析dependencies的版本,安装依赖时更快、占用体积更小;但欠缺了统一工作流方面的实现。

大多monorepo即会使用lerna又会在package.json声明workspaces。这样无论你的包管理器是npm还是yarn,都能发挥monorepo的优势;要是包管理是yarnlerna就会把依赖安装交给yarn处理。怎么交给yarn处理呢?配置lerna.jsonnpmClient字段为yarn

关于monorepo更多的了解,请看好文Monorepo-大型项目代码管理方式。本文关于monorepo的介绍都取自该文。

一、环境搭建


1. lerna安装

全局安装lerna,注意node版本要在v10.4以上。

npm install lerna -g
 

建议win10用系统的cmd终端来安装,尽量不要用vscode提供的终端,vscode的终端中安装会莫名其妙报需要配置环境变量。
如果安装过程中出错,加--force参数强制安装。

2. 生成lerna.json和package.json文件

lerna init
 
// lerna.json
{
  "packages": [
    "packages/*"  // 表示管理packages下的所有模块
  ],
  "npmClient": "yarn", // 依赖包的安装交给yarn
  "useWorkspaces": true, // 是否使用空间,lerna本身不使用workspace,yarn要使用。
  "version": "0.0.0"
}
 
// package.json
{
  "name": "root",
  "private": true,
  "workspaces": [ // yarn管理packages下所有模块
    "packages/*"
  ],
  "devDependencies": {
    "lerna": "^4.0.0"
  }
}
 
// 安装本地依赖 lerna
yarn install
 

3. vue@next安装

yarn add vue@next -W  # -W忽略提示,默认安装到根目录全局使用
 

编写vue声明文件

// typings/vue-shim.d.ts

declare module '*.vue' {
    // 取defineComponent的返回值,标识组件类型
    import { defineComponent, App } from 'vue';
    const component: ReturnType<typeof defineComponent> & { install(app: App): void }
    // 导出组件类型
    export default component;
}
 

组件类型,要保证组件有install方法。

4. typescript安装,tsconfig.json生成

yarn add typescript -W
npx tsc --init             # 生成tsconfg.json配置文件
 
// tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",                         # 打包的目标语法
    "module": "ESNext",                         # 模块转化后的格式
    "esModuleInterop": true, # 支持模块转化 import fs from 'fs'; 编译前 let fs = require('fs'); fs.default编译后,fs无default属性,所引引用时会出问题
    "skipLibCheck": true,                       # 跳过类库检测
    "forceConsistentCasingInFileNames": true,   # 强制区分大小写
    "moduleResolution": "node",                 # 模块解析方式
    "jsx": "preserve",                          # 不转化jsx
    "declaration": true,                        # 生成声明文件
    "sourceMap": true                           # 生成映射文件
  }
}
 

5. 初始化组件

lerna create button
 

name 要按照模块名称来命名 @Will-ui/button
生成的button文件夹下有lib文件夹,项目默认会读这个文件夹,如果你的组件没有生效,记得回来看看是不是这儿搞得鬼。lib用不到的话,直接删掉。

├─button
│  │  package.json
│  │  README.md
│  ├─src
|  ├─button.vue # 组件实现
│  ├─index.ts   # 组件入口
│  └─__tests__  # 测试相关
 
<template>
  <button>按钮</button>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
  name: "WButton"
});
</script>
 
// 入口 index.ts 对应的install方法

import { App, defineComponent, createApp } from 'vue';
import Button from './src/button.vue'; // .vue后缀的类型声明在 vue-shim.d.ts中。定义了.vue后缀的组件的类型

Button.install = (app: App): void => {
    // 全局注册组件
    console.log('button组件注册全局');
    app.component(Button.name, Button);
}

type IWithInstall = ReturnType<typeof defineComponent> & { install(app: App): void };

let _Button: IWithInstall = Button;
export default _Button;

// createApp({}).use(_Button); // use调用,需要组件提供install方法
 

6. 整合所有组件

lerna create will-ui
 

name 为默认的 will-ui

// will-ui/index.ts

import { App } from 'vue';
import Button from '@will-ui/button';
const components = [
    Button
];

const install = (app: App): void => {
    components.forEach(component => {
        // 遍历组件,挂载到全局
        app.component(component.name, component);
    })
}

export default {
    install // 导出install方法。createApp({}).use() 需要install方法
}
 

7. theme-chalk样式管理

样式遵循BEM样式规范。

lerna create theme-chalk
 
// BEM规范
$namespace: 'w'; // 命名空间
$state-prefix: 'is-'; // 状态
$modifier-separator: '--'; // 元素修饰作用 
$element-separator: '__'; // 元素直接的分割

// 示例:
<div class="w-xxx">
        <button class="w-button--primary"></button>
        <button class="is-readonly is-disabled"></button>
    <div class="w-xxx__header"></div>
    <div class="w-xxx__body"></div>
    <div class="w-xxx__footer"></div>
</div>
 

yarn installnode_modules下生成包的软链。

二、搭建文档


1. 安装webpack及编译打包依赖

yarn add webpack webpack-cli webpack-dev-server vue-loader@next @vue/compiler-sfc -D -W

webpack-cli        # webpack命令行解析工具
webpack-dev-server # 启动静态服务
vue-loader         # 处理.vue文件,解析vue模板
@vue/compiler-sfc  # 解析vue模板
 
yarn add babel-loader @babel/core @babel/preset-env @babel/preset-typescript @babel/plugin-transform-typescript babel-plugin-module-resolver url-loader file-loader html-webpack-plugin css-loader sass-loader style-loader sass -D -W

babel-loader                              # babel解析js语法
@babel/core                               # babel-loader默认会调babel-core(babel核心包)
@babel/preset-env                         # 将高级语法转成低级预发
@babel/preset-typescript                  # babel转化解析ts
@babel/plugin-transform-typescript        # .vue文件中使用ts,对ts代码进行转化
babel-plugin-module-resolver              # babel模块解析插件
url-loader                                # url-loader解析文件资源(编译成base64)
file-loader                               # file-loader解析文件资源(生成真实文件)
html-webpack-plugin                       # 调用html的插件
css-loader sass-loader style-loader sass  # 处理css样式
 
// babel.config.js

module.exports = {
    presets: [ // babel解析的预设,是反着执行的
        '@babel/preset-env',
        '@babel/preset-typescript'
    ],
    overrides: [{ // .vue文件中使用了ts,对ts代码进行转化
        test: /\.vue$/,
        plugins: [
            '@babel/transform-typescript',
        ]
    }]
}
 
// 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: 'bundle.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|woff2|eot|gif|png)$/, loader: 'url-loader' },
            {
                test: /\.(scss|css)$/,
                use: [
                    'style-loader',
                    'css-loader',
                    'sass-loader'
                ]
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin(), // vue-loader 解析.vue文件
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, 'template.html')
        })
    ]
}
 
// package.json
"scripts": {
    "website-dev": "webpack serve --config ./website/webpack.config.js"
}
 
// main.ts
mport { createApp } from 'vue';
import App from './App.vue';

// 引入组件库
import WillUI from 'will-ui'; // 开发阶段
import 'theme-chalk/src/index.scss'; // 开发阶段引入样式
// 创建应用并使用组件库
createApp(App).use(WillUI).mount('#app');
 

配置好website后,执行npm run website-dev,启动本地服务,之后看效果就在这儿了。

三、组件库打包


1. 打包umd格式组件库

使用webpack打包成umd格式。

// builds/webpack.config.js
const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');

module.exports = {
    mode: 'production',
    entry: path.resolve(__dirname, '../packages/will-ui/index.ts'),
    output: {
        path: path.resolve(__dirname, '../lib'),
        filename: 'index.js',
        libraryTarget: 'umd', // umd打包格式,支持commonjs和amd,不支持es6,可以在浏览器直接使用。
        library: 'will-ui' // 全局名称
    },
    externals: { // 打包忽略vue,防止把vue源码打进去
        vue: {
            root: 'Vue', // 根下的Vue
            commonjs: 'vue', // commonjs规范的(import {xxx} from '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() // vue-loader 解析.vue文件
    ]
}
 
// package.json

"scripts": {
    "website-dev": "webpack serve --config ./website/webpack.config.js",
    "build": "webpack --config builds/webpack.config.js",
  },
 

npm run build 打包umd格式
umd为了直接能使用,但是只支持commonjsamd规范。

2. 全量打包esModel格式组件库

使用rollup打包esmodel格式。

yarn add rollup rollup-plugin-typescript2 @rollup/plugin-node-resolve rollup-plugin-vue -D -W

@rollup/plugin-node-resolve # 解析第三方模块
rollup-plugin-vue           # 支持vue
 
// 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, `../packages/will-ui/index.ts`),
    output: {
        format: 'es',
        file: `lib/index.esm.js`,
    },
    plugins: [
        nodeResolve(), // 支持第三方模块,vue,typescript
        vue({
            target: 'browser'
        }),
        typescript({ // 默认调用tsconfig.json  帮我们生成声明文件
            tsconfigOverride: {
                exclude: [
                    'node_modules',
                    'website'
                ]
            }
        })
    ],
    external(id) { // 排除vue本身
        return /^vue/.test(id)
    }
}
 
// package.json

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

npm run build:esm-bundle 全量打包esModel格式

3. 组件单独打包esModel格式组件库(按需加载)

// 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,找到以 @will-ui 开头的
const inputs = getPackagesSync().map(pck => pck.name).filter(name => name.includes('@will-ui'));

export default inputs.map(name => {
    const pckName = name.split('@will-ui')[1] // button icon
    return {
        input: path.resolve(__dirname, `../packages/${pckName}/index.ts`),
        output: {
            format: 'es',
            file: `lib/${pckName}/index.js`,
        },
        plugins: [
            nodeResolve(),
            vue({
                target: 'browser'
            }),
            typescript({
                tsconfigOverride: {
                    compilerOptions: { // 打包单个组件的时候不生成ts声明文件
                        declaration: false,
                    },
                    exclude: [
                        'node_modules'
                    ]
                }
            })
        ],
        external(id) { // 对vue本身 和 自己写的包 都排除掉不打包
            return /^vue/.test(id) || /^@will-ui/.test(id)
        },
    }
})
 
"scripts": {
    "website-dev": "webpack serve --config ./website/webpack.config.js",
    "build:esm": "rollup -c ./builds/rollup.config.js"
  },
 

npm run build:esm 组件单个打包esModel格式,满足按需引入。
你可能好奇,配了wepack的依赖,又要用rollup,为什么不用一个呢?
因为打包esm格式,rollup优势很明显,首先他天生对ES6支持的很好,支持tree-shaking。所以使用rollup来打包esm格式。rollup既支持多个文件单独打包,又支持整体打包,所以全量打包和按需加载就靠它。

4. 组件样式单独打包

yarn add gulp gulp-autoprefixer gulp-cssmin gulp-dart-sass gulp-rename -D -W

gulp-autoprefixer   # 加前缀
gulp-cssmin         # 压缩css
gulp-dart-sass      # 处理sass
gulp-rename         # 重命名
 

gulp虽然项目里使用的不多了,但有时候用它来打包css,打包js,配置简单,也很好用。

// gulp.config.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);
 

四、组件编写

稍微复杂一点的组件,会有流程图,帮助理解,简单组件大家看代码就懂了。

1. checkbox、checkbox-group

checkbox:
image.png

checkbox-group:
image.png

持续更新中。。。

参考资料


  • Element-plus
  • Monorepo-大型项目代码管理方式
  • lerna官网
  • Workspaces | Yarn
(0)
上一篇 2021年3月27日 下午12:00
下一篇 2021年3月27日 下午12:15

相关推荐

发表回复

登录后才能评论