mirror of
https://github.com/doocs/md.git
synced 2024-10-30 15:57:50 +08:00
feat: Support command npm command line to quickly deploy private server (#106)
This commit is contained in:
parent
8cb566f143
commit
61cfa68e65
52
README.md
52
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 = {
|
||||
|
1
md-cli/.gitignore
vendored
Normal file
1
md-cli/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
doocs-md-cli-*
|
33
md-cli/index.js
Normal file
33
md-cli/index.js
Normal 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
55
md-cli/mm.config.js
Normal 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
28
md-cli/package.json
Normal 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"
|
||||
}
|
||||
}
|
0
md-cli/public/upload/.gitkeep
Normal file
0
md-cli/public/upload/.gitkeep
Normal file
190
md-cli/util.js
Normal file
190
md-cli/util.js
Normal 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,
|
||||
}
|
@ -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/)
|
||||
|
@ -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": [
|
||||
|
Loading…
Reference in New Issue
Block a user