webpack的打包和性能優化

webpack的打包和性能優化

tree shaking

tree shaking 是一個術語,通常用于描述移除 JavaScript 上下文中的未引用代碼(dead-code)。它依賴于 ES2015 模塊系統中的靜態結構特性,例如 importexport

所謂的“未引用代碼(dead code)”,也就是說,應該刪除掉未被引用的 export,但是它仍然被包含在 bundle 中,優化體積

解決方法

將文件標記為無副作用(side-effect-free)

通過 package.json 的 "sideEffects" 屬性來實現的

「副作用」的定義是,在導入時會執行特殊行為的代碼,而不是僅僅暴露一個 export 或多個 export。舉例說明,例如 polyfill,它影響全局作用域,并且通常不提供 export。

// 如果所有代碼都不包含副作用,我們就可以簡單地將該屬性標記為 false,來告知 webpack,它可以安全地刪除未用到的 export 導出
{
  "name": "your-project",
  "sideEffects": false
}

// 如果你的代碼確實有一些副作用,那么可以改為提供一個數組

{
  "name": "your-project",
  "sideEffects": [
    "./src/some-side-effectful-file.js"
  ]
}

壓縮輸出

通過如上方式,我們已經可以通過 importexport 語法,找出那些需要刪除的“未使用代碼(dead code)”,然而,我們不只是要找出,還需要在 bundle 中刪除它們。為此,我們將使用 -p(production) 這個 webpack 編譯標記,來啟用 uglifyjs 壓縮插件

注意,--optimize-minimize 標記也會在 webpack 內部調用 UglifyJsPlugin

從 webpack 4 開始,也可以通過 "mode" 配置選項輕松切換到壓縮輸出,只需設置為 "production"

為了學會使用 tree shaking,你必須……

  • 使用 ES2015 模塊語法(即 importexport)。
  • 在項目 package.json 文件中,添加一個 "sideEffects" 入口。
  • 引入一個能夠刪除未引用代碼(dead code)的壓縮工具(minifier)(例如 UglifyJSPlugin)。

代碼分離

代碼分離是 webpack 中最引人注目的特性之一。此特性能夠把代碼分離到不同的 bundle 中,然后可以按需加載或并行加載這些文件。代碼分離可以用于獲取更小的 bundle,以及控制資源加載優先級,如果使用合理,會極大影響加載時間。(優化加載時間)

入口起點(entry points)

這是迄今為止最簡單、最直觀的分離代碼的方式。

project

webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
+ |- another-module.js
|- /node_modules

another-module.js

import _ from 'lodash';

console.log(
  _.join(['Another', 'module', 'loaded!'], ' ')
);

webpack.config.js

const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    index: './src/index.js',
    another: './src/another-module.js'
  },
  plugins: [
    new HTMLWebpackPlugin({
      title: 'Code Splitting'
    })
  ],
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

這種方法存在一些問題:

  • 如果入口 chunks 之間包含重復的模塊,那些重復模塊都會被引入到各個 bundle 中。
  • 這種方法不夠靈活,并且不能將核心應用程序邏輯進行動態拆分代碼。

以上兩點中,第一點對我們的示例來說無疑是個問題,因為之前我們在 ./src/index.js 中也引入過 lodash,這樣就在兩個 bundle 中造成重復引用。接著,我們通過使用 CommonsChunkPlugin 來移除重復的模塊。

防止重復(prevent duplication)

SplitChunksPlugin允許我們共同的依賴提取到一個現有的條目塊或一個全新的塊。讓我們用它來重復lodash上一個例子的依賴:

webpack.config.js

  const path = require('path');

  module.exports = {
    mode: 'development',
    entry: {
      index: './src/index.js',
      another: './src/another-module.js'
    },
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    },
   optimization: {
     splitChunks: {
       chunks: 'all'
     }
   }
  };

有了optimization.splitChunks配置選項,我們現在應該看到從我們的index.bundle.js和中刪除了重復的依賴項another.bundle.js。該插件應該注意到我們已經分離lodash出一個單獨的塊并從我們的主包中移除了自重。

動態導入(dynamic imports)

當涉及到動態代碼拆分時,webpack 提供了兩個類似的技術。對于動態導入,第一種,也是優先選擇的方式是,使用符合 ECMAScript 提案import() 語法。第二種,則是使用 webpack 特定的 require.ensure

import() 調用會在內部用到 promises。如果在舊有版本瀏覽器中使用 import(),記得使用 一個 polyfill 庫(例如 es6-promisepromise-polyfill),來 shim Promise

現在,我們不再使用靜態導入 lodash,而是通過使用動態導入來分離一個 chunk:

src/index.js


 function getComponent() {
   return import(/* webpackChunkName: "lodash" */ 'lodash').then(_ => {
     var element = document.createElement('div');

     element.innerHTML = _.join(['Hello', 'webpack'], ' ');

     return element;

   }).catch(error => 'An error occurred while loading the component');
  }


 getComponent().then(component => {
   document.body.appendChild(component);
 })

注意,在注釋中使用了 webpackChunkName。這樣做會導致我們的 bundle 被命名為 lodash.bundle.js ,而不是 [id].bundle.js 。想了解更多關于 webpackChunkName 和其他可用選項,請查看 import() 相關文檔。讓我們執行 webpack,查看 lodash 是否會分離到一個單獨的 bundle:

由于 import() 會返回一個 promise,因此它可以和 async 函數一起使用。但是,需要使用像 Babel 這樣的預處理器和Syntax Dynamic Import Babel Plugin。下面是如何通過 async 函數簡化代碼:

src/index.js


 async function getComponent() {
   var element = document.createElement('div');
   const _ = await import(/* webpackChunkName: "lodash" */ 'lodash');

   element.innerHTML = _.join(['Hello', 'webpack'], ' ');

   return element;
  }

  getComponent().then(component => {
    document.body.appendChild(component);
  });

緩存

輸出文件的文件名(Output Filenames)

通過使用 output.filename 進行文件名替換,可以確保瀏覽器獲取到修改后的文件。[hash] 替換可以用于在文件名中包含一個構建相關(build-specific)的 hash,但是更好的方式是使用 [chunkhash] 替換,在文件名中包含一個 chunk 相關(chunk-specific)的哈希

project

webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
|- /node_modules

webpack.config.js

  const path = require('path');
  const CleanWebpackPlugin = require('clean-webpack-plugin');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: './src/index.js',
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
       title: 'Caching'
      })
    ],
    output: {
    filename: '[name].[contenthash].js',
      path: path.resolve(__dirname, 'dist')
    }
  };

bundle 的名稱是它內容(通過 hash)的映射。如果我們不做修改,然后再次運行構建,我們以為文件名會保持不變。然而,如果我們真的運行,可能會發現情況并非如此:(譯注:這里的意思是,如果不做修改,文件名可能會變,也可能不會。)

提取模板(Extracting Boilerplate)

SplitChunksPlugin可以使用它將模塊拆分為單獨的包。webpack提供了一個優化功能,它根據提供的選項將運行時代碼拆分為單獨的塊,只需使用optimization.runtimeChunkset來single創建一個運行時包:

webpack.config.js

  const path = require('path');
  const CleanWebpackPlugin = require('clean-webpack-plugin');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: './src/index.js',
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
        title: 'Caching'
    ],
    output: {
      filename: '[name].[contenthash].js',
      path: path.resolve(__dirname, 'dist')
    },
   optimization: {
     runtimeChunk: 'single'
  }
  };

將第三方庫(library)(例如 lodashreact)提取到單獨的 vendor chunk 文件中,是比較推薦的做法,這是因為,它們很少像本地的源代碼那樣頻繁修改。因此通過實現以上步驟,利用客戶端的長效緩存機制,可以通過命中緩存來消除請求,并減少向服務器獲取資源,同時還能保證客戶端代碼和服務器端代碼版本一致。

webpack.config.js

  var path = require('path');
  const CleanWebpackPlugin = require('clean-webpack-plugin');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: './src/index.js',
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
        title: 'Caching'
      }),
    ],
    output: {
      filename: '[name].[contenthash].js',
      path: path.resolve(__dirname, 'dist')
    },
    optimization: {

     runtimeChunk: 'single',
     splitChunks: {
      cacheGroups: {
         vendor: {
           test: /[\\/]node_modules[\\/]/,
           name: 'vendors',
           chunks: 'all'
         }
       }
     }
    }
  }

模塊標識符(Module Identifiers)

  • main捆綁包因其新內容而發生變化。
  • vendor包更改,因為它module.id改變了。
  • 而且,runtime捆綁包已更改,因為它現在包含對新模塊的引用。

第一個和最后一個是預期的 - 這是vendor我們想要解決的哈希值。幸運的是,我們可以使用兩個插件來解決此問題。第一個是NamedModulesPlugin,它將使用模塊的路徑而不是數字標識符。雖然此插件在開發期間對于更易讀的輸出非常有用,但運行起來需要更長的時間。第二個選項是HashedModuleIdsPlugin,建議用于生產構建:

  const path = require('path');
+ const webpack = require('webpack');
  const CleanWebpackPlugin = require('clean-webpack-plugin');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: './src/index.js',
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
        title: 'Caching'
      }),
+      new webpack.HashedModuleIdsPlugin()
    ],
    output: {
      filename: '[name].[contenthash].js',
      path: path.resolve(__dirname, 'dist')
    },
    optimization: {
      runtimeChunk: 'single',
      splitChunks: {
        cacheGroups: {
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            chunks: 'all'
          }
        }
      }
    }
  };

如果改變項目代碼,依賴不變的保持runtime 和vendor的id不變,緩存

shimming

webpack 編譯器(compiler)能夠識別遵循 ES2015 模塊語法、CommonJS 或 AMD 規范編寫的模塊。然而,一些第三方的庫(library)可能會引用一些全局依賴(例如 jQuery 中的 $)。這些庫也可能創建一些需要被導出的全局變量。這些“不符合規范的模塊”就是 shimming 發揮作用的地方。

我們不推薦使用全局的東西!在 webpack 背后的整個概念是讓前端開發更加模塊化。也就是說,需要編寫具有良好的封閉性(well contained)、彼此隔離的模塊,以及不要依賴于那些隱含的依賴模塊(例如,全局變量)。請只在必要的時候才使用本文所述的這些特性。

shimming 另外一個使用場景就是,當你希望 polyfill 瀏覽器功能以支持更多用戶時。在這種情況下,你可能只想要將這些 polyfills 提供給到需要修補(patch)的瀏覽器(也就是實現按需加載)。

shimming 全局變量

使用 ProvidePlugin 后,能夠在通過 webpack 編譯的每個模塊中,通過訪問一個變量來獲取到 package 包。如果 webpack 知道這個變量在某個模塊中被使用了,那么 webpack 將在最終 bundle 中引入我們給定的 package。讓我們先移除 lodashimport 語句,并通過插件提供它:

src/index.js

- import _ from 'lodash';
-
  function component() {
    var element = document.createElement('div');

-   // Lodash, now imported by this script
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');

    return element;
  }

  document.body.appendChild(component());

webpack.config.js

  const path = require('path');
+ const webpack = require('webpack');

  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist')
-   }
+   },
+   plugins: [
+     new webpack.ProvidePlugin({
+       _: 'lodash'
+     })
+   ]
  };

本質上,我們所做的,就是告訴 webpack……

如果你遇到了至少一處用到 lodash 變量的模塊實例,那請你將 lodash package 包引入進來,并將其提供給需要用到它的模塊。

我們還可以使用 ProvidePlugin 暴露某個模塊中單個導出值,只需通過一個“數組路徑”進行配置(例如 [module, child, ...children?])。所以,讓我們做如下設想,無論 join 方法在何處調用,我們都只會得到的是 lodash 中提供的 join 方法。

src/index.js

  function component() {
    var element = document.createElement('div');

-   element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+   element.innerHTML = join(['Hello', 'webpack'], ' ');

    return element;
  }

  document.body.appendChild(component());

