0. 起因
老板要搞個Log分析工具谜酒,數(shù)據(jù)存儲選用的是Elasticsearch屡谐,起初想法是做個Kibana的插件,后來覺得依靠Kibana太龐大剑梳,而且后期想要把代碼直接部署在GitHub page上语卤,因此打算做成個獨立的工具羊壹。最終選用了Node.js+React,主要原因是相中了一個React UI庫EUI(Elastic Stack推出的一個開源UI庫,風(fēng)格與Kibana一樣)毡证,畢竟摇邦,不是誰都能寫出漂亮的UI居扒,強如Linux之父Linus都表示“如果我被困在一個與世隔絕的島上竿裂,逃離這座島的唯一辦法是寫出漂亮的UI诈茧,那我估計就老死在島上了”这嚣。
雖然最終由于內(nèi)網(wǎng)權(quán)限問題沒部署上GitHub page姐帚,但基本流程都跑通了唯蝶。既然代碼都寫了痹换,順路寫個總結(jié)吧匙姜。
1. 工程搭建
首先,我們需要安裝Node.js郭计,到Node.js官網(wǎng)隨便下一個安裝包安裝澎迎,或者下載壓縮包解壓縮后手動設(shè)置環(huán)境變量使用灵份。我比較喜歡直接使用壓縮包鸟辅,因為這樣可以隨意在多個版本間切換而且不用額外的工具輔助。例如在Ubuntu下下載壓縮包解壓縮并通過命令export PATH=$NODEJS_ROOT/bin:$PATH
即完成了安裝贸铜。安裝完成后可以通過以下命令查看是否安裝成功:
node --version
npm --version
安裝完成后烤镐,按照以下結(jié)構(gòu)建立一個目錄:
my-app/
package.json
public/
index.html
src/
index.js
其中my-app
可以改成任意你喜歡的名字鹊杖,剩余的部分名字必須與例子給出的一致川尖,這是工程可以構(gòu)建的前提。
然后馍悟,打開package.json
,在其中填入以下內(nèi)容并保存:
{
"name": "eui-demo",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.3"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"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"
]
}
}
上面的內(nèi)容中悼嫉,dependencies
和scripts
這兩個對象必須有,其他的可選擇性添加,即最小要求如下:
{
"dependencies": {
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.3"
},
"scripts": {
"start": "react-scripts start",
}
}
package.json
文件準(zhǔn)備好后執(zhí)行以下命令:
npm install
npm start
執(zhí)行完上面的命令候味,整個工程已經(jīng)構(gòu)建完畢,在瀏覽器中輸入http://localhost:3000/
即可訪問你剛構(gòu)建起來的應(yīng)用,雖然目前這個應(yīng)用什么也沒做也沒有顯示任何內(nèi)容。
當(dāng)然,其實還有另一個更加簡單的方法來構(gòu)建應(yīng)用卵沉,Node.js安裝完成后只需要在終端輸入一條命令即可 拒垃,同樣怜森,my-app
可以換成任意名字:
npx create-react-app my-app/
等待命令執(zhí)行完畢即可。
2. Hello World?
如果工程是我們自己手動一步一步搭建起來的,通過瀏覽器訪問http://localhost:3000/
是什么都不會顯示的胚泌。下一步零蓉,我們就需要讓它顯示點什么。編程嘛,就從Hello World開始吧。
首先妥色,我們打開my-app/public/index.html
吮便,在里面輸入一下信息并保存:
<html>
<head></head>
<body>
<div id='content'>
</div>
</body>
</html>
上面的超文本標(biāo)記代碼很簡單房蝉,如果用瀏覽器打開的話還是什么也看不到咧擂。它只是為后續(xù)的React UI代碼提供了一個掛載點 —— id為content
的一個div
俯逾,在我們的小例子中皇筛,對HTML的編輯就算完了,身下的就全部交給JS代碼了。
接著,我們打開my-app/src/index.js
想虎,輸入一下代碼:
import React from 'react';
import ReactDOM from 'react-dom';
function HelloWorld(props) {
return (<div><p>Hello World!</p></div>)
}
ReactDOM.render(
<HelloWorld />,
document.getElementById('content')
);
編輯完my-app/src/index.js
并保存之后忿薇,我們在my-app
目錄中執(zhí)行npm start
,就可以在瀏覽器中看到如圖1結(jié)果:
在上面的代碼中弊攘,我們做了三件事:
- 第一第二行代碼分別從
react
以及react-dom
這兩個模塊中導(dǎo)入了React
以及ReactDOM
這兩個類。值得注意的是捣域,雖然我們沒有直接看到使用導(dǎo)入的React
竟宋,但是這個導(dǎo)入語句是必須的丘侠,否則編譯就會報錯! - 接下來打肝,定義了一個名為
HelloWorld
的函數(shù)粗梭,這個函數(shù)在React中稱為函數(shù)組件*(Function Components),它與類組件(Class Components)一起組成了React 渲染UI的核心滞乙。這個函數(shù)只有一個參數(shù)props
,這個props
實際上是一個字典兔簇,可以通過它傳遞任意參數(shù)給函數(shù)組件硬耍;函數(shù)返回一個描述如何顯示UI的React元素经柴,雖然看著像超文本標(biāo)記語言(HTML),但是它卻不是。它的名字叫做JSX(JavaScript eXtension)鹃操,它是JavaScript語法的擴展荆隘。 - 第三步赴背,就是將我們定義的函數(shù)組件通過
ReactDOM.render()
函數(shù)渲染出來。ReactDOM.render()
需要一個掛載節(jié)點燃观,在我們的例子中的掛載節(jié)點是前面提到的id為content
的一個div
缆毁,通過ReactDOM.render()
渲染的界面都托管在React DOM中到涂,由React DOM負責(zé)管理以及更新。
3. 實踐
有了Hello World的鋪墊沉御,我們現(xiàn)在可以正式搭建一個簡單點的應(yīng)用了昭灵。我們選用的UI框架是Elastic UI,單然如果你有自己喜歡的其他框架也是可以的硫痰。
假設(shè)我們要搭建一個Markdown編輯器效斑。我們確定它的結(jié)構(gòu)如圖2柱徙,我們需要用EUI實現(xiàn)我們的目標(biāo):
3.1. 搭架子
我們在EUI中找到一個名叫Page
的布局空間护侮,其布局如圖3羊初,正好符合我們的期望:
同樣长赞,我們分別找到導(dǎo)航組件(tree-view)得哆、標(biāo)簽組件(tabs)以及Markdown編輯框組件(markdown-editor),將它們搭積木一樣組合起來栋操,并做些調(diào)整就能得到如圖4所示的界面:
為了方便起見矾芙,我們將Page組件蠕啄、導(dǎo)航欄組件歼跟、標(biāo)簽欄組件、編輯器組件代碼放到獨立JS文件中留瞳,分別命名為page.js, file-nav.js, tabs.js, markdown-editor.js
她倘,具體結(jié)構(gòu)如下:
my-app/
package.json
public/
index.html
src/
file-nav.js
index.js
markdown-editor.js
page.js
tabs.js
他們的代碼分別如下所示:
// file-nav.js
import React from 'react';
import { EuiIcon, EuiTreeView, EuiToken } from '@elastic/eui';
export default () => {
const showAlert = () => {
alert('You squashed a bug!');
};
const items = [
{
label: 'src',
id: 'src',
icon: <EuiIcon type="folderClosed" />,
iconWhenExpanded: <EuiIcon type="folderOpen" />,
isExpanded: true,
children: [
{
label: 'index.md',
id: 'item_a',
icon: <EuiIcon type="document" />,
},
{
label: 'level2 folder',
id: 'item_b',
icon: <EuiIcon type="folderOpen" />,
iconWhenExpanded: <EuiIcon type="folderOpen" />,
children: [
{
label: 'monosodium_glutammate.md',
id: 'item_cloud',
icon: <EuiIcon type="document" />,
},
{
label: "cobalt.md",
id: 'item_bug',
icon: <EuiIcon type="document" />,
callback: showAlert,
},
],
},
{
label: 'xxxxx folder',
id: 'item_c',
icon: <EuiIcon type="folderOpen" />,
iconWhenExpanded: <EuiIcon type="folderOpen" />,
children: [
{
label: 'Another Cloud.md',
id: 'item_cloud2',
icon: <EuiIcon type="document" />,
},
{
label:
'elastic_link.md',
id: 'item_bug2',
icon: <EuiIcon type="document" />,
callback: showAlert,
},
],
},
],
},
{
label: 'othter',
id: 'src2',
icon: <EuiIcon type="folderClosed" />,
iconWhenExpanded: <EuiIcon type="folderOpen" />,
isExpanded: true,
},
];
return (
<div style={{ width: '20rem' }}>
<EuiTreeView items={items} aria-label="eui-markdown-editor" />
</div>
);
};
// markdown-editor.js
import React, { useCallback, useState } from 'react';
import {
EuiMarkdownEditor,
EuiSpacer,
EuiCodeBlock,
EuiButtonToggle,
} from '@elastic/eui';
const initialContent = `## Hello world!
Basic "github flavored" markdown will work as you'd expect.
The editor also ships with some built in plugins. For example it can handle checkboxes. Notice how they toggle state even in the preview mode.
- [ ] Checkboxes
- [x] Can be filled
- [ ] Or empty
`;
const dropHandlers = [
{
supportedFiles: ['.jpg', '.jpeg'],
accepts: itemType => itemType === 'image/jpeg',
getFormattingForItem: item => {
// fake an upload
return new Promise(resolve => {
setTimeout(() => {
const url = URL.createObjectURL(item);
resolve({
text: `![${item.name}](${url})`,
config: { block: true },
});
}, 1000);
});
},
},
];
export default () => {
const [value, setValue] = useState(initialContent);
const [messages, setMessages] = useState([]);
const [ast, setAst] = useState(null);
const [isAstShowing, setIsAstShowing] = useState(false);
const onParse = useCallback((err, { messages, ast }) => {
setMessages(err ? [err] : messages);
setAst(JSON.stringify(ast, null, 2));
}, []);
return (
<>
<EuiMarkdownEditor
aria-label="EUI markdown editor demo"
value={value}
onChange={setValue}
height={400}
onParse={onParse}
errors={messages}
dropHandlers={dropHandlers}
/>
{isAstShowing && <EuiCodeBlock language="json">{ast}</EuiCodeBlock>}
</>
);
};
// page.js
import React from 'react';
import {
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageContentHeader,
EuiPageContentHeaderSection,
EuiPageHeader,
EuiPageSideBar,
} from '@elastic/eui';
import FileNav from './file-nav';
import MarkdownEditor from './markdown-editor';
import Tabs from './tabs';
export default () => (
<EuiPage>
<EuiPageSideBar>
<FileNav />
</EuiPageSideBar>
<EuiPageBody component="div">
<EuiPageHeader>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentHeader>
<EuiPageContentHeaderSection>
<Tabs />
</EuiPageContentHeaderSection>
<EuiPageContentHeaderSection>
</EuiPageContentHeaderSection>
</EuiPageContentHeader>
<EuiPageContentBody>
<MarkdownEditor />
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
// tabs.js
import React, { useState, Fragment } from 'react';
import {
EuiIcon,
EuiTabs,
EuiTab,
EuiSpacer,
} from '@elastic/eui';
const tabs = [
{
id: 'cobalt',
name: (<span>
cobalt.md <EuiIcon type="cross" />
</span>),
disabled: false,
},
{
id: 'dextrose',
name: (<span>
dextrose.md <EuiIcon type="cross" />
</span>),
disabled: false,
},
{
id: 'hydrogen',
name: (
<span>
Hydrogen <EuiIcon type="cross" />
</span>
),
disabled: false,
},
{
id: 'monosodium_glutammate',
name: (<span>
monosodium_glutammate.md <EuiIcon type="cross" />
</span>),
disabled: false,
},
{
id: 'elastic_link',
name: (<span>
elastic_link.md <span onClick={e =>{alert('close me?')}}><EuiIcon type="cross" /></span>
</span>),
disabled: false,
},
];
export default () => {
const [selectedTabId, setSelectedTabId] = useState('cobalt');
const onSelectedTabChanged = id => {
setSelectedTabId(id);
};
const renderTabs = () => {
return tabs.map((tab, index) => (
<EuiTab
{...(tab.href && { href: tab.href, target: '_blank' })}
onClick={() => onSelectedTabChanged(tab.id)}
isSelected={tab.id === selectedTabId}
disabled={tab.disabled}
key={index}>
{tab.name}
</EuiTab>
));
};
return (
<Fragment>
<EuiTabs size="s">{renderTabs()}</EuiTabs>
</Fragment>
);
};
// index.js
import React, {useState} from 'react';
import ReactDOM from 'react-dom';
import "@elastic/eui/dist/eui_theme_light.css";
import Editor from './page';
ReactDOM.render(
<Editor />,
document.getElementById('content')
);
好了,到這里跃巡,我們的架子已經(jīng)搭起來了牧愁,我們接下來就需要為他們注入靈魂猪半,讓各個組件之間互動起來磨确。例如我們希望點擊不同的文件標(biāo)簽,編輯框顯示的是不同的文件內(nèi)容安接。
3.2. 關(guān)聯(lián)UI
點擊不同的文件標(biāo)簽讓編輯框顯示不同內(nèi)容主要涉及的就是UI如何更新自己的狀態(tài)或者UI如何通知別的UI更新其狀態(tài)。在React中歇式,UI被前面提到的類組件(Class Component)以及函數(shù)組件(Function Component)分為一個個獨立材失、可復(fù)用的模塊。類組件和函數(shù)組件的形式分別如下:
// Class components
class ClazzComponent extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
// Function Components
function FuncComponent(props) {
return <h1>Hello, {props.name}</h1>;
}
從顯示效果上看熊响,它們是一完全一樣的诗赌。類組件相對復(fù)雜但是擁有更多的特性铭若,例如類組件就有一個state
字典,通過setState
方法類組件可以更新state
的內(nèi)容瞳腌,一旦state
改變了嫂侍,那么直接或者間接使用state
的React元素(React Elements)就會被更新吵冒。例如我們把上面的ClazzComponent
做下修改:
// Class components
export default class ClazzComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
what: 'World',
};
}
render() {
return <h1 onClick={e => this.setState({what: this.state.what + ' World'})}>Hello, {this.state.what}</h1>;
}
}
當(dāng)我們點擊界面上的Hello, World
痹栖,它就會變成Hello, World World
瞭空,Hello, World World World
等咆畏。函數(shù)組件相比于類組件最大的區(qū)別就是很多類組件有的特性它沒有,例如它就沒有state
這個成員以及setState
方法溺健,因為理論上它是一個函數(shù)(至于說在JavaScript中“萬物皆對象”鞭缭,方法其實也是個對象魏颓,那不是本文關(guān)注的重點)甸饱。但是,在React中通過一些鉤子函數(shù)偷遗,就能讓函數(shù)組件具有類組件的一些特性氏豌,例如“state”箩溃。
讓stateless的方法函數(shù)變得state瞭吃,可以通過useState
這個鉤子函數(shù)。例如涣旨,我們把上面的示例也做下修改:
// Function Components
import React, {useState} from 'react';
function FuncComponent(props) {
const [name, setName] = useState('World');
return <h1 onClick={e => setName(name + ' World')}>Hello, {name}</h1>;
}
經(jīng)過修改歪架,函數(shù)組價的的表現(xiàn)也和類組件一樣了。首先霹陡,我們從React模塊中導(dǎo)入useState
這個鉤子和蚪,然后我們在函數(shù)組件中通過它獲得了一個廚師長name
已經(jīng)更新方法setName
,這里name
和setName
可以是任意的名字烹棉。
知道了如何更新組件的狀態(tài),接下來我們就能著手進行我們的Markdown編輯器的編碼了浆洗。篇幅有限催束,我們搞得簡單點。主要分以下三步:
- 首先伏社,我們定義一個回調(diào)函數(shù)抠刺,將這個回調(diào)函數(shù)注冊到
tabs.js
的函數(shù)組件中; - 然后摘昌,當(dāng)
tabs
的標(biāo)簽有改變的時候速妖,tabs
調(diào)用我們注冊的回調(diào)函數(shù),并將被選中的tab的id傳給我們回調(diào)函數(shù)聪黎,這樣我們就能知道當(dāng)前那個標(biāo)簽被選中了罕容; - 最后,在回調(diào)函數(shù)中稿饰,我們通過判斷id知道用戶希望顯示的內(nèi)容锦秒,通過
useState
導(dǎo)出的setContent
方法通知Markdown編輯器控件更改其顯示的內(nèi)容。
具體代碼如下所示湘纵,我們只更改了page.js, markdown-editor.j, tabs.js
脂崔,其他代碼保持不變:
// page.js
import React, {useState} from 'react';
import {
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageContentHeader,
EuiPageContentHeaderSection,
EuiPageHeader,
EuiPageSideBar,
} from '@elastic/eui';
import FileNav from './file-nav';
import MarkdownEditor from './markdown-editor';
import Tabs from './tabs';
import tabs from './tabs';
const tab1Content = `## Hello world!
Basic "github flavored" markdown will work as you'd expect.
The editor also ships with some built in plugins. For example it can handle checkboxes. Notice how they toggle state even in the preview mode.
- [ ] Checkboxes
- [x] Can be filled
- [ ] Or empty
`;
const tab2Content = `## I am tab two, name , not Tattoo!
#### I am tab two, not Tattoo!
`;
const tab3Content = `
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### \`npm start\`
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
`;
export default () => {
const [content, setContent] = useState('init content')
let tabSelected = (tabId) => {
if (tabId == 'cobalt')
setContent(tab1Content);
else if (tabId == 'dextrose')
setContent(tab2Content);
else if (tabId == 'hydrogen')
setContent(tab3Content);
}
return (
<EuiPage>
<EuiPageSideBar>
<FileNav />
</EuiPageSideBar>
<EuiPageBody component="div">
<EuiPageHeader>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentHeader>
<EuiPageContentHeaderSection>
<Tabs onTabSelected={tabSelected}/>
</EuiPageContentHeaderSection>
<EuiPageContentHeaderSection>
</EuiPageContentHeaderSection>
</EuiPageContentHeader>
<EuiPageContentBody>
<MarkdownEditor content={content}/>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>);
};
//markdown-editor.js
import React, { useCallback, useState } from 'react';
import {
EuiMarkdownEditor,
EuiSpacer,
EuiCodeBlock,
EuiButtonToggle,
} from '@elastic/eui';
const dropHandlers = [
{
supportedFiles: ['.jpg', '.jpeg'],
accepts: itemType => itemType === 'image/jpeg',
getFormattingForItem: item => {
// fake an upload
return new Promise(resolve => {
setTimeout(() => {
const url = URL.createObjectURL(item);
resolve({
text: `![${item.name}](${url})`,
config: { block: true },
});
}, 1000);
});
},
},
];
export default (props) => {
const [value, setValue] = useState(props.content);
const [messages, setMessages] = useState([]);
const [ast, setAst] = useState(null);
const [isAstShowing, setIsAstShowing] = useState(false);
const onParse = useCallback((err, { messages, ast }) => {
setMessages(err ? [err] : messages);
setAst(JSON.stringify(ast, null, 2));
}, []);
return (
<>
<EuiMarkdownEditor
aria-label="EUI markdown editor demo"
value={props.content}
onChange={setValue}
height={400}
onParse={onParse}
errors={messages}
dropHandlers={dropHandlers}
/>
{isAstShowing && <EuiCodeBlock language="json">{ast}</EuiCodeBlock>}
</>
);
};
// tabs.js
import React, { useState, Fragment } from 'react';
import {
EuiIcon,
EuiTabs,
EuiTab,
EuiSpacer,
} from '@elastic/eui';
const tabs = [
{
id: 'cobalt',
name: (<span>
cobalt.md <EuiIcon type="cross" />
</span>),
disabled: false,
},
{
id: 'dextrose',
name: (<span>
dextrose.md <EuiIcon type="cross" />
</span>),
disabled: false,
},
{
id: 'hydrogen',
name: (
<span>
Hydrogen <EuiIcon type="cross" />
</span>
),
disabled: false,
},
{
id: 'monosodium_glutammate',
name: (<span>
monosodium_glutammate.md <EuiIcon type="cross" />
</span>),
disabled: false,
},
{
id: 'elastic_link',
name: (<span>
elastic_link.md <span onClick={e =>{alert('close me?')}}><EuiIcon type="cross" /></span>
</span>),
disabled: false,
},
];
export default (props) => {
const [selectedTabId, setSelectedTabId] = useState('cobalt');
const onSelectedTabChanged = id => {
setSelectedTabId(id);
if (props && props.onTabSelected)
props.onTabSelected(id);
};
const renderTabs = () => {
return tabs.map((tab, index) => (
<EuiTab
{...(tab.href && { href: tab.href, target: '_blank' })}
onClick={() => onSelectedTabChanged(tab.id)}
isSelected={tab.id === selectedTabId}
disabled={tab.disabled}
key={index}>
{tab.name}
</EuiTab>
));
};
return (
<Fragment>
<EuiTabs size="s">{renderTabs()}</EuiTabs>
</Fragment>
);
};
這樣滤淳,我們點擊不同的標(biāo)簽就能看到不同的內(nèi)容了梧喷。
4. 托管
想要將我們的應(yīng)用托管在GitHub,需要做一下幾步:
- 執(zhí)行
npm build
命令進行編譯; - 然后在將遠程倉庫中的
gh-pages
拉取到本地铺敌; - 清空
gh-pages
分支汇歹; - 對編譯出的文件做些調(diào)整,因為編譯的路徑如果不做修改很多文件提示找不到偿凭;
- 將修改后的文件復(fù)制到
gh-pages
目錄产弹; - 提交。
整個過程的命令如下(Linux下)弯囊,唯一需要修改的就是倉庫地址:
# 一下內(nèi)容位于 my-app/Makefile
publish_github_pages:
rm -rf ./build
rm -rf ./gh-pages
npm run build
git clone --depth=1 https://github.com/SunnyZhou-1024/eui-markdown-editor.git --branch gh-pages ./gh-pages 2>&1 > /dev/null
rm -rf ./gh-pages/*
cp -R ./build/* ./gh-pages/
sed -i -e "s/\/static/.\/static/g" ./gh-pages/index.html
sed -i -e "s/\/favi/.\/favi/g" ./gh-pages/index.html
sed -i -e "s/\/logo/.\/logo/g" ./gh-pages/index.html
sed -i -e "s/\/mani/.\/mani/g" ./gh-pages/index.html
git -C ./gh-pages add --all
git -C ./gh-pages commit --amend --no-edit
git -C ./gh-pages push --force origin gh-pages
5. 總結(jié)
由于篇幅所限痰哨,在本文例子中,只選取了一些關(guān)鍵的點來講解匾嘱,主要講解的是從如何搭建一個React App以及React 元素如何更新斤斧,到將其部署到GitHub的整個流程。具體代碼邏輯可能和一個真正的編輯器有很大出入霎烙,并且非常不完善撬讽,例如顯示的內(nèi)容應(yīng)該來自文件而不是硬編碼。
本文的目的并不是講解如何寫出漂亮的UI悬垃,這不是我擅長的游昼;也不是深入的講解React,這一部分我覺得React官網(wǎng)的文檔已經(jīng)非常完善了尝蠕;更不是介紹如何使用EUI烘豌。本文的只是想以EUI為例,介紹如何通過現(xiàn)有的UI框架看彼、工具鏈構(gòu)建起一個可用扇谣,也還看得過去的網(wǎng)頁應(yīng)用。
本文例子代碼位于個人GitHub倉庫:https://github.com/zmychou/eui-markdown-editor
6. References
[1] https://reactjs.org/docs/getting-started.html
[2] https://create-react-app.dev/docs/getting-started/
[3] https://elastic.github.io/eui/#/