1. 程式人生 > >基於Redux架構的單頁應用開發總結(一)

基於Redux架構的單頁應用開發總結(一)

寫在前面

“大學四年,細細回味。大一,面帶稚嫩的面龐,一腔傻傻的熱情。可愛帥氣的小涵妹,帶我認識時尚,好基友終生難忘。大二,踏上程式設計師之旅,曦點無緣,Smart不棄,恩師點撥學長提攜,滴水之恩湧泉報。大三,有了自己的團隊,樂雁老朱,程式設計遊戲我們都在一起。專案經驗,點點積累,低下小中探尋的是學以致用的真理。大四,杭州漂泊的一年,八愛到貝貝,小公司磨練,大公司學習,前端工程師之路,勇往直行!
振哥、超哥,帶我實踐和探索,彰顯、健芬,讓我開眼和提升自己。你們是我的良師益友,感謝每一段的指點。現在,新的團隊,可靠的後背,我們不畏懼任何艱辛,未來如何,樂意笑迎!”

好久沒更新部落格了,因為這段時間一直被專案進度和畢業的事情壓的透不過氣,公司學校之間來回奔波了許久。好在,順利完成了畢業任務,公司的專案最後也按時交付,總算可以緩下來寫寫文章了。接下來我會分幾篇把近期開發的基於Redux的單頁應用來一次技術剖析。

系統架構介紹

本專案開發基於 React + Redux + React-Route 框架,利用 webpack 進行模組化構建,前端編寫語言是 JavaScript ES6,利用 babel進行轉換。

|--- project
        |--- build                    // 專案打包編譯目錄
        |--- src                      // 專案開發的原始碼
            |--- actions              // redux的動作
            |--- components           //
redux的元件 |--- containers // redux的容器 |--- images // 靜態圖片 |--- mixins // 通用的函式庫 |--- reducers // redux的store操作 |--- configureStore.js // redux的store對映 |--- index.js // 頁面入口 |
--- routes.js // 路由配置 |--- index.html // 入口檔案 |--- .babelrc // babel配置 |--- main.js // webkit打包的殼子 |--- package.json // 包資訊 |--- webpack.config.js // webpack配置檔案 |--- readme.md
"dependencies": {
    "babel-polyfill": "^6.7.4",
    "base-64": "^0.1.0",
    "immutable": "^3.7.6",
    "isomorphic-fetch": "^2.2.1",
    "moment": "^2.13.0",
    "normalizr": "^2.0.1",
    "react": "^0.14.8",
    "react-datetimepicker": "^2.0.0",
    "react-dom": "^0.14.8",
    "react-redux": "^4.4.1",
    "react-redux-spinner": "^0.4.0",
    "react-router": "^2.0.1",
    "react-router-redux": "^4.0.1",
    "redux": "^3.3.1",
    "redux-immutablejs": "0.0.8",
    "redux-logger": "^2.6.1",
    "redux-thunk": "^2.0.1"
  },
  "devDependencies": {
    "babel-core": "^6.7.5",
    "babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.6.0",
    "babel-preset-react": "^6.5.0",
    "babel-preset-stage-1": "^6.5.0",
    "css-loader": "^0.23.1",
    "file-loader": "^0.8.5",
    "img-loader": "^1.2.2",
    "less": "^2.6.1",
    "less-loader": "^2.2.3",
    "mocha": "^2.4.5",
    "style-loader": "^0.13.1",
    "url-loader": "^0.5.7",
    "webpack": "^1.12.14"
  }

webpack配置

也算是實際體驗了一把webpack,不得不說,論React最佳搭檔,非此貨莫屬!真的很強大,很好用。

var webpack = require('webpack');   // 引入webpack模組
var path = require('path');         // 引入node的path模組
var nodeModulesPath = path.join(__dirname, '/node_modules');  // 設定node_modules目錄

module.exports = {
    // 配置入口(此處定義了雙入口)
    entry: {
        bundle: './src/index',
        vendor: ['react', 'react-dom', 'redux']
    },
    // 配置輸出目錄
    output: {
        path: path.join(__dirname, '/build'),
        publicPath: "/assets/",
        filename: 'bundle.js'
    },
    module: {
        noParse: [
            path.join(nodeModulesPath, '/react/dist/react.min'),
            path.join(nodeModulesPath, '/react-dom/dist/react-dom.min'),
            path.join(nodeModulesPath, '/redux/dist/redux.min'),
        ],
        // 載入器
        loaders: [
            // less載入器
            { test: /\.less$/, loader: 'style!css!less' },
            // babel載入器
            { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' },
            // 圖片載入器(圖片超過8k會自動轉base64格式)
            { test: /\.(gif|jpg|png)$/, loader: "url?limit=8192&name=images/[name].[hash].[ext]"},
            // 載入icon字型檔案
            { test: /\.(woff|svg|eot|ttf)$/, loader: 'url?limit=50000&name=fonts/[name].[hash].[ext]'}
        ]
    },
    // 外部依賴(不會打包到bundle.js裡)
    externals: { 
        'citys': 'Citys'
    },
    // 外掛
    plugins: [
        //new webpack.HotModuleReplacementPlugin(),  // 版本上線時開啟
        new webpack.DefinePlugin({
            // 定義生產環境
            "process.env": {
                NODE_ENV: JSON.stringify("production")
            }
        }),
        //new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }), // 版本上線時開啟
        // 公共部分會被抽離到vendor.js裡
        new webpack.optimize.CommonsChunkPlugin('vendor',  'vendor.js'),
        // 比對id的使用頻率和分佈來得出最短的id分配給使用頻率高的模組
        new webpack.optimize.OccurenceOrderPlugin(),
        // 允許錯誤不打斷程式
        new webpack.NoErrorsPlugin()
    ],
};

延伸-Webpack效能優化

最小化

為了瘦身你的js(還有你的css,如果你用到css-loader的話)webpack支援一個簡單的配置項:

new webpack.optimize.UglifyJsPlugin()

這是一種簡單而有效的方法來優化你的webapp。而webpack還提供了modules 和 chunks ids 來區分他們倆。利用下面的配置項,webpack就能夠比對id的使用頻率和分佈來得出最短的id分配給使用頻率高的模組。

new webpack.optimize.OccurenceOrderPlugin()

入口檔案對於檔案大小有較高的優先順序(入口檔案壓縮優化率儘量的好)

去重

如果你使用了一些有著很酷的依賴樹的庫,那麼它可能存在一些檔案是重複的。webpack可以找到這些檔案並去重。這保證了重複的程式碼不被大包到bundle檔案裡面去,取而代之的是執行時請求一個封裝的函式。不會影響語義

new webpack.optimize.DedupePlugin()

這個功能可能會增加入口模組的一些花銷

對於chunks的優化

當coding的時候,你可能已經添加了許多分割點來按需載入。但編譯完了之後你發現有太多細小的模組造成了很大的HTTP損耗。幸運的是Webpack可以處理這個問題,你可以做下面兩件事情來合併一些請求:

  • Limit the maximum chunk count with
new webpack.optimize.LimitChunkCountPlugin({maxChunks: 15})
  • Limit the minimum chunk size with
new webpack.optimize.MinChunkSizePlugin({minChunkSize: 10000})

Webpack通過合併來管理這些非同步載入的模組(合併更多的時候發生在當前這個chunk有複用的地方)。檔案只要在入口頁面載入的時候沒有被引入,那麼就不會被合併到chunk裡面去。

單頁

Webpack 是為單頁應用量身定做的 你可以把app拆成很多chunk,這些chunk由路由來載入。入口模組僅僅包含路由和一些庫,沒有別的內容。這麼做在使用者通過導航瀏覽表現很好,但是初始化頁面載入的時候你需要2個網路請求:一個是請求路由,一個是載入當前內容。

如果你利用HTML5的HistoryAPI 來讓URL影響當前內容頁的話。你的伺服器可以知道那個內容頁面將被客戶端請求。為了節約請求數,服務端可以把要請求的內容模組放到響應頭裡面:以script標籤的形式來新增,瀏覽器將並行的載入這倆請求。

<script src="entry-chunk.js" type="text/javascript" charset="utf-8"></script>
<script src="3.chunk.js" type="text/javascript" charset="utf-8"></script>

你可以從build stas裡面提取出chunk的filename (stats-webpack-plugin )

多頁

當編譯一個多頁面的app時,你想要在頁面之間共享一些程式碼。這在webpack看來很簡單的:只需要和多個入口檔案一起編譯就好

webpack p1=./page1 p2=./page2 p3=./page3 [name].entry-chunk.js
module.exports = {
    entry: {
        p1: "./page1",
        p2: "./page2",
        p3: "./page3"
    },
    output: {
        filename: "[name].entry.chunk.js"
    }
}

由上面可以產出多個入口檔案

p1.entry.chunk.js, p2.entry.chunk.js and p3.entry.chunk.js

但是可以增加一個chunk來共享她們中的一些程式碼。 如果你的chunks有一些公用的modules,那我推薦一個很酷的外掛CommonsChunkPlugin,它能辨別共用模組並把他們放倒一個檔案裡面去。你需要在你的頁面裡新增兩個script標籤來分別引入入口檔案和共用模組檔案。

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
    entry: {
        p1: "./page1",
        p2: "./page2",
        p3: "./page3"
    },
    output: {
        filename: "[name].entry.chunk.js"
    },
    plugins: [
        new CommonsChunkPlugin("commons.chunk.js")
    ]
}

由上面可以產出入口檔案

p1.entry.chunk.js, p2.entry.chunk.js and p3.entry.chunk.js

和共用檔案

commons.chunk.js

在頁面中要首先載入 commons.chunk.js 在載入xx.entry.chunk.js 你可以出實話很多個commons chunks ,通過選擇不同的入口檔案。並且你可以堆疊使用這些commons chunks。

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
    entry: {
        p1: "./page1",
        p2: "./page2",
        p3: "./page3",
        ap1: "./admin/page1",
        ap2: "./admin/page2"
    },
    output: {
        filename: "[name].js"
    },
    plugins: [
        new CommonsChunkPlugin("admin-commons.js", ["ap1", "ap2"]),
        new CommonsChunkPlugin("commons.js", ["p1", "p2", "admin-commons.js"])
    ]
};

輸出結果:

page1.html: commons.js, p1.js
page2.html: commons.js, p2.js
page3.html: p3.js
admin-page1.html: commons.js, admin-commons.js, ap1.js
admin-page2.html: commons.js, admin-commons.js, ap2.js

另外你可以將多個共用檔案打包到一個共用檔案中。

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
    entry: {
        p1: "./page1",
        p2: "./page2",
        commons: "./entry-for-the-commons-chunk"
    },
    plugins: [
        new CommonsChunkPlugin("commons", "commons.js")
    ]
};