寫在之前
今年8.03-8.10,我有幸參加了阿裡雲的雲開發校園合夥人創造營,成為了雲開發校園合夥人。這篇文章是對之前學習的總結和我自己對阿裡雲severless雲開發的一些經驗。水準有限,多多包涵!!
開發前的準備工作
首先你得有一個阿裡雲賬号,之後在谷歌浏覽器中輸入
https://workbench.aliyun.com/點選免費雲開發登入雲開發平台,建立一個新應用。有很多應用場景,根據自己的需求選擇即可。我們這裡選擇實驗室,選擇midway serverless ots資料庫示例。(因為ots資料庫基本免費)。

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

建立完成以後點選應用配置,在浏覽器輸入
https://www.aliyun.com/product/ots,點選管理控制台,點選建立執行個體,輸入名稱,點選确定。點選建立好的執行個體,把執行個體名稱和公網分别複制到應用配置中的執行個體名和endPoint上,點選自己的頭像,檢視自己的accesskey與secret,并複制自己的accesskey與secret。




點選建立資料表,建立兩個表blog和user。設定blog的主鍵為id,user的主鍵為username和password。
之後點選建立資料表,建立完成後傳回項目頁面


點選開發部署
ok,熟悉的味道

安裝依賴
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的配置,如果不行,手動配置一下~,之後點選部署。

之後預發環境與線上環境與之一樣,按順序即可。部署成功後,會給出一個免費的臨時測試域名用于通路部署到線上的效果。
如果你要用自己的域名長期通路,可以參見以下文檔繼續線上上環境進行部署和釋出上線。
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雲開發的有獎征文活動,已經獲得作者授權