theme: condensed-night-purple
本文正在参加「金石计划 . 瓜分6万现金大奖
一直觉得自己在配置搭建方面很薄弱,于是下决心系统化学习一遍。但是理想和现实还是有很大的差距,从学习到写这篇文章断断续续花了一个多月的时间。中间还考虑过放弃,但是再阅读一遍的时候发现还是有点东西,于是还是决定写完它。由于自己能力有限,中间可能有细节忽略了,希望读者可以指出,不胜感激。
实现最简组件库
第一部分的源代码已经上传到 GitHub
使用 Monorepo 方式管理组件库生态
前置知识
为什么使用 pnpm 进行依赖包管理?当我们创建100个项目的时候,这些项目都会使用同一个依赖包,npm 或者 yarn 会将该依赖包的100份存放到磁盘中,而 pnpm 会将所有相同的依赖包存放到同一个位置。当然,如果该依赖包有多种版本,pnpm 会保存该依赖包的所有版本。在 pnpm 管理模式下,每个项目通过硬连接找到对应的依赖包。显然,pnpm 依赖包管理可以减少磁盘空间,提高依赖包的安装速度。
pnpm 内置支持 Monorepo ,可以创建一个 workspace 来联合所有项目到一个仓库,该 workspace 的根目录必须包含一个 pnpm-workspace.yaml 文件。
操作步骤
// a. 创建根目录 smart-admin
// b. 在根目录下初始化项目
pnpm init
// c. 修改 package.json,禁用 npm 和 yarn
"scripts": {
"preinstall": "npx only-allow pnpm"
}
// d. 创建 pnpm-workspace.yaml,声明所有软件包存放的目录
packages:
- "packages/**"
搭建 vite 开发环境
前置知识
vite 是一个开发服务器,而 index.html 是入口文件。vite 默认支持 .ts 文件,但 vite 仅支持转译工作,不进行类型检查
操作步骤
// a. 初始化环境,安装 vite 开发依赖包
// 目录: packages/smart-ui-vite
pnpm init -y
pnpm i -D vite
// b. 修改 package.json 文件,配置启动命令
// 目录: packages/smart-ui-vite
"scripts": {
"dev": "vite"
},
// c. 生成 index.html 文件,运行命令 pnpm dev,浏览器查看运行结果
// 目录: packages/smart-ui-vite/index.html
<!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>title</title>
</head>
<body>
<h1>My Vite</h1>
</body>
</html>
// d. 在 src 目录中创建 index.ts 文件。
// 目录: packages/smart-ui-vite/src/index.ts
const s: string = "Hello Vite";
alert(s);
// e. 修改 index.html 文件,增加以下语句,导入创建的 index.ts 文件。运行成功后,浏览器出现“Hello Vite”字样的弹窗提示
// 目录: packages/smart-ui-vite/index.html
<script src="./src/index.ts" type="module"></script>
三种开发 vue 组件的方法
前置知识
安装 vue 依赖包后,可以直接编写使用渲染函数的 vue 组件。render 函数是模版字符串的一种替代形式,该方法可以通过 JavaScript 声明组件的渲染输出
单文件组件是框架指定的文件格式,需要 @vue/compiler-sfc 编译成 javascript 和 css ,这样就可以按照 es 模块的方式进行导入使用。目前 Vue3 已经内置该依赖,不过 vite 还需要安装依赖 @vitejs/plugin-vue 来处理该文件。
开发 jsx 组件,需要安装 @vitejs/plugin-vue-jsx 依赖。当使用 tsx 语法的时候,还需要在 tsconfig.json 文件中增加 "jsx": "preserve"
配置项,这样 typescript 才能保证 jsx 编译的完整性。
操作步骤
- 使用渲染函数
// a. 安装依赖包
// 目录: packages/smart-ui-vite
pnpm i vue
// b. 通过渲染函数创建 vue 组件
// 目录: packages/smart-ui-vite/src/button/render-button.ts
import { defineComponent, h } from "vue";
export default defineComponent({
name: "renderButton",
render() {
return h("button", null, "RenderButton");
},
});
// c. 引入 RenderButton 组件
// 目录: packages/smart-ui-vite/src/index.ts
import { createApp } from "vue";
import RenderButton from "./button/render-button";
createApp(RenderButton).mount("#app");
// d. index.html 增加挂载点
// 目录: packages/smart-ui-vite/index.html
<div id="app"></div>
- 使用单文件组件
// a. 安装插件
// 目录: packages/smart-ui-vite
pnpm i @vitejs/plugin-vue -D
// b. 增加 vite 配置项
// 目录: packages/smart-ui-vite/vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()],
});
// c. 添加单文件组件
// 目录: packages/smart-ui-vite/src/button/sfc-button.vue
<template>
<button>SFC Button</button>
</template>
<script lang="ts">
export default {
name: "SFCButton",
};
</script>
// d. 修改 index.ts 文件,引入创建的 sfc-button 组件
// 目录: packages/smart-ui-vite/src/index.ts
import { createApp } from "vue";
// import RenderButton from "./button/render-button";
import SFCButton from "./button/sfc-button.vue";
createApp(SFCButton).mount("#app");
// e. 创建 shims-vue.d.ts 文件,解决 ts 文件中引入 vue 文件的报错问题
// 目录: packages/smart-ui-vite/src/shims-vue.d.ts
declare module "*.vue" {
import { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}
- 使用 JSX 组件
// a. 安装 jsx 依赖包
// 目录: packages/smart-ui-vite
pnpm i @vitejs/plugin-vue-jsx -D
// b. 修改 vite.config.ts 文件
// 目录: packages/smart-ui-vite/vite.config.ts
import vueJsx from "@vitejs/plugin-vue-jsx";
export default defineConfig({
plugins: [vue(), vueJsx()],
});
// c. 创建 jsx 组件
// 目录: packages/smart-ui-vite/src/button/jsx-button.tsx
import { defineComponent } from "vue";
export default defineComponent({
name: "JSXButton",
render() {
return <button>JSX Button</button>;
},
});
// d. 修改 index.ts 文件,导入 jsx 编写的组件
// 目录: packages/smart-ui-vite/src/index.ts
import JSXButton from "./button/jsx-button";
createApp(JSXButton).mount("#app");
// e. 新建 tsconfig.json 文件,解决 tsx 文件中“找不到 React 的报错”
// 目录: packages/smart-ui-vite/tsconfig.json
{
"compilerOptions": {
"declaration": true /* 生成相关的 '.d.ts' 文件。 */,
"declarationDir": "./dist/types" /* '.d.ts' 文件输出目录 */,
"jsx": "preserve"
},
"include": ["./**/*.*", "./shims-vue.d.ts"],
"exclude": ["node_modules"],
"esModuleInterop": true,
"allowSyntheticDefaultImports": "true"
}
库文件封装
前置知识
组件库一般支持全局组件引入和单个组件引入,所以在做组件封装的时候提供了单个组件导出,以及通过插件的方式进行全局组件注入。
操作步骤
- 文件封装
// a. 创建入口文件 /src/entry.ts,导出全部组件和单个组件
// 目录: packages/smart-ui-vite/src/entry.ts
import { App } from "vue";
import RenderButton from "./button/render-button";
import SFCButton from "./button/sfc-button.vue";
import JSXButton from "./button/jsx-button";
export { RenderButton, SFCButton, JSXButton };
export default {
install(app: App): void {
app.component(RenderButton.name, RenderButton);
app.component(SFCButton.name, SFCButton);
app.component(JSXButton.name, JSXButton);
},
};
// b. 修改配置文件 vite.config.ts
// 目录: packages/smart-ui-vite/vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
const rollupOptions = {
external: ["vue"],
output: {
globals: {
vue: "Vue",
},
},
};
export default defineConfig({
plugins: [vue(), vueJsx()],
build: {
rollupOptions,
minify: false,
lib: {
entry: "./src/entry.ts",
name: "SmartUI",
fileName: "smart-ui",
formats: ["es", "umd", "iife"],
},
},
});
// c. 修改 package.json
// 目录: packages/smart-ui-vite/package.json
"scripts": {
"build": "vite build"
},
// d. 执行打包命令
// 目录: packages/smart-ui-vite
pnpm build
- 验证结果
// a. 创建 demo/esm/index.html 文件,验证全局导入组件的方式
// 目录: packages/smart-ui-vite/demo/esm/index.html
<!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>Document</title>
</head>
<body>
<h1>全局引入</h1>
<div id="app"></div>
<script type="module">
import { createApp } from "vue/dist/vue.esm-bundler.js";
import SmartUI from "../../dist/smart-ui.mjs";
createApp({
template: `
<RenderButton/>
<TSXButton/>
<SFCButton/>
`,
})
.use(SmartUI)
.mount("#app");
</script>
</body>
</html>
// b. 创建 demo/esm/button.html 文件,验证按需引入组件的情况
// 目录: packages/smart-ui-vite/demo/esm/button.html
<!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>Document</title>
</head>
<body>
<h1>按需引入</h1>
<div id="app"></div>
<script type="module">
import { createApp } from "vue/dist/vue.esm-bundler.js";
import {
SFCButton,
TSXButton,
RenderButton,
} from "../../dist/smart-ui.mjs";
createApp({
template: `
<RenderButton/>
<TSXButton/>
<SFCButton/>
`,
})
.component(SFCButton.name, SFCButton)
.component(TSXButton.name, TSXButton)
.component(RenderButton.name, RenderButton)
.mount("#app");
</script>
</body>
</html>
- 代码压缩和生成 sourcemap
// a. 安装依赖
// 目录: packages/smart-ui-vite
pnpm i terser -D
// b. 修改 vite.config.ts 配置文件,配置 terser 进行代码混淆,开启 sourcemap 可以在控制台看到源码
// 目录: packages/smart-ui-vite/vite.config.ts
export default defineConfig({
build: {
minify: "terser",
sourcemap: true,
...
},
});
观察打包后的文件即可看到配置是否生效
用 UnoCSS 实现原子化样式
前置知识点
UnoCSS 不是一个框架,而是一个引擎,它并没有提供核心工具类,而是引入预设和配置项来实现功能,详细介绍可见重新构想原子化CSS
safelist。当使用到 prop 动态引入样式的时候,类似这种形式 bg-${props.color}-500,组件无法正常加载相应的颜色。这是因为 unocss 是在编译的时候静态提取对应的样式,解决方式是配置一个 safelist,提前告知 uncoss 会动态加载那些样式。
操作步骤
// a. 安装依赖库,其中 @iconify-json/ic 是 json 形式的 icon 集合
// 目录: packages/smart-ui-vite
pnpm i -D unocss @iconify-json/ic
// b. 由于 Unocss 的配置比较多,所以另外新建一个文件 unocss.ts
// 目录: packages/smart-ui-vite/config/unocss.ts
import Unocss from "unocss/vite";
import { presetUno, presetAttributify, presetIcons } from "unocss";
const colors = ["red", "gray", "yellow"];
const safelist = [
...colors.map((v) => `bg-${v}-500`),
...colors.map((v) => `hover:bg-${v}-700`),
...["search", "edit", "check"].map((v) => `i-ic-baseline-${v}`),
];
export default () =>
Unocss({
safelist,
presets: [presetUno(), presetAttributify(), presetIcons()],
});
// c. vite.config.ts 导入 vite 配置
// 目录: packages/smart-ui-vite/vite.config.ts
import Unocss from "./config/unocss";
export default defineConfig({
plugins: [Unocss()],
build: {
cssCodeSplit: true, // 独立输出 css 样式
...
}
})
// d. 新建文件 index.tsx, 创建提供 color 和 icon 这两个 props 的 jsx 组件。
// 目录: packages/smart-ui-vite/src/button/index.tsx
import { defineComponent, PropType } from "vue";
import "uno.css";
type IColor = "red" | "gray" | "yellow";
type IIcon = "search" | "edit" | "check";
const props = {
color: {
type: String as PropType<IColor>,
default: "blue",
},
icon: {
type: String as PropType<IIcon>,
},
};
export default defineComponent({
name: "UButton",
props,
setup(props, { slots }) {
return () => (
<button
class={`py-2 bg-${props.color}-500 hover:bg-${props.color}-700 border-none`}
>
{!props.icon ? "" : <i class={`i-ic-baseline-${props.icon} p-3`}></i>}
{slots.default ? slots.default() : ""}
</button>
);
},
});
// e. 修改 index.ts 文件,测试组件使用不同 color 和 icon 的情况。
// 目录: packages/smart-ui-vite/src/index.ts
import UButton from "./button/index";
import { createApp } from "vue/dist/vue.esm-bundler.js";
createApp({
template: `<div>
<UButton color="red" icon="search">红色按钮</UButton>
<UButton color="gray" icon="edit">绿色按钮</UButton>
<UButton color="yellow" icon="check">绿色按钮</UButton>
</div>`,
})
.component(UButton.name, UButton)
.mount("#app");
运行 pnpm build 之后就可以看到如图所示的效果
创建新项目验证 monoropo 思想
前置知识点
docs-vite 项目可以通过安装依赖包的形式引入项目 smart-ui-vite,并且依赖包是指向 workspace,这是 monorepo 的关键功能点。
操作步骤
// a. packages 目录下创建新的文件夹 docs-vite
// b. 初始化项目
// 目录: packages/docs-vite
pnpm init
// c. 在 workspace 也就是项目根目录中(如果 packages 中的子项目有安装该依赖,可以删除掉,使用 workspace 的 vite 依赖包)
// 项目根目录
pnpm i vite -w
// d. 安装 vue 和 smart-ui-vite
// 目录: packages/docs-vite
pnpm i vue
pnpm i smart-ui-vite
// e. 创建 index.html 文件,引入依赖进行测试
// 目录: packages/docs-vite/index.html
<!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>Document</title>
</head>
<body>
<div id="app"></div>
<script type="module">
import SmartUI from "smart-ui-vite/dist/smart-ui.mjs";
import { createApp } from "vue/dist/vue.esm-bundler.js";
import 'smart-ui-vite/dist/assets/entry.d2de1b65.css' // 注意:生成的 css 文件默认会包含有 hash 值,需要根据实际情况进行引入
createApp({
template: `
<UButton color="gray" icon="search">gray button</UButton>
<UButton color="red" icon="edit">red button</UButton>
<UButton color="yellow" icon="check">red button</UButton>
`,
})
.use(SmartUI)
.mount("#app");
</script>
</body>
</html>
// f. 配置 docs-vite 项目的启动命令,并启动 pnpm dev
// 目录: packages/docs-vite/package.json
{
...
"scripts": {
"dev": "vite"
},
...
}
使用 monoropo 方式引入的依赖包
运行命令后即可在浏览器中查看到对应的效果
完善项目工程化
该部分的源代码已经上传到 GitHub
文档建设
前置知识
vitepress 是基于 vuepress,不同的是 vitepress 使用了 vite 进行构建,而不是 vuepress 的 webpack 构建,这明显提高了启动,更新和构建的速度,详情可查看官方中文网站。
vitepress-theme-demoblock 是一款 vitepress 的主题插件,它增强了编写 vue 组件的能力。一方面解决了 vitepress 不能现实 script 和 style 部分代码的问题,另一方面解决了组件代码和代码示例相同却需要写两遍的问题。详情可查看 GitHub 的介绍。
操作步骤
// a. 安装两个开发环境依赖包
// 目录: packages/smart-ui-vite
pnpm i vitepress vitepress-theme-demoblock -D
// b. 因为文档需要编写 jsx 代码,引入 unocss 调整样式,所以需要增加 vite.config.ts 文件
// 目录: packages/smart-ui-vite/docs/vite.config.ts
import { defineConfig } from "vite";
import vueJsx from "@vitejs/plugin-vue-jsx";
import Unocss from "../config/unocss";
export default defineConfig({
plugins: [vueJsx(), Unocss()],
});
// c. 配置文档相关的脚本命令
// 目录: packages/smart-ui-vite/package.json
"scripts": {
...
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:serve": "vitepress serve docs"
},
// d. 生成导航菜单,增加 markdown 插槽,可以同时显示源代码和界面
// 目录:packages/smart-ui-vite/docs/.vitepress/config.ts
const sidebar = {
"/": [
{ text: "快速开始" },
{ text: "通用", children: [{ text: "Button 按钮", link: "/" }] },
{ text: "导航" },
],
};
const config = {
themeConfig: {
sidebar,
},
markdown: {
config: (md) => {
const { demoBlockPlugin } = require("vitepress-theme-demoblock");
md.use(demoBlockPlugin);
},
},
};
export default config;
// e. enhanceApp 方法中引入 SmartUI 组件,另外注册 Demo 和 DemoBlock 组件
// 目录:packages/smart-ui-vite/docs/.vitepress/theme/index.ts
import Theme from "vitepress/dist/client/theme-default";
import SmartUI from "../../../src/entry";
import Demo from "vitepress-theme-demoblock/components/Demo.vue";
import DemoBlock from "vitepress-theme-demoblock/components/DemoBlock.vue";
import "vitepress-theme-demoblock/theme/styles/index.css";
export default {
...Theme,
enhanceApp({ app }) {
app.use(SmartUI);
app.component("Demo", Demo);
app.component("DemoBlock", DemoBlock);
},
};
- 按钮示例的核心代码
目录: packages/smart-ui-vite/docs/index.md
- 最后的实现效果
文档建设
前置知识
当前选用了 vercel 作为文档部署的网站,它提供了图形化的操作界面,提供了简洁的部署服务。需要注意的是,由于 vercel 是国外网站,需要翻墙才能访问部署后的网站。
操作步骤
- 配置静态页面部署服务器。
选用了 vercel 作为部署服务器,它可以通过 GitHub 账号免费注册登陆,然后可以选择需要部署的项目,如下图所示。其中包括5个项目配置项,包括构建工具,构建命令,构建文件存放的目录,安装依赖命令,部署项目所在的相对路径。配置完成后即可部署访问生成的网站,之后有代码提交后也会重新自动部署。
- 配置 homepage
// 可以将 vercel 生成的静态网站地址放到 homepage
// 目录: package.json
"homepage": "https://smart-admin-plus.vercel.app/",
单元测试
前置知识
vitest 是一款极速的单元测试框架,它可以和 vite 共享配置文件 vite.confit.ts。另外,它使用了和 Jest 相同的 API,可以很快的上手,也实现了单元测试最常见的模拟,快照及覆盖率功能。
代码在开发环境中验证通过后,一般需要部署到一个全新的系统环境去验证,这种机制就是持续集成服务。这里为了方便,使用了 GitHub Action 去实现持续集成服务。通俗来说,就是创建 workflow,通过在 GitHub 服务器运行单元测试来进行回归验证。
操作步骤
// a. 安装依赖,其中 happy-dom 可以在 node 环境下模拟 dom 环境,@vue/test-utils 是 vue 提供的测试工具库。
// 目录:packages/smart-ui-vite
pnpm i -D vitest happy-dom @vue/test-utils
// b. vite 增加 test 配置项
// 目录:packages/smart-ui-vite/vite.config.ts
/// <reference types="vitest"/>
import { defineConfig } from "vite";
export default defineConfig({
...
test: {
globals: true,
environment: "happy-dom",
transformMode: {
web: [/.[tj]sx$/],
},
},
});
// c. 将 index.tsx 文件变成只处理组件导出功能,而原来的组件代码迁移到同目录的 button.tsx 文件中
// 目录:packages/smart-ui-vite/src/button/index.tsx
import Button from "./button";
export default Button;
// d. 编写测试用例,第一个测试按钮的文本内容,第二个测试组件的默认类名是否生效
// 目录:packages/smart-ui-vite/src/button/__tests__/button.spec.ts
import { describe, expect, test } from "vitest";
import { shallowMount } from "@vue/test-utils";
import Button from "../button";
describe("button", () => {
test("mount", () => {
const wrapper = shallowMount(Button, {
slots: { default: "Button" },
});
expect(wrapper.text()).toBe("Button");
});
});
describe("color", () => {
test("default", () => {
const wrapper = shallowMount(Button);
expect(wrapper.classes().includes("bg-blue-500")).toBe(true);
});
});
// e. 配置运行命令
// 目录:packages/smart-ui-vite/package.json
{
...
"scripts": {
...
"test": "vitest",
}
}
// f. 在文件 main.yml 中创建 jobs,push 到 GitHub 后就会运行 CI 服务
// .github/workflows/main.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
# Lint:
UnitTest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: pnpm/action-setup@v2.1.0
with:
version: 7.2.1
- name: Install modules
run: pnpm install
- name: Run Test
run: cd packages/smart-ui-vite && pnpm run test:run
部署成功后,每次 push 都会触发。
可以通过 GitHub Action 右上角的 create status badge 生成徽章,然后将代码粘贴到 readme.md 中
// g. 增加 coverage 参数,用于生成测试覆盖率报告。安装对应的开发依赖包 @vitest/coverage-istanbul 和 @vitest/coverage-c8
// packages/smart-ui-vite/package.json
"scripts": {
"coverage": "vitest run --coverage"
},
// h. 修改配置文件
// packages/smart-ui-vite/vite.config.ts
test: {
coverage: {
provider: "istanbul", // 指定覆盖引擎
reporter: ["text", "json", "html"], // 指定输出格式
},
},
运行 pnpm coverage 即可看到生成的测试报告目录,打开 index.html 文件即可看到对应的测试结果。
规范化
前置知识
eslint 和 prettier 常常结合起来,用于对代码语法和格式进行校验,其中 eslint 常用于语法校验,而 prettier 用于代码格式化
husky 是 git hooks 工具,可以在提交 git 操作的时候执行自定义操作
commitlint 可以约束 git commit 提交的内容,使其符合规范
步骤
- eslint + prettier 代码检查工具
// a. 安装依赖
// 目录:packages/smart-ui-vite
pnpm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-formatter-pretty eslint-plugin-json eslint-plugin-prettier eslint-plugin-vue @vue/eslint-config-prettier babel-eslint prettier
// b. 增加 eslint 配置项
// 目录:packages/smart-ui-vite/.eslintrc.cjs
module.exports = {
root: true,
env: {
browser: true,
es2020: true,
node: true,
jest: true,
},
globals: {
ga: true,
chrome: true,
__DEV__: true,
},
parser: "vue-eslint-parser",
extends: [
"plugin:json/recommended",
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/prettier",
],
plugins: ["@typescript-eslint"],
parserOptions: {
parser: "@typescript-eslint/parser",
},
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
"prettier/prettier": "error",
},
};
// c. 配置不进行 eslint 检查的文件或目录
// 目录:packages/smart-ui-vite/.eslintignore
*.sh
node_modules
lib
coverage
*.md
*.scss
*.woff
*.ttf
src/index.ts
dist
// d. 配置校验和格式化命令
// 目录:packages/smart-ui-vite/package.json
{
"scripts": {
"lint": "eslint --fix --ext .ts,.vue src",
"format": "prettier --write \"src/**/*.ts\" \"src/**/*.vue\"",
},
}
- husky 自动化提交验证。由于 .git 文件存放在项目根目录,所以 husky 相关的指令在 workspace 中进行
// a. 安装依赖
// 根目录
pnpm i husky -D
// b. 增加 prepare 命令,运行 pnpm run prepare 即可生成 .husky 目录
// 目录:package.json
{
"scripts": {
...
"prepare": "husky install"
},
}
// c. 运行命令,增加 commit 之前的代码检查
// 根目录
npx husky add .husky/pre-commit "cd packages/smart-ui-vite && pnpm run lint"
// d. 增加测试命令,可以在不检视文件更改的条件下执行一次测试
// 目录:packages/smart-ui-vite/package.json
{
"scripts": {
...
"test:run": "vitest run",
}
}
// e. 运行命令,提交代码前进行一次单元测试
// 根目录
npx husky add .husky/pre-push "cd packages/smart-ui-vite && pnpm test:run"
- git commit 提交规范。类似于 husky,这个也是作用于 workspace 中
// a. 安装依赖
// 根目录
pnpm i -D @commitlint/config-conventional @commitlint/cli
// b. 增加 commitlint 配置文件
// 目录: commitlint.config.ts
module.exports = {
extends: ["@commitlint/config-conventional"],
};
// c. 执行命令,git commit 的时候增加对 commit message 的内容校验
// 根目录
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit ""'
发布到 NPM 仓库
前置知识
确定依赖的版本号,就可以打上 tag 进行发布。首次发布的时候可以手动使用 npm 的命令进行操作,后续可以使用 GitHub Action 进行自动发布。
操作步骤
- 确定依赖包的版本号,版本号的格式一般是“主版本号.次版本号.修订号”
- 打上 tag
# 创建版本号对应的 git tag,并推送到远程仓库
git tag 1.0.0
git push --tag
- 首次发布
// a. 创建发布 npm 包用的 shell 脚本。发布的时候可能出现连接错误,可取消重试几次
// packages/smart-ui-vite/publish.sh
#!/usr/bin/env bash
npm config get registry
npm config set registry=https://registry.npmjs.org
echo "请进行登录操作"
npm login
echo "-------publishing------"
npm publish
npm config set registry=https://registry.npm.taobao.org
echo "发布完成"
exit
// b. 使用命令行,给发布脚本增加执行权限
chmod +x publish.sh
// c. 使用命令行运行发布命令
./publish.sh
发布的时候需要输入用户名,密码,邮箱地址,发送到邮箱的一次性密码
-
使用 GitHub Action 自动发布
首次发布的时候需要用户名和密码,之后就可以使用 github action 实现自动发布。
a. 创建发布用的 NPM_PUBLISH_TOKEN。创建 npm token 的具体步骤可见截图。注意,创建新 token 的时候,type 需要选择 publish 类型;生成 token 之后需要注意保存,因为只有首次生成后可见,GitHub 生成的 token 也是如此。
b. 创建发布用的 ACCESS_TOKEN。创建 GitHub 的 access token,具体步骤可见截图。注意,创建的 token 需要有操作目标仓库的权限。
c. 将生成的 NPM_PUBLISH_TOKEN 和 ACCESS_TOKEN 放到 actions secrets 里面,相当于创建了两个保存 token 的变量,这样代码里面就可以直接使用而不会泄漏 token。
d. 创建发布分支,增加发布脚本。当发布分支增加内容后,就会触发发布脚本,将 smart-ui-vite 发布到 npm 仓库
// 创建分支
// 根目录
git checkout -b publish-smart-ui-vite
git push --set-upstream origin publish-smart-ui-vite
// 编写发布脚本
// 目录:.github/workflows/publish.yml
name: Publish Smart-ui-vite To Npm
on:
push:
branches: [publish-smart-ui-vite]
jobs:
publish:
runs-on: ubuntu-latest
name: "publish npm"
environment: npm
steps:
- uses: actions/checkout@master
- uses: pnpm/action-setup@v2.1.0
with:
version: 6.31.0
- name: Install modules
run: pnpm install
- name: Build
run: cd packages/smart-ui-vite && npm run build
- name: "Publish to the npm registry"
uses: primer/publish@3.0.0
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
NPM_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} # 使用上一步生成的变量
with:
default_branch: "publish"
dir: "packages/smart-ui-vite"
e. 查看发布成功的 npm 包。需要注意的是,由于 npm 有严格的重名检测,所以这里使用的包名是 mock-ui-vite-plus
架构复用:使用 cli 工具提高研发效率
前置知识
脚手架是帮助开发过程的工具和环境配置的集合,现在的脚手架一般是通过 cli,也就是命令行界面进行封装的。
步骤
- 创建模版项目
项目源码已经上传到 GitHub
// a. 这里直接参考 Vue3 官网创建一个模版项目,使用命令`npm init vue@latest`,后面跟着提示操作即可,之后的配置可以都选择 No。
// 接下来通过 `npm i mock-ui-vite-plus` 安装已经发布的 npm 包
// b. 修改 main.ts 文件,导入 npm 包提供的组件和样式
// 目录:src/main.ts
import { createApp } from "vue";
import App from "./App.vue";
import SmartUI from "mock-ui-vite-plus"; // 导入 npm 包
import "mock-ui-vite-plus/dist/assets/entry.css"; // 导入样式
import "./assets/main.css";
createApp(App).use(SmartUI).mount("#app");
// c. 修改 app.vue 文件,使用 mock-ui-vite-plus 提供的组件
// src/app.vue
<script setup lang="ts"></script>
<template>
<div style="margin-top: 50px">
<UButton color="gray" icon="search">gray button</UButton>
<UButton color="red" icon="edit">red button</UButton>
<UButton color="yellow" icon="check">red button</UButton>
</div>
</template>
d. 另外可以将背景色修改成白色,这样启动项目后方便看到如下图所示的效果。
// e. 将项目中的 package.json 文件修改为模版生成代码,这里只是将 name 变成变量,可以跟随项目名称进行变化。
// 目录: template/package.hbs.json
{
"name": "{{ name }}",
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check build-only",
"preview": "vite preview --port 4173",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"mock-ui-vite-plus": "0.0.0-4201a75",
"vue": "^3.2.38"
},
"devDependencies": {
"@types/node": "^16.11.56",
"@vitejs/plugin-vue": "^3.0.3",
"@vitejs/plugin-vue-jsx": "^2.0.1",
"@vue/tsconfig": "^0.1.3",
"npm-run-all": "^4.1.5",
"typescript": "~4.7.4",
"vite": "^3.0.9",
"vue-tsc": "^0.40.7"
}
}
- 创建 cli 工具
// a. 在 packages 文件中创建 cli 项目 create-smart-app-cli,安装相关依赖
// 目录:packages/create-smart-app-cli
pnpm init
pnpm i chalk chalk-animation clear download-git-repo figlet handlebars inquirer ora
// b. 编写生成终端操作界面的代码,可以选择使用模版还是退出
// 目录:packages/create-smart-app-cli/bin/index.js
#!/usr/bin/env node
import { promisify } from "util";
import figlet from "figlet";
import clear from "clear";
import chalk from "chalk";
import inquirer from "inquirer";
import chalkAnimation from "chalk-animation";
const opt = {
"SmartUI应用模版(Vite)": "smart-ui-vite",
退出: "quit",
};
const question = [
{
type: "rawlist" /* 选择框 */,
message: "请选择要创建的项目?",
name: "operation",
choices: Object.keys(opt),
},
];
clear();
const logo = figlet.textSync("Smart UI!", { // 打印欢迎画面
horizontalLayout: "default",
verticalLayout: "default",
width: 80,
whitespaceBreak: true,
});
const rainbow = chalkAnimation.rainbow(logo);
setTimeout(() => {
rainbow.stop(); // Animation stops
query();
}, 500);
async function query() {
const answer = await inquirer.prompt(question);
if (answer.operation === "退出") return;
const { default: op } = await import(`../lib/operations/${opt[answer.operation]}.js`);
await op();
}
// c. 编写克隆项目 smart-ui-app-js-template 的代码
// 目录: packages/create-smart-app-cli/lib/operations/smart-ui-vite.js
import clone from "../utils/clone.js";
import inquirer from "inquirer";
import { resolve } from "path";
import fs from "fs";
import chalk from "chalk";
import handlebars from "handlebars";
const log = (...args) => console.log(chalk.green(...args));
export default async () => {
const { name } = await inquirer.prompt([
{
type: "input" /* 选择框 */,
message: "请输入项目的名称?",
name: "name",
},
]);
log("创建项目:" + name);
// 从github克隆项目到指定文件夹
await clone("github:leedawn/smart-ui-app-js-template", name);
// 生成路由定义
compile(
{
name,
},
`./${name}/package.json`,
`./${name}/template/package.hbs.json`
);
log(`
安装完成:
To get Start:
===========================
cd ${name}
npm i
npm run dev
===========================
`);
};
// 编译模板文件
function compile(meta, filePath, templatePath) {
if (fs.existsSync(templatePath)) {
const content = fs.readFileSync(templatePath).toString();
const result = handlebars.compile(content)(meta);
fs.writeFileSync(filePath, result);
log(`${filePath} 修改成功`);
} else {
log(`${filePath} 修改失败`);
}
}
// d. 克隆项目通过 ora 库增加进度条
// 目录:packages/create-smart-app-cli/lib/utils/clone.js
import { promisify } from "util";
import download from "download-git-repo";
import ora from "ora";
export default async (repo, desc) => {
const process = ora(`下载.....${repo}`);
process.start();
await promisify(download)(repo, desc);
process.succeed();
};
// e. package.json 增加一个 bin 属性,声明一个可执行文件 create-smart
// 目录:packages/create-smart-app-cli/package.json
{
"name": "create-smart-app-cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"bin": {
"create-smart": "./bin/index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"chalk": "^5.1.2",
"chalk-animation": "^2.0.3",
"clear": "^0.1.0",
"download-git-repo": "^3.0.2",
"figlet": "^1.5.2",
"handlebars": "^4.7.7",
"inquirer": "^9.1.4",
"ora": "^6.1.2"
}
}
// f. 通过 npm link 模拟全局安装的效果
// 目录:packages/create-smart-app-cli
sudo npm link
create-smart
执行命令后,控制台出现如下界面,选择应用模版即可创建对应的项目
-
发布 cli 工具
现在 Vue3 推荐创建项目的方法是使用 npm init vue 命令,类似的,刚才创建的 create-smart-app-cli 项目(项目名称需要以“create-”开头)也可以使用这种方式进行创建。按照前面的步骤,只需将项目 create-smart-app-cli 发布到 npm,然后本地就可以通过 npm init smart-app-cli 命令创建项目。需要注意的是,从项目发布到本地使用存在一定的延迟,所以最好过几分钟再使用命令创建项目。
总结
本项目使用了多包单仓库的开发模式来实践前端工程化方案。首先使用三种方法编写了 vue 组件,UnoCSS 引擎添加样式;采用 vitepress 进行文档建设,vercel 进行线上的文档部署;采用 vitest 搭建单元测试环境,生成覆盖率测试报告;使用 eslint,prettier,husky,commitLint 实现代码规范化 ;使用 GitHub Action 实现持续集成服务。最后也实现了一个 mini 的 cli 工具,并且发布到了 npm 仓库。当然,这里面每个模块都可以进一步细化,都值得进一步探索。
本文参考《基于 vite 的组件库工程化实战》
暂无评论内容