# 一、前端二次开发规范

> 前端开发规范以及注意事项说明

## 1.1 开发规范

### 1.1.1 项目结构


![](/api/ecode/resource/view/${appRes}/1695727070-image.png)

- resources ： 只存放资源文件、图片、字体、样式资源、第三方js库等静态资源；AI生成的前端业务代码、入口代码、组件代码、工具代码和配置代码禁止放入resources目录

- config: 应用配置参数信息

- styles: 样式文件，通常非前置的css

- coms: 自定义组件目录

- utils:工具库存放目录

- stores: store存放目录

	- index.js： 异步的主入口文件

	- entry.js: 前置加载默认入口文件（自动执行）

	- init.js: 自定义的前置加载文件（非自动执行）

### 1.1.2 正确使用weId【<font color='red'>只匹配weId最后一段！</font>】

在使用ecode复写组件时，如果需要限定复写某个组件时，区分weId时，请不要使用全路径匹配，weId是多级父子组件的weId拼接而成，不同场景，不同页面weId可能不一样。

<font color='red'>**正确做法是取weId最后一段以下划线结尾的字符串作为该组件的唯一标识符。如果会有重复可以考虑再加上props参数或者路由进行匹配判断。**
</font>
  
示例：

table组件weId值：f56o40_9d1m87_ktrfwy_su9cgs_8d2ywu_i83sct

正确判断方法：

```javascript
regOvProps('weappUi', 'Table', (props) => {
  const {weId=''} = props;
  if(weId && weId.endsWith("_i83sct")){
    //逻辑处理
  }
  return props;
}, 0);

错误判断方法：
regOvProps('weappUi', 'Table', (props) => {
  const {weId=''} = props;
if(weId==='f56o40_9d1m87_ktrfwy_su9cgs_8d2ywu_i83sct'){
    //逻辑处理
  }
  return props;
}, 0);
```

### 1.1.3 理解前置加载和非前置加载

1.前置加载：页面加载时会立即加载

2.非前置加载：需要用到时才会加载

3.两种加载方式使用不当会影响页面加载速度

4.前置加载文件只能使用React,ReactDOM,'@weapp/utils'，'@weapp/ecodesdk',axios这写比较重要的前端模块

5.前置加载文件无法使用'@weapp/ui'组件模块，所以在前置加载文件写代码会报错

6.前置加载文件只存放相关重写组件等钩子函数，功能判断等代码

7.非前置加载文件存放组件，不需要立即执行的代码块

### 1.1.4 正确使用发布应用功能

1.公共应用：登录前，登录后，免登页面

2.租户应用：登录后

3.发布到pc端和移动端

4.pc端单页和移动端单页需要注册两个路由，pc:/sp  移动端：/mobile

### 1.1.5 异步引入组件需要嵌套Suspense组件，避免组件引入异常报错

### 1.1.6 css要加限定，避免影响其他组件样式

js: 

```javascript
<div className='app-${appId}-container'></div>
```

css: 

```css
.app-720941244693372928-container{
	color: red;
}
```

### 1.1.7 二级域名或多集路径下开发规范

不需要处理二级域名及多级路径的情况：

```javascript
import { getSecondPathUrl, request } from '@weapp/utils';

/**
 * 如果使用标准的上传，request接口请求，window.open方法，以及css样式文件里涉及路径时，使用标准组件以及方法的情况
 * 底层会默认支持替换二级域名以及多级路径，所以不需要考虑
 * 以下为不需要处理的情况
 */

//接口请求
// request({
//   url: '/api/doc/.....'
// })

//window.open方法
// window.open("/sp/doc/.....")

// css文件
// .test{
//   background: url('/build/doc/....')
// }

//上传组件
// <Upload
//   uploadUrl="/api/file/module/upload"
// />
```

需要处理二级域名及多级路径的情况：

