用Docker簡(jiǎn)化Nodejs開(kāi)發(fā)4-全棧項(xiàng)目模板(vue+nginx+node+mongodb)

本文分析了用docker搭建一個(gè)Web全棧項(xiàng)目(vue+nginx+node+mongodb)運(yùn)行環(huán)境時(shí)碰到的問(wèn)題,以一個(gè)開(kāi)箱即用的項(xiàng)目為例,整理了制作應(yīng)用docker鏡像的基礎(chǔ)模板和一些使用技巧。

現(xiàn)在越來(lái)越多的項(xiàng)目采用vue+nginx+node+mongodb的組合,這樣一個(gè)JS全棧工程師就可以獨(dú)立搞定一個(gè)完整的應(yīng)用。要達(dá)到這個(gè)目標(biāo),只會(huì)敲代碼是不夠的,還要能搞定運(yùn)行環(huán)境。通過(guò)使用docker可以極大簡(jiǎn)化運(yùn)行環(huán)境的搭建,并且給開(kāi)發(fā)和運(yùn)維的銜接工作帶來(lái)便利。

典型的全棧項(xiàng)目包括3個(gè)部分:前端(vue+nginx),后端(node)和數(shù)據(jù)庫(kù)(mongodb)。從部署的角度看,每個(gè)部分又可以分為3個(gè)部分:應(yīng)用(代碼+配置)、中間件和主機(jī)。docker解決了中間件和主機(jī)的組合問(wèn)題,我們用到的各種中間件都有標(biāo)準(zhǔn)docker鏡像,通過(guò)docker,它們的安裝和運(yùn)行都實(shí)現(xiàn)了標(biāo)準(zhǔn)化。我們真正要干的活是,如何以中間件鏡像為基礎(chǔ),加上應(yīng)用代碼和環(huán)境配置,制作項(xiàng)目的應(yīng)用鏡像。有了應(yīng)用鏡像,后面的工作就可以由運(yùn)維接手了。

因此,對(duì)于全棧工程師來(lái)說(shuō),需要掌握一項(xiàng)新技能:制作應(yīng)用鏡像。

下面,通過(guò)一個(gè)實(shí)際項(xiàng)目,描述一種制作應(yīng)用鏡像的通用方法。


項(xiàng)目概況

https://github.com/jasony62/tms-finder

tms-finder項(xiàng)目是一個(gè)在線文檔管理系統(tǒng),back目錄下是用node實(shí)現(xiàn)的后端服務(wù),ue目錄下是用Vue實(shí)現(xiàn)的用戶端應(yīng)用,build后部署到nginx。上傳文件時(shí)用戶可以輸入文件的描述信息(可配置),文件會(huì)存在放在服務(wù)端指定的本地硬盤(pán)上(可配置),描述信息會(huì)保存在指定的mongodb中(可配置)。

這個(gè)項(xiàng)目是開(kāi)箱即用的,在安裝好dockerdocker-compose的機(jī)器上,從github拉取代碼,執(zhí)行docker-compose up -d命令就可以把整個(gè)應(yīng)用運(yùn)行起來(lái)。

這個(gè)項(xiàng)目是環(huán)境友好的,制作的默認(rèn)鏡像可以靈活部署在不同的環(huán)境中(通過(guò)設(shè)置環(huán)境變量),也可以根據(jù)環(huán)境的要求制作新的鏡像(通過(guò)設(shè)置構(gòu)建參數(shù))。

這個(gè)項(xiàng)目是編碼友好的,程序員可以有選擇地使用docker,前后端都可以在容器外運(yùn)行,方便調(diào)試代碼。


關(guān)鍵概念

node環(huán)境變量

后臺(tái)服務(wù)是node應(yīng)用,Vue本質(zhì)上也是node應(yīng)用,所以應(yīng)該知道node的使用環(huán)境變量的方式。node中通過(guò)process.env這個(gè)對(duì)象訪問(wèn)環(huán)境變量,該對(duì)象可以修改,但是只會(huì)在應(yīng)用內(nèi)有效。

進(jìn)入容器后,可以通過(guò)下面的命令查看可用的環(huán)境變量:

node -e "console.log(process.env)"

