使用dva+antd快速构建单页面应用

项目结构及使用工具集

原文地址: 我的博客joescott.coding.me/blogcss

`project
   |----- src    项目源代码
   |----- dist   项目编译目标
   |----- .roadhogrc 路霸运行配置文件
   |----- lumen_api RESTful api代码目录
   |----- mock   模拟数据服务目录


`src
  |---  index.js      入口js文件
  |---  index.html    项目入口html文件
  |---  router.js     路由文件
  |---  routes        子路由目录, 下面每一个子路由使用一个单独的文件夹
  |---  components    组件目录,这里特指公共组件
  |---  models        model目录
  |---  services      服务目录
  |---  utils         工具包目录
  |---  constants.js  常量文件,这个文件其实可放入utils目录,而后统一暴露出去

以上是项目中的整体目录结构。 下面详细介绍几个重要部分的结构。html

此应用是当入口应用,入口在src/index.js, 配置在.roadhogrc中,固然roadhog还支持多入口模式,这里不涉及。node

组件系统

项目中组件分为两大类, 容器组件和呈现组件。python

容器组件

容器组件对应于每一个独立的route页面。每一个容器组件都维护一个相关的state, 全部的state改变都由容器最终执行。容器组件负责向其子组件(呈现组件)分配属性(props)。react

该项目中,全部子组件仅做呈现组件,没有state, 只有从父级组件传递下来的props。state由容器组件统一管理,而后分发到子组件中。webpack

容器组件在该项目中以路由组件的形式存在,存放在src/routes下面对应的子目录中。每一个容器组件使用的子组件(非共享的)都在路由组件目录中存放。而使用到的公共组件则存放在components目录下面。例如公共组件提供数据表的包装,下拉操做控件包装等等,在多个容器组件的子组件中会用到。都被抽离到components目录中。git

容器组件的范本以下:github

// routes/users/index.js
import React, { PropTypes } from 'react'
import { RouterRedux } from 'dva/router'
import { connect } from 'dva'
function Users({ location, dispatch, users, loading }) {
}
Users.propTypes = {
  menus: PropTypes.object,
  // ...
}
function mapStateToProps(state) {
  return {
    users: state.users,
    loading: state.loading.models.users,
  }
}
export default connect(mapStateToProps)(Users)

建立一个类Users, 接收一些参数,用于类本身使用,后面会经过connect将state联系给这些参数。
设置类的propTypes, 编译的时候会对属性进行检查,发现类型错误,编译失败。确保项目质量。web

将state和类的属性联系起来, 经过connect方法来实现导出组件ajax

呈现组件

项目中的呈现组件根据共享特性,分别存放于routers目录和components目录中。它们是无state组件,只从父组件获取到props。好比容器组件向呈现组件传入state相关的部分属性和相应的操做方法给呈现组件的props, 一级级递归传下去。 而子组件的交互产生改变state的操做,则由子组件沿原路上传回给容器组件,最终由容器组件的具体方法来触发state的同步,以及UI的更新。

呈现组件的范本以下:

import React, { PropTypes } from 'react'
// ...
function XView ({
  prop1,
  prop2,
  prop3,
  // ...
}) => {
  // create XView propOpts
  const propOpts = {
    p1,
    p2,
    // ...
  }
  return (
    <div {...propOpts}>
     <div>something to render</div>
    </div>
  )
}
XView.propTypes = {
  // ...
}
export default XView

呈现组件和容器组件相比,就是没有使用connect进行state到prop创建联系。这很正常,由于呈现组件是无状态的的,它只有属性,从父层传下来的属性而已。

有了这样的呈现组件,那么就能够直接在父层调用:

<XView {...props}>
</XView>

XView调用的时候,属性props会做为XView类构造函数的输入。

模型系统

该应用的模型model按业务维度设计。模型设计有两种实现方式:

  • 按数据维度设计: 抽离数据和相关操做的方法。 只关心数据自己,至于使用数据模型的组件所遇到的状态管理则与模型无关,而是做为组件自身的state来维护。

  • 按照业务维度设计: 将数据和使用数据强关联组件中的状态抽象成model的方法。

该应用使用后者。

