From 61cfa68e653b280d4668195db9de81eb7247594c Mon Sep 17 00:00:00 2001 From: xw Date: Sun, 28 Nov 2021 20:11:00 +0800 Subject: [PATCH] feat: Support command npm command line to quickly deploy private server (#106) --- README.md | 52 ++++++++-- md-cli/.gitignore | 1 + md-cli/index.js | 33 ++++++ md-cli/mm.config.js | 55 ++++++++++ md-cli/package.json | 28 +++++ md-cli/public/upload/.gitkeep | 0 md-cli/util.js | 190 ++++++++++++++++++++++++++++++++++ mm/readme.md | 3 +- package.json | 2 + 9 files changed, 352 insertions(+), 12 deletions(-) create mode 100644 md-cli/.gitignore create mode 100644 md-cli/index.js create mode 100644 md-cli/mm.config.js create mode 100644 md-cli/package.json create mode 100644 md-cli/public/upload/.gitkeep create mode 100644 md-cli/util.js diff --git a/README.md b/README.md index 19202c2..9eba470 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,33 @@ Markdown 文档自动即时渲染为微信图文,让你不再为微信文章 欢迎各位朋友随时提交 PR,让这款微信 Markdown 编辑器变得更好!如果你有新的想法,也欢迎在 [Discussions 讨论区](https://github.com/doocs/md/discussions)反馈。 +## 快速搭建私有服务 + +通过我们的 npm cli 你可以轻易搭建属于自己的 markdown 微信编辑器。 + +```sh +# 安装 +npm i -g @doocs/md-cli + +# 启动 +md-cli + +# 访问 +open http://127.0.0.1:8800/md/ +``` + +支持命令行参数: + +- `port` 指定端口号,默认 8800,如果被占用会随机使用一个新端口。 +- `spaceId` dcloud 服务空间配置 +- `clientSecret` dcloud 服务空间配置 + +参数示例: + +```sh +md-cli port=8899 +``` + ## 如何开发和部署 ```sh @@ -71,7 +98,7 @@ npm run build:h5-netlify | 4 | [阿里云](https://www.aliyun.com/product/oss) | 配置 `AccessKey ID`、`AccessKey Secret`、`Bucket`、`Region` 参数 | [如何使用阿里云 OSS?](https://help.aliyun.com/document_detail/31883.html) | | 5 | [腾讯云](https://cloud.tencent.com/act/pro/cos) | 配置 `SecretId`、`SecretKey`、`Bucket`、`Region` 参数 | [如何使用腾讯云 COS?](https://cloud.tencent.com/document/product/436/38484) | | 6 | [七牛云](https://www.qiniu.com/products/kodo) | 配置 `AccessKey`、`SecretKey`、`Bucket`、`Domain`、`Region` 参数 | [如何使用七牛云 Kodo?](https://developer.qiniu.com/kodo) | -| - | 自定义上传逻辑 | 是 | 参考[自定义上传逻辑参数详情](#自定义上传逻辑) | +| - | 自定义上传逻辑 | 是 | 参考[自定义上传逻辑参数详情](#自定义上传逻辑) | ![select-and-change-color-theme](https://doocs.oss-cn-shenzhen.aliyuncs.com/img//1606034542281-a8c99fa7-c11e-4e43-98da-e36012f54dc8.gif) @@ -90,16 +117,19 @@ npm run build:h5-netlify 示例代码: ```js -const {file, util, okCb, errCb} = CUSTOM_ARG -const param = new FormData() -param.append('file', file) -util.axios.post('http://127.0.0.1:9000/upload', param, { - headers: { 'Content-Type': 'multipart/form-data' } -}).then(res => { - okCb(res.url) -}).catch(err => { - errCb(err) -}) +const { file, util, okCb, errCb } = CUSTOM_ARG; +const param = new FormData(); +param.append("file", file); +util.axios + .post("http://127.0.0.1:9000/upload", param, { + headers: { "Content-Type": "multipart/form-data" }, + }) + .then((res) => { + okCb(res.url); + }) + .catch((err) => { + errCb(err); + }); // 提供的可用参数: // CUSTOM_ARG = { diff --git a/md-cli/.gitignore b/md-cli/.gitignore new file mode 100644 index 0000000..ca00455 --- /dev/null +++ b/md-cli/.gitignore @@ -0,0 +1 @@ +doocs-md-cli-* \ No newline at end of file diff --git a/md-cli/index.js b/md-cli/index.js new file mode 100644 index 0000000..56c430e --- /dev/null +++ b/md-cli/index.js @@ -0,0 +1,33 @@ +#!/usr/bin/env node + +const getPort = require(`get-port`) +const { + colors, + spawn, + parseArgv, +} = require(`./util.js`) + +const arg = parseArgv() + +new Promise(async () => { + let { port = 8800, testPort, replayPort } = arg + port = Number(port) + ;[port, testPort, replayPort] = await Promise.all([port, port+1, port+2].map(item => getPort({port: item}) )).catch(err => console.log(`err`, err)) + const line = Object.entries({ + ...arg, + proxy: `https://doocs.gitee.io/`, + port, + testPort, + replayPort, + '--config': `"${__dirname}/mm.config.js"`, + }).map(([key, val]) => `${key}=${val}`).join(` `) + const cliArg = [`"${__dirname}/node_modules/mockm/run.js"`, `--log-line`, line] + spawn(`node`, cliArg) + setTimeout(() => { + // process.stdout.write('\33c\33[3J') + console.log(``) + console.log(`doocs/md 服务已启动:`) + console.log(`打开链接 ${colors.green(`http://127.0.0.1:${port}/md/`)} 即刻使用吧~`) + console.log(``) + }, 3*1e3); +}) diff --git a/md-cli/mm.config.js b/md-cli/mm.config.js new file mode 100644 index 0000000..65a4fc4 --- /dev/null +++ b/md-cli/mm.config.js @@ -0,0 +1,55 @@ +const fs = require(`fs`) +const path = require(`path`) + +const { + dcloud, + parseArgv, +} = require(`./util.js`) + +const arg = parseArgv() + +// unicloud 服务空间配置 +const spaceInfo = { + spaceId: ``, + clientSecret: ``, + ...arg, +} + +/** + * 配置说明请参考文档: + * https://hongqiye.com/doc/mockm/config/option.html + * @type {import('mockm/@types/config').Config} + */ +module.exports = util => { + const port = Number(arg.port) || 9000 + return { + api: { + async '/upload'(req, res) { + const multiparty = await util.toolObj.generate.initPackge(`multiparty`) + const form = new multiparty.Form({ + uploadDir: `${__dirname}/public/upload/`, + }) + form.parse(req, async (err, fields = [], files) => { + const file = files.file[0] + let url = `http://127.0.0.1:${port}/public/upload/${path.parse(file.path).base}` + try { + url = await dcloud(spaceInfo)({name: file.originalFilename, file: fs.createReadStream(file.path)}) + } catch (err) { + // console.log(err) + } + res.json({url}) + }) + }, + }, + static: [ + { + fileDir: `${__dirname}/dist`, + path: `/md`, + }, + { // 访问公共目录 + fileDir: `${__dirname}/public`, + path: `/public`, + }, + ], + } +} diff --git a/md-cli/package.json b/md-cli/package.json new file mode 100644 index 0000000..e620c09 --- /dev/null +++ b/md-cli/package.json @@ -0,0 +1,28 @@ +{ + "name": "@doocs/md-cli", + "version": "0.0.2", + "description": "✍ 一款高度简洁的微信 Markdown 编辑器:支持 Markdown 所有基础语法、色盘取色、一键复制并粘贴到公众号后台、多图上传、一键下载文档、自定义 CSS 样式、一键重置等特性", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "bin": { + "md-cli": "index.js" + }, + "files": [ + "dist", + "public", + "index.js", + "mm.config.js", + "util.js" + ], + "keywords": [], + "author": "wll8", + "license": "ISC", + "dependencies": { + "form-data": "2.3.3", + "get-port": "5.1.1", + "mockm": "^1.1.25", + "node-fetch": "2.6.2" + } +} diff --git a/md-cli/public/upload/.gitkeep b/md-cli/public/upload/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/md-cli/util.js b/md-cli/util.js new file mode 100644 index 0000000..e411f15 --- /dev/null +++ b/md-cli/util.js @@ -0,0 +1,190 @@ +const fetch = require('node-fetch') +const FormData = require(`form-data`) + +/** +* 自定义控制台颜色 +* https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color +* nodejs 内置颜色: https://nodejs.org/api/util.html#util_foreground_colors +*/ +function colors () { + const util = require('util') + + function colorize (color, text) { + const codes = util.inspect.colors[color] + return `\x1b[${codes[0]}m${text}\x1b[${codes[1]}m` + } + + let returnValue = {} + Object.keys(util.inspect.colors).forEach((color) => { + returnValue[color] = (text) => colorize(color, text) + }) + + const colorTable = new Proxy(returnValue, { + get (obj, prop) { + // 在没有对应的具名颜色函数时, 返回空函数作为兼容处理 + const res = obj[prop] ? obj[prop] : (arg => arg) + return res + } + }) + + // 取消下行注释, 查看所有的颜色和名字: + // Object.keys(returnValue).forEach((color) => console.log(returnValue[color](color))) + return colorTable +} + +/** + * 以 Promise 方式运行 spawn + * @param {*} cmd 主程序 + * @param {*} args 程序参数数组 + * @param {*} opts spawn 选项 + */ +function spawn (cmd, args, opts) { + opts = { stdio: `inherit`, ...opts } + opts.shell = opts.shell || process.platform === 'win32' + return new Promise((resolve, reject) => { + const cp = require('child_process') + const child = cp.spawn(cmd, args, opts) + let stdout = '' + let stderr = '' + child.stdout && child.stdout.on('data', d => { stdout += d }) + child.stderr && child.stderr.on('data', d => { stderr += d }) + child.on('error', reject) + child.on('close', code => { + resolve({code, stdout, stderr}) + }) + }) +} + +/** + * 解析命令行参数 + * @param {*} arr + * @returns + */ +function parseArgv(arr) { + return (arr || process.argv.slice(2)).reduce((acc, arg) => { + let [k, ...v] = arg.split('=') + v = v.join(`=`) // 把带有 = 的值合并为字符串 + acc[k] = v === '' // 没有值时, 则表示为 true + ? true + : ( + /^(true|false)$/.test(v) // 转换指明的 true/false + ? v === 'true' + : ( + /[\d|.]+/.test(v) + ? (isNaN(Number(v)) ? v : Number(v)) // 如果转换为数字失败, 则使用原始字符 + : v + ) + ) + return acc + }, {}) +} + +function dcloud(spaceInfo) { + if(Boolean(spaceInfo.spaceId && spaceInfo.clientSecret) === false) { + throw new Error(`请填写 spaceInfo`) + } + + function sign(data, secret) { + const hmac = require(`crypto`).createHmac(`md5`, secret) + // 排序 obj 再转换为 key=val&key=val 的格式 + const str = Object.keys(data).sort().reduce((acc, cur) => `${acc}&${cur}=${data[cur]}`, ``).slice(1) + hmac.update(str) + return hmac.digest(`hex`) + } + + async function anonymousAuthorize() { + const data = { + method: `serverless.auth.user.anonymousAuthorize`, + params: `{}`, + spaceId: spaceInfo.spaceId, + timestamp: Date.now(), + } + return await fetch(`https://api.bspapp.com/client`, { + headers: { + 'x-serverless-sign': sign(data, spaceInfo.clientSecret), + }, + body: `{"method":"serverless.auth.user.anonymousAuthorize","params":"{}","spaceId":"${spaceInfo.spaceId}","timestamp":${data.timestamp}}`, + method: `POST`, + }).then((res) => res.json()) + } + + async function report({ id, token }) { + const reportReq = { + method: `serverless.file.resource.report`, + params: `{"id":"${id}"}`, + spaceId: spaceInfo.spaceId, + timestamp: Date.now(), + token: token, + } + return await fetch(`https://api.bspapp.com/client`, { + headers: { + 'x-basement-token': reportReq.token, + 'x-serverless-sign': sign(reportReq, spaceInfo.clientSecret), + }, + body: JSON.stringify(reportReq), + method: `POST`, + }).then((res) => res.json()) + } + + async function generateProximalSign({ name, token }) { + const data = { + method: `serverless.file.resource.generateProximalSign`, + params: `{"env":"public","filename":"${name}"}`, + spaceId: spaceInfo.spaceId, + timestamp: Date.now(), + token, + } + const res = await fetch(`https://api.bspapp.com/client`, { + headers: { + 'x-basement-token': data.token, + 'x-serverless-sign': sign(data, spaceInfo.clientSecret), + }, + body: JSON.stringify(data), + method: `POST`, + }).then((res) => res.json()) + return res + } + + async function upload({ data, file }) { + const formdata = new FormData() + Object.entries({ + 'Cache-Control': `max-age=2592000`, + 'Content-Disposition': `attachment`, + OSSAccessKeyId: data.accessKeyId, + Signature: data.signature, + host: data.host, + id: data.id, + key: data.ossPath, + policy: data.policy, + success_action_status: 200, + file, + }).forEach(([key, val]) => formdata.append(key, val)) + + return await fetch(`https://${data.host}`, { + headers: { + 'X-OSS-server-side-encrpytion': `AES256`, + }, + body: formdata, + method: `POST`, + }) + } + + async function uploadFile({ name = `unnamed.file`, file }) { + const token = (await anonymousAuthorize()).data.accessToken + const res = await generateProximalSign({ name, token }) + await upload({ data: res.data, file }) + await report({ id: res.data.id, token }) + const fileUrl = `https://${res.data.cdnDomain}/${res.data.ossPath}` + return fileUrl + } + + return uploadFile + +} + +module.exports = { + colors: colors(), + spawn, + parseArgv, + dcloud, +} diff --git a/mm/readme.md b/mm/readme.md index 8c509ff..71de439 100644 --- a/mm/readme.md +++ b/mm/readme.md @@ -16,6 +16,7 @@ mm/ ``` ## 参考 + - [mm 代码仓库](https://github.com/wll8/mockm/) - [mm 文档](https://hongqiye.com/doc/mockm/) -- [mockjs 文档](http://wll8.gitee.io/mockjs-examples/) \ No newline at end of file +- [mockjs 文档](http://wll8.gitee.io/mockjs-examples/) diff --git a/package.json b/package.json index a5c7ca1..0e32d14 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "serve": "vue-cli-service serve", "build:h5-netlify": "cross-env SERVER_ENV=NETLIFY vue-cli-service build", "build": "vue-cli-service build", + "build-cli": "npm run build && npx shx rm -rf md-cli/dist && npx shx rm -rf dist/**/*.map && npx shx cp -r dist md-cli/ && cd md-cli && npm pack", "mm": "npx mockm --cwd=mm" }, "dependencies": { @@ -49,6 +50,7 @@ "postcss-comment": "^2.0.0", "raw-loader": "^4.0.2", "sass-loader": "^11.0.1", + "shx": "^0.3.3", "vue-template-compiler": "^2.6.12" }, "browserslist": [