Taro 小程序开发大型实战(三):实现微信和支付宝多端登录
欢迎继续阅读《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 (
useState
、useEffect
)有所了解,后面图雀社区将推出 “一杯茶的时间,上手 React Hooks”,敬请期待!
除此之外,你还需要下载并安装支付宝开发者工具[5],登录后创建自己的小程序 ID。
多端登录,群魔乱舞
与普通的 Web 应用相比,小程序能够在所在的平台实现一键登录,非常方便。这一步,我们也将实现多端登录(主要包括微信登录和支付宝登录)。之所以标题取为“群魔乱舞”,不仅受了“震惊”小编们的启发,也是因为当今各平台处理登录和鉴权的方式差异很大,很遗憾的是在 Taro 框架下我们依然需要踩很多“坑”才能真正实现“多端登录”。
准备工作
组件设计规划
这一节的代码很长,在正式开始之前我们先查看一下组件设计的规划,便于你对接下来我们要做的工作有清晰的了解。
可以看到“我的”页面整体拆分成了 Header
和 Footer
:
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.js
和 index.scss
。index.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.js
和 index.scss
。index.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.jsx
和 index.scss
。index.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.js
和 index.scss
。index.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.jsx
和 index.scss
。index.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.jsx
和 index.scss
。index.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
中只需暴露出 Header
和 Footer
。修改 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 }
更新“我的”页面
是时候用上写好的 Header
和 Footer
组件了,但在此之前,我们先来讲一下我们需要用到的 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
创建了四个状态:用户有关信息(nickName
和avatar
),登录弹出层是否打开(isOpened
),是否登录成功(isLogged
),以及相应的更新函数通过
useEffect
Hook 尝试从本地缓存中获取用户信息(Taro.getStorage[12]),并用来更新nickName
和avatar
状态实现了久违的
setLoginInfo
函数,其中我们不仅更新了nickName
和avatar
的状态,还把用户数据存入本地缓存(Taro.getStorage[13]),确保下次打开时保持登录状态实现了同样久违的
handleLogout
函数,其中不仅更新了相关状态,还去掉了本地缓存中的数据(Taro.removeStorage[14])实现了用于处理普通登录的
handleSubmit
函数,内容基本上与setLoginInfo
一致在返回 JSX 代码时渲染
Header
和Footer
组件,传入相应的状态和回调函数
调整 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 小程序开发大型实战(三):实现微信和支付宝多端登录相关推荐
- Taro 小程序开发大型实战(六):尝鲜微信小程序云(上篇)
欢迎继续阅读<Taro 小程序开发大型实战>系列,前情回顾: 熟悉的 React,熟悉的 Hooks[1]:我们用 React 和 Hooks 实现了一个非常简单的添加帖子的原型 多页面跳 ...
- Taro 小程序开发大型实战(七):尝鲜微信小程序云(下篇)
欢迎继续阅读<Taro 小程序开发大型实战>系列,前情回顾: 熟悉的 React,熟悉的 Hooks:我们用 React 和 Hooks 实现了一个非常简单的添加帖子的原型 多页面跳转和 ...
- 微信小程序开发系列教程三:微信小程序的调试方法
微信小程序开发系列教程 微信小程序开发系列一:微信小程序的申请和开发环境的搭建 微信小程序开发系列二:微信小程序的视图设计 这个教程的前两篇文章,介绍了如何用下图所示的微信开发者工具自动生成一个Hel ...
- 视频教程-微信小程序开发全案精讲-微信开发
微信小程序开发全案精讲 负责过多个软件项目的研发.设计和管理工作,拥有项目管理师认证.项目监理师中级认证.出版过的图书有<微信小程序开发图解案例教程><Axure RP8原型设计图解 ...
- 视频教程-微信小程序开发教程(第1篇)-微信开发
微信小程序开发教程(第1篇) 微信企业号星级会员.10多年软件从业经历,国家级软件项目负责人,主要从事软件研发.软件企业员工技能培训.已经取得计算机技术与软件资格考试(软考)--"信息系统项 ...
- 微信小程序开发教程第八章:微信小程序分组开发与左滑功能实现
接着上面微信小程序开发教程第八章:微信小程序分组开发与左滑功能实现.(第一二章:微信小程序开发教程,第三四章:微信小程序项目结构以及配置&微信小程序首页面开发,第五章:微信小程序名片夹详情页开 ...
- 1个月uni-app微信小程序开发上线实战专栏介绍
一.专栏介绍: <uni-app开发微信小程序1个月上线实战>,目标带领1000位同学成功开发上线一个自己的个人小程序! 作者介绍:国服第二切图仔--资深前端开发工程师,具有六年以上的前端 ...
- 微信小程序开发入门实战
小程序注册流程 1.注册方法 在微信公众平台官网首页(mp.weixin.qq.com)点击右上角的"立即注册"按钮. 2.选择注册的帐号类型 选择"小程序", ...
- 微信小程序开发—项目实战之聊天机器人
项目功能介绍 人工智能虚拟机器人"微软小冰"大家玩过吧,很酷的一个应用.发送文字.语音和图片都能得到智能的回复. 那现在我们就用小程序做一个模拟的应用,智能聊天机器人.发送文字它可 ...
最新文章
- 最强的目标检测网络:DetectoRS 54.7 AP
- 几乎死循环的存储过程
- 前端学习(3241):react生命周期forceUpdate
- 面对SDN/NFV部署挑战 网络厂商能做什么?
- 基于Prometheus+Grafana监控SQL Server数据库
- iis8使用url2.0模块实现http跳转到https
- delphi BLE 后台
- Unieap3.5-Grid翻页不提示修改
- 房地产微信营销方案微信“危”与“机”
- unity3d 怎么生成网页版_急求unity3D动画简易版制作步骤?
- “科林明伦杯”哈尔滨理工大学第十届程序设计竞赛——H.直线【JAVA大数 | Python】
- 网络基础知识——交换机路由器
- 邵阳学院大学计算机基础实验报告答案,实验报告正文(大学计算机基础)
- Ubuntu16.04安装系统监控器System Monitor
- 我用VB做的孔明棋游戏
- 华为设备web登录,安全连接失败问题解决办法
- ISE如何查看顶层文件(原语)
- Linux配置ntp时间同步服务
- Origin平台意料之外错误修复
- 期货交易入门流程(期货交易入门书籍)