更新说明:对文章目录排版做了调整。
更新时间:2022-05-04

第一章 本周导学

1-1 本章介绍

  • 组件平台
  • 组件预览
  • 组件README

第二章 组件平台架构设计和技术选型

2-1 组件平台架构设计
点击查看【processon】
2-2 组件平台技术选型和框架搭建
umi-component-test改项目代码提交至:https://github.com/liugezhou/umi-component-test

  1. 初始化UmiJS
  • mkdir umi-component-dev
  • cd umi-component-dev
  • yarn create @umijs/umi-app
  • yarn install
  • yarn start
  1. 新建页面

npx umi g page detail

  1. 配置路由

.umirc.ts新建页面路由
routes: [
{ path:‘/’, component:‘@/pages/index’ },
{ path:‘/nice’, component:‘@/pages/detail’ },
],
yarn start启动项目后,访问 http://192.168.1.3:8000/nice即可看到最新的页面

  1. 安装 Ant Design

yarn add antd

  1. 使用
1
2
3
import { Button } from "antd";

<Button type="primary">Button</Button>

第三章 组件平台基础功能开发

3-1 umi 项目全局入口文件+国际化开发

  • 运行时配置:约定src/app.tsx为运行时配置,该配置文件下可以做一些全局性的操作
  • 国际化:文档-插件-@umijs/plugin-locale
    • **mkdir:**src/locales/zh-CN.ts | en-US.ts,配置WELCOME_TO_UMI_WORLD字段内容
    • .umirc.ts中配置国际化[代码如下所示]
    • src/app.tsx中引入国际化[代码如下所示]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// src/umirc.ts
locale: {
default: 'zh-CN',
antd: false,
title: false,
baseNavigator: true,
baseSeparator: '-',
},

// src/app.tsx
import 'antd/dist/antd.css';
import { getLocale,setLocale } from 'umi';
import qs from 'qs';
const { search } = window.location;
const { locale = 'zh-CN' } = qs.parse(search, { ignoreQueryPrefix: true });
setLocale(locale,false)


// src/pages/index.tsx
import { useIntl } from 'umi';
export default function IndexPage() {
const init = useIntl()
const msg = init.formatMessage({
id:'WELCOME_TO_UMI_WORLD',
defaultMessage:'你好,牛逼的前端架构师!'
},{
name:'liugezhou'
})
console.log(msg)
}

最后:yarn start启动文件,访问查看控制台:http://localhost:8000/?locale=en-US

3-2 组件平台功能展示 + 页头页脚开发

umijs支持layout引入,于是我们在开发页头页脚的时候,页面页头与页脚是在各个页面都存在的,于是我们可以将页面不同的地方以layout的形式注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// src/layouts/index.js

import styles from './index.less'

function BasicLayout(props){
return (
<div className={styles.normal}>
<div className={styles.title}>{slogan}</div>
{props.children}
<div className={styles.footer}>{copyright}</div>
</div>
)
}
export default BasicLayout

//src/layouts/index.less
@import '~antd/dist/antd';

.normal{
text-align:center
}

.title{
font-size:15px;
font-weight: normal;
background: @primary-color;
padding: 10px 0;
color: white;
margin:0;
}

.footer {
font-size: 12px;
font-weight: normal;
background: @primary-color;
padding: 10px 0;
color: white;
margin: 0;
}

// .umijsrc.js
import { defineConfig } from 'umi';

export default defineConfig({
nodeModulesTransform: {
type: 'none',
},
routes: [
{
path: '/',
component: '@/layouts/index',
routes: [
{
path: '/',
component: '@/pages/index'
},
{
path: '/detail',
component: '@/pages/detail'
}
]
}
],
fastRefresh: {},
locale: {
default: 'zh-CN',
antd: false,
title: false,
baseNavigator: true,
baseSeparator: '-',
}
});

3-3 组件平台动态配置 API 开发

我们的页面与页脚内容需要从接口获取,因此,本节内容为在 cloudscope-cli-server服务中去编写接口代码。
本周相关代码提交至:cloudscope-cli/server/lesson33

3-3-1 添加路由

1
2
3
4
5
6
7
8
9
10
// app/route.js
'use strict';

module.exports = app => {
const { router, controller } = app;

router.resources('componentSite', '/api/v1/componentSite', controller.v1.componentSite);

};