```javascript
/**
 * 涉及的地方包括页面内置的img,a,iframe等标签内置了地址的情况，非使用标准组件以及方法的情况才需要处理，标准的组件及方法默认已经处理过
 * 在开启二级域名和多级路径时
 * 需要进行调整链接，在写路径的时候使用weappUtils里的getSecondPathUrl方法进行获取带二级域名及多级路径的地址即可
 */

//页面内置图片：
//错误写法：<img src={'/build/test/test.jpg'}/>
//正确写法：<img src={getSecondPathUrl('/build/test/test.jpg')}/>

//页面内置a标签：
//错误写法：<a href={'/build/test/test.jpg'}/>
//正确写法：<a href={getSecondPathUrl('/build/test/test.jpg')}/>

//页面内置iframe：
//错误写法：<iframe src={'/sp/doc/docPreview/....'}/>
//正确写法：<iframe src={getSecondPathUrl('/sp/doc/docPreview/....')}/>


//上面方法在不涉及按照仓库进行区分二级域名及多级路径时可以使用getSecondPathUrl获取
//如果客户存在按照仓库进行区分二级域名及多级路径时，可以在window抛出下面事件，进行获取地址，使用方法和上面一样

//页面内置iframe：
//错误写法：<iframe src={'/sp/doc/docPreview/....'}/>
//正确写法：<iframe src={window.packageUrl('/sp/doc/docPreview/....')}/>

/**
 * url地址封装多级路径或多级域名，包括单页地址、接口地址、静态文件地址
 * window.open 和 request 不需要走此封装，底层组件会封装
 * @param url
 * @param otherParams
 */
// window.packageUrl=(url:string,otherParams?:{
//   haveOrigin:boolean;//是否带域名，默认不带
// })=>{
//   const {haveOrigin = false} = otherParams||{};
//   let matchUrl = url.match(/^\/(splayout|sp|build|mobile|p?api|info)?(\/app)?(\/bs)?\/([^/?]+)/);
//   let origin = "";
//   let secondPath = "";
//   let module = "";
//   if(matchUrl){
//     module = matchUrl[4]||"";//模块
//     const {publicDomain="",publicUrl=""} = window?.weappUtils?.appInfo?.(`@weapp/${module}`)||{};
//     switch (matchUrl[1]){
//       case "sp":
//       case "splayout":
//       case "mobile":
//       case "info":
//         origin = haveOrigin?window.location.origin:"";
//         secondPath = publicUrl;
//         break;
//       case "build":
//         origin = haveOrigin? (publicDomain||window.location.origin):publicDomain;
//         secondPath = publicUrl;
//         break;
//       case "api":
//       case "papi":
//         origin = haveOrigin?window.location.origin:"";
//         module = module.slice(0,1).toUpperCase()+module.slice(1);
//         secondPath = window[`apiUrlweapp${module}`] || window.publicUrlapi || window.publicUrl ||"";
//         break;
//       default:
//         break;
//     }
//     return `${origin||""}${secondPath||""}${url}`;
//   }else {
//     matchUrl = url.match(/^\/(doc|sp|mobile|splayout|build)/);
//     if(matchUrl){
//       module = "doc";
//       const {publicUrl=""} = window?.weappUtils?.appInfo?.(`@weapp/${module}`)||{};
//       origin = haveOrigin?window.location.origin:"";
//       secondPath = publicUrl;
//       return `${origin||""}${secondPath||""}${url}`;
//     }else {
//       return url;
//     }
//   }
// }
```


### 1.1.8 前端接口请求和拦截规范

请求通过 devRequest，不允许使用 request，示例：

```javascript
// 请求工具方法
import { devRequest } from '@weapp/ecodesdk';

// 使用方式同 request, 参数保持一致
// get 调用
devRequest({ url: '/papi/secondev/health' }).then(res => {
  if (res.code === 200) {
    Dialog.message({ type: 'info', content: `请求成功：${dayjs().unix()}` })
  } else {
    Dialog.message({ type: 'info', content: `请求失败：${dayjs().unix()}` })
  }
}).catch(err => {
  Dialog.message({ type: 'info', content: `请求异常：${dayjs().unix()}` })
  console.error(`请求异常：${dayjs().unix()}`, err)
})

// post 调用
devRequest({ url: '/papi/secondev/health', method: 'post' }).then(res => {
  if (res.code === 200) {
    Dialog.message({ type: 'info', content: `请求成功：${dayjs().unix()}` })
  } else {
    Dialog.message({ type: 'info', content: `请求失败：${dayjs().unix()}` })
  }
}).catch(err => {
  Dialog.message({ type: 'info', content: `请求异常：${dayjs().unix()}` })
  console.error(`请求异常：${dayjs().unix()}`, err)
})

```