需要注意的是,使用vue-service-cli命令時(shí),Vue對(duì)process.env做了處理,并不直接傳遞所有環(huán)境變量,而是要通過(guò).env傳遞,且必須以VUE_APP_開(kāi)頭。這會(huì)對(duì)制作鏡像產(chǎn)生影響。

docker環(huán)境變量

docker-compose中和環(huán)境變量設(shè)置相關(guān)的指令主要是enviromentenv_file,另外,多配置文件也會(huì)影響影響環(huán)境變量設(shè)置,詳細(xì)信息請(qǐng)看在線文檔。tms-finder項(xiàng)目中需要知道的是,在compose配置文件中設(shè)置的變量會(huì)傳遞給容器(process.env),docker-compose.override.yml中的內(nèi)容會(huì)覆蓋docker-compose.yml中的內(nèi)容。(后面會(huì)用到)

我采用在docker-compose.yml定義環(huán)境變量的默認(rèn)值(在版本庫(kù)),如果需要修改就通過(guò)docker-compose.override.yml覆蓋(不在版本庫(kù))。

tms-finder中包含了這兩個(gè)文件,docker-compose.yml用于指定基礎(chǔ)設(shè)置,docker-compose.override.yml用于指定和運(yùn)行環(huán)境相關(guān)的設(shè)置,例如:端口號(hào)等。如果有更多的配置要求,可以通過(guò)docker-compose -f解決。

注意:端口(ports)不能通過(guò)覆蓋,環(huán)境變量(environment)和文件卷(volumes)可以,所以端口沒(méi)有寫(xiě)在docker-compose.yml文件中,這樣有利于復(fù)用。另外,只能是“覆蓋”并不能“清除”,例如:volumns只能從一個(gè)設(shè)置改成另一個(gè),而不能清除掉。

參考:https://docs.docker.com/compose/environment-variables/

參考:https://docs.docker.com/compose/extends/

強(qiáng)調(diào)一個(gè)概念,環(huán)境變量是作用于容器的,在構(gòu)造階段是無(wú)效的。

docker參數(shù)(ARG)

Dockerfile有個(gè)ARG指令,用來(lái)定義在構(gòu)造鏡像時(shí),從外部接收的參數(shù)。在docker-compose中和ARG對(duì)應(yīng)的是build/args。

通過(guò)ARG在鏡像構(gòu)造階段傳遞參數(shù)。

docker網(wǎng)絡(luò)

docker網(wǎng)絡(luò)涉及很多內(nèi)容,現(xiàn)在只需要記住一點(diǎn),docker-compose.yml中定義的服務(wù)會(huì)被自動(dòng)添加到一個(gè)默認(rèn)docker的網(wǎng)絡(luò)(tms-finder_default)中,服務(wù)之間可以將服務(wù)名用作主機(jī)名相互訪問(wèn)。

參考:https://docs.docker.com/compose/networking/


數(shù)據(jù)庫(kù)(mongodb)

mongodb的標(biāo)準(zhǔn)鏡像是以ubuntu為基礎(chǔ),需要調(diào)整時(shí)區(qū),編寫(xiě)mongodb/Dockerfile文件。

RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

分別在docker-compose.ymldocker-compose.override.yml中添加服務(wù)。

  mongodb:
    build: ./mongodb
    image: tms-finder/mongo:latest
    container_name: tms-finder-mongo
  mongodb:
    volumes:
      - ./mongodb/data:/data/db
    ports:
      - '27017:27017'
    # logging:
    #   driver: 'none'

存儲(chǔ)數(shù)據(jù)文件

這里需要注意volumes部分。通常,數(shù)據(jù)庫(kù)中間件應(yīng)該將數(shù)據(jù)目錄掛載在主機(jī)的路徑上,這樣每次重啟容器數(shù)據(jù)還在(docker-compose.override.yml中的設(shè)置)。但是,有時(shí)候可能并不需要持久化數(shù)據(jù)(在windows環(huán)境下不能掛載宿主機(jī)的目錄),例如:測(cè)試,這時(shí)可以把數(shù)據(jù)放在容器內(nèi)部,重啟容器就可以清理數(shù)據(jù)了。

啟動(dòng)服務(wù)

編碼階段如果為了方便調(diào)試,可以單獨(dú)啟動(dòng)mongodb服務(wù),命令如下:

docker-compose up mongodb


后端(node)

編寫(xiě)back/Dockerfile文件,將后端代碼放在標(biāo)準(zhǔn)node鏡像中形成新鏡像。

FROM node:alpine

# 設(shè)置時(shí)區(qū)
RUN sed -i 's?http://dl-cdn.alpinelinux.org/?https://mirrors.aliyun.com/?' /etc/apk/repositories && \
  apk add -U tzdata && \
  cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
  apk del tzdata

RUN npm install cnpm -g

WORKDIR /home/node/app

COPY . .

RUN cnpm i

CMD [ "node", "app" ]

選擇node的官方標(biāo)準(zhǔn)鏡像node:alpine。用npmyarn安裝依賴包都會(huì)失敗,所以在鏡像中安裝并使用cnpm。這里提個(gè)醒,應(yīng)該避免從操作系統(tǒng)的鏡像開(kāi)始制作自己的鏡像,優(yōu)先從hub.docker.com上找中間件官方鏡像,并且按照在線文檔進(jìn)行相應(yīng)的設(shè)置,這樣省很多事。

docker-compose.yml中添加服務(wù)。

  back:
    build: ./back
    image: tms-finder/back:latest
    container_name: tms-finder-back
    # ports:
    #   - '3000:3000'
    environment:
      - NODE_ENV=production
      - TMS_FINDER_MONGODB_HOST=mongodb
      - TMS_FINDER_MONGODB_PORT=27017
      - TMS_FINDER_FS_ROOTDIR=/home/storage
    volumes:
      - ./back/storage/upload:/home/storage/upload # 指定上傳文件的外部存儲(chǔ)位置
    command: ['./wait-for.sh', 'mongodb:27017', '-t', '300', '--', 'node', 'app']

docker-compose.override.yml中添加服務(wù)。

  back:
    ports:
      - '3000:3000'

設(shè)置環(huán)境變量

后端服務(wù)中使用了多個(gè)配置文件,包括:連接mongodbconfig/mongodb.js,設(shè)置文件上傳服務(wù)的config/fs.js等。因?yàn)殓R像只是一個(gè)“模板”,實(shí)際部署時(shí)運(yùn)行環(huán)境不是確定的,例如:可能在生產(chǎn)環(huán)境中提供了獨(dú)立的mongodb服務(wù),需要通過(guò)配置將應(yīng)用指向這個(gè)服務(wù)。我們通過(guò)設(shè)置環(huán)境變量解決這個(gè)問(wèn)題。

下面以連接mongodb服務(wù)的配置文件config/mongodb.js為例:

module.exports = {
  master: {
    host: process.env.TMS_FINDER_MONGODB_HOST,
    port: parseInt(process.env.TMS_FINDER_MONGODB_PORT)
  }
}

docker-compose.yml文件中enviroment指令部分定義了環(huán)境變量TMS_FINDER_MONGODB_HOSTTMS_FINDER_MONGODB_PORT的值,容器啟動(dòng)后,node中可以通過(guò)process.env訪問(wèn)這些環(huán)境變量。注意這里的TMS_FINDER_MONGODB_HOST=mongodb,其中的mongodb是服務(wù)名,前面提到可以將服務(wù)名作為主機(jī)名進(jìn)行訪問(wèn)。

服務(wù)啟動(dòng)順序

通過(guò)docker-compose.yml同時(shí)啟動(dòng)多個(gè)服務(wù)時(shí)存在啟動(dòng)順序的問(wèn)題:后端服務(wù)back要連接mongodb,但是,如果mongodb啟動(dòng)需要很長(zhǎng)時(shí)間(例如:數(shù)據(jù)文件放在宿主機(jī)上),back服務(wù)就有可能因連接超時(shí)啟動(dòng)失敗。為了解決這個(gè)問(wèn)題,我找到了一個(gè)腳本wait_for。通過(guò)這個(gè)腳本可以測(cè)試指定的端口是否已經(jīng)可用,如果在指定時(shí)間內(nèi)確定可用,就執(zhí)行后面的命令。