webpack.config.js

  const path = require('path');
  const webpack = require('webpack');

  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist')
    },
    plugins: [
      new webpack.ProvidePlugin({
-       _: 'lodash'
+       join: ['lodash', 'join']
      })
    ]
  };

這樣就能很好的與 tree shaking 配合(壓縮),將 lodash 庫中的其他沒用到的部分去除。

細粒度 shimming

一些傳統的模塊依賴的 this 指向的是 window 對象。在接下來的用例中,調整我們的 index.js

  function component() {
    var element = document.createElement('div');

    element.innerHTML = join(['Hello', 'webpack'], ' ');
+
+   // Assume we are in the context of `window`
+   this.alert('Hmmm, this probably isn\'t a great idea...')

    return element;
  }

  document.body.appendChild(component());

當模塊運行在 CommonJS 環境下這將會變成一個問題,也就是說此時的 this 指向的是 module.exports。在這個例子中,你可以通過使用 imports-loader 覆寫 this

webpack.config.js

  const path = require('path');
  const webpack = require('webpack');

  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist')
    },
+   module: {
+     rules: [
+       {
+         test: require.resolve('index.js'),
+         use: 'imports-loader?this=>window'
+       }
+     ]
+   },
    plugins: [
      new webpack.ProvidePlugin({
        join: ['lodash', 'join']
      })
    ]
  };

全局 exports

讓我們假設,某個庫(library)創建出一個全局變量,它期望用戶使用這個變量。為此,我們可以在項目配置中,添加一個小模塊來演示說明:

project

  webpack-demo
  |- package.json
  |- webpack.config.js
  |- /dist
  |- /src
    |- index.js
+   |- globals.js
  |- /node_modules

src/globals.js

var file = 'blah.txt';
var helpers = {
  test: function() { console.log('test something'); },
  parse: function() { console.log('parse something'); }
}

你可能從來沒有在自己的源碼中做過這些事情,但是你也許遇到過一個老舊的庫(library),和上面所展示的代碼類似。在這個用例中,我們可以使用 exports-loader,將一個全局變量作為一個普通的模塊來導出。例如,為了將 file 導出為 file 以及將 helpers.parse 導出為 parse,做如下調整:

webpack.config.js

  const path = require('path');
  const webpack = require('webpack');

  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist')
    },
    module: {
      rules: [
        {
          test: require.resolve('index.js'),
          use: 'imports-loader?this=>window'
-       }
+       },
+       {
+         test: require.resolve('globals.js'),
+         use: 'exports-loader?file,parse=helpers.parse'
+       }
      ]
    },
    plugins: [
      new webpack.ProvidePlugin({
        join: ['lodash', 'join']
      })
    ]
  };

現在從我們的 entry 入口文件中(即 src/index.js),我們能 import { file, parse } from './globals.js';,然后一切將順利進行。

加載 polyfills

目前為止我們所討論的所有內容都是處理那些遺留的 package 包,讓我們進入到下一個話題:polyfills

有很多方法來載入 polyfills。例如,要引入 babel-polyfill 我們只需要如下操作:

npm install --save babel-polyfill

然后使用 import 將其添加到我們的主 bundle 文件:

src/index.js

+ import 'babel-polyfill';
+
  function component() {
    var element = document.createElement('div');

    element.innerHTML = join(['Hello', 'webpack'], ' ');

    return element;
  }

  document.body.appendChild(component());

請注意,我們沒有將 import 綁定到變量。這是因為只需在基礎代碼(code base)之外,再額外執行 polyfills,這樣我們就可以假定代碼中已經具有某些原生功能。

polyfills 雖然是一種模塊引入方式,但是并不推薦在主 bundle 中引入 polyfills,因為這不利于具備這些模塊功能的現代瀏覽器用戶,會使他們下載體積很大、但卻不需要的腳本文件。

讓我們把 import 放入一個新文件,并加入 whatwg-fetch polyfill:

npm install --save whatwg-fetch

src/index.js

- import 'babel-polyfill';
-
  function component() {
    var element = document.createElement('div');

    element.innerHTML = join(['Hello', 'webpack'], ' ');

    return element;
  }

  document.body.appendChild(component());

