原文:https://dev.to/dkb868/build-a-decentralized-todo-list-with-react-and-blockstack-59cm
沒翻譯完,請看 http://www.reibang.com/p/078c1dae4397
在線例子:
https://rebase-todolist.netlify.com
npm run eject
在本教程中,你將學(xué)習(xí)使用Blockstack和React構(gòu)建去中心化的Todolist拐辽。Blockstack是一個平臺梨州,它使構(gòu)建Dapp變得非常容易,與傳統(tǒng)的auth/storage方法相比详囤,使用Blockstack身份驗證和存儲構(gòu)建一個簡單的App更快、更安全。
Blockstack的去中心化方法
像Google和Facebook這樣的大公司都有中心化的數(shù)據(jù)庫巡球,它們可以控制你的數(shù)據(jù),并且可以對數(shù)據(jù)做任何他們想做的事情邓嘹。
Blockstack App允許用戶完全控制自己的數(shù)據(jù)酣栈。沒有用戶的允許,任何人都不能訪問用戶的數(shù)據(jù)汹押。用戶數(shù)據(jù)被加密并存儲在私人的“數(shù)據(jù)鎖”中矿筝,用戶可以給App權(quán)限來讀取或?qū)懭霐?shù)據(jù)到他們的存儲中。
在我們的Todolist App中棚贾,這意味著App開發(fā)人員永遠不會知道你的Todolist上有什么窖维。
The App
我們的Todolist將非常簡單,這樣我們就可以專注于學(xué)習(xí)Blockstack是如何工作的妙痹。
完成后的App是這個樣子:
Todolist App
demo 網(wǎng)址: https://blockstack-todo-list.netlify.com/
Github 網(wǎng)址: https://github.com/dkb868/secure-todo-list
The Setup
首先铸史,我們將設(shè)置環(huán)境,安裝node.js
的最新版本怯伊。
React
我們使用 create-react-app
, 在你命令行窗口輸入 npx create-react-app secure-todo-list
創(chuàng)建新的項目琳轿。
大約一分鐘后,就應(yīng)該完成了耿芹。
控制臺
進入項目 cd secure-todo-list
然后輸入 npm start
啟動項目
Then open up the project folder in your coding editor and let's do some cleanup. Delete the following files:
App.css
App.test.js
index.css
logo.svg
然后打開App.js
崭篡,將內(nèi)容替換為:
import React from "react"
class App extends React.Component {
render() {
return <div>Nice Meme</div>
}
}
export default App
并更新 index.js
import React from "react"
import ReactDOM from "react-dom"
import App from "./App"
import * as serviceWorker from "./serviceWorker"
ReactDOM.render(<App />, document.getElementById("root"))
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister()
Prettier 插件
如果你不使用Prettier
,我強烈推薦它猩系。它使你的代碼更干凈媚送。你可以通過尋找Prettier
插件將其添加到編輯器中。
將.prettierrc
文件添加到項目根目錄(secure-todo-list/)寇甸,內(nèi)容為空塘偎,這將提供默認設(shè)置疗涉。
{}
Semantic UI
我們將使用Semantic UI
,一個CSS庫吟秩,給我們的應(yīng)用程序一些樣式咱扣。
將這個url (https://cdnjs.cloudflare.com/ajax/libs/semanticui/2.4.1 /semantic.min.css)復(fù)制到你的public/index.html
中,方法是將這一行添加到html文件的頭部涵防。
<link
rel="stylesheet"
/>
現(xiàn)在闹伪,你應(yīng)該已經(jīng)完成了一個非常漂亮的、極簡主義的網(wǎng)站壮池。
Blockstack 賬戶
你需要一個Blockstack帳戶 以便你可以登錄和使用App偏瓤。你可以通過https://blockstack.org/并從菜單中選擇Create ID獲取一個賬戶。
Blockstack
一個簡單的 Todo List
首先椰憋,我們用React
構(gòu)建一個簡單的Todolist厅克,其中不包含任何Blockstack技術(shù)。每當(dāng)頁面刷新時橙依, App狀態(tài)將丟失证舟,這將使它更容易看到Blockstack的作用。
初始化狀態(tài)
我們添加一些狀態(tài)到我們的App窗骑。在App.js的render函數(shù)上面添加內(nèi)容:
state = {
todos: [
{
id: 1,
title: "Wash the dishes",
done: false,
},
{
id: 2,
title: "Clean my room",
done: false,
},
],
}
現(xiàn)在我們的應(yīng)用程序跟蹤todos女责,它有三個屬性:
- id:todo的唯一標(biāo)識符
- title: 任務(wù)的名稱
- done:是否已完成這個任務(wù)
顯示Todos
現(xiàn)在我們有了一些todo,讓我們在頁面上顯示它們创译。
改變render
方法抵知,代碼如下:
render() {
return (
<div style={{ padding: "30px 0" }}
className="ui text container center aligned">
<h2>My Todos</h2>
<div className="ui grid">
<div className="row centered">
<div className="column twelve wide">
<div className="grouped fields">
{this.state.todos
.filter(todo => !todo.done)
.map(todo => (
<div key={todo.id} className="field">
<div className="ui checkbox">
<input type="checkbox" />
<label>{todo.title}</label>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
}
所有的類名,比如 ui text container center aligned
昔榴,都來自 Semantic UI
辛藻,幫助我們的應(yīng)用程序看起來更好。
這 1 行 this.state.todos.filter(todo => !todo.done).map(todo => ...
過濾掉已經(jīng)完成的待辦事項互订,并將它們隱藏在頁面之外吱肌。
現(xiàn)在有一個看起來像待辦事項列表的東西。
todolist
如果單擊其中一個復(fù)選框仰禽,就會發(fā)現(xiàn)什么也不發(fā)生氮墨。理想情況下,我們想讓東西在檢查時消失吐葵,所以我們把它加進去规揪。
完成 Todos
Add an onClick
handler to the checkbox.
在復(fù)選框中添加一個onClick
處理程序。
<input
type="checkbox"
onClick={() => {
this.handleCheckboxClick(todo.id)
}}
/>
我們使用了一個稍微奇怪的語法温峭,因為我們希望將所選todo的id傳遞給處理函數(shù)猛铅。
處理程序應(yīng)該添加到render
函數(shù)之上。
handleCheckboxClick(id) {
let newTodos = [...this.state.todos];
newTodos[newTodos.findIndex(todo => todo.id === id)].done = true;
this.setState({
todos: newTodos
});
}
這是React中修改數(shù)組狀態(tài)的多種方法之一凤藏。首先復(fù)制當(dāng)前todos列表奸忽,然后將選擇的todo(通過其id標(biāo)識)標(biāo)記為done并更新狀態(tài)堕伪。
現(xiàn)在,當(dāng)你選中復(fù)選框時栗菜,todo就會從頁面中消失欠雌,因為我們將過濾掉標(biāo)記為done的任何todo 項。
添加 Todos
在現(xiàn)實生活中疙筹,人們可能有比洗碗和打掃房間更多的任務(wù)要做富俄,所以讓我們允許用戶添加他們自己的待辦事項。
首先而咆,向render
方法添加一個輸入表單霍比。
render() {
return (
<div
style={{ padding: "30px 0" }}
className="ui text container center aligned"
>
<h2>My Todos</h2>
<div className="ui grid">
<div className="row centered">
<div className="column twelve wide">
<form className="ui form" onSubmit={this.handleAddTodoClick}>
<div className="inline fields">
<div className="twelve wide field">
<input
type="text"
value={this.state.newTodo}
onChange={this.hanldeInputChange}
/>
</div>
<button className="ui button primary" type="submit">
Add todo
</button>
</div>
</form>
</div>
</div>
<div className="row centered">
<div className="column twelve wide">
<div className="grouped fields">
{this.state.todos
.filter(todo => !todo.done)
.map(todo => (
<div key={todo.id} className="field">
<div className="ui checkbox">
<input
type="checkbox"
onClick={() => {
this.handleCheckboxClick(todo.id);
}}
/>
<label>{todo.title}</label>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
}
然后讓我們實現(xiàn)所有這些處理函數(shù)。
更新初始狀態(tài)以跟蹤新的todo值暴备,并清除那些默認的todo桂塞。
state = {
todos: [],
newTodo: "",
}
實現(xiàn)handleInputChange
函數(shù),該函數(shù)將跟蹤用戶輸入的內(nèi)容馍驯。
hanldeInputChange = e => {
this.setState({
newTodo: e.target.value,
})
}
接下來,我們實現(xiàn)handleAddTodoClick
玛痊,當(dāng)用戶單擊enter或單擊按鈕來添加他們的新todo項時汰瘫,將調(diào)用handleAddTodoClick
。
handleAddTodoClick = e => {
e.preventDefault()
const newTodo = {
id: this.state.todos.length + 1,
title: this.state.newTodo,
done: false,
}
const todos = [...this.state.todos]
todos.push(newTodo)
this.setState({
todos: todos,
newTodo: "",
})
}
你的整個App.js
應(yīng)該是這樣的:
import React from "react"
class App extends React.Component {
state = {
todos: [],
newTodo: "",
}
handleCheckboxClick(id) {
let newTodos = [...this.state.todos]
newTodos[newTodos.findIndex(todo => todo.id === id)].done = true
this.setState({
todos: newTodos,
})
}
handleAddTodoClick = e => {
e.preventDefault()
const newTodo = {
id: this.state.todos.length + 1,
title: this.state.newTodo,
done: false,
}
const todos = [...this.state.todos]
todos.push(newTodo)
this.setState({
todos: todos,
newTodo: "",
})
}
hanldeInputChange = e => {
this.setState({
newTodo: e.target.value,
})
}
render() {
return (
<div
style={{ padding: "30px 0" }}
className="ui text container center aligned"
>
<h2>My Todos</h2>
<div className="ui grid">
<div className="row centered">
<div className="column twelve wide">
<form className="ui form" onSubmit={this.handleAddTodoClick}>
<div className="inline fields">
<div className="twelve wide field">
<input
type="text"
value={this.state.newTodo}
onChange={this.hanldeInputChange}
/>
</div>
<button className="ui button primary" type="submit">
Add todo
</button>
</div>
</form>
</div>
</div>
<div className="row centered">
<div className="column twelve wide">
<div className="grouped fields">
{this.state.todos
.filter(todo => !todo.done)
.map(todo => (
<div key={todo.id} className="field">
<div className="ui checkbox">
<input
type="checkbox"
onClick={() => {
this.handleCheckboxClick(todo.id)
}}
/>
<label>{todo.title}</label>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
)
}
}
export default App
現(xiàn)在擂煞,你應(yīng)該能夠添加新的待辦事項混弥,并勾選它們。唯一的問題是对省,當(dāng)你刷新頁面時蝗拿,你會丟失所有珍貴的待辦事項。現(xiàn)在是時候使用Blockstack來保存我們的todo了蒿涎。
我們加上 Blockstack
現(xiàn)在哀托,我們將使用Blockstack添加用戶身份驗證和存儲。先停止App劳秋,安裝 npm install blockstack
仓手,然后我們可以重新啟動應(yīng)用程序npm start
。
身份認證
在blockstack 的App.js
的類聲明的上方添加以下行:
import { UserSession, AppConfig } from "blockstack";
const appConfig = new AppConfig(["store_write"]);
const userSession = new UserSession({ appConfig: appConfig });
class App extends React.Component {
...
}
這一行const appConfig = new AppConfig(["store_write"]);
用于設(shè)置Blockstack App 的配置玻淑。 你要向用戶請求所需的權(quán)限嗽冒。在這個例子中,我們請求store_write
權(quán)限补履,它允許我們將數(shù)據(jù)存儲在用戶的私有存儲中添坊。
如果我們想構(gòu)建社交類的App,我們需要publish_data
權(quán)限箫锤,它允許某些用戶數(shù)據(jù)對其他用戶可見贬蛙。
const userSession = new UserSession({ appConfig: appConfig });
建立用戶session雨女,允許我們處理身份驗證。
在頁面頂部添加一個登錄按鈕速客。
<div style={{ padding: "30px 0" }} className="ui text container center aligned">
<button className="ui button positive" onClick={this.handleSignIn}>
Sign in with blockstack
</button>
<h2>My Todos</h2>
...
</div>
And implement our handler function this.handleSignIn
like this:
并實現(xiàn)函數(shù) this.handleSignIn
:
handleSignIn = () => {
userSession.redirectToSignIn()
}
實現(xiàn)登錄只需要一行代碼戚篙,頁面現(xiàn)在應(yīng)該是這樣的:
登錄頁面
我們點擊那個按鈕,看看會發(fā)生什么!
我們被帶到blockstack瀏覽器登錄溺职,但看起來有一個問題
提示錯誤
提示"Failed to fetch information about the app requesting authentication. Please contact the app maintainer to resolve the issue." 不知道在說什么岔擂,但控制臺顯示了一些更有用的東西。
Access to fetch at 'http://localhost:3000/manifest.json'
from origin 'https://browser.blockstack.org' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
這實際上這是一個很常見的bug浪耘。
解決 CORS 問題
這個問題是Blockstack瀏覽器試圖從你的網(wǎng)站訪問一個名為manifest.json
的文件乱灵,其中包含App的一些信息。由于CORS七冲,在默認情況下痛倚,網(wǎng)站在不同的域不能向其他網(wǎng)站發(fā)出請求 。這樣做是出于安全澜躺。所以我們的網(wǎng)站現(xiàn)在拒絕了Blockstack瀏覽器對我們manifest.json
的請求蝉稳。實際上希望Blockstack能夠訪問該文件。
To do that, we'll need to modify our webpack config. Since we used create-react-app
, the webpack config is hidden. To modify it, we use the command npm run eject
. You will probably get a warning about having untracked files and uncommitted changes. So commit all your changes to git first.
為此掘鄙,我們需要修改webpack
配置耘戚。因為我們使用了create- response -app
,webpack配置是隱藏的操漠。要修改它收津,我們使用命令npm run eject
。就會得到一個關(guān)于git
的警告浊伙。因此撞秋,首先將所有更改提交到git。
git add -A
git commit -m "did things"
npm run eject
git提交
You'll see two new folders in your directory called scripts
and config
. Go to config/webpackDevServer.config.js
and add the following line on top of the module exports function.
在目錄中看到兩個新文件夾scripts
和config
嚣鄙,在 config / webpackDevServer.config
添加以下行吻贿。
module.exports = function(proxy, allowedHost) {
return {
headers: {
"Access-Control-Allow-Origin": "*"
},
// WebpackDevServer 2.4.3 introduced a security fix that prevents remote
// websites from potentially accessing local content through DNS rebinding:
...
}
}
使用npm start
重新啟動并重新登錄。
進入public/manifest.json
哑子,在這里修改 App 的名稱廓八。
{
"short_name": "Todo List",
"name": "Secure Todo List",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
Authentication Continued 繼續(xù)驗證
現(xiàn)在,根據(jù)用戶是否登錄來修改頁面赵抢。退出的用戶不應(yīng)該看到他們的todo列表剧蹂,而登錄后的用戶不需要看到login按鈕。
To make this a bit cleaner, we're going to separate those two things into different components. We'll have a TodoList
component which shows the Todo List and a Login
component which shows the login page.
為了是 App更簡潔烦却,我們把這兩個東西分成不同的部分宠叼。我們將有一個顯示Todo列表的TodoList
組件和一個顯示登錄頁面的Login
組件。
Copy the contents of App.js
into a new file called TodoList.js
and modify it as follows.
復(fù)制 App.js
的內(nèi)容的到新的頁面 TodoList.js
, 修改如下:
import React from "react"
class TodoList extends React.Component {
state = {
todos: [],
newTodo: "",
}
handleCheckboxClick(id) {
let newTodos = [...this.state.todos]
newTodos[newTodos.findIndex(todo => todo.id === id)].done = true
this.setState({
todos: newTodos,
})
}
handleAddTodoClick = e => {
e.preventDefault()
const newTodo = {
id: this.state.todos.length + 1,
title: this.state.newTodo,
done: false,
}
const todos = [...this.state.todos]
todos.push(newTodo)
this.setState({
todos: todos,
newTodo: "",
})
}
hanldeInputChange = e => {
this.setState({
newTodo: e.target.value,
})
}
render() {
return (
<div
style={{ padding: "30px 0" }}
className="ui text container center aligned"
>
<h2>My Todos</h2>
<div className="ui grid">
<div className="row centered">
<div className="column twelve wide">
<form className="ui form" onSubmit={this.handleAddTodoClick}>
<div className="inline fields">
<div className="twelve wide field">
<input
type="text"
value={this.state.newTodo}
onChange={this.hanldeInputChange}
/>
</div>
<button className="ui button primary" type="submit">
Add todo
</button>
</div>
</form>
</div>
</div>
<div className="row centered">
<div className="column twelve wide">
<div className="grouped fields">
{this.state.todos
.filter(todo => !todo.done)
.map(todo => (
<div key={todo.id} className="field">
<div className="ui checkbox">
<input
type="checkbox"
onClick={() => {
this.handleCheckboxClick(todo.id)
}}
/>
<label>{todo.title}</label>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
)
}
}
export default TodoList
寫一個 Login.js
組件冒冬,代碼如下
import React from "react"
class Login extends React.Component {
handleSignIn = () => {
this.props.userSession.redirectToSignIn()
}
render() {
return (
<div
style={{ padding: "30px 0" }}
className="ui text container center aligned"
>
<h1>Decentralized Todo List</h1>
<p>This is the most secure todo list on the market.</p>
<button className="ui button positive" onClick={this.handleSignIn}>
Sign in with blockstack
</button>
</div>
)
}
}
export default Login
我們將userSession
作為props
伸蚯,這個對象包含用戶身份驗證相關(guān)的函數(shù)。
最后當(dāng)用戶注銷時简烤,App.js
顯示Login
組件剂邮,當(dāng)用戶登錄后顯示TodoList
組件。
import React from "react"
import { UserSession, AppConfig } from "blockstack"
import Login from "./Login"
import TodoList from "./TodoList"
const appConfig = new AppConfig(["store_write"])
const userSession = new UserSession({ appConfig: appConfig })
class App extends React.Component {
render() {
return (
<div>
{userSession.isUserSignedIn() ? (
<TodoList userSession={userSession} />
) : (
<Login userSession={userSession} />
)}
</div>
)
}
}
export default App
We use the function userSession.isUserSignedIn() to find out whether there is a logged in user or not.
Now you should see the login page by default. When you click the button, you are redirected to Blockstack, then once you select your id you are redirected to your app, then...it still shows you the login page. What's up with that?
Turns out we're actually in an intermediary login stage. By this point, Blockstack has given the app a token with all of the user information. We need to add one more function call to extract information from that toke and finish the sign in.
Add these lines above the render() function in your App component.
componentWillMount() {
if (userSession.isSignInPending()) {
userSession
.handlePendingSignIn()
.then(() => {
window.location = window.location.origin;
})
.catch(err => console.log(err));
}
}
This extracts the user information from the token, and completes the sign in, then refreshes the page.
Here is a chart that explains the whole Blockstack authentication process.
With this in place, try logging in again and you should be redirected to the todo list.
Lastly, let's add a sign out button to the todo list page. Go to TodoList.js and add a button to the top of the page in the render function.
<div
style={{ padding: "30px 0" }}
className="ui text container center aligned"
>
<button className="ui button negative" onClick={this.handleSignout}>
Sign out
</button>
<h2>My Todos</h2>
<div className="ui grid">
...
</div>
</div>
Add the handleSignout function somewhere above the render function.
handleSignout = () => {
this.props.userSession.signUserOut(window.location.origin)
}
Now you can login and logout of the app with Blockstack.
Storing The Todos
Now that the user can login to our app, we can store their data with Blockstack.
We'll be using two core functions of the blockstack.js library: putFile and getFile.
They do exactly what they sound like. putFile allows you to store files, and getFile allows you to retrieve files. You can store any type of file, and they can be encrypted if you want.
In our case, we'll be storing our todos in JSON format because it makes them easy to handle.
Go to TodoList.js and modify the handleAddTodoClick function as follows:
handleAddTodoClick = e => {
e.preventDefault()
const newTodo = {
id: this.state.todos.length + 1,
title: this.state.newTodo,
done: false,
}
const todos = [...this.state.todos]
todos.push(newTodo)
const options = { encrypt: true }
this.props.userSession
.putFile("todos.json", JSON.stringify(todos), options)
.then(() => {
this.setState({
todos,
newTodo: "",
})
})
}
This stores all the user's todos in a file called todos.json
Modify handleCheckboxClick so that when we mark todos as done, this is also updated in the user storage.
handleCheckboxClick(id) {
let newTodos = [...this.state.todos];
newTodos[newTodos.findIndex(todo => todo.id === id)].done = true;
const options = { encrypt: true };
this.props.userSession
.putFile("todos.json", JSON.stringify(newTodos), options)
.then(() => {
this.setState({
todos: newTodos
});
});
}
Try making some todos now and you should see something like this in your console, indicating that the files were stored.
If you refresh the page you won't see anything, because we still need to retrieve the todos.
Add a new function to your class called fetchData which will get the todo list from user storage.
async fetchData() {
const options = { decrypt: true };
const file = await this.props.userSession.getFile("todos.json", options);
let todos = JSON.parse(file || "[]");
this.setState({
todos
});
}
We will call this function in our componentDidMount
componentDidMount() {
this.fetchData();
}
Now you can add a todo item, refresh your page, and it will still be there!
Adding User Profile Data
Right now our app doesn't feel very personal, but we can use Blockstack to get information like the user's name to customize their experience.
Add a new field to the state to store the user object.
state = {
newTodo: "",
todos: [],
user: null,
}
Then modify the fetchData function to update the state with user info.
async fetchData() {
const options = { decrypt: true };
const file = await this.props.userSession.getFile("todos.json", options);
let todos = JSON.parse(file || "[]");
this.setState({
todos,
user: new Person(this.props.userSession.loadUserData().profile)
});
}
And add an import statement at the top of your file.
import { Person } from "blockstack"
The Person object puts the user data in an easily accessible format.
Modify the render function to display some user information. We'll be showing their name and profile image.
render() {
const { user } = this.state;
return (
<div
style={{ padding: "30px 0" }}
className="ui text container center aligned"
>
<button className="ui button negative" onClick={this.handleSignout}>
Sign out
</button>
<h1>{user && user.name()}</h1>
<img
className="ui centered medium rounded image"
src={user && user.avatarUrl()}
alt="user profile image"
/>
<h2>My Todos</h2>
...
Now the app should feature the user's name and profile image.
Our app looks good to go, now let's deploy it for the rest of the world to see.
Deploying To Netlify
There are many ways to deploy your React app, but Netlify is one of the best. It allows you to easily setup continuous deployment.
First let's make a new repository on github.
Add and commit all of your files.
git add -A
git commit -m "made everything"
Then follow the commands to push an existing repository. For me that would be:
git remote add origin https://github.com/dkb868/secure-todo-list.git
git push -u origin master
Now you should have a beautiful new repo up on github.
Make an account on Netlify, then in your dashboard, select "New site from Git".
Select Github, and search for your repo.
Use the following build settings, then click Deploy Site
Give it a few minutes, then you should have your site up at something.netlify.com. You can modify this name if you want, or add a custom domain.
If we go to our newly launched app, we'll see a familiar error.
We know this is a CORS error, and we fixed it in our development environment, so now we need to fix it in production.
With Netlify, this is as simple as adding a netlify.toml file in your root project directory.
[[headers]]
for = "/*"
[headers.values]
Access-Control-Allow-Origin = "*"
Add that file and push it to GitHub. Once you have continuous deploy enabled, it will be deployed automatically in a few minutes.
Now everything should be working great.
Conclusion
If you made it this far, congrats for finishing the app!
If you got lost at some point, you can check out the github repo or the demo website for reference.
Demo Website: https://blockstack-todo-list.netlify.com/
Github Repo: https://github.com/dkb868/secure-todo-list
This is my first coding tutorial, so if you have any feedback on things I can improve, please let me know.
參考: