天天看點

「Serverless雲開發72變」實作一個簡單的部落格

寫在之前

今年8.03-8.10,我有幸參加了阿裡雲的雲開發校園合夥人創造營,成為了雲開發校園合夥人。這篇文章是對之前學習的總結和我自己對阿裡雲severless雲開發的一些經驗。水準有限,多多包涵!!

開發前的準備工作

首先你得有一個阿裡雲賬号,之後在谷歌浏覽器中輸入

https://workbench.aliyun.com/

點選免費雲開發登入雲開發平台,建立一個新應用。有很多應用場景,根據自己的需求選擇即可。我們這裡選擇實驗室,選擇midway serverless ots資料庫示例。(因為ots資料庫基本免費)。

「Serverless雲開發72變」實作一個簡單的部落格

輸入應用名稱和應用介紹,點選完成。稍等一會,項目就建立成啦。檢視環境管理裡面依賴的雲服務,如果還有未開通的服務,開通即可,都是免費,知道環境管理旁邊綠色對勾出現。

「Serverless雲開發72變」實作一個簡單的部落格

建立完成以後點選應用配置,在浏覽器輸入

https://www.aliyun.com/product/ots

,點選管理控制台,點選建立執行個體,輸入名稱,點選确定。點選建立好的執行個體,把執行個體名稱和公網分别複制到應用配置中的執行個體名和endPoint上,點選自己的頭像,檢視自己的accesskey與secret,并複制自己的accesskey與secret。

「Serverless雲開發72變」實作一個簡單的部落格
「Serverless雲開發72變」實作一個簡單的部落格
「Serverless雲開發72變」實作一個簡單的部落格
「Serverless雲開發72變」實作一個簡單的部落格

點選建立資料表,建立兩個表blog和user。設定blog的主鍵為id,user的主鍵為username和password。

之後點選建立資料表,建立完成後傳回項目頁面

「Serverless雲開發72變」實作一個簡單的部落格
「Serverless雲開發72變」實作一個簡單的部落格

點選開發部署

ok,熟悉的味道

「Serverless雲開發72變」實作一個簡單的部落格

安裝依賴

npm i      

試這運作一下

npm run dev      

來看一下demo的頁面

至此準備工作就完成啦

Fass能做什麼

目前的函數,可以當做一個小容器,原來我們要寫一個完整的應用來承載能力,現在隻需要寫中間的邏輯部分,以及考慮輸入和輸出的資料。

随着時間的更替,平台的疊代,函數的能力會越來越強,而使用者的上手成本,伺服器成本則會越來越低。

Midway Serverless

Midway Serverless 是用于建構 Node.js 雲函數的 Serverless 架構。幫助你在雲原生時代大幅降低維護成本,更專注于産品研發。

基本使用方式就是在f.yml裡面配置路由,通過裝飾器實作函數的依賴注入

官網介紹

https://www.yuque.com/midwayjs/faas/

編寫後端接口

編寫注冊函數

首先在f.yml裡functions中配置register函數,注意格式

functions:
  register:
    handler: user.register
    events:
      - apigw:
          path: /api/user/register      

之後在src/apis/index.ts裡,把預設的幾個函數删除

新增一個register函數

@Func('user.register')
  async  register() {
    const { username, password } = this.ctx.request.body;
    const params = {
      tableName: "user",
      condition: new TableStore.Condition(TableStore.RowExistenceExpectation.IGNORE, null),
      primaryKey: [
        { username }, { password }
      ]
    };
    return new Promise(resolve => {
      this.tb.putRow(params, async function (err, data) {
        if (err) {

          resolve({
            success: false,
            errmsg: err.message
          });
        } else {
          resolve({
            success: true
          });
        }
      });
    });
  }      

編寫登入函數

配置f.yml

login:
    handler: user.login
    events:
      - apigw:
          path: /api/user/login      

編寫login函數

