jsx 与 .vue 模板开发选择
jsx:适合高度复杂可定制化的组件使用 .vue: 适合简单的、一般的定制化组件使用
创建项目
mkdir vue3-ui-lib
cd vue3-ui-lib
项目结构
.
├── docs
├── package.json
├── packages
│ ├── components
│ │ └── package.json
│ ├── theme-chalk
│ │ └── package.json
│ └── utils
│ └── package.json
├── play
├── pnpm-workspace.yaml
├── README.md
├── tsconfig.json
└── typings
└── vue-shime.d.ts
PS: -w
等价于 --workspace-root
创建 package.json
pnpm init
{
"name": "vue3-ui-lib",
"private": true,
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "pnpm -C play dev"
},
"keywords": [],
"author": "",
"license": "ISC"
}
PS:这里 "dev": "pnpm -C play dev"
指定了在项目根目录下执行脚本命令可以执行 play 这个下面的脚本。
创建 .npmrc
touch .npmrc
# 使用 pnpm 安装包时提升模块的依赖到 node_modules 下
shamefully-hoist = true
安装 typescript
pnpm install typescript -D
创建 tsconfig.json
npx tsc --init
{
"compilerOptions": {
"target": "es2016", // 遵循 ES6 规范
"module": "ESNext", // 打包模块类型
"declaration": false, // 默认打包后不要声明文件
"noImplicitAny": true, // 默认可以 any
"removeComments": true, // 移除注释
"moduleResolution": "node", // 安装 node 模块解析
"esModuleInterop": true, // 支持 es、commonjs 模块
"jsx": "preserve", // 告诉 ts 不处理 jsx 语法
"noLib": false, // 不处理库
"sourceMap": true,
"lib": [
// 编译时使用的库
"ESNext",
"DOM"
],
"allowSyntheticDefaultImports": true, // 允许没有导出的模块中导入
"experimentalDecorators": true, // 实验语法,支持装饰器
"forceConsistentCasingInFileNames": true, // 强制区分大小写
"resolveJsonModule": true, // 解析 json 模块
"strict": true, // 严格模式
"skipLibCheck": true // 跳过类库检测
},
"exclude": [
// 不需要解析的文件
"node_modules",
"**/__tests__",
"dist/**"
]
}
创建 pnpm-workspace.yaml
touch pnpm-workspace.yaml
packages:
- "packages/**" # 存放所有编写的组件
- play # 存放开发测试组件的代码
- docs # 存放组件使用的开发文档
创建 typings/vue-shime.d.ts
mkdir typings
cd typings
touch vue-shime.d.ts
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}
在根项目 package.json 中添加我们的组件包依赖
# 切换到项目根目录执行如下命令,这样就可以在 package.json 查看依赖了
pnpm install @yun/components -w
PS:
-w
等价--workspace-root
@yun/components
:我们的组件中 package.json 中配置的 name 属性值名称
创建 play 的测试项目
mkdir play
cd play
pnpm init
pnpm install vue vue-tsc vite @vitejs/plugin-vue -D
play/package.json
pnpm init
{
"name": "play",
"version": "1.0.0",
"description": "",
"main": "index.js",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@vitejs/plugin-vue": "^3.0.1",
"vite": "^3.0.2",
"vue": "^3.2.37"
}
}
play/vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()],
});
play/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 src="src/main.ts" type="module"></script>
</body>
</html>
play/src/main.ts
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
app.mount("#app");
play/src/App.vue
<template>Test</template>
<script setup lang="ts"></script>
<style scoped></style>
BEM 类名规范
block:代码片段 => y-button
element:元素 => y-button__element
modifier:装饰 => y-button__element--disabled
state:状态 => is-checked
packages/utils/create.ts
/**
* BEM 规范
* block:代码片段 => y-button
* element:元素 => y-button__element
* modifier:装饰 => y-button__element--disabled
* state:状态 => is-checked
*
* y-button
* y-button__element
* y-button__element--disabled
* is-checked
*/
// z-button-box__element--modifier
function _bem(
prefixName: string,
blockSuffix: string,
element: string,
modifier: string
) {
if (blockSuffix) {
prefixName += `-${blockSuffix}`;
}
if (element) {
prefixName += `__${element}`;
}
if (modifier) {
prefixName += `--${modifier}`;
}
return prefixName;
}
function createBEM(prefixName: string) {
const b = (blockSuffix: string = "") => _bem(prefixName, blockSuffix, "", "");
const e = (element: string = "") =>
element ? _bem(prefixName, "", element, "") : "";
const m = (modifier: string = "") =>
modifier ? _bem(prefixName, "", "", modifier) : "";
const be = (blockSuffix: string = "", element: string = "") =>
blockSuffix && element ? _bem(prefixName, blockSuffix, element, "") : "";
const bm = (blockSuffix: string = "", modifier: string = "") =>
blockSuffix && modifier ? _bem(prefixName, blockSuffix, "", modifier) : "";
const em = (element: string = "", modifier: string = "") =>
element && modifier ? _bem(prefixName, "", element, modifier) : "";
const bem = (
blockSuffix: string = "",
element: string = "",
modifier: string = ""
) =>
element && modifier && blockSuffix
? _bem(prefixName, blockSuffix, element, modifier)
: "";
const is = (name: string, state: boolean) => (state ? `is-${name}` : "");
return {
b,
e,
m,
be,
bm,
em,
bem,
is,
};
}
export function createNamespace(name: string) {
const prefixName = `y-${name}`;
return createBEM(prefixName);
}
// const bem = createNamespace("icon");
// console.log(bem.b()); // y-icon
// console.log(bem.b("box")); // y-icon-box
// console.log(bem.e("element")); // y-icon__element
// console.log(bem.e()); // 空
// console.log(bem.m("modifier")); // y-icon--modifier
// console.log(bem.bem("box", "element", "modifier")); // y-icon-box__element--modifier
// console.log(bem.is("checked", true)); // is-checked
// console.log(bem.is("checked", false)); // 空
// console.log(bem.be("box", "element")); // y-icon-box__element
// console.log(bem.bm("box", "modifier")); // y-icon-box--modifier
// console.log(bem.em("element", "modifier")); // y-icon__element--modifier
第一个组件:Icon
packages/components/icon/src/icon.vue
<template>
<i :class="bem.b()" :style="style">
<slot />
</i>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { createNamespace } from "@yun/utils/create";
import { iconProps } from "./icon";
const bem = createNamespace("icon");
const props = defineProps(iconProps);
const style = computed(() => {
if (!props.size && !props.color) return {};
return {
...(props.size ? { fontSize: props.size + "px" } : {}),
...(props.color ? { color: props.color } : {}),
};
});
</script>
**注意:**这里使用了 script 的 setup 写法,不能自定义组件名,那如何解决呢?
-
方案一:不推荐
在对应的组件中添加脚本
+ <script lang="ts"> +import { defineComponent } from "vue"; +export default defineComponent({ + name: "YIcon", // 指定组件名 +}); + </script> <script setup lang="ts"> // TODO </script>
-
方案二:推荐
-
配置插件:
unplugin-vue-define-options
pnpm install unplugin-vue-define-options -D -w
-
配置 vite.config.ts
import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; +// 解决setup编写组件时, 自定义组件名 +import defineOptions from "unplugin-vue-define-options/vite"; export default defineConfig({ plugins: [ vue(), + defineOptions() ], });
-
在 .vue 的文件组件中调用 defineOptions 来指定组件名
<script setup lang="ts"> // other code ... +// 该函数调用需要在 vite.config.ts 配置插件 unplugin-vue-define-options/vite +defineOptions({ + name: "YIcon", // 指定组件名 +}); // other code ... </script>
-
packages/components/icon/src/icon.ts
import { ExtractPropTypes, PropType } from "vue";
export const iconProps = {
color: String,
size: [Number, String] as PropType<number | string>,
} as const;
export type IconProps = ExtractPropTypes<typeof iconProps>;
packages/components/icon/index.ts
import YIcon from "./src/icon.vue";
export default YIcon;
export * from "./src/icon";
使用 Icon 组件
在 play 测试项目中,使用 Icon 组件
play/src/App.vue
<template>+ <Icon>Test</Icon></template>
<script setup lang="ts">
+// import Icon from "@yun/components/icon/src/icon.vue";
+import Icon from "@yun/components/icon";
+console.log(Icon);
</script>
<style scoped></style>
通过 vue.use() 注册我们的组件
packages/components/icon/index.ts
import { Plugin } from "vue";
import _Icon from "./src/icon.vue";
//#region
export type SFCWithInatall<T> = T & Plugin;
/**
* 为组件添加 install 方法,便于 vue.use 使用该组件
* @param comp 组件对象
* @returns 组件
*/
export function withInstall<T>(comp: T) {
(comp as SFCWithInatall<T>).install = function (app) {
// 全局注册组件
app.component((comp as unknown as { name: string }).name, comp);
};
return comp as SFCWithInatall<T>;
}
//#endregion
export const YIcon = withInstall(_Icon);
export default YIcon;
export * from "./src/icon";
// 为了在template模板中使用组件时,有对应的 ts 提示功能
declare module "vue" {
export interface GlobalComponents {
YIcon: typeof YIcon;
}
}
volar 配合在 template 模板中使用组件有代码提示
// 为了在模板中使用组件时,有对应的 ts 提示功能
declare module "vue" {
export interface GlobalComponents {
YIcon: typeof YIcon;
}
}
play/src/main.ts
import { createApp } from "vue";
import App from "./App.vue";
+import YIcon from "@yun/components/icon";
const app = createApp(App);
+const plugins = [YIcon];
+plugins.forEach((plugin) => app.use(plugin));
app.mount("#app");
play/src/App.vue
<template>
<YIcon :color="'red'" :size="18">已经在main.ts注册了该组件,直接使用</YIcon>
<y-icon color="skyblue" :size="14"
>已经在main.ts注册了该组件,直接使用</y-icon
>
</template>
VitePress 编写文档
安装 vitepress
mkdir docs
cd docs
pnpm init
pnpm install vitepress -D
docs/package.json
{
"name": "docs",
"scripts": {
"dev": "vitepress dev ."
},
"devDependencies": {
"vitepress": "1.0.0-alpha.4"
}
}
项目根目录 package.json 配置执行脚本
"scripts": {
+ "docs:dev": "pnpm -C docs dev"
},
首页配置
docs/index.md
---
layout: home
hero:
name: yun-ui 组件库
text: 基于 Vue 3 的组件库.
tagline: 掌握 Vue3 组件编写
actions:
- theme: brand
text: 快速开始
link: /guide/quickStart
features:
- icon: 🛠️
title: 组件库构建流程
details: Vue3 组件库构建 ...
- icon: ⚙️
title: 组件库单元测试
details: Vue3 组件库测试 ...
---
vite.config.ts
docs/vite.config.ts
import { defineConfig } from "vite";
// 解决在 script 的 setup 中定义组件名: defineOptions({name: "YIcon"})
import DefineOptions from "unplugin-vue-define-options/vite";
export default defineConfig({
plugins: [DefineOptions()],
});
文档配置
docs/ .vitepress/config.js
module.exports = {
title: "YUN-UI",
description: "Vue3 YUN UI",
themeConfig: {
lastUpdated: "最后更新时间",
docsDir: "docs",
editLinks: true,
editLinkText: "编辑此网站",
repo: "https://gitee.com/login",
footer: {
message: "Released under the MIT License.",
copyright: "Copyright © 2022-present Timly",
},
nav: [
{ text: "指南", link: "/guide/installation", activeMatch: "/guide/" },
{ text: "组件", link: "/component/icon", activeMatch: "/component/" },
],
sidebar: {
"/guide/": [
{
text: "指南",
items: [
{ text: "安装", link: "/guide/installation" },
{ text: "快速开始", link: "/guide/quickStart" },
],
},
],
"/component/": [
{
text: "基础组件",
items: [{ text: "Icon", link: "/component/icon" }],
},
],
},
},
};
注册组件
在 vitepress 中注册我们的组件
docs/ .vitepress/theme/index.js
import DefaultTheme from "vitepress/theme";
import YIcon from "@yun/components/icon";
import "@yun/theme-chalk/src/index.scss";
console.log(YIcon);
export default {
...DefaultTheme,
enhanceApp({ app }) {
app.use(YIcon); // 在 vitepress 中,注册全局组件
},
};
guide
docs/guide/installation.md
# 安装
docs/guide/quickStart.md
# 快速开始
组件文档
编写我们的组件文档
docs/component/icon.md
# Icon 图标
yun-ui 推荐使用 [xicons](https://www.xicons.org/#/) 作为图标库。
```sh
pnpm install @vicons/material
```
## 使用图标
- 如果你想像用例一样直接使用,你需要全局注册组件,才能够直接在项目里使用。
<script setup lang="ts">
import {AddBoxOutlined} from "@vicons/material";
</script>
<y-icon :size="18" color="red">
<AddBoxOutlined />
</y-icon>
<y-icon :size="34" color="skyblue">
<AddBoxOutlined />
</y-icon>
```vue
<script setup lang="ts">
import { AddBoxOutlined } from "@vicons/material";
</script>
<template>
<y-icon :size="18" color="red">
<AddBoxOutlined />
</y-icon>
<y-icon :size="34" color="skyblue">
<AddBoxOutlined />
</y-icon>
</template>
```
## API
### Icon Props
| 名称 | 类型 | 默认值 | 说明 |
| ----- | ---------------- | --------- | -------- |
| color | string | undefined | 图标颜色 |
| size | number \| string | undefined | 图标大小 |
Eslint
npx eslint --init
安装依赖
pnpm install eslint-plugin-vue@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest eslint@latest -D -w
# 支持 vue 的 typescript 的 eslint 配置
pnpm install @vue/eslint-config-typescript -D -w
.eslintrc.js
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
"eslint:recommended",
"plugin:vue/vue3-recommended", // https://eslint.vuejs.org/
"plugin:@typescript-eslint/recommended",
"@vue/typescript/recommended",
],
parserOptions: {
ecmaVersion: "latest",
parser: "@typescript-eslint/parser",
sourceType: "module",
},
plugins: ["vue", "@typescript-eslint"],
rules: {
"vue/html-self-closing": "off",
"vue/singleline-html-element-content-newline": "off",
"vue/multi-word-component-names": "off",
"vue/prefer-import-from-vue": "off",
},
globals: {
defineOptions: "readonly",
},
};
.eslintignore
node_modules
dist
*.css
*.jpg
*.jpeg
*.png
*.gif
*.d.ts
prettier
.prettierrc.js
module.exports = {
singleQuote: false, // 使用双引号
semi: true, // 使用分号
trailingComma: 'none', // 末尾逗号
arrowParens: 'avoid', // 箭头函数括号
endOfLine: 'auto' // 结尾换行自动
}
.prettierignore
node_modules
dist
编辑器
.editorconfig
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
打包 packages/theme-chalk 样式
安装
pnpm install gulp @types/gulp sucrase -D -w
pnpm install gulp-sass gulp-autoprefixer gulp-clean-css sass @types/gulp-sass @types/sass @types/gulp-autoprefixer @types/gulp-clean-css -D -w
packages.json
- 配置项目根目录下 package.json 的执行脚本
"scripts": {
"dev": "pnpm -C play dev",
"docs:dev": "pnpm -C docs dev",
+ "build": "gulp -f build/gulpfile.ts"
},
build/gulpfile.ts
import { series } from "gulp";
import { run, withTaskName } from "./tools";
// 清除根目录下已经打包的 dist
const cleanDist = withTaskName("clean", async () => run("rm -rvf ./dist"));
// 执行 packages 目录中 package.json 配置有 build 的对应命令脚本
const buildPackages = withTaskName("buildPackages", async () =>
// 注意这里 --filter ./packages,在 pnpm7.6.0 中是找不到 packages 下的项目的,有 bug
run("pnpm run --filter ./packages --parallel build")
);
export default series(cleanDist, buildPackages);
build/tools/index.ts
import { spawn } from "child_process";
import { projectRoot } from "./paths";
export const withTaskName = <T>(name: string, fn: T) =>
Object.assign(fn, { displayName: name });
export const run = (command: string) => {
return new Promise((resolve, rejected) => {
const [cmd, ...args] = command.split(" ");
// 执行命令
const app = spawn(cmd, args, {
cwd: projectRoot,
stdio: "inherit", // 将子进程输出共享给父进程
shell: true
});
app.on("close", resolve);
});
};
build/tools/paths.ts
import path from "path";
export const projectRoot = path.resolve(__dirname, "../../");
packages/theme-chalk/package.json
"scripts": {
+ "build": "gulp"
},
packages/theme-chalk/gulpfile.ts
import path from "path";
import { series, src, dest } from "gulp";
import gulpSass from "gulp-sass";
import sass from "sass";
import gulpCleanCss from "gulp-clean-css";
import autoPrefixer from "gulp-autoprefixer";
function r(p: string) {
return path.resolve(__dirname, p);
}
// 编译 sass 到 css
const complieSassToCss = () => {
return src(r("./src/*.scss"))
.pipe(gulpSass(sass).sync())
.pipe(autoPrefixer())
.pipe(gulpCleanCss())
.pipe(dest("./dist/css"));
};
// 拷贝字体图标
const copyFont = () => {
return src(r("./src/font/**")).pipe(dest("./dist/fonts"));
};
// 把编译好的 css 拷贝到根目录下 dist 中
const copyFullStyle = () => {
return src(r("./dist/**")).pipe(dest(r("../../dist/theme-chalk")));
};
// 注意执行顺序
export default series(complieSassToCss, copyFont, copyFullStyle);
打包 packages/utils 工具
安装
pnpm install gulp-typescript -D -w
build/tools/config.ts
import { resolve } from "path";
import { outDir } from "./paths";
export const buildConfig = {
esm: {
module: "ESNext", // tsconfig 输出的结果 ES6 模块
format: "esm", // 需要配置格式化化后的模块规范
output: {
name: "es", // 打包到 dist 目录下的那个目录
path: resolve(outDir, "es")
},
bundle: {
path: "yun-ui/es"
}
},
cjs: {
module: "CommonJS",
format: "cjs",
output: {
name: "lib",
path: resolve(outDir, "lib")
},
bundle: {
path: "yun-ui/lib"
}
}
};
export type BuildConfig = typeof buildConfig;
build/tools/path.ts
import path from "path";
export const projectRoot = path.resolve(__dirname, "../../");
+ export const outDir = path.resolve(__dirname, "../../dist");
build/packages.ts
import path from "path";
import { src, series, dest, parallel } from "gulp";
import ts from "gulp-typescript";
import { withTaskName } from "./tools";
import { buildConfig } from "./tools/config";
import { outDir, projectRoot } from "./tools/paths";
export function buildPackages(dirname: string, name: string) {
const tasks = Object.entries(buildConfig).map(([module, config]) => {
const output = path.resolve(dirname, config.output.name);
const build = withTaskName(`build:${name || dirname}`, () => {
const tsConfig = path.resolve(projectRoot, "tsconfig.json"); // ts的配置文件的路径
const inputs = ["**/*.ts", "!gulpfile.ts", "!node_modules"];
return src(inputs)
.pipe(
ts.createProject(tsConfig, {
declaration: true, // 需要生成声明文件
strict: false,
module: config.module
})()
)
.pipe(dest(output));
});
const copy = withTaskName(`copy:${name || dirname}`, () => {
// 将 utils 模块编译 ts 后的 js 拷贝到 dist 目录下的 es 目录和 lib 目录
return src(`${output}/**`).pipe(
dest(path.resolve(outDir, config.output.name, name))
);
});
return series(build, copy);
});
// 将 ts -> js, 也可以使用 rullup 处理
return parallel(...tasks);
}
packages/utils/packages.json
"scripts": {
+ "build": "gulp"
},
packages/utils/gulpfile.ts
import { buildPackages } from "../../build/packages";
export default buildPackages(__dirname, "utils");
打包 packages/components 组件
安装
pnpm install rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs rollup-plugin-vue rollup-plugin-typescript2 -D -w
打包为整个组件文件
build/gulpfile.ts
import { series } from "gulp";
import { run, withTaskName } from "./tools";
// 清除根目录下已经打包的 dist
const cleanDist = withTaskName("clean", async () => run("rm -rf ./dist"));
// 执行 packages 目录中 package.json 配置有 build 的对应命令脚本
const buildPackages = withTaskName("buildPackages", async () =>
// 注意这里 --filter ./packages,在 pnpm7.6.0 中是找不到 packages 下的项目的,有 bug
run("pnpm run --filter ./packages --parallel build")
);
+// 处理 packages/components 的所有组件成一个单文件
+const buildAllComponent = withTaskName("buildFullComponent", async () =>
+ // 会去找 full-component.ts 中 buildFullComponent
+ run("pnpm run build buildFullComponent")
+);
// buildFullComponent 函数在 full-component.ts 中
+export * from "./full-component";
export default series(
cleanDist,
buildPackages,
+ buildAllComponent
);
build/tools/paths.ts
import path from "path";
export const projectRoot = path.resolve(__dirname, "../../");
export const outDir = path.resolve(__dirname, "../../dist");
+export const yunRoot = path.resolve(__dirname, "../../packages/yun");
build/full-component.ts
import path from "path";
import fs from "fs/promises";
import vue from "rollup-plugin-vue";
import typescript from "rollup-plugin-typescript2";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import { parallel } from "gulp";
import { OutputOptions, rollup } from "rollup";
import { outDir, yunRoot } from "./tools/paths";
const buildFull = async () => {
console.log("-----------------buildFull------------------------");
// rollup打包的配置信息
const config = {
input: path.resolve(yunRoot, "index.ts"), // 打包的入口
plugins: [nodeResolve(), typescript(), vue(), commonjs()],
external: (id: string) => /^vue/.test(id) // 排除打包的时候不需要打包 vue 代码
};
const buildConfig = [
{
format: "umd",
file: path.resolve(outDir, "index.js"),
name: "yun", // 全局的名字
exports: "named", // 导出的名字用命名的方式导出 liraryTarget:"var" var name = "xxx"
globals: {
// 表示使用的 vue 是全局的
vue: "Vue"
}
},
{
format: "esm",
file: path.resolve(outDir, "index.esm.js")
}
];
const bundle = await rollup(config);
return Promise.all(
buildConfig.map(config => bundle.write(config as OutputOptions))
);
};
export const buildFullComponent = parallel(buildFull);
打包成多个组件文件(支持引入组件库时可以按需加载)
安装
pnpm install fast-glob -D -w
build/gulpfile.ts
import { series } from "gulp";
import { run, withTaskName } from "./tools";
// 清除根目录下已经打包的 dist
const cleanDist = withTaskName("clean", async () => run("rm -rf ./dist"));
// 执行 packages 目录中 package.json 配置有 build 的对应命令脚本
const buildPackages = withTaskName("buildPackages", async () =>
// 注意这里 --filter ./packages,在 pnpm7.6.0 中是找不到 packages 下的项目的,有 bug
run("pnpm run --filter ./packages --parallel build")
);
// 处理 packages/components 的所有组件成一个文件
const buildAllComponent = withTaskName("buildFullComponent", async () =>
// 会去找 full-component.ts 中 buildFullComponent
run("pnpm run build buildFullComponent")
);
+// 引用我们组件时,打包按需加载的多个文件组件
+const buildSingleComponent = withTaskName("buildComponent", () =>
+ // 会去找 component.ts 中 buildComponent
+ run("pnpm run build buildComponent")
+);
// buildFullComponent 函数在 full-component.ts 中
export * from "./full-component";
+// buildComponent 函数在 component.ts 中
+export * from "./component";
export default series(
cleanDist,
buildPackages,
buildAllComponent,
+ buildSingleComponent
);
build/tools/paths.ts
import path from "path";
export const projectRoot = path.resolve(__dirname, "../../");
export const outDir = path.resolve(__dirname, "../../dist");
export const yunRoot = path.resolve(__dirname, "../../packages/yun");
+export const componentRoot = path.resolve(projectRoot, "packages/components");
build/tools/index.ts
import { spawn } from "child_process";
import { projectRoot } from "./paths";
export const withTaskName = <T>(name: string, fn: T) =>
Object.assign(fn, { displayName: name });
export const run = (command: string) => {
return new Promise((resolve, rejected) => {
const [cmd, ...args] = command.split(" ");
// 执行命令
const app = spawn(cmd, args, {
cwd: projectRoot,
stdio: "inherit", // 将子进程输出共享给父进程
shell: true
});
app.on("close", resolve);
});
};
+export const pathRewriter = (format: string) => {
+ return (id: string) => {
+ id = id.replaceAll("@yun", `yun/${format}`);
+ return id;
+ };
+};
build/component.ts
import path from "path";
import { series, parallel } from "gulp";
import { sync } from "fast-glob";
import vue from "rollup-plugin-vue";
import typescript from "rollup-plugin-typescript2";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import { OutputOptions, rollup } from "rollup";
import { componentRoot } from "./tools/paths";
import { buildConfig } from "./tools/config";
import { pathRewriter } from "./tools";
const buildEachComponent = async () => {
console.log("------------------buildEachComponent-------------------");
// 打包每个组件
const files = sync("*", {
cwd: componentRoot,
onlyDirectories: true
});
// 分别把 components 文件夹下的组件,放到 dist/es/components 下 和 dist/lib/compmonents
const builds = files.map(async (file: string) => {
// 每个组件的入口
const input = path.resolve(componentRoot, file, "index.ts");
const config = {
input,
plugins: [nodeResolve(), vue(), typescript(), commonjs()],
external: (id: string) => /^vue/.test(id) || /^@yun/.test(id) // 排除引入包那些不需要打包
};
const bundle = await rollup(config);
const options = Object.values(buildConfig).map(config => ({
format: config.format,
file: path.resolve(config.output.path, `components/${file}/index.js`),
paths: pathRewriter(config.output.name) // @yun => yun/es yun/lib
// 视情况,是否配置,否在会包报警告 Use `output.exports: "named"` to disable this warning
// name: "yun", // 全局的名字
// exports: "named" // 导出的名字用命名的方式导出 liraryTarget:"var" var name = "xxx"
}));
await Promise.all(
options.map(option => bundle.write(option as OutputOptions))
);
});
return Promise.all(builds);
};
export const buildComponent = series(buildEachComponent);
解析 .vue 文件中 指定 lang="ts" 的代码
安装
pnpm install ts-morph -D -w
生成 types 类型文件 .d.ts
build/component.ts
import path from "path";
import fs from "fs/promises";
import { series, parallel } from "gulp";
import glob, { sync } from "fast-glob";
import vue from "rollup-plugin-vue";
import typescript from "rollup-plugin-typescript2";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import { OutputOptions, rollup } from "rollup";
import { Project, SourceFile } from "ts-morph";
import * as VueCompiler from "@vue/compiler-sfc";
import { componentRoot, outDir, projectRoot } from "./tools/paths";
import { buildConfig } from "./tools/config";
import { pathRewriter, run } from "./tools";
const buildEachComponent = async () => {
console.log("------------------buildEachComponent---------------------");
// 打包每个组件
const files = sync("*", {
cwd: componentRoot,
onlyDirectories: true
});
// 分别把 components 文件夹下的组件,放到 dist/es/components 下 和 dist/lib/compmonents
const builds = files.map(async (file: string) => {
// 每个组件的入口
const input = path.resolve(componentRoot, file, "index.ts");
const config = {
input,
plugins: [nodeResolve(), vue(), typescript(), commonjs()],
external: (id: string) => /^vue/.test(id) || /^@yun/.test(id) // 排除引入包那些不需要打包
};
const bundle = await rollup(config);
const options = Object.values(buildConfig).map(config => ({
format: config.format,
file: path.resolve(config.output.path, `components/${file}/index.js`),
paths: pathRewriter(config.output.name), // @yun => yun/es yun/lib
// 视情况,是否配置,否在会包报警告 Use `output.exports: "named"` to disable this warning
name: "yun", // 全局的名字
exports: "named" // 导出的名字用命名的方式导出 liraryTarget:"var" var name = "xxx"
}));
await Promise.all(
options.map(option => bundle.write(option as OutputOptions))
);
});
return Promise.all(builds);
};
// 生成 ts 的 type 文件
+const genTypes = async () => {
+ console.log("----------------genTypes--------------------");
+
+ const project = new Project({
+ // 生成 .d.ts,需要有一个 tsconfig.json
+ compilerOptions: {
+ allowJs: true,
+ declaration: true,
+ emitDeclarationOnly: true,
+ noEmitOnError: true,
+ outDir: path.resolve(outDir, "types"),
+ baseUrl: projectRoot,
+ paths: {
+ "@yun/*": ["packages/*"]
+ },
+ skipLibCheck: true,
+ strict: false
+ },
+ tsConfigFilePath: path.resolve(projectRoot, "tsconfig.json"),
+ skipAddingFilesFromTsConfig: true
+ });
+
+ const filePaths = await glob("**/*", {
+ // ** 任意目录 * 任意文件
+ cwd: componentRoot,
+ onlyFiles: true,
+ absolute: true
+ });
+
+ const sourceFiles: SourceFile[] = [];
+
+ await Promise.all(
+ filePaths.map(async function (file) {
+ if (file.endsWith(".vue")) {
+ const content = await fs.readFile(file, "utf8");
+ const sfc = VueCompiler.parse(content);
+ const { script } = sfc.descriptor;
+
+ if (script) {
+ const content = script.content; // 拿到脚本 icon.vue.ts => icon.vue.d.ts
+ const sourceFile = project.createSourceFile(file + ".ts", content);
+ sourceFiles.push(sourceFile);
+ }
+ } else {
+ // 把所有的 ts 文件都放在一起发射成 .d.ts 文件
+ const sourceFile = project.addSourceFileAtPath(file);
+ sourceFiles.push(sourceFile);
+ }
+ })
+ );
+
+ await project.emit({
+ emitOnlyDtsFiles: true // 默认是放到内存中的
+ });
+
+ const tasks = sourceFiles.map(async (sourceFile: any) => {
+ const emitOutput = sourceFile.getEmitOutput();
+ const tasks = emitOutput.getOutputFiles().map(async (outputFile: any) => {
+ const filepath = outputFile.getFilePath();
+ await fs.mkdir(path.dirname(filepath), {
+ recursive: true
+ });
+ // @yun -> yun/es -> .d.ts 不用去 lib 下查找
+ await fs.writeFile(filepath, pathRewriter("es")(outputFile.getText()));
+ });
+ await Promise.all(tasks);
+ });
+
+ await Promise.all(tasks);
+};
export const buildComponent = series(
buildEachComponent,
+ genTypes
);
对应的组件生成对应的类型文件 .d.ts
build/component.ts
const copyTypes = () => {
console.log("----------------copyTypes--------------------");
const src = path.resolve(outDir, "types/components/");
const copy = (module: string) => {
const output = path.resolve(outDir, module, "components");
return () => run(`cp -r ${src}/* ${output}`);
};
return parallel(copy("es"), copy("lib"));
};
注意:还要保证每个组件的 index.js 的入口存在
// build/component.ts
const buildComponentEntry = async () => {
console.log("----------------buildComponentEntry--------------------");
const config = {
input: path.resolve(componentRoot, "index.ts"),
plugins: [typescript()],
external: () => true
};
const bundle = await rollup(config);
return Promise.all(
Object.values(buildConfig)
.map(config => ({
format: config.format,
file: path.resolve(config.output.path, "components/index.js")
}))
.map(config => bundle.write(config as OutputOptions))
);
};
打包组件库入口 yun 生成入口文件 index.js 和类型文件 .d.ts
build/full-component.ts 生成入口文件 index.js
const buildEntry = async () => {
console.log("-----------------buildEntry------------------------");
const entryFiles = await fs.readdir(yunRoot, { withFileTypes: true });
const entryPoints = entryFiles
.filter(f => f.isFile())
.filter(f => !["package.json"].includes(f.name))
.map(f => path.resolve(yunRoot, f.name));
const config = {
input: entryPoints,
plugins: [nodeResolve(), vue(), typescript()],
external: (id: string) => /^vue/.test(id) || /^@yun/.test(id)
};
const bundle = await rollup(config);
return Promise.all(
Object.values(buildConfig)
.map(config => ({
format: config.format,
dir: config.output.path,
paths: pathRewriter(config.output.name),
// 视情况,是否配置,否在会包报警告 Use `output.exports: "named"` to disable this warning
name: "yun", // 全局的名字
exports: "named" // 导出的名字用命名的方式导出 liraryTarget:"var" var name = "xxx"
}))
.map(option => bundle.write(option as OutputOptions))
);
};
build/entry-types.ts 生成 yun 入口文件的类型文件 .d.ts
import path from "path";
import fs from "fs/promises";
import glob from "fast-glob";
import { ModuleKind, Project, ScriptTarget, SourceFile } from "ts-morph";
import { outDir, projectRoot, yunRoot } from "./tools/paths";
import { parallel, series } from "gulp";
import { run, withTaskName } from "./tools";
import { buildConfig } from "./tools/config";
const genEntryTypes = async () => {
console.log("---------------genEntryTypes------------------------");
const files = await glob("*.ts", {
cwd: yunRoot,
absolute: true,
onlyFiles: true
});
const project = new Project({
compilerOptions: {
declaration: true,
module: ModuleKind.ESNext,
allowJs: true,
emitDeclarationOnly: true,
noEmitOnError: false,
outDir: path.resolve(outDir, "entry/types"),
target: ScriptTarget.ESNext,
rootDir: yunRoot,
strict: false
},
skipFileDependencyResolution: true,
tsConfigFilePath: path.resolve(projectRoot, "tsconfig.json"),
skipAddingFilesFromTsConfig: true
});
const sourceFiles: SourceFile[] = [];
files.map(f => {
const sourceFile = project.addSourceFileAtPath(f);
sourceFiles.push(sourceFile);
});
await project.emit({
emitOnlyDtsFiles: true
});
const tasks = sourceFiles.map(async sourceFile => {
const emitOutput = sourceFile.getEmitOutput();
for (const outputFile of emitOutput.getOutputFiles()) {
const filepath = outputFile.getFilePath();
await fs.mkdir(path.dirname(filepath), { recursive: true });
await fs.writeFile(
filepath,
outputFile.getText().replaceAll("@yun", "."),
"utf8"
);
}
});
await Promise.all(tasks);
};
const copyEntryTypes = () => {
const src = path.resolve(outDir, "entry/types");
const copy = (module: any) =>
parallel(
withTaskName(`copyEntryTypes:${module}`, async () => {
const p = path.resolve(
outDir,
(buildConfig as any)[module].output.path
);
return run(`cp -r ${src}/* ${p}/`);
})
);
return parallel(copy("esm"), copy("cjs"));
};
export const genTypes = series(genEntryTypes, copyEntryTypes());
build/gulpfile.ts
import { series, parallel } from "gulp";
+import { genEntryTypes } from "./entry-types";
import { run, withTaskName } from "./tools";
// 清除根目录下已经打包的 dist
const cleanDist = withTaskName("clean", async () => run("rm -rf ./dist"));
// 执行 packages 目录中 package.json 配置有 build 的对应命令脚本
const buildPackages = withTaskName("buildPackages", async () =>
// 注意这里 --filter ./packages,在 pnpm7.6.0 中是找不到 packages 下的项目的,有 bug
run("pnpm run --filter ./packages --parallel build")
);
// 处理 packages/components 的所有组件成一个文件
const buildAllComponent = withTaskName("buildFullComponent", async () =>
// 会去找 full-component.ts 中 buildFullComponent
run("pnpm run build buildFullComponent")
);
// 引用我们组件时,打包按需加载的多个文件组件
const buildSingleComponent = withTaskName("buildComponent", () =>
// 会去找 component.ts 中 buildComponent
run("pnpm run build buildComponent")
);
// buildFullComponent 函数在 full-component.ts 中
export * from "./full-component";
// buildComponent 函数在 component.ts 中
export * from "./component";
export default series(
cleanDist,
buildPackages,
buildAllComponent,
buildSingleComponent,
+ parallel(genEntryTypes)
)
拷贝 package.json
build/gulpfile.ts
import { series, parallel } from "gulp";
import { genTypes } from "./entry-types";
import { run, withTaskName } from "./tools";
+import { outDir, yunRoot } from "./tools/paths";
// 清除根目录下已经打包的 dist
const cleanDist = withTaskName("clean", async () => run("rm -rf ./dist"));
// 执行 packages 目录中 package.json 配置有 build 的对应命令脚本
const buildPackages = withTaskName("buildPackages", async () =>
// 注意这里 --filter ./packages,在 pnpm7.6.0 中是找不到 packages 下的项目的,有 bug
run("pnpm run --filter ./packages --parallel build")
);
// 处理 packages/components 的所有组件成一个文件
const buildAllComponent = withTaskName("buildFullComponent", async () =>
// 会去找 full-component.ts 中 buildFullComponent
run("pnpm run build buildFullComponent")
);
// 引用我们组件时,打包按需加载的多个文件组件
const buildSingleComponent = withTaskName("buildComponent", () =>
// 会去找 component.ts 中 buildComponent
run("pnpm run build buildComponent")
);
// buildFullComponent 函数在 full-component.ts 中
export * from "./full-component";
// buildComponent 函数在 component.ts 中
export * from "./component";
+// 拷贝 package.json
+const copySourceCode = () => async () => {
+ await run(`cp ${yunRoot}/package.json ${outDir}/package.json`);
+};
export default series(
cleanDist,
buildPackages,
buildAllComponent,
buildSingleComponent,
parallel(genTypes,
+ copySourceCode())
);