主要内容
- pnpm使用
- ci搭建 eslint + prettier + husky + commitlint + stylelint
- 单元测试 vitest
- 搭建文档 vitepress
- 文档部署 GitHub
- 项目打包 tsup
- 测试路径搭建 vite+antd-design-pro
- npm发布 changesets
- npm自动部署 GitHub workflow
pnpm使用
- pnpm安装:
// Windows
iwr https://get.pnpm.io/install.ps1 -useb | iex
// mac
brew install pnpm
// 通用
npm install -g pnpm
- 初始化:
pnpm init
npx tsc --init
- pnpm-workspace.yaml:
// pnpm包路径
packages:
- packages/*
- playground
- docs
- pnpm workspace 指南:pnpm官网
// 安装公共依赖
pnpm i typescript -w
// 安装开发依赖
pnpm i typescript -Dw
// 安装指定依赖
pnpm add <package_name> --filter <package_selector>
// 运行单个包的scripts脚本
pnpm dev --filter <package_selector>
// 各个 packages/* 模块包间的相互依赖
pnpm install <package_selector1> -r --filter <package_selector2>
// 但是安装后的包会带上具体版本 所以需要我们手动更改"package_selector1": "workspace:^1.0.0"
"package_selector1": "workspace:*",
ci搭建
-
配置 eslint
-
安装
-
pnpm i eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin -Dw -
新建
.eslintrc(eslint配置) 和.eslintignore(eslint检查忽略文件) eslint官网- 需要自定义插件可以使用yo eslint:plugin开发,命名需要以eslint-plugin开头
- eslint和prettier配置可能出现冲突,一般会使用eslint-plugin-prettier解决冲突
-
pnpm i eslint-plugin-prettier -Dw
-
// eslint配置 可以根据自己需要添加规则 配置eslint插件时需要注意插件支持的node版本号 { "root": true, "env": { "browser": true, "es2021": true, "es6": true, "node": true }, "extends": ["prettier"], "parser": "@typescript-eslint/parser", "parserOptions": { "sourceType": "module", "ecmaVersion": 12, "ecmaFeatures": { "jsx": true, "tsx": true } }, "plugins": ["@typescript-eslint"], "ignorePatterns": ["*.md", "!.vitepress/**"], "rules": { "no-console": "warn", "no-debugger": "warn" } } -
在
package.json中script添加脚本 -
"lint:js": "eslint . --ext .js --ext .ts --ext .jsx --ext .tsx --no-fix --cache", "lint:js:fix": "eslint . --ext .js --ext .ts --ext .jsx --ext .tsx --fix",
-
-
配置 prettier:
- 安装
-
pnpm i prettier eslint-config-prettier eslint-plugin-prettier -Dw - 新建 .prettierrc(非必须,可不填)
-
{ "bracketSpacing": true, "jsxBracketSameLine": true, "jsxSingleQuote": false, "printWidth": 140, "semi": false, "useTabs": false, "singleQuote": true, "tabWidth": 2, "endOfLine": "auto", "trailingComma": "none" } - 在
package.json中script添加脚本 -
"lint:format": "prettier -c ./ --cache .", "lint:format:fix": "prettier -w ./",
-
配置 stylelint
- 安装
-
pnpm i stylelint stylelint-config-recess-order stylelint-config-standard stylelint-scss -Dw - 新建 .stylelintrc.json
-
{ "extends": ["stylelint-config-standard", "stylelint-config-recess-order"], "plugins": ["stylelint-scss"], "rules": { "selector-pseudo-class-no-unknown": null, "at-rule-no-unknown": null } } - 在
package.json中script添加脚本 -
"lint:style": "stylelint **/*.{css,less,scss} --cache", "lint:style:fix": "stylelint **/*.{css,less,scss} --fix",
-
配置 type-check:
- 安装
-
pnpm i type-check -Dw
-
配置 husky:
-
安装
-
pnpm i husky lint-staged -Dw -
在
package.json中script添加脚本 提交时可以自动修复代码 -
"script":{ "prepare": "husky", }, "lint-staged": { "*.{vue,js,ts,jsx,tsx,json}": [ "eslint --fix", "prettier --write", "bash -c 'npm run type-check'" ], "*.{scss,css,less}": [ "stylelint --fix" ] }, -
初始化
husky(按顺序运行以下命令) -
# 按顺序运行以下命令 npx husky install npx husky init -
编辑pre-commit文件
```js npx --no-install lint-staged ```
-
-
配置 commitlint:
- 安装
-
pnpm i @commitlint/config-conventional @commitlint/cli -Dw - 创建 commitlint.config.ts
-
module.exports = { extends: ['@commitlint/config-conventional'], }; - 新增.husky/commit-msg文件,并且输入
-
npx --no -- commitlint --edit "$1"
单元测试
- 安装
pnpm i vitest -Dw
- 新建 vitest.config.ts
import { resolve } from 'path'
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true
},
resolve: {
alias: {
'@yu-kit/utils': resolve(__dirname, 'packages/utils/index.ts'),
'@yu-kit/kit': resolve(__dirname, 'packages/kit/index.ts'),
'@yu-kit/components': resolve(__dirname, 'packages/components/index.ts'),
'@yu-kit/hooks': resolve(__dirname, 'packages/hooks/index.ts')
}
}
})
- 配置运行脚本:package.json
"script": {
...
"test": "vitest test", // 执行测试
"coverage": "vitest run --coverage" // 执行测试覆盖率,需要安装 @vitest/coverage-c8
...
}
搭建文档
这里选择的是vitepress,后续会采用dumi
- 安装
pnpm i vitepress -Dw
- 配置运行脚本:
package.json
"script": {
...
"docs:dev": "vitepress dev packages",
"docs:build": "vitepress build packages",
...
}
- 基本配置:新建
packages/index.md文件 【主页配置】
---
layout: home
sidebar: false
title: yu-kit
titleTemplate: 前端工具库
hero:
name: yu-kit
text: 前端工具库
tagline: 🎉 前端工具库
actions:
- theme: brand
text: 快速开始
link: /guide/
- theme: brand
text: 组件文档
link: /components/Button/
- theme: brand
text: hooks
link: /hooks/useCallback/
- theme: brand
text: 工具类
link: /kits/ElementHandler/
- theme: brand
text: 实用函数
link: /utils/interval/
---
- 基本配置:新建
packages/.vitepress/config.ts文件:【文件配置】
const Guide = [{ text: '快速开始', link: '/guide/' }]
const components = [
{ text: '按钮组件', link: '/components/Button/' },
{ text: '轻提示组件', link: '/components/Toast/' }
]
const hooks = [
{ text: 'useCallback', link: '/hooks/useCallback/' },
{ text: 'useStates', link: '/hooks/useStates/' },
{ text: 'useLoading', link: '/hooks/useLoading/' },
{ text: 'usePrev', link: '/hooks/usePrev/' },
{ text: 'useMemo', link: '/hooks/useMemo/' }
]
const kits = [{ text: '组件处理器', link: '/kits/ElementHandler/' }]
const functions = [
{ text: 'interval', link: '/utils/interval/' },
{ text: 'copyToClipboard', link: '/utils/copyToClipboard/' },
{ text: 'cleanFileUrl', link: '/utils/cleanFileUrl/' },
{ text: 'downloadFile', link: '/utils/downloadFile/' },
{ text: 'delNulOp', link: '/utils/delNulOp/' }
]
const DefaultSideBar = [
{ text: '指南', items: Guide },
{ text: '组件文档', items: components },
{ text: 'hooks', items: hooks },
{ text: '工具类', items: kits },
{ text: '实用函数', items: functions }
]
export default {
base: '/yu-kit/',
title: 'yu-kit',
lang: 'zh-CN',
themeConfig: {
logo: '/logo.png',
lastUpdated: true,
lastUpdatedText: '最后修改时间',
socialLinks: [{ icon: 'github', link: 'https://github.com/encodedecod/yu-kit/' }],
nav: Guide,
// 侧边栏
sidebar: DefaultSideBar
}
}
- 基本配置:新建
packages/.vitepress/theme/index.ts文件:【主题】
import DefaultTheme from 'vitepress/theme'
export default {
...DefaultTheme
}
- vitepress的一些基础使用 文档参考 vitepress
// 表格
| 名称 | 类型 | 描述 | 默认值 | 是否必填 |
| --------- | :-------------: | -------------------: | -----: | -------: |
| title | React.ReactNode | 轻提示内容 | | 是 |
| visible | boolean | 是否显示 | false | |
| mask | boolean | 是否有遮罩层 | false | |
| duration | number | 轻提示持续显示的时间 | 1500 | |
| className | string | className 扩展 | | |
// 主题
---title: Blogging Like a Hacker
lang: en-US
---
// js代码
```js
console.log('Hello, VuePress!')
```
- 运行
pnpm docs:dev
文档部署
-
生成 GitHub 的 Secerts
- 进入 GitHub 设置页面生成一个密钥: github.com/settings/to…
- 设置当前仓库的 Secerts
- 设置page
-
创建
.github/workflows/docs-deploy.yml文件 下面的GITHUB_TOKEN: ${{ secrets.H_DEVKIT }}的H_DEVKIT为设置的
name: docs-deploy
on: # 触发条件
# 每当 push 到 main 分支时触发部署
push:
branches: [main]
jobs:
docs:
runs-on: ubuntu-latest # 指定运行所需要的虚拟机环境(必填)
steps:
- uses: actions/checkout@v3
with:
# “最近更新时间” 等 git 日志相关信息,需要拉取全部提交记录
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 7
- name: Setup Node.js
uses: actions/setup-node@v2
with:
# 选择要使用的 node 版本
node-version: '16'
cache: 'pnpm'
# 如果缓存没有命中,安装依赖
- name: Install dependencies
run: pnpm install --no-frozen-lockfile --ignore-scripts
# 运行构建脚本
- name: Build vitepress site
run: pnpm docs:build
env:
DOC_ENV: preview
NODE_OPTIONS: --max-old-space-size=4096
# 查看 workflow 的文档来获取更多信息
# @see https://github.com/crazy-max/ghaction-github-pages
- name: Deploy to GitHub Pages
uses: crazy-max/ghaction-github-pages@v3
# 环境变量
env:
GITHUB_TOKEN: ${{ secrets.H_DEVKIT }}
with:
# 部署到 gh-pages 分支
target_branch: gh-pages
# 部署目录为 vitepress 的默认输出目录
build_dir: docs/.vitepress/dist
项目打包
什么是 esm、cjs、iife 格式
-
esm格式:ECMAScript Module,现在使用的模块方案,使用importexport来管理依赖; -
cjs格式:CommonJS,只能在 NodeJS 上运行,使用require("module")读取并加载模块; -
iife格式:通过<script>标签引入的自执行函数; -
安装
pnpm add tsup -Dw
-
在根目录下配置文件:
tsup.config.ts****tsup配置文档- 实现了packpage下的文件夹多包自动打包
- 针对esbuild不引入css模块做了引入处理
import { defineConfig, Format, Options } from 'tsup'
import fg from 'fast-glob'
import { sassPlugin } from 'esbuild-sass-plugin'
import fs from 'fs'
import postcss from 'postcss'
import autoprefixer from 'autoprefixer'
const baseConfigs = [
{
dts: true, // 添加 .d.ts 文件
metafile: false, // 添加 meta 文件
minify: true, // 压缩
splitting: false,
sourcemap: false, // 添加 sourcemap 文件
clean: true, // 是否先清除打包的目录,例如 dist
format: ['cjs'] as Format[]
},
{
dts: true, // 添加 .d.ts 文件
metafile: false, // 添加 meta 文件
minify: true, // 压缩
splitting: false,
sourcemap: false, // 添加 sourcemap 文件
clean: true, // 是否先清除打包的目录,例如 dist
format: ['esm'] as Format[]
}
]
const filePaths: { text: string; path: string }[] = []
const hasHandlePath: string[] = []
const myReadfile = () => {
const entries = fg.sync([`./packages/**/index.ts`, `./packages/**/index.tsx`], {
onlyFiles: false,
deep: Infinity,
ignore: [`**/cli/**`, `**/node_modules/**`, `**/*.test.ts`]
})
const configs: Options[] = []
baseConfigs.forEach((baseConfig) =>
entries.forEach((file) => {
const outDir = file.replace(/(packages/)(.*?)//, `./packages/$2/cli/${baseConfig.format[0]}/`).replace(//index.(ts|tsx)$/, '')
configs.push({
target: ['esnext'],
entry: [file],
outDir: outDir,
loader: {
'.js': 'jsx',
'.jsx': 'jsx',
'.scss': 'css',
'.sass': 'css',
'.less': 'css',
'.css': 'css',
'.tsx': 'tsx'
},
...baseConfig,
esbuildPlugins: [
sassPlugin({
async transform(source) {
const { css } = await postcss([autoprefixer]).process(source)
return css
}
}),
{
name: 'scss-plugin',
setup: (build) => {
build.onEnd((result) => {
result.outputFiles?.forEach((item) => {
if (
/index.(mjs|js)$/.test(item.path) &&
result.outputFiles?.find((outputItem) => outputItem.path === item.path.replace(/(.js|.mjs)$/, '.css'))
) {
filePaths.push({ text: item.text, path: item.path })
}
})
})
}
}
],
onSuccess: async () => {
filePaths.forEach((item) => {
if (!hasHandlePath.find((val) => val === item.path)) {
fs.access(item.path, (err) => {
if (!err) {
let data = item.text
data = `import "./index.css"; ${data}`
fs.writeFile(
item.path,
`import "./index.css"; ${item.text}`,
{
encoding: 'utf-8'
},
(fileError) => {
if (!fileError) {
hasHandlePath.push(item.path)
}
}
)
}
})
}
})
}
})
})
)
return defineConfig(configs)
}
export default myReadfile()
- 在
package.json下添加脚本
"scripts": {
"dev": "tsup --watch",
"build": "tsup"
},
- 运行效果
测试路径搭建
- 根目录新建test文件夹, pnpm-workspace.yaml新增test
packages:
- packages/*
- playground
- docs
- test
- 在test执行
pnpm init
npx tsc --init
- 根路径执行下面命令
pnpm add antd react-dom react-router-dom @ant-design/icons @ant-design/pro-components -D --filter test
pnpm add @types/react @types/react-dom vite @vitejs/plugin-react -Dw --filter test
- 在新建test/index.html文件
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>yu-kit-test</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
<script>
window.global = window
</script>
</body>
</html>
- test/src/index.tsx
import { createRoot } from 'react-dom/client'
import App from './App'
import React from 'react'
import { BrowserRouter } from 'react-router-dom'
const root = document.getElementById('root')
if (root) {
createRoot(root).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
)
}
- test/src/App.tsx
import { Route, Routes } from 'react-router-dom'
import Home from '@/pages/home'
import './App.scss'
const App = () => {
return (
<div className="App">
<Routes>
<Route path="/*" element={<Home />} />
</Routes>
</div>
)
}
export default App
- test/src/index.tsx(主文件目录)
import { createRoot } from 'react-dom/client'
import App from './App'
import React from 'react'
import { BrowserRouter } from 'react-router-dom'
const root = document.getElementById('root')
if (root) {
createRoot(root).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
)
}
- test/src/App.tsx(路由总目录)
import { Route, Routes } from 'react-router-dom'
import Home from '@/pages/home'
import './App.scss'
const App = () => {
return (
<div className="App">
<Routes>
<Route path="/*" element={<Home />} />
</Routes>
</div>
)
}
export default App
- test/vite.config.ts
import react from '@vitejs/plugin-react'
import path from 'path'
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
base: '/',
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
})
- 在 test/
package.json中script添加脚本
"scripts": {
"dev": "vite",
"build": "tsc && vite build"
},
- 在
package.json中script添加脚本
"test:dev": "pnpm --filter @yu-kit/test dev",
"test:build": "pnpm --filter @yu-kit/test build"
- 运行pnpm test:dev效果
npm发布
- 登录 npm(按照提示输入用户名密码 邮箱 即可,未注册需在 www.npmjs.com/ 注册)
npm login
-
package.json配置
- 如果发布的
npm包名为:@xxx/yyy格式,需要先在npm注册名为:xxx 的organization,否则会出现提交不成功;如@yu-kit/components组件需要注册名为yu-kit的organization - 发布到
npm group时默认为private,所以我们需要手动在每个packages子包中的package.json中添加如下配置 (为private时不支持发布,对于不需要打包的包文件可以改为private) -
"publishConfig": { "access": "public" }, - 添加packages子包导出路径
-
"main": "./cli/cjs/index.js", "module": "./cli/esm/index.mjs", "types": "./cli/cjs/index.d.ts", "exports": { ".": { "require": "./cli/cjs/index.js", "import": "./cli/esm/index.mjs", "types": "./cli/cjs/index.d.ts" }, "./*": "./*" }, - 设置npm打包导出的具体包
-
"files": [ "cli" ],
- 如果发布的
-
由于项目存在多个包文件,需安装changesets
pnpm i @changesets/cli -Dw
- 初始化
changesets
pnpm changeset init
配置 package.json 的发布脚本
{
"script": {
"release": "changeset publish",
}
}
- 发布效果
npm自动部署
- 配置NPM Token
- 设置当前仓库的 Secerts
- 配置好后再gitflow下加 (NPM_PUBLISH为配置的秘钥名)
- name: NPM BUILD KIT # 打包工具的cli包
run: pnpm build
env:
DOC_ENV: preview
- name: Publish to NPM # 推送到 NPM 上
run: |
pnpm config set //registry.npmjs.org/:_authToken=$NPM_PUBLISH
pnpm release
env:
NPM_PUBLISH: ${{ secrets.NPM_PUBLISH }}
- 打包效果
后续:对于公司级项目,需要自己搭建npm私有域 一般借助verdaccio 需要对应的服务器资源
参考文档
vscode eslint prettier stylelint typescript 配置