前言
组件库一直是前端开发不可或缺的部分,基于组件库我们可以为开发提效,甚至实现对某一特定业务场景设计语言的统一。
搭建过程中涉及很多知识,再看看社区开源成熟的组件库设计,自己也有了更深刻的感悟,故记录下来与大家一同分享,
文章较干较长,建议先 “收藏”。~ 文末 附源码免费 获取方式
准备
涉及要点:
-
Webpack
-
Gulp
-
Rollup
-
Babel
-
Typescript
-
Less
-
Markdown 文档组件渲染
-
单组件、文档双开发模式
-
CI/CD
-
Unit Test 单元测试
-
国际化支持
-
主题定制
预览
我们先来预览一下我们的成果
目标
我们需要先明确我们这个组件库需要达成哪些要点:
针对以上要点,我们就可以开始我们组件库的架构设计工作啦:
初始化搭建
npm init
# 初始化,根据提示输入
npm init
目录、文件创建
components
解析:
- _constant 常量
- _hooks 通用vue hook
- _locale 多语言
- button 组件
-
- __tests__单元测试
- demos 组件特性demo
- locale 组件多语言配置
- style 组件样式
-
- *.less 样式文件
- index.ts 样式入口
-
- Button.tsx
- buttonTypes.ts 组件类型
- index.ts 组件入口
- README.zh-CN.md 中文文档
- README.en-US.md 英文文档
tsconfig.json
{
"compilerOptions": {
"rootDir":"./",
"baseUrl": "./",
"paths": {
"k-view-next": [
"components/index.ts"
],
"k-view-next/lib/*": [
"components/*"
],
"k-view-next/es/*": [
"components/*"
]
},
"target": "es6",
"strict": true,
"jsx": "preserve",
"module": "esnext",
"moduleResolution": "node",
"noUnusedParameters": true,
"noUnusedLocals": true,
"noImplicitAny": false,
"skipLibCheck": true,
"allowJs": true,
"importsNotUsedAsValues": "preserve"
},
"exclude": [
"node_modules",
"build",
"lib",
"es",
"dist",
"scripts",
"**/__tests__/**/*"
]
}
build
-
md-loader Markdown预览loader
-
getBabelConf 获取Babel配置
-
getTsConf 获取 typescript 配置
-
injectEnv 环境变量注入
-
server.dev 单组件开发调试
-
server.prod 文档预览调试
-
server.prod.build 文档预览构建
-
webpack.base 基础webpack编译配置
-
webpack.dev 单组件调试配置
-
webpack.prod 文档站点调试配置
md-loader
这里我们简单介绍一下md-loader
- md to vue 解析流程:
开发模式
组件调试
组件调试,我们仅需要编译组件相关的文件资源:
- 组件源码
- 组件 README
- 组件特性 demo
脚本
yarn dev [components/[组件]]
路由注册
- 遍历
components/[组件]/demos/*.vue文件, 注册路由 - 解析 README.md 中的
<code src="引用相对路径地址"> - 注册路由子视图
- 相对路径引用 代码转换
引用相对路径地址 => 源码文本 + [import XXX from 引用相对路径地址]
源码实现
package.json script
node script
webpack.dev.js
路由注册
通过 process.env.DemoPath 做路由注入策略
实现效果
可能部分小伙伴注意到了 【order: 0 xxxxx 】这部分没有渲染处理,这里可以使用 [npm] gray-matter 这个包来做解析,暂略。🐶
文档站点
与组件调试模式不同的是:
-
文档站点模式直接以 components.json 为数据源来注册路由
-
入口文件更改为文档站点入口 site/pages/prod/main.js
路由注册
这里我们看下核心的路由注册部分site/pages/prod/router/index.js :
import Home from '../views/Home.vue';
import compJson from '../../../../components.json';
/**
* @description 路由获取设置
* @param {Object} dataSource 数据源
* @param {String} sep 分隔符
* @returns {Array<Route>} routes
*/
const getComponentsRoutes = (dataSource, sep = '/', routeBase='Components', initRoutes = []) => {
const dpFn = (data, parentKey = '') =>
Object.keys(data).reduce((prev, key) => {
const nodeKey = `${parentKey ? parentKey + sep : ''}${key}`;
const { label, icon, children } = data[key];
const item = {
label,
icon,
name: key,
key: nodeKey,
path: `/${routeBase}/${nodeKey}`,
};
if (!children) {
item.component = ()=>import(`../../../../components/${key}/README.zh-CN.md`);
prev.push(item);
}else {
prev.push(...dpFn(children, key));
}
return prev;
}, [...initRoutes]);
return dpFn(dataSource);
};
export const navMenuDataSource = {
Documents: {
label: '文档',
},
Components: {
label: '组件',
},
CHANGELOG: {
label: '更新日志',
link: 'https://gitlab.dianchu.cc/dc_platform/dc_website/front_utils/dc-vizier-ui-next/-/releases',
},
Gitlab: {
label: 'Gitlab',
icon: 'gitlab',
link: 'https://gitlab.dianchu.cc/dc_platform/dc_website/front_utils/dc-vizier-ui-next',
},
Typescript: {
label: 'Typescript',
icon: 'Typescript',
link: `${process.env.BASE_URL}ppt/typescript.html`,
}
};
export const documentsDataSource = {
Production: {
label: '简介',
},
Contribute: {
label: '贡献指南',
},
FAQ: {
label: 'FAQ',
}
};
export const menuDataSource = (()=>{
return {
Architecture: {
label: '架构设计',
icon: 'sketch',
},
...compJson,
};
})();
/**
* @description 基础路由
*/
export const routes = [
{
name: 'Components',
path: '/Components',
redirect: '/Components/Architecture',
component: ()=> import('../views/Components.vue'),
children: [
{
path: '/Components/Architecture',
name: 'Architecture',
component: ()=> import('../../../../docs/Architecture.md'),
},
].concat(
getComponentsRoutes(compJson),
),
},
{
path:'/Home',
name: 'Home',
component: Home,
},
{
name: 'Documents',
path: '/Documents',
redirect: '/Documents/Production',
component: ()=> import('../views/Documents.vue'),
children: [
{
path: '/Documents/Production',
name: 'Production',
component: ()=> import('../../../../README.md'),
},
{
path: '/Documents/Contribute',
name: 'Contribute',
component: ()=> import('../../../../docs/Contribute.md'),
},
{
path: '/Documents/FAQ',
name: 'FAQ',
component: ()=> import('../../../../docs/FAQ.md'),
}
],
},
{
path: '/CHANGELOG',
name: 'CHANGELOG',
component: ()=> import('../views/CHANGELOG.vue'),
},
{
path:'/',
redirect: '/Home',
},
];
console.log(routes);
实现效果
快速创建
通过前面两个开发步骤的实现,我们做个规范+提效的组件快速创建脚本, 核心原理是:
通过 inquirer 获取用户快速创建组件的配置字段,然后读取配置,替换模板 tpl 文件中的预设占位符来新建我们需要的组件文件
tpl 资源文件准备
genComp.js 脚本文件
const path = require('path')
const fsEx = require('fs-extra')
const inquirer = require('inquirer')
const compJson = require('../components.json')
const allComps = Object.keys(compJson).reduce((prev, compType) => {
const { children = {} } = compJson[compType]
Object.keys(children).forEach(compName => {
prev[compName] = {
label: children[compName].label,
compType
}
})
return prev
}, {})
function readFile (filePath) {
return new Promise(function (resolve, reject) {
fsEx.readFile(filePath, 'utf8', function (error, data) {
if (error) return reject(error)
resolve(data)
})
})
}
// 写文件
async function writeFile (filePath, content) {
try {
const res = await fsEx.outputFile(filePath, content)
return res
} catch (error) {
console.error('write file error:', error)
}
}
function tplReplace (str, obj) {
return str.replace(/%(\w+)%/g, (...args) => {
const [, key] = args
return obj[key] || '-'
})
}
function transCamel (_str, symbol = '-') {
const str = _str[0].toLowerCase() + _str.substr(1)
return str.replace(/([A-Z])/g, function ($1) {
return ''.concat(symbol).concat($1.toLowerCase())
})
}
class GenComp {
constructor (cwd) {
this.cwd = cwd || process.cwd()
}
async gen () {
// ESM in commonjs
const ora = await (await import('ora')).default
const spin = ora()
try {
// 1. 选择组件类型
const res = await inquirer.prompt([
{
type: 'list',
message: '请选择组件类型',
name: 'compType',
choices: Object.keys(compJson).map(cType => {
const { label } = compJson[cType]
return {
name: label,
value: cType
}
})
},
{
type: 'input',
message: '请输入组件名[英文, eg: Button]',
name: 'compName',
validate: function (val) {
if (!val || /^[A-Z]{1}[a-z]+[A-Za-z]+$/.test(val) !== true) {
return '请参照示例输入英文组件名'
}
if (Object.prototype.hasOwnProperty.call(allComps, val)) {
return '组件已存在,请重新输入'
}
return true
}
},
{
type: 'input',
message: '请输入组件名[中文, eg: 按钮]',
name: 'labelName',
validate: function (val) {
if (!val || /[\u4e00-\u9fa5]/.test(val) !== true) {
return '请输入组件中文名'
}
return true
}
}
])
spin.start('\n组件创建中')
const { compType, compName, labelName } = res
const dashName = transCamel(compName)
const lowCompName = compName[0].toLowerCase() + compName.substr(1)
const { cwd } = this
const tplObj = {
CompName: compName,
DashName: dashName,
LabelName: labelName,
lowCompName
}
// 2. 读取文件内容
const tplFiles = [
'basic',
'comp',
'index.less',
'style.index.ts',
'index.ts',
'README',
'spec',
'types'
].map(item=>readFile(path.resolve(__dirname, `../static/comp/${item}.tpl`)))
const [demoTpl, compTpl, lessTpl, styleIndexTpl, indexTpl, readmeTpl, specTpl, typesTpl] = await Promise.all(tplFiles)
// 3. 组件输出文件
const parseArr = [
{
dest: 'demos/basic.vue',
content: tplReplace(demoTpl, tplObj)
},
{
dest: `${compName}.tsx`,
content: tplReplace(compTpl, tplObj)
},
{
dest: `style/index.less`,
content: tplReplace(lessTpl, tplObj)
},
{
dest: `style/index.ts`,
content: tplReplace(styleIndexTpl, tplObj)
},
{
dest: `index.ts`,
content: tplReplace(indexTpl, tplObj)
},
{
dest: `${lowCompName}Types.ts`,
content: tplReplace(typesTpl, tplObj)
},
{
dest: 'README.zh-CN.md',
content: tplReplace(readmeTpl, tplObj)
},
{
dest: `__test__/${compName}.spec.js`,
content: tplReplace(specTpl, tplObj)
}
]
// 3.1 创建组件文件
await Promise.all(parseArr.map(item =>
writeFile(`${cwd}/components/${dashName}/${item.dest}`, item.content)
))
// components.ts & style.ts
const [cpTpl, stTpl] = await Promise.all([
readFile(path.resolve(__dirname, `../components/components.ts`)),
readFile(path.resolve(__dirname, `../components/style.ts`)),
])
await Promise.all([
writeFile(`${cwd}/components/components.ts`, `${cpTpl}\nexport { default as ${compName} } from './${dashName}'`),
writeFile(`${cwd}/components/style.ts`, `${stTpl}\nimport './${dashName}/style'`),
])
// 3.2 写入component.json
compJson[compType].children[compName] = {
label: labelName
}
writeFile(`${cwd}/components.json`, JSON.stringify(compJson, null, 2))
spin.succeed('创建成功')
} catch (error) {
spin.fail(`\n创建异常:${error}`)
}
spin.stop()
}
}
// 执行
const genInstance = new GenComp()
genInstance.gen()
编译打包
首先明确我们的目标:
- 导出esm 至 es
- 导出cjs 至 lib
- 支持babel-plugin-import按需加在
- 支持样式css、less引入
编译其实可以通过 babel 或者 tsc 直接生成我们想要的文件,本文通过 gulp 来串联一下流程
为什么不用webpack、rollup ? 因为我们要做的是代码的编译而非打包, 同时需要考虑到样式的处理和按需加载
d.ts 声明文件
上文有提到使用tsc可以得到我们想要的d.ts 文件,在本工程中,我们先只做声明文件的生成
设置 compilerOptions. emitDeclarationOnly = true
function generateDts () {
// 仅生成声明文件
tsConf.emitDeclarationOnly = true
return gulp.src(RESOURCES_MAP.componentsScripts)
.pipe(ts(tsConf))
.pipe(gulp.dest(RESOURCES_MAP.dest.es))
.pipe(gulp.dest(RESOURCES_MAP.dest.lib))
}
⚠️ 而特殊的场景是当我们以 .vue 组件形式来编写组件的时候,通过上述方式,我们是没办法直接生成预期中完整的类型声明文件的 —— 基于这个问题,小伙伴们是否联想到了 vscode 编辑器插件 Vue Language Features (Volar) 是怎么来做如下的类型检测的呢?
答案就是 vue-tsc , 我们可以通过以下脚本生成我们需要的 d.ts 文件 🚀 :
vue-tsc --declaration --emitDeclarationOnly
esm、cjs 资源生成
对esm和cjs有疑问的推荐阅读:「前端」import、require、export、module.exports 混合详解
babel 配置
依赖安装
yarn add -D @babel/core
@babel/plugin-proposal-class-properties
@babel/plugin-transform-runtime
@babel/plugin-transform-typescript
@babel/preset-env
@babel/preset-typescript
@vue/babel-plugin-jsx
cjs 输出资源babel 配置
{
presets: [
resolve("@babel/preset-env")
],
plugins: [
[
resolve("@babel/plugin-transform-typescript"),
{
isTSX: true,
},
],
[
resolve("@vue/babel-plugin-jsx"),
{ mergeProps: false, enableObjectSlots: false },
],
resolve("@babel/plugin-proposal-class-properties"),
resolve("@babel/plugin-syntax-dynamic-import"),
resolve("@babel/plugin-transform-runtime"),
]
}
接下来我们通过 gulp-babel 来做components/**/*.{ts,tsx}的编译
通过使用babel ,编译过程中需要注意的是 缓存的清理
const RESOURCES_MAP = {
dest:{
lib: libDir,
es: esDir
},
componentsScripts: [
'components/**/*.js',
'components/**/*.jsx',
'components/**/*.tsx',
'components/**/*.ts',
'!components/*/__tests__/*'
],
styles: [
'components/**/*.less',
]
}
function compileScripts (format, dest){
// esm 资源构建,则设置 modules
const modules = format === 'es'? false : undefined
const babelConf = getBabelConf(modules)
// 缓存清理
babelConf.babelrc = false
delete babelConf.cacheDirectory
return gulp.src(RESOURCES_MAP.componentsScripts)
.pipe(gulpBabel(babelConf))
.pipe(gulp.dest(dest))
}
esm 输出资源的配置
与 cjs 的区别配置如下
{
presets: [
[
resolve("@babel/preset-env"),
{
modules: false
},
],
],
plugins: [
// ...
[
resolve("@babel/plugin-transform-runtime"),
{
useESModules: true,
},
],
]
}
样式
样式的打包产物需要覆盖以下场景:
-
less 引用
-
css 按需引用
-
完整 css 全量引用
less产物构建
less的支持比较简单,我们直接复制到目标目录(lib、es)即可
function copyLess (){
return gulp.src(RESOURCES_MAP.styles).pipe(gulp.dest(RESOURCES_MAP.dest.es)).pipe(gulp.dest(RESOURCES_MAP.dest.lib))
}
css 产物构建
为覆盖非 less工程项目中的使用场景, 我们有以下方案可选:
-
告知工程增加 less-loader ; (业务工程使用成本增加)
-
打包一份全量css;(无法按需引入)
-
css in js ;(组件代码中直接import 后进行编译)
-
提供一份style/css.js ,其引入组件css 样式依赖(支持按需加载,兼容业务工程直接引用)
我们侧重来看下 后面两个方案:
css in js 可以说是编写第三方组件的利器, 某种程度上来说组件开发就像在开发util一样简单,并且支持 esm treeShaking,不需要业务方去维护样式依赖,不过缺点也显而易见:
-
样式无法缓存
-
覆写组件样式需要注意css作用域(大部分场景基于css属性选择器来做)
而最后一种方案的优势在哪里?
-
支持按需加载
-
方便样式依赖管理
最终我们决定采用最后一种方案
less to css
依赖安装
yarn add -D gulp-less gulp-autoprefixer gulp-cssnano
编译
function compileStyles (){
return gulp.src(RESOURCES_MAP.styles)
.pipe(less())
.pipe(autoprefixer())
.pipe(filter(function (file){ // 过滤空文件
return file.stat && file.contents.length
}))
.pipe(gulp.dest(RESOURCES_MAP.dest.es))
.pipe(gulp.dest(RESOURCES_MAP.dest.lib))
}
⚠️ 其中需要注意的是less使用场景中可能会有空文件产生(less变量、函数编译后为空), 我们需要过滤掉它们
css.js
css 文件已经构建完成,接下来我们需要产出css.js 来引入组件的css样式
安装依赖
yarn add -D through2
编译
我们在scripts构建管道中进行操作,匹配style/index.{js,ts,tsx}文件,替换内部的样式引用代码,生成我们需要的css.js
function compileScripts (format, dest){
// esm 资源构建,则设置 modules
const modules = format === 'es'? false : undefined
const babelConf = getBabelConf(modules)
// 缓存清理
babelConf.babelrc = false
delete babelConf.cacheDirectory
return gulp.src(RESOURCES_MAP.componentsScripts)
.pipe(gulpBabel(babelConf))
.pipe(through2.obj(function (file, encode, next){
this.push(file.clone())
// [comp]/style/css.js 文件生成, 提供以兼容使用方非less样式处理器(sass, stylus等)
if (file.path.match(//style/index.(js|ts|tsx)$/)) {
const content = file.contents.toString(encode);
file.contents = Buffer.from(
content
.replace(//style/?'/g, "/style/css'")
.replace(//style/?"/g, '/style/css"')
.replace(/.less/g, '.css'),
);
file.path = file.path.replace(/index.(js|ts|tsx)$/, 'css.js');
this.push(file);
}
next()
}))
.pipe(gulp.dest(dest))
}
按需加载
经过上一步的操作,我们可以开始我们的按需加载测试了
不过,在此之前我们拓展一点关于sideEffects的使用—— 当我们的组件库采用的css in js时,可在package.json 中添加如下 sideEffects 属性,配合 es module 达到我们想要的 tree shaking 效果
"sideEffects":[
"lib/**/style/*",
"es/**/style/*",
"*.less"
],
⚠️本文我们不添加此字段
关于 sideEffects 可以参考 Webpack 中的 sideEffects 到底该怎么用?
手动引入
import { Button } from 'k-view-next'
import 'k-view-next/es/button/style'
也可以用以下方式引入
import Button from 'k-view-next/es/button'
import 'k-view-next/es/button/style'
babel-plugin-import
手动引入不是很优雅,我们可以通过babel插件来做按需引入
import { Button } from 'k-view-next'
至此,我们的构建流程结束, 依赖babel插件我们达成了完整的按需引入效果 🚀
主题定制
主题定制我们采用 less modifyVars 方案, 实现参考如下:
国际化支持
国际化我们通过 vue 原生的 provide/inject 注入配置来实现,主要包含两部分:
-
config-provider 顶层配置注入
-
组件 vue use hook 配置接收处理
配置注入
通过代码可以看到我们默认注入了中文语言包,并提供了 useLocaleReceive 让组件来快速获取当前组件的语言配置 🎸
组件配置获取 useLocaleReceive
我们以Button 组件为例, 来看下如何获取语言配置:
使用起来还是很简单的,我们的多语言配置就这样获取到啦~
单元测试
单元测试的工具很多,这里我们采用 jest 来做
依赖安装
yarn add -D @vue/test-utils @vue/vue3-jest jest jest-serializer-vue
配置
jest.config.js
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
const path = require('path')
module.exports = {
preset: 'ts-jest',
verbose: true,
rootDir: path.resolve(__dirname, './'),
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'vue'],
transform: {
'^.+\.(vue|md)$': '<rootDir>/node_modules/@vue/vue3-jest',
'^.+\.(js|jsx)$': '<rootDir>/node_modules/babel-jest',
'^.+\.(ts|tsx)$': '<rootDir>/node_modules/ts-jest',
},
transformIgnorePatterns: [
'node_modules',
],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1',
'k-view-next$': '<rootDir>/components/index.ts',
'k-view-next/es': '<rootDir>/components',
},
testMatch: [
'**/*/__tests__/**/*.spec.js',
],
snapshotSerializers: ['<rootDir>/node_modules/jest-serializer-vue'],
setupFiles: [
'<rootDir>/tests/setup/jest.init.js',
],
coverageDirectory: '<rootDir>/coverage',
collectCoverageFrom: [
'components/**/*.ts',
'components/**/*.tsx',
],
testEnvironment: 'jsdom',
globals: {
'ts-jest': {
babelConfig: true,
},
},
}
babel.config.js
测试环境配置如下
module.exports = {
env: {
test: {
presets: [
[
'@babel/preset-env',
{
targets: { node: true }
}
]
],
plugins: [
[
'@vue/babel-plugin-jsx',
{ mergeProps: false, enableObjectSlots: false }
],
'@babel/plugin-proposal-class-properties',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-runtime',
],
},
},
}
package.json
"scripts": {
"test": "cross-env NODE_ENV=test jest --coverage"
},
测试用例编写
这里我们以Button为例
components/button/tests/button.spec.js
import { mount } from '@vue/test-utils'
import Button from '../index'
describe('Button', () => {
it('renders correctly', () => {
const wrapper = mount({
render() {
return <Button>Follow</Button>
},
})
expect(wrapper.html()).toMatchSnapshot()
})
it('click event should be called correctly', () => {
const fn = jest.fn()
const wrapper = mount({
render (){
return <Button onClick={fn}>点击</Button>
},
})
wrapper.trigger('click')
expect(fn).toBeCalled()
wrapper.trigger('click')
expect(fn).toBeCalledTimes(2)
})
})
测试用例需要覆盖组件的特性分支,这里仅给出demo演示
unit 测试
CI/CD
本文我们采用circle-ci 来做文档站点部署和npm包发布
环境变量配置
写入circleci 权限
本文我们需要授权 CircleCI 自动更新 gh-pages 分支的权限,我们需要配置ssh密钥来支持
生成ssh密钥
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
按照命令行交互,您将获得两个 ssh 密钥文件id_rsa和id_rsa.pub(记得更改默认文件位置或您的本地ssh密钥将被覆盖)
上传密钥
- 通过github.com/<your_name>/<your_repo>/settings/Deploy keys上传您的GitHub repo设置上的id_rsa.pub。
-
跳转到
https://app.circleci.com/settings/project/github/<your-name>/<your-respo>/ssh?return-to=https://app.circleci.com/pipelines/github/<your-name>/<your-respo>并添加您刚刚创建的私钥id_rsa。在主机名字段中输入github.com,然后按 提交按钮。并添加您刚刚创建的 私钥id_rsa。在主机名字段 hostname 中输 入 github.com, 然后按提交按钮
保存 fingerprints
后面yml文件配置中会用到
- add_ssh_keys:
fingerprints:
- "ae:xxxxxxxxx:c3:4f:0f:ff:5f"
npm-token 设置
npm-auth-token 获取
- 查看npm 配置
npm config ls -l
cat /Users/gorgechan/.npmrc
找到你要发布的npm源的autoToken, 复制 “=” 后的字符串,供下一步配置使用
circle-ci 环境变量配置
至此,我们的环境变量配置完成啦 🎸
新建.circleci/config.yml
配置文件主要由3部分组成:
- 版本 version
-
- 你要使用的circleci 版本
- 工作 jobs
-
- 需要执行的job, 名称需要唯一
- 如果不使用工作流workflows,则必须包含 build 的 job 来作为默认工作
- 工作流
-
-
用于编排所有的 job
-
工作
我们需要明确我们需要做的事情:
- 依赖安装
- 文档站点部署
- npm包发布
内容
version: 2
defaults: &defaults
working_directory: ~/repo
docker:
- image: circleci/node:14
jobs:
install:
<<: *defaults
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "package.json" }}
# fallback to using the latest cache if no exact match is found
- v1-dependencies-
- run:
name: Install
command: yarn install
- save_cache:
paths:
- node_modules
key: v1-dependencies-{{ checksum "package.json" }}
- persist_to_workspace:
root: ~/repo
paths: .
site:
<<: *defaults
steps:
- attach_workspace:
at: ~/repo
- add_ssh_keys:
fingerprints:
- "ae:9d:1b:7b:6a:98:6b:1e:46:77:49:c3:4f:0f:ff:5f"
- run:
name: Knows_hosts add
command: ssh-keyscan github.com >> ~/.ssh/known_hosts
- run:
name: Prepare shell commands
command: cp .circleci/github.sh ./ && chmod +x ./github.sh && ls -a
- run:
name: Run github shell
command: ./github.sh
test:
<<: *defaults
steps:
- attach_workspace:
at: ~/repo
- run:
name: Run tests
command: yarn test
deploy:
<<: *defaults
steps:
- attach_workspace:
at: ~/repo
- run:
name: Authenticate in registry
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
- run:
name: Publish Package
command: npm publish
# Orchestrate our job run sequence
workflows:
version: 2
build-deploy:
jobs:
- install:
filters: &tagged
tags:
only: /^v.*/
branches:
ignore: /.*/
- site:
requires:
- install
filters:
<<: *tagged
- test:
requires:
- install
filters:
<<: *tagged
- deploy:
requires:
- test
filters:
<<: *tagged
.circleci/github.sh
#!/usr/bin/env sh
# 构建
yarn docs:build
# 进入生成的构建文件夹
cd dist
git init
git config user.email "ab140140@163.com"
git config user.name "SoldierAb"
git checkout -b gh-pages
git add .
git commit -m 'docs: site deploy'
echo "before docs deploy"
git push --force git@github.com:SoldierAb/k-view-next.git gh-pages
版本提交
状态查看
通过github actions 和circle-ci的项目管理面板,我们就能看到我们的持续集成状态啦 🚀
gh-pages 查看
npm包查看
总结
举一反三:
-
React 组件库是否一样适用这套构建体系?
-
rollup、vite 如何实现?优势劣势对比?
-
... ...
一个组件库从需求、设计、开发、部门内方案实施,这一系列的流程走下来,相信我们能一起成长的!🚀
最后
关注公众号,更多技术好文 每周1篇+持续更新(上周阳了停更🌚~)
私信 “组件库” 获取源码~