├── app
├── app.js
├── app.wxss
├── images
│ ├── logo.png
│ ├── qr.png
│ └── camera.png
├── config.js
├── pages
│ ├── index
│ │ ├── index.json
│ │ ├── index.js
│ │ ├── index.wxml
│ │ └── index.wxss
│ └── album
│ │ ├── album.wxss
│ │ ├── album.wxml
│ │ └── album.js
├── lib
│ ├── api.js
│ ├── request.js
│ └── util.js
└── app.json
├── server
├── globals.js
├── routes
│ ├── index.js
│ └── album
│ │ ├── routehub.js
│ │ └── handlers
│ │ ├── list.js
│ │ ├── delete.js
│ │ └── upload.js
├── process.json
├── services
│ └── cos
│ │ └── index.js
├── common
│ └── routerbase.js
├── package.json
├── config.js
├── app.js
└── middlewares
│ └── route_dispatcher.js
├── .gitignore
├── screenshot.png
├── LICENSE
└── README.md
/app/app.js:
--------------------------------------------------------------------------------
1 | App({
2 |
3 | });
--------------------------------------------------------------------------------
/server/globals.js:
--------------------------------------------------------------------------------
1 | global.SERVER_ROOT = __dirname;
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | npm-debug.log
3 | tmp
--------------------------------------------------------------------------------
/app/app.wxss:
--------------------------------------------------------------------------------
1 | page {
2 | background-color: #1c1b16;
3 | }
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CFETeam/weapp-demo-album/HEAD/screenshot.png
--------------------------------------------------------------------------------
/server/routes/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | '/album': 'album/routehub',
3 | };
4 |
--------------------------------------------------------------------------------
/app/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CFETeam/weapp-demo-album/HEAD/app/images/logo.png
--------------------------------------------------------------------------------
/app/images/qr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CFETeam/weapp-demo-album/HEAD/app/images/qr.png
--------------------------------------------------------------------------------
/app/images/camera.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CFETeam/weapp-demo-album/HEAD/app/images/camera.png
--------------------------------------------------------------------------------
/app/config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | /** 通讯域名 */
3 | host: 'www.qcloud.la',
4 | basePath: '/applet/album',
5 | };
--------------------------------------------------------------------------------
/app/pages/index/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarBackgroundColor": "#2277da",
3 | "navigationBarTextStyle": "white"
4 | }
--------------------------------------------------------------------------------
/app/pages/index/index.js:
--------------------------------------------------------------------------------
1 | Page({
2 | // 前往相册页
3 | gotoAlbum() {
4 | wx.navigateTo({ url: '../album/album' });
5 | },
6 | });
7 |
--------------------------------------------------------------------------------
/app/lib/api.js:
--------------------------------------------------------------------------------
1 | var config = require('../config.js');
2 |
3 | module.exports = {
4 | getUrl(route) {
5 | return `https://${config.host}${config.basePath}${route}`;
6 | },
7 | };
--------------------------------------------------------------------------------
/server/process.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "album",
3 | "script": "app.js",
4 | "cwd": "./",
5 | "exec_mode": "fork",
6 | "watch": true,
7 | "ignore_watch": ["tmp"],
8 | "env": {
9 | "NODE_ENV": "production"
10 | }
11 | }
--------------------------------------------------------------------------------
/server/routes/album/routehub.js:
--------------------------------------------------------------------------------
1 | module.exports = (router) => {
2 | // 获取图片列表
3 | router.get('/list', require('./handlers/list'));
4 |
5 | // 上传图片
6 | router.post('/upload', require('./handlers/upload'));
7 |
8 | // 删除图片
9 | router.post('/delete', require('./handlers/delete'));
10 | };
--------------------------------------------------------------------------------
/app/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "pages": [
3 | "pages/index/index",
4 | "pages/album/album"
5 | ],
6 |
7 | "window": {
8 | "backgroundTextStyle": "dark",
9 | "navigationBarBackgroundColor": "#373b3e",
10 | "navigationBarTitleText": "相册回收站",
11 | "windowBackground": "#1c1b16",
12 | "navigationBarTextStyle": "white"
13 | }
14 | }
--------------------------------------------------------------------------------
/server/services/cos/index.js:
--------------------------------------------------------------------------------
1 | const COS = require('cos-nodejs-sdk-v5')
2 | const _cosConfig = require('cos-nodejs-sdk-v5/sdk/config')
3 | const config = require('../../config')
4 |
5 | /**
6 | * init COS config
7 | * see: https://github.com/tencentyun/cos-nodejs-sdk-v5/blob/master/sdk/config.js#L24
8 | */
9 | _cosConfig.setAppInfo(config.cosAppId, config.cosSecretId, config.cosSecretKey);
10 |
11 | module.exports = COS
12 |
--------------------------------------------------------------------------------
/app/lib/request.js:
--------------------------------------------------------------------------------
1 | module.exports = (options) => {
2 | return new Promise((resolve, reject) => {
3 | options = Object.assign(options, {
4 | success(result) {
5 | if (result.statusCode === 200) {
6 | resolve(result.data);
7 | } else {
8 | reject(result);
9 | }
10 | },
11 |
12 | fail: reject,
13 | });
14 |
15 | wx.request(options);
16 | });
17 | };
--------------------------------------------------------------------------------
/app/pages/index/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 恭喜你
4 | 成功地搭建了一个微信小程序
5 |
6 |
7 |
8 |
9 |
10 | 分享二维码邀请好友结伴一起写小程序!
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/lib/util.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // 一维数组转二维数组
3 | listToMatrix(list, elementsPerSubArray) {
4 | let matrix = [], i, k;
5 |
6 | for (i = 0, k = -1; i < list.length; i += 1) {
7 | if (i % elementsPerSubArray === 0) {
8 | k += 1;
9 | matrix[k] = [];
10 | }
11 |
12 | matrix[k].push(list[i]);
13 | }
14 |
15 | return matrix;
16 | },
17 |
18 | // 为promise设置简单回调(无论成功或失败都执行)
19 | always(promise, callback) {
20 | promise.then(callback, callback);
21 | return promise;
22 | },
23 | };
--------------------------------------------------------------------------------
/server/common/routerbase.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 封装的路由公共基类,用于添加公用方法,不能直接实例化
3 | */
4 |
5 | class RouterBase {
6 | constructor(req, res, next) {
7 | Object.assign(this, { req, res, next });
8 | }
9 |
10 | /**
11 | * 静态工厂方法:创建用以响应路由的回调函数
12 | */
13 | static makeRouteHandler() {
14 | return (req, res, next) => new this(req, res, next).handle();
15 | }
16 |
17 | /**
18 | * 子类实现该方法处理请求
19 | */
20 | handle() {
21 | throw new Error(`Please implement instance method \`${this.constructor.name}::handle\`.`);
22 | }
23 | }
24 |
25 | module.exports = RouterBase;
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "album-server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start-dev": "nodemon app.js",
8 | "start": "pm2 start process.json"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "body-parser": "^1.15.2",
15 | "cos-nodejs-sdk-v5": "^1.0.0",
16 | "express": "^4.14.0",
17 | "file-type": "^3.8.0",
18 | "lodash": "^4.16.1",
19 | "morgan": "^1.7.0",
20 | "multiparty": "^4.1.2",
21 | "qcloud_cos": "^1.0.6",
22 | "read-chunk": "^2.0.0",
23 | "shortid": "^2.2.6"
24 | },
25 | "devDependencies": {
26 | "nodemon": "^1.10.2"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/server/config.js:
--------------------------------------------------------------------------------
1 | const CONF = {
2 | port: '9993',
3 | ROUTE_BASE_PATH: '/applet',
4 |
5 | /**
6 | * COS 信息配置
7 | * 查看:https://console.qcloud.com/capi
8 | */
9 |
10 | // APPID
11 | cosAppId: '',
12 | /**
13 | * 区域
14 | * 华北:cn-north
15 | * 华东:cn-east
16 | * 华南:cn-south
17 | * 西南:cn-southwest
18 | */
19 | cosRegion: 'cn-north',
20 | // SecretId
21 | cosSecretId: '',
22 | // SecretKey
23 | cosSecretKey: '',
24 | // Bucket 名称
25 | cosFileBucket: '',
26 | // 文件夹
27 | cosUploadFolder: '/'
28 | }
29 |
30 | // 生成访问 COS 的域名,无需修改
31 | CONF.cosDomain = (() => `http://${CONF.cosFileBucket}-${CONF.cosAppId}.costj.myqcloud.com/`)()
32 |
33 | module.exports = CONF
34 |
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | require('./globals');
2 |
3 | const http = require('http');
4 | const express = require('express');
5 | const bodyParser = require('body-parser');
6 | const morgan = require('morgan');
7 | const config = require('./config');
8 |
9 | const app = express();
10 |
11 | app.set('query parser', 'simple');
12 | app.set('case sensitive routing', true);
13 | app.set('jsonp callback name', 'callback');
14 | app.set('strict routing', true);
15 | app.set('trust proxy', true);
16 |
17 | app.disable('x-powered-by');
18 |
19 | // 记录请求日志
20 | app.use(morgan('tiny'));
21 |
22 | // parse `application/x-www-form-urlencoded`
23 | app.use(bodyParser.urlencoded({ extended: true }));
24 |
25 | // parse `application/json`
26 | app.use(bodyParser.json());
27 |
28 | app.use(require('./middlewares/route_dispatcher'));
29 |
30 | // 打印异常日志
31 | process.on('uncaughtException', error => {
32 | console.log(error);
33 | });
34 |
35 | // 启动server
36 | http.createServer(app).listen(config.port, () => {
37 | console.log('Express server listening on port: %s', config.port);
38 | });
39 |
--------------------------------------------------------------------------------
/server/middlewares/route_dispatcher.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 通用路由分发器
3 | */
4 |
5 | const express = require('express');
6 | const path = require('path');
7 | const _ = require('lodash');
8 | const config = require('../config');
9 | const routes = require('../routes');
10 |
11 | const routeOptions = { 'caseSensitive': true, 'strict': true };
12 | const routeDispatcher = express.Router(routeOptions);
13 |
14 | _.each(routes, (route, subpath) => {
15 | const router = express.Router(routeOptions);
16 |
17 | let routePath;
18 |
19 | // ignore `config.ROUTE_BASE_PATH` if `subpath` begin with `~`
20 | if (subpath[0] === '~') {
21 | routePath = subpath.slice(1);
22 | } else {
23 | routePath = config.ROUTE_BASE_PATH + subpath;
24 | }
25 |
26 | require(path.join(global.SERVER_ROOT, 'routes', route))(router);
27 |
28 | routeDispatcher.use(routePath, router, (err, req, res, next) => {
29 | // mute `URIError` error
30 | if (err instanceof URIError) {
31 | return next();
32 | }
33 |
34 | throw err;
35 | });
36 | });
37 |
38 | module.exports = routeDispatcher;
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | LICENSE - "MIT License"
2 |
3 | Copyright (c) 2016 by Tencent Cloud
4 |
5 | Permission is hereby granted, free of charge, to any person
6 | obtaining a copy of this software and associated documentation
7 | files (the "Software"), to deal in the Software without
8 | restriction, including without limitation the rights to use,
9 | copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the
11 | Software is furnished to do so, subject to the following
12 | conditions:
13 |
14 | The above copyright notice and this permission notice shall be
15 | included in all copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24 | OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/server/routes/album/handlers/list.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash');
2 | const path = require('path');
3 | const RouterBase = require('../../../common/routerbase');
4 | const config = require('../../../config');
5 | const cos = require('../../../services/cos');
6 |
7 | class ListImages extends RouterBase {
8 | handle() {
9 | const cosParams = {
10 | Bucket : config.cosFileBucket,
11 | Region : config.cosRegion,
12 | MaxKeys : 100
13 | }
14 |
15 | cos.getBucket(cosParams, (err, res) => {
16 |
17 | if (err) {
18 | this.res.json({ code: -1, msg: 'failed', data: {} });
19 | return;
20 | }
21 |
22 | this.res.json({
23 | code: 0,
24 | msg: 'ok',
25 | data: _.map(res.Contents, 'Key').filter(item => {
26 | let extname = String(path.extname(item)).toLowerCase();
27 |
28 | // 只返回`jpg/png`后缀图片
29 | return ['.jpg', '.png'].includes(extname)
30 | }).map(v => config.cosDomain + v),
31 | });
32 | });
33 | }
34 | }
35 |
36 | module.exports = ListImages.makeRouteHandler();
--------------------------------------------------------------------------------
/app/pages/album/album.wxss:
--------------------------------------------------------------------------------
1 | .container {
2 | width: 100%;
3 | height: 100%;
4 | }
5 |
6 | .album-container {
7 | margin: 0.1rem 0;
8 | }
9 |
10 | .item-group {
11 | display: flex;
12 | }
13 |
14 | .album-item {
15 | flex: 1;
16 | margin: 0.1rem;
17 | background: #333;
18 | text-align: center;
19 | height: 6.66rem;
20 | line-height: 6.66rem;
21 | }
22 |
23 | .album-item.empty {
24 | background: transparent;
25 | }
26 |
27 | .upload-image {
28 | color: #ccc;
29 | background: #333;
30 | position: absolute;
31 | left: 0.1rem;
32 | top: 0.2rem;
33 | width: 6.46rem;
34 | height: 6.66rem;
35 | text-align: center;
36 | line-height: 6.66rem;
37 | }
38 |
39 | .upload-image image {
40 | position: absolute;
41 | left: 0;
42 | width: 100%;
43 | height: 2.6rem;
44 | top: 1.2rem;
45 | }
46 |
47 | .upload-image text {
48 | position: absolute;
49 | width: 100%;
50 | height: 2rem;
51 | left: 0;
52 | top: 1.6rem;
53 | }
54 |
55 | .swiper-container {
56 | position: fixed;
57 | left: 0;
58 | top: 0;
59 | width: 100%;
60 | height: 100%;
61 | background: #000;
62 | }
63 |
64 | .swiper-container image {
65 | width: 100%;
66 | height: 100%;
67 | }
68 |
69 | action-sheet-item.warn {
70 | color: #e64340;
71 | }
--------------------------------------------------------------------------------
/server/routes/album/handlers/delete.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash');
2 | const RouterBase = require('../../../common/routerbase');
3 | const config = require('../../../config');
4 | const cos = require('../../../services/cos');
5 |
6 | class DeleteImage extends RouterBase {
7 | handle() {
8 | let filePath = String(this.req.body.filepath || '');
9 |
10 | try {
11 | filePath = decodeURIComponent(filePath);
12 |
13 | if (!filePath.startsWith(config.cosUploadFolder)) {
14 | throw new Error('operation forbidden');
15 | }
16 |
17 | } catch (err) {
18 | return this.res.json({
19 | code: -1,
20 | msg: 'failed',
21 | data: _.pick(err, ['name', 'message']),
22 | });
23 | }
24 |
25 | filePath = filePath.slice(1)
26 |
27 | const params = {
28 | Bucket: config.cosFileBucket,
29 | Region: config.cosRegion,
30 | Key: filePath
31 | }
32 |
33 | cos.deleteObject(params, (err, data) => {
34 | if (err) {
35 | this.res.json({
36 | code: -1,
37 | msg: 'failed',
38 | data: _.pick(err, ['name', 'message']),
39 | })
40 | } else {
41 | this.res.json({
42 | code: 0,
43 | msg: 'ok',
44 | data: {},
45 | })
46 | }
47 | })
48 | }
49 | }
50 |
51 | module.exports = DeleteImage.makeRouteHandler();
--------------------------------------------------------------------------------
/app/pages/album/album.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | 上传图片
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | 保存到本地
30 | 删除图片
31 | 取消
32 |
33 |
34 | {{loadingMessage}}
35 | {{toastMessage}}
--------------------------------------------------------------------------------
/server/routes/album/handlers/upload.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const multiparty = require('multiparty');
4 | const readChunk = require('read-chunk');
5 | const fileType = require('file-type');
6 | const shortid = require('shortid');
7 | const RouterBase = require('../../../common/routerbase');
8 | const config = require('../../../config');
9 | const cos = require('../../../services/cos');
10 |
11 | class ImageUploader extends RouterBase {
12 | constructor() {
13 | super(...arguments);
14 |
15 | // 图片允许上传的最大文件大小,单位(M)
16 | this.MAX_FILE_SIZE = 5;
17 | }
18 |
19 | handle() {
20 | const result = { 'code': -1, 'msg': '', 'data': {} };
21 |
22 | this.parseForm()
23 | .then(({ files }) => {
24 | if (!('image' in files)) {
25 | result.msg = '参数错误';
26 | return;
27 | }
28 |
29 | const imageFile = files.image[0];
30 |
31 | const buffer = readChunk.sync(imageFile.path, 0, 262);
32 | const resultType = fileType(buffer);
33 | if (!resultType || !['image/jpeg', 'image/png'].includes(resultType.mime)) {
34 | result.msg = '仅jpg/png格式';
35 | return;
36 | }
37 |
38 | const srcpath = imageFile.path
39 | const imgKey = `${Date.now()}-${shortid.generate()}.${resultType.ext}`
40 | const params = {
41 | Bucket: config.cosFileBucket,
42 | Region: config.cosRegion,
43 | Key: imgKey,
44 | Body: srcpath,
45 | ContentLength: imageFile.size
46 | }
47 |
48 | return new Promise((resolve, reject) => {
49 | cos.putObject(params, (err, data) => {
50 | if (err) reject(err)
51 |
52 | console.log(data)
53 | result.code = 0
54 | result.msg = 'ok'
55 | result.data.imgUrl = config.cosDomain + imgKey
56 | resolve()
57 |
58 | // remove uploaded file
59 | fs.unlink(srcpath)
60 |
61 | });
62 | });
63 |
64 | })
65 | .catch(e => {
66 | console.log(e)
67 | if (e.statusCode === 413) {
68 | result.msg = `单个不超过${this.MAX_FILE_SIZE}MB`;
69 | } else {
70 | result.msg = '图片上传失败,请稍候再试';
71 | }
72 | })
73 | .then(() => {
74 | this.res.json(result);
75 | });
76 | }
77 |
78 | parseForm() {
79 | const form = new multiparty.Form({
80 | encoding: 'utf8',
81 | maxFilesSize: this.MAX_FILE_SIZE * 1024 * 1024,
82 | autoFiles: true,
83 | uploadDir: path.join(global.SERVER_ROOT, 'tmp'),
84 | })
85 |
86 | return new Promise((resolve, reject) => {
87 | form.parse(this.req, (err, fields = {}, files = {}) => {
88 | err ? reject(err) : resolve({ fields, files })
89 | })
90 | })
91 | }
92 | }
93 |
94 | module.exports = ImageUploader.makeRouteHandler();
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 微信小程序示例 - 小相册
2 |
3 | 小相册是结合腾讯云[对象存储服务](https://www.qcloud.com/product/cos.html)(Cloud Object Service,简称COS)制作的一个微信小程序示例。在代码结构上包含如下两部分:
4 |
5 | - `app`: 小相册应用包代码,可直接在微信开发者工具中作为项目打开
6 | - `server`: 搭建的Node服务端代码,作为服务器和`app`通信,提供 CGI 接口示例用于拉取 COS 图片资源、上传图片到 COS、删除 COS 图片
7 |
8 | 小相册主要功能如下:
9 | * 列出 COS 服务器中的图片列表
10 | * 点击左上角**上传图片**图标,可以调用相机拍照或从手机相册选择图片,并将选中的图片上传到 COS 服务器中
11 | * 轻按任意图片,可进入全屏图片预览模式,并可左右滑动切换预览图片
12 | * 长按任意图片,可将其保存到本地,或从 COS 中删除
13 |
14 | 
15 |
16 |
17 | ## 部署和运行
18 |
19 | 拿到了本小程序源码的朋友可以尝试自己运行起来。
20 |
21 | ### 整体架构
22 |
23 | 
24 |
25 | ### 1. 准备域名和证书
26 |
27 | 在微信小程序中,所有的网路请求受到严格限制,不满足条件的域名和协议无法请求,具体包括:
28 |
29 | * 只允许和在 MP 中配置好的域名进行通信,如果还没有域名,需要注册一个。
30 | * 网络请求必须走 HTTPS 协议,所以你还需要为你的域名申请一个 SSL 证书。
31 |
32 | > 腾讯云提供[域名注册](https://www.qcloud.com/product/dm.html)和[证书申请](https://console.qcloud.com/ssl)服务,还没有域名或者证书的可以去使用
33 |
34 | 域名注册好之后,可以登录[微信公众平台](https://mp.weixin.qq.com)配置通信域名了。
35 |
36 | 
37 |
38 | ### 2. Nginx 和 Node 代码部署
39 |
40 | 小相册服务要运行,需要进行以下几步:
41 |
42 | * 部署 Nginx,Nginx 的安装和部署请大家自行搜索(注意需要把 SSL 模块也编译进去)
43 | * 配置 Nginx 反向代理到 `http://127.0.0.1:9993`
44 | * Node 运行环境,可以安装 [Node V6.6.0](https://nodejs.org/)
45 | * 部署 `server` 目录的代码到服务器上,如 `/data/release/qcloud-applet-album`
46 | * 使用 `npm install` 安装依赖模块
47 | * 使用 `npm install pm2 -g` 安装 pm2
48 |
49 | > 上述环境配置比较麻烦,剪刀石头布的服务器运行代码和配置已经打包成[腾讯云 CVM 镜像](https://buy.qcloud.com/cvm?marketImgId=371),推荐大家直接使用。
50 | > * 镜像部署完成之后,云主机上就有运行 WebSocket 服务的基本环境、代码和配置了。
51 | > * 腾讯云用户可以[免费领取礼包](https://www.qcloud.com/act/event/yingyonghao.html#section-voucher),体验腾讯云小程序解决方案。
52 | > * 镜像已包含「剪刀石头布」和「小相册」两个小程序的服务器环境与代码,需要体验两个小程序的朋友无需重复部署
53 |
54 | ### 3. 配置 HTTPS
55 |
56 | 镜像中已经部署了 nginx,需要在 `/etc/nginx/conf.d` 下修改配置中的域名、证书、私钥。
57 |
58 | 
59 |
60 |
61 | 配置完成后,即可启动 nginx。
62 |
63 | ```sh
64 | nginx
65 | ```
66 |
67 | ### 4. 域名解析
68 |
69 | 我们还需要添加域名记录解析到我们的云服务器上,这样才可以使用域名进行 HTTPS 服务。
70 |
71 | 在腾讯云注册的域名,可以直接使用[云解析控制台](https://console.qcloud.com/cns/domains)来添加主机记录,直接选择上面购买的 CVM。
72 |
73 | 
74 |
75 | 解析生效后,我们在浏览器使用域名就可以进行 HTTPS 访问。
76 |
77 | 
78 |
79 | ### 5. 开通和配置 COS
80 |
81 | 小相册示例的图片资源是存储在 COS 上的,要使用 COS 服务,需要登录 [COS 管理控制台](https://console.qcloud.com/cos/overview),然后在其中完成以下操作:
82 |
83 | - 开通 COS 服务分配得到唯一的`APP ID`
84 | - 使用密钥管理生成一对`SecretID`和`SecretKey`(用于调用 COS API)
85 | - 在 Bucket 列表中创建**公有读私有写**访问权限、**CDN加速**的 bucket(存储图片的目标容器)
86 | - 在创建的 bucket 容器中创建文件夹,命名为`photos`,图片将会上传存储到该目录下
87 |
88 | ### 6. 启动小相册示例 Node 服务
89 |
90 | 在镜像中,小相册示例的 Node 服务代码已部署在目录 `/data/release/qcloud-applet-album` 下:
91 |
92 | 进入该目录:
93 |
94 | ```bash
95 | cd /data/release/qcloud-applet-album
96 | ```
97 |
98 | 在该目录下有个名为`config.js`的配置文件(如下所示),按注释修改对应的 COS 配置:
99 |
100 | ```js
101 | module.exports = {
102 | // Node 监听的端口号
103 | port: '9993',
104 | ROUTE_BASE_PATH: '/applet',
105 |
106 | cosAppId: '填写开通 COS 时分配的 APP ID',
107 | cosSecretId: '填写密钥 SecretID',
108 | cosSecretKey: '填写密钥 SecretKey',
109 | cosFileBucket: '填写创建的公有读私有写的bucket名称',
110 | };
111 | ```
112 |
113 | 代码运行需要临时目录,在部署目录下创建一个临时目录 `tmp`。
114 |
115 | ```sh
116 | mkdir tmp
117 | ```
118 |
119 | 小相册示例使用`pm2`管理 Node 进程,执行以下命令启动 Node 服务:
120 |
121 | ```bash
122 | pm2 start process.json
123 | ```
124 |
125 | ### 7. 微信小程序服务器配置
126 |
127 | 进入微信公众平台管理后台设置服务器配置,配置类似如下设置:
128 |
129 | 
130 |
131 | 注意:需要将 `www.qcloud.la` 设置为上面申请的域名,将 downloadFile 合法域名设置为在 COS 管理控制台中自己创建的 bucket 的相应 **CDN 加速访问地址**,如下图所示:
132 |
133 | 
134 |
135 | ### 8. 启动小相册 Demo
136 |
137 | 在微信开发者工具将小相册应用包源码添加为项目,并把源文件`config.js`中的通讯域名修改成上面申请的域名。
138 |
139 | 
140 |
141 | 然后点击调试即可打开小相册 Demo 开始体验。
142 |
143 | 
144 |
145 | 这里有个问题。截止目前为止,微信小程序提供的上传和下载 API 无法在调试工具中正常工作,需要用手机微信扫码预览体验。
146 |
147 | ## 主要功能实现
148 |
149 | ### 上传图片
150 |
151 | 上传图片使用了微信小程序提供的`wx.chooseImage(OBJECT)`获取需要上传的文件路径,然后调用上传文件接口`wx.request(OBJECT)`发送 HTTPS POST 请求到自己指定的后台服务器。和传统表单文件上传一样,请求头`Content-Type`也是`multipart/form-data`。后台服务器收到请求后,使用 npm 模块 multiparty 解析 `multipart/form-data` 请求,将解析后的数据保存为指定目录下的临时文件。拿到临时文件的路径后,就可直接调用 COS SDK 提供的[文件上传 API](https://www.qcloud.com/doc/product/430/5947#.E6.96.87.E4.BB.B6.E4.B8.8A.E4.BC.A0) 进行图片存储,最后得到图片的存储路径及访问地址(存储的图片路径也可以直接在 COS 管理控制台看到)。
152 |
153 | ### 获取图片列表
154 |
155 | 调用[列举目录下文件&目录 API](https://www.qcloud.com/doc/product/430/5947#.E5.88.97.E4.B8.BE.E7.9B.AE.E5.BD.95.E4.B8.8B.E6.96.87.E4.BB.B6.26amp.3B.E7.9B.AE.E5.BD.95)可以获取到在 COS 服务端指定 bucket 和该 bucket 指定路径下存储的图片。
156 |
157 | ### 下载和保存图片
158 |
159 | 指定图片的访问地址,然后调用微信小程序提供的 `wx.downloadFile(OBJECT)` 和 `wx.saveFile(OBJECT)` 接口可以直接将图片下载和保存本地。这里要注意图片访问地址的域名需要和服务器配置的 downloadFile 合法域名一致,否则无法下载。
160 |
161 | ### 删除图片
162 |
163 | 删除图片也十分简单,直接调用[文件删除 API](https://www.qcloud.com/doc/product/430/5947#.E6.96.87.E4.BB.B6.E5.88.A0.E9.99.A4) 就可以将存储在 COS 服务端的图片删除。
164 |
165 | ## LICENSE
166 |
167 | [MIT](LICENSE)
168 |
--------------------------------------------------------------------------------
/app/pages/album/album.js:
--------------------------------------------------------------------------------
1 | const config = require('../../config.js');
2 | const { listToMatrix, always } = require('../../lib/util.js');
3 | const request = require('../../lib/request.js');
4 | const api = require('../../lib/api.js');
5 |
6 | Page({
7 | data: {
8 | // 相册列表数据
9 | albumList: [],
10 |
11 | // 图片布局列表(二维数组,由`albumList`计算而得)
12 | layoutList: [],
13 |
14 | // 布局列数
15 | layoutColumnSize: 3,
16 |
17 | // 是否显示loading
18 | showLoading: false,
19 |
20 | // loading提示语
21 | loadingMessage: '',
22 |
23 | // 是否显示toast
24 | showToast: false,
25 |
26 | // 提示消息
27 | toastMessage: '',
28 |
29 | // 是否显示动作命令
30 | showActionsSheet: false,
31 |
32 | // 当前操作的图片
33 | imageInAction: '',
34 |
35 | // 图片预览模式
36 | previewMode: false,
37 |
38 | // 当前预览索引
39 | previewIndex: 0,
40 | },
41 |
42 | // 显示loading提示
43 | showLoading(loadingMessage) {
44 | this.setData({ showLoading: true, loadingMessage });
45 | },
46 |
47 | // 隐藏loading提示
48 | hideLoading() {
49 | this.setData({ showLoading: false, loadingMessage: '' });
50 | },
51 |
52 | // 显示toast消息
53 | showToast(toastMessage) {
54 | this.setData({ showToast: true, toastMessage });
55 | },
56 |
57 | // 隐藏toast消息
58 | hideToast() {
59 | this.setData({ showToast: false, toastMessage: '' });
60 | },
61 |
62 | // 隐藏动作列表
63 | hideActionSheet() {
64 | this.setData({ showActionsSheet: false, imageInAction: '' });
65 | },
66 |
67 | onLoad() {
68 | this.renderAlbumList();
69 |
70 | this.getAlbumList().then((resp) => {
71 | if (resp.code !== 0) {
72 | // 图片列表加载失败
73 | return;
74 | }
75 |
76 | this.setData({ 'albumList': this.data.albumList.concat(resp.data) });
77 | this.renderAlbumList();
78 | });
79 | },
80 |
81 | // 获取相册列表
82 | getAlbumList() {
83 | this.showLoading('加载列表中…');
84 | setTimeout(() => this.hideLoading(), 1000);
85 | return request({ method: 'GET', url: api.getUrl('/list') });
86 | },
87 |
88 | // 渲染相册列表
89 | renderAlbumList() {
90 | let layoutColumnSize = this.data.layoutColumnSize;
91 | let layoutList = [];
92 |
93 | if (this.data.albumList.length) {
94 | layoutList = listToMatrix([0].concat(this.data.albumList), layoutColumnSize);
95 |
96 | let lastRow = layoutList[layoutList.length - 1];
97 | if (lastRow.length < layoutColumnSize) {
98 | let supplement = Array(layoutColumnSize - lastRow.length).fill(0);
99 | lastRow.push(...supplement);
100 | }
101 | }
102 |
103 | this.setData({ layoutList });
104 | },
105 |
106 | // 从相册选择照片或拍摄照片
107 | chooseImage() {
108 | wx.chooseImage({
109 | count: 9,
110 | sizeType: ['original', 'compressed'],
111 | sourceType: ['album', 'camera'],
112 |
113 | success: (res) => {
114 | this.showLoading('正在上传图片…');
115 |
116 | console.log(api.getUrl('/upload'));
117 | wx.uploadFile({
118 | url: api.getUrl('/upload'),
119 | filePath: res.tempFilePaths[0],
120 | name: 'image',
121 |
122 | success: (res) => {
123 | let response = JSON.parse(res.data);
124 |
125 | if (response.code === 0) {
126 | console.log(response);
127 |
128 | let albumList = this.data.albumList;
129 | albumList.unshift(response.data.imgUrl);
130 |
131 | this.setData({ albumList });
132 | this.renderAlbumList();
133 |
134 | this.showToast('图片上传成功');
135 | } else {
136 | console.log(response);
137 | }
138 | },
139 |
140 | fail: (res) => {
141 | console.log('fail', res);
142 | },
143 |
144 | complete: () => {
145 | this.hideLoading();
146 | },
147 | });
148 |
149 | },
150 | });
151 | },
152 |
153 | // 进入预览模式
154 | enterPreviewMode(event) {
155 | if (this.data.showActionsSheet) {
156 | return;
157 | }
158 |
159 | let imageUrl = event.target.dataset.src;
160 | let previewIndex = this.data.albumList.indexOf(imageUrl);
161 |
162 | this.setData({ previewMode: true, previewIndex: previewIndex });
163 | },
164 |
165 | // 退出预览模式
166 | leavePreviewMode() {
167 | this.setData({ previewMode: false, previewIndex: 0 });
168 | },
169 |
170 | // 显示可操作命令
171 | showActions(event) {
172 | this.setData({ showActionsSheet: true, imageInAction: event.target.dataset.src });
173 | },
174 |
175 | // 下载图片
176 | downloadImage() {
177 | this.showLoading('正在保存图片…');
178 | console.log('download_image_url', this.data.imageInAction);
179 |
180 | wx.downloadFile({
181 | url: this.data.imageInAction,
182 | type: 'image',
183 | success: (resp) => {
184 | wx.saveFile({
185 | tempFilePath: resp.tempFilePath,
186 | success: (resp) => {
187 | this.showToast('图片保存成功');
188 | },
189 |
190 | fail: (resp) => {
191 | console.log('fail', resp);
192 | },
193 |
194 | complete: (resp) => {
195 | console.log('complete', resp);
196 | this.hideLoading();
197 | },
198 | });
199 | },
200 |
201 | fail: (resp) => {
202 | console.log('fail', resp);
203 | },
204 | });
205 |
206 | this.setData({ showActionsSheet: false, imageInAction: '' });
207 | },
208 |
209 | // 删除图片
210 | deleteImage() {
211 | let imageUrl = this.data.imageInAction;
212 | let filepath = '/' + imageUrl.split('/').slice(3).join('/');
213 |
214 | this.showLoading('正在删除图片…');
215 | this.setData({ showActionsSheet: false, imageInAction: '' });
216 |
217 | request({
218 | method: 'POST',
219 | url: api.getUrl('/delete'),
220 | data: { filepath },
221 | })
222 | .then((resp) => {
223 | if (resp.code !== 0) {
224 | // 图片删除失败
225 | return;
226 | }
227 |
228 | // 从图片列表中移除
229 | let index = this.data.albumList.indexOf(imageUrl);
230 | if (~index) {
231 | let albumList = this.data.albumList;
232 | albumList.splice(index, 1);
233 |
234 | this.setData({ albumList });
235 | this.renderAlbumList();
236 | }
237 |
238 | this.showToast('图片删除成功');
239 | })
240 | .catch(error => {
241 | console.log('failed', error);
242 | })
243 | .then(() => {
244 | this.hideLoading();
245 | });
246 | },
247 | });
--------------------------------------------------------------------------------
/app/pages/index/index.wxss:
--------------------------------------------------------------------------------
1 | .page-top {
2 | width: 750rpx;
3 | height: 594rpx;
4 | background-image: url(data:image/png;base64,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);
5 | background-repeat: no-repeat;
6 | background-size: 750rpx 694rpx;
7 | background-position:0 -40rpx;
8 | position: relative;
9 | z-index: 2;
10 | }
11 | .username,.text-info {
12 | position: absolute;
13 | left:50%;
14 | transform: translateX(-50%);
15 | white-space: nowrap;
16 | }
17 | .username {
18 | font-size: 40rpx;
19 | color: #fff;
20 | top:339rpx;
21 | }
22 | .text-info {
23 | font-size: 32rpx;
24 | color:#bdd0ee;
25 | top:400rpx;
26 | }
27 | .page-btn-wrap {
28 | position: absolute;
29 | top: 470rpx;
30 | width: 100%;
31 | text-align: center;
32 | }
33 | .page-btn {
34 | position:relative;
35 | margin:0 20rpx;
36 | padding:0;
37 | box-sizing:border-box;
38 | font-size:32rpx;
39 | text-decoration:none;
40 | -webkit-tap-highlight-color:transparent;
41 | overflow:hidden;
42 | display: inline-block;
43 | width: 300rpx;
44 | height: 85rpx;
45 | background-color: #fff;
46 | color: #2277da;
47 | line-height: 85rpx;
48 | }
49 | .page-bottom {
50 | background-color: #fff;
51 | width: 100%;
52 | height: 100%;
53 | position: absolute;
54 | top: 0;
55 | left: 0;
56 | padding: 624rpx 0 0;
57 | z-index: 1;
58 | box-sizing: border-box;
59 | }
60 | .qr-img {
61 | display: block;
62 | width: 300rpx;
63 | height: 300rpx;
64 | margin: 40rpx auto;
65 | }
66 | .qr-txt {
67 | display: block;
68 | color: #666;
69 | font-size: 32rpx;
70 | margin: 20rpx auto 0;
71 | text-align: center;
72 | }
73 | .page-logo {
74 | display: block;
75 | width: 200rpx;
76 | height: 54rpx;
77 | margin: 40rpx auto 40rpx;
78 | }
--------------------------------------------------------------------------------