@Func('user.login')
  async  login() {
    const { username, password } = this.ctx.request.body;
    const params = {
      tableName: 'user',
      primaryKey: [{ username }, { password }],
      direction: TableStore.Direction.BACKWARD
    };
    return new Promise(resolve => {
      this.tb.getRow(params, async (_, data) => {

        await format.row(data.row)
        const row = format.row(data.row)
        if (row) {

          resolve({
            author: row.username,
            success: true
          });
        } else {
          resolve({ success: false });
        }
      });
    })
  }      

編寫擷取部落格清單函數

list:
    handler: blog.list
    events:
      - apigw:
          path: /api/blog/list      

編寫list函數

@Func('blog.list')
  async handler() {
    const params = {
      tableName: 'blog',
      direction: TableStore.Direction.BACKWARD,
      inclusiveStartPrimaryKey: [{ id: TableStore.INF_MAX }],
      exclusiveEndPrimaryKey: [{ id: TableStore.INF_MIN }]
    };
    return new Promise(resolve => {
      this.tb.getRange(params, (_, data) => {
        const rows = format.rows(data, { email: true });
        resolve(rows);
      });
    })
  }      

編寫部落格詳情頁函數

配置f.yml 檔案

detail:
    handler: blog.detail
    events:
      - apigw:
          path: /api/blog/detail      

編寫detail函數

@Func('blog.detail')
  async  detail() {

    const { id } = this.ctx.query;
    const params = {
      tableName: 'blog',
      primaryKey: [{ 'id': id }],
      direction: TableStore.Direction.BACKWARD,
      inclusiveStartPrimaryKey: [{ id: TableStore.INF_MAX }],
      exclusiveEndPrimaryKey: [{ id: TableStore.INF_MIN }]
    };
    return new Promise(resolve => {
      this.tb.getRow(params, (_, data) => {
        const row = format.row(data.row);
        resolve(row);
      });
    })
  }      

編寫删除目前部落格函數

del:
    handler: blog.del
    events:
      - apigw:
          path: /api/blog/del      

編寫remove函數

@Func('blog.del')
  async remove() {
    const { id } = this.ctx.query;
    const params = {
      tableName: "blog",
      condition: new TableStore.Condition(TableStore.RowExistenceExpectation.IGNORE, null),
      primaryKey: [{ id }]
    };
    return new Promise(resolve => {
      this.tb.deleteRow(params, function (err, data) {
        if (err) {
          resolve({
            success: false,
            errmsg: err.message
          });
        } else {
          resolve({
            success: true
          });
        }
      });
    });
  }      

編寫建立部落格的函數

new:
    handler: blog.new
    events:
      - apigw:
          path: /api/blog/new      

編寫 add 函數

@Func('blog.new')
  async add() {
    const { content, title, author } = this.ctx.query;

    const params = {
      tableName: "blog",
      condition: new TableStore.Condition(TableStore.RowExistenceExpectation.IGNORE, null),
      primaryKey: [
        { id: `${Date.now()}-${Math.random()}` }
      ],
      attributeColumns: [
        { content },
        { title },
        { author }
      ]
    };
    return new Promise(resolve => {
      this.tb.putRow(params, async function (err, data) {
        if (err) {
          resolve({
            success: false,
            errmsg: err.message
          });
        } else {
          resolve({
            success: true
          });
        }
      });
    });
  }      

編寫更新部落格的函數

update:
    handler: blog.update
    events:
      - apigw:
          path: /api/blog/update      

編寫update函數

@Func('blog.update')
  async update() {
    const { id, content, title, author } = this.ctx.query;
    const params = {
      tableName: "blog",
      condition: new TableStore.Condition(TableStore.RowExistenceExpectation.IGNORE, null),
      primaryKey: [
        { 'id': id },
      ],
      attributeColumns: [
        { content },
        { title },
        { author }
      ]
    };
    return new Promise((resolve) => {
      this.tb.putRow(params, function (err, data) {
        if (err) {
          resolve(false);
        } else {
          resolve(true);
        }
      });
    });
  }      

