在线视频教育系统与作品集展示是当前微信小程序开发中两个常见且实用的场景,前者聚焦于知识传递与学习管理,后者则用于个人或机构的能力展示。将两者结合,可以构建一个既能承载课程内容,又能展示教学成果的综合性平台。对于开发者而言,理解如何从零开始搭建这样一个系统,掌握其核心模块的设计与实现,是提升微信小程序开发能力的关键路径。
本文将以一个“基于微信小程序的在线视频教育系统”为蓝本,重点讲解如何在其基础上集成一个“作品集展示”模块。我们将从项目环境搭建、核心功能设计、前后端交互实现,到上线前的优化与测试,进行完整的梳理。无论你是希望学习微信小程序开发的新手,还是想了解如何构建内容型应用的开发者,都能通过本文获得一套可复现的实践方案。最终,你将得到一个具备视频播放、课程管理、用户交互以及作品展示功能的微信小程序原型。
1. 理解微信小程序在线教育系统的核心架构
在动手编码之前,必须厘清一个在线视频教育小程序需要哪些基本组成部分。这不仅仅是功能列表,更是理解数据如何流动、模块如何协作的基础。
1.1 核心功能模块拆解
一个典型的在线教育小程序至少包含以下四个核心模块:
-
用户系统
:这是所有交互的起点。包括微信授权登录、用户信息管理(如昵称、头像)、学习进度记录。微信生态提供了便捷的登录能力(
wx.login,getUserProfile),但需要后端配合换取openid和session_key来建立用户唯一标识。 -
内容管理系统(CMS)
:用于管理视频课程、图文资料等学习内容。在小程序端,这通常表现为课程列表页、课程详情页和视频播放器。后端需要提供课程分类、列表、详情等接口。视频播放直接使用微信小程序的
<video>组件,但视频源地址(URL)需要从后端动态获取。 - 学习交互系统 :让学习过程可记录、可追踪。包括视频播放进度记录、收藏课程、发表评论或笔记、完成课后练习等。这些交互数据需要与用户ID强关联,并持久化到数据库。
- 作品集展示模块 :这是本文的重点扩展模块。它不同于普通的课程列表,更侧重于成果的视觉化、结构化展示。例如,学员可以将自己的结业项目、设计作品、代码仓库链接等,以图文、视频或链接的形式发布出来,形成一个个人作品画廊。
1.2 前后端数据流设计
小程序采用前后端分离架构。前端(小程序)负责界面渲染和用户交互,后端(服务器)提供数据接口和业务逻辑处理。
-
前端(小程序)职责
:
- 使用 WXML/WXSS/JavaScript 构建页面。
-
调用微信 JS-SDK API(如网络请求
wx.request、本地存储wx.setStorage)。 - 向后端发起 HTTP/HTTPS 请求,获取或提交数据。
- 处理用户交互事件(点击、滑动等)。
-
后端(服务器)职责
:
- 提供 RESTful API 接口。
- 处理用户认证与授权(验证微信登录凭证)。
- 从数据库(如 MySQL、MongoDB)中存取课程、用户、作品集等数据。
- 处理文件上传(如图片、视频封面),返回可访问的 URL。
- 实现业务逻辑,如更新学习进度、计算作品集浏览量等。
数据交互的核心是 API 设计。例如,获取作品集列表的接口可能设计为
GET /api/portfolio/list
,提交一个新作品的接口为
POST /api/portfolio/create
。
1.3 技术选型与开发工具准备
对于前端,微信开发者工具是必备的。对于后端,选择非常灵活,可以是 Node.js (Express/Koa)、Python (Django/Flask)、Java (Spring Boot)、Go (Gin) 等。为了快速原型开发,我们以 Node.js + Express 和 MySQL 为例。
开发环境清单:
| 工具/环境 | 用途 | 备注 |
|---|---|---|
| 微信开发者工具 | 小程序代码编写、调试、预览、上传 | 需注册微信小程序账号获取 AppID |
| Node.js (LTS版本) | 运行 JavaScript 后端服务器 | 建议版本 16.x 或 18.x |
| MySQL 5.7+/8.0 | 关系型数据库,存储结构化数据 | 也可使用云数据库服务 |
| Postman / Apifox | API 接口测试工具 | 用于模拟前端请求,调试后端接口 |
| 代码编辑器 (VS Code) | 编写前后端代码 | 安装相关插件提升效率 |
注意:微信小程序要求后端服务器域名必须经过 ICP 备案,且需要在微信公众平台配置合法域名。开发阶段可使用开发者工具的“不校验合法域名”选项进行调试,但上线前必须完成配置。
2. 从零搭建项目基础框架与核心功能
我们将按照“后端先行”的思路,先搭建起提供数据服务的基础,再实现小程序前端界面。
2.1 后端服务初始化与数据库设计
首先,创建一个新的 Node.js 项目并安装基础依赖。
# 创建项目目录
mkdir edu-portfolio-backend
cd edu-portfolio-backend
# 初始化项目
npm init -y
# 安装核心依赖
npm install express mysql2 cors dotenv
npm install -D nodemon
# 创建基础文件
touch app.js .env
mkdir routes models controllers utils
在
.env
文件中配置环境变量,如数据库连接信息和微信 AppSecret(切勿提交到代码仓库)。
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=yourpassword
DB_NAME=edu_portfolio
WX_APPID=your_appid
WX_APPSECRET=your_appsecret
PORT=3000
接着,设计核心数据表。这里给出
users
(用户)、
courses
(课程)、
portfolio
(作品集)三个表的简化 SQL。
-- 用户表
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`openid` varchar(100) NOT NULL UNIQUE COMMENT '微信用户唯一标识',
`nickname` varchar(100) DEFAULT NULL,
`avatar_url` varchar(500) DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 课程表
CREATE TABLE `courses` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(200) NOT NULL,
`description` text,
`cover_img` varchar(500) DEFAULT NULL COMMENT '封面图URL',
`video_url` varchar(500) NOT NULL COMMENT '视频地址',
`duration` int(11) DEFAULT NULL COMMENT '视频时长(秒)',
`sort_order` int(11) DEFAULT 0,
`is_published` tinyint(1) DEFAULT 1,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 作品集表
CREATE TABLE `portfolio` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL COMMENT '关联用户ID',
`title` varchar(200) NOT NULL,
`description` text,
`cover_image` varchar(500) DEFAULT NULL COMMENT '作品封面',
`content_type` enum('image', 'video', 'link', 'text') NOT NULL DEFAULT 'image',
`content_url` varchar(500) DEFAULT NULL COMMENT '根据类型,可能是图片URL、视频URL或外部链接',
`views` int(11) DEFAULT 0 COMMENT '浏览量',
`is_public` tinyint(1) DEFAULT 1 COMMENT '是否公开',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
在
app.js
中编写一个简单的 Express 服务器,并连接数据库。
// app.js
const express = require('express');
const cors = require('cors');
require('dotenv').config();
const db = require('./utils/database'); // 假设数据库连接封装在此文件
const app = express();
const port = process.env.PORT || 3000;
// 中间件
app.use(cors()); // 允许跨域,开发时使用,生产环境需精确配置
app.use(express.json()); // 解析 JSON 请求体
app.use(express.urlencoded({ extended: true }));
// 简单的健康检查路由
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', message: 'Server is running' });
});
// 在此处引入其他路由,例如:
// const courseRoutes = require('./routes/courses');
// const portfolioRoutes = require('./routes/portfolio');
// app.use('/api/courses', courseRoutes);
// app.use('/api/portfolio', portfolioRoutes);
// 错误处理中间件
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ code: 500, message: '服务器内部错误' });
});
app.listen(port, () => {
console.log(`后端服务运行在 http://localhost:${port}`);
});
2.2 实现微信登录与用户认证接口
用户登录是小程序与后端建立信任关系的第一步。流程如下:
-
小程序端调用
wx.login()获取临时凭证code。 -
小程序将
code发送给后端。 -
后端用
code、appid、appsecret请求微信接口,换取openid和session_key。 -
后端根据
openid查询或创建用户记录,并生成自己的会话标识(如 JWT Token)返回给小程序。 - 小程序存储此 Token,后续请求在 Header 中携带。
后端登录接口示例 (
routes/auth.js
):
const router = require('express').Router();
const axios = require('axios');
const jwt = require('jsonwebtoken'); // 需安装 jsonwebtoken
const db = require('../utils/database');
router.post('/login', async (req, res, next) => {
const { code } = req.body;
if (!code) {
return res.status(400).json({ code: 400, message: '缺少code参数' });
}
const appid = process.env.WX_APPID;
const secret = process.env.WX_APPSECRET;
const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${appid}&secret=${secret}&js_code=${code}&grant_type=authorization_code`;
try {
// 1. 向微信服务器请求 openid
const response = await axios.get(url);
const { openid, session_key, errcode, errmsg } = response.data;
if (errcode) {
return res.status(401).json({ code: errcode, message: errmsg });
}
// 2. 查找或创建用户
let [users] = await db.query('SELECT * FROM users WHERE openid = ?', [openid]);
let userId;
if (users.length === 0) {
// 新用户,插入记录(此时可能还没有昵称头像,可后续更新)
const [result] = await db.query('INSERT INTO users (openid) VALUES (?)', [openid]);
userId = result.insertId;
} else {
userId = users[0].id;
}
// 3. 生成JWT Token(示例,实际项目需考虑刷新机制)
const token = jwt.sign({ userId, openid }, process.env.JWT_SECRET, { expiresIn: '7d' });
// 4. 返回用户信息和Token
res.json({
code: 0,
message: 'success',
data: {
token,
userInfo: users[0] || { id: userId, openid }
}
});
} catch (error) {
console.error('登录失败:', error);
next(error);
}
});
module.exports = router;
2.3 构建课程列表与视频播放功能
课程模块需要列表接口和详情接口。列表接口通常支持分页和筛选。
课程列表接口 (
routes/courses.js
):
router.get('/list', async (req, res, next) => {
const { page = 1, pageSize = 10 } = req.query;
const offset = (page - 1) * pageSize;
try {
// 查询已发布的课程总数
const [[{ total }]] = await db.query(
'SELECT COUNT(*) as total FROM courses WHERE is_published = 1'
);
// 查询课程列表
const [courses] = await db.query(
'SELECT id, title, description, cover_img, duration FROM courses WHERE is_published = 1 ORDER BY sort_order DESC, created_at DESC LIMIT ? OFFSET ?',
[parseInt(pageSize), offset]
);
res.json({
code: 0,
message: 'success',
data: {
list: courses,
pagination: {
current: parseInt(page),
pageSize: parseInt(pageSize),
total
}
}
});
} catch (error) {
next(error);
}
});
在小程序端,使用
wx.request
调用此接口,并使用
wx:for
渲染列表。视频播放则使用
<video>
组件,其
src
绑定从详情接口获取的
video_url
。
<!-- pages/course/list.wxml -->
<view class="course-list">
<block wx:for="{{courseList}}" wx:key="id">
<view class="course-item" bindtap="navigateToDetail" data-id="{{item.id}}">
<image class="cover" src="{{item.cover_img}}" mode="aspectFill"></image>
<view class="info">
<text class="title">{{item.title}}</text>
<text class="duration">{{item.duration}}秒</text>
</view>
</view>
</block>
</view>
// pages/course/list.js
Page({
data: {
courseList: [],
page: 1,
hasMore: true
},
onLoad() {
this.loadCourses();
},
loadCourses() {
if (!this.data.hasMore) return;
wx.request({
url: 'https://your-domain.com/api/courses/list',
data: { page: this.data.page },
success: (res) => {
if (res.data.code === 0) {
const newList = res.data.data.list;
const oldList = this.data.courseList;
this.setData({
courseList: oldList.concat(newList),
hasMore: (this.data.page * 10) < res.data.data.pagination.total
});
}
}
});
},
navigateToDetail(e) {
const id = e.currentTarget.dataset.id;
wx.navigateTo({
url: `/pages/course/detail?id=${id}`
});
}
});
3. 核心扩展:实现作品集展示模块
作品集模块是区别于普通课程列表的特色功能,它更强调用户的个性化创作和视觉展示。
3.1 作品集数据模型与接口设计
回顾之前设计的
portfolio
表,它包含了作品标题、描述、封面、内容类型和链接等字段。我们需要创建对应的增删改查接口。
-
GET /api/portfolio/list: 获取作品列表(可分页,可按用户筛选)。 -
GET /api/portfolio/:id: 获取作品详情,并增加浏览量。 -
POST /api/portfolio: 创建新作品(需要用户认证)。 -
PUT /api/portfolio/:id: 更新作品(需要验证作者权限)。 -
DELETE /api/portfolio/:id: 删除作品(需要验证作者权限)。
创建作品接口示例 (
controllers/portfolioController.js
):
exports.createPortfolio = async (req, res, next) => {
// 假设用户信息已通过JWT中间件附加到req.user
const userId = req.user.userId;
const { title, description, cover_image, content_type, content_url, is_public } = req.body;
// 基础验证
if (!title || !content_type) {
return res.status(400).json({ code: 400, message: '标题和内容类型为必填项' });
}
try {
const [result] = await db.query(
`INSERT INTO portfolio (user_id, title, description, cover_image, content_type, content_url, is_public)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[userId, title, description || null, cover_image || null, content_type, content_url || null, is_public !== undefined ? is_public : 1]
);
res.status(201).json({
code: 0,
message: '创建成功',
data: { portfolioId: result.insertId }
});
} catch (error) {
next(error);
}
};
3.2 小程序端作品集页面开发
前端需要两个主要页面:作品集画廊页 (
portfolio/index
) 和作品发布/编辑页 (
portfolio/edit
)。
画廊页 (
portfolio/index
)
: 以网格或瀑布流形式展示作品。可以设计一个选项卡,切换“全部作品”和“我的作品”。
<!-- pages/portfolio/index.wxml -->
<view class="portfolio-container">
<view class="filter-tabs">
<text class="tab {{activeTab==='all'?'active':''}}" bindtap="switchTab" data-tab="all">全部作品</text>
<text class="tab {{activeTab==='mine'?'active':''}}" bindtap="switchTab" data-tab="mine">我的作品</text>
</view>
<view class="portfolio-grid">
<block wx:for="{{portfolioList}}" wx:key="id">
<view class="portfolio-item" bindtap="viewDetail" data-id="{{item.id}}">
<image class="cover" src="{{item.cover_image || '/images/default-cover.png'}}" mode="aspectFill"></image>
<view class="title">{{item.title}}</view>
<view class="meta">
<text class="author">{{item.user_nickname}}</text>
<text class="views">浏览: {{item.views}}</text>
</view>
</view>
</block>
</view>
<view wx:if="{{hasMore}}" class="load-more" bindtap="loadMore">加载更多</view>
</view>
发布页 (
portfolio/edit
)
: 包含表单,用于输入作品信息。关键点在于
内容类型
的选择。根据用户选择的类型(图片、视频、链接),动态显示不同的输入区域(如上传组件或链接输入框)。
// pages/portfolio/edit.js
Page({
data: {
contentType: 'image', // 默认类型
formData: {
title: '',
description: '',
coverImage: '',
contentUrl: '',
isPublic: true
}
},
onContentTypeChange(e) {
const type = e.detail.value;
this.setData({ contentType: type });
// 切换类型时,可以清空之前的内容URL
if (type !== this.data.contentType) {
this.setData({ 'formData.contentUrl': '' });
}
},
// 上传封面图
chooseCover() {
wx.chooseMedia({
count: 1,
mediaType: ['image'],
success: (res) => {
const tempFilePath = res.tempFiles[0].tempFilePath;
// 调用后端上传接口,获取永久URL后更新formData.coverImage
this.uploadFile(tempFilePath, 'cover').then(url => {
this.setData({ 'formData.coverImage': url });
});
}
});
},
// 根据类型上传内容文件或直接保存链接
handleContentInput() {
if (this.data.contentType === 'image' || this.data.contentType === 'video') {
wx.chooseMedia({
count: 1,
mediaType: [this.data.contentType],
success: (res) => {
const tempFilePath = res.tempFiles[0].tempFilePath;
this.uploadFile(tempFilePath, 'content').then(url => {
this.setData({ 'formData.contentUrl': url });
});
}
});
}
// 如果是 link 或 text 类型,则通过输入框绑定 formData.contentUrl
},
// 提交表单
submitForm() {
const { title } = this.data.formData;
if (!title.trim()) {
wx.showToast({ title: '请输入标题', icon: 'none' });
return;
}
const postData = {
...this.data.formData,
content_type: this.data.contentType
};
wx.request({
url: 'https://your-domain.com/api/portfolio',
method: 'POST',
header: { 'Authorization': `Bearer ${wx.getStorageSync('token')}` },
data: postData,
success: (res) => {
if (res.data.code === 0) {
wx.showToast({ title: '发布成功' });
setTimeout(() => wx.navigateBack(), 1500);
}
}
});
}
});
3.3 文件上传与云存储集成
小程序端上传文件使用
wx.uploadFile
API。由于微信小程序对后端域名有严格限制,文件通常需要先上传到后端服务器,再由后端服务器转存到云存储(如腾讯云COS、阿里云OSS)或本地。
后端文件上传接口示例:
// routes/upload.js
const multer = require('multer'); // 需安装 multer
const path = require('path');
const fs = require('fs');
// 配置临时存储
const upload = multer({ dest: 'uploads/temp/' });
router.post('/file', upload.single('file'), async (req, res) => {
if (!req.file) {
return res.status(400).json({ code: 400, message: '未上传文件' });
}
const { originalname, mimetype, path: tempPath } = req.file;
const fileExt = path.extname(originalname).toLowerCase();
// 1. 此处应进行文件类型、大小校验
// 2. 生成唯一文件名,防止冲突
const cloudFileName = `portfolio/${Date.now()}-${Math.random().toString(36).substr(2)}${fileExt}`;
// 3. 调用云存储SDK上传文件 (以腾讯云COS为例)
// const cos = require('../utils/cos-client');
// const result = await cos.putObject({
// Bucket: 'your-bucket',
// Region: 'your-region',
// Key: cloudFileName,
// Body: fs.createReadStream(tempPath)
// }).promise();
// const fileUrl = `https://${result.Location}`;
// 4. 删除临时文件
fs.unlinkSync(tempPath);
// 5. 返回可访问的URL (此处为模拟)
const mockFileUrl = `https://your-cdn-domain.com/${cloudFileName}`;
res.json({ code: 0, message: '上传成功', data: { url: mockFileUrl } });
});
注意:生产环境务必对上传文件进行严格的安全检查,包括文件类型白名单、大小限制、病毒扫描等,并避免将文件存储在应用服务器本地。
4. 项目联调、优化与上线前检查
功能开发完成后,需要进行全面的测试和优化,以确保良好的用户体验和符合平台规范。
4.1 前后端联调与常见问题排查
联调阶段最常见的问题是网络请求失败和数据渲染错误。
问题1:
wx.request
报错
fail url not in domain list
- 原因 :请求的域名未在微信公众平台配置。
-
排查
:登录微信公众平台,在“开发”->“开发设置”->“服务器域名”中,将你的后端 API 域名(如
https://api.yourdomain.com)添加到request合法域名中。开发阶段可在开发者工具“详情”->“本地设置”中勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”,但这仅用于调试。
问题2:登录成功但后续接口返回 401 未授权
- 原因 :Token 未正确携带或已过期。
-
排查
:
-
检查登录接口返回的 Token 是否被成功存储(
wx.setStorageSync)。 -
检查后续请求的 Header 是否正确添加了
Authorization: Bearer <token>。 - 在后端中间件中打印接收到的 Token 并验证其有效性。
- 检查 Token 过期时间,实现 Token 刷新逻辑。
-
检查登录接口返回的 Token 是否被成功存储(
问题3:真机预览时图片或视频无法加载
- 原因 :资源地址是本地路径或内网地址,真机无法访问。
- 排查 :确保所有图片、视频、文件等资源的 URL 都是通过 HTTPS 协议可公开访问的互联网地址。上传功能必须使用能返回公网 URL 的服务。
问题4:小程序包体积过大,无法预览或上传
- 原因 :主包(或某些分包)大小超过 2MB 限制。
-
优化
:
- 使用小程序分包加载功能。将作品集、个人中心等非首页功能放到独立分包中。
- 压缩图片等静态资源,使用 WebP 格式。
- 检查是否有未使用的代码或组件,利用开发者工具的“代码依赖分析”功能。
- 如果使用 uni-app 等框架,注意优化其运行时体积。
4.2 性能与体验优化实践
-
图片懒加载
:列表页中的图片使用
<image>组件的lazy-load属性。 - 视频优化 :视频列表页使用封面图,详情页再加载播放器。对于长视频,考虑使用微信的“视频号”或“腾讯云点播”等专业服务,以获得更好的播放体验和节省流量。
-
请求缓存
:对于不常变的数据(如课程分类),可以使用
wx.setStorage进行本地缓存,并设置合理的过期时间。 -
下拉刷新与上拉加载
:列表页务必实现
onPullDownRefresh和onReachBottom生命周期函数,提供流畅的浏览体验。 - 骨架屏 :在数据加载前,使用骨架屏(Skeleton Screen)占位,避免白屏。
4.3 上线前安全检查清单
提交微信审核前,务必逐项检查以下内容:
| 检查项 | 说明 | 自查方法 |
|---|---|---|
| 基本信息 | 小程序名称、简介、头像、服务类目是否准确合规。 | 对照《微信小程序平台运营规范》检查。 |
| 隐私协议 | 是否在必要时机(如获取用户信息前)弹窗提示并获取用户同意。 |
检查
wx.getUserProfile
等接口的调用逻辑。
|
| 权限声明 |
在
app.json
的
requiredPrivateInfos
或
permission
字段中声明了所需的隐私接口(如相册、位置)。
| 查看开发者工具“详情”->“项目配置”中的权限列表。 |
| 内容安全 | 用户生成内容(UGC)如作品标题、描述、评论是否有过滤机制。 | 后端接口对文本内容进行敏感词过滤。 |
| 支付与虚拟支付 | 如涉及,必须使用微信支付。iOS 端禁止虚拟支付。 | 确认支付场景符合规范。 |
| 域名与证书 | 所有请求的服务器域名均已配置,且支持 HTTPS 并具备有效证书。 | 在真机上测试所有网络请求。 |
| 体验流畅性 | 无明显的卡顿、白屏、错误提示。 | 在多款真机上进行功能遍历。 |
| 无测试数据 | 清除所有控制台日志、测试账号、模拟数据。 |
检查代码中是否有
console.log
或写死的测试数据。
|
完成以上检查和优化后,便可以在微信开发者工具中点击“上传”,填写版本信息,提交至微信平台进行审核。审核通过后,即可发布上线。
通过以上步骤,我们完成了一个集在线视频教育与作品集展示于一体的微信小程序从设计到上线的全过程。关键在于理解小程序的生命周期、前后端数据交互、用户认证以及微信平台的各种限制与规范。在实际开发中,你可能会遇到更多具体问题,如视频播放兼容性、列表页卡顿、文件上传中断等,此时需要结合官方文档、社区讨论和具体的错误信息进行针对性排查。这个项目作为一个起点,你可以在此基础上继续扩展,例如加入社交分享、消息通知、在线支付购买课程、更复杂的作品分类与标签系统等,使其功能更加完善。

1599

被折叠的 条评论
为什么被折叠?