拦截通过 regApiInterceptor，不允许使用 axios，示例：

```javascript
// 拦截工具方法
import { regApiInterceptor } from '@weapp/ecodesdk';

// 注册请求拦截器1
regApiInterceptor.regRequestInterceptor('流程-我的请求拦截1', config => {
  console.info('拦截成功1 => ', config);
  // 请求路径增加指定参数
  if (config.url.includes('?')) {
    config.url = config.url + '&params1=xxx'
  } else {
    config.url = config.url + '?params1=xxx'
  }
  return config;
});

// 注册请求拦截器2
regApiInterceptor.regRequestInterceptor('流程-我的请求拦截2', config => {
  console.info('拦截成功2 => ', config);
  // 请求路径增加指定参数
  if (config.url.includes('?')) {
    config.url = config.url + '&params2=xxx'
  } else {
    config.url = config.url + '?params2=xxx'
  }
  return config;
}, 2);

// 注册响应拦截器
regApiInterceptor.regResponseInterceptor('流程-我的请求拦截响应1', resp => {

  // 拦截指定的url
  if (resp.config.url.indexOf('/xxxx') >= 0) {
      // 处理附加逻辑
      console.info('响应拦截成功1 => ', resp);
      return resp;
  }
  return resp;
})
regApiInterceptor.regResponseInterceptor('流程-我的请求拦截响应2', resp => {
  console.info('响应拦截成功2 => ', resp);
  return resp;
}, 2);

```


## 1.2 注意事项

### 1.2.1 组件替换

>组件替换的需要注意，不能在替换的组件中使用原组件，否则会造成死循环导致页面奔溃

前置代码如下：（entry.js）

```javascript
import React from 'react';
import { regOvComponent } from '@weapp/utils';
import { asyncImport } from '@weapp/ecodesdk';

const NewButton = React.lazy(() => asyncImport('${appId}', 'NewButton'));

regOvComponent('weappUi', 'Button', (Com) => {
  return React.forwardRef((props, ref) => {
    return (
      <React.Suspense fallback={() => {}}>
        <NewButton ref={ref} {...props} OriginCom={Com} />
      </React.Suspense>
    )
  });
}, 1)
```
替换组件（NewButton.js）

```javascript
import React from 'react';
import { Button } from '@weapp/ui';
export default class NewButton extends React.Component {
  render() {
    return (
      <span className="app-${appId}-newButton">
        <span className="app-${appId}-newButton-point" />
		<Button {...this.props} />
      </span>
    )
  }
}
```

#### 处理方式

- 在需要引用原组件的时候，可通过props中添加OriginCom传递

修复NewButton.js 如下

```javascript
import React from 'react';

export default class NewButton extends React.Component {
  render() {
    const { OriginCom } = this.props;
    return (
      <span className="app-${appId}-newButton">
        <span className="app-${appId}-newButton-point" />
		<OriginCom {...this.props} />
      </span>
    )
  }
}
```
### 1.2.2 新页面开发

>在ecode中开发新页面，需要复写Switch的props注入Route

>这个时候需要对props.children 进行塞值

>此处建议使用提供的工具方法来操作，避免塞值错误导致页面出不来

```javascript
import { asyncImport } from '@weapp/ecodesdk';
import { regOvProps, regReactChildren, appInfo } from '@weapp/utils';
import { Route } from 'react-router-dom';

const { publicUrl } = appInfo('@weapp/ecodesdk');

const AsyncPage = React.lazy(() => {
  return asyncImport('${appId}', 'index.js');
});

const AsyncPageRoute = (
  <Route path={`${publicUrl}/sp/custom/page`}>
    <React.Suspense fallback= {() => { }}>
      <AsyncPage />
    </React.Suspense>
  </Route>
);

regOvProps('weappEcodesdk', 'Switch', (props) => {
  // 这里建议使用工具包中的regReactChildren 对children进行添加注册路由
  // 不要自行直接改children 以免出错
  regReactChildren(props, AsyncPageRoute);
  return props;
}, 0);

// 访问地址 /sp/custom/page
```

### 1.2.3 新页面加载不出来

有用ecode开发新页面但页面加载不出来的情况，一种情况是代码本身错误，另一种是是有应用在注册路由时，组件外层非Route组件，导致后续注册路由无法加载的情况。
例：（错误写法,外层为Provider组件）

```javascript
const archive = (
    <Provider {...stores}>
        <Router>
<Route path={`${publicUrl}/sp/custom/` + 'archive/inFilePage'}>
<React.Suspense fallback={() => {}}>
<ArcInFilePage />
</React.Suspense>
</Route>
</Route>
        </Router>
    </Provider>
);
regOvProps('weappEcodesdk','Switch',(props) => {
        regReactChildren(props, archive);
        return props;
},0,);
```

因为目前标准的路由react-router是v5版本，上面写法会导致前序路由无法匹配的情况时，会匹配到当前组件而无法匹配后序路由的情况，如果确实想按照上面写法注册路由时，regOvProps最后一个参数设置大一点即可（但不建议）。

正确写法：

```javascript
const todo = (
  <Route path={`${publicUrl}/sp/custom/` + '${appId}/todo'}>
    <React.Suspense fallback= {() => { }}>
      <Todo />
    </React.Suspense>
  </Route>
);
regOvProps('weappEcodesdk', 'Switch', (props) => {
  regReactChildren(props, todo);
  return props;
}, 1);
```

PS. 这个时候将会导致死循环，因为在NewButton.js中又引用了Button 而 Button被替换为了NewButton,如此反复

### 1.2.4 异步组件引入

>使用asyncImport 引入异步组件的时候

>组件在使用的时候需要 包裹React.Suspense标签

```javascript
import { asyncImport } from '@weapp/ecodesdk';
const AsyncCom = React.lazy(() => {
  return asyncImport('${appId}', 'index.js');
});

// 不可直接之用 <AsyncCom />
// 需要使用 
const Com = (
    <React.Suspense fallback= {() => { }}>
      <AsyncCom />
    </React.Suspense>
);
<Com /> 进行使用
```

### 1.2.5 如何判断ecode是否正常执行

1.控制台是否报错

2.init.js文件是否正常加载

3.console打印信息

4.alert弹出调试信息（console无法使用的情况，可能被屏蔽了）

5.是否是代理影响了没有正常加载js文件

