欢迎继续阅读《Taro 小程序开发大型实战》系列,前情回顾:

  • 熟悉的 React,熟悉的 Hooks[1]:我们用 React 和 Hooks 实现了一个非常简单的添加帖子的原型

  • 多页面跳转和 Taro UI 组件库[2]:我们用 Taro 自带的路由功能实现了多页面跳转,并用 Taro UI 组件库升级了应用界面

而在这一篇中,我们将实现微信和支付宝多端登录。如果你希望直接从这一篇开始,请运行以下命令:

git clone -b third-part https://github.com/tuture-dev/ultra-club.git
cd ultra-club

本文所涉及的源代码都放在了 Github[3] 上,如果您觉得我们写得还不错,希望您能给 ❤️ 这篇文章点个在看+Github 仓库加星 ❤️ 哦~

在正式开始之前,我们希望你已经具备以下知识:

  • 基本的 React 框架知识,可参考这篇文章[4]进行学习

  • 对常用的 React Hooks (useStateuseEffect)有所了解,后面图雀社区将推出 “一杯茶的时间,上手 React Hooks”,敬请期待!

除此之外,你还需要下载并安装支付宝开发者工具[5],登录后创建自己的小程序 ID。

多端登录,群魔乱舞

与普通的 Web 应用相比,小程序能够在所在的平台实现一键登录,非常方便。这一步,我们也将实现多端登录(主要包括微信登录和支付宝登录)。之所以标题取为“群魔乱舞”,不仅受了“震惊”小编们的启发,也是因为当今各平台处理登录和鉴权的方式差异很大,很遗憾的是在 Taro 框架下我们依然需要踩很多“坑”才能真正实现“多端登录”。

准备工作

组件设计规划

这一节的代码很长,在正式开始之前我们先查看一下组件设计的规划,便于你对接下来我们要做的工作有清晰的了解。

可以看到“我的”页面整体拆分成了 HeaderFooter

  • Header 包括 LoggedMine(个人信息),如果在未登录状态下则还有 LoginButton(普通登录按钮)、WeappLoginButton(微信登录按钮,仅在微信小程序中出现)以及 AlipayLoginButton(支付宝登录按钮,仅在支付宝小程序中出现)

  • Footer 则用来显示是否已登录的文字,在已登录的情况下会显示 Logout(退出登录按钮)

配置 Babel 插件

从这一步开始,我们将首次开始写异步代码。本项目将采用流行的 async/await 来编写异步逻辑,因此我们配置一下相应的 Babel 插件:

npm install babel-plugin-transform-runtime --save-dev
# yarn add babel-plugin-transform-runtime -D

然后在 config/index.js 中为 config.babel.plugins 添加相应的配置如下:

const config = {// ...babel: {// ...plugins: [// ...['transform-runtime',{helpers: false,polyfill: false,regenerator: true,moduleName: 'babel-runtime',},],],},// ...
}// ...

各组件的实现

实现 LoginButton

首先,我们来实现普通登录按钮 LoginButton 组件。创建 src/components/LoginButton 目录,在其中创建 index.js,代码如下:

import Taro from '@tarojs/taro'
import { AtButton } from 'taro-ui'export default function LoginButton(props) {return (<AtButton type="primary" onClick={props.handleClick}>普通登录</AtButton>)
}

我们使用了 Taro UI 的 AtButton 组件,并定义了一个 handleClick 事件,后面在使用时会传入。

实现 WeappLoginButton

接着我们实现微信登录按钮 WeappLoginButton。创建 src/components/WeappLoginButton 目录,在其中分别创建 index.jsindex.scssindex.js 代码如下:

import Taro, { useState } from '@tarojs/taro'
import { Button } from '@tarojs/components'import './index.scss'export default function LoginButton(props) {const [isLogin, setIsLogin] = useState(false)async function onGetUserInfo(e) {setIsLogin(true)const { avatarUrl, nickName } = e.detail.userInfoawait props.setLoginInfo(avatarUrl, nickName)setIsLogin(false)}return (<ButtonopenType="getUserInfo"onGetUserInfo={onGetUserInfo}type="primary"className="login-button"loading={isLogin}>微信登录</Button>)
}

