Bootstrap

【UI组件库】打包发布到npm上

如何将TS转换为ES6 modules

在create-react-app中运行的代码,首先通过tsc将.tsc文件转换为.jsx文件(ES6 modules),
关于tsc的配置都写在tsconfig.json中,create-react-app有一个默认的tsconfig.json,但这个tsconfig.json和开发环境相关,而我们使用的tsconfig和最后打包模块相关的,所以我们单独新建tsconfig.build.json:

{
  "compilerOptions": {
    "outDir": "dist",//输出文件路径
    "module": "esnext",//输出的module类型
    "target": "es5",// 编译后符合什么样的ES标准
    "declaration": true,// 会为每个js文件生成对应的.d.ts类型文件
    "jsx": "react",// 编译出来的的文件,里面的代码就能用reactCreateElement方法代替JSX语法
    //"moduleResolution":"Node",//
    /*
    allowSyntheticDefaultImports默认为false,此时引入默认模块:import * as React from 'react'
    为true时:import React from 'react'。我们使用的都是这种,所以这里还要配置为true
    */
    //"allowSyntheticDefaultImports": true,
  },
  "include": [// 要编译的文件
    "src"
  ],
  "exclude": [//不编译的文件,**代表任意长度,*代表全匹配
    "src/**/*.test.tsx",
    "src/**/*.stories.tsx",
    "src/setupTests.ts",
  ]
}

在package.json中的"script"中添加命令:"build-ts": "tsc -p tsconfig.build.json",他的作用是把ts文件打包成es module文件。配置好了之后运行:npm run build-ts,然后报错了:cannot find module '@babel/types'
在这里插入图片描述
这是因为TS处理模块的方式和node不一样,模块加载分为相对路径和绝对路径,默认是classic方法,即相对路径,
在这里插入图片描述
如果改为node方法,即绝对路径:
在这里插入图片描述
所以我们还需要在tsconfig.build.json:中配置:"moduleResolution":"Node"
然后有运行:npm run build-ts,继续报错:
在这里插入图片描述
这是因为allowSyntheticDefaultImports默认为false,此时引入默认模块:import * as React from 'react',为true时:import React from 'react',我们使用的都是这种,所以这里还要配置还需要在tsconfig.build.json中配置allowSyntheticDefaultImports为true:"allowSyntheticDefaultImports": true

生成最终的样式文件

在create-react-app中已经安装使用了sass,现在可以使用node-sass来将sass文件编译成css文件:node-sass 源文件地址 输出的css文件地址。先在package.json中的"scripts"中新增:"build-css": "node-sass ./src/styles/index.scss ./build/index.css",并修改:"build": "npm run build-ts && npm run build-css"。然后在项目终端先删除之前打包的build文件:rm -rf build,然后再重新打包:npm run build,成功打包。
以后每次打包前都需要删除之前打包的build文件,我们希望在运行npm run build后,系统自动帮我们打包前都自动帮我们删除了之前打包的build文件。使用rimraf完成跨平台的删除任务,在项目终端输入:npm install rimraf --save-dev,然后在package.json中的"scripts"中新增:"clean": "rimraf ./dist",并修改:"build": "npm run clean && npm run build-ts && npm run build-css"
以后每次打包直接运行npm run build即可,无需手动删除之前打包的build文件

使用npm link本地测试组件库

包发布之前需要在本地测试一下打包出来的格式文件是否好用,假如我有两个项目,一个是本地测试项目vikingTest,一个是本地项目vikingShip组件库,前者要使用后者作为他的依赖,那么在前者的package.json中配置:'vikingship:'0.1.0',假如vikingShip已经发布到npm上,我们可以使用npm install来安装。现在我们修改了vikingShip的内容,需要在vikingTest中测试,最笨的方法就是,修改vikingShip并发布到线上,发布为一个新的版本0.1.1,发布完成后,在vikingTest的package.json中修改vikingShip的版本号为:'vikingship:'0.1.1',然后再重新安装vikingShip。
我们希望vikingTest可以直接关联本地vikingShip,这样vikingShip的修改就是实时的,不用重新安装,你引用的就是他本地的文件,那么我们可以在vikingShip文件夹终端:npm link创建软连接到全局,再在vikingTest终端:npm link vikingship创建软连接,连接到vikingShip刚创建的软连接(他是全局的),然后该软连接再连接到vikingShip。vikingTest import vikingShip时,他需要知道怎样引入哪几个文件,所以需要在vikingShip的package.json中添加:

"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts"

然后在vikingTest的package.json中"dependencies"添加个假依赖:”vikingship": "0.1.0"“
然后在app.tsx中使用vikingShip,再使用npm start运行即可

发布到NPM,添加CICD支持

  1. 注册账号:
    方法一:NPM上注册账号
    方法二:终端输入npm whoami查看自己的身份,如果报错说明你没登录,使用npm adduser登录/注册账户,输入用户名密码邮箱即可。

注意,由于npm太慢了,很多人用的是淘宝的镜像,终端输入:npm config ls查看当前信息metrics-registry,如果不是https://registry.npmjs.org/,需要删除镜像,切换回https://registry.npmjs.org/,否则你无法注册登录

  1. 配置package.json:
{
  "name": "vikingship",// package名
  /*
  版本号。
  格式:主版本号.次版本号.修订号
  主版本号:做了不兼容的API修改
  次版本号:做了向下兼容的功能性新增
  修订号:做了向下兼容的问题修正
  */
  "version": "0.1.4",
  "description": "React components library",// 描述包的内容
  "author": "Viking Zhang",// 作者
  "private": false,// 是否是私有的包
  "main": "dist/index.js",
  "module": "dist/index.js",
  "types": "dist/index.d.ts",
  "license": "MIT",// 开源软件遵守的协议
  "keywords": [// 关键字,便于搜索
    "Component",
    "UI",
    "React"
  ],
  "homepage": "http://vikingship.xyz",// 项目主页
  "repository": {// github仓库
    "type": "git",
    "url": "https://github.com/vikingmute/vikingship"
  },
  "files": [// 把哪些文件上传到npm中
    "dist"// 注意:之前我们把打包后的文件输出到build文件中,这种情况下,这里应该写"build"。
    // 现在我们把打包后的文件输出到dist文件中,所以这里写的dist,包括全文所有输出文件的地方,都改为dist
  ],
  "dependencies": {
    //图标
    "@fortawesome/fontawesome-svg-core": "^1.2.26",
    "@fortawesome/free-solid-svg-icons": "^5.12.0",
    "@fortawesome/react-fontawesome": "^0.1.8",
    "axios": "^0.25.0",// 发送请求
    "classnames": "^2.2.6",// 合并类名
    "react-transition-group": "^4.3.0",
    // 声明types定义
    "@types/classnames": "^2.2.9",
    "@types/jest": "24.0.23",
    "@types/node": "12.12.14",
    "@types/react": "^16.9.13",
    "@types/react-dom": "16.9.4",
    "@types/react-transition-group": "^4.2.3",
    "@types/storybook__addon-info": "^5.2.1",
    "node-sass": "^4.14.1",// 预处理器
    "react": "^16.12.0",
    "react-docgen-typescript-loader": "^3.6.0",
    "react-dom": "^16.12.0",
    "react-scripts": "3.2.0",// 开发的工具
    "typescript": "3.7.2"// TS编译器
  },
  "scripts": {
    "start": "react-scripts start",
    "clean": "rimraf ./dist",
    "build": "npm run clean && npm run build-ts && npm run build-css",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "build-ts": "tsc -p tsconfig.build.json",// 把ts文件打包成es module文件
    "build-css": "node-sass ./src/styles/index.scss ./dist/index.css",
    "storybook": "start-storybook -p 9009 -s public",// 把storybook生成静态文档页面
    "build-storybook": "build-storybook -s public",
    //在publish之前我们要编译生成一下最终的代码,所以我们要添加一个钩子函数prepublishOnly,他会自主运行我们在prepublishOnly中配置的代码
    "prepublishOnly": "npm run build"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "@storybook/addon-actions": "^5.2.8",
    "@storybook/addon-info": "^5.3.21",
    "@storybook/addon-links": "^5.2.8",
    "@storybook/addons": "^5.2.8",
    "@storybook/react": "^5.2.8",
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "cross-env": "^7.0.0",
    "husky": "^4.2.1",
    "rimraf": "^3.0.1",
  }
}
  1. 在项目终端输入:npm publish发布