使用react編寫前端頁面

使用ant degsin 作為ui元件 官方文檔看這裡

https://ant.design/components/overview-cn/

使用 echarts 作為統計使用者部落格數量的插件 官方文檔看這裡

https://echarts.apache.org/zh/tutorial.html

使用 axios 調用後端接口 官方文檔看這裡

http://www.axios-js.com/docs/

使用react-router編寫前端路由 官方文檔看這裡

http://react-guide.github.io/react-router-cn/

這是所需要的package.json檔案

{
  "name": "midway-faas-ots-demo",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@midwayjs/faas": "^0.3.0",
    "@midwayjs/faas-middleware-static-file": "^0.0.4",
    "echarts": "^4.9.0",
    "echarts-for-react": "^2.0.16",
    "koa-session": "^6.0.0",
    "otswhere": "^0.0.4",
    "tablestore": "^5.0.7",
    "todomvc-app-css": "^2.3.0"
  },
  "midway-integration": {
    "tsCodeRoot": "src/apis",
    "lifecycle": {
      "before:package:cleanup": "npm run build"
    }
  },
  "scripts": {
    "dev": "WORKBENCH_ENV=development npm run local:url & npm run watch",
    "watch": "react-scripts start",
    "local:url": "node scripts/local.js",
    "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"
    ]
  },
  "devDependencies": {
    "@midwayjs/faas-cli": "*",
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "@testing-library/user-event": "^7.1.2",
    "@types/jest": "^24.0.0",
    "@types/node": "^12.0.0",
    "@types/react": "^16.9.0",
    "@types/react-dom": "^16.9.0",
    "midway-faas-workbench-dev": "^1.0.1",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-scripts": "3.4.1",
    "typescript": "~3.7.2",
    "antd": "^4.5.4",
    "axios": "^0.19.2",
    "moment": "^2.27.0",
    "react-infinite-scroller": "^1.2.4",
    "react-router-dom": "^5.2.0"
  }
}      

覆寫原來的package.json檔案後

在指令行輸入 npm i 安裝依賴

npm i      

在src中建立檔案index.css

@import '~antd/dist/antd.css';
html,body {  
    background-color: #f1f8fd;
    height: 100%;
}      

編寫主菜單元件

先把原來src/components裡面的檔案清空,

在src/components建立menu.tsx檔案

import React, { useState,useEffect } from 'react'
import { Layout, Menu, Input, Button, Row, Col, Card } from 'antd';
import { BrowserRouter, Route, Link} from 'react-router-dom';
import axios from 'axios'
import {InfiniteListExample} from './CardList'
import Tea from './Tea'
import Advise from './Advise'
import Login from './Login'
import Detail from './Detail'
import Update from './Update';
import Register from './Register';
import New from './new'
import {
  HomeOutlined,
  FileTextOutlined,
  CoffeeOutlined,
  AudioOutlined
} from '@ant-design/icons';

const { Search } = Input;

const suffix = (
  <AudioOutlined
    style={{
      fontSize: 16,
      color: '#1890ff',
    }}
  />
);

const { Header, Sider, Content, Footer } = Layout;

export default function SiderDemo() {
  const [collapsed, SetCollapsed] = useState(false);
  
 const toggle = () => {
     SetCollapsed(!collapsed)
  }
 


  return (
    <>
     
    <BrowserRouter>
      <Layout >
    
        <Sider className='sider' collapsible trigger={null} breakpoint='lg' onBreakpoint={toggle} >
        
          <Menu className='menu'  mode="inline" defaultSelectedKeys={['1']}>
            <Menu.Item key="1" icon={<HomeOutlined />}>
              <Link to="/">首頁</Link>
            </Menu.Item>
            <Menu.Item key="2" icon={<FileTextOutlined />}>
              <Link to="/advise">排行榜</Link>
            </Menu.Item>
            <Menu.Item key="3" icon={ <CoffeeOutlined /> }>
              <Link to="/tea"> 須知 </Link>
            </Menu.Item>
          </Menu>
        </Sider>
        <Layout className="site-layout">
         
          <Header className="site-layout-background" style={{ padding: 0 }}>
          <Row style={{ background: "white" }}>
            <Col span={6}></Col>
            <Col> 
            <Search
              placeholder="目前還不支援搜尋功能"
              
             
              style={{
                width: 200,
              }} />
            </Col>
            <Col span={6}></Col>
            <Col><Button type="primary" style={{
            }}><Link to="/login">登入</Link></Button><Button><Link to="/register">注冊</Link></Button></Col></Row>
          </Header>
          <Content
            className="site-layout-background"
            style={{
             
              margin: '24px 16px',
              padding: 24,
              minHeight: 800,
            }}
          >
          
           
              <Route path='/' exact render={() =><InfiniteListExample/>}></Route>
          
            <Route path='/advise' exact render={() => <Advise />}></Route>
            <Route path='/tea' exact render={() => <Tea />}></Route>
            <Route path='/login' exact render={() =>  <Login/>}></Route>
             <Route path='/register' exact render={() =>  <Register/>}></Route>
            <Route path='/detail' exact render={() =>  <Detail/>}></Route>
            <Route path='/update' exact render={() =>  <Update/>}></Route>
            <Route path='/new' exact render={() =>  <New/>}></Route>             
             
          </Content>
          <Footer style={{ textAlign: 'center' }}>BBBlog ©2020 Created by kunpeng</Footer>
        </Layout>
      </Layout>
    </BrowserRouter>
    </>
  );


}      

在src/index.tsx中引入

import React from 'react'
import ReactDOM from 'react-dom';
import './index.css';
import Sider from './components/menu' 
export default function App() {
  return (
    <div>
        <Sider/>
    </div>
  )
}

ReactDOM.render(
    
    <App />
  ,
  document.getElementById('root')
);      

編寫對應的css檔案

在src中建立style檔案夾,建立menu.css檔案

#components-layout-demo-custom-trigger .trigger {
    font-size: 18px;
    line-height: 64px;
    padding: 0 24px;
    cursor: pointer;
    transition: color 0.3s;
  }
  
  #components-layout-demo-custom-trigger .trigger:hover {
    color: #1890ff;
  }
  
  #components-layout-demo-custom-trigger .logo {
    height: 32px;
    background: rgba(255, 255, 255, 0.2);
    margin: 16px;
  }
  
  .site-layout .site-layout-background {
    background: #fff;
   
  }
  .ant-layout-sider-children{
    background: #fff;
  }
  
  .site-layout{
    display:flex;
  }      

在index.css引入

@import './style/menu.css';      

編寫注冊元件

建立register.tsx檔案

import React, { useState } from 'react';
import { Form, Input, Button, Checkbox } from 'antd';
import axios from 'axios'
const Register = () => {
    const [username, setUsername] = useState('')
    const [password, setPassword] = useState('')
    const handleRegister = () => {
       
        axios.post('/api/user/register',{
            username,password
        })
        .then(resp => resp.data)
        .then(resp => {
          if (resp) {
            alert(`注冊成功,快去登入吧`)
          } else {
            alert(`注冊失敗`)
          }
        })
        
      
    } 
    return (
        <Form
          
            name="basic"
            layout='inline'
            initialValues={{
                remember: true,
            }}

        >
            <Form.Item
                label="使用者名"
                name="使用者名"

                rules={[
                    {
                        required: true,
                        message: '請輸入你的使用者名!',
                    },
                ]}
            >
                <Input onChange={e => {

                    setUsername(e.target.value)
                }} />
            </Form.Item>

            <Form.Item
                label="密碼"
                name="密碼"
                rules={[
                    {
                        required: true,
                        message: '請輸入你的密碼!',
                    },
                ]}
            >
                <Input.Password onChange={e => {

                    setPassword(e.target.value)
                }} />
            </Form.Item>
            <Form.Item >
                <Button  htmlType="submit" onClick={handleRegister}>
                    注冊
        </Button>
            </Form.Item>
        </Form>
    );
};



export default Register;      

編寫登入元件

建立login.tsx 檔案

import React, {useState}from 'react';
import { Form, Input, Button} from 'antd';
import axios from 'axios'


const Login = () => {
  const [username, setUsername] = useState('')
  const [password, setPassword] = useState('')
 
   const handleLogin = () =>{
    axios.post(`/api/user/login`,{
     username,password
    }).then(resp => {
       
    if (resp.data.success) {
      console.log(resp.data)
           localStorage.setItem('author',resp.data.author)       
          alert(`登入成功`)
       }
       
       else {
        alert(`登入失敗`)
       }
     })}
   
  return (
    <Form
     
      name="basic"
      initialValues={{
        remember: true,
      }}
      layout='inline'
    >
      <Form.Item
        label="使用者名"
        name="username"
        rules={[
          {
            required: true,
            message: '請輸入你的使用者名',
          },
        ]}
      >
        <Input  onChange={e => {
                  
                  setUsername(e.target.value)
                }}/>
      </Form.Item>

      <Form.Item
        label="密碼"
        name="password"
        rules={[
          {
            required: true,
            message: '請輸入你的密碼',
          },
        ]}
      >
        <Input.Password onChange={e => {
                  
                  setPassword(e.target.value)
                }}/>
      </Form.Item>


      <Form.Item >
        <Button type="primary" htmlType="submit"onClick={handleLogin}>
          登入
        </Button>
      </Form.Item>
    </Form>
  );
};

export default Login;      

編寫部落格清單元件

建立CardList.tsx檔案

import React from 'react'
import axios from 'axios'
import { List, message, Avatar, Spin, Button } from 'antd';
import InfiniteScroll from 'react-infinite-scroller';
import { Link } from 'react-router-dom';


export class InfiniteListExample extends React.Component {
  state = {
    data: [],
    loading: false,
    hasMore: true,
    author:localStorage.getItem('author')
  };

  componentDidMount() {
    this.fetchData.then(res => {

      this.setState({
        data: res.list,
      });

    });
  }

  fetchData = axios.get('/api/blog/list').then(res => res.data
  )
  renderRow(item) {
  return (
    <div key={item.id} className="row">
      <div className="image">
       
      </div>
      <div className="content">
        <div>{item.title}</div>
        <div className='content'>{item.content.substring(0,100).concat('...')}</div>
        <div className='author'>by   {item.author}</div>
        <Button type="dashed"><Link to={`/detail?${item.id}`}>點選檢視詳情</Link></Button>
      </div>
    </div>
  );
}


  handleInfiniteOnLoad = () => {
    let { data } = this.state;
    this.setState({
      loading: true,
    });
    if (data.length > 14) {
      message.warning('Infinite List loaded all');
      this.setState({
        hasMore: false,
        loading: false,
      });
      return;
    }
    this.fetchData.then(res => {
      data = data.concat(res);
      this.setState({
        data,
        loading: false,
      });
    });
  };

  render() {
    return (
      <div className="demo-infinite-container">
        <InfiniteScroll
          initialLoad={false}
          pageStart={0}
          loadMore={this.handleInfiniteOnLoad}
          hasMore={!this.state.loading && this.state.hasMore}
          useWindow={false}
        >
          <div className="list">
        {this.state.data.map(this.renderRow.bind(this))}
      </div>
        </InfiniteScroll>
        <Button>  {this.state.author ? <Link to='new'>點選新增部落格</Link> : '請先登入才能新增部落格哦'} </Button>
      </div>
    );
  }

}      

在src/style檔案夾下建立檔案CardList.css

.list {
  padding: 10px;
}
  .content
{
text-overflow:ellipsis;
}
.author{
  position:relative;
  left:10px;
}
.row { 
  border-bottom: 1px solid #ebeced;
  text-align: left;
  margin: 5px 0;
  display: flex;
  align-items: center;
}

/* .image {
  margin-right: 10px;
} */

.content {
  padding: 10px;
}      

在index.css中新增引入

@import './style/Cardlist.css';      

編寫部落格詳情頁元件

建立detail.tsx 檔案

import React, { useState } from 'react'
import axios from 'axios'
import {Col,Row, Button, Alert} from 'antd'
import { Link } from 'react-router-dom';
export default function Detail() {
    let list = window.location.search.split('?');
    let id = list[1];
    
    const  Author = localStorage.getItem('author')
    const [author, setAuthor] = useState('');
    const [content, SetContent] = useState("");
    const [title, setTitle] = useState('')
    axios.get(`/api/blog/detail?id=${id}`).then(
      res => res.data
    ). then(res => {
       setAuthor(res.author)
     
  
      setTitle(res.title)
       SetContent(res.content)

   })
    const handleDel =()=>{
          
                axios(`/api/blog/del?id=${id}`).then(
            res=>res.data
        )
        .then(
               res=>{
                   if(res.success){
                       alert('删除成功')
                   }else{
                       alert('删除失敗')
                   }
               }
            )
            
        
          
        
       
    }

    return (<div>
       <Row align='middle'justify='center'><h2 className="title">{title}</h2></Row> 
       <Row><div><span style={{
           color:'grey',
           fontSize:'12px'
       }}> write By {author}</span><span style={{
        color:'grey',
        fontSize:'12px'
    }}> </span></div></Row> 
    <br/>
    <br/>
        <div><p>{content}</p></div>
       <Button>{Author===author?<Link to={`/update?${id}`}>更新</Link>:''}</Button> 
        <br/>
         {Author===author?<Button onClick={handleDel}>删除 </Button>:<div></div>}

        
       
    </div>

    )
}      

編寫更新部落格元件

建立update元件

import React, { useState, useContext } from 'react'
import { Input,Button } from 'antd';
import axios from 'axios'
export default  function Update(){
    let list = window.location.search.split('?');
    let id = list[1];
    const { TextArea } = Input;
    const [title,SetTitle]=useState('')
    const [content,SetContent]=useState('')
    //  const [author,SetAuthor]=useState('')
     const author = localStorage.getItem('author')
    const HandleUpdate=()=>{
        axios(`/api/blog/update?id=${id}&title=${title}&content=${content}&author=${author}`,).then(res=>res.data
        ).then(res=>{
          // console.log(res)
            if(res){
                alert('更新成功')
            }
            else{
                alert('更新失敗')
            }
        }
          
        )
    }
   

    return(
        <div> <Input onChange={e=>{SetTitle(e.target.value)}}  placeholder="請輸入标題" />
         {/* <Input onChange={e=>{SetAuthor(e.target.value)}}  placeholder="請輸入作者姓名" /> */}
        <TextArea onChange={e=>{SetContent(e.target.value)}} rows={4} placeholder='請輸入内容'/>
       
       <Button onClick={HandleUpdate}>送出更新</Button></div>
    )}      

編寫新增部落格元件

建立new.tsx檔案

import React, { useState, useContext } from 'react'
import { Input,Button } from 'antd';
import axios from 'axios'
export default  function New(){
    const { TextArea } = Input;
    const [title,SetTitle]=useState('')
    const [content,SetContent]=useState('')
    
    const author = localStorage.getItem('author')
    const HandleUpdate=()=>{
        axios(`/api/blog/new?title=${title}&content=${content}&author=${author}`).then(res=>
          res.data
         
        )
        .then(res=>{
          
             if(res.success){
                 alert('新增成功')
             }
             else{
                 alert('新增失敗')
             }
         }
          
         )
    }
   

    return(
        <div> <Input onChange={e=>{SetTitle(e.target.value)}}  placeholder="請輸入标題" />
        {/* <Input onChange={e=>{SetAuthor(e.target.value)}}  placeholder="請輸入作者姓名" /> */}
        <TextArea onChange={e=>{SetContent(e.target.value)}} rows={4} placeholder='請輸入内容'/>
       
       <Button onClick={HandleUpdate}>送出</Button></div>
    )}      

編寫說明元件

建立檔案Tea.tsx

import React, { useState } from 'react'
import { Alert } from 'antd'
export default  function Tea(){
    return(
     <div>
       <Alert
      message="請注意"
      description="不要發不良的資訊呦!"
      type="info"
      showIcon
    />
    <br/>
        <h1>這個blog有很多不足</h1>
          <h1>但俺才快大二,有時間去更新和維護</h1>
        <h1>求大家點贊^ ^</h1></div>
    )}      

編寫統計部落格數量的元件

建立檔案Advise.tsx

import React, { useState,useRef,useEffect } from 'react'
import Bar from '../echarts/bar'


export default  function Advise(){ 
    return(
        <div> 
         
          <Bar/>
          
    
    
    </div>
    )}      

在src下建立echarts檔案夾,建立檔案bar.jsx

import React, { useState } from 'react'
import {Card} from 'antd'
import axios from 'axios'
import echarts from 'echarts'
import ReactEcharts  from 'echarts-for-react'
import { useEffect } from 'react'


export default  function Bar (){
  const  [keys,setKeys] = useState([]);
   const  [ values ,setValues] = useState([]); 
   echarts.registerTheme('my_theme', {
  backgroundColor: '#f0ffff'
});
 useEffect(()=>{
  
 axios.get('/api/blog/list').then(res => res.data.list).then(res=>res.map(item=>item.author)).then(res=>res.reduce(function (allNames, name) { 
  if (name in allNames) {
    allNames[name]++;
  }
  else {
    allNames[name] = 1;
  }
  return allNames;
}, {})).then(res=>{
 setKeys(Object.keys(res)) 

   setValues(Object.values(res))
   
})
 
},[])


 function getOption(){
   let option = {
            title: {
                text: '釋出部落格文章數量'
            },
            tooltip: {},
            xAxis: {
                data: keys
            },
            yAxis: {},
            series: [{
                name: '數量',
                type: 'bar',
                data: values
            }]
        };
    
   return option
}
 



  return(
    <div>
     <Card title='來看看釋出文章的數量吧'>

       <ReactEcharts option={getOption()} theme={"theme_name"}/>
     </Card>

    </div>
  )
}      

ok,至此我們開發完畢了

終端中輸入

npm run dev      

來看看效果吧

部署上線

注意部署之前先把檔案克隆到本地,以防丢失

點左側第一個部署按鈕,首先選擇日常環境,點選與檔案同步,自動拉取f.yml的配置,如果不行,手動配置一下~,之後點選部署。

「Serverless雲開發72變」實作一個簡單的部落格

之後預發環境與線上環境與之一樣,按順序即可。部署成功後,會給出一個免費的臨時測試域名用于通路部署到線上的效果。

如果你要用自己的域名長期通路,可以參見以下文檔繼續線上上環境進行部署和釋出上線。

https://help.aliyun.com/document_detail/176711.html

總結

參加訓練營,讓我受益良多,感受到serverless的強大之處。serverless 大大降低了開發的成本和上線周期,而且免運維 (伺服器運維、容量管理、彈性伸縮等),按資源的使用量付費使得上線後的成本極低。

上線位址

http://bk.ckpbk.top/

項目github位址  

https://github.com/JokerChen-peng/BBBlog_midway

由于筆者才疏學淺,這個項目的代碼肯定很多優化的空間,歡迎大家來幫我找bug和重構O(∩_∩)O哈哈~

還沒有使用過Serverless雲開發?

現在花3分鐘體驗新手任務即領10元阿裡雲無門檻代金券。

「Serverless雲開發72變」實作一個簡單的部落格

本文參加Serverless雲開發的有獎征文活動,已經獲得作者授權