# Tossjs

#### 灵智数科 React 脚手架和基础运行库，主要服务于管理端/中台项目，包含一些实用型的脚本命令和通用的运行时功能。

> **Toss** 折腾的意思

## 1. 核心概念和功能说明

### 1.1 应用

分为**根应用**和**子应用**，根应用提供和业务相关联的基础模块，例如侧边栏、顶栏等，子应用则是独立于框架与根应用的模块，但是其运行依赖于框架和根应用，类似于 exe 应用和 windows 系统的关系。

可以通过`toss.apps.js`来配置子应用（下文**3.3**的示例），通过脚本工具`toss update-apps`来更新子应用

### 1.2 服务

无论根应用还是子应用，都有许多需要与后端接口进行数据交互的地方，这些交互在框架中被定义为**对服务的调用**，其本质是对叶峰的`createRequest`机制的封装和完善。

##### 服务定义规则

通过键值对对象的方式进行服务定义，其中键是服务名，值则是服务对应的接口地址、请求方式以及传参规则的组合：

`{ '<SERVICE_NAME>': '[METHOD] <API_URL>[?param=:value]' }`

其中`METHOD`表示请求的方式，默认为`POST`，以下是示例：

```js
module.exports = {
  'common.getProductList': 'GET /api/ms-product-backend/products/tree?category=:id',
  'common.updateProduct': '/api/ms-product-backend/products/update',
  // ... 更多服务定义
}
```

##### 根应用的服务定义

在根应用中，可以在`src/services/`目录中进行服务定义，例如：

```js
// src/services/common.js
// 服务配置文件会被node脚本工具处理，为此不能直接使用ES6的export default方式来导出
module.exports = {
  'common.getCityTree': 'GET /api/ms-base-backend/city/tree',
  'common.downloadTemplate': 'GET /api/ms-base-backend/template/download?templatId=:templateId',
}
```

`src/services/`目录下面的所有 js 文件都会被认为是服务配置文件，请不要在此目录下放置其他的文件

##### 子应用的服务定义

在子应用根目录下面的`services.js`中进行服务的定义，例如：

```js
// src/pages/marketing/services.js
module.exports = {
  'marketing.getCoupons': 'GET /api/ms-marketing-backend/product/list',
}
```

> 不同子应用的服务名，务必加上不同的前缀以避免冲突，多个子应用公用的服务，最好也放到根应用的服务定义中去

框架会适时读取所有的服务定义文件并汇总成生成到`src/.services.js`中，例如启动 Dev Server 之后和进行构建之前，以及启动 Dev Server 之后监听到任何服务定义文件有变化的时候，也可以手动执行`npm run update-services`来进行更新（需要参考后文配置 npm scripts）。

##### 服务的调用

通过`toss.request`中的`requestService`方法来调用服务：

```js
import { requestService } from 'toss.request'

// 发起一个正常的请求
requestService('marketing.getCoupons', { page: 1, pageSize: 10 }).then(response => {
  console.log(response)
})

// 下载文件
requestService.download('common.downloadTemplate', {}, { templateId: 'xxx' })
```

### 1.3 路由

框架中运行的不同应用，需要通过不同的路由地址进行访问。默认情况下，在启动 Dev Server 之后，框架会监听所有子应用目录的变化，自动生成一级路由声明文件提供给根应用使用，如果需要对子应用的子路由进行更详细的配置，则需要在子应用的根目录创建一个`route.js`文件，以下是示例：

```js
// src/pages/marketing/route.js
module.exports = {
  name: 'marketing',
  path: '/marketing',
  entry: 'pages/marketing/index.jsx', // 子应用根路由入口文件
  local: true, // local为true时，框架会将子路由声明生成为子应用根目录下的route.jsx，需要子应用可以自行引入处理，local为false时，子路由声明也会合并到根应用中去
  children: [
    {
      name: 'marketingCoupon',
      path: '/coupon',
      entry: 'pages/marketing/pages/coupon/index.jsx',
      exact: true,
    },
    {
      name: 'marketingCouponData',
      path: '/coupon-data',
      entry: 'pages/marketing/pages/couponData/index.jsx',
      exact: true,
    },
  ],
}
```

### 1.4 国际化

框架摒弃了之前基于`react-intl`的国际化方案，因为配置繁琐，而且实时切换语言的需求场景实际上也很少：

在现有方案中，在需要显示文字的地方使用`lang`这个全局函数即可，示例：

```jsx
// 显示纯字符串
console.log(lang('你好啊！'))
// > 你好啊！

// 显示包含动态数据的字符串
console.log(lang('{name}，你好啊！', { name: '曾祥瑞' }))
// > 曾祥瑞，你好啊！
```

后续会提完善的工具来协助创建完整的语言配置文件。

## 2. 框架结构与说明

```
tossjs/
  - runtime/ # 运行库目录
    - action
    - assets
    - components
    - helpers
    - history
    - service
    - store
    - utils
  - scripts/ # 脚本目录
    - build
    - plugins
    - tools
    - utils
  - cli.js
  - index.js
```

对于 runtime 目录中的模块，都可以通过`toss.{模块名}`来引用，例如：

```js
import action from 'toss.action'
import BasePage from 'toss.components/BasePage'
import broadcast from 'toss.helpers/broadcast'
import baseUtils from 'toss.utils/base'
import { requestService } from 'toss.service'

// 也可以使用下列语法来引入单个的component/helper/util
import { Loading } from 'toss.components'
import { storage } from 'toss.helpers'
import { validate, request } from 'toss.utils'
```