可以看到,微信登录按钮和之前的普通登录按钮多了很多东西:

  • 添加了 isLogin 状态,用于表示是否在等待登录中,以及修改状态的 setIsLogin 函数

  • 实现了 onGetUserInfo async 函数,用于处理在用户点击登录按钮、获取到信息之后的逻辑。其中,我们将获取到的用户信息传入 props 中的 setLoginInfo,从而修改整个应用的登录状态

  • 添加了 openType(微信开放能力)属性,这里我们输入的是 getUserInfo(获取用户信息),欲查看所有支持的 open-type,请查看微信开放文档对应部分[6]

  • 添加了 onGetUserInfo 这个 handler,用于编写在获取到用户信息后的处理逻辑,这里就是传入刚刚实现的 onGetUserInfo

WeappLoginButton 的样式 index.scss 代码如下:

.login-button {width: 100%;margin-top: 40px;margin-bottom: 40px;
}

实现 AlipayLoginButton

让我们来实现支付宝登录按钮组件。创建 src/components/AlipayLoginButton 目录,在其中分别创建 index.jsindex.scssindex.js 代码如下:

import Taro, { useState } from '@tarojs/taro'
import { Button } from '@tarojs/components'import './index.scss'export default function LoginButton(props) {const [isLogin, setIsLogin] = useState(false)async function onGetAuthorize(res) {setIsLogin(true)try {let userInfo = await Taro.getOpenUserInfo()userInfo = JSON.parse(userInfo.response).responseconst { avatar, nickName } = userInfoawait props.setLoginInfo(avatar, nickName)} catch (err) {console.log('onGetAuthorize ERR: ', err)}setIsLogin(false)}return (<ButtonopenType="getAuthorize"scope="userInfo"onGetAuthorize={onGetAuthorize}type="primary"className="login-button"loading={isLogin}>支付宝登录</Button>)
}