3-3-2 API编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// app/controller/v1/ComponentSite.js
'use strict'
const Controller = require('egg').Controller;
const mongo = require('../../utils/mongo')

class ComponentSiteController extends Controller{
asynx index(){
const { ctx } = this;
const data = await mongo.query('componentSite);
if(data && data.length>0){
ctx.body = data[0]
}else{
ctx.body = []
}
}
}
  • 需要在mongoDB中新建集合 componentSite,添加页面页脚数据
  • 启动项目,浏览器输入地址,测试访问结果

3-4 前端动态配置 API 接入

代码无分支提交至:umi-component-test

上一节我们开发完毕api接口后,在前端请求该接口

  • 首先需要install axios
  • 接着需要封装request请求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// src/layouts/util/reques.js

import axios from 'axios';
const BASE_URl = 'http://liugezhou.com:7001'
const service = axios.create({
baseURl:BASE_URL,
timeout:5000
})

service.interceptor.request.use({
config => {
return config;
}
error => {
return new Promise.reject(error)
}

service.interceptor.response.use({
response => {
return new response.data;
}
error => {
return new Promise.reject(error)
}
})

export default request;

//src/layouts/utils/service.js

import request from './request'

export function getSiteInfo(){
return request({
url:'/api/v1/componentSite'
})
}

export default {}
  • 最后,在首页中去调用该接口,且赋值为页头与页脚
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// src/layouts/index.js

import styles from './index.less';
import {getSiteInfo } from '../utils/service';
import { useState, useEffect } from 'react'';

function BasicLayout(props){
const [init,setInit] = useState(false);
const [slogan,setSlogan] = useState('');
const [copyright,setCopyright ] = useState('');

useEffect( () =>{
if (!init) {
setInit(true);
getSiteInfo().then(data => {
setSlogan(data.slogan);
setCopyright(data.copyright);
}).catch(err => {
console.log(err);
setSlogan('');
setCopyright('');
});
}
},[init])

return (
<div className={styles.normal}>
<div className={styles.title}>{slogan}</div>
{props.children}
<div className={styles.footer}>{copyright}</div>
</div>
)
}
export default BasiLayout;

第四章 组件平台组件列表页面开发

4-1 组件列表 API 开发

本周相关代码提交至:cloudscope-cli/server/lesson33

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// app/controller/v1/Components.js

async index(){
const { ctx, app } = this;
const { name } = ctx.query;
const andWhere = name ? `AND c.name LIKE '%${name}%'` : '';
const sql = `SELECT c.id, c.name, c.classname, c.description, c.npm_name, c.npm_version, c.git_type, c.git_remote, c.git_owner, c.git_login, c.create_dt, c.update_dt, v.version, v.build_path, v.example_path, v.example_list
FROM component AS c
LEFT JOIN version AS v ON c.id = v.component_id
WHERE c.status = 1 AND v.status = 1 ${andWhere}
ORDER BY c.create_dt, v.version DESC`;
const result = await app.mysql.query(sql);
const components = [];
result.forEach(component => {
let hasComponent = components.find(item => item.id === component.id);
if (!hasComponent) {
hasComponent = {
...component,
};
delete hasComponent.version;
delete hasComponent.build_path;
delete hasComponent.example_path;
delete hasComponent.example_list;
hasComponent.versions = [];
components.push(hasComponent);
hasComponent.versions.push({
version: component.version,
build_path: component.build_path,
example_path: component.example_path,
example_list: component.example_list,
});
} else {
hasComponent.versions.push({
version: component.version,
build_path: component.build_path,
example_path: component.example_path,
example_list: component.example_list,
});
}
});
ctx.body = components;
}

4-2 测试组件数据创建

  • 上一节我们获取的组件列表数据为一条,本节首先再去创建几条测试数据。

  • 在此之前,先去更改之前的组件模版 cloudscope-cli-components,packahe.json的配置:publishConfig为access:true 和 build:demo

    • 外层package.json 增加一个版本号
    • template内package.json 配置 publishConfig
    • template内package.json 配置‘build:demo’:“npm install && npm run build && cd examples && npm install && npm run build”
    • npm publish
    • mongodb数据库中将版本号升级
  • mkdir cloudscope-cli_component-test3

  • cd cloudscope-cli_component-test3

  • cloudscope-cli init -tp /Users/liumingzhou/Documents/imoocCourse/Web前端架构师/cloudscope-cli/commands/init/ 【这里需要注意的是,由于我本读安装且默认设置了node的版本为14,而之前开发的本地脚手架为12.16,因此支持以上代码需要更换node版本】

    • @cloudscope-cli/components-test2
    • 1.0.0
    • 通用的Vue3组件库模版
    • components test2
  • code .

  • npm run build:demo

  • cloudscope-cli publish -tp /Users/liumingzhou/Documents/imoocCourse/Web前端架构师/cloudscope-cli/commands/publish/ [不加prod属性,不用关心OSS与NPM发布]

检查:

  • git remote -v 【查看远程仓库信息】

  • 查看mysql数据库插入信息

  • 如果添加了 --prod属性

  • 查看npm发布组件信息

  • 查看云OSS上传信息

4-3 组件列表页面开发
4-4 组件卡片面板开发
4-5 搜索框组件开发+模糊搜索API开发

这三节内容为组件首页列表的umi项目代码开发,包括布局、请求、点击事件等功能,代码分类为:国际化配置、工具类、业务代码,其中核心内容为业务代码,主要是使用UI库ant-design-react和umi以及react的一些用法。

  • 国际化配置
1
2
3
4
5
6
7
8
9
10
11
12
// src/locales/en-US.js
export default {
WELCOME_TO_UMI_WORLD: "{name}, welcome to umi's world",
INDEX_SEARCH: 'Search',
INDEX_PLACEHOLDER: 'Component Name',
};
// src/locales/zh-CN.js
export default {
WELCOME_TO_UMI_WORLD: '{name},欢迎光临umi的世界',
INDEX_SEARCH: '搜索',
INDEX_PLACEHOLDER: '组件名称',
};
  • 工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// src/utils/index.js

export function formatName(name) {
let _name = name;
if (_name && _name.startsWith('@') && _name.indexOf('/') > 0) {
// @cloudscope-cli/component-test ->
// @cloudscope-cli_component-test
const nameArray = _name.split('/');
_name = nameArray.join('_').replace('@', '');
}
return _name;
}

export default {};
// src/utils/request.js
import axios from 'axios'

const BASE_URL = 'http://liugezhou.com:7001'

const service = axios.create({
baseURL: BASE_URL,
timeout: 5000
})

service.interceptors.request.use(
config => {
return config
},
error => {
return Promise.reject(error)
}
)

service.interceptors.response.use(
response => {
return response.data
},
error => {
return Promise.reject(error)
}
);

export default service;

//src/utils/service.js
import request from './request';

export function getSiteInfo() {
return request({
url: '/api/v1/componentSite',
});
}

export function getComponentList(params) {
return request({
url: '/api/v1/components',
params,
});
}

export default {};

//src/utils/git.js
import { formatName } from '../utils';

export function getGitUrl(item) {
let name = item.classname;
if (name.startsWith('@') && name.indexOf('/') > 0) {
const nameArray = name.split('/');
name = nameArray.join('_').replace('@', '');
}
return `https://${item.git_type}.com/${item.git_login}/${name}`;
}

/**
* 获取组件预览链接
* @param name 组件名称
* @param version 组件版本
* @param path 组件预览文件路径
* @param file 组件预览文件名称
*/
export function getPreviewUrl({ name, version, path, file }) {
// 上传至OSS再来配置
return `https:// /${formatName(name)}@${version}/${path}/${file}`;
}

export default {};
  • 业务代码 ✨✨✨✨✨
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// src/pages/index.jsx
import styles from './index.less';
import { Divider, Row, Col, Card, Input } from 'antd';
import { EditOutlined, EllipsisOutlined, EyeOutlined } from '@ant-design/icons';
import { useIntl,history } from 'umi';
import { getGitUrl,getPreviewUrl } from "../utils/git";
import { getComponentList } from '../utils/service';
import { useState, useEffect } from "react";

const { Meta } = Card;
const { Search } = Input;

export default function IndexPage() {
const [init, setInit] = useState(false);
const [list, setList] = useState([]);
const [name, setName] = useState(null);
const intl = useIntl();

useEffect(() => {
if (!init) {
setInit(true);
getComponentList({ name }).then(data => {
console.log(data);
setList(data);
}).catch(err => {
console.error(err);
setList([]);
});
}
}, [init, name]);

function getAvatar(item) {
if (item.git_type === 'gitee') {
return {
img: 'https://gitee.com/static/images/logo-black.svg',
style: { height: '20px', cursor: 'pointer' },
};
} else {
return {
img: 'https://www.youbaobao.xyz/arch/img/github.jpeg',
style: { height: '40px', cursor: 'pointer' },
};
}
}
function getLastPreviewUrl(item) {
const lastVersion = item.versions[0];
const examplePath = lastVersion.example_path;
const exampleFile = JSON.parse(lastVersion.example_list)[0];
return getPreviewUrl({
name: item.classname,
version: lastVersion.version,
path: examplePath,
file: exampleFile,
});
}

return (
<div className={styles.container}>
<div className={styles.search}>
<Search
style={{ width: '50%' }}
placeholder={intl.formatMessage({ id: 'INDEX_PLACEHOLDER' })}
allowClear
enterButton={intl.formatMessage({ id: 'INDEX_SEARCH' })}
size='large'
onSearch={value => {
setName(value);
setInit(false);
}}
/>
</div>
<Divider orientation='right'>共{list.length}个组件</Divider>
<Row gutter={16} justify='space-around'>
{
list.map(item => (
<Col span={6} key={item.id}>
<Card
actions={[
<EyeOutlined key='setting' onClick={() => {
window.open(getLastPreviewUrl(item));
}}
/>,
<EditOutlined key='edit' onClick={() => {
history.push({
pathname: '/detail',
query: {
id: item.id,
},
});
}}
/>,
<EllipsisOutlined key='ellipsis' />,
]}
>
<Meta
avatar={<img
alt={item.name}
src={getAvatar(item).img}
style={getAvatar(item).style}
onClick={() => window.open(getGitUrl(item))}
/>}
title={item.name}
description={item.description}
/>
</Card>
</Col>
))
}
</Row>
</div>
);
}
// src/pages/index.less
.containter {
padding: 20px;
}

.search {
padding:30px;
}

第五章 组件平台组件详情页面开发

5-1 组件详情获取API开发

首先在umi-component-dev项目下写details页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// src/pages/details.jsx
import { useState, useEffect } from 'react';
import styles from './detail.css';
import { getComponentItem } from "../utils/service";

export default function Page(props) {
const [init, setInit] = useState(false)
const [data, setData] = useState(null)

useEffect(() => {
const query = props.location.query
if(!init && query.id){
setInit(true);
getComponentItem(query.id).then( data=> {
console.log(data)
})
}
})
return (
<div>
</div>
);
}
// src/utils/service.js
export function getComponentItem(id) {
return request({
url: `/api/v1/components/${id}`,
});
}

接着,重点就是去开发接口获取组件的具体信息了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// app/controller/v1/Components.js

// api/v1/components/:id
async show() {
const { ctx, app } = this;
const id = ctx.params.id
const results = await app.mysql.select('component', {
where: { id }
})
if (results && results.length > 0) {
const component = results[0]
component.versions = await app.mysql.select('version', {
where: { component_id: id },
orders: [['version', 'desc']]
})
// gitee GET https://gitee.com/api/v5/repos/{owner}/{repo}/contents(/{path})
// git GET https://api.github.com/repos/{owner}/{repo}/{path})
let readmeUrl;
let _name = component.classname;
if (_name && _name.startsWith('@') && _name.indexOf('/') > 0) {
const nameArray = _name.split('/');
_name = nameArray.join('_').replace('@', '');
}
if (component.git_type === 'gitee') {
readmeUrl = `https://gitee.com/api/v5/repos/${component.git_login}/${_name}/contents/README.md`
} else {
readmeUrl = `https://api.github.com/repos/${component.git_login}/${_name}/README.md`
}
const readme = await axios.get(readmeUrl);
let content = readme.data && readme.data.content;
if (content) {
content = decode(content)
if (content) {
component.readme = content
}
}
ctx.body = component
} else {
ctx.body = {}
}
}

5-2 组件基本信息样式开发
5-3 组件代码+预览样式开发
5-4 组件安装样式和复制命令功能开发
5-5 组件多预览文件上传工作
5-6 组件多预览文件上传开发