feat: Support command npm command line to quickly deploy private server (#106)

This commit is contained in:
xw 2021-11-28 20:11:00 +08:00 committed by GitHub
parent 8cb566f143
commit 61cfa68e65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 352 additions and 12 deletions

View File

@ -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 = {

1
md-cli/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
doocs-md-cli-*

33
md-cli/index.js Normal file
View File

@ -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);
})

55
md-cli/mm.config.js Normal file
View File

@ -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`,
},
],
}
}

28
md-cli/package.json Normal file
View File

@ -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"
}
}

View File

190
md-cli/util.js Normal file
View File

@ -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,
}

View File

@ -16,6 +16,7 @@ mm/
```
## 参考
- [mm 代码仓库](https://github.com/wll8/mockm/)
- [mm 文档](https://hongqiye.com/doc/mockm/)
- [mockjs 文档](http://wll8.gitee.io/mockjs-examples/)
- [mockjs 文档](http://wll8.gitee.io/mockjs-examples/)

View File

@ -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": [