可以看到,内容与之前的微信登录按钮基本相似,但是有以下差别:

  • 实现 onGetAuthorize 回调函数。与之前微信的回调函数不同,这里我们要调用 Taro.getOpenUserInfo 手动获取用户基础信息(实际上调用的是支付宝开放平台 my.getOpenUserInfo[7]

  • Button 组件的 openType(支付宝开放能力)设置成 getAuthorize(小程序授权)

  • 在设定开放能力为 getAuthorize 时,需要添加 scope 属性为 userInfo,让用户可以授权小程序获取支付宝会员的基础信息(另一个有效值是 phoneNumber,用于获取手机号码)

  • 传入 onGetAuthorize 回调函数

提示

关于支付宝小程序登录按钮的细节,可以查看官方文档[8]

样式文件 index.scss 的代码如下:

.login-button {width: 100%;margin-top: 40px;
}

实现 LoggedMine

接着我们实现已经登录状态下的 LoggedMine 组件。创建 src/components/LoggedMine 目录,在其中分别创建 index.jsxindex.scssindex.jsx 代码如下:

import Taro from '@tarojs/taro'
import { View, Image } from '@tarojs/components'
import PropTypes from 'prop-types'import './index.scss'
import avatar from '../../images/avatar.png'export default function LoggedMine(props) {const { userInfo = {} } = propsfunction onImageClick() {Taro.previewImage({urls: [userInfo.avatar],})}return (<View className="logged-mine"><Imagesrc={userInfo.avatar ? userInfo.avatar : avatar}className="mine-avatar"onClick={onImageClick}/><View className="mine-nickName">{userInfo.nickName ? userInfo.nickName : '图雀酱'}</View><View className="mine-username">{userInfo.username}</View></View>)
}LoggedMine.propTypes = {avatar: PropTypes.string,nickName: PropTypes.string,username: PropTypes.string,
}

这里我们添加了点击头像可以预览的功能,可以通过 `Taro.previewImage` 函数[9]实现。

LoggedMine 组件的样式文件如下:

.logged-mine {display: flex;flex-direction: column;align-items: center;
}.mine-avatar {width: 200px;height: 200px;border-radius: 50%;
}.mine-nickName {font-size: 40;margin-top: 20px;
}.mine-username {font-size: 32px;margin-top: 16px;color: #777;
}

实现 Header 组件

在所有的“小零件”全部实现后,我们就实现整个登录界面的 Header 部分。创建 src/components/Header 目录,在其中分别创建 index.jsindex.scssindex.js 代码如下:

import Taro from '@tarojs/taro'
import { View } from '@tarojs/components'
import { AtMessage } from 'taro-ui'import LoggedMine from '../LoggedMine'
import LoginButton from '../LoginButton'
import WeappLoginButton from '../WeappLoginButton'
import AlipayLoginButton from '../AlipayLoginButton'import './index.scss'export default function Header(props) {const isWeapp = Taro.getEnv() === Taro.ENV_TYPE.WEAPPconst isAlipay = Taro.getEnv() === Taro.ENV_TYPE.ALIPAYreturn (<View className="user-box"><AtMessage /><LoggedMine userInfo={props.userInfo} />{!props.isLogged && (<View className="login-button-box"><LoginButton handleClick={props.handleClick} />{isWeapp && <WeappLoginButton setLoginInfo={props.setLoginInfo} />}{isAlipay && <AlipayLoginButton setLoginInfo={props.setLoginInfo} />}</View>)}</View>)
}

可以看到,我们根据 Taro.ENV_TYPE 查询当前所在的平台(微信、支付宝或其他),然后确定是否显示相应平台的登录按钮。

提示

你也许发现了,setLoginInfo 还是要等待父组件的传入。虽然 Hooks 简化了状态的定义和更新方式,但是却没有简化跨组件修改状态的逻辑。在接下来的一步,我们将用 Redux 进行简化。

Header 组件的样式代码如下:

.user-box {display: flex;flex-direction: column;align-items: center;justify-content: flex-start;
}.login-button-box {margin-top: 60px;width: 100%;
}

实现 LoginForm

接着我们实现用于普通登录的 LoginForm 组件。由于本系列教程的目标是讲解 Taro,因此这里简化了注册/登录的流程,用户可以直接输入用户名并上传头像进行注册/登录,无需设置密码和其他验证过程。创建 src/components/LoginForm 目录,在其中分别创建 index.jsxindex.scssindex.jsx 代码如下:

import Taro, { useState } from '@tarojs/taro'
import { View, Form } from '@tarojs/components'
import { AtButton, AtImagePicker } from 'taro-ui'import './index.scss'export default function LoginForm(props) {const [showAddBtn, setShowAddBtn] = useState(true)function onChange(files) {if (files.length > 0) {setShowAddBtn(false)}props.handleFilesSelect(files)}function onImageClick() {Taro.previewImage({urls: [props.files[0].url],})}return (<View className="post-form"><Form onSubmit={props.handleSubmit}><View className="login-box"><View className="avatar-selector"><AtImagePickerlength={1}mode="scaleToFill"count={1}files={props.files}showAddBtn={showAddBtn}onImageClick={onImageClick}onChange={onChange}/></View><InputclassName="input-nickName"type="text"placeholder="点击输入昵称"value={props.formNickName}onInput={props.handleNickNameInput}/><AtButton formType="submit" type="primary">登录</AtButton></View></Form></View>)
}

这里我们使用 Taro UI 的 ImagePicker 图片选择器组件[10],让用户能够选择图片进行上传。AtImagePicker 最重要的属性就是 onChange 回调函数,这里我们通过父组件传进来的 handleFilesSelect 函数来搞定。

LoginForm 组件的样式代码如下:

.post-form {margin: 0 30px;padding: 30px;
}.input-nickName {border: 1px solid #eee;padding: 10px;font-size: medium;width: 100%;margin-top: 40px;margin-bottom: 40px;
}.avatar-selector {width: 200px;margin: 0 auto;
}

实现 Logout

在登录之后,我们还需要退出登录的按钮。创建 src/components/Logout/index.js 文件,代码如下:

import Taro from '@tarojs/taro'
import { AtButton } from 'taro-ui'export default function LoginButton(props) {return (<AtButtontype="secondary"fullloading={props.loading}onClick={props.handleLogout}>退出登录</AtButton>)
}

实现 Footer

所有的子组件全部实现之后,我们就来实现 Footer 组件。创建 src/components/Footer 目录,在其中分别创建 index.jsxindex.scssindex.jsx 代码如下:

import Taro, { useState } from '@tarojs/taro'
import { View } from '@tarojs/components'
import { AtFloatLayout } from 'taro-ui'import Logout from '../Logout'
import LoginForm from '../LoginForm'
import './index.scss'export default function Footer(props) {// Login Form 登录数据const [formNickName, setFormNickName] = useState('')const [files, setFiles] = useState([])async function handleSubmit(e) {e.preventDefault()// 鉴权数据if (!formNickName || !files.length) {Taro.atMessage({type: 'error',message: '您还有内容没有填写!',})return}// 提示登录成功Taro.atMessage({type: 'success',message: '恭喜您,登录成功!',})// 缓存在 storage 里面const userInfo = { avatar: files[0].url, nickName: formNickName }await props.handleSubmit(userInfo)// 清空表单状态setFiles([])setFormNickName('')}return (<View className="mine-footer">{props.isLogged && (<Logout loading={props.isLogout} handleLogout={props.handleLogout} />)}<View className="tuture-motto">{props.isLogged ? 'From 图雀社区 with Love ❤' : '您还未登录'}</View><AtFloatLayoutisOpened={props.isOpened}title="登录"onClose={() => props.handleSetIsOpened(false)}><LoginFormformNickName={formNickName}files={files}handleSubmit={e => handleSubmit(e)}handleNickNameInput={e => setFormNickName(e.target.value)}handleFilesSelect={files => setFiles(files)}/></AtFloatLayout></View>)
}

Footer 组件的样式文件代码如下:

.mine-footer {font-size: 28px;color: #777;margin-bottom: 20px;
}.tuture-motto {margin-top: 40px;text-align: center;
}

所有小组件都搞定之后,我们在 src/components 中只需暴露出 HeaderFooter。修改 src/components/index.jsx,代码如下:

import PostCard from './PostCard'
import PostForm from './PostForm'
import Footer from './Footer'
import Header from './Header'export { PostCard, PostForm, Footer, Header }

更新“我的”页面

是时候用上写好的 HeaderFooter 组件了,但在此之前,我们先来讲一下我们需要用到的 useEffect Hooks。

useEffect Hooks

useEffect Hooks 是用来替代原 React 的生命周期钩子函数的,我们可以在里面发起一些 “副作用” 操作,比如异步获取后端数据、设置定时器或是进行 DOM 操作等:

import React, { useState, useEffect } from 'react';function Example() {const [count, setCount] = useState(0);// 和 componentDidMount 以及 componentDidUpdate 类似:useEffect(() => {// 使用浏览器 API 更新 document 的标题document.title = `你点击了 ${count} 次`;});return (<div><p>你点击了 {count} 次</p><button onClick={() => setCount(count + 1)}>点我</button></div>);
}

上面的对 document 标题的修改是具有副作用的操作,在之前的 React 应用中,我们通常会这么写:

class Example extends React.Component {constructor(props) {super(props);this.state = {count: 0};}componentDidMount() {document.title = `你点击了 ${this.state.count} 次`;}componentDidUpdate() {document.title = `你点击了 ${this.state.count} 次`;}render() {return (<div><p>你点击了 {this.state.count} 次</p><button onClick={() => this.setState({ count: this.state.count + 1 })}>点我</button></div>);}
}

如果你想了解 useEffect 具体的详情,可以去查看 React 的官方文档[11]

做的好!了解了 useEffect Hooks 的概念之后,我们马上来更新“我的”页面组件 src/pages/mine/mine.jsx,代码如下:

import Taro, { useState, useEffect } from '@tarojs/taro'
import { View } from '@tarojs/components'import { Header, Footer } from '../../components'
import './mine.scss'export default function Mine() {const [nickName, setNickName] = useState('')const [avatar, setAvatar] = useState('')const [isOpened, setIsOpened] = useState(false)const [isLogout, setIsLogout] = useState(false)// 双取反来构造字符串对应的布尔值,用于标志此时是否用户已经登录const isLogged = !!nickNameuseEffect(() => {async function getStorage() {try {const { data } = await Taro.getStorage({ key: 'userInfo' })const { nickName, avatar } = datasetAvatar(avatar)setNickName(nickName)} catch (err) {console.log('getStorage ERR: ', err)}}getStorage()})async function setLoginInfo(avatar, nickName) {setAvatar(avatar)setNickName(nickName)try {await Taro.setStorage({key: 'userInfo',data: { avatar, nickName },})} catch (err) {console.log('setStorage ERR: ', err)}}async function handleLogout() {setIsLogout(true)try {await Taro.removeStorage({ key: 'userInfo' })setAvatar('')setNickName('')} catch (err) {console.log('removeStorage ERR: ', err)}setIsLogout(false)}function handleSetIsOpened(isOpened) {setIsOpened(isOpened)}function handleClick() {handleSetIsOpened(true)}async function handleSubmit(userInfo) {// 缓存在 storage 里面await Taro.setStorage({ key: 'userInfo', data: userInfo })// 设置本地信息setAvatar(userInfo.avatar)setNickName(userInfo.nickName)// 关闭弹出层setIsOpened(false)}return (<View className="mine"><HeaderisLogged={isLogged}userInfo={{ avatar, nickName }}handleClick={handleClick}setLoginInfo={setLoginInfo}/><FooterisLogged={isLogged}isOpened={isOpened}isLogout={isLogout}handleLogout={handleLogout}handleSetIsOpened={handleSetIsOpened}handleSubmit={handleSubmit}/></View>)
}Mine.config = {navigationBarTitleText: '我的',
}

可以看到,我们做了这么些工作:

  • 使用 useState 创建了四个状态:用户有关信息(nickNameavatar),登录弹出层是否打开(isOpened),是否登录成功(isLogged),以及相应的更新函数

  • 通过 useEffect Hook 尝试从本地缓存中获取用户信息(Taro.getStorage[12]),并用来更新 nickNameavatar 状态

  • 实现了久违的 setLoginInfo 函数,其中我们不仅更新了 nickNameavatar 的状态,还把用户数据存入本地缓存(Taro.getStorage[13]),确保下次打开时保持登录状态

  • 实现了同样久违的 handleLogout 函数,其中不仅更新了相关状态,还去掉了本地缓存中的数据(Taro.removeStorage[14]

  • 实现了用于处理普通登录的 handleSubmit 函数,内容基本上与 setLoginInfo 一致

  • 在返回 JSX 代码时渲染 HeaderFooter 组件,传入相应的状态和回调函数

调整 Mine 组件的样式 src/pages/mine/mine.scss 代码如下:

.mine {margin: 30px;height: 90vh;padding: 40px 40px 0;display: flex;flex-direction: column;justify-content: space-between;
}

最后在 src/app.scss 中引入相应的 Taro UI 组件的样式:

@import './custom-theme.scss';@import '~taro-ui/dist/style/components/button.scss';
@import '~taro-ui/dist/style/components/fab.scss';
@import '~taro-ui/dist/style/components/icon.scss';
@import '~taro-ui/dist/style/components/float-layout.scss';
@import '~taro-ui/dist/style/components/textarea.scss';
@import '~taro-ui/dist/style/components/message.scss';
@import '~taro-ui/dist/style/components/avatar.scss';
@import '~taro-ui/dist/style/components/image-picker.scss';
@import '~taro-ui/dist/style/components/icon.scss';

查看效果

终于到了神圣的验收环节。首先是普通登录:

而微信和支付宝登录,点击之后就会直接以登录开发者工具所用的帐号登录了。下面贴出我微信和支付宝登录后的界面展示:

登录后点击下方的“退出登录”按钮,就会将当前登录帐户注销哦。

至此,《Taro 多端小程序开发大型实战》第三篇也就结束啦。在接下来的第四篇[15]中,我们将逐步用 Redux 来重构业务数据流,让我们现在略显臃肿的状态管理变得清晰可控。

想要学习更多精彩的实战技术教程?来图雀社区[16]逛逛吧。

本文所涉及的源代码都放在了 Github[17] 上,如果您觉得我们写得还不错,希望您能给 ❤️ 这篇文章点个在看+Github 仓库加星 ❤️ 哦~

参考资料

[1]

熟悉的 React,熟悉的 Hooks: https://juejin.im/post/5e046c4fe51d45584221e508

[2]

多页面跳转和 Taro UI 组件库: https://juejin.im/post/5e0891b66fb9a0165936fb0b

[3]

Github: https://github.com/tuture-dev/ultra-club

[4]

这篇文章: https://juejin.im/post/5df2f559e51d45584c553060

[5]

支付宝开发者工具: https://docs.alipay.com/mini/ide/download

[6]

微信开放文档对应部分: https://developers.weixin.qq.com/miniprogram/dev/component/button.html

[7]

my.getOpenUserInfo: https://docs.alipay.com/mini/api/ch8chh

[8]

官方文档: https://docs.alipay.com/mini/component/button

[9]

Taro.previewImage 函数: https://nervjs.github.io/taro/docs/apis/multimedia/images/previewImage.html#docsNav

[10]

ImagePicker 图片选择器组件: https://taro-ui.jd.com/#/docs/imagepicker

[11]

官方文档: https://zh-hans.reactjs.org/docs/hooks-effect.html

[12]

Taro.getStorage: https://nervjs.github.io/taro/docs/apis/storage/getStorage.html#docsNav

[13]

Taro.getStorage: https://nervjs.github.io/taro/docs/apis/storage/setStorage.html#docsNav

[14]

Taro.removeStorage: https://nervjs.github.io/taro/docs/apis/storage/removeStorage.html#docsNav

[15]

第四篇: https://juejin.im/post/5e100f78e51d4541493621cd

[16]

图雀社区: https://tuture.co?utm_source=juejin_zhuanlan

[17]

Github: https://github.com/tuture-dev/ultra-club

● 一杯茶的时间,上手React框架开发● Taro小程序开发大型实战(一):熟悉的React,熟悉的Hooks● Taro小程序开发大型实战(二):多页面跳转和TaroUI组件库

·END·

图雀社区

汇聚精彩的免费实战教程

关注公众号回复 z 拉学习交流群

喜欢本文,点个“在看”告诉我

Taro 小程序开发大型实战(三):实现微信和支付宝多端登录相关推荐

  1. Taro 小程序开发大型实战(六):尝鲜微信小程序云(上篇)

    欢迎继续阅读<Taro 小程序开发大型实战>系列,前情回顾: 熟悉的 React,熟悉的 Hooks[1]:我们用 React 和 Hooks 实现了一个非常简单的添加帖子的原型 多页面跳 ...

  2. Taro 小程序开发大型实战(七):尝鲜微信小程序云(下篇)

    欢迎继续阅读<Taro 小程序开发大型实战>系列,前情回顾: 熟悉的 React,熟悉的 Hooks:我们用 React 和 Hooks 实现了一个非常简单的添加帖子的原型 多页面跳转和 ...

  3. 微信小程序开发系列教程三:微信小程序的调试方法

    微信小程序开发系列教程 微信小程序开发系列一:微信小程序的申请和开发环境的搭建 微信小程序开发系列二:微信小程序的视图设计 这个教程的前两篇文章,介绍了如何用下图所示的微信开发者工具自动生成一个Hel ...

  4. 视频教程-微信小程序开发全案精讲-微信开发

    微信小程序开发全案精讲 负责过多个软件项目的研发.设计和管理工作,拥有项目管理师认证.项目监理师中级认证.出版过的图书有<微信小程序开发图解案例教程><Axure RP8原型设计图解 ...

  5. 视频教程-微信小程序开发教程(第1篇)-微信开发

    微信小程序开发教程(第1篇) 微信企业号星级会员.10多年软件从业经历,国家级软件项目负责人,主要从事软件研发.软件企业员工技能培训.已经取得计算机技术与软件资格考试(软考)--"信息系统项 ...

  6. 微信小程序开发教程第八章:微信小程序分组开发与左滑功能实现

    接着上面微信小程序开发教程第八章:微信小程序分组开发与左滑功能实现.(第一二章:微信小程序开发教程,第三四章:微信小程序项目结构以及配置&微信小程序首页面开发,第五章:微信小程序名片夹详情页开 ...

  7. 1个月uni-app微信小程序开发上线实战专栏介绍

    一.专栏介绍: <uni-app开发微信小程序1个月上线实战>,目标带领1000位同学成功开发上线一个自己的个人小程序! 作者介绍:国服第二切图仔--资深前端开发工程师,具有六年以上的前端 ...

  8. 微信小程序开发入门实战

    小程序注册流程 1.注册方法 在微信公众平台官网首页(mp.weixin.qq.com)点击右上角的"立即注册"按钮. 2.选择注册的帐号类型 选择"小程序", ...

  9. 微信小程序开发—项目实战之聊天机器人

    项目功能介绍 人工智能虚拟机器人"微软小冰"大家玩过吧,很酷的一个应用.发送文字.语音和图片都能得到智能的回复. 那现在我们就用小程序做一个模拟的应用,智能聊天机器人.发送文字它可 ...

最新文章

  1. 最强的目标检测网络:DetectoRS 54.7 AP
  2. 几乎死循环的存储过程
  3. 前端学习(3241):react生命周期forceUpdate
  4. 面对SDN/NFV部署挑战 网络厂商能做什么?
  5. 基于Prometheus+Grafana监控SQL Server数据库
  6. iis8使用url2.0模块实现http跳转到https
  7. delphi BLE 后台
  8. Unieap3.5-Grid翻页不提示修改
  9. 房地产微信营销方案微信“危”与“机”
  10. unity3d 怎么生成网页版_急求unity3D动画简易版制作步骤?
  11. “科林明伦杯”哈尔滨理工大学第十届程序设计竞赛——H.直线【JAVA大数 | Python】
  12. 网络基础知识——交换机路由器
  13. 邵阳学院大学计算机基础实验报告答案,实验报告正文(大学计算机基础)
  14. Ubuntu16.04安装系统监控器System Monitor
  15. 我用VB做的孔明棋游戏
  16. 华为设备web登录,安全连接失败问题解决办法
  17. ISE如何查看顶层文件(原语)
  18. Linux配置ntp时间同步服务
  19. Origin平台意料之外错误修复
  20. 期货交易入门流程(期货交易入门书籍)

热门文章

  1. linux 串口格式化输出字符串,glibc中的printf如何输出到串口
  2. elk之拼音插件可选参数
  3. 阿里云大数据助理工程师认证考试考什么内容?
  4. 通用Excel数据导入功能模板
  5. 变频器VF模式和矢量模式的区别
  6. 华为openGauss初级OGCA认证100%通过
  7. Ubuntu下用安装包安装MySQL
  8. 水果之王之猕猴桃-系列三(猕猴桃的功能和禁忌)
  9. Java 必会的工具库,让你的代码量减少90%
  10. python软件电脑配置要求-Python实现的读取电脑硬件信息功能示例