原文:https://dev.to/dkb868/build-a-decentralized-todo-list-with-react-and-blockstack-59cm
没翻译完,请看 https://www.jianshu.com/p/078c1dae4397
在线例子:
https://rebase-todolist.netlify.com
npm run eject
在本教程中,你将学习使用Blockstack和React构建去中心化的Todolist。Blockstack是一个平台,它使构建Dapp变得非常容易,与传统的auth/storage方法相比,使用Blockstack身份验证和存储构建一个简单的App更快、更安全。
Blockstack的去中心化方法
像Google和Facebook这样的大公司都有中心化的数据库,它们可以控制你的数据,并且可以对数据做任何他们想做的事情。
Blockstack App允许用户完全控制自己的数据。没有用户的允许,任何人都不能访问用户的数据。用户数据被加密并存储在私人的“数据锁”中,用户可以给App权限来读取或写入数据到他们的存储中。
在我们的Todolist App中,这意味着App开发人员永远不会知道你的Todolist上有什么。
The App
我们的Todolist将非常简单,这样我们就可以专注于学习Blockstack是如何工作的。
完成后的App是这个样子:
demo 网址: https://blockstack-todo-list.netlify.com/
Github 网址: https://github.com/dkb868/secure-todo-list
The Setup
首先,我们将设置环境,安装node.js
的最新版本。
React
我们使用 create-react-app
, 在你命令行窗口输入 npx create-react-app secure-todo-list
创建新的项目。
大约一分钟后,就应该完成了。
进入项目 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
,将内容替换为:
import React from "react"
class App extends React.Component {
render() {
return Nice Meme
}
}
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( , 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/),内容为空,这将提供默认设置。
{}
Semantic UI
我们将使用Semantic UI
,一个CSS库,给我们的应用程序一些样式。
将这个url (https://cdnjs.cloudflare.com/ajax/libs/semanticui/2.4.1 /semantic.min.css)复制到你的public/index.html
中,方法是将这一行添加到html文件的头部。
现在,你应该已经完成了一个非常漂亮的、极简主义的网站。
Blockstack 账户
你需要一个Blockstack帐户 以便你可以登录和使用App。你可以通过https://blockstack.org/并从菜单中选择Create ID获取一个账户。
一个简单的 Todo List
首先,我们用React
构建一个简单的Todolist,其中不包含任何Blockstack技术。每当页面刷新时, App状态将丢失,这将使它更容易看到Blockstack的作用。
初始化状态
我们添加一些状态到我们的App。在App.js的render函数上面添加内容:
state = {
todos: [
{
id: 1,
title: "Wash the dishes",
done: false,
},
{
id: 2,
title: "Clean my room",
done: false,
},
],
}
现在我们的应用程序跟踪todos,它有三个属性:
- id:todo的唯一标识符
- title: 任务的名称
- done:是否已完成这个任务
显示Todos
现在我们有了一些todo,让我们在页面上显示它们。
改变render
方法,代码如下:
render() {
return (
My Todos
{this.state.todos
.filter(todo => !todo.done)
.map(todo => (
))}
);
}
所有的类名,比如 ui text container center aligned
,都来自 Semantic UI
,帮助我们的应用程序看起来更好。
这 1 行 this.state.todos.filter(todo => !todo.done).map(todo => ...
过滤掉已经完成的待办事项,并将它们隐藏在页面之外。
现在有一个看起来像待办事项列表的东西。
如果单击其中一个复选框,就会发现什么也不发生。理想情况下,我们想让东西在检查时消失,所以我们把它加进去。
完成 Todos
Add an onClick
handler to the checkbox.
在复选框中添加一个onClick
处理程序。
{
this.handleCheckboxClick(todo.id)
}}
/>
我们使用了一个稍微奇怪的语法,因为我们希望将所选todo的id传递给处理函数。
处理程序应该添加到render
函数之上。
handleCheckboxClick(id) {
let newTodos = [...this.state.todos];
newTodos[newTodos.findIndex(todo => todo.id === id)].done = true;
this.setState({
todos: newTodos
});
}
这是React中修改数组状态的多种方法之一。首先复制当前todos列表,然后将选择的todo(通过其id标识)标记为done并更新状态。
现在,当你选中复选框时,todo就会从页面中消失,因为我们将过滤掉标记为done的任何todo 项。
添加 Todos
在现实生活中,人们可能有比洗碗和打扫房间更多的任务要做,所以让我们允许用户添加他们自己的待办事项。
首先,向render
方法添加一个输入表单。
render() {
return (
My Todos
{this.state.todos
.filter(todo => !todo.done)
.map(todo => (
{
this.handleCheckboxClick(todo.id);
}}
/>
))}
);
}
然后让我们实现所有这些处理函数。
更新初始状态以跟踪新的todo值,并清除那些默认的todo。
state = {
todos: [],
newTodo: "",
}
实现handleInputChange
函数,该函数将跟踪用户输入的内容。
hanldeInputChange = e => {
this.setState({
newTodo: e.target.value,
})
}
接下来,我们实现handleAddTodoClick
,当用户单击enter或单击按钮来添加他们的新todo项时,将调用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
应该是这样的:
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 (
My Todos
{this.state.todos
.filter(todo => !todo.done)
.map(todo => (
{
this.handleCheckboxClick(todo.id)
}}
/>
))}
)
}
}
export default App
现在,你应该能够添加新的待办事项,并勾选它们。唯一的问题是,当你刷新页面时,你会丢失所有珍贵的待办事项。现在是时候使用Blockstack来保存我们的todo了。
我们加上 Blockstack
现在,我们将使用Blockstack添加用户身份验证和存储。先停止App,安装 npm install blockstack
,然后我们可以重新启动应用程序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"]);
用于设置Blockstack App 的配置。 你要向用户请求所需的权限。在这个例子中,我们请求store_write
权限,它允许我们将数据存储在用户的私有存储中。
如果我们想构建社交类的App,我们需要publish_data
权限,它允许某些用户数据对其他用户可见。
const userSession = new UserSession({ appConfig: appConfig });
建立用户session,允许我们处理身份验证。
在页面顶部添加一个登录按钮。
My Todos
...
And implement our handler function this.handleSignIn
like this:
并实现函数 this.handleSignIn
:
handleSignIn = () => {
userSession.redirectToSignIn()
}
实现登录只需要一行代码,页面现在应该是这样的:
我们点击那个按钮,看看会发生什么!
我们被带到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浏览器试图从你的网站访问一个名为manifest.json
的文件,其中包含App的一些信息。由于CORS,在默认情况下,网站在不同的域不能向其他网站发出请求 。这样做是出于安全。所以我们的网站现在拒绝了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
。就会得到一个关于git
的警告。因此,首先将所有更改提交到git。
git add -A
git commit -m "did things"
npm run eject
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 继续验证
现在,根据用户是否登录来修改页面。退出的用户不应该看到他们的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.
复制 App.js
的内容的到新的页面 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 (
My Todos
{this.state.todos
.filter(todo => !todo.done)
.map(todo => (
{
this.handleCheckboxClick(todo.id)
}}
/>
))}
)
}
}
export default TodoList
写一个 Login.js
组件,代码如下
import React from "react"
class Login extends React.Component {
handleSignIn = () => {
this.props.userSession.redirectToSignIn()
}
render() {
return (
Decentralized Todo List
This is the most secure todo list on the market.
)
}
}
export default Login
我们将userSession
作为props
,这个对象包含用户身份验证相关的函数。
最后当用户注销时,App.js
显示Login
组件,当用户登录后显示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 (
{userSession.isUserSignedIn() ? (
) : (
)}
)
}
}
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.
My Todos
...
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 (
{user && user.name()}
My Todos
...
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.
参考:
https://www.jianshu.com/p/078c1dae4397