Typescript组件库搭建指南

1,186 阅读5分钟

前言

组件库一直是前端开发不可或缺的部分,基于组件库我们可以为开发提效,甚至实现对某一特定业务场景设计语言的统一。

搭建过程中涉及很多知识,再看看社区开源成熟的组件库设计,自己也有了更深刻的感悟,故记录下来与大家一同分享,

文章较干较长,建议先 “收藏”。~ 文末 附源码免费 获取方式

准备

涉及要点:

  • 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篇+持续更新(上周阳了停更🌚~)

qr.jpg

私信 “组件库” 获取源码~