single-spa.js

single-spa.js

什么是 single-spa.js

single-spa 是一個(gè)可以把多種 JavaScript 框架所開(kāi)發(fā)的應(yīng)用聚合在一個(gè)應(yīng)用的前端框架.

它有如下特點(diǎn):

  • 多個(gè)SPA的切換無(wú)需刷新
  • 每個(gè)應(yīng)用獨(dú)立部署
  • 兼容多元框架
  • 懶加載

這是一個(gè) single-spa 應(yīng)用的在線例子

從架構(gòu)上來(lái)講, single-spa 應(yīng)用由兩個(gè)部分組成:

  • applications
  • single-spa-config

Applications

single-spa apps 會(huì)包含眾多的 SPA 應(yīng)用, 并且每一個(gè)應(yīng)用都是一個(gè)完整的應(yīng)用, 都可以從 DOM 中裝載和卸載自身. 與傳統(tǒng)的 SPA 相比, single-spa apps 最大的不同是它可以與其他的應(yīng)用共存, 共享同一個(gè) html page.

如果你的網(wǎng)絡(luò)還不錯(cuò), 一定已經(jīng)打開(kāi)了在線例子. 在元素檢視面板可以看到很多 div.

1.png

這些 div 就像插槽一樣, 在啟動(dòng)特定的應(yīng)用時(shí), 在對(duì)應(yīng)的插槽裝載元素. 而當(dāng)應(yīng)用不在是激活狀態(tài)時(shí), 又會(huì)從 DOM 中卸載掉該應(yīng)用的元素. 并且, 所有的應(yīng)用都共享著同一個(gè) html 頁(yè).

single-spa-config

single-spa-config 用于在 single-spa 中注冊(cè)應(yīng)用, 每個(gè)應(yīng)用的注冊(cè)需要如下 3 樣?xùn)|西:

  1. application name
  2. application load function
  3. application active status switch function

從零開(kāi)始

本教程的目標(biāo)是在結(jié)束時(shí), 完成零開(kāi)始到集成完畢. 它共需要 6 步:

  1. 初始化項(xiàng)目
  2. 新建 html 文件
  3. Registering
  4. Create the home application
  5. Create the navBar application
  6. Create the angularJs application

初始化項(xiàng)目

隨意新建一個(gè)文件夾, 例如 single-spa 作為本此嘗試所使用的目錄:

0: 初始化目錄

mkdir single-spa && cd single-spa
yarn init              # or npm init
yarn add single-spa    # or npm install --save single-spa
mkdir src

1. 安裝配置 Babel

yarn add --dev @babel/core @babel/preset-env @babel/preset-react @babel/plugin-syntax-dynamic-import @babel/plugin-proposal-object-rest-spread

2. 安裝配置 Webpack

single-spa 現(xiàn)階段不得不使用 Webpack, 執(zhí)行如下命令安裝 Webpack, Webpack plugins, loaders.

# Webpack core
yarn add webpack webpack-dev-server webpack-cli --dev
# Webpack plugins
yarn add clean-webpack-plugin --dev
# Webpack loaders
yarn add style-loader css-loader html-loader babel-loader --dev

安裝結(jié)束之后, 在根目錄創(chuàng)建一個(gè) webpack.config.js 文件, 貼入如下代碼:

const path = require('path');
const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
  mode: 'development',
  entry: {
    // Set the single-spa config as the project entry point
    'single-spa.config': './single-spa.config.js',
  },
  output: {
    publicPath: '/dist/',
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        // Webpack style loader added so we can use materialize
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }, {
        test: /\.js$/,
        exclude: [path.resolve(__dirname, 'node_modules')],
        loader: 'babel-loader',
      }, {
        // This plugin will allow us to use AngularJS HTML templates
        test: /\.html$/,
        exclude: /node_modules/,
        loader: 'html-loader',
      },
    ],
  },
  node: {
    fs: 'empty'
  },
  resolve: {
    modules: [path.resolve(__dirname, 'node_modules')],
  },
  plugins: [
    // A webpack plugin to remove/clean the output folder before building
    new CleanWebpackPlugin(),
  ],
  devtool: 'source-map',
  externals: [],
  devServer: {
    historyApiFallback: true
  }
};

3. 配置 npm run scripts

打開(kāi)根目錄的 package.json 文件, 添加如下腳本:

"scripts": {
  "start": "webpack-dev-server --open",
  "build": "webpack --config webpack.config.js -p"
},

新建 index.html 文件

在根目錄新建一個(gè) index.html 文件, 內(nèi)容如下:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>single-spa</title>
</head>

<body>
  <div id="navBar"></div>
  <div id="home"></div>
  <div id="angularJS"></div>

  <script src="/dist/single-spa.config.js"></script>
</body>

</html>

如果有一些公共的樣式什么的, 可以在此時(shí)一并引入到 index.html 中來(lái).

由于 Webpack 隨后會(huì)將 single-spa.config.js 輸出到 /dist 目錄, 所以single-spa 的配置文件的路徑會(huì)是指向 /dist 的.

注冊(cè)應(yīng)用

通過(guò)配置 single-spa.config.js 注冊(cè)應(yīng)用, 可以告訴 single-spa 如何去引導(dǎo)牡辽、裝載、卸載我們的應(yīng)用.

在根目錄創(chuàng)建一個(gè) single-spa.config.js 文件, 內(nèi)容如下:

import { registerApplication, start } from 'single-spa';

registerApplication(
  // 注冊(cè)的應(yīng)用的名稱
  'home',
  // 加載函數(shù)
  () => { },
  // 活動(dòng)函數(shù)
  location => location.pathname === '' ||
    location.pathname === '/' ||
    location.pathname.startsWith('/home')
)

start();

該配置注冊(cè)了一個(gè) home app, 并分別指明了其名稱棒坏、加載函數(shù)妨猩、活動(dòng)函數(shù).

加載函數(shù)

loadingFunction 必須是一個(gè) async 函數(shù)或者其他返回一個(gè) 已決議 Promise 的函數(shù), 意思也是一樣的.

當(dāng) loading 一個(gè) app 時(shí), 會(huì)首先調(diào)用本函數(shù), 從這個(gè)角度來(lái)看, 定位有些像鉤子函數(shù).

活動(dòng)函數(shù)

activityFunction 必須是一個(gè)返回 boolean 值或其他可判斷真假的值的純函數(shù), 當(dāng)返回結(jié)果為真時(shí), 本應(yīng)用認(rèn)為是活動(dòng)狀態(tài).

Home App

初始化 home app

src/ 目錄下新建 home/ 文件夾, 并且在 Home/ 目錄下新建兩個(gè) js 文件:

  • home.app.js
  • root.component.js

安裝 react 依賴:

yarn add react react-dom single-spa-react react-router-dom react-transition-group

定義 home app 生命周期

注冊(cè)應(yīng)用以后, single-spa 就已經(jīng)開(kāi)始監(jiān)聽(tīng)?wèi)?yīng)用的引導(dǎo)與狀態(tài)了, 屆時(shí)對(duì)應(yīng)的 app 會(huì)對(duì)此做出響應(yīng).

single-spa-react 提供了將 react 注冊(cè)為 singleSpaReact 所需的通用生命周期鉤子, 可以很方便的注冊(cè).

singleSpaReact 需要 4 個(gè)參數(shù):

  1. React 實(shí)例
  2. ReactDOM 實(shí)例
  3. root 組件
  4. domElementGetter 函數(shù)

打開(kāi) src/home/home.app.js 文件, 內(nèi)容如下:

import React from 'react';

import ReactDOM from 'react-dom';

import singleSpaReact from 'single-spa-react';

import Home from './root.component';

// 獲取自己的槽
function domElementGetter () {
  return document.querySelector('#home');
}

// 對(duì)應(yīng)三個(gè)鉤子函數(shù) bootstrap, mount, unmount
const reactLifecycles = singleSpaReact({
  React, ReactDOM, rootComponent: Home, domElementGetter
});

export const bootstrap = [reactLifecycles.bootstrap];

export const mount = [reactLifecycles.mount];

export const unmount = [reactLifecycles.unmount];

構(gòu)建 React app

打開(kāi) src/home/root.component.js 文件, 內(nèi)容如下:

