挑战2小时编程:撸了个微信小程序做私人网盘视频教程分享
通过Taro脚手架搭建项目
本人用React技术栈较多,用React开发极快,所以本次进行快速开发小程序挑战,就采用了Taro 3.x的开发框架(一套代码支持运行在H5、RN、微信/京东/百度/支付宝/头条/钉钉/企业微信/飞书小程序)。 Taro是京东出品的开源多端解决方案,市场上比较类似的多端解决方案还有uni-app,大家根据个人爱好使用。
Taro 3.x开发文档参考 taro-docs.jd.com/taro/docs/
快速开始
安装taro cli 脚手架
首先,你需要使用 npm 或者 yarn 全局安装 @tarojs/cli,或者直接使用 npx:
# 使用 npm 安装 CLI
$ npm install -g @tarojs/cli
# OR 使用 yarn 安装 CLI
$ yarn global add @tarojs/cli
# OR 安装了 cnpm,使用 cnpm 安装 CLI
$ cnpm install -g @tarojs/cli
值得一提的是,如果安装过程出现sass相关的安装错误,请在安装mirror-config-china后重试
npm install -g mirror-config-china
安装成功后,可以用npm info 或 taro -v查看版本信息
npm info @tarojs/cli
taro 初始化模板项目
taro init 99minidevelop
然后到 99minidevelop 这个目录下安装依赖即可。
微信小程序云开发模板
打开微信开发者工具,创建项目并勾选微信云开发,然后创建项目。
创建完毕的项目,打开后会默认展示云开发的新手教程,不熟悉云开发的童鞋可以自己看看。
点击“云开发”按钮,会打开云开发的后台部分,也就是存储云函数(nodejs server层) 和 数据库 及存储服务(类似oss)
更改taro项目配置与微信云开发结合
本地的项目结构
- 99mini
|- 99minidevelop # taro项目开发项目
|- 99miniprogram # 微信小程序项目
|- cloudfunctions # 云函数目录
|- miniprogram # 微信小程序前端
taro开发项目配置更改
- 修改99minidevelop/config/index.js文件的outputRoot到99miniprogram/miniprogram
- 根据个人爱好设置alias (目录或文件别名)
- 配置mini的cssModules (比较懒的情况下就开启全局cssModules转换,防止css类名重复)
cssModules: {
enable: true, // 默认为 false,如需使用 css modules 功能,则设为 true
config: {
namingPattern: 'global', // 转换模式,取值为 global/module
generateScopedName: '[name]_[local]_[hash:base64:2]'
}
}
运行启动taro项目
npm run dev:weapp
小程序产品初设
代码实现
获取的openid等信息只是简单传播,跟云函数层的接口认证一般实际项目中都会生成x-token鉴权,这里不做处理,毕竟2小时不能要求太多,哈哈。
app.config.js 注册页面路由及TabBar配置
export default defineAppConfig({
pages: [
'pages/home/index',
'pages/list/index',
'pages/detail/index',
'pages/my/index',
'pages/about/index'
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: 'WeChat',
navigationBarTextStyle: 'black'
},
tabBar: {
color: '#cdcdcd',
selectedColor: '#333333',
backgroundColor: '#fff',
borderStyle: 'black',
list: [
{
pagePath: 'pages/home/index',
text: '首页',
iconPath: './assets/images/icon_home.png',
selectedIconPath: './assets/images/icon_home_chk.png',
},
{
pagePath: 'pages/list/index',
text: '发现',
iconPath: './assets/images/icon_list.png',
selectedIconPath: './assets/images/icon_list_chk.png',
},
{
pagePath: 'pages/my/index',
text: '我的',
iconPath: './assets/images/icon_my.png',
selectedIconPath: './assets/images/icon_my_chk.png',
}
]
}
})
引入全局iconfont.less
在iconfont上挑选图标组成一个项目,导出字体图标,然后在项目入口引用
@import "./assets/iconfont/iconfont.less";
body,
page {
background: #f2f2f2;
}
在jsx文件中引用图标
import {Text} from '@tarojs/components';
<Text className={`icon iconfont icon-morentouxiang`} />
我的页面(含微信授权)
my/index.jsx页面代码如下:
import {Component} from 'react';
import Taro from '@tarojs/taro';
import {View, Image, Button, Text} from '@tarojs/components';
import {observer} from 'mobx-react';
import { setOpenId, setAvatarUrl, setNickName } from '@common/config';
import Model from './vm';
import styles from './index.less';
@observer
class Index extends Component {
canIUse = wx.canIUse('button.open-type.getUserInfo')
componentWillMount() {
this.vm = new Model();
this.vm.init();
}
onGetUserInfo = () => {
wx.getUserProfile({
desc: '获取您的昵称、头像信息',
success: (res) => {
const { avatarUrl, nickName } = res.userInfo || {};
Taro.setStorageSync('avatarUrl', avatarUrl);
Taro.setStorageSync('nickName', nickName);
this.vm.avatarUrl = avatarUrl;
this.vm.nickName = nickName;
setAvatarUrl(avatarUrl);
setNickName(nickName);
this.vm.getOpenId().then(() => {
const openId = this.vm.openId;
Taro.setStorageSync('openId', openId);
setOpenId(openId);
});
}
});
}
onAbout = () => {
Taro.redirectTo({
url: '/pages/about/index'
})
}
render() {
const { avatarUrl, nickName } = this.vm;
return (
<View className={styles.container}>
<View className={styles.banner}>
<View className={styles.photoView}>
{
avatarUrl ? <Image src={avatarUrl} className={styles.ava} /> :
<Button openType="getUserProfile"
onClick={this.onGetUserInfo}
className={styles.authBtnImg}
plain>
<Text className={`icon iconfont icon-morentouxiang ${styles.defaultPhoto}`} />
</Button>
}
</View>
{
nickName ? <View className={styles.nick}>{nickName}</View> :
<Button openType="getUserProfile"
onClick={this.onGetUserInfo}
className={styles.authBtn}
plain={true}>登录</Button>
}
</View>
<View className={styles.list}>
<View className={styles.row} onClick={this.onAbout}>
<Text
className={`icon iconfont icon-jifenshuoming ${styles.customerIcon}`}
/>
<View className={styles.text}>使用说明</View>
</View>
</View>
</View>
)
}
}
export default Index
my/vm.js代码如下
import { action, observable } from 'mobx';
import Taro from '@tarojs/taro';
import config, { setOpenId, setAvatarUrl, setNickName } from '@common/config';
class HomeModel {
@observable openId = null;
@observable avatarUrl = '';
@observable nickName = '';
@action
init = () => {
const nickName = Taro.getStorageSync('nickName');
if (nickName) {
this.nickName = nickName;
setNickName(nickName);
}
const openId = Taro.getStorageSync('openId');
if (openId) {
this.openId = openId;
setOpenId(openId);
}
const avatarUrl = Taro.getStorageSync('avatarUrl');
if (avatarUrl) {
this.avatarUrl = avatarUrl;
setAvatarUrl(avatarUrl);
}
}
@action
getOpenId = () => {
// 通过云函数层获取openid
return wx.cloud.callFunction({
name: 'quickstartFunctions',
config: {
env: config.envId
},
data: {
type: 'getOpenId'
}
}).then((resp) => {
const { openid } = resp.result || {};
this.openId = openid;
})
}
}
export default HomeModel;
首页
import Taro from '@tarojs/taro';
import { Component } from 'react';
import { View, Image } from '@tarojs/components';
import { observer } from 'mobx-react';
import { getAvatarUrl, getNickName } from '@common/config';
import Model from './vm';
import styles from './index.less';
// 暂时先用本地资源,原则上除tabbar的图片外最好都放在CDN上,可以减少小程序体积并且加速图片展示
import bannerImg from '../../assets/images/banner.jpg';
import fe from '../../assets/images/fe.png';
import all from '../../assets/images/all.png';
import ts from '../../assets/images/ts.png';
import python from '../../assets/images/python.png';
import node from '../../assets/images/node_js.png';
@observer
class Home extends Component {
componentWillMount () {
this.vm = new Model();
this.vm.getList();
}
onClassGo = type => {
if (type) {
Taro.setStorageSync('TYPE_CLICK', type);
}
Taro.switchTab({
url: '/pages/list/index',
})
}
onDetail = id => {
if (getAvatarUrl() && getNickName()) {
Taro.redirectTo({
url: '/pages/detail/index?id=' + id
})
} else {
wx.showToast({
title: '请您登录!',
icon: 'error',
duration: 1000
});
setTimeout(() => {
wx.hideToast();
Taro.switchTab({
url: '/pages/my/index',
})
}, 1000);
}
}
render () {
const { list } = this.vm;
return (
<View className={styles.container}>
<Image className={styles.banner} src={bannerImg} />
<View className={styles.classes}>
<Image src={fe} onClick={this.onClassGo.bind(this, '1')} className={styles.class} />
<Image src={node} onClick={this.onClassGo.bind(this, '2')} className={styles.class} />
<Image src={ts} onClick={this.onClassGo.bind(this, '3')} className={styles.class} />
<Image src={python} onClick={this.onClassGo.bind(this, '4')} className={styles.class} />
<Image src={all} onClick={this.onClassGo.bind(this, null)} className={styles.class} />
</View>
<View className={styles.hot}>
推荐
</View>
<View className={styles.list}>
{
list.map(item => <View onClick={this.onDetail.bind(this,item._id)} className={styles.content}>
<Image className={styles.img} src={item.image} />
<View className={styles.desc}>
<View className={styles.title}>{item.name}</View>
<View className={styles.power}>{item.level === '0' ? '免费' : ''}</View>
<View className={styles.date}>{item.create}</View>
</View>
</View>)
}
</View>
</View>
)
}
}
export default Home
发现页
import Taro from '@tarojs/taro';
import { Component } from 'react';
import { View, Image } from '@tarojs/components';
import { observer } from 'mobx-react';
import { getAvatarUrl, getNickName } from '@common/config';
import Model from './vm';
import styles from './index.less';
// 暂时先用本地资源,原则上除tabbar的图片外最好都放在CDN上,可以减少小程序体积并且加速图片展示
import fe from '../../assets/images/fe.png';
import ts from '../../assets/images/ts.png';
import python from '../../assets/images/python.png';
import node from '../../assets/images/node_js.png';
import nginx from '../../assets/images/nginx_.png';
import ps from '../../assets/images/ps.png';
import docker from '../../assets/images/docker.png';
import other from '../../assets/images/other.png';
@observer
class Index extends Component {
componentWillMount() {
this.vm = new Model();
}
init = () => {
const type = Taro.getStorageSync('TYPE_CLICK');
if (typeof type === 'string') {
Taro.removeStorageSync('TYPE_CLICK');
this.vm.getList(type);
return;
}
this.vm.getList();
}
componentDidShow() {
this.init();
}
onClassGo = type => {
this.vm.getList(type);
}
onDetail = id => {
if (getAvatarUrl() && getNickName()) {
Taro.redirectTo({
url: '/pages/detail/index?id=' + id
})
} else {
wx.showToast({
title: '请您登录!',
icon: 'error',
duration: 1000
});
setTimeout(() => {
wx.hideToast();
Taro.switchTab({
url: '/pages/my/index',
})
}, 1000);
}
}
render () {
const { list = [] } = this.vm;
return (
<View className={styles.container}>
<View className={styles.classes}>
<Image src={fe} onClick={this.onClassGo.bind(this, '1')} className={styles.class} />
<Image src={node} onClick={this.onClassGo.bind(this, '2')} className={styles.class} />
<Image src={ts} onClick={this.onClassGo.bind(this, '3')} className={styles.class} />
<Image src={python} onClick={this.onClassGo.bind(this, '4')} className={styles.class} />
<Image src={ps} onClick={this.onClassGo.bind(this, '5')} className={styles.class} />
<Image src={docker} onClick={this.onClassGo.bind(this, '6')} className={styles.class} />
<Image src={nginx} onClick={this.onClassGo.bind(this, '7')} className={styles.class} />
<Image src={other} onClick={this.onClassGo.bind(this, '8')} className={styles.class} />
</View>
<View className={styles.list}>
{
list.map(item => <View onClick={() => this.onDetail(item['_id'])} className={styles.content}>
<Image className={styles.img} src={item.image} />
<View className={styles.desc}>
<View className={styles.title}>{item.name}</View>
<View className={styles.power}>{item.level === '0' ? '免费' : ''}</View>
<View className={styles.date}>{item.create}</View>
</View>
</View>)
}
</View>
</View>
)
}
}
export default Index
详情页
import Taro from '@tarojs/taro'
import {Component} from 'react';
import {View, Input, Button, Textarea} from '@tarojs/components';
import {observer} from 'mobx-react';
import Model from './vm';
import styles from './index.less';
@observer
class Detail extends Component {
componentWillMount() {
const {id} = Taro.getCurrentInstance().router.params || {};
this.vm = new Model();
this.vm.getDetail(id);
}
onCopy = str => {
Taro.setClipboardData({
data: str,
success: function (res) {
Taro.getClipboardData({
success: function (res) {
console.log(res.data)
}
})
}
})
}
render() {
const {result} = this.vm;
const {link_url = '', code = ''} = result;
return (
<View className={styles.container}>
<View className={styles.content}>
<View className={styles.row}>
<Textarea className={`${styles.txt} ${styles.t2}`} value={link_url} />
<Button className={styles.btn} onClick={this.onCopy.bind(this, link_url)}>复制</Button>
</View>
<View className={styles.row}>
<View className={`${styles.txt} ${styles.t3}`} >提取码:{code}</View>
<Button className={styles.btn} type="primary" onClick={this.onCopy.bind(this, code)}>复制</Button>
</View>
</View>
</View>
)
}
}

export default Detail
云函数层
数据库设计
tags 表
| 字段 | 类型 | 含义 |
|---|---|---|
_id | string | 默认uuid |
| key | string | 有意义的标识 |
| name | string | 技术分类,eg: h5、docker、python |
resource 表
| 字段 | 类型 | 含义 |
|---|---|---|
_id | string | 默认uuid |
| create | string | 创建日期 |
| image | string | 资源封面图片url |
| level | string | 资源访问所需的最低用户等级,0免费 |
| name | string | 资源名称 |
| tagId | string | 资源所属技术分类 |
links 表
| 字段 | 类型 | 含义 |
|---|---|---|
_id | string | 默认uuid |
| code | string | 网盘提取码 |
| link_url | string | 网盘分享链接 |
| res_id | string | 资源id |
源代码请访问 github.com/lazyperson/… 获取,欢迎star。