模型位于src/models, 每一个独立的route都对应一个model, 每一个model包含以下属性:

  • namespace: 模型的命名空间,这个是必须的,并且在同一个应用中每一个模型的该属性是惟一的。使用可读性较强的词语做namespace, 好比users, categories, menus之类的。

  • state: 与具体route相关的全部状态数据结构存放在该属性中。好比数据列表,当前操做项,弹出层的显隐状态等等均可以保存在该属性中。

  • subscriptions: 该属性是dva的8个核心概念之一。 该属性存放从源获取数据的设置。 好比当pathname和给定的名称匹配的时候,执行什么操做之类的设置。

  • effects: 该属性存放的是异步操做的一些方法。从词语字面意思理解来讲,是反作用,就是请求非幂等性的。好比异步获取数据列表、异步更新、异步插入、异步删除等等操做。

  • reducers: 该属性存放的是对state的合并方法。基本上就是将新的state值合并到原来的state中, 以达到state的同步。reducer的含义就是多个合并返回一个的意思。

除了上面的几个属性外,须要另外注意几个方法的使用:

  • select: 从state中查找所需的子state属性。该方法参数为state, 返回一个子state对象。

  • put: 建立一条effect信息, 指示middleware发起一个action到Store. put({type: ‘xxxx’, payload: {}})

  • call: 建立一条effect信息,指示middleware使用args做为fn的参数执行,例如call(services.create, payload)

基本的model范本以下:

// models/users.js
export default {
  namespace: 'users',
  state: {},
  subscriptions: {},
  effects: {},
  reducers: {}
}

服务(services)

有了上面的两个部分,基本的静态交互已经就绪,就剩下和真正的或模拟的API交互了,这部分抽离为services, 即services提供异步数据获取。
每一个services对应一个route的操做集合,好比query查询列表,update更新记录,create新增记录,delete删除记录。

这个层面的设计,相对比较简单,直接在utils中包装一个request类,提供fetch或ajax功能,而后services中直接将请求参数传入相应方法便可。返回请求的结果Promise。

mock服务

roadhog使用json做为运行时配置,它提供了代理的配置,简单配置以下:

"proxy": {
    "/api": {
      "target": "http://localhost:3004/",
//      "target": "http://192.168.200.30:8099/api",
      "changeOrigin": true,
      "pathRewrite": { "^/api" : "" }
    }
  }

好比使用json-server+mockjs实现的mock服务,启动端口号为3004, 那么使用target指向3004端口,那么请求/api/xxx的时候就进入json-server提供的mock服务。

另外若是和api服务连调的话,一样能够将target指向真实api服务的base url。 例如上面注释掉的那行。

而在正式打包上线后,就不走proxy, 免配置修改,直接生效。

API设计

API采用lumen微框架实现的restful api, 这块的不做过多介绍,若有兴趣自行搜索lumen官网查看, 或参照lumen_api中的代码来查看。

总结

整个设计下来, 开发流畅性很是不错。 开发体验也很是好。 暂时该项目不支持less, 对图片的处理也稍逊色,后续待解决。

roadhog源码分析

roadhog是对webpack功能做的一个封装,roadhog会读取本身的配置信息,而后转换为webpack的配置对象,最终调用webpack做项目打包。下面对roadhog源码做简单分析。

roadhog提供了三个命令:

  • roadhog build: 构建production bundle

  • roadhog server: 启动开发环境

  • roadhog test: 启动测试

result = spawn.sync(
  'node',
  [require.resolve(`../lib/${script}`)].concat(args),
  { stdio: 'inherit' }
);
process.exit(result.status);

上面代码中的script的值为build, server或test, 而args是roadhog命令后面的option选项。

Options:
  --debug            Build without compress           [boolean] [default: false]
  --watch, -w        Watch file changes and rebuild   [boolean] [default: false]
  --output-path, -o  Specify output path                [string] [default: null]
  --analyze          Visualize and analyze your Webpack bundle.
                                                      [boolean] [default: false]
  -h                 Show help                                         [boolean]

roadhog源码中还有一个异步post上报功能, 上报给阿里你当前的平台信息,git用户信息等。 不知道这个具体用于干啥的。 ^-^。
roadhog xxx其实是调用lib/xxx.js执行具体任务。

咱们下面先看看build.js的逻辑。

roadhog build

build.js代码骨架以下:

var _extends = Object.assign || function (target) {
  // Object.assign polyfill
}
exports.build = build;
process.env.NODE_ENV = 'production';
var argv = require('yargs').usage()
  .option()
  .option()
// ...
function build(argv) {
  // the body of the build
}
if (require.main === module) {
  build(_extends({}, argv, { cwd: process.cwd() }));
}

注意这里require.main === module判断模块是否为应用的主模块,相似于python的if name == “__main__“。

也就是说roadhog build实际上就是调用了build.js暴露出去的build方法。

argv分析

  • debug: 布尔类型值,表示是否使用压缩模式构建

  • watch: 短选项名w, 表示观察文件的改动,而后从新构建

  • output-path: 别名o, 表示构建的目标地址, 默认为./dist目录。

  • analyze: 可视化并分析你的webpack打包

  • h: 显示帮助信息

build函数分析

path(lib/config/path.js)

该文件根据build.js当前工做目录,获取应用程序几个重要的相关文件或文件夹的绝对路径:

  • appBuild: dist目录的绝对路径

  • appPublic: public目录的绝对路径

  • appPackageJson: package.json文件的绝对路径

  • appSrc: src源代码目录的绝对路径

  • appNodeModules: node_modules目录的绝对路径

  • ownNodeModules: roadhog自身的node_modules的绝对路径

  • resolveApp: 该函数接收一个相对路径,返回该目录相对应用程序目录的绝对路径

  • appDirectory: 应用程序所在目录的绝对路径

  • getConfig(lib/utils/getConfig.js)

该方法根据环境获取应用程序当前目录下面的真实配置文件的内容:realGetConfig(‘.roadhogrc’, env, pkg, paths)。

默认使用.roadhogrc配置文件,env为当前环境模式,pkg为package.json文件内容,paths是上面的path相关的路径信息。

roadhog默认配置文件使用json格式的配置,容许在文件中使用注释:

return (0, _parseJsonPretty2.default)((0, _stripJsonComments2.default)((0, _fs.readFileSync)(rcConfig, 'utf-8')), './roadhogrc');

另外若是不使用.roadhogrc这种配置文件,还可使用.roadhogrc.js文件,使用纯js来实现配置。返回一个配置对象就能够了。

使用.js配置文件能够容许在配置中使用js变量和方法。灵活度仍是蛮高的。

若是二者都没有,roadhog依然能够正常使用,自定义配置对象为空对象而已。

另外配置文件中可使用package.json中的包名称(name)和版本信息(version)。 分别使用$npm_package_name变量和$npm_package_version变量。

另外若是是test环境模式,能够注册babel。这块经过lib/utils/registerBabel.js代码中实现的:

require('babel-register')({
  only: ...
  presets: ...
  plugins: ...
  babelrc: ...
})

roadhog配置转webpack配置

在获取了roadhog配置以后,就会将roadhog的配置转换成webpack的配置对象,毕竟底层使用的是webpack来打包的。
roadhog将命令选项(argv), 应用构建目录(appBuild), 自有配置(.roadhogrc内容)和应用程序的路径信息合并到默认的webpack.config.prod.js中。

webpack.config.prod.js返回一个函数,该函数返回合并后的webpack对象。

// lib/config/webpack.config.prod.js
export default function(args, appBuild, config, paths) {
  return {
    bail: true,
    entry: xxxx
    // ...
  }
}

roadhog除了提供默认的webpack配置,还支持用户自定义webpack配置覆盖roadhog默认配置, 在项目根目录下面创建webpack.config.js文件,该文件的模版以下:

export default function (config, env) {
  const newConfig = {};
  // merge or override
  return newConfig;
}

接收的config为roadhog合并默认配置后的配置对象, env是环境模式。

也就是说彻底能够利用全部webpack的功能来实现。

构建过程

在构建以前,先递归读取构建目录中以前全部的.js文件和.css文件,记录原始文件尺寸, 并清理原来的构建目录中的文件。 而后将这些尺寸信息传入构建过程,进行真实构建。

realBuild

真实构建函数实现很是简单,代码以下:

function realBuild(previousSizeMap, resolve, argv) {
  if (argv.debug) {
    console.log('不压缩的方式构建');
  } else {
    console.log('优化的方式构建');
  }
  var compiler = (0, _webpack2.default)(config);
  var done = doneHandler.bind(null, previousSizeMap, argv, resolve);
  if (argv.watch) {
    compiler.watch(200, done);
  } else {
    compiler.run(done);
  }
}

到目前为止,roadhog的打包构建功能已经彻底解读完了。归根结底就是webpack打包。

参考链接

相关文章
相关标签/搜索