本地环境
- node:v20.10.0
- pnpm:v8.15.5
搭建 monorepo 环境
使⽤ pnpm 安装包速度快,磁盘空间利⽤率⾼效,使⽤ pnpm 可以快速建⽴ monorepo,so ~ 这⾥我们使⽤ pnpm workspace 来实现 monorepo
npm install pnpm -g # 全局安装pnpm
pnpm init # 初始化package.json配置⽂件 私有库
pnpm install vue typescript -D # 全局下添加依赖
提升 pnpm 下载的包
下载了 vue 和 ts 包,和这两个包相关的依赖包都在 .pnpm 中。要把依赖包提升。
.npmrc
shamefully-hoist = true
再执行 npm install,使用的依赖包就暴露在外部。
tsconfig.json
ts 配置文件初始化:pnpm tsc --init
{
"compilerOptions": {
"module": "ESNext", // 打包模块类型ESNext
"declaration": false, // 默认不要声明文件
"noImplicitAny": true, // 支持类型不标注可以默认any
"removeComments": true, // 删除注释
"moduleResolution": "node", // 按照node模块来解析
"esModuleInterop": true, // 支持es6,commonjs模块
"jsx": "preserve", // jsx 不转
"noLib": false, // 不处理类库
"target": "es6", // 遵循es6版本
"sourceMap": true,
"lib": [ // 编译时用的库
"ESNext",
"DOM"
],
"allowSyntheticDefaultImports": true, // 允许没有导出的模块中导入
"experimentalDecorators": true, // 装饰器语法
"forceConsistentCasingInFileNames": true, // 强制区分大小写
"resolveJsonModule": true, // 解析json模块
"strict": true, // 是否启动严格模式
"skipLibCheck": true // 跳过类库检测
},
"exclude": [ // 排除掉哪些类库
"node_modules",
"**/__tests__", // 单元测试
"dist/**"
]
}
创建 pnpm 工作空间
pnpm-workspace.yaml
packages:
- 'packages/**' # 组件相关
- docs # ⽂档
- play # 运行、测试组件
组件库目录
packages 目录下的文件都是一个单独的包,且每一个包都有自己的包名:
- components
- core
- hooks
- theme
- utils
在 packages 中的各个包要互相调用,所以要在最外层项目将 packages 中的包下载成依赖, 在 根目录 package.json 中添加如下内容
"dependencies": {
"@yujun-element/components": "workspace:^",
"@yujun-element/hooks": "workspace:^",
"@yujun-element/theme": "workspace:^",
"@yujun-element/utils": "workspace:^",
"typescript": "^5.2.2",
"vue": "^3.4.21",
"yujun-element": "workspace:^"
}
docs 文档
组件库文档利用 vitepress 创建
pnpm install vitepress -D # 在doc⽬录下安装
doc 目录下的 script 指令
"scripts": {
"dev": "vitepress dev",
"build": "vitepress build"
}
在根项⽬中增添启动命令
"scripts": {
"docs:dev": "pnpm -C docs dev",
"docs:build": "pnpm -C docs build"
}
文档发布在 github.io
.github/workflows/deploy.yml
name: Deploy VitePress site to Pages
on:
push:
branches:
- main
jobs:
test:
name: Run Lint and Test
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
- name: Install pnpm
run: npm install -g pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run tests
run: npm run test
build:
name: Build docs
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
- name: Install pnpm
run: npm install -g pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build docs
run: npm run docs:build
- name: Upload docs
uses: actions/upload-artifact@v3
with:
name: docs
path: ./docs/.vitepress/dist
deploy:
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
needs: build
steps:
- name: Download docs
uses: actions/download-artifact@v3
with:
name: docs
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GH_TOKEN }}
publish_dir: .
组件库文档发布到 github
- 申请 github token
- 创建 github action
-
查看 workflows
play 环境
components 中编写的组件在 play 中直接运行。
pnpm create vite play --template vue-ts
cd play
pnpm i
vite-env.d.ts 相当于给 .vue 文件的一个类型提示。我使用 vue 导出的方法时会给出提示。
/// <reference types="vite/client" />
// 为 vue 文件的类型说明
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
根目录启动 play 项目
- play/package.json
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview"
}
- 最外层 package.json
"scripts": {
"dev": "pnpm -C play dev"
}
pnpm -C是pnpm命令的一种用法,结合了-C选项。这个用法表示在执行pnpm命令时,将会先切换到指定的工作目录,然后在该目录下执行后续的pnpm命令。 例如,如果你运行pnpm -C path/to/project install,那么pnpm将会先切换到path/to/project目录,然后在这个目录下执行install命令,即安装项目的依赖。
BEM函数
// BEM 函数
const _bem = (prefixedName:string, blockSuffix:string,
element:string, modifier:string) => {
if (blockSuffix) {
prefixedName += `-${blockSuffix}`
}
if (element) {
prefixedName += `__${element}`
}
if (modifier) {
prefixedName += `--${modifier}`
}
return prefixedName
}
function createBEM(prefixedName: string) {
const b = (blockSuffix = '') => _bem(prefixedName, blockSuffix, '', '')
const e = (element = '') =>
element ? _bem(prefixedName, '', element,
'') : ''
const m = (modifier = '') =>
modifier ? _bem(prefixedName, '', '',
modifier) : ''
const be = (blockSuffix = '', element = '') =>
blockSuffix && element ? _bem(prefixedName,
blockSuffix, element, '') : ''
const em = (element:string, modifier:string) =>
element && modifier ? _bem(prefixedName, '',
element, modifier) : ''
const bm = (blockSuffix:string, modifier:string) =>
blockSuffix && modifier ? _bem(prefixedName,
blockSuffix, '', modifier) : ''
const bem = (blockSuffix:string, element:string, modifier:string) =>
blockSuffix && element && modifier
? _bem(prefixedName, blockSuffix, element,
modifier)
: ''
const is = (name:string, state:string) => (state ? `is-
${name}` : '')
return {
b,
e,
m,
be,
em,
bm,
bem,
is
}
}
export function createNamespace(name: string) {
const prefixedName = `z-${name}`
return createBEM(prefixedName)
}
BEM样式
- mixins/config.scss
$namespace: 'z';
$element-separator: '__';
$modifier-separator:'--';
$state-prefix:'is-';
- mixins/mixins.scss
@use 'config' as *;
@forward 'config';
// .z-button{}
@mixin b($block) {
$B: $namespace+'-'+$block;
.#{$B}{
@content;
}
}
// .z-button.is-desiabled
@mixin when($state) {
@at-root {
&.#{$state-prefix + $state} {
@content;
}
}
}
// &--primary => .z-button--primary
@mixin m($modifier) {
@at-root {
#{&+$modifier-separator+$modifier} {
@content;
}
}
}
// &__header => .z-button__header
@mixin e($element) {
@at-root {
#{&+$element-separator+$element} {
@content;
}
}
}
demo 组件创建
- 组件编写 components/Button/Button.vue
<script setup lang="ts">
defineOptions({
name:'Button'
})
</script>
<template>
<button>this is a button</button>
</template>
- 组件导出 components/Button/index.ts
import Button from './Button.vue'
import { withInstall } from '@yujun-element/utils' // 每一个组件都绑定一个注册方法 app.use(ElButton)
export const ElButton = withInstall(Button)
- withInstall 文件
import type { App, Plugin } from "vue";
type SFCWithInstall<T> = T & Plugin;
export const withInstall = <T>(component: T) => {
(component as SFCWithInstall<T>).install = (app: App) => {
const name = (component as any)?.name || "UnnamedComponent";
// 全局注册组件
app.component(name, component as SFCWithInstall<T>);
};
return component as SFCWithInstall<T>;
};
- 组件库核心包:packages/core/index.ts
import { makeInstaller } from '@yujun-element/utils'
import components from './components'
import '@yujun-element/theme/index.css'
const installer = makeInstaller(components)
export * from '@yujun-element/components'
export default installer
- makeInstaller 方法:app.use()所有组件
import type { App, Plugin } from "vue";
import { each } from "lodash-es";
type SFCWithInstall<T> = T & Plugin;
export function makeInstaller(components: Plugin[]) {
const install = (app: App) =>
each(components, (c) => {
// use 方法会调用每一个组件的 install 方法
app.use(c);
});
return install;
}
- 在 paly 中使用 packages/core 包
pnpm i yujun-element
play/src/main.ts
import YuJunElement from 'yujun-element'
const app = createApp(App)
app.use(YuJunElement)
app.mount('#app')
play/src/App.vue
<Button />