前言
现在前端开发越来越多的项目都采用 monorepo 的方式管理代码;比如 Vue3 和 ElementPlus 都是采用了pnpm的monorepo。monorepo可以在一个项目仓库中管理多个模块/包,共用基础配置,本地互相依赖的项目调试非常方便。
pnpm monorepo 搭建
安装pnpm
shell
npm install -g pnpm
新建一个文件夹,初始化package.json
shell
pnpm init
在根目录新建一个.npmrc
文件
ini
# 提升所有依赖到根node_modules目录下
shamefully-hoist = true
在根目录新建一个pnpm-workspace.yaml
文件,用来声明对应的工作区。
yaml
packages:
- play # 存放组件测试的代码
- docs # 存放组件的文档
- packages/* # 存放组件相关代码
- build # 存放打包相关的代码
packages目录下又有多个包:
- components:存放组件代码
- theme-chalk:存放公共样式和组件样式
- utils:存放一些工具方法
- constants:存放一些常量
每个包都有自己的package.json,比如components目录下:
json
{
"name": "@storm/components",
"version": "1.0.0",
"description": "all components",
"main": "index.ts",
"module": "index.ts"
}
其他包分别为:@storm/constants
,@storm/theme-chalk
和 @storm/utils
。
初步的目录结构为:
js
|------ build
|------ docs
|------ packages
| |------ components
| | └── package.json
| |------ constants
| | └── package.json
| |------ theme-chalk
| | └── package.json
| |------ utils
| └── package.json
|------ play
|------ .npmrc
|------ package.json
|------ pnpm-workspace.yaml
|------ README.md
然后在根目录执行:
shell
pnpm install @storm/components -w
pnpm install @storm/constants -w
pnpm install @storm/theme-chalk -w
pnpm install @storm/utils -w
-w
表示安装到根目录下的 package.json 中,这样方便开发的时候几个包互相进行调用,安装后 package.json 中的内容:
json
"dependencies": {
"@storm/components": "workspace:*",
"@storm/constants": "workspace:*",
"@storm/theme-chalk": "workspace:*",
"@storm/utils": "workspace:*"
}
TypeScript 初始化配置
先安装一下开发时所需要的依赖:
shell
pnpm install vue typescript @types/node -D -w
接下来初始化一下ts的配置,在根目录新建 tsconfig.json
:
json
{
"compilerOptions": {
"module": "ESNext", // 指定生成什么模块代码
"declaration": false, // 默认不生成声明文件
"noImplicitAny": true, // 类型不标注可以默认any
"removeComments": false, // 是否删除注释
"moduleResolution": "node", // 按照node模块来解析
"esModuleInterop": true, // 简化对导入CommonJS模块的支持
"jsx": "preserve", // 不转换jsx
"target": "ES6", // 遵循es6版本
"sourceMap": true,
"lib": [ // 获得ECMAScript和dom的ts声明
"ESNext",
"DOM"
],
"allowSyntheticDefaultImports": true, // 允许使用默认导入没有默认导出的模块
"experimentalDecorators": true, // 装饰器语法
"forceConsistentCasingInFileNames": true, // 强制区分大小写
"resolveJsonModule": true, // 解析json模块
"strict": true, // 严格模式
"skipLibCheck": true, // 跳过对所有.d.ts文件的类型检查
},
"exclude": [ // 排除的目录和文件
"node_modules",
"dist/**"
]
}
Play环境
之后编写的组件都会放在 play
中运行和调试,所以需要在play目录下创建一个开发环境,在根目录执行:
shell
pnpm create vite play --tempate vue-ts
// 进入play目录执行
pnpm install
// 启动项目
pnpm run dev
如果希望在根目录运行play项目,可以在根目录的 package.json 的 scripts 中配置:
json
"scripts": {
"dev": "pnpm -C play dev"
}
BEM的使用
BEM 规范下的命名格式:
- 模块和模块之间使用
-
连接 - Block 与 Element 之间使用
__
连接 - Block/Element 与 Mofifier 之间使用
--
连接
比如 ElementPlus el-switch
组件的结构:
html
<div class="el-switch is-checked">
<input class="el-switch__input" type="checkbox">
<span class="el-switch__core">
<div class="el-switch__action"></div>
</span>
</div>
通过JS生成BEM规范
在utils目录下新建一个 create.ts
文件:
ts
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) {
// 创建模块 el-switch
const b = (blockSuffix: string = '') => _bem(prefixName, blockSuffix, '', '')
// 创建元素 el-switch__input
const e = (element: string = '') => element ? _bem(prefixName, '', element, '') : ''
// 创建块修饰 el-switch--large
const m = (modifier: string = '') => modifier ? _bem(prefixName, '', '', modifier) : ''
// 创建元素块 el-switch-on-color
const be = (blockSuffix: string = '', element: string = '') =>
blockSuffix && element ? _bem(prefixName, blockSuffix, element, '') : ''
// 创建块修饰 el-switch-on-color--large
const bm = (blockSuffix: string = '', modifier: string = '') =>
blockSuffix && modifier ? _bem(prefixName, blockSuffix, '', modifier) : ''
// 创建元素修饰 el-switch__input--large
const em = (element: string = '', modifier: string = '') =>
element && modifier ? _bem(prefixName, '', element, modifier) : ''
// 创建块元素修饰 el-switch-on-color__input--large
const bem = (blockSuffix: string = '', element: string = '', modifier: string = '') =>
blockSuffix && element && modifier ? _bem(prefixName, blockSuffix, element, modifier) : ''
// 创建状态 is-success
const is = (name: string, state?: string | boolean) => {
return name && (state ?? true) ? `is-${name}` : ''
}
return {
b,
e,
m,
be,
bm,
em,
bem,
is
}
}
// 用于创建BEM命名空间
export function createNamespace(name: string) {
// 默认命名前缀
const prefixName = `s-${name}`
return createBEM(prefixName)
}
在utils目录下新建一个 index.ts
文件作为工具库统一导出的入口:
ts
export * from './create'
然后在页面中使用:
html
<template>
<!-- 会生成s-name的类名 -->
<div :class="bem.b()"></div>
</template>
<script setup lang="ts">
import { createNamespace } from '@storm/utils'
const bem = createNamespace('name')
</script>
通过scss生成BEM样式规范
在 theme-chalk
目录下新建src目录,然后在src目录下新建mixins目录,接着在mixins目录下新建2个文件:config.scss和mixins.scss。
scss
// config.scss
$namespace: "s";
$element-separator: "__";
$modifier-separator: "--";
$state-prefix: "is-";
scss
// mixins.scss
@use "config" as *; // @import可能有多次引用的问题
@forward "config";
// .s-button
@mixin b($block) {
$B: $namespace + "-" + $block;
.#{$B} {
// @include后 样式会替换@content
@content;
}
}
// s-button.is-xxx
@mixin when($state) {
// 输出到全局作用域
@at-root {
&.#{$state-prefix + $state} {
@content;
}
}
}
// s-button__header
@mixin e($element) {
@at-root {
#{& + $element-separator + $element} {
@content;
}
}
}
// s-button--primary
@mixin m($modifier) {
@at-root {
#{& + $modifier-separator + $modifier} {
@content;
}
}
}
组件的注册
因为后续会有很多组件,每个组件注册都需要添加一个install方法,所以在 packages/utils/with-install.ts
封装一个公共的方法:
ts
// 每个组件既可以通过app.use来使用 也可以通过import来使用
import { Plugin } from "vue"
type SFCWithInstall<T> = T & Plugin
export const withInstall = <T, E extends Record<string, any>>(
main: T, extra?: E
) => {
(main as SFCWithInstall<T>).install = (app): void => {
// 比如注册checkbox组件同时会注册checkbox-group组件
const comps = [main, ...Object.values(extra ?? {})]
for (const comp of comps) {
app.component(comp.name, comp)
}
}
return main as SFCWithInstall<T> & E
}
实践一下
写一个简单的Icon组件实践一下BEM规范,需要先安装一下sass,在根目录执行:
shell
pnpm install sass -D -w
在packages目录下的components目录新建一个icon目录,创建一下目录结构:
js
|------ packages
| |------ components
| | |------ icon
| | | |------ src # 组件入口目录
| | | | |------ icon.vue # 组件代码
| | | | └── icon.ts # Ts类型和组件属性
| | | └── index.ts # 组件入口文件
| | └── package.json
ExtractPropTypes
可以把构造函数类型转换成对应的类型,StringConstructor => string。PropType
可以对类型进行更加详细的申明。
ts
// icon.ts
import { PropType, ExtractPropTypes } from "vue";
export const iconProps = {
color: String,
size: [Number, String] as PropType<number | string>
}
// ExtractPropTypes可以将props类型抽取出来给外部使用
export type IconProps = ExtractPropTypes<typeof iconProps>
vue3版本大于3.3,内部已经实现defineOptions方法。
html
// icon.vue
<template>
<i
:class="bem.b()"
:style="style"
>
<slot />
</i>
</template>
<script setup lang="ts">
import { createNamespace } from '@storm/utils'
import { iconProps } from './icon'
import { computed } from 'vue'
defineOptions({ name: 'SIcon' })
const props = defineProps(iconProps)
const bem = createNamespace('icon')
const style = computed(() => {
if (!props.size && !props.color) return {}
return {
...(props.size ? { 'font-size': props.size + 'px' } : {}),
...(props.color ? { 'color': props.color } : {})
}
})
</script>
ts
// index.ts
import { withInstall } from '@storm/utils'
import _Icon from './src/icon.vue'
// 添加install方法
export const Icon = withInstall(_Icon)
export default Icon
export * from './src/icon'
在theme-chalk目录下新建一个icon.scss文件,然后新建一个index.scss作为所有样式文件导出的入口:
scss
// icon.scss
@use "./mixins/mixins.scss" as *;
// 供别的组件loading状态使用
@keyframes rotating {
0% {
transform: rotateZ(0deg);
}
100% {
transform: rotateZ(360deg);
}
}
@include b(icon) {
display: inline-flex;
width: 1em;
height: 1em;
vertical-align: middle;
@include when(loading) {
animation: rotating 2s linear infinite;
}
}
scss
// index.scss
@use "./icon.scss";
然后在play目录的main.ts下引入组件和样式:
ts
import { createApp } from 'vue'
import App from './App.vue'
import Icon from '@storm/components/icon'
import '@storm/theme-chalk/src/index.scss'
const plugins = [
Icon
]
const app = createApp(App)
plugins.forEach(plugin => app.use(plugin))
app.mount('#app')
看下运行效果: