项目结构及使用工具集
原文地址: 个人博客或joescott.coding.me/blog
`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目录,然后统一暴露出去
以上是项目中的总体目录结构。 下面详细介绍几个重要部分的结构。
此应用是当入口应用,入口在src/index.js, 配置在.roadhogrc中,当然roadhog还支持多入口模式,这里不涉及。
组件系统
项目中组件分为两大类, 容器组件和呈现组件。
容器组件
容器组件对应于每个独立的route页面。每个容器组件都维护一个相关的state, 所有的state改变都由容器最终执行。容器组件负责向其子组件(呈现组件)分配属性(props)。
该项目中,所有子组件仅作呈现组件,没有state, 只有从父级组件传递下来的props。state由容器组件统一管理,然后分发到子组件中。
容器组件在该项目中以路由组件的形式存在,存放在src/routes下面对应的子目录中。每个容器组件使用的子组件(非共享的)都在路由组件目录中存放。而使用到的公共组件则存放在components目录下面。例如公共组件提供数据表的包装,下拉操作控件包装等等,在多个容器组件的子组件中会用到。都被抽离到components目录中。
容器组件的范本如下:
// 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, 编译的时候会对属性进行检查,发现类型错误,编译失败。确保项目质量。
将state和类的属性联系起来, 通过connect方法来实现导出组件
呈现组件
项目中的呈现组件根据共享特性,分别存放于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 (
something to render
)
}
XView.propTypes = {
// ...
}
export default XView
呈现组件和容器组件相比,就是没有使用connect进行state到prop建立联系。这很正常,因为呈现组件是无状态的的,它只有属性,从父层传下来的属性而已。
有了这样的呈现组件,那么就可以直接在父层调用:
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打包。
参考连接
项目代码
组件设计方法
Redux-saga中文文档
ES6开发中的兼容性考虑