Browse Source

add template

charblus 5 years ago
commit
d6fe278e21
69 changed files with 14116 additions and 0 deletions
  1. 18 0
      .babelrc
  2. 9 0
      .editorconfig
  3. 2 0
      .eslintignore
  4. 42 0
      .eslintrc.js
  5. 13 0
      .gitignore
  6. 7 0
      .postcssrc.js
  7. 3 0
      .vscode/settings.json
  8. 37 0
      README.md
  9. 45 0
      build/build.js
  10. 48 0
      build/check-versions.js
  11. 9 0
      build/dev-client.js
  12. 111 0
      build/dev-server.js
  13. 117 0
      build/utils.js
  14. 21 0
      build/vue-loader.conf.js
  15. 151 0
      build/webpack.base.conf.js
  16. 85 0
      build/webpack.dev.conf.js
  17. 120 0
      build/webpack.prod.conf.js
  18. 6 0
      config/dev.env.js
  19. 68 0
      config/index.js
  20. 3 0
      config/prod.env.js
  21. 11 0
      index.html
  22. 9328 0
      package-lock.json
  23. 94 0
      package.json
  24. 12 0
      package.swan.json
  25. 33 0
      project.config.json
  26. 12 0
      project.swan.json
  27. 16 0
      server/.eslintrc.json
  28. 126 0
      server/README.md
  29. 19 0
      server/app.js
  30. 55 0
      server/config.js
  31. 5 0
      server/controllers/demo.js
  32. 30 0
      server/controllers/index.js
  33. 10 0
      server/controllers/login.js
  34. 29 0
      server/controllers/message.js
  35. 156 0
      server/controllers/tunnel.js
  36. 9 0
      server/controllers/upload.js
  37. 11 0
      server/controllers/user.js
  38. 31 0
      server/middlewares/response.js
  39. 16 0
      server/nodemon.json
  40. 2770 0
      server/package-lock.json
  41. 34 0
      server/package.json
  42. 14 0
      server/process.prod.json
  43. 36 0
      server/qcloud.js
  44. 37 0
      server/routes/index.js
  45. 28 0
      server/tools.md
  46. 37 0
      server/tools/cAuth.sql
  47. 42 0
      server/tools/initdb.js
  48. 43 0
      src/App.vue
  49. 10 0
      src/config.js
  50. 42 0
      src/main.js
  51. 14 0
      src/pages/books/Book.vue
  52. 11 0
      src/pages/books/main.js
  53. 14 0
      src/pages/comments/Comment.vue
  54. 5 0
      src/pages/comments/main.js
  55. 13 0
      src/pages/me/Me.vue
  56. 5 0
      src/pages/me/main.js
  57. 43 0
      src/util.js
  58. 0 0
      static/.gitkeep
  59. BIN
      static/img/book-active.png
  60. BIN
      static/img/book.png
  61. BIN
      static/img/me-active.png
  62. BIN
      static/img/me.png
  63. BIN
      static/img/other-active.png
  64. BIN
      static/img/other.png
  65. BIN
      static/img/room-active.png
  66. BIN
      static/img/room.png
  67. BIN
      static/img/todo-active.png
  68. BIN
      static/img/todo.png
  69. BIN
      static/img/unlogin.png

+ 18 - 0
.babelrc

@@ -0,0 +1,18 @@
+{
+  "presets": [
+    ["env", {
+      "modules": false,
+      "targets": {
+        "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
+      }
+    }],
+    "stage-2"
+  ],
+  "plugins": ["transform-runtime"],
+  "env": {
+    "test": {
+      "presets": ["env", "stage-2"],
+      "plugins": ["istanbul"]
+    }
+  }
+}

+ 9 - 0
.editorconfig

@@ -0,0 +1,9 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true

+ 2 - 0
.eslintignore

@@ -0,0 +1,2 @@
+build/*.js
+config/*.js

+ 42 - 0
.eslintrc.js

@@ -0,0 +1,42 @@
+// http://eslint.org/docs/user-guide/configuring
+
+module.exports = {
+  root: true,
+  parser: 'babel-eslint',
+  parserOptions: {
+    sourceType: 'module'
+  },
+  env: {
+    browser: false,
+    node: true,
+    es6: true
+  },
+  // https://github.com/standard/standard/blob/master/docs/RULES-en.md
+  extends: 'standard',
+  // required to lint *.vue files
+  plugins: [
+    'html'
+  ],
+  // add your custom rules here
+  'rules': {
+    // allow paren-less arrow functions
+    'arrow-parens': 0,
+    // allow async-await
+    'generator-star-spacing': 0,
+    // allow debugger during development
+    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
+  },
+  globals: {
+    App: true,
+    Page: true,
+    wx: true,
+    swan: true,
+    tt: true,
+    my: true,
+    getApp: true,
+    getPage: true,
+    requirePlugin: true,
+    mpvue: true,
+    mpvuePlatform: true
+  }
+}

+ 13 - 0
.gitignore

@@ -0,0 +1,13 @@
+.DS_Store
+node_modules/
+dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Editor directories and files
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln

+ 7 - 0
.postcssrc.js

@@ -0,0 +1,7 @@
+// https://github.com/michael-ciniawsky/postcss-load-config
+
+module.exports = {
+  "plugins": {
+    "postcss-mpvue-wxss": {}
+  }
+}

+ 3 - 0
.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+    "editor.tabSize": 2
+}

+ 37 - 0
README.md

@@ -0,0 +1,37 @@
+# my-mpvue
+
+> A Mpvue project
+
+## Build Setup
+
+``` bash
+# 初始化项目
+vue init mpvue/mpvue-quickstart myproject
+cd myproject
+
+# 安装依赖
+yarn
+
+# 开发时构建
+npm dev
+
+# 打包构建
+npm build
+
+# 指定平台的开发时构建(微信、百度、头条、支付宝)
+npm dev:wx
+npm dev:swan
+npm dev:tt
+npm dev:my
+
+# 指定平台的打包构建
+npm build:wx
+npm build:swan
+npm build:tt
+npm build:my
+
+# 生成 bundle 分析报告
+npm run build --report
+```
+
+For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).

+ 45 - 0
build/build.js

@@ -0,0 +1,45 @@
+require('./check-versions')()
+
+process.env.NODE_ENV = 'production'
+process.env.PLATFORM = process.argv[process.argv.length - 1] || 'wx'
+
+var ora = require('ora')
+var rm = require('rimraf')
+var path = require('path')
+var chalk = require('chalk')
+var webpack = require('webpack')
+var config = require('../config')
+var webpackConfig = require('./webpack.prod.conf')
+var utils = require('./utils')
+
+var spinner = ora('building for production...')
+spinner.start()
+
+rm(path.join(config.build.assetsRoot, '*'), err => {
+  if (err) throw err
+  webpack(webpackConfig, function (err, stats) {
+    spinner.stop()
+    if (err) throw err
+    if (process.env.PLATFORM === 'swan') {
+      utils.writeFrameworkinfo()
+    }
+    process.stdout.write(stats.toString({
+      colors: true,
+      modules: false,
+      children: false,
+      chunks: false,
+      chunkModules: false
+    }) + '\n\n')
+
+    if (stats.hasErrors()) {
+      console.log(chalk.red('  Build failed with errors.\n'))
+      process.exit(1)
+    }
+
+    console.log(chalk.cyan('  Build complete.\n'))
+    console.log(chalk.yellow(
+      '  Tip: built files are meant to be served over an HTTP server.\n' +
+      '  Opening index.html over file:// won\'t work.\n'
+    ))
+  })
+})

+ 48 - 0
build/check-versions.js

@@ -0,0 +1,48 @@
+var chalk = require('chalk')
+var semver = require('semver')
+var packageConfig = require('../package.json')
+var shell = require('shelljs')
+function exec (cmd) {
+  return require('child_process').execSync(cmd).toString().trim()
+}
+
+var versionRequirements = [
+  {
+    name: 'node',
+    currentVersion: semver.clean(process.version),
+    versionRequirement: packageConfig.engines.node
+  }
+]
+
+if (shell.which('npm')) {
+  versionRequirements.push({
+    name: 'npm',
+    currentVersion: exec('npm --version'),
+    versionRequirement: packageConfig.engines.npm
+  })
+}
+
+module.exports = function () {
+  var warnings = []
+  for (var i = 0; i < versionRequirements.length; i++) {
+    var mod = versionRequirements[i]
+    if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
+      warnings.push(mod.name + ': ' +
+        chalk.red(mod.currentVersion) + ' should be ' +
+        chalk.green(mod.versionRequirement)
+      )
+    }
+  }
+
+  if (warnings.length) {
+    console.log('')
+    console.log(chalk.yellow('To use this template, you must update following to modules:'))
+    console.log()
+    for (var i = 0; i < warnings.length; i++) {
+      var warning = warnings[i]
+      console.log('  ' + warning)
+    }
+    console.log()
+    process.exit(1)
+  }
+}

+ 9 - 0
build/dev-client.js

@@ -0,0 +1,9 @@
+/* eslint-disable */
+require('eventsource-polyfill')
+var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
+
+hotClient.subscribe(function (event) {
+  if (event.action === 'reload') {
+    window.location.reload()
+  }
+})

