npm package: @mm-works/p000005
Description: hack
Latest version: 1.0.202004272011

# 示例

<!-- TOC -->

- [1. vscode扩展](#1-vscode扩展)
- [2. 项目调试](#2-项目调试)
	- [2.1. 启动调试](#21-启动调试)
	- [2.2. 停止调试](#22-停止调试)
	- [2.3. 重启调试](#23-重启调试)
- [3. 页面初始化](#3-页面初始化)
	- [3.1. 服务端渲染和浏览器端渲染的差别](#31-服务端渲染和浏览器端渲染的差别)
		- [3.1.1. 在实际项目中应该选用哪种方式](#311-在实际项目中应该选用哪种方式)
	- [3.2. 浏览器端渲染](#32-浏览器端渲染)
	- [3.3. 服务端渲染](#33-服务端渲染)
- [4. 原子操作](#4-原子操作)
	- [4.1. 分类](#41-分类)
	- [4.2. 创建方法](#42-创建方法)
	- [4.3. 通用型原子操作](#43-通用型原子操作)
	- [4.4. 项目级原子操作](#44-项目级原子操作)
	- [4.5. snippet的写法](#45-snippet的写法)
- [5. 控件](#5-控件)
	- [5.1. 创建方法](#51-创建方法)
	- [5.2. 通用型控件](#52-通用型控件)
	- [5.3. 项目级控件](#53-项目级控件)
	- [5.4. 控件方法的调用](#54-控件方法的调用)
- [6. 文件转换](#6-文件转换)
	- [6.1. 转pdf](#61-转pdf)
	- [6.2. 转word文档](#62-转word文档)
	- [6.3. 转xlsx表格](#63-转xlsx表格)
- [7. 文件服务](#7-文件服务)
	- [7.1. 配置](#71-配置)
	- [7.2. 上传](#72-上传)
	- [7.3. 下载](#73-下载)
	- [7.4. 上传office文档，转换为ppt和图片](#74-上传office文档转换为ppt和图片)
	- [7.5. 重新上传](#75-重新上传)
	- [7.6. 删除](#76-删除)
- [8. 获取ip](#8-获取ip)
- [9. 自定义路由](#9-自定义路由)
	- [9.1. 添加通用服务](#91-添加通用服务)
	- [9.2. 添加路由](#92-添加路由)
	- [9.3. 添加附加数据](#93-添加附加数据)
- [10. 前置路由过滤器](#10-前置路由过滤器)
- [11. 负载均衡](#11-负载均衡)
- [12. 两个单例服务](#12-两个单例服务)
	- [12.1. 编码服务](#121-编码服务)
	- [12.2. 定时任务](#122-定时任务)
- [13. 第三方库的引用](#13-第三方库的引用)
	- [13.1. 添加依赖](#131-添加依赖)
	- [13.2. 全局引用](#132-全局引用)
	- [13.3. amd](#133-amd)
	- [13.4. TypeScript定义](#134-typescript定义)
	- [13.5. 简单粗野的做法](#135-简单粗野的做法)

<!-- /TOC -->

<https://mm-works.github.io/>

## 1. vscode扩展

以下操作说明均以安装[mmstudio扩展](https://marketplace.visualstudio.com/items?itemName=mm.mmstudio)为前提.

## 2. 项目调试

### 2.1. 启动调试

启动项目调试的命令为`npm t`,简单的，可以通过`alt+d`快速打开终端并启动调试

### 2.2. 停止调试

找到启动命令的终端，按下`ctrl+c`停止命令，注意windows上经常会出现无法停止的情况，这种情况下，杀掉该终端即可。

### 2.3. 重启调试

先[停止调试](#22-停止调试)，再次启动调试即可

需要重启调试的情况：

1. 第一次添加某个公共的客户端原子操作后
1. 第一次添加某个公共控件后
1. 添加[自定义路由](#9-自定义路由)之后
1. 添加[前置路由过滤器](#10-前置路由过滤器)之后

## 3. 页面初始化

### 3.1. 服务端渲染和浏览器端渲染的差别

比如我们要访问一个页面，这个页面初始化需要呈现出来："render-demo",它的html片段见`pages/render.html`

如果是以上的html片段保存成一个文件`render.html`，直接在浏览器打开就能正确呈现出来。但是它是完全静态的，我们试着把它进行**动态化**

我们需要先简单说明一下这个页面呈现的的详细过程

1. 浏览器打开这个文件，读取文件内容，即以上代码，实际上它是一段文本
1. 浏览器尝试解析这段文本，识别里面的各个标签，像`html`,`body`,`h1`,`h2`，分析它们的结构。
1. 浏览器将这些标签用正确的方式进行处理，其中会把`h1`和`h2`用不同的字号以及合适的字体通过像素显示到显示设备上。
1. 如果标签中有`<script>`标签，也会将其里面的脚本使用js引擎（chrome中为v8）执行。
1. 在脚本执行过程中，如果脚本有对dom进行操作，页面会重新渲染

基于以上步骤，我们有两种不同的处理方式

1. 如果我们在1和2之间将整个页面的文本处理好，浏览器在3就能正确解析并显示出来。
1. 如果我们在4进行处理，同样在5时也能正确呈现出我们想要的效果

以上的两种不同的处理方式，第一种就是“服务端渲染”的思路，第二种就是“浏览器端”处理的逻辑

#### 3.1.1. 在实际项目中应该选用哪种方式

1. 服务端渲染任务在客户端进行，不占用服务器cpu资源
1. 服务端渲染可以使用服务器缓存，大并发站点节省服务器cpu资源
1. 综上，使用哪种方式需要综合评定，一般业务型站点，如无特殊需求，建议选择开发效率高，成本低的一种，无须理论拘泥。

### 3.2. 浏览器端渲染

相对来讲，这种方式较容易被开发人员理解和接受，其过程就是取数，然后渲染页面，如果页面有条件，则使用该条件查询和排序。以下为实战的操作顺序

1. 新建页面(alt+p)pg001
1. 因页面较为简单，作为示例，我们将其内容划分为一个组件
1. 添加一个响应a001
1. 将其设置为初始化事件，在s.ts中添加初始化事件:

	```ts
	'mm-events-init': 'a001'
	```

1. 将tpl中的部分内容生成渲染块，具体操作为：选中`<div><h1>mm</h1></h2>studio</h2></div>`，按下快捷键(alt+x)即可自动生成p01.tpl。注意渲染原子操作使用的是一个非常优秀的[dot模板引擎](http://olado.github.com/doT)
1. 在响应a001.ts中引用渲染原子操作。`alt+t a`，选择页面操作类回车，再次选择渲染原子操作即可插入代码模板，填入相应参数值就可以，或者在`alt+t a`直接输入原子操作编号即可快速定位至原子操作，回车插入代码
1. 渲染的原子操作第二个参数为需要渲染的数据，通常这里会调用一个自定义的服务。我们来快速创建一个服务`alt+s`。
1. 为了演示方便，这里我直接返回一个数组，但也模拟了排序条件改变，完整的服务代码见`pg001/zj-001/s001.ts`

1. 我们在组件的初始化响应中调用该服务

	```ts
	// 调用nodejs服务
	const r1583660193 = await (() => {
		const service_name = 'pg001/zj-001/s001';	// 服务名称
		const msg = { sort: r1583724505 };	// 参数
		return aw4<string[]>(service_name, msg);
	})();
	```

1. 渲染的第三个参数我们通过另外一个原子操作获得

	```ts
	const r1583724505 = (() => {
		const key = 'sort';	// url参数名
		const default_value = 'asc';	// 默认值,如果不希望使用默认值，可以删除该参数或者传入空值
		return aw3(mm, key, default_value);
	})();
	```

1. 将以上步骤中服务返回的结果作为渲染的第二个参数，p01作为渲染的第三个参数，填入渲染的实参。注意渲染原子操作使用的是一个非常优秀的[dot模板引擎](http://olado.github.com/doT)

	```ts
	// html渲染
	(() => {
		const data = r1583660193;	// 要渲染的数据
		const position = 'inner';	// inner 替换全部子结点 after 当前结点之后 before 当前结点之前 firstin 第一个子结点之前 lastin 最后一个子结点之后
		return aw5(mm, data, p01, 'p01', position);
	})();
	```

1. 经过以上步骤，我们就完成了浏览器端的渲染，再复杂一些，可以使用原子操作把渲染的条件放到url参数中，渲染时取出。逻辑比较简单这里不作解释。

### 3.3. 服务端渲染

服务端的渲染整个思路较长，并且参数修改往往还会牵涉到一个页面生命周期迭代的问题。上面浏览器端的例子最后提到的条件放在url参数中即为这种方式的一个引子，如果各位能够理解，那么服务端渲染就好理解得多。

必须强调，虽然在调试时为了调试方便，服务端的部分代码是在浏览器中运行。但是实际运行环境中，一定一定不要在服务端的代码中使用任何**浏览器特性的东西**，比如alert，比如window，webStorage关键字等等,原子操作已经做过过滤，使用vscode扩展自动添加的代码不会存在这种问题，不能使用的原子操作是无法选择的。但如果存在大量开发人员手动添加的代码的情况，一定一定要保证这一点，或者出现某个页面本地调试正常，部署后无法正常加载的问题，也可以从这个思路进行排查问题。

步骤：

1. 同浏览器端渲染1,2步。
1. 添加服务端初始化响应，具体操作为:在组件的n.ts文件中按下快捷键`alt+a`创建响应，注意该操作一个组件只能操作一次，多次操作生成的多个响应并非系统问题，多余的响应也不会执行，请注意。设计如此，非bug。
1. 创建tpl，步骤同浏览器端原子操作
1. 添加一个服务`s001.ts`，方法和内容参见浏览器端原子操作
1. 在响应`na001.ts`中添加服务调用并渲染，内容见`pg002/zj-001/na001.ts`：

这个时候启动调试(`alt+m d`)页面应该就可以看到效果了(如果之前开启过调试，也许需要先手动关闭，具体操作为在终端界面按下`ctrl+d`)

## 4. 原子操作

在响应，服务，项目级原子操作中，均可引用原子操作。

### 4.1. 分类

原子操作分为通用型原子操作和项目级原子操作，通用型原子操作可以在任何项目中使用，其实现要求比较高。项目级原子操作则只要该项目可用即可。

### 4.2. 创建方法

通过vscode命令`mmstudio: Add new atom`创建。

### 4.3. 通用型原子操作

注意事项：

1. 最好先确定好原子操作的名称，因为它将展示给所有开发人员，尽可能将其描述精确、简练。
1. 希望加入团队的人员请[联系我](mailto:tao_qiufeng@126.com).我将非常乐意接受社区的贡献。
1. 原子操作要有单元测试，否则有可能将无法通过审核合并。

### 4.4. 项目级原子操作

可以在项目中实现某个通用操作,其代码实现制作为一个原子操作`index.ts`（目前限定其为某一个函数），并将其插入代码模板写入`use.snippet`。也可以将项目中常用的几个原子操作的使用编写为一个原子操作，在项目中快速引用。

### 4.5. snippet的写法

1. 普通的文本按原样插入到使用位置
1. `$`为一个特殊字符，如果要在代码模板中输入一个字符`$`，必须加上转义符`\`,即`\$`方可。
1. 如果`$`它后跟一个数字，如`$1`,`$2`，则表示一个停止符，在插入原子操作时通过tab键可以按`$`后的数字切换光标位置。
1. `$`后如果后跟大括号，如`${1}`,`${2}`，其作用同`$1`,`$2`。
1. `$`后大括号中数字后如跟`:`，如`${1:val}`，则`:`后为默认插入内容，当光标停留时默认内容将被选中，且可修改
1. `$`后大括号中数字后如跟`|`,需要保证大括号结束前也要有一个`|`，如`${1:|a,b,c|}`，则`|`之间的内容当光标停留时将按`,`分隔，作为枚举列表供选择，注意应当按实际使用场景调整顺序。
1. `$CURRENT_SECONDS_UNIX`为一个数字值，通常它是唯一的，可以用它作为变量名

## 5. 控件

在tpl中可引用控件,在项目中可以调用控件。

### 5.1. 创建方法

通过vscode命令`mmstudio: Add new widgets`创建。

### 5.2. 通用型控件

注意事项同[通用型原子操作](#23-通用型原子操作)

### 5.3. 项目级控件

可以在项目中实现某个通用控件,控件用到一些封装方法和web组件及shadowdom的技术，不过其基础代码已添加，开发人员按照示例的方法添加自己的实现代码就好。并需其插入代码模板写入`use.snippet`。也可以将项目中固定搭配的控件合并为一个，在项目中快速引用。

注意：

引用控件时请先选定要插入控件的位置，这样插入的代码模板才不会乱。

### 5.4. 控件方法的调用

因为typescript类型的关系，如果要使用控件`mm-000001`，需在客户端响应中手动引入控件类型

```ts
import Widget1 from '@mmstudio/ww000001';
// 引入项目内控件
import Widget2 from '../../widgets/pw001';

const w1 = document.querySelector<Widget1>('#widgetid1')!;
w1.method01('foo', 'bar');

const w2 = document.querySelector<Widget2>('#widgetid2')!;
w2.method01('foo', 'bar');
```

## 6. 文件转换

### 6.1. 转pdf

使用服务端渲染的方法制作页面即可，然后将页面后缀(html)修改为pdf即可查看效果（如果是开发阶段，需要修改页面的端口为8889）

### 6.2. 转word文档

使用服务端渲染的方法制作页面，然后，然后将页面后缀(html)修改为xlsx即可查看效果（如果是开发阶段，需要修改页面的端口为8889）

### 6.3. 转xlsx表格

使用服务端渲染的方法制作页面，注意必须将页面使用`table`渲染，然后，然后将页面后缀(html)修改为xlsx即可查看效果（如果是开发阶段，需要修改页面的端口为8889），这是一种相对简单的做法，无法满足复杂表格（如带有公式等高级功能的表格）

## 7. 文件服务

通常使用控件和原子操作来完成。以下为简单介绍。

### 7.1. 配置

需要配置一个文件数据库

mm.json

```json
{
	"minio": {
		"endPoint": "127.0.0.1",
		"port": 9000,
		"accessKey": "mmstudio",
		"secretKey": "Mmstudio111111",
		"useSSL": false,
		"region": "cn-north-1",
		"partSize": 5242880
	}
}
```

同时需要启动一个文件数据库minio

[docker-compose安置](https://download.daocloud.io/Docker_Mirror/Docker_Compose)

```sh
[sudo] docker-compose -f db.yaml up
```

```yaml
version: '3.7'

services:
  minio:
    image: minio/minio
    container_name: minio
    command: server /data
    volumes:
      - /home/taoqf/data/minio:/data
    ports:
      - 9000:9000
    environment:
      MINIO_ACCESS_KEY: mmstudio
      MINIO_SECRET_KEY: Mmstudio123
```

### 7.2. 上传

当前项目页面地址/fsweb/upload，支持一次上传多个文件

### 7.3. 下载

当前项目页面地址/fsweb/getfile?id=xxx，如果是图片，不添加`download`参数直接展示在页面展示，如果要下载，请添加 `download` 参数，`download`参数支持以下几种形态.

1. /fsweb/getfile?id=xxx&downlaod
1. /fsweb/getfile?id=xxx&downlaod=false
1. /fsweb/getfile?id=xxx&downlaod=abc.jpe

如果一次性下载多个文件，请使用 `/fsweb/getfile?id=xxx,yyy,zzz`

### 7.4. 上传office文档，转换为ppt和图片

当前项目页面地址/fsweb/upload-office，支持选择性转换为图片。

### 7.5. 重新上传

当前项目页面地址/fsweb/reupload?id=xxx

### 7.6. 删除

当前项目页面地址/fsweb/delfile?id=xxx

## 8. 获取ip

在任意一个服务(s000.ts)中，可以通过`msg.realip`获得客户端ip。注意，如果使用了反向代理，请设置请求头`x-real-ip`或`x-forwarded-for`，如果反向代理设置了其它的ip（比如真实情况的反向代理为第三方），一般也可以通过`msg.headers.xxx`获取。

## 9. 自定义路由

在某些情况下，比如支付的回调等，我们需要第三方服务回调提供一些路由。

### 9.1. 添加通用服务

首先必须要先添加一个服务，由该服务响应该路由的请求，但也有可能是可能是某个组件下已完成的服务，这里以新建通用服务为例说明新建通用服务的方法。

因为我们希望把通用的服务单独列出目录来区分，比如希望通用的服务都放在`src/services`目录下以方便管理。

因为新建服务的逻辑是需要打开一个可编辑文件，这样新建的服务会位于该可编辑文件相同的目录下。所以我们新建**第一个服务**时会用到一些技巧。

1. 我们先在vscode中新建一个文件`src/services/mm`

	```sh
	touch src/services/mm
	```

1. 点击打开这个文件.
1. `alt+s`添加服务
1. 删除第一次添加的文件

	```sh
	rm src/services/mm
	```

1. 编写服务逻辑

### 9.2. 添加路由

1. `alt+r`添加路由
1. 选中服务`services/s001`
1. 使用`get`请求访问该路由（也有可能是`post`,`put`,`delete`,`all`，根据第三方服务情况而定）
1. [重启项目调试](#23-重启调试)
1. `curl http://localhost:8889/r001`即可触发调用服务

### 9.3. 添加附加数据

有时候，一个服务的逻辑几乎能被多个路由调用，又有一些**细微**的差别，这个时候，为了项目维护方便，通常不建议复制并修改原服务。我们有两种方法来实现：

1. 添加项目级服务端原子操作，将服务的逻辑封装起来，暴躁出某个参数，在不同服务中调用该原子操作。
1. 在路由定义中附加上该参数，将多个路由同时关联同一个服务，在服务中通过msg.foo获取到参数进行逻辑判定处理。

路由定义中附加参数的方法

在mm.json中找到`routers`下的多个路由，分别为它们附加参数（如`foo`)

```json
{
	"routers": [
		{
			"method": "get",
			"service": "searvices/s001",
			"url": "/r001",
			"data: {
				"foo": "bar1"
			}
		},
		{
			"method": "get",
			"service": "searvices/s001",
			"url": "/r002",
			"data: {
				"foo": "bar2"
			}
		}
	]
}
```

## 10. 前置路由过滤器

前置路由过滤器将提前将某个请求进行处理，其操作类似于添加路由。以下列出不同点：

1. 前置路由过滤器对应的服务返回的结果中如果有`data`，该请求将提前返回，后续路由逻辑全部跳过，但可以设置cookie，和响应头header。
1. 返回结果中可以有重定向`redirect`,如果有，请求也请提前返回，后续路由逻辑全部跳过，可同时设置cookie和响应头
1. 前置路由示波器通常使用通配符作为url，以下几种示例，都是正确的写法

	```json
	{
		"filters": [
			{
				"method": "get",
				"service": "searvices/s001",
				"url": "/*",
				"data: {}
			},
			{
				"method": "get",
				"service": "searvices/s001",
				"url": "/*.html",
				"data: {}
			},
			{
				"method": "get",
				"service": "searvices/s001",
				"url": "/mmstudio",
				"data: {}
			{
				"method": "get",
				"service": "searvices/s001",
				"url": "/m?studio",
				"data: {}
			{
				"method": "get",
				"service": "searvices/s001",
				"url": "/m+studio",
				"data: {}
			{
				"method": "get",
				"service": "searvices/s001",
				"url": "/mm(studio)?",
				"data: {}
			}
		]
	}
	```

## 11. 负载均衡

通常的nginx的反向代理，负载均衡就可以满足绝大多数大型应用（体量小的应用不用担心负载问题）

## 12. 两个单例服务

有些服务不能启动多个实例，所以需要独立部署为单例。这类服务在负荷大（非常大）时，将会拖慢整个应用的速度，所以在业务设计时，需要非常注意。

原理上来讲，这类服务是不可能通过增加结点实现的，除非修改设计，比如不在必要的时候不生成唯一的编码，使用uuid替代。

### 12.1. 编码服务

详见[调用编码服务原子操作](https://www.npmjs.com/package/@mmstudio/an000015)

### 12.2. 定时任务

使用vscode扩展添加定时任务配置。

服务详情见[定时任务服务](https://www.npmjs.com/package/@mmstudio/schedule)

## 13. 第三方库的引用

使用第三方库的时候，由于第三方库的代码质量我们无法保证，所以可能会有以下问题，列出以待查阅分析：

### 13.1. 添加依赖

1. 服务端原子操作如果引用第三方包，将务必将包依赖添加到package.json中的`dependencies`中。
1. 客户端原子操作如果引用第三方包，将务必将包依赖添加到package.json中的`devDependencies`中。
1. 控件如果引用第三方包，将务必将包依赖添加到package.json中的`devDependencies`中。

### 13.2. 全局引用

客户端原子操作或控件引用的第三包如果有全局引用，最常见的有`jquery`，这一类引用在使用时通常会有两种问题：

1. 需要在页面中通过script全局引入js文件
1. 打包时多个对jquery依赖的控件有可能会冲突，造成找不到jquery对象的问题
1. ts定义问题，使用全局还是模块引用需要权衡，多数国内jquery依赖的包质量不高，非常乱。

这里只指出解决问题的线索，具体问题请自行解决。

1. 根目录下的gulpfile.js，本地调试及打包相关
1. n.ts，打包相关

### 13.3. amd

虽然一些脚手架工具已经可以比较方便进行页面调试了，但是依然，会存在各种各样的奇怪问题，项目开发人员技术水平不过硬时，出现问题很难自行解决。并且通常地，当项目比较大时，执行速度过慢，从而导致开发效率降低。所以我们在开发过程中使用的是amd的加载方式，这种技术相对成熟，各种工具和第三方包的支持也比较好，万一出现问题也相对容易解决。但这会产生一些问题，即有部分第三方包并不提供amd的版本，虽然在打包时理论上可以使用，但无法开发调试。这就需要项目使用人员去开源贡献或是自行解决（配置amd.json文件，同时项目下添加amd的版本以供开发时使用，将commonjs版本修改umd版本并不复杂，通常只需要添加头尾即可）。amd.json的格式参看[amd加载器](https://www.npmjs.com/package/@dojo/loader#usage)中的相关说明。

### 13.4. TypeScript定义

有些第三方类库可能会缺少ts定义，如果是比较流行的类库，试试到`@types`下找一找。示例(xxx为类库名称)：

```sh
yarn add --dev @types/xxx
```

如果没有，可以到开源社区贡献。

### 13.5. 简单粗野的做法

如果不想贡献，还要使用第三方类库，在许可允许的范围内（哈哈），可以源码级引入，修改为ts，去掉全局依赖。当然，这种作法我个人并不推荐，但是当今国内开发的圈子里，这样做的不在少数，在项目工期比较紧且项目开发人员普遍素质不很高的情况下，这确实是项目开发成本比较低的一种方法，祝好运！