精简package.json中的"dependencies"

依赖分为两类:
(1)“dependencies”:运行项目业务逻辑需要依赖的第三方库,当运行npm install时,这些依赖都会被解析下载到node_modules中
(2)“devDependencies”:开发模式工作流(与核心业务逻辑和最终生成的模块无关的任务,这些任务又支撑着核心业务的开发过程,以及程序从开发环境向生产环境的支撑过程,如单元测试、语法转换、CSS预处理器、程序构建等)下需要依赖的第三方库都可以声明到该类之下
目前,我们"dependencies"中有很多库,当运行npm install时,这些库都会被安装,但是在使用时,很多都没有被用到,这完全是浪费,所以我们需要把这些库都移动到"devDependencies"中:

  1. types用于声明types定义,和最终的产品没有关系,移动到"devDependencies"
  2. node-sass是预处理器,将sass文件转化为css文件,移动到"devDependencies"
  3. react-scripts是开发工具,是creat-react-app自带的脚本,移动到"devDependencies"
  4. typescript是TS编译器,移动到"devDependencies"
  5. 当react项目使用组件库时,组件库有react版本,react项目也有react版本,react会被安装两次,当有两份react同时引到源代码中时,会报一个hook错误,react是我们整个组件库的核心依赖库,所以组件库运行的前提是核心依赖库必须先下载安装,不能脱离核心依赖库而被单独依赖并引入,npm的package.json中有一个字段"peerDependencies"提供核心依赖库的信息,告诉用户,如果你想使用我的插件或库,你必须先安装这些依赖。我们在该package.json中添加该字段:
  "peerDependencies": {
    // 核心库:react、react-dom 且版本都大于等于16.8.0 16.8.0之后才引入了react hook
    "react": ">=16.8.0",
    "react-dom": ">=16.8.0"
  },

然后在"dependencies"中去掉这两项,这样就能解决react安装多次的问题,但是在开发时我们还需要这两个库,所以我们需要将这两项(刚从"dependencies"中去掉内容)移动到"devDependencies"中,在本地npm install时就有这两个依赖了

添加发布和commit前检查

单元测试用来保证代码中尽量减少bug,代码规范有助于项目的维护,为了防止bug和不规范的代码被commit,并push到远端,还要防止他们被publish,被用户直接使用,需要一些钩子函数,首先验证开发者是否通过代码规范和单元测试,然后才能进行commit和publish。

  1. 代码规范的检查:在开发环境中,creat-react-app自带eslint,会自动启动。现在我们需要添加一个单独的npm命令,一运行,他就检查代码规范,也就是说把代码规范检查这一功能单独剥离出来,我手动启动。在package.json中"scripts"中添加命令:"lint": "eslint --ext js,ts,tsx src --max-warnings 5"

在src中检查扩展名(–ext)文件js,ts,tsx
–max-warnings 5:运行允许最多的warnings数量为5

  1. 添加测试用例:在package.json中"scripts"中已经有test命令了,但这个test命令默认在开发时使用,他不会返回最终通过/没通过的结果,而是处于watch模式下,我们需要一个测试命令,和lint一样,可以直接返回结果,但是我们发现,在不同操作环境下,设置测试命令的代码是不一样的。推荐使用cross-env,他能很方便的跨平台设置环境变量,在项目终端安装:npm install cross-env --save-dev,然后在package.json中"scripts"中添加命令:"test:nowatch": "cross-env CI=true react-scripts test"。终端运行:npm run test:nowatch,会出现测试结果。
  2. 更改package.json中"scripts"的prepublishOnly:"prepublishOnly": "npm run test:nowatch && npm run lint && npm run build"
  3. 以上,我们就把代码规范检查、添加测试用例两个需求剥离出来了,并且在publish前会进行测试和代码规范检查。接下来我们可以使用husky让程序在push和commit前也进行测试和代码规范检查。在项目终端安装:npm install husky --save-dev,然后在package.json中添加:
  "husky": {
    "hooks": {
      "pre-commit": "npm run test:nowatch && npm run lint"
    }
  },

综上,package.json代码:

{
  "name": "vikingship",
  "version": "0.1.4",
  "description": "React components library",
  "author": "Viking Zhang",
  "private": false,
  "main": "dist/index.js",
  "module": "dist/index.js",
  "types": "dist/index.d.ts",
  "license": "MIT",
  "keywords": [
    "Component",
    "UI",
    "React"
  ],
  "homepage": "http://vikingship.xyz",
  "repository": {
    "type": "git",
    "url": "https://github.com/vikingmute/vikingship"
  },
  "files": [
    "dist"
  ],
  "dependencies": {
    "@fortawesome/fontawesome-svg-core": "^1.2.26",
    "@fortawesome/free-solid-svg-icons": "^5.12.0",
    "@fortawesome/react-fontawesome": "^0.1.8",
    "axios": "^0.25.0",
    "classnames": "^2.2.6",
    "react-transition-group": "^4.3.0"
  },
  "peerDependencies": {
    "react": ">=16.8.0",
    "react-dom": ">=16.8.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "clean": "rimraf ./dist",
    "lint": "eslint --ext js,ts,tsx src --max-warnings 5",
    "build": "npm run clean && npm run build-ts && npm run build-css",
    "test": "react-scripts test",
    "test:nowatch": "cross-env CI=true react-scripts test",
    "eject": "react-scripts eject",
    "build-ts": "tsc -p tsconfig.build.json",
    "build-css": "node-sass ./src/styles/index.scss ./dist/index.css",
    "storybook": "start-storybook -p 9009 -s public",
    "build-storybook": "build-storybook -s public",
    "prepublishOnly": "npm run test:nowatch && npm run lint && npm run build"
  },
  "husky": {
    "hooks": {
      "pre-commit": "npm run test:nowatch && npm run lint"
    }
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "@storybook/addon-actions": "^5.2.8",
    "@storybook/addon-info": "^5.3.21",
    "@storybook/addon-links": "^5.2.8",
    "@storybook/addons": "^5.2.8",
    "@storybook/react": "^5.2.8",
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "@types/classnames": "^2.2.9",
    "@types/jest": "24.0.23",
    "@types/node": "12.12.14",
    "@types/react": "^16.9.13",
    "@types/react-dom": "16.9.4",
    "@types/react-transition-group": "^4.2.3",
    "@types/storybook__addon-info": "^5.2.1",
    "cross-env": "^7.0.0",
    "husky": "^4.2.1",
    "node-sass": "^4.14.1",
    "react": "^16.12.0",
    "react-docgen-typescript-loader": "^3.6.0",
    "react-dom": "^16.12.0",
    "react-scripts": "3.2.0",
    "rimraf": "^3.0.1",
    "typescript": "3.7.2"
  }
}

使用storybook生成静态文档页面

新建src/welcome.stories.tsx作为文档页面的首页,代码如下:

import React from 'react'
import { storiesOf } from '@storybook/react'

storiesOf('Welcome page', module)
  .add('welcome', () => {
    return (
      <>
        <h1>欢迎来到 vikingship 组件库</h1>
        <p>vikingship 是为慕课网课程打造的一套教学组件库,从零到一让大家去学习</p>
        <h3>安装试试</h3>
        <code>
          npm install vikingship --save
        </code>
      </>
    )
  }, { info : { disable: true }})

接下来将其配置为文档页面的首页,在.storybook/config.tsx的config函数中配置:

const loaderFn = () => {
  const allExports = [require('../src/welcome.stories.tsx')];
  const req = require.context('../src/components', true, /\.stories\.tsx$/);
  req.keys().forEach(fname => allExports.push(req(fname)));
  return allExports;
};
configure(loaderFn, module);
;