project

  webpack-demo
  |- package.json
  |- webpack.config.js
  |- /dist
  |- /src
    |- index.js
    |- globals.js
+   |- polyfills.js
  |- /node_modules

src/polyfills.js

import 'babel-polyfill';
import 'whatwg-fetch';

webpack.config.js

  const path = require('path');
  const webpack = require('webpack');

  module.exports = {
-   entry: './src/index.js',
+   entry: {
+     polyfills: './src/polyfills.js',
+     index: './src/index.js'
+   },
    output: {
-     filename: 'bundle.js',
+     filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    },
    module: {
      rules: [
        {
          test: require.resolve('index.js'),
          use: 'imports-loader?this=>window'
        },
        {
          test: require.resolve('globals.js'),
          use: 'exports-loader?file,parse=helpers.parse'
        }
      ]
    },
    plugins: [
      new webpack.ProvidePlugin({
        join: ['lodash', 'join']
      })
    ]
  };

如此之后,我們可以在代碼中添加一些邏輯,根據條件去加載新的 polyfills.bundle.js 文件。你該如何決定,依賴于那些需要支持的技術以及瀏覽器。我們將做一些簡單的試驗,來確定是否需要引入這些 polyfills:

dist/index.html

  <!doctype html>
  <html>
    <head>
      <title>Getting Started</title>
+     <script>
+       var modernBrowser = (
+         'fetch' in window &&
+         'assign' in Object
+       );
+
+       if ( !modernBrowser ) {
+         var scriptElement = document.createElement('script');
+
+         scriptElement.async = false;
+         scriptElement.src = '/polyfills.bundle.js';
+         document.head.appendChild(scriptElement);
+       }
+     </script>
    </head>
    <body>
      <script src="index.bundle.js"></script>
    </body>
  </html>

現在,我們能在 entry 入口文件中,通過 fetch 獲取一些數據:

src/index.js

  function component() {
    var element = document.createElement('div');

    element.innerHTML = join(['Hello', 'webpack'], ' ');

    return element;
  }

  document.body.appendChild(component());
+
+ fetch('https://jsonplaceholder.typicode.com/users')
+   .then(response => response.json())
+   .then(json => {
+     console.log('We retrieved some data! AND we\'re confident it will work on a variety of browser distributions.')
+     console.log(json)
+   })
+   .catch(error => console.error('Something went wrong when fetching this data: ', error))

當我們開始執行構建時,polyfills.bundle.js 文件將會被載入到瀏覽器中,然后所有代碼將正確無誤的在瀏覽器中執行。請注意,以上的這些設定可能還會有所改進,我們只是對于如何解決「將 polyfills 提供給那些需要引入它的用戶」這個問題,向你提供一個很棒的想法。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,748評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,165評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,595評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,633評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,435評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,943評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,035評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,175評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,713評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,599評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,788評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,303評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,034評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,412評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,664評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,408評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,747評論 2 370

推薦閱讀更多精彩內容

  • Depthwise separable convolution(深度可分離卷積) 核心:深度可分離卷積其實是一種可...
    LaLa_2539閱讀 2,419評論 0 2
  • 如果你有面子,那我就給你備上里子,讓你的幸福盡最大限度飽滿! ——題記 好模好樣的怎么就睡不著了呢? 很長時間了,...
    春箋素心閱讀 211評論 0 0
  • 第一話:小心!黑夜里的追殺者! 凌晨4點的城市,被陰霾的夜幕覆蓋著,淅淅瀝瀝的下著微涼的雨。整個城市如同一個玩累了...
    喬七爻閱讀 471評論 1 1
  • (2017年3月11日) 等待援藏結束,我再擁你入懷。那不是奇跡出現,而是用六年時間等來的那片為你盛開的花海。--...
    老葫蘆閱讀 215評論 0 1
  • 自從在表姐家住下后,我本以為從此就和過去一刀兩斷,可是,心始終隱隱作痛。那個男人的影子無論如何都趕不走。愛恨撕扯著...
    7af8eaec95e9閱讀 192評論 0 1