挑战2小时编程:撸了个微信小程序做私人网盘视频教程分享

4,478 阅读1分钟

挑战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

image.png

taro 初始化模板项目

taro init 99minidevelop

然后到 99minidevelop 这个目录下安装依赖即可。

微信小程序云开发模板

打开微信开发者工具,创建项目并勾选微信云开发,然后创建项目。

image.png 创建完毕的项目,打开后会默认展示云开发的新手教程,不熟悉云开发的童鞋可以自己看看。

点击“云开发”按钮,会打开云开发的后台部分,也就是存储云函数(nodejs server层) 和 数据库 及存储服务(类似oss) image.png

image.png

image.png

更改taro项目配置与微信云开发结合

本地的项目结构

- 99mini
  |- 99minidevelop    # taro项目开发项目
  |- 99miniprogram    # 微信小程序项目
    |- cloudfunctions # 云函数目录 
    |- miniprogram    # 微信小程序前端

taro开发项目配置更改

  1. 修改99minidevelop/config/index.js文件的outputRoot到99miniprogram/miniprogram
  2. 根据个人爱好设置alias (目录或文件别名)
  3. 配置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

小程序产品初设

image.png

代码实现

获取的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>
    )
  }
}

![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bd9c91fe0bf74d44a9f23575214b4078~tplv-k3u1fbpfcp-watermark.image?)
export default Detail

云函数层

image.png

数据库设计

tags 表

字段类型含义
_idstring默认uuid
keystring有意义的标识
namestring技术分类,eg: h5、docker、python

resource 表

字段类型含义
_idstring默认uuid
createstring创建日期
imagestring资源封面图片url
levelstring资源访问所需的最低用户等级,0免费
namestring资源名称
tagIdstring资源所属技术分类

links 表

字段类型含义
_idstring默认uuid
codestring网盘提取码
link_urlstring网盘分享链接
res_idstring资源id

源代码请访问 github.com/lazyperson/… 获取,欢迎star。

https://github.com/lazyperson/99mini

gh_925af3d90186_430.jpg