原文地址: 我的博客或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: {} }
有了上面的兩個部分,基本的靜態交互已經就緒,就剩下和真正的或模擬的API交互了,這部分抽離爲services, 即services提供異步數據獲取。
每一個services對應一個route的操做集合,好比query查詢列表,update更新記錄,create新增記錄,delete刪除記錄。
這個層面的設計,相對比較簡單,直接在utils中包裝一個request類,提供fetch或ajax功能,而後services中直接將請求參數傳入相應方法便可。返回請求的結果Promise。
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採用lumen微框架實現的restful api, 這塊的不做過多介紹,若有興趣自行搜索lumen官網查看, 或參照lumen_api中的代碼來查看。
整個設計下來, 開發流暢性很是不錯。 開發體驗也很是好。 暫時該項目不支持less, 對圖片的處理也稍遜色,後續待解決。
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的邏輯。
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方法。
debug: 布爾類型值,表示是否使用壓縮模式構建
watch: 短選項名w, 表示觀察文件的改動,而後從新構建
output-path: 別名o, 表示構建的目標地址, 默認爲./dist目錄。
analyze: 可視化並分析你的webpack打包
h: 顯示幫助信息
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打包。