使用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打包。

參考鏈接

相關文章
相關標籤/搜索