+ 111 - 0
build/dev-server.js

@@ -0,0 +1,111 @@
+require('./check-versions')()
+
+process.env.PLATFORM = process.argv[process.argv.length - 1] || 'wx'
+var config = require('../config')
+if (!process.env.NODE_ENV) {
+  process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
+}
+
+// var opn = require('opn')
+var path = require('path')
+var express = require('express')
+var webpack = require('webpack')
+var proxyMiddleware = require('http-proxy-middleware')
+var portfinder = require('portfinder')
+var webpackConfig = require('./webpack.dev.conf')
+var utils = require('./utils')
+
+// default port where dev server listens for incoming traffic
+var port = process.env.PORT || config.dev.port
+// automatically open browser, if not set will be false
+var autoOpenBrowser = !!config.dev.autoOpenBrowser
+// Define HTTP proxies to your custom API backend
+// https://github.com/chimurai/http-proxy-middleware
+var proxyTable = config.dev.proxyTable
+
+var app = express()
+var compiler = webpack(webpackConfig)
+if (process.env.PLATFORM === 'swan') {
+  utils.writeFrameworkinfo()
+}
+
+// var devMiddleware = require('webpack-dev-middleware')(compiler, {
+//   publicPath: webpackConfig.output.publicPath,
+//   quiet: true
+// })
+
+// var hotMiddleware = require('webpack-hot-middleware')(compiler, {
+//   log: false,
+//   heartbeat: 2000
+// })
+// force page reload when html-webpack-plugin template changes
+// compiler.plugin('compilation', function (compilation) {
+//   compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
+//     hotMiddleware.publish({ action: 'reload' })
+//     cb()
+//   })
+// })
+
+// proxy api requests
+Object.keys(proxyTable).forEach(function (context) {
+  var options = proxyTable[context]
+  if (typeof options === 'string') {
+    options = { target: options }
+  }
+  app.use(proxyMiddleware(options.filter || context, options))
+})
+
+// handle fallback for HTML5 history API
+app.use(require('connect-history-api-fallback')())
+
+// serve webpack bundle output
+// app.use(devMiddleware)
+
+// enable hot-reload and state-preserving
+// compilation error display
+// app.use(hotMiddleware)
+
+// serve pure static assets
+var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
+app.use(staticPath, express.static('./static'))
+
+// var uri = 'http://localhost:' + port
+
+var _resolve
+var readyPromise = new Promise(resolve => {
+  _resolve = resolve
+})
+
+// console.log('> Starting dev server...')
+// devMiddleware.waitUntilValid(() => {
+//   console.log('> Listening at ' + uri + '\n')
+//   // when env is testing, don't need open it
+//   if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
+//     opn(uri)
+//   }
+//   _resolve()
+// })
+
+module.exports = new Promise((resolve, reject) => {
+  portfinder.basePort = port
+  portfinder.getPortPromise()
+  .then(newPort => {
+      if (port !== newPort) {
+        console.log(`${port}端口被占用,开启新端口${newPort}`)
+      }
+      var server = app.listen(newPort, 'localhost')
+      // for 小程序的文件保存机制
+      require('webpack-dev-middleware-hard-disk')(compiler, {
+        publicPath: webpackConfig.output.publicPath,
+        quiet: true
+      })
+      resolve({
+        ready: readyPromise,
+        close: () => {
+          server.close()
+        }
+      })
+  }).catch(error => {
+    console.log('没有找到空闲端口,请打开任务管理器杀死进程端口再试', error)
+  })
+})

+ 117 - 0
build/utils.js

@@ -0,0 +1,117 @@
+var path = require('path')
+var fs = require('fs')
+var config = require('../config')
+var ExtractTextPlugin = require('extract-text-webpack-plugin')
+var mpvueInfo = require('../node_modules/mpvue/package.json')
+var packageInfo = require('../package.json')
+var mkdirp = require('mkdirp')
+
+exports.assetsPath = function (_path) {
+  var assetsSubDirectory = process.env.NODE_ENV === 'production'
+    ? config.build.assetsSubDirectory
+    : config.dev.assetsSubDirectory
+  return path.posix.join(assetsSubDirectory, _path)
+}
+
+exports.cssLoaders = function (options) {
+  options = options || {}
+
+  var cssLoader = {
+    loader: 'css-loader',
+    options: {
+      minimize: process.env.NODE_ENV === 'production',
+      sourceMap: options.sourceMap
+    }
+  }
+
+  var postcssLoader = {
+    loader: 'postcss-loader',
+    options: {
+      sourceMap: true
+    }
+  }
+
+  var px2rpxLoader = {
+    loader: 'px2rpx-loader',
+    options: {
+      baseDpr: 1,
+      rpxUnit: 0.5
+    }
+  }
+
+  // generate loader string to be used with extract text plugin
+  function generateLoaders (loader, loaderOptions) {
+    var loaders = [cssLoader, px2rpxLoader, postcssLoader]
+    if (loader) {
+      loaders.push({
+        loader: loader + '-loader',
+        options: Object.assign({}, loaderOptions, {
+          sourceMap: options.sourceMap
+        })
+      })
+    }
+
+    // Extract CSS when that option is specified
+    // (which is the case during production build)
+    if (options.extract) {
+      return ExtractTextPlugin.extract({
+        use: loaders,
+        fallback: 'vue-style-loader'
+      })
+    } else {
+      return ['vue-style-loader'].concat(loaders)
+    }
+  }
+
+  // https://vue-loader.vuejs.org/en/configurations/extract-css.html
+  return {
+    css: generateLoaders(),
+    wxss: generateLoaders(),
+    postcss: generateLoaders(),
+    less: generateLoaders('less'),
+    sass: generateLoaders('sass', { indentedSyntax: true }),
+    scss: generateLoaders('sass'),
+    stylus: generateLoaders('stylus'),
+    styl: generateLoaders('stylus')
+  }
+}
+
+// Generate loaders for standalone style files (outside of .vue)
+exports.styleLoaders = function (options) {
+  var output = []
+  var loaders = exports.cssLoaders(options)
+  for (var extension in loaders) {
+    var loader = loaders[extension]
+    output.push({
+      test: new RegExp('\\.' + extension + '$'),
+      use: loader
+    })
+  }
+  return output
+}
+
+const writeFile = async (filePath, content) => {
+  let dir = path.dirname(filePath)
+  let exist = fs.existsSync(dir)
+  if (!exist) {
+    await mkdirp(dir)
+  }
+  await fs.writeFileSync(filePath, content, 'utf8')
+}
+
+exports.writeFrameworkinfo = function () {
+  var buildInfo = {
+    'toolName': mpvueInfo.name,
+    'toolFrameWorkVersion': mpvueInfo.version,
+    'toolCliVersion': packageInfo.mpvueTemplateProjectVersion || '',
+    'createTime': Date.now()
+  }
+
+  var content = JSON.stringify(buildInfo)
+  var fileName = '.frameworkinfo'
+  var rootDir = path.resolve(__dirname, `../${fileName}`)
+  var distDir = path.resolve(config.build.assetsRoot, `./${fileName}`)
+
+  writeFile(rootDir, content)
+  writeFile(distDir, content)
+}

+ 21 - 0
build/vue-loader.conf.js

@@ -0,0 +1,21 @@
+var utils = require('./utils')
+var config = require('../config')
+// var isProduction = process.env.NODE_ENV === 'production'
+// for mp
+var isProduction = true
+
+module.exports = {
+  loaders: utils.cssLoaders({
+    sourceMap: isProduction
+      ? config.build.productionSourceMap
+      : config.dev.cssSourceMap,
+    extract: isProduction
+  }),
+  transformToRequire: {
+    video: 'src',
+    source: 'src',
+    img: 'src',
+    image: 'xlink:href'
+  },
+  fileExt: config.build.fileExt
+}

+ 151 - 0
build/webpack.base.conf.js

@@ -0,0 +1,151 @@
+var path = require('path')
+var fs = require('fs')
+var utils = require('./utils')
+var config = require('../config')
+var webpack = require('webpack')
+var merge = require('webpack-merge')
+var vueLoaderConfig = require('./vue-loader.conf')
+var MpvuePlugin = require('webpack-mpvue-asset-plugin')
+var glob = require('glob')
+var CopyWebpackPlugin = require('copy-webpack-plugin')
+var relative = require('relative')
+
+function resolve (dir) {
+  return path.join(__dirname, '..', dir)
+}
+
+function getEntry (rootSrc) {
+  var map = {};
+  glob.sync(rootSrc + '/pages/**/main.js')
+  .forEach(file => {
+    var key = relative(rootSrc, file).replace('.js', '');
+    map[key] = file;
+  })
+   return map;
+}
+
+const appEntry = { app: resolve('./src/main.js') }
+const pagesEntry = getEntry(resolve('./src'), 'pages/**/main.js')
+const entry = Object.assign({}, appEntry, pagesEntry)
+
+let baseWebpackConfig = {
+  // 如果要自定义生成的 dist 目录里面的文件路径,
+  // 可以将 entry 写成 {'toPath': 'fromPath'} 的形式,
+  // toPath 为相对于 dist 的路径, 例:index/demo,则生成的文件地址为 dist/index/demo.js
+  entry,
+  target: require('mpvue-webpack-target'),
+  output: {
+    path: config.build.assetsRoot,
+    jsonpFunction: 'webpackJsonpMpvue',
+    filename: '[name].js',
+    publicPath: process.env.NODE_ENV === 'production'
+      ? config.build.assetsPublicPath
+      : config.dev.assetsPublicPath
+  },
+  resolve: {
+    extensions: ['.js', '.vue', '.json'],
+    alias: {
+      'vue': 'mpvue',
+      '@': resolve('src')
+    },
+    symlinks: false,
+    aliasFields: ['mpvue', 'weapp', 'browser'],
+    mainFields: ['browser', 'module', 'main']
+  },
+  module: {
+    rules: [
+      {
+        test: /\.(js|vue)$/,
+        loader: 'eslint-loader',
+        enforce: 'pre',
+        include: [resolve('src'), resolve('test')],
+        options: {
+          formatter: require('eslint-friendly-formatter')
+        }
+      },
+      {
+        test: /\.vue$/,
+        loader: 'mpvue-loader',
+        options: vueLoaderConfig
+      },
+      {
+        test: /\.js$/,
+        include: [resolve('src'), resolve('test')],
+        use: [
+          'babel-loader',
+          {
+            loader: 'mpvue-loader',
+            options: Object.assign({checkMPEntry: true}, vueLoaderConfig)
+          },
+        ]
+      },
+      {
+        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
+        loader: 'url-loader',
+        options: {
+          limit: 10000,
+          name: utils.assetsPath('img/[name].[ext]')
+        }
+      },
+      {
+        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
+        loader: 'url-loader',
+        options: {
+          limit: 10000,
+          name: utils.assetsPath('media/[name].[ext]')
+        }
+      },
+      {
+        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
+        loader: 'url-loader',
+        options: {
+          limit: 10000,
+          name: utils.assetsPath('fonts/[name].[ext]')
+        }
+      }
+    ]
+  },
+  plugins: [
+    // api 统一桥协议方案
+    new webpack.DefinePlugin({
+      'mpvue': 'global.mpvue',
+      'mpvuePlatform': 'global.mpvuePlatform'
+    }),
+    new MpvuePlugin(),
+    new CopyWebpackPlugin([{
+      from: '**/*.json',
+      to: ''
+    }], {
+      context: 'src/'
+    }),
+    new CopyWebpackPlugin([
+      {
+        from: path.resolve(__dirname, '../static'),
+        to: path.resolve(config.build.assetsRoot, './static'),
+        ignore: ['.*']
+      }
+    ])
+  ]
+}
+
+// 针对百度小程序,由于不支持通过 miniprogramRoot 进行自定义构建完的文件的根路径
+// 所以需要将项目根路径下面的 project.swan.json 拷贝到构建目录
+// 然后百度开发者工具将 dist/swan 作为项目根目录打
+const projectConfigMap = {
+  tt: '../project.config.json',
+  swan: '../project.swan.json'
+}
+
+const PLATFORM = process.env.PLATFORM
+if (/^(swan)|(tt)$/.test(PLATFORM)) {
+  baseWebpackConfig = merge(baseWebpackConfig, {
+    plugins: [
+      new CopyWebpackPlugin([{
+        from: path.resolve(__dirname, projectConfigMap[PLATFORM]),
+        to: path.resolve(config.build.assetsRoot)
+      }])
+    ]
+  })
+}
+
+module.exports = baseWebpackConfig

+ 85 - 0
build/webpack.dev.conf.js

@@ -0,0 +1,85 @@
+var utils = require('./utils')
+var webpack = require('webpack')
+var config = require('../config')
+var merge = require('webpack-merge')
+var baseWebpackConfig = require('./webpack.base.conf')
+// var HtmlWebpackPlugin = require('html-webpack-plugin')
+var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
+var MpvueVendorPlugin = require('webpack-mpvue-vendor-plugin')
+
+// copy from ./webpack.prod.conf.js
+var path = require('path')
+var ExtractTextPlugin = require('extract-text-webpack-plugin')
+var CopyWebpackPlugin = require('copy-webpack-plugin')
+var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
+
+// add hot-reload related code to entry chunks
+// Object.keys(baseWebpackConfig.entry).forEach(function (name) {
+//   baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
+// })
+
+module.exports = merge(baseWebpackConfig, {
+  module: {
+    rules: utils.styleLoaders({
+      sourceMap: config.dev.cssSourceMap,
+      extract: true
+    })
+  },
+  // cheap-module-eval-source-map is faster for development
+  // devtool: '#cheap-module-eval-source-map',
+  // devtool: '#source-map',
+  output: {
+    path: config.build.assetsRoot,
+    // filename: utils.assetsPath('[name].[chunkhash].js'),
+    // chunkFilename: utils.assetsPath('[id].[chunkhash].js')
+    filename: utils.assetsPath('[name].js'),
+    chunkFilename: utils.assetsPath('[id].js')
+  },
+  plugins: [
+    new webpack.DefinePlugin({
+      'process.env': config.dev.env
+    }),
+
+    // copy from ./webpack.prod.conf.js
+    // extract css into its own file
+    new ExtractTextPlugin({
+      // filename: utils.assetsPath('[name].[contenthash].css')
+      filename: utils.assetsPath(`[name].${config.dev.fileExt.style}`)
+    }),
+    // Compress extracted CSS. We are using this plugin so that possible
+    // duplicated CSS from different components can be deduped.
+    new OptimizeCSSPlugin({
+      cssProcessorOptions: {
+        safe: true
+      }
+    }),
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'common/vendor',
+      minChunks: function (module, count) {
+        // any required modules inside node_modules are extracted to vendor
+        return (
+          module.resource &&
+          /\.js$/.test(module.resource) &&
+          module.resource.indexOf('node_modules') >= 0
+        ) || count > 1
+      }
+    }),
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'common/manifest',
+      chunks: ['common/vendor']
+    }),
+    new MpvueVendorPlugin({
+      platform: process.env.PLATFORM
+    }),
+    // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
+    // new webpack.HotModuleReplacementPlugin(),
+    new webpack.NoEmitOnErrorsPlugin(),
+    // https://github.com/ampedandwired/html-webpack-plugin
+    // new HtmlWebpackPlugin({
+    //   filename: 'index.html',
+    //   template: 'index.html',
+    //   inject: true
+    // }),
+    new FriendlyErrorsPlugin()
+  ]
+})

+ 120 - 0
build/webpack.prod.conf.js

@@ -0,0 +1,120 @@
+var path = require('path')
+var utils = require('./utils')
+var webpack = require('webpack')
+var config = require('../config')
+var merge = require('webpack-merge')
+var baseWebpackConfig = require('./webpack.base.conf')
+var UglifyJsPlugin = require('uglifyjs-webpack-plugin')
+var CopyWebpackPlugin = require('copy-webpack-plugin')
+// var HtmlWebpackPlugin = require('html-webpack-plugin')
+var ExtractTextPlugin = require('extract-text-webpack-plugin')
+var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
+var MpvueVendorPlugin = require('webpack-mpvue-vendor-plugin')
+var env = config.build.env
+
+var webpackConfig = merge(baseWebpackConfig, {
+  module: {
+    rules: utils.styleLoaders({
+      sourceMap: config.build.productionSourceMap,
+      extract: true
+    })
+  },
+  devtool: config.build.productionSourceMap ? '#source-map' : false,
+  output: {
+    path: config.build.assetsRoot,
+    // filename: utils.assetsPath('[name].[chunkhash].js'),
+    // chunkFilename: utils.assetsPath('[id].[chunkhash].js')
+    filename: utils.assetsPath('[name].js'),
+    chunkFilename: utils.assetsPath('[id].js')
+  },
+  plugins: [
+    // http://vuejs.github.io/vue-loader/en/workflow/production.html
+    new webpack.DefinePlugin({
+      'process.env': env
+    }),
+    // extract css into its own file
+    new ExtractTextPlugin({
+      // filename: utils.assetsPath('[name].[contenthash].css')
+      filename: utils.assetsPath(`[name].${config.build.fileExt.style}`)
+    }),
+    // Compress extracted CSS. We are using this plugin so that possible
+    // duplicated CSS from different components can be deduped.
+    new OptimizeCSSPlugin({
+      cssProcessorOptions: {
+        safe: true
+      }
+    }),
+    // generate dist index.html with correct asset hash for caching.
+    // you can customize output by editing /index.html
+    // see https://github.com/ampedandwired/html-webpack-plugin
+    // new HtmlWebpackPlugin({
+    //   filename: config.build.index,
+    //   template: 'index.html',
+    //   inject: true,
+    //   minify: {
+    //     removeComments: true,
+    //     collapseWhitespace: true,
+    //     removeAttributeQuotes: true
+    //     // more options:
+    //     // https://github.com/kangax/html-minifier#options-quick-reference
+    //   },
+    //   // necessary to consistently work with multiple chunks via CommonsChunkPlugin
+    //   chunksSortMode: 'dependency'
+    // }),
+    // keep module.id stable when vender modules does not change
+    new webpack.HashedModuleIdsPlugin(),
+    // split vendor js into its own file
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'common/vendor',
+      minChunks: function (module, count) {
+        // any required modules inside node_modules are extracted to vendor
+        return (
+          module.resource &&
+          /\.js$/.test(module.resource) &&
+          module.resource.indexOf('node_modules') >= 0
+        ) || count > 1
+      }
+    }),
+    // extract webpack runtime and module manifest to its own file in order to
+    // prevent vendor hash from being updated whenever app bundle is updated
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'common/manifest',
+      chunks: ['common/vendor']
+    }),
+    new MpvueVendorPlugin({
+      platform: process.env.PLATFORM
+    })
+  ]
+})
+
+// if (config.build.productionGzip) {
+//   var CompressionWebpackPlugin = require('compression-webpack-plugin')
+
+//   webpackConfig.plugins.push(
+//     new CompressionWebpackPlugin({
+//       asset: '[path].gz[query]',
+//       algorithm: 'gzip',
+//       test: new RegExp(
+//         '\\.(' +
+//         config.build.productionGzipExtensions.join('|') +
+//         ')$'
+//       ),
+//       threshold: 10240,
+//       minRatio: 0.8
+//     })
+//   )
+// }
+
+if (config.build.bundleAnalyzerReport) {
+  var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
+  webpackConfig.plugins.push(new BundleAnalyzerPlugin())
+}
+
+var useUglifyJs = process.env.PLATFORM !== 'swan'
+if (useUglifyJs) {
+  webpackConfig.plugins.push(new UglifyJsPlugin({
+    sourceMap: true
+  }))
+}
+
+module.exports = webpackConfig

+ 6 - 0
config/dev.env.js

@@ -0,0 +1,6 @@
+var merge = require('webpack-merge')
+var prodEnv = require('./prod.env')
+
+module.exports = merge(prodEnv, {
+  NODE_ENV: '"development"'
+})

+ 68 - 0
config/index.js

@@ -0,0 +1,68 @@
+// see http://vuejs-templates.github.io/webpack for documentation.
+var path = require('path')
+var fileExtConfig = {
+    swan: {
+        template: 'swan',
+        script: 'js',
+        style: 'css',
+        platform: 'swan'
+    },
+    tt: {
+        template: 'ttml',
+        script: 'js',
+        style: 'ttss',
+        platform: 'tt'
+    },
+    wx: {
+        template: 'wxml',
+        script: 'js',
+        style: 'wxss',
+        platform: 'wx'
+    },
+    my: {
+        template: 'axml',
+        script: 'js',
+        style: 'acss',
+        platform: 'my'
+    }
+}
+var fileExt = fileExtConfig[process.env.PLATFORM]
+
+module.exports = {
+  build: {
+    env: require('./prod.env'),
+    index: path.resolve(__dirname, `../dist/${fileExt.platform}/index.html`),
+    assetsRoot: path.resolve(__dirname, `../dist/${fileExt.platform}`),
+    assetsSubDirectory: '',
+    assetsPublicPath: '/',
+    productionSourceMap: false,
+    // Gzip off by default as many popular static hosts such as
+    // Surge or Netlify already gzip all static assets for you.
+    // Before setting to `true`, make sure to:
+    // npm install --save-dev compression-webpack-plugin
+    productionGzip: false,
+    productionGzipExtensions: ['js', 'css'],
+    // Run the build command with an extra argument to
+    // View the bundle analyzer report after build finishes:
+    // `npm run build --report`
+    // Set to `true` or `false` to always turn it on or off
+    bundleAnalyzerReport: process.env.npm_config_report,
+    fileExt: fileExt
+  },
+  dev: {
+    env: require('./dev.env'),
+    port: 8080,
+    // 在小程序开发者工具中不需要自动打开浏览器
+    autoOpenBrowser: false,
+    assetsSubDirectory: '',
+    assetsPublicPath: '/',
+    proxyTable: {},
+    // CSS Sourcemaps off by default because relative paths are "buggy"
+    // with this option, according to the CSS-Loader README
+    // (https://github.com/webpack/css-loader#sourcemaps)
+    // In our experience, they generally work as expected,
+    // just be aware of this issue when enabling this option.
+    cssSourceMap: false,
+    fileExt: fileExt
+  }
+}

+ 3 - 0
config/prod.env.js

@@ -0,0 +1,3 @@
+module.exports = {
+  NODE_ENV: '"production"'
+}

+ 11 - 0
index.html

@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>my-mpvue</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

File diff suppressed because it is too large
+ 9328 - 0
package-lock.json


+ 94 - 0
package.json

@@ -0,0 +1,94 @@
+{
+  "name": "my-mpvue",
+  "version": "1.0.0",
+  "mpvueTemplateProjectVersion": "0.1.0",
+  "description": "A Mpvue project",
+  "author": "charblus <charblus7582@gmail.com>",
+  "private": true,
+  "scripts": {
+    "dev:wx": "node build/dev-server.js wx",
+    "start:wx": "npm run dev:wx",
+    "build:wx": "node build/build.js wx",
+    "dev:swan": "node build/dev-server.js swan",
+    "start:swan": "npm run dev:swan",
+    "build:swan": "node build/build.js swan",
+    "dev:tt": "node build/dev-server.js tt",
+    "start:tt": "npm run dev:tt",
+    "build:tt": "node build/build.js tt",
+    "dev:my": "node build/dev-server.js my",
+    "start:my": "npm run dev:my",
+    "build:my": "node build/build.js my",
+    "dev": "node build/dev-server.js wx",
+    "start": "npm run dev",
+    "build": "node build/build.js wx",
+    "lint": "eslint --fix --ext .js,.vue src"
+  },
+  "dependencies": {
+    "mpvue": "^2.0.0",
+    "vuex": "^3.0.1"
+  },
+  "devDependencies": {
+    "babel-core": "^6.22.1",
+    "babel-loader": "^7.1.1",
+    "babel-plugin-transform-runtime": "^6.22.0",
+    "babel-preset-env": "^1.3.2",
+    "babel-preset-stage-2": "^6.22.0",
+    "babel-register": "^6.22.0",
+    "chalk": "^2.4.0",
+    "connect-history-api-fallback": "^1.3.0",
+    "copy-webpack-plugin": "^4.5.1",
+    "css-loader": "^0.28.11",
+    "cssnano": "^3.10.0",
+    "eslint": "^4.19.1",
+    "eslint-friendly-formatter": "^4.0.1",
+    "eslint-loader": "^2.0.0",
+    "eslint-plugin-import": "^2.11.0",
+    "eslint-plugin-node": "^6.0.1",
+    "eslint-plugin-html": "^4.0.3",
+    "eslint-config-standard": "^11.0.0",
+    "eslint-plugin-promise": "^3.4.0",
+    "eslint-plugin-standard": "^3.0.1",
+    "babel-eslint": "^8.2.3",
+    "eventsource-polyfill": "^0.9.6",
+    "express": "^4.16.3",
+    "extract-text-webpack-plugin": "^3.0.2",
+    "file-loader": "^1.1.11",
+    "friendly-errors-webpack-plugin": "^1.7.0",
+    "glob": "^7.1.2",
+    "html-webpack-plugin": "^3.2.0",
+    "http-proxy-middleware": "^0.18.0",
+    "optimize-css-assets-webpack-plugin": "^3.2.0",
+    "ora": "^2.0.0",
+    "portfinder": "^1.0.13",
+    "postcss-loader": "^2.1.4",
+    "postcss-mpvue-wxss": "^1.0.0",
+    "prettier": "~1.12.1",
+    "px2rpx-loader": "^0.1.10",
+    "relative": "^3.0.2",
+    "rimraf": "^2.6.0",
+    "semver": "^5.3.0",
+    "shelljs": "^0.8.1",
+    "uglifyjs-webpack-plugin": "^1.2.5",
+    "url-loader": "^1.0.1",
+    "vue-style-loader": "^4.1.0",
+    "mkdirp": "^0.5.1",
+    "mpvue-loader": "^2.0.0",
+    "mpvue-template-compiler": "^2.0.0",
+    "mpvue-webpack-target": "^1.0.3",
+    "webpack-mpvue-vendor-plugin": "^2.0.0",
+    "webpack-mpvue-asset-plugin": "^2.0.0",
+    "webpack-bundle-analyzer": "^2.2.1",
+    "webpack-dev-middleware-hard-disk": "^1.12.0",
+    "webpack-merge": "^4.1.0",
+    "webpack": "^3.11.0"
+  },
+  "engines": {
+    "node": ">= 4.0.0",
+    "npm": ">= 3.0.0"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not ie <= 8"
+  ]
+}

+ 12 - 0
package.swan.json

@@ -0,0 +1,12 @@
+{
+  "appid": "touristappid",
+  "setting": {
+    "urlCheck": false
+  },
+  "condition": {
+    "swan": {
+      "current": -1,
+      "list": []
+    }
+  }
+}

+ 33 - 0
project.config.json

@@ -0,0 +1,33 @@
+{
+	"description": "项目配置文件。",
+	"setting": {
+		"urlCheck": true,
+		"es6": false,
+		"postcss": true,
+		"minified": true,
+		"newFeature": true
+	},
+	"miniprogramRoot": "dist/wx/",
+	"qcloudRoot": "server/",
+	"compileType": "miniprogram",
+	"appid": "wxa28c90f932b1fb61",
+	"projectname": "my-mpvue",
+	"condition": {
+		"search": {
+			"current": -1,
+			"list": []
+		},
+		"conversation": {
+			"current": -1,
+			"list": []
+		},
+		"game": {
+			"currentL": -1,
+			"list": []
+		},
+		"miniprogram": {
+			"current": -1,
+			"list": []
+		}
+	}
+}

+ 12 - 0
project.swan.json

@@ -0,0 +1,12 @@
+{
+  "appid": "testappid",
+  "setting": {
+    "urlCheck": false
+  },
+  "condition": {
+    "swan": {
+      "current": -1,
+      "list": []
+    }
+  }
+}

+ 16 - 0
server/.eslintrc.json

@@ -0,0 +1,16 @@
+{
+  "root": true,
+  "parser": "babel-eslint",
+  "parserOptions": {
+    "sourceType": "module"
+  },
+  "extends": "standard",
+  "rules": {
+    "indent": [2, 4, { "SwitchCase": 1 }],
+    "arrow-parens": 0,
+    "generator-star-spacing": 0
+  },
+  "env": {
+    "mocha": true
+  }
+}

+ 126 - 0
server/README.md

@@ -0,0 +1,126 @@
+# 腾讯云小程序解决方案 Demo - Node.js
+
+Node.js 版本 Wafer SDK 的服务端 Demo
+
+## 下载源码
+
+你可以直接通过 git 将代码 clone 到本地,也可以点击[这里](https://github.com/tencentyun/wafer-node-server-demo/releases)下载。
+
+```bash
+git clone https://github.com/tencentyun/wafer-node-server-demo.git
+```
+
+## 开始使用
+
+#### 安装依赖
+
+```bash
+# 安装全局依赖
+npm i pm2 nodemon -g
+
+# 安装项目依赖
+npm i
+```
+
+#### 启动项目
+
+```bash
+# 开发环境,监听文件变化自动重启,并会输出 debug 信息
+tnpm run dev
+
+# 线上部署环境
+tnpm start
+```
+
+按照[小程序创建资源配置指引](https://github.com/tencentyun/weapp-doc)进行操作,可以得到运行本示例所需的资源和服务,其中包括已部署好的示例代码及自动下发的 SDK 配置文件 `/etc/qcloud/sdk.config`。
+
+- 示例代码部署目录:`/data/release/node-weapp-demo`
+- 运行示例的 Node 版本:`v8.1.0`
+- Node 进程管理工具:`pm2`
+
+## 项目结构
+
+```
+koa-weapp-demo
+├── README.md
+├── app.js
+├── controllers
+│   ├── index.js
+│   ├── login.js
+│   ├── message.js
+│   ├── tunnel.js
+│   ├── upload.js
+│   └── user.js
+├── middlewares
+│   └── response.js
+├── config.js
+├── package.json
+├── process.json
+├── nodemon.json
+├── qcloud.js
+└── routes
+    └── index.js
+```
+`app.js` 是 Demo 的主入口文件,Demo 使用 Koa 框架,在 `app.js` 创建一个 Koa 实例并响应请求。
+
+`routes/index.js` 是 Demo 的路由定义文件
+
+`controllers` 存放 Demo 所有业务逻辑的目录,`index.js` 不需要修改,他会动态的将 `controllers` 文件夹下的目录结构映射成 modules 的 Object,例如 Demo 中的目录将会被映射成如下的结构:
+
+```javascript
+// index.js 输出
+{
+  login: require('login'),
+  message: require('message'),
+  tunnel: require('tunnel'),
+  upload: require('upload'),
+  user: require('user')
+}
+```
+
+`qcloud.js` 导出了一个 SDK 的单例,包含了所有的 SDK 接口,之后使用的时候只需要 `require` 这个文件就行,无需重复初始化 SDK。
+
+`config.js` 主要的配置如下:
+
+```javascript
+{
+  port: '5757',                             // 项目启动的端口
+
+  appId: 'wx00dd00dd00dd00dd',              // 微信小程序 App ID
+  appSecret: 'abcdefg',                     // 微信小程序 App Secret
+  wxLoginExpires: 7200,                     // 微信登录态有效期
+  useQcloudLogin: false,                    // 是否使用腾讯云代理登录
+
+  /**
+   * MySQL 配置,用来存储用户登录态和用户信息
+   * 如果不提供 MySQL 配置,模式会使用自动配置好的本地镜像中的 MySQL 储存信息
+   * 具体查看文档-登录态储存和校验
+   **/
+  mysql: {
+    host: 'localhost',
+    port: 3306,
+    user: 'root',
+    db: 'cAuth',
+    pass: '',
+    char: 'utf8'
+  },
+  
+  // COS 配置,用于上传模块使用
+  cos: {
+    /**
+     * 区域
+     * 华北:cn-north
+     * 华东:cn-east
+     * 华南:cn-south
+     * 西南:cn-southwest
+     */
+    region: 'cn-south',
+    fileBucket: 'test',                    // Bucket 名称
+    uploadFolder: ''                       // 文件夹
+  }
+}
+```
+
+除了 `config.js` ,腾讯云还会在你初始化小程序解决方案的时候,向你的机器下发 `sdk.config`,里面包含了你的腾讯云 AppId、SecretId、SecretKey 和服务器等信息,无需修改,`qcloud.js` 会自动引入。如果你想要在自己的机器上部署 SDK 的 Demo,请查看[自行部署 Demo 说明]()。
+
+除此以外,关于 SDK 的详细配置信息,还可以查看 [SDK 的 API 文档]()。

+ 19 - 0
server/app.js

@@ -0,0 +1,19 @@
+const Koa = require('koa')
+const app = new Koa()
+const debug = require('debug')('koa-weapp-demo')
+const response = require('./middlewares/response')
+const bodyParser = require('koa-bodyparser')
+const config = require('./config')
+
+// 使用响应处理中间件
+app.use(response)
+
+// 解析请求体
+app.use(bodyParser())
+
+// 引入路由分发
+const router = require('./routes')
+app.use(router.routes())
+
+// 启动程序,监听端口
+app.listen(config.port, () => debug(`listening on port ${config.port}`))

+ 55 - 0
server/config.js

@@ -0,0 +1,55 @@
+const CONF = {
+    serverHost: 'localhost',
+    tunnelServerUrl: '',
+    tunnelSignatureKey: '27fb7d1c161b7ca52d73cce0f1d833f9f5b5ec89',
+    // 腾讯云相关配置可以查看云 API 密钥控制台:https://console.cloud.tencent.com/capi
+    qcloudAppId: '1253858020',
+    qcloudSecretId: 'AKIDve8qJWnBerpdHhbTpwnQo21kDRQ9OqRq',
+    qcloudSecretKey: 'JOEtJVWQ6XjgR4TluoTs5C3f1JCpDLEt',
+    wxMessageToken: 'weixinmsgtoken',
+    networkTimeout: 30000,
+
+    port: '5757',
+    rootPathname: '',
+
+    // 微信小程序 App ID
+    appId: '',
+
+    // 微信小程序 App Secret
+    appSecret: '',
+
+    // 是否使用腾讯云代理登录小程序
+    useQcloudLogin: true,
+
+    /**
+     * MySQL 配置,用来存储 session 和用户信息
+     * 若使用了腾讯云微信小程序解决方案
+     * 开发环境下,MySQL 的初始密码为您的微信小程序 appid
+     */
+    mysql: {
+        host: 'localhost',
+        port: 3306,
+        user: 'root',
+        db: 'cAuth',
+        pass: '12345678',
+        char: 'utf8mb4'
+    },
+
+    cos: {
+        /**
+         * 地区简称
+         * @查看 https://cloud.tencent.com/document/product/436/6224
+         */
+        region: 'ap-guangzhou',
+        // Bucket 名称
+        fileBucket: 'qcloudtest',
+        // 文件夹
+        uploadFolder: ''
+    },
+
+    // 微信登录态有效期
+    wxLoginExpires: 7200
+    // wxMessageToken: 'abcdefgh'
+}
+
+module.exports = CONF

+ 5 - 0
server/controllers/demo.js

@@ -0,0 +1,5 @@
+module.exports = async (ctx) => {
+    ctx.state.data = {
+        msg: 'hello 小程序后台'
+    }
+}

+ 30 - 0
server/controllers/index.js

@@ -0,0 +1,30 @@
+const _ = require('lodash')
+const fs = require('fs')
+const path = require('path')
+
+/**
+ * 映射 d 文件夹下的文件为模块
+ */
+const mapDir = d => {
+    const tree = {}
+
+    // 获得当前文件夹下的所有的文件夹和文件
+    const [dirs, files] = _(fs.readdirSync(d)).partition(p => fs.statSync(path.join(d, p)).isDirectory())
+
+    // 映射文件夹
+    dirs.forEach(dir => {
+        tree[dir] = mapDir(path.join(d, dir))
+    })
+
+    // 映射文件
+    files.forEach(file => {
+        if (path.extname(file) === '.js') {
+            tree[path.basename(file, '.js')] = require(path.join(d, file))
+        }
+    })
+
+    return tree
+}
+
+// 默认导出当前文件夹下的映射
+module.exports = mapDir(path.join(__dirname))

+ 10 - 0
server/controllers/login.js

@@ -0,0 +1,10 @@
+// 登录授权接口
+module.exports = async (ctx, next) => {
+    // 通过 Koa 中间件进行登录之后
+    // 登录信息会被存储到 ctx.state.$wxInfo
+    // 具体查看:
+    if (ctx.state.$wxInfo.loginState) {
+        ctx.state.data = ctx.state.$wxInfo.userinfo
+        ctx.state.data['time'] = Math.floor(Date.now() / 1000)
+    }
+}

+ 29 - 0
server/controllers/message.js

@@ -0,0 +1,29 @@
+const { message: { checkSignature } } = require('../qcloud')
+
+/**
+ * 响应 GET 请求(响应微信配置时的签名检查请求)
+ */
+async function get (ctx, next) {
+    const { signature, timestamp, nonce, echostr } = ctx.query
+    if (checkSignature(signature, timestamp, nonce)) ctx.body = echostr
+    else ctx.body = 'ERR_WHEN_CHECK_SIGNATURE'
+}
+
+async function post (ctx, next) {
+    // 检查签名,确认是微信发出的请求
+    const { signature, timestamp, nonce } = ctx.query
+    if (!checkSignature(signature, timestamp, nonce)) ctx.body = 'ERR_WHEN_CHECK_SIGNATURE'
+
+    /**
+     * 解析微信发送过来的请求体
+     * 可查看微信文档:https://mp.weixin.qq.com/debug/wxadoc/dev/api/custommsg/receive.html#接收消息和事件
+     */
+    const body = ctx.request.body
+
+    ctx.body = 'success'
+}
+
+module.exports = {
+    post,
+    get
+}

+ 156 - 0
server/controllers/tunnel.js

@@ -0,0 +1,156 @@
+const { tunnel } = require('../qcloud')
+const debug = require('debug')('koa-weapp-demo')
+
+/**
+ * 这里实现一个简单的聊天室
+ * userMap 为 tunnelId 和 用户信息的映射
+ * 实际使用请使用数据库存储
+ */
+const userMap = {}
+
+// 保存 当前已连接的 WebSocket 信道ID列表
+const connectedTunnelIds = []
+
+/**
+ * 调用 tunnel.broadcast() 进行广播
+ * @param  {String} type    消息类型
+ * @param  {String} content 消息内容
+ */
+const $broadcast = (type, content) => {
+    tunnel.broadcast(connectedTunnelIds, type, content)
+        .then(result => {
+            const invalidTunnelIds = result.data && result.data.invalidTunnelIds || []
+
+            if (invalidTunnelIds.length) {
+                console.log('检测到无效的信道 IDs =>', invalidTunnelIds)
+
+                // 从 userMap 和 connectedTunnelIds 中将无效的信道记录移除
+                invalidTunnelIds.forEach(tunnelId => {
+                    delete userMap[tunnelId]
+
+                    const index = connectedTunnelIds.indexOf(tunnelId)
+                    if (~index) {
+                        connectedTunnelIds.splice(index, 1)
+                    }
+                })
+            }
+        })
+}
+
+/**
+ * 调用 TunnelService.closeTunnel() 关闭信道
+ * @param  {String} tunnelId 信道ID
+ */
+const $close = (tunnelId) => {
+    tunnel.closeTunnel(tunnelId)
+}
+
+/**
+ * 实现 onConnect 方法
+ * 在客户端成功连接 WebSocket 信道服务之后会调用该方法,
+ * 此时通知所有其它在线的用户当前总人数以及刚加入的用户是谁
+ */
+function onConnect (tunnelId) {
+    console.log(`[onConnect] =>`, { tunnelId })
+
+    if (tunnelId in userMap) {
+        connectedTunnelIds.push(tunnelId)
+
+        $broadcast('people', {
+            'total': connectedTunnelIds.length,
+            'enter': userMap[tunnelId]
+        })
+    } else {
+        console.log(`Unknown tunnelId(${tunnelId}) was connectd, close it`)
+        $close(tunnelId)
+    }
+}
+
+/**
+ * 实现 onMessage 方法
+ * 客户端推送消息到 WebSocket 信道服务器上后,会调用该方法,此时可以处理信道的消息。
+ * 在本示例,我们处理 `speak` 类型的消息,该消息表示有用户发言。
+ * 我们把这个发言的信息广播到所有在线的 WebSocket 信道上
+ */
+function onMessage (tunnelId, type, content) {
+    console.log(`[onMessage] =>`, { tunnelId, type, content })
+
+    switch (type) {
+        case 'speak':
+            if (tunnelId in userMap) {
+                $broadcast('speak', {
+                    'who': userMap[tunnelId],
+                    'word': content.word
+                })
+            } else {
+                $close(tunnelId)
+            }
+            break
+
+        default:
+            break
+    }
+}
+
+/**
+ * 实现 onClose 方法
+ * 客户端关闭 WebSocket 信道或者被信道服务器判断为已断开后,
+ * 会调用该方法,此时可以进行清理及通知操作
+ */
+function onClose (tunnelId) {
+    console.log(`[onClose] =>`, { tunnelId })
+
+    if (!(tunnelId in userMap)) {
+        console.log(`[onClose][Invalid TunnelId]=>`, tunnelId)
+        $close(tunnelId)
+        return
+    }
+
+    const leaveUser = userMap[tunnelId]
+    delete userMap[tunnelId]
+
+    const index = connectedTunnelIds.indexOf(tunnelId)
+    if (~index) {
+        connectedTunnelIds.splice(index, 1)
+    }
+
+    // 聊天室没有人了(即无信道ID)不再需要广播消息
+    if (connectedTunnelIds.length > 0) {
+        $broadcast('people', {
+            'total': connectedTunnelIds.length,
+            'leave': leaveUser
+        })
+    }
+}
+
+module.exports = {
+    // 小程序请求 websocket 地址
+    get: async ctx => {
+        const data = await tunnel.getTunnelUrl(ctx.req)
+        const tunnelInfo = data.tunnel
+
+        userMap[tunnelInfo.tunnelId] = data.userinfo
+
+        ctx.state.data = tunnelInfo
+    },
+
+    // 信道将信息传输过来的时候
+    post: async ctx => {
+        const packet = await tunnel.onTunnelMessage(ctx.request.body)
+
+        debug('Tunnel recive a package: %o', packet)
+
+        switch (packet.type) {
+            case 'connect':
+                onConnect(packet.tunnelId)
+                break
+            case 'message':
+                onMessage(packet.tunnelId, packet.content.messageType, packet.content.messageContent)
+                break
+            case 'close':
+                onClose(packet.tunnelId)
+                break
+        }
+    }
+
+}

+ 9 - 0
server/controllers/upload.js

@@ -0,0 +1,9 @@
+const { uploader } = require('../qcloud')
+
+module.exports = async ctx => {
+    // 获取上传之后的结果
+    // 具体可以查看:
+    const data = await uploader(ctx.req)
+
+    ctx.state.data = data
+}

+ 11 - 0
server/controllers/user.js

@@ -0,0 +1,11 @@
+module.exports = async (ctx, next) => {
+    // 通过 Koa 中间件进行登录态校验之后
+    // 登录信息会被存储到 ctx.state.$wxInfo
+    // 具体查看:
+    if (ctx.state.$wxInfo.loginState === 1) {
+        // loginState 为 1,登录态校验成功
+        ctx.state.data = ctx.state.$wxInfo.userinfo
+    } else {
+        ctx.state.code = -1
+    }
+}

+ 31 - 0
server/middlewares/response.js

@@ -0,0 +1,31 @@
+const debug = require('debug')('koa-weapp-demo')
+
+/**
+ * 响应处理模块
+ */
+module.exports = async function (ctx, next) {
+    try {
+        // 调用下一个 middleware
+        await next()
+
+        // 处理响应结果
+        // 如果直接写入在 body 中,则不作处理
+        // 如果写在 ctx.body 为空,则使用 state 作为响应
+        ctx.body = ctx.body ? ctx.body : {
+            code: ctx.state.code !== undefined ? ctx.state.code : 0,
+            data: ctx.state.data !== undefined ? ctx.state.data : {}
+        }
+    } catch (e) {
+        // catch 住全局的错误信息
+        debug('Catch Error: %o', e)
+
+        // 设置状态码为 200 - 服务端错误
+        ctx.status = 200
+
+        // 输出详细的错误信息
+        ctx.body = {
+            code: -1,
+            error: e && e.message ? e.message : e.toString()
+        }
+    }
+}

+ 16 - 0
server/nodemon.json

@@ -0,0 +1,16 @@
+{
+    "restartable": "rs",
+    "ignore": [
+        ".git",
+        "node_modules/**/node_modules"
+    ],
+    "verbose": true,
+    "execMap": {
+        "js": "node --harmony"
+    },
+    "env": {
+        "NODE_ENV": "development",
+        "DEBUG": "*,-nodemon:*,-nodemon,-knex:pool"
+    },
+    "ext": "js json"
+}

File diff suppressed because it is too large
+ 2770 - 0
server/package-lock.json


+ 34 - 0
server/package.json

@@ -0,0 +1,34 @@
+{
+    "name": "koa-weapp-demo",
+    "version": "1.0.0",
+    "description": "",
+    "main": "app.js",
+    "scripts": {
+        "start": "pm2 start process.prod.json --no-daemon",
+        "dev": "nodemon --config nodemon.json app.js",
+        "initdb": "npm install && node tools/initdb.js"
+    },
+    "author": "Jason",
+    "license": "MIT",
+    "dependencies": {
+        "axios": "^0.15.3",
+        "knex": "^0.13.0",
+        "koa": "^2.0.0",
+        "koa-bodyparser": "^3.2.0",
+        "koa-log4": "^2.1.0",
+        "koa-router": "^7.0.1",
+        "lodash": "^4.17.4",
+        "mkdir-p": "0.0.7",
+        "mysql": "^2.14.1",
+        "pify": "^2.3.0",
+        "wafer-node-sdk": "^1.4.0"
+    },
+    "devDependencies": {
+        "babel-eslint": "^7.1.0",
+        "debug": "^2.6.8",
+        "eslint": "^3.9.1",
+        "eslint-config-standard": "^6.2.1",
+        "eslint-plugin-promise": "^3.3.1",
+        "eslint-plugin-standard": "^2.0.1"
+    }
+}

+ 14 - 0
server/process.prod.json

@@ -0,0 +1,14 @@
+{
+    "name": "session",
+    "script": "app.js",
+    "cwd": "./",
+    "exec_mode": "fork",
+    "watch": true,
+    "ignore_watch": ["tmp"],
+    "env": {
+        "NODE_ENV": "production"
+    },
+    "engines": {
+        "node": ">=7.6"
+    }
+}

+ 36 - 0
server/qcloud.js

@@ -0,0 +1,36 @@
+const fs = require('fs')
+const qcloud = require('wafer-node-sdk')
+
+// 获取基础配置
+const configs = require('./config')
+
+// 获取 sdk.config
+const sdkConfig = (() => {
+    const sdkConfigPath = '/data/release/sdk.config.json'
+
+    // 检查文件是否存在
+    try {
+        const stats = fs.statSync(sdkConfigPath)
+
+        if (!stats.isFile()) {
+            console.log('sdk.config.json 不存在,将使用 config.js 中的配置')
+            return {}
+        }
+    } catch (e) {
+        return {}
+    }
+
+    // 返回配置信息
+    try {
+        const content = fs.readFileSync(sdkConfigPath, 'utf8')
+        return JSON.parse(content)
+    } catch (e) {
+        // 如果配置读取错误或者 JSON 解析错误,则输出空配置项
+        console.log('sdk.config.json 解析错误,不是 JSON 字符串')
+        return {}
+    }
+})()
+
+// 初始化 SDK
+// 将基础配置和 sdk.config 合并传入 SDK 并导出初始化完成的 SDK
+module.exports = qcloud(Object.assign({}, sdkConfig, configs))

+ 37 - 0
server/routes/index.js

@@ -0,0 +1,37 @@
+/**
+ * ajax 服务路由集合
+ */
+const router = require('koa-router')({
+    prefix: '/weapp'
+})
+const controllers = require('../controllers')
+
+// 从 sdk 中取出中间件
+// 这里展示如何使用 Koa 中间件完成登录态的颁发与验证
+const { auth: { authorizationMiddleware, validationMiddleware } } = require('../qcloud')
+
+// --- 登录与授权 Demo --- //
+// 登录接口
+router.get('/login', authorizationMiddleware, controllers.login)
+// 用户信息接口(可以用来验证登录态)
+router.get('/user', validationMiddleware, controllers.user)
+
+// --- 图片上传 Demo --- //
+// 图片上传接口,小程序端可以直接将 url 填入 wx.uploadFile 中
+router.post('/upload', controllers.upload)
+
+// --- 信道服务接口 Demo --- //
+// GET  用来响应请求信道地址的
+router.get('/tunnel', controllers.tunnel.get)
+// POST 用来处理信道传递过来的消息
+router.post('/tunnel', controllers.tunnel.post)
+
+// --- 客服消息接口 Demo --- //
+// GET  用来响应小程序后台配置时发送的验证请求
+router.get('/message', controllers.message.get)
+// POST 用来处理微信转发过来的客服消息
+router.post('/message', controllers.message.post)
+
+// 测试 demo api //
+router.get('/demo', controllers.demo)
+module.exports = router

+ 28 - 0
server/tools.md

@@ -0,0 +1,28 @@
+# 腾讯云小程序解决方案 Demo 工具使用文档
+
+本文件夹下的脚本为腾讯云小程序解决方案 Demo 配套的工具,旨在让用户方便快捷的使用并创建小程序的开发环境。
+
+工具包括:
+
+- [数据库初始化工具](#数据库初始化工具)
+
+## 数据库初始化工具
+
+本工具是为了让用户快速的按照腾讯云制定的数据库 schema 创建符合 SDK 标准的数据库结构。
+
+_**注意**:本工具支持的 MySQL 版本为 **5.7**,并且需提前在数据库中创建名为 `cAuth` 的数据库。`charset` 设置为 `utf8mb4`。_
+
+快速使用:
+
+```bash
+npm run initdb
+```
+
+或直接执行 `tools` 目录下的 `initdb.js` 文件:
+
+```bash
+# 请保证已经执行了 npm install 安装了所需要的依赖
+node tools/initdb.js
+```
+
+我们提供了初始化的 SQL 文件,你也可以用其他数据库工具(如 Navicat)直接导入 SQL 文件。

+ 37 - 0
server/tools/cAuth.sql

@@ -0,0 +1,37 @@
+/*
+ Navicat Premium Data Transfer
+
+ Source Server         : Localhost
+ Source Server Type    : MySQL
+ Source Server Version : 50717
+ Source Host           : localhost
+ Source Database       : cAuth
+
+ Target Server Type    : MySQL
+ Target Server Version : 50717
+ File Encoding         : utf-8
+
+ Date: 08/10/2017 22:22:52 PM
+*/
+
+SET NAMES utf8;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ----------------------------
+--  Table structure for `cSessionInfo`
+-- ----------------------------
+DROP TABLE IF EXISTS `cSessionInfo`;
+CREATE TABLE `cSessionInfo` (
+  `open_id` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
+  `uuid` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
+  `skey` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
+  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `last_visit_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `session_key` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
+  `user_info` varchar(2048) COLLATE utf8mb4_unicode_ci NOT NULL,
+  PRIMARY KEY (`open_id`),
+  KEY `openid` (`open_id`) USING BTREE,
+  KEY `skey` (`skey`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='会话管理用户信息';
+
+SET FOREIGN_KEY_CHECKS = 1;

+ 42 - 0
server/tools/initdb.js

@@ -0,0 +1,42 @@
+/**
+ * 腾讯云微信小程序解决方案
+ * Demo 数据库初始化脚本
+ * @author Jason
+ */
+const fs = require('fs')
+const path = require('path')
+const { mysql: config } = require('../config')
+
+console.log('\n======================================')
+console.log('开始初始化数据库...')
+
+// 初始化 SQL 文件路径
+const INIT_DB_FILE = path.join(__dirname, './cAuth.sql')
+
+const DB = require('knex')({
+    client: 'mysql',
+    connection: {
+        host: config.host,
+        port: config.port,
+        user: config.user,
+        password: config.pass,
+        database: config.db,
+        charset: config.char,
+        multipleStatements: true
+    }
+})
+
+console.log(`准备读取 SQL 文件:${INIT_DB_FILE}`)
+
+// 读取 .sql 文件内容
+const content = fs.readFileSync(INIT_DB_FILE, 'utf8')
+
+console.log('开始执行 SQL 文件...')
+
+// 执行 .sql 文件内容
+DB.raw(content).then(res => {
+    console.log('数据库初始化成功!')
+    process.exit(0)
+}, err => {
+    throw new Error(err)
+})

+ 43 - 0
src/App.vue

@@ -0,0 +1,43 @@
+
+<script>
+
+export default {
+  async created () {
+    // const res = await get('/weapp/demo')
+    // console.log(123, res)
+    // wx.request({
+    //   url:config.host+'/weapp/demo',
+    //   success:function(res){
+    //     console.log(res)
+    //   }
+    // })
+    console.log('小程序启动了')
+  }
+}
+</script>
+
+<style>
+.text-footer{
+  text-align: center;
+  font-size: 12px;
+  margin-bottom:5px;
+}
+.text-primary{
+  color:#EA5149;
+}
+.btn{
+  color:white;
+  background:#EA5A49;
+  margin-bottom: 10px;
+  padding-left: 15px;
+  padding-left: 15px;
+  border-radius: 2px;
+  font-size: 16px;
+  line-height: 40px;
+  height: 40px;
+  width: 100%;
+}
+.btn:active{
+  background: #FA5A49;
+}
+</style>

+ 10 - 0
src/config.js

@@ -0,0 +1,10 @@
+// 配置项
+
+const host = 'http://localhost:5757'
+
+const config = {
+  host,
+  loginUrl: `${host}/weapp/login`,
+  userUrl: `${host}/weapp/user`
+}
+export default config

+ 42 - 0
src/main.js

@@ -0,0 +1,42 @@
+import Vue from 'vue'
+import App from './App'
+
+Vue.config.productionTip = false
+const app = new Vue(App)
+app.$mount()
+
+export default{
+  config: {
+    pages: ['^pages/books/main'],
+    'window': {
+      'backgroundTextStyle': 'light',
+      'navigationBarBackgroundColor': '#EA5149',
+      'navigationBarTitleText': '蜗牛图书',
+      'navigationBarTextStyle': 'light'
+    },
+    'tabBar': {
+      selectedColor: '#EA5149',
+      list: [
+        {
+          pagePath: 'pages/books/main',
+          text: '图书',
+          iconPath: 'static/img/book.png',
+          selectedIconPath: 'static/img/book-active.png'
+        },
+        {
+          pagePath: 'pages/comments/main',
+          text: '评论',
+          iconPath: 'static/img/todo.png',
+          selectedIconPath: 'static/img/todo-active.png'
+        },
+        {
+          pagePath: 'pages/me/main',
+          text: '我',
+          iconPath: 'static/img/me.png',
+          selectedIconPath: 'static/img/me-active.png'
+        }
+
+      ]
+    }
+  }
+}

+ 14 - 0
src/pages/books/Book.vue

@@ -0,0 +1,14 @@
+<template>
+  <div>
+    图书列表页面
+  </div>
+</template>
+<script>
+export default {
+
+}
+</script>
+<style>
+
+
+</style>

+ 11 - 0
src/pages/books/main.js

@@ -0,0 +1,11 @@
+import Vue from 'vue'
+import Book from './Book'
+
+const app = new Vue(Book)
+app.$mount()
+
+export default{
+  config: {
+    enablePullDownRefresh: true
+  }
+}

+ 14 - 0
src/pages/comments/Comment.vue

@@ -0,0 +1,14 @@
+<template>
+  <div>
+    评论过的书页面
+  </div>
+</template>
+<script>
+export default {
+
+}
+</script>
+<style>
+
+
+</style>

+ 5 - 0
src/pages/comments/main.js

@@ -0,0 +1,5 @@
+import Vue from 'vue'
+import Comment from './Comment'
+
+const app = new Vue(Comment)
+app.$mount()

+ 13 - 0
src/pages/me/Me.vue

@@ -0,0 +1,13 @@
+<template>
+  <div>
+    个人中心
+  </div>
+</template>
+<script>
+
+</script>
+
+<style>
+
+
+</style>

+ 5 - 0
src/pages/me/main.js

@@ -0,0 +1,5 @@
+import Vue from 'vue'
+import Me from './Me'
+
+const app = new Vue(Me)
+app.$mount()

+ 43 - 0
src/util.js

@@ -0,0 +1,43 @@
+// 工具函数库
+import config from './config'
+
+// http get工具函数 获取数据
+export function get (url, data) {
+  return request(url, 'GET', data)
+}
+export function post (url, data) {
+  return request(url, 'POST', data)
+}
+
+function request (url, method, data, header = {}) {
+  return new Promise((resolve, reject) => {
+    wx.request({
+      data,
+      method,
+      header,
+      url: config.host + url,
+      success: function (res) {
+        if (res.data.code === 0) {
+          resolve(res.data.data)
+        } else {
+          showModal('失败', res.data.data.msg)
+          reject(res.data)
+        }
+      }
+    })
+  })
+}
+
+export function showModal (title, content) {
+  wx.showModal({
+    title,
+    content,
+    showCancel: false
+  })
+}
+export function showSuccess (text) {
+  wx.showToast({
+    title: text,
+    icon: 'success'
+  })
+}

+ 0 - 0
static/.gitkeep


BIN
static/img/book-active.png


BIN
static/img/book.png


BIN
static/img/me-active.png


BIN
static/img/me.png


BIN
static/img/other-active.png


BIN
static/img/other.png


BIN
static/img/room-active.png


BIN
static/img/room.png


BIN
static/img/todo-active.png


BIN
static/img/todo.png


BIN
static/img/unlogin.png