mirror of
https://github.com/doocs/md.git
synced 2024-11-24 19:10:34 +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)反馈。
|
欢迎各位朋友随时提交 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
|
```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) |
|
| 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) |
|
| 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) |
|
| 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)
|
![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
|
```js
|
||||||
const {file, util, okCb, errCb} = CUSTOM_ARG
|
const { file, util, okCb, errCb } = CUSTOM_ARG;
|
||||||
const param = new FormData()
|
const param = new FormData();
|
||||||
param.append('file', file)
|
param.append("file", file);
|
||||||
util.axios.post('http://127.0.0.1:9000/upload', param, {
|
util.axios
|
||||||
headers: { 'Content-Type': 'multipart/form-data' }
|
.post("http://127.0.0.1:9000/upload", param, {
|
||||||
}).then(res => {
|
headers: { "Content-Type": "multipart/form-data" },
|
||||||
okCb(res.url)
|
})
|
||||||
}).catch(err => {
|
.then((res) => {
|
||||||
errCb(err)
|
okCb(res.url);
|
||||||
})
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
errCb(err);
|
||||||
|
});
|
||||||
|
|
||||||
// 提供的可用参数:
|
// 提供的可用参数:
|
||||||
// CUSTOM_ARG = {
|
// 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://github.com/wll8/mockm/)
|
||||||
- [mm 文档](https://hongqiye.com/doc/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",
|
"serve": "vue-cli-service serve",
|
||||||
"build:h5-netlify": "cross-env SERVER_ENV=NETLIFY vue-cli-service build",
|
"build:h5-netlify": "cross-env SERVER_ENV=NETLIFY vue-cli-service build",
|
||||||
"build": "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"
|
"mm": "npx mockm --cwd=mm"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -49,6 +50,7 @@
|
|||||||
"postcss-comment": "^2.0.0",
|
"postcss-comment": "^2.0.0",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"sass-loader": "^11.0.1",
|
"sass-loader": "^11.0.1",
|
||||||
|
"shx": "^0.3.3",
|
||||||
"vue-template-compiler": "^2.6.12"
|
"vue-template-compiler": "^2.6.12"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
|
Loading…
Reference in New Issue
Block a user