import React from "react";
import { TransitionGroup, CSSTransition } from "react-transition-group";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  Redirect
} from "react-router-dom";
/* you'll need this CSS somewhere
.fade-enter {
  opacity: 0;
  z-index: 1;
}
.fade-enter.fade-enter-active {
  opacity: 1;
  transition: opacity 250ms ease-in;
}
*/
const AnimationExample = () => (
  <Router basename="/home">
    <Route
      render={({ location }) => (
        <div style={{position: 'relative', height: '100%'}}>
          <Route
            exact
            path="/"
            render={() => <Redirect to="/hsl/10/90/50" />}
          />
          <ul style={styles.nav}>
            <NavLink to="/hsl/10/90/50">Red</NavLink>
            <NavLink to="/hsl/120/100/40">Green</NavLink>
            <NavLink to="/rgb/33/150/243">Blue</NavLink>
            <NavLink to="/rgb/240/98/146">Pink</NavLink>
          </ul>
          <div style={styles.content}>
            <TransitionGroup>
              {/* no different than other usage of
                CSSTransition, just make sure to pass
                `location` to `Switch` so it can match
                the old location as it animates out
              */}
              <CSSTransition key={location.key} classNames="fade" timeout={300}>
                <Switch location={location}>
                  <Route exact path="/hsl/:h/:s/:l" component={HSL} />
                  <Route exact path="/rgb/:r/:g/:b" component={RGB} />
                  {/* Without this `Route`, we would get errors during
                    the initial transition from `/` to `/hsl/10/90/50`
                  */}
                  <Route render={() => <div>Not Found</div>} />
                </Switch>
              </CSSTransition>
            </TransitionGroup>
          </div>
        </div>
      )}
    />
  </Router>
);
const NavLink = props => (
  <li style={styles.navItem}>
    <Link {...props} style={{ color: "inherit" }} />
  </li>
);
const HSL = ({ match: { params } }) => (
  <div
    style={{
      ...styles.fill,
      ...styles.hsl,
      background: `hsl(${params.h}, ${params.s}%, ${params.l}%)`
    }}
  >
    hsl({params.h}, {params.s}%, {params.l}%)
  </div>
);
const RGB = ({ match: { params } }) => (
  <div
    style={{
      ...styles.fill,
      ...styles.rgb,
      background: `rgb(${params.r}, ${params.g}, ${params.b})`
    }}
  >
    rgb({params.r}, {params.g}, {params.b})
  </div>
);
const styles = {};
styles.fill = {
  position: "absolute",
  left: 0,
  right: 0,
  top: 0,
  bottom: 0
};
styles.content = {
  ...styles.fill,
  top: "40px",
  textAlign: "center"
};
styles.nav = {
  padding: 0,
  margin: 0,
  position: "absolute",
  top: 0,
  height: "40px",
  width: "100%",
  display: "flex"
};
styles.navItem = {
  textAlign: "center",
  flex: 1,
  listStyleType: "none",
  padding: "10px"
};
styles.hsl = {
  ...styles.fill,
  color: "white",
  paddingTop: "20px",
  fontSize: "30px"
};
styles.rgb = {
  ...styles.fill,
  color: "white",
  paddingTop: "20px",
  fontSize: "30px"
};
export default AnimationExample;

定義 loading function

打開(kāi)根目錄下的 single-spa.config.js 文件, 修改 loading function:

import { registerApplication, start } from 'single-spa';

registerApplication(
  // 注冊(cè)的應(yīng)用的名稱
  'home',
  // 加載函數(shù)
  () => import('./src/home/home.app'), // <- here
  // 激活函數(shù)
  location => location.pathname === '' ||
    location.pathname === '/' ||
    location.pathname.startsWith('/home')
)

start();

這時(shí)候可以嘗試啟動(dòng)一下.

Run yarn start, 如果一切正常的話, 將會(huì)成功啟動(dòng), 并且看到如下頁(yè)面:

2.png

NavBar App

創(chuàng)建和注冊(cè) NavBar app 的過(guò)程與 Home app 非常的相似. 不同點(diǎn)在于 NavBar 會(huì)導(dǎo)出一個(gè)帶有生命周期的對(duì)象并且通過(guò)懶加載的方式獲取各個(gè)應(yīng)用的對(duì)象.

注冊(cè) navBar

single-spa.config.js 添加如下的代碼:

registerApplication(
  'navBar',
  () => import('./src/navBar/navBar.app.js').then(module => module.navBar),
  () => true
)

navBar.app.js 目前還沒(méi)有, 不過(guò)隨后就會(huì)創(chuàng)建.

由于 navBar 是需要始終顯示的, 因此, activityFunction 這里固定返回一個(gè) true.

初始化 navBar app