![](https://weapp.eteams.cn/papi/ecode/resource/view/985372549947613185/1713077910-image.png)

![](https://weapp.eteams.cn/papi/ecode/resource/view/985372549947613185/1713078320-image.png)

### 1.2.6 如何判断是否是ecode影响原有功能

1.屏蔽ecode文件，看问题是否还存在

chrome F12 打开开发者工具，刷新页面，在【网络】里搜索关键词 `/build/ecodesdk/static/js/lib.js` 进行过滤，右键选择【屏蔽请求网址】

![](${appMdRes}/1733205334-image.png)

![](${appMdRes}/1733205425-image.png)

屏蔽之后，刷新页面，即会在当前页面屏蔽掉所有 e-code 代码块。

2.是否其他ecode影响的，看是否有人ecode做过类似功能

### 1.2.7 判断需求应该可以用什么方式实现

1.是否可以通过复写组件参数实现

2.是否可以通过替换组件实现

3.是否可以通过接口拦截实现

4.是否可以通过css样式调整。

5.页面渲染自定义组件。

### 1.2.8 浏览器console调试功能
从基线10.0.2406.01起，系统默认全局屏蔽console，不允许通过console进行调试

如果确实需要通过conosole进行调试，可以在页面地址后拼接wea_link_show_console即可正常输出调试内容 

如：https://weapp.yunteams.cn?wea_link_show_console

### 1.2.9 通过weId区分指定复写组件 【<font color='red'>只匹配weId最后一段！</font>】

在使用ecode复写组件时，如果需要限定复写某个组件时，区分weId时，请不要使用全路径匹配，weId是多级父子组件的weId拼接而成，不同场景，不同页面weId可能不一样。

正确做法是取weId最后一段以下划线结尾的字符串作为该组件的唯一标识符。如果会有重复可以考虑再加上props参数或者路由进行匹配判断。

示例：

table组件weId值：f56o40_9d1m87_ktrfwy_su9cgs_8d2ywu_i83sct

正确判断方法：

```javascript
regOvProps('weappUi', 'Table', (props) => {
  const {weId=''} = props;
  if(weId && weId.endsWith("_i83sct")){
    //逻辑处理
  }
  return props;
}, 0);
```

错误判断方法：

```javascript
regOvProps('weappUi', 'Table', (props) => {
  const {weId=''} = props;
  if(weId==='f56o40_9d1m87_ktrfwy_su9cgs_8d2ywu_i83sct'){
    //逻辑处理
  }
  return props;
}, 0);
```

### 1.2.10 接口响应拦截错误写法导致其他接口报错
如果有用ecode进行接口响应拦截的，如果有传第二个参数进行error方法处理的，需要return Promise.reject(error);，不然在某些情况会导致接口异常。标准ecode案例“通用接口响应拦截”已更新，按照最新案例开发

正确写法：

```javascript
axios.interceptors.response.use((response) => {
  return response;
}, function (error) {
  return Promise.reject(error);
});
```

错误写法：

```javascript
axios.interceptors.response.use((response) => {
  return response;
}, function (error) {
});
```

### 1.2.11 严重越权风险提醒

请大家注意下，在前端二开代码中，不要直接调用开放平台接口，关键词 papi/openapi 或者 /openserver/，前端直接调用会存在严重的越权风险，前端可以直接调用标准的/api相关口子，非开放平台的接口；如一定要调用openapi，请在后端二开代码中调用。

如果已有项目二开存在上述情况，请开发同事改造下对应代码，后续标准计划拦截掉所有前端发起的openapi调用，请大家知晓并重视！！！

### 1.2.12 css样式复写

1.正确选择样式限定

（1）复写组件，给组件添加自定义样式，并且自定义样式中加入当前项目appId

```
import {regOvProps} from '@weapp/utils';
regOvProps("weappUi","Button",props=>{
	if(判断条件){
  	props.className+=" app-${appId}-custom";
  }
  return props;
},0)
```


```css
.app-\$\{appId\}-custom{
	
}
```

**注意事项：**

- css中appid变量：\$\{appId\}  支持基线版本：10.0.2408.01

- 保持js中的classNme的名称与css中的一致。如果js中classame使用了${appId}变量，css中使用的是当前应用appid的固定值。应用导出再次导入后，应用的appid发生变化，会导致css失效

- 如果使用了固定appid，注意导出新增导入到其他环境或者项目中后，要手动修改下appid为导入后应用的appid

--------------

(2)查找页面模块内特定样式进行限定

如：查找带有模块名之类的，非ui组件的样式名作为限定

查找时，定位到对应组件，往组件父级一层一层找，找到有相关特殊标识进行区分即可

下方案例复写流程页面按钮样式

![](${appMdRes}/1740712710-image.png)

```css
.wffp-frame-dialog.wffp-frame-dialog-main>.ui-title .ui-title-right .ui-btn{
}

```


2.样式复写错误写法案例

（1）不加样式限定

```css
/*此写法就会影响全局的按钮样式*/
.ui-btn{  
}

```

（2）使用dom上的weId作为样式限定
```css
/*此写法如果组件层级变更，weId会跟着变更，就会导致该样式限定失效，后续基线版本中dom中的weId会去除*/
div[weId="1ya7y7_5xscdt_bw3fqs_gcyujy_l7k9dx_hy43as_d74nmg_ev39x1@side-inner-left"]{
}
```


### 1.2.13 组件复写，如何限定复写组件

**由于某些场景特殊，以下几种情况可以考虑组合使用**

1.根据url进行区分限定
```javascript
//可根据path，urlparams进行区分
if(window.location.url.indexOf("/sp/doc/...")>=0){
	
}
```

2.根据weId进行区分限定
```javascript
//系统重的每一个组件都有自己的weId
//每一个组件的weId会继承父组件的weId，以下划线_进行拼接，如A(父组件)：abcd  B（子组件）：abcd_efg 
//以下划线结尾的最后一段字符串就是该组件的weId
if(props.weId.endsWith("_abc")){

}
```
3.根据组件参数进行区分限定
```javascript
//由于有些某块情况特殊，如表单内的同一组件weId都是一样，无法知道具体复写哪个组件，故此表单会在组件props中存放
//该字段的唯一标识，如fieldId，该字段的唯一id，我们可以根据此参数进行区分判断
if(props.fieldId==='1234578'){
}
```
4.其他

如流程，表单，eb页面之类的，都有sdk获取当前打开页面的id，可以根据这些相关信息进行区分



# 二、后端二次开发规范

> 后端开发相关的规范以及一些注意事项

## 2.1 package

所有的二开代码必须要放在 **com.weaver.seconddev** ，可再针对二开的模块 或者功能点再加上一层包名


## 2.2 http接口路由规范


- 二开接口路径（weaver-secondev-service服务）

> /api 需要登录才能访问
> /sapi 内部服务间通信接口路由，无法通过e10地址访问，可通过开放平台访问

```
web端：/(s)api/secondev/xxx/xxx
移动端：/(s)api/app/secondev/xxx/xxx
后台接口：/(s)api/bs/secondev/xxx/xxx
```

<font color="red">**二开严禁随意声明/papi接口！**
  
  **如需发布接口给外部系统调用，请统一声明为/sapi接口并通过开放平台认证调用！**</font>

## 2.3 controller响应参数规范


controller接口的返回数据类型必须为`com.weaver.common.base.entity.result.WeaResult`

```java
// 正确示例一
    @GetMapping("/getString")
    public WeaResult<String> getString(@RequestParam("msg") String msg) {
        log.info("request params is {}", msg);
        // todo
        return WeaResult.success("Hello, " + msg);
    }

// 正确示例二
    @GetMapping("/getMap")
    public WeaResult<Map<String, Object>> getMap(@RequestParam("msg") String msg) {
        log.info("request params is {}", msg);
        // todo
        Map<String, Object> res = new HashMap<>();
        res.put("params", msg);
        return WeaResult.success(res);
    }
```

## 2.4 数据库操作

执行自定义SQL必须通过数据源接口，不允许使用Mybatis。

[数据库操作规范接口](https://www.e-cology.com.cn/ecode/playground/doc/share/view/916733818655342593#3.2.1%20%E6%95%B0%E6%8D%AE%E5%BA%93%E6%93%8D%E4%BD%9C)

## 2.5 xml配置文件修改注意事项

修改二开服务xml配置的时候，一定不要手动修改服务器上的 applicationContext-secondev-dubbo.xml 文件，此文件为标准文件，升级版本时会覆盖，会导致这部分二开功能全部失效！

修改xml统一到e-code监控管理平台里面改，访问地址 /ecode/monitor/loom/deploy/jar 配置文件编辑入口；如果已经手动改了上述文件，那么需要将xml中的二开配置拷贝迁移到e-code里面，确保升级不会受到影响

## 2.6 二开jar包

### 2.6.1 打包规范

必须通过标准提供的打包模版进行打包，查看`build.gradle`：

1、使用标准模版打包，会给每个jar包增加唯一标识：weaver-ecode-seconddev-id，格式为 rootProject.group + '-' + rootProject.name

2、打jar包需要排除resources目录下的所有文件

```gradle
sourceSets {
    main {
        resources {
            exclude '**'
        }
    }
}
```

### 2.6.2 命名规范

二开jar命名统一以secondev为前缀，修改项目下的文件`settings.gradle`

```gradle
rootProject.name = 'secondev-xxx'
```

## 2.7 部署规范

1、二开代码统一部署到 secondev 服务（weaver-secondev-service），不允许部署到其他标准服务；

2、如果 secondev 服务和其他服务组合在一起，则二开包部署到这个组合服务下

## 2.8 serverless开发规范

[serverless开发规范](https://weapp.eteams.cn/ecode/playground/doc/share/view/1034480398440054785)






























































































































































































































































































































































































































































































































































































































































































































































































































