參考:https://docs.docker.com/v17.12/compose/startup-order/

啟動(dòng)指定服務(wù)

如果為了調(diào)試前端代碼,只需要同時(shí)啟動(dòng)mongodbback,可以執(zhí)行如下命令:

docker-compose up mongodb back

容器外運(yùn)行

不用容器啟動(dòng)時(shí)node服務(wù)時(shí),用npm run pm2啟動(dòng)。pm2支持設(shè)置環(huán)境變量,在ecosystem.config.js文件中進(jìn)行設(shè)置。

      env: {
        NODE_ENV: 'development',
        TMS_FINDER_MONGODB_HOST: 'localhost',
        TMS_FINDER_MONGODB_PORT: 27017
      }

容器外運(yùn)行主要是為了方便調(diào)試代碼,通過(guò)容器啟動(dòng)的mongodb就在本機(jī),所以主機(jī)地址設(shè)置為localhost。

參考:https://pm2.keymetrics.io/docs/usage/environment/


前端鏡像(vue+nginx)

Vue項(xiàng)目要部署的內(nèi)容是通過(guò)yarn build命令生成的靜態(tài)代碼,這些代碼可以部署到任何WebServer中,例如:nginx。

編寫(xiě)ue/Dockerfile文件。

# 標(biāo)準(zhǔn)基礎(chǔ)鏡像(構(gòu)建階段)
FROM node:alpine

RUN npm install cnpm -g

WORKDIR /home/node/app

COPY . .

# 生成.env文件
ARG vue_app_base_url
ARG vue_app_auth_server
ARG vue_app_login_key_username=username
ARG vue_app_login_key_password=password
ARG vue_app_login_key_pin=pin
ARG vue_app_api_server

RUN echo VUE_APP_BASE_URL=$vue_app_base_url > .env && \
  echo VUE_APP_AUTH_SERVER=$vue_app_auth_server >> .env && \
  echo VUE_APP_LOGIN_KEY_USERNAME=$vue_app_login_key_username >> .env && \
  echo VUE_APP_LOGIN_KEY_PASSWORD=$vue_app_login_key_password  >> .env && \
  echo VUE_APP_LOGIN_KEY_PIN=$vue_app_login_key_pin  >> .env && \
  echo VUE_APP_API_SERVER=$vue_app_api_server >> .env

# 安裝依賴包,構(gòu)建代碼
RUN cnpm i && yarn build

# 標(biāo)準(zhǔn)基礎(chǔ)鏡像(部署階段)
FROM nginx:alpine

# 設(shè)置時(shí)區(qū)
RUN sed -i 's?http://dl-cdn.alpinelinux.org/?https://mirrors.aliyun.com/?' /etc/apk/repositories && \
  apk add -U tzdata && \
  cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
  apk del tzdata

# 修改配置文件
ADD ./nginx.conf.template /etc/nginx/nginx.conf.template

ADD ./start_nginx.sh /usr/local/bin/start_nginx.sh

RUN chmod +x /usr/local/bin/start_nginx.sh

# 將構(gòu)建階段代碼放在指定位置
COPY --from=0 /home/node/app/dist /usr/share/nginx/html

CMD ["start_nginx.sh"]

docker-compose.yml中添加服務(wù)。

  ue:
    build:
      context: ./ue
      args:
        vue_app_base_url: /finder_ue
        vue_app_auth_server: http://localhost:3000
        vue_app_login_key_username: username
        vue_app_login_key_password: password
        vue_app_login_key_pin: pin
        vue_app_api_server: http://localhost:3000
    image: tms-finder/ue:latest
    container_name: tms-finder-ue
    environment:
      - NGINX_WEB_BASE_URL=/finder_ue
      - NGINX_ACCESS_CONTROL_ALLOW_ORIGIN=*
    # ports:
    #   - '8080:80'

docker-compose.override.yml中添加服務(wù)。

  ue:
    ports:
      - '8080:80'

多階段構(gòu)建

FROM can appear multiple times within a single Dockerfile to create multiple images or use one build stage as a dependency for another. Simply make a note of the last image ID output by the commit before each new FROM instruction. Each FROM instruction clears any state created by previous instructions.

Optionally a name can be given to a new build stage by adding AS name to the FROM instruction. The name can be used in subsequent FROM and COPY --from=<name|index> instructions to refer to the image built in this stage.

上面兩段來(lái)自docker的官方文檔,簡(jiǎn)單說(shuō)就是在一個(gè)Dockerfile中,可以有多個(gè)FROM,每個(gè)構(gòu)成一個(gè)階段,后面的階段可使用前面階段生成的內(nèi)容,最終生成的鏡像只包含最后一個(gè)FROM的內(nèi)容。

這個(gè)特性恰好滿足了制作Vue前端代碼鏡像的需求,因?yàn)?code>build要依賴node環(huán)境,但是最終運(yùn)行環(huán)境只需要nginx和代碼。所以ue/Dockerfile分為了構(gòu)建和部署兩個(gè)階段,構(gòu)建階段生成代碼,然后在部署階段放到nginx的指定目錄。

關(guān)于路由(VueRouter)

通常,復(fù)雜一些的Vue項(xiàng)目中都會(huì)用到Router,如果采用html5的history模式,需要在nginx.conf文件中進(jìn)行設(shè)置。

參考:https://router.vuejs.org/zh/guide/essentials/history-mode.html

ue目錄下編制nginx.conf.template文件。

location $NGINX_WEB_BASE_URL/web {
  root   /usr/share/nginx/html;
  try_files $uri $uri/index.html $NGINX_WEB_BASE_URL/web/index.html;
}

try_filesnginx的指令,功能是查找指定的文件,找到了就返回,如果找不到就轉(zhuǎn)發(fā)最后1個(gè)url。在配置路由的Vue項(xiàng)目中,如果url找不到對(duì)應(yīng)的文件就返回index.html,由前端代碼解決路由問(wèn)題。

$NGINX_WEB_BASE_URL是環(huán)境變量,下面講。

關(guān)于基礎(chǔ)路徑(publicPath和BASE_URI)

Vue項(xiàng)目的vue.config.js文件中有個(gè)publicPath設(shè)置,默認(rèn)值是'/',其作用如下:

默認(rèn)情況下,Vue CLI 會(huì)假設(shè)你的應(yīng)用是被部署在一個(gè)域名的根路徑上,例如 https://www.my-app.com/。如果應(yīng)用被部署在一個(gè)子路徑上,你就需要用這個(gè)選項(xiàng)指定這個(gè)子路徑。例如,如果你的應(yīng)用被部署在 https://www.my-app.com/my-app/,則設(shè)置publicPath為 /my-app/。

采用默認(rèn)值,build生成的index.html文件中引用生成的js文件路徑如下:

<script src="/js/chunk-vendors.4aa92667.js"></script>
<script src="/web/js/app.b474d9b9.js"></script>

如果publicPath設(shè)置為‘/my-app’,引用的js文件路徑如下:

<script src="/my-app/js/chunk-vendors.4aa92667.js"></script>
<script src="/my-app/web/js/app.b474d9b9.js"></script>

雖然nginx可以通過(guò)rewriteproxy_pass改變url指向的內(nèi)容,但是,如果build時(shí)沒(méi)有指定publicPath,而是通過(guò)nginxhttps://www.my-app.com/my-app/指向index.html,那么即使瀏覽器可以正確獲得html文件,可是它引用的js文件地址為https://www.my-app.com/js/chunk-vendors.4aa92667.js,還是無(wú)法找到這個(gè)文件。

如果編碼階段就確定publicPath,直接修改相關(guān)配置就好了。但是,我們不希望代碼與環(huán)境硬綁定,基礎(chǔ)路徑和代碼自身的業(yè)務(wù)邏輯無(wú)關(guān),是一個(gè)部署需求,因此通過(guò)環(huán)境變量進(jìn)行設(shè)置。

這時(shí)又會(huì)用到Vue的另外一個(gè)參數(shù)outputDir,默認(rèn)值為dist,其作用如下:

當(dāng)運(yùn)行 vue-cli-service build 時(shí)生成的生產(chǎn)環(huán)境構(gòu)建文件的目錄。

ue下新建vue.config.js文件中進(jìn)行這兩個(gè)參數(shù)的設(shè)置。

const VUE_APP_BASE_URL = process.env.VUE_APP_BASE_URL ? process.env.VUE_APP_BASE_URL : ''

module.exports = {
  publicPath: `${VUE_APP_BASE_URL}/web`,
  outputDir: `dist${VUE_APP_BASE_URL}/web`
}

注:為什么以web為結(jié)尾和這篇文章的主題無(wú)關(guān),可忽略。

調(diào)用后臺(tái)API

前端代碼中需要確定后端服務(wù)API的地址,而且在構(gòu)建階段就要確定。按照代碼不應(yīng)該與環(huán)境硬綁定的原則,也需要通過(guò)環(huán)境變量進(jìn)行指定,下面以2個(gè)文件為例。

程序文件ue/src/apis/auth.js

const baseAuth = (process.env.VUE_APP_AUTH_SERVER || '') + '/auth'

程序文件ue/src/apis/file/browse.js

const baseApi = (process.env.VUE_APP_API_SERVER || '') + '/file/browse'

生成.env文件

本以為環(huán)境變量可以直接傳遞到vuebuild過(guò)程中,但是因?yàn)関ue對(duì)process.env做了處理,環(huán)境變量只能通過(guò).env文件傳遞。所以,我在Dockerfile中直接通過(guò)傳遞的ARG生成.env文件。

生成nginx.conf文件

生成nginx.conf文件復(fù)雜一些,需要用到envsubst命令,這個(gè)命令在nginx:alpine中已經(jīng)包含,它的作用是替換文件中的環(huán)境變量并生成新文件。編寫(xiě)start_nginx.sh這個(gè)shell腳本,實(shí)現(xiàn)替換nginx.conf.template中的環(huán)境變量,生成新的nginx.conf文件,并啟動(dòng)nginx。

注意:.env文件不能用這種模板文件的方式生成,因?yàn)榄h(huán)境變量只作用于容器內(nèi),多階段構(gòu)建中,前面的階段并不會(huì)創(chuàng)建容器,所以環(huán)境變量用不上。

容器外運(yùn)行

Vue在編碼階段并不需要docker,通過(guò)yarn serve命令就可以運(yùn)行。

Vue設(shè)置環(huán)境變量的官方方法是用.env文件。

參考:https://cli.vuejs.org/zh/guide/mode-and-env.html

ue目錄下編寫(xiě).env文件(包含在版本中)

VUE_APP_BASE_URL=/finder_ue
VUE_APP_AUTH_SERVER=http://localhost:3000
VUE_APP_LOGIN_KEY_USERNAME=username
VUE_APP_LOGIN_KEY_PASSWORD=password
VUE_APP_LOGIN_KEY_PIN=pin
VUE_APP_API_SERVER=http://localhost:3000

如果需要修改定義的值,可以在ue目錄編寫(xiě).env.local(不包含在版本中)進(jìn)行覆蓋,例如:

VUE_APP_BASE_URL=

需要注意的是不要把.env或者.env.local不要放入docker容器,應(yīng)該用.dockerignore文件忽略掉。

總結(jié)和其它

docker極大降低了運(yùn)行環(huán)境搭建的門(mén)檻,只要掌握基本用法,程序員完成可以搞定一個(gè)應(yīng)用的基本運(yùn)行環(huán)境。

為了讓?xiě)?yīng)用更容易部署,應(yīng)該盡量減少代碼對(duì)運(yùn)行環(huán)境的硬依賴,將這些依賴轉(zhuǎn)化為可在部署時(shí)指定的環(huán)境變量。

本項(xiàng)目總結(jié)了不少使用docker的實(shí)用方法,這些方法可以用到同類(lèi)型的項(xiàng)目中,這樣可以更有效地搭建全棧項(xiàng)目。

本系列其他文章

用Docker簡(jiǎn)化Nodejs開(kāi)發(fā)1——開(kāi)發(fā)環(huán)境

用Docker簡(jiǎn)化Nodejs開(kāi)發(fā)2——開(kāi)發(fā)環(huán)境到測(cè)試環(huán)境

用Docker簡(jiǎn)化Nodejs開(kāi)發(fā)3——用webhook實(shí)現(xiàn)自動(dòng)更新

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容