在 src 目錄下新建 navBar 目錄, 并在其中分別創(chuàng)建 navBar.app.jsroot.component.js 文件.

可以通過(guò)在根路徑執(zhí)行以下命令創(chuàng)建:

mkdir src/navBar
touch src/navBar/navBar.app.js src/navBar/root.component.js

定義 navBar app 生命周期

打開(kāi) navBar.app.js 文件, 貼入下面的代碼:

import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import NavBar from './root.component.js';
function domElementGetter() {
  return document.getElementById("navBar")
}
export const navBar = singleSpaReact({
  React,
  ReactDOM,
  rootComponent: NavBar,
  domElementGetter,
})

編寫 navBar app 頁(yè)面

import React from 'react'
import {navigateToUrl} from 'single-spa'
const NavBar = () => (
  <nav>
    <div className="nav-wrapper">
      <a href="/" onClick={navigateToUrl} className="brand-logo">single-spa</a>
      <ul id="nav-mobile" className="right hide-on-med-and-down">
        <li><a href="/" onClick={navigateToUrl}>Home</a></li>
        <li><a href="/angularJS" onClick={navigateToUrl}>AngularJS</a></li>
      </ul>
    </div>
  </nav>
)
export default NavBar

AngularJs App

初始化 AngularJs app

執(zhí)行如下命令:

mkdir src/angularJS
cd src/angularJS
touch angularJS.app.js root.component.js root.template.html routes.js app.module.js gifs.component.js gifs.template.html

為了演示子應(yīng)用內(nèi)部路由效果, 這里需要添加一些包:

yarn add angular angular-ui-router single-spa-angularjs

注冊(cè) AngularJs app

打開(kāi) single-spa.config.js 文件, 添加如下的代碼:

function pathPrefix(prefix) {
    return function(location) {
        return location.pathname.startsWith(prefix);
    }
}

registerApplication(
  'angularJS',
  () => import ('./src/angularJS/angularJS.app.js'),
  pathPrefix('/angularJS')
)

定義 AngularJs app 生命周期

angularJs.app.js 文件中貼入如下代碼:

import singleSpaAngularJS from 'single-spa-angularjs';

import angular from 'angular';

import './app.module.js';
import './routes.js';

const domElementGetter = () => document.querySelector('#angularJS');

const angularLifecycles = singleSpaAngularJS({
  angular,
  domElementGetter,
  mainAngularModule: 'angularJS-app',
  uiRouter: true,
  preserveGlobal: false,
});

export const bootstrap = [
  angularLifecycles.bootstrap,
];

export const mount = [
  angularLifecycles.mount,
];

export const unmount = [
  angularLifecycles.unmount,
];

配置 angular 應(yīng)用

app.module.js

import angular from 'angular';
import 'angular-ui-router';
angular
.module('angularJS-app', ['ui.router']);

root.component.js

import angular from 'angular';

import template from './root.template.html';
angular
  .module('angularJS-app')
  .component('root', {
    template,
  });

root.template.html

<div ng-style='vm.styles'>
  <div class="container">
    <div class="row">
      <h4 class="light">
        Angular 1 example
      </h4>
      <p class="caption">
        This is a sample application written with Angular 1.5 and angular-ui-router.
      </p>
    </div>
    <div>
    <!-- These Routes will be set up in the routes.js file -->
      <a class="waves-effect waves-light btn-large" href="/angularJS/gifs" style="margin-right: 10px">
        Show me cat gifs
      </a>
      <a class="waves-effect waves-light btn-large" href="/angularJS" style="margin-right: 10px">
        Take me home
      </a>
    </div>
    <div class="row">
      <ui-view />
    </div>
  </div>
</div>

gifs.component.js

import angular from 'angular';

import template from './gifs.template.html';
angular
  .module('angularJS-app')
  .component('gifs', {
    template,
    controllerAs: 'vm',
    controller ($http) {
      const vm = this;
      $http
        .get('https://api.giphy.com/v1/gifs/search?q=cat&api_key=dc6zaTOxFJmzC')
        .then(response => {
          vm.gifs = response.data.data;
        })
        .catch(err => {
          setTimeout(() => {
            throw err;
          }, 0);
        });
    },
  });

gif.template.html

<div style="padding-top: 20px">
  <h4 class="light">
    Cat Gifs gifs
  </h4>
  <p>
  </p>
  <div ng-repeat="gif in vm.gifs" style="margin: 5px;">
    <img ng-src="{{gif.images.downsized_medium.url}}" class="col l3">
  </div>
</div>

設(shè)置 AngularJs app 內(nèi)部路由

routes.js

import angular from 'angular';

import './gifs.component.js';
import './root.component.js';
angular
  .module('angularJS-app')
  .config(($stateProvider, $locationProvider) => {
    $locationProvider.html5Mode({
      enabled: true,
      requireBase: false,
    });
    $stateProvider
      .state('root', {
        url: '/angularJS',
        template: '<root />',
      })
      .state('root.gifs', {
        url: '/gifs',
        template: '<gifs />',
      });
  });

完成

雖然過(guò)程真的好繁瑣, 但是不可否認(rèn)以這種簡(jiǎn)單的例子來(lái)說(shuō)確實(shí)成功了.

執(zhí)行 yarn start 可以查看效果.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末潜叛,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌威兜,老刑警劉巖销斟,帶你破解...
    沈念sama閱讀 216,651評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異椒舵,居然都是意外死亡蚂踊,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門笔宿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)犁钟,“玉大人,你說(shuō)我怎么就攤上這事泼橘±远” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 162,931評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵炬灭,是天一觀的道長(zhǎng)醋粟。 經(jīng)常有香客問(wèn)我,道長(zhǎng)重归,這世上最難降的妖魔是什么米愿? 我笑而不...
    開(kāi)封第一講書人閱讀 58,218評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮鼻吮,結(jié)果婚禮上吗货,老公的妹妹穿的比我還像新娘。我一直安慰自己狈网,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布笨腥。 她就那樣靜靜地躺著拓哺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪脖母。 梳的紋絲不亂的頭發(fā)上士鸥,一...
    開(kāi)封第一講書人閱讀 51,198評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音谆级,去河邊找鬼烤礁。 笑死,一個(gè)胖子當(dāng)著我的面吹牛肥照,可吹牛的內(nèi)容都是我干的脚仔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,084評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼舆绎,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼鲤脏!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 38,926評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤猎醇,失蹤者是張志新(化名)和其女友劉穎窥突,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體硫嘶,經(jīng)...
    沈念sama閱讀 45,341評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡发皿,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評(píng)論 2 333
  • 正文 我和宋清朗相戀三年磅叛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,731評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡夹界,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出窖逗,到底是詐尸還是另有隱情累奈,我是刑警寧澤,帶...
    沈念sama閱讀 35,430評(píng)論 5 343
  • 正文 年R本政府宣布彻桃,位于F島的核電站坛善,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏邻眷。R本人自食惡果不足惜眠屎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望肆饶。 院中可真熱鬧改衩,春花似錦、人聲如沸驯镊。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,676評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)板惑。三九已至橄镜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間冯乘,已是汗流浹背洽胶。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,829評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留裆馒,地道東北人姊氓。 一個(gè)月前我還...
    沈念sama閱讀 47,743評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像喷好,于是被迫代替她去往敵國(guó)和親翔横。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • single-spa 起因是看了一下掘金的這篇鏈接; [每日優(yōu)鮮供應(yīng)鏈前端團(tuán)隊(duì)微前端改造](https://jue...
    寶妞兒閱讀 40,415評(píng)論 7 17
  • ng-model 指令ng-model 指令 綁定 HTML 元素 到應(yīng)用程序數(shù)據(jù)梗搅。ng-model 指令也可以:...
    壬萬(wàn)er閱讀 869評(píng)論 0 2
  • 寫在開(kāi)頭 先說(shuō)說(shuō)為什么要寫這篇文章, 最初的原因是組里的小朋友們看了webpack文檔后, 表情都是這樣的: (摘...
    Lefter閱讀 5,286評(píng)論 4 31
  • 清晨的細(xì)雨 催生了炊煙升起 雞鳴擾亂了思緒 誰(shuí)家的額娘 早起 讓我聞到了油鍋里爆姜炒蒜的香氣 是誰(shuí)和我在故鄉(xiāng)的情感...
    心者悟道閱讀 142評(píng)論 0 3
  • 今天早上發(fā)生了一件不太美妙的事棕孙,沉寂已久的情緒還是壓制不住,和別人吵架了。說(shuō)實(shí)話蟀俊,自從開(kāi)始讀書寫字之后钦铺,發(fā)現(xiàn)我的情...
    夏末微光閱讀 185評(píng)論 0 1