在实际项目中,我们常常会遇到这样的一些场景:比如现在需要做一个下载报表的功能,而且这个功能在很多页面都有,下载的也是同类型的报表,如果在每个页面都去写重复功能的代码显得特别冗余,可以将其封装成一个组件多处调用呢?只不过这个组件跟我们常见的一些基础组件有些区别,区别在于这个组件里头包含了业务逻辑,称之为“业务组件”,这类组件是专门为了提高开发效率衍生出来的一种方案,这个组件库可能由专门维护组件库的人来维护,也可能是单个项目组自己的业务组件库。废话不多说,我们来实操一下:
初始化项目
lerna
Lerna
是一个优化使用 git
和 npm
管理多包存储库的工作流工具,用于管理具有多个包的 JavaScript
项目。
将大型代码库拆分为独立的独立版本包对于代码共享非常有用。 然而,在许多存储库中进行更改是麻烦和难以跟踪的事情。为了解决这些(和许多其他)问题,一些项目将它们的代码库组织成多包存储库。 像 Babel、React、Angular、Ember、Meteor、Jest
等等。
lerna解决什么问题
- 假设主项目是
Angular
技术栈的,依赖两个自研npm
包,这两个包也依赖Angular
,现在主项目要升级Angular
版本,那么这两个npm
包也得跟着升级,且需要升级两次(一个包一次),可否只升级一次? - 假设有两个
npm
包A
和B
,A
依赖B
,那么每当B
有更新时,要想让A
用上B
的更新,需要B
发版,然后A
升级B
的依赖,可否更简单些?
解法就是 lerna
,一种多包依赖解决方案,简单来讲:
1、可以管理公共依赖和单独依赖;
2、多package
相互依赖直接内部 link
,不必发版;
3、可以单独发布和全体发布
4、多包放一个git
仓库,也有利于代码管理,如配置统一的代码规范
lerna搭建工程
首先使用 npm
将 Lerna
安装到全局环境中,推荐使用 Lerna 2.x
版本:
yarn add lerna -g
接下来,创建一个新的 git
代码仓库:
git init pony-bre-component && cd pony-bre-component
并与github
远程仓库关联
get remote add origin xxx
现在,我们将上述仓库转变为一个 Lerna
仓库:
lerna init
另外,安装react、react-dom、typescript、@types/react、@types/react-dom
yarn add typescript react react-dom @types/react @types/react-dom
执行npx typescript --init
在根目录生成tsconfig.json
配置文件
你的代码仓库目前应该是如下结构:
pony-business-component/
packages/ 存放每一个组件
package.json
lerna.json lerna配置
tsconfig.json typescript编译配置文件
lerna.json配置如下:
{
"packages": [
"packages/*"
],
"version": "0.0.0"
}
package.json配置如下:
{
"name": "root",
"private": true,
"devDependencies": {
"lerna": "^4.0.0"
},
"dependencies": {
"@types/react": "^17.0.4",
"@types/react-dom": "^17.0.3",
"axios": "^0.21.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"typescript": "^4.2.4"
}
}
创建组件分包
每一个组件都是一个仓库包,比如我们创建一个TodoList
业务组件,在packages
目录下创建bre-todo-list
文件夹,执行下面命令初始化组件包
cd packages/bre-todo-list
npm init 生成package.json
接着,按照如下结构搭建目录
pony-business-component
├── packages
├── bre-todo-list
├── src 组件
├── api 接口api定义
└── index.ts
├── interfaces 类型定义
└── index.ts
├── styles 样式
└── index.scss
├── TodoList.tsx
└── index.ts
├── package.json
└── tsconfig.json
接口定义
TodoList
组件内部需要通过接口获取清单数据,在src/api/index.ts
定义好接口方法
import axios from 'axios';
export const getTodoList = (id: string) => {
return axios.get(`/mock/16430/todolist/${id}`).then(res => res.data)
}
编写组件
编写TodoList
组件,初始渲染时调用接口获取清单数据,并渲染
// src/TodoList.tsx
import React, { useCallback, useEffect, useState } from 'react';
import { getTodoList } from './api';
interface TodoListProps {
id: string;
}
const TodoList = (props: TodoListProps) => {
const [source, setSource] = useState<string[]>([]);
const init = useCallback(async () => {
const { id } = props;
if (id) {
const { code , data} = await getTodoList(id);
if (code === 200 && !!data) {
setSource(data);
}
}
}, [])
useEffect(() => {
init();
}, []);
return (
<ul>
{
source.map((s: string, index: number) => <li key={index}>{s}</li>)
}
</ul>
)
}
export { TodoList };
在src/index.ts
中抛出组件和类型
export * from './TodoList';
export * from './interfaces';
这样,一个业务组件示例就写好了,接下来需要对它进行编译配置
es module打包
业务组件一般属于公司或者项目组私有组件,只需要满足特殊场景即可。现在前端开发中最常还是采用es module
方式组织代码,因此,只需要采用es module
打包格式输出组件分包即可,不需要满足AMD、CommonJS
使用场景。
lerna
提供了一个命令,可以在每一个分包下执行一些指令
lerna exec tsc // 表示在每一个分包在会执行tsc指令
在每个分包下执行tsc
编译时,会优先在分包下找tsconfig.json
文件,如果没有再向上一级去找,直到找到根目录下的tsconfig.json
文件
我们在分包tsconfig.json
文件做如下配置,只要指定编译的入口和出口,并继承根目录下的配置选项:
// 分包tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./lib", /* Redirect output structure to the directory. */
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
},
"include": ["src"],
"exclude": ["*/__tests__/*"],
}
根目录tsconfig.json
配置如下,主要关注module、declaration
两个选项,module
指定编译后模块的组织方式采用es module
,declaration
表明在编译时会生成相应的.d.ts
类型声明文件,项目中不采用typescript
可以不用指定
{
"compilerOptions": {
"target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"lib": [
"ES2015"
], /* Specify library files to be included in the compilation. */
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
"declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
"downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
"strict": true, /* Enable all strict type-checking options. */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"baseUrl": "./", /* Base directory to resolve non-absolute module names. */
"paths": {
"bre-*": ["packages/bre-*/src"],
"*": [
"node_modules"
]
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
},
"include": [
"packages/bre-*/src"
],
"exclude": [
"node_modules",
"packages/**/node_modules",
"packages/**/lib"
]
}
在根目录下package.json
中加上脚本命令
"scripts": {
"build": "lerna exec tsc",
},
当在根目录下执行yarn build
时,会对每一个分包的src
目录进行编译,编译生成的JS
脚本、类型声明文件以及source-map
文件会输出到分包下lib
文件夹下
打包完成后,需要在分包package.json
中指定抛出的文件
name
:指定组件包的名称main
:指定组件包的入口文件,比如AMD
引入types
:指定组件包类型声明的入口文件module
:指定es module
引入时的入口文件files
:指定安装bre-todo-list
组件包时会下载哪些文件
{
"name": "bre-todo-list",
"version": "1.0.0",
"description": "任务清单业务组件",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"module": "lib/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"files": [
"lib/*",
"src/*",
"styles/*",
"package.json"
]
}
大家是否有个疑问:为什么每一个分包下都需要有一个tsconfig.json
文件呢?为什么不直接用根目录下的tsconfig.json
文件呢?
这是因为在根目录tsconfig.json
无法配置将每一个分包编译后生成的文件输出到对应分包下的lib
目录,因此需要在每个分包下tsconfig.json
中量身配置编译入口和出口文件路径
docz搭建组件文档
Docz
是一套与 Storybook
相比更简约的组件库文档实现方案,它解决了组件库文档开发最主要的问题(组件列表、组件展示、组件属性列表、组件编辑调试)
Docz
基于 MDX
实现,MDX(Markdown + jsx)
允许你在 markdown
样式的文件中导入根使用 JSX
组件,并且 Docz
提供了一些内置组件,可以方便快速的实现文档搭建,并且 Docz
对 Typescript
的支持良好,可以根据 ts
的类型与注释生成文档
Gatsby
是基于 React
构建的静态站点生成器,拥有丰富的插件生态,其主要目标之一是交付访问速度快速的网页,它通过利用良好的缓存、静态页面生成和基于边缘的 CDN
数据源来实现这一目标。
安装docz
yarn add docz
在项目中安装Docz
之后,将三个脚本命令添加到package.json
以便运行
"scripts": {
"docz:dev": "docz dev",
"docz:build": "docz build",
"docz:serve": "docz build && docz serve"
},
在根目录下创建docs
文件夹,编写组件文档
/docs/bre-todo-list.mdx
---
name: bre-todo-list
menu: Components
---
import { Playground, Props } from "docz";
import { TodoList } from "../packages/bre-todo-list/src/index.ts";
import '../packages/bre-todo-list/styles/index.scss';
## 安装
yarn add bre-todo-list
## 引用
import { TodoList } from 'bre-todo-list';
import 'bre-todo-list/styles/index.scss';
## 属性
<Props of={TodoList} isToggle/>
## 基础用法
<Playground>
<TodoList id="123456"></TodoList>
</Playground>
在根目录下创建doczrc.js
,自定义dcoz
配置
export default {
title: 'bre-component', // 网站的标题
typescript: true, // 如果需要在.mdx文件中引入Typescript组件,则使用此选项
dest: 'build-docs', // 指定docz构建的输出目录
files: 'docs/*.mdx', // Glob模式用于查找文件。默认情况下,Docz会在源文件夹中找到所有扩展名为.mdx的文件。
ignore: ['README.md', 'CHANGELOG.md'] // 用于忽略由docz解析的文件的选项
};
mdx
文档中使用了scss
预处理器,需要加上对scss
处理的配置,安装node-sass
和gatsby-plugin-sass
,并在根目录下创建gatsby-config.js
,添加如下配置:
module.exports = {
plugins: [
{
resolve: `gatsby-plugin-sass`,
options: {
implementation: require("node-sass"),
},
}
],
}
组件中使用了接口数据,在本地启动静态网站调用时会出现跨域问题,还需要向gatsby-config.js
中添加代理配置:
const proxy = require('http-proxy-middleware')
module.exports = {
plugins: [
{
resolve: `gatsby-plugin-sass`,
options: {
implementation: require("node-sass"),
},
}
],
developMiddleware: app => {
app.use(
proxy(['/mock/16430'], {
target: 'https://mock.yonyoucloud.com/',
changeOrigin: true,
})
)
}
}
如果在执行yarn docz:build
构建后产生下面错误,这是因为Gastby
采用的是服务端渲染(SSR
),服务端渲染中不支持使用window
的模块,因此报了window
没有被定义
可以在根目录下创建gastby-node.js
,添加如下代码,在服务器渲染期间用虚拟模块替换有问题的模块,详情参考官网:https://www.gatsbyjs.cn/docs/...
exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
/** 服务端渲染中不支持使用window的模块,在服务器渲染期间用虚拟模块替换有问题的模块
* https://www.gatsbyjs.com/docs/debugging-html-builds/
*/
if (stage === 'build-html' || stage === 'develop-html') {
actions.setWebpackConfig({
module: {
rules: [
{
test: /bad-module/, // 需要处理的模块
use: loaders.null(),
},
],
},
});
}
};
这里说一个实际项目遇到的问题,当时组内基于公司基础组件库bricks
搭建业务组件库,但是bricks
组件库中有个别组件中使用了window
对象,如果将上述配置中/bad-module/
定义为/bricks/
会导致构建失败,因为很多业务组件使用到了bricks
组件,把使用bricks
组件的地方注释掉就可以构建成功。进一步分析报错信息发现只有bricks
中scrollbar
组件使用了window,因此我将/bad-module/
定义为/scrollbar/
,这样只要业务组件中不使用scrollbar
组件都可以构建成功,这也只是个暂时处理方案,后期我们改用dumi
搭建文档完全避免了这一问题。
最后,在终端执行yarn docz:dev
发布
lerna publish
命令可以将分包发布到npm
上,发布完后会自定执行git push --follow-tags --no-verify origin master
将代码推代仓库,提交备注是Publish
,并打上tag
。往scripts
中加入发包脚本命令:
"scripts": {
"docz:dev": "docz dev",
"docz:build": "docz build",
"docz:serve": "docz build && docz serve",
"build": "lerna exec tsc",
"release": "lerna exec tsc && lerna publish"
},
先执行npm login
登录npm
,注意设置的register
一定要是npm
镜像,不然登录不上,如果不是,就需要更改register
npm config set registry http://registry.npmjs.org
然后,执行yarn release
或者npm run publish
发布(npm run publish
是npm
自带的发布命令),但是报了如下错误,这是因为本地代码没有提交
提交代码并推到远程仓库
git add .
git commit -m 'xxx'
git push --set-upstream origin master
另外,发布package
的名字如果是以@
开头的,例如@deepred/core
,npm
默认以为是私人发布,需要使用npm publish --access public
发布。但是lerna publish
不支持该参数,解决方法参考: issues。本文package
名称没有使用@
开头
再次执行yarn release
或者npm run publish
发布,成功发布到npm
仓库
独立发版
上面我们采用的发版方式是lerna
默认的集中模式,所有的package
共用一个version
,这个版本号在lerna.json
中维护。业务组件库中的每一个组件分包都应该是闭环的,一个组件分包的更改不应该将所有的组件分包的版本号都更新,这跟基础组件库是有区别的。
lerna
提供独立发版的方式,该模式下lerna
只会发布有变更的组件分包,让不同的package
拥有自己的版本,只需要将lerna.json
中的version
字段设置为“independent”
我们在原有基础上再添加一个组件分包bre-mutil-list
,提交代码,编译,发布,你会看到只会发布刚才新建的组件分包bre-mutil-list
,之前的bre-todo-list
组件分包不会发布,版本号不会更新
两种发布方式打的tag
也有所不同,下面为示例图:
部署
我们公司并没有将构建好的目录部署到服务器上,而是通过nginx
代理去拉取gitlab
上的静态文件,详细流程如下:
nginx配置:
server {
listen 83;
server_name 10.118.71.232;
location / {
root /opt/web/gitweb/inquiry-bre-component/build-docs;
index index.html index.htm;
if ( $request_uri !~* \. ) {
rewrite ^/([\w/]+).*/? /index.html break;
}
}
}
server {
listen 82;
server_name 10.118.71.232;
location / {
root /opt/web/gitweb/bre-components/build-docs;
index index.html index.htm;
if ( $request_uri !~* \. ) {
rewrite ^/([\w/]+).*/? /index.html break;
}
}
}
server {
listen 81;
server_name 10.118.71.232;
location ~ ^/v {
root /opt/web/gitweb/bricks-docs;
index index.html index.htm;
}
location / {
root /opt/web/gitweb/bricks-docs;
index index.html index.htm;
if ( $request_uri !~* \. ) {
rewrite ^/([\w/]+).*/? /index.html break;
}
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。