## 3. 使用方式

### 3.1 新建一个项目作为根应用，结构如下：

```
project/
  - src/
    - pages/
    - templates/
      - index.ejs
    - index.jsx
```

### 3.2 执行`npm init`初始化`package.json`文件，

```
project/
  - src/
    - pages/
    - templates/
      - index.ejs
    - index.jsx
  - package.json
```

### 3.3 执行`npm install tossjs`安装`tossjs`，然后创建 配置文件：`toss.app.js`和`toss.config.js`

```
project/
  - src/
    - pages/
    - templates/
      - index.ejs
    - index.jsx
  - package.json
  - toss.app.js
  - toss.config.js
```

#### `toss.app.js`文件示例：

```js
// 定义所有可用的子应用
const apps = {
  app1: {
    name: '应用1',
    dir: 'src/pages/app1',
    repo: 'http://gitlab.xxx.com/app1.git',
  },
  app2: {
    name: '应用2',
    dir: 'src/pages/app2',
    repo: 'http://gitlab.xxx.com/app2.git',
  },
  // ... 更多子应用配置
}

// 定义不同环境用到的子应用，其中配置的具体规则是`{APP_NAME}@{BRANCH_NAME}`
// 这个环境配置的用处，会在后文说明
const env = {
  default: ['app1@dev'], // 默认配置
  dev: ['app1@dev', 'app2@dev'], // 开发环境配置
  test: ['app1@test', 'app2@test'], // 测试环境配置
  uat: ['app1@pre_release', 'app2@pre_release'], // 预演环境配置
  prod: ['app1@master', 'app2@master'], // 生产环境配置
}

// 导出配置对象
module.exports = { apps, env }
```

#### `toss.config.js`文件示例：

```js
module.exports = {
  // 子应用所在的目录
  subAppsFolder: 'src/pages',
  // 路由相关的配置
  route: {
    // 根路由跳转路径，直接访问/会跳转至/welcome
    indexRedirect: 'welcome',
    // 路由文件生产路径
    output: 'src/.routes.jsx',
    // 是否监听文件变化自动更新路由
    watch: true,
  },
  // 全局变量配置
  globalVars: {
    // JS全局变量配置，可以通过window.__TOSS_GLOBAL__来访问这里设置的值
    js: {
      publicPathPrefix: '/test',
    },
    // SCSS全局变量配置，所有SCSS文件都可以访问到在这里定义的变量
    scss: {
      publicPathPrefix: '/test',
    },
  },
  // webpack构建配置
  build: {
    // html模板文件路径
    htmlTemplate: './src/templates/index.ejs',
    // 基础的webpack配置，可以在此处扩展loader等，参考webpack配置手册
    base: {},
    // webpack开发配置，例如devServer等，参考webpack配置手册
    dev: {},
    // webpack打包配置，参考webpack配置手册
    prod: {},
    // webpack dll配置，参考webpack dll配置手册
    dll: {},
    // babel配置文件，可以扩展babel插件等，参考babel配置手册
    babel: {},
  },
}
```

### 3.4 在`package.json`的`script`字段中加入自定义脚本配置：

```js
"scripts": {
  "start": "toss start",
  "build": "toss build",
  "build:dev": "toss build --env=dev",
  "build:test": "toss build --env=test",
  "build:uat": "toss build --env=uat",
  "build:prod": "toss build --env=prod",
  "build:dll": "toss build-dll",
  "update-apps": "toss update apps",
  "update-apps:dev": "toss update apps --env=dev",
  "update-apps:test": "toss update apps --env=test",
  "update-apps:uat": "toss update apps --env=uat",
  "update-apps:prod": "toss update apps --env=prod",
  "update-routes": "toss update routes",
  "update-services": "toss update services",
},
```

脚本说明：

- **start** 启动开发环境配置，在非 windows 环境中，可以通过`npm start -- --dashboard`来启动一个带有友好交互形式的构建窗口
- **build** 执行打包操作
- **build:{ENV}** 先更新指定环境的子应用，再执行打包
- **update-apps** 更新默认环境的子应用，或者通过`npm run update-apps app1@dev app2@master`来更新指定的子应用到指定分支
- **update-apps:{ENV}** 更新指定环境的子应用
- **update-routes** 更新路由配置
- **update-services** 更新服务配置

### 3.5 完善`src/templates/index.ejs`内容：

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
    <meta http-equiv="cache-control" content="no-cache" />
    <meta http-equiv="Pragma" content="no-cache" />
    <meta http-equiv="expires" content="0" />
    <title>Tenon Test</title>
  </head>
  <body>
    <script>
      //
      window.__TOSS_GLOBAL__ = <%= JSON.stringify(htmlWebpackPlugin.options.globalVars.js) %>
    </script>
    <div id="root" class="site-container">
      <div id="container"></div>
    </div>
  </body>
</html>
```

### 3.6 完善`src/index.jsx`内容：

```jsx
import React from 'react'
import { render } from 'react-dom'
import TossEntry from 'toss.entry'
import AppRoutes from '.routes.jsx'

const RootRoute = props => {
  return <AppRoutes match={props.match} />
}

render(
  <TossEntry basename="/admin">
    <RootRoute />
  </TossEntry>,
  document.getElementById('root')
)
```
