以前使用 Vue 全家桶開發了我的博客,並部署在阿里雲服務器上,最近在學習 React,因而使用 React 開發重構了本身的博客。css
主要技術棧以下:html
React 搭建博客先後臺部分,這裏不會細講,只會說說中間遇到的一些問題和一些解決方法,具體開發教程可參考 React Hooks+Egg.js實戰視頻教程-技術胖Blog開發。前端
部署部分這裏會是重點講解,由於也是第一次接觸 Docker,這裏只記錄本身的學習心得,有不對的地方還請多多指教。vue
剛好本次項目裏前臺頁面是 node 運行,後臺界面是靜態 HTML,服務接口須要鏈接 Mysql,我以爲 Docker 來部署這幾種狀況也是比較全面的例子了,能夠給後來同窗做爲參考,內容比較囉嗦,但願能幫助後來的同窗少走一點坑,由於有些是本身的理解,可能會有錯誤,還請你們指正,互相學習。node
源碼地址:https://github.com/Moon-Futur...mysql
clone 下來參照目錄哦~react
博客前臺使用 Next.js 服務端渲染框架搭建,後臺管理界面使用 create-react-app 腳手架搭建,服務接口使用 Egg 框架(基於 Koa)。後臺管理和服務接口沒什麼好說的,就是一些 React 基礎知識,這裏主要說下 Next.js 中遇到的一些問題。linux
項目目錄:blog:前臺界面,Next.jsios
admin:後臺管理界面,create-react-app 腳手架搭建nginx
service:先後臺服務接口
由於是服務端渲染,因此頁面初始數據會在服務器端獲取後,渲染頁面後返回給前端,這裏有兩個官方 API,getStaticProps
,getServerSideProps
,從名字能夠稍微看出一點區別。(Next.js 9.3 版本以上,使用 getStaticProps
或 getServerSideProps
來替代 getInitialProps
。)
getStaticProps:服務端獲取靜態數據,在獲取數據後生成靜態 HTML 頁面,以後在每次請求時都重用此頁面
const Article = (props) => { return () } /* 也可 export default class Article extends React.Component { render() { return } } */ export async function getStaticProps(context) { try { const result = await axios.post(api.getArticleList) return { props: { articleList: result.data }, // will be passed to the page component as props } } catch (e) { return { props: { articleList: [] }, // will be passed to the page component as props } } } export default Article
getServerSideProps:每次請求時,服務端都會去從新獲取獲取生成 HTML 頁面
const Article = (props) => { return () } /* 也可 export default class Article extends React.Component { render() { return } } */ export async function getServerSideProps(context) { try { const result = await axios.post(api.getArticleList) return { props: { articleList: result.data }, // will be passed to the page component as props } } catch (e) { return { props: { articleList: [] }, // will be passed to the page component as props } } } export default Article
能夠看到二者用法是同樣的。
開發模式下 npm run dev
,二者沒什麼區別,每次請求頁面都會從新獲取數據。
生產環境下,須要先npm run build
生成靜態頁面,使用 getStaticProps 獲取數據的話就會在此命令下生產靜態 HTML 頁面,而後npm run start
,後面每次請求都會重用靜態頁面,而使用 getServerSideProps 每次請求都會從新獲取數據。
返回數據 都是對象形式,且只能是對象,key 是 props,會傳遞到類或函數裏面的 props。
博客這裏由於是獲取博客文章列表,數據隨時可能變化,因此選用 getServerSideProps 。
這裏使用 try,catch 捕獲異常,防止獲取數據失敗或者後端接口報錯,服務端渲染錯誤返回不了頁面。
還有一些數據,咱們並不但願在服務端獲取渲染到頁面裏,而是但願頁面加載後再操做。
使用 React Hook,能夠在 useEffect
中操做:
const Article = (props) => { useEffect(async () => { await axios.get('') }, []) return () } export async function getServerSideProps(context) { try { const result = await axios.post(api.getArticleList) return { props: { articleList: result.data }, // will be passed to the page component as props } } catch (e) { return { props: { articleList: [] }, // will be passed to the page component as props } } } export default Article
這裏注意 useEffect
第二個參數,表明是否執行的依賴。
使用 Class,能夠在 componenDidMount
中操做:
export default class Article extends React.Component { componenDidMount() { await axios.get('') } render() { return } } export async function getServerSideProps(context) { try { const result = await axios.post(api.getArticleList) return { props: { articleList: result.data }, // will be passed to the page component as props } } catch (e) { return { props: { articleList: [] }, // will be passed to the page component as props } } } export default Article
頁面進入、退出動畫找到一個比較好用的庫 framer-motion, https://www.framer.com/api/motion/
先改造一下 pages/_app.js,引入 framer-motion
npm install framer-motion -S
import { AnimatePresence } from 'framer-motion' export default function MyApp({ Component, pageProps, router }) { return ( <AnimatePresence exitBeforeEnter> <Component {...pageProps} route={router.route} key={router.route} /> </AnimatePresence> ) }
在每一個頁面裏經過在元素標籤前加 motion 實現動畫效果,如 pages/article.js 頁面
const postVariants = { initial: { scale: 0.96, y: 30, opacity: 0 }, enter: { scale: 1, y: 0, opacity: 1, transition: { duration: 0.5, ease: [0.48, 0.15, 0.25, 0.96] } }, exit: { scale: 0.6, y: 100, opacity: 0, transition: { duration: 0.5, ease: [0.48, 0.15, 0.25, 0.96] }, }, } const sentenceVariants = { initial: { scale: 0.96, opacity: 1 }, exit: { scale: 0.6, y: 100, x: -300, opacity: 0, transition: { duration: 0.5, ease: [0.48, 0.15, 0.25, 0.96] }, }, } const Article = (props) => { const { articleList, route } = props const [poetry, setPoetry] = useState(null) const getPoetry = (data) => { setPoetry(data) } return ( <div className="container article-container"> <Head> <title>學無止境,厚積薄發</title> </Head> <Header route={route} /> <div className="page-background"></div> <div style={{ height: '500px' }}></div> <Row className="comm-main comm-main-index" type="flex" justify="center"> <Col className="comm-left" xs={0} sm={0} md={0} lg={5} xl={4} xxl={3}> <Author /> <Project /> <Poetry poetry={poetry} /> </Col> <Col className="comm-center" xs={24} sm={24} md={24} lg={16} xl={16} xxl={16}> <motion.div className="sentence-wrap" initial="initial" animate="enter" exit="exit" variants={sentenceVariants}> <PoetrySentence staticFlag={true} handlePoetry={getPoetry} /> </motion.div> <div className="comm-center-bg"></div> <motion.div initial="initial" animate="enter" exit="exit" variants={postVariants} className="comm-center-content"> <BlogList articleList={articleList} /> </motion.div> </Col> </Row> </div> ) }
須要實現動畫效果的元素標籤前加上 motion,在傳入 initial,animate,exit,variants 等參數,variants 中
const postVariants = { initial: { scale: 0.96, y: 30, opacity: 0 }, enter: { scale: 1, y: 0, opacity: 1, transition: { duration: 0.5, ease: [0.48, 0.15, 0.25, 0.96] } }, exit: { scale: 0.6, y: 100, opacity: 0, transition: { duration: 0.5, ease: [0.48, 0.15, 0.25, 0.96] }, }, } // initial 初始狀態 // enter 進入動畫 // exit 退出狀態 // 不想有退出動畫,不寫 exit 變量便可
注意:這裏使用 AnimatePresence 改造了 _app.js 後,每一個頁面都要使用到 motion,不然頁面切換不成功,不想要動畫的能夠以下給默認狀態便可:
const Article = (props) =>{ return ( <motion.div initial="initial" animate="enter" exit="exit"> ... </motion.div> ) }
在 Next.js 中使用 import Link from 'next/link'
能夠實現不刷新頁面切換頁面
import Link from 'next/link' const BlogList = (props) => { return ( <> <Link href={'/detailed?id=' + item.id}> <div className="list-title">{item.title}</div> </Link> </> ) } export default BlogList
由於是在服務端渲染,在點擊 Link 連接時,頁面會有一段時間沒任何反應,Next.js 默認會在右下角有一個轉動的黑色三角,但實在是引不起用戶注意。
這裏使用插件 nprogress,實現頂部加載進度條
npm install nprogress -S
仍是改造 _app.js
import 'antd/dist/antd.css' import '../static/style/common.less' import { AnimatePresence } from 'framer-motion' import NProgress from 'nprogress' import 'nprogress/nprogress.css' import Router from 'next/router' NProgress.configure({ minimum: 0.3, easing: 'ease', speed: 800, showSpinner: false, }) Router.events.on('routeChangeStart', () => NProgress.start()) Router.events.on('routeChangeComplete', () => NProgress.done()) Router.events.on('routeChangeError', () => NProgress.done()) export default function MyApp({ Component, pageProps, router }) { return ( <AnimatePresence exitBeforeEnter> <Component {...pageProps} route={router.route} key={router.route} /> </AnimatePresence> ) }
主要使用到 next/router 去監聽路由切換狀態,這裏也能夠自定義加載狀態。
在 Next.js 開發模式下,當第一次進入某個頁面時,發現當前頁面樣式加載失敗,必須刷新一下才能加載成功。
next-css: Routing to another page doesn't load CSS in development mode
Cant change page with 'next/link' & 'next-css'
在 Github 上也查到相關問題,說是在 _app.js 都引入一下,可是我試了下,仍是不行,不過好在這種狀況只在開發模式下,生產模式下沒什麼問題,因此也就沒在折騰了,就這樣刷新一下吧。
在 components/PoetrySentence.js 中實現動態寫一句詩的效果,在 class 中能夠同經過 setInterval 簡單實現,但在 React Hoot 中每次 render 從新渲染後都會執行 useEffect,或者 useEffect 依賴[] 就又只會執行一次,這裏就經過依賴單一變量加 setTimeout 實現。
在 components/PoetrySentence.js 中
import { useState, useEffect } from 'react' import { RedoOutlined } from '@ant-design/icons' import { getPoetry, formatTime } from '../util/index' const PoetrySentence = (props) => { const [sentence, setSentence] = useState('') const [finished, setFinished] = useState(false) const [words, setWords] = useState(null) const { staticFlag, handlePoetry } = props // 是否靜態展現 useEffect( async () => { if (words) { if (words.length) { setTimeout(() => { setWords(words) setSentence(sentence + words.shift()) }, 150) } else { setFinished(true) } } else { let tmp = await todayPoetry() if (staticFlag) { setFinished(true) setSentence(tmp.join('')) } else { setWords(tmp) setSentence(tmp.shift()) } } }, [sentence] ) const todayPoetry = () => { return new Promise((resolve) => { const now = formatTime(Date.now(), 'yyyy-MM-dd') let poetry = localStorage.getItem('poetry') if (poetry) { poetry = JSON.parse(poetry) if (poetry.time === now) { handlePoetry && handlePoetry(poetry) resolve(poetry.sentence.split('')) return } } getPoetry.load((result) => { poetry = { time: now, sentence: result.data.content, origin: { title: result.data.origin.title, author: result.data.origin.author, dynasty: result.data.origin.dynasty, content: result.data.origin.content, }, } handlePoetry && handlePoetry(poetry) localStorage.setItem('poetry', JSON.stringify(poetry)) resolve(poetry.sentence.split('')) }) }) } const refresh = () => { getPoetry.load((result) => { const poetry = { time: formatTime(Date.now(), 'yyyy-MM-dd'), sentence: result.data.content, origin: { title: result.data.origin.title, author: result.data.origin.author, dynasty: result.data.origin.dynasty, content: result.data.origin.content, }, } handlePoetry && handlePoetry(poetry) localStorage.setItem('poetry', JSON.stringify(poetry)) if (staticFlag) { setSentence(poetry.sentence) } else { setFinished(false) setWords(null) setSentence('') } }) } return ( <p className="poetry-sentence"> {sentence} {finished ? <RedoOutlined style={{ fontSize: '14px' }} onClick={() => refresh()} /> : null} <span style={{ visibility: finished ? 'hidden' : '' }}>|</span> </p> ) } export default PoetrySentence
useEffect 依賴變量 sentence,在 useEffect 中又去更改 sentence,sentence 更新後觸發從新渲染,又會從新執行 useEffect,在 useEffect 中加上 setTimeout 延遲,恰好完美實現了 setInterval 效果。
本來項目中使用的是 sass,但在後面 docker 部署安裝依賴時,實在時太慢了,還各類報錯,以前也是常常遇到,因此索性直接換成了 less,語法也差很少,安裝起來省心多了。
Docker 是一個開源的應用容器引擎,可讓開發者打包他們的應用以及依賴包到一個輕量級、可移植的容器中,而後發佈到任何流行的 Linux 機器上,也能夠實現虛擬化。
容器是徹底使用沙箱機制,相互之間不會有任何接口(相似 iPhone 的 app),更重要的是容器性能開銷極低。
對我而言,由於如今使用的是阿里雲服務器,部署了好幾個項目,若是服務器到期後,更換服務器的話,就須要將全部項目所有遷移到新服務器,每一個項目又要去依次安裝依賴,運行,nginx 配置等等,想一想都頭大。而使用 Docker 後,將單個項目與其依賴打包成鏡像,鏡像能夠在任何 Linux 中生產一個容器,遷移部署起來就方便多了。
其餘而已,使用 Docker 可讓開發環境、測試環境、生產環境一致,而且每一個容器都是一個服務,也方便後端實現微服務架構。
Docker 安裝最好是參照官方文檔,避免出現版本更新問題。https://docs.docker.com/engine/install/ 英文吃力的,這兩推薦一款神奇詞典 歐陸詞典,哪裏不會點哪裏,誰用誰說好。
Mac 和 Windows 都有客戶端,能夠很簡單的下載安裝,另外 Window 注意區分專業版、企業版、教育版、家庭版
由於我這裏使用的是阿里雲 Centos 7 服務器,因此簡單介紹一下在 Centos 下的安裝。
首先若已經安裝過 Docker,想再裝最新版,先協助舊版
$ sudo yum remove docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-engine
有三種安裝方式:
這裏選擇官方推薦的第一種方式安裝 Install using the repository。
一、SET UP THE REPOSITORY
安裝 yum-utils 工具包,設置存儲庫
$ sudo yum install -y yum-utils $ sudo yum-config-manager \ --add-repo \ https://download.docker.com/linux/centos/docker-ce.repo
二、安裝 docker
$ sudo yum install docker-ce docker-ce-cli containerd.io
這樣安裝的是最新的版本,也能夠選擇指定版本安裝
查看版本列表:
$ yum list docker-ce --showduplicates | sort -r Loading mirror speeds from cached hostfile Loaded plugins: fastestmirror Installed Packages docker-ce.x86_64 3:20.10.0-3.el7 docker-ce-stable docker-ce.x86_64 3:20.10.0-3.el7 @docker-ce-stable docker-ce.x86_64 3:19.03.9-3.el7 docker-ce-stable docker-ce.x86_64 3:19.03.8-3.el7 docker-ce-stable docker-ce.x86_64 3:19.03.7-3.el7 docker-ce-stable docker-ce.x86_64 3:19.03.6-3.el7 docker-ce-stable docker-ce.x86_64 3:19.03.5-3.el7 docker-ce-stable docker-ce.x86_64 3:19.03.4-3.el7 docker-ce-stable docker-ce.x86_64 3:19.03.3-3.el7 docker-ce-stable docker-ce.x86_64 3:19.03.2-3.el7 docker-ce-stable docker-ce.x86_64 3:19.03.14-3.el7 docker-ce-stable ......
選擇指定版本安裝
$ sudo yum install docker-ce-<VERSION_STRING> docker-ce-cli-<VERSION_STRING> containerd.io
安裝完成,查看版本
$ docker -v Docker version 20.10.0, build 7287ab3
三、啓動 docker
$ sudo systemctl start docker
關閉 docker
$ sudo systemctl stop docker
重啓 docker
$ sudo systemctl restart docker
Docker 把應用程序及其依賴,打包在 image 文件裏面。只有經過這個文件,才能生成 Docker 容器(Container)。image 文件能夠看做是容器的模板。Docker 根據 image 文件生成容器的實例。同一個 image 文件,能夠生成多個同時運行的容器實例。
image 文件是通用的,一臺機器的 image 文件拷貝到另外一臺機器,照樣可使用。通常來講,爲了節省時間,咱們應該儘可能使用別人製做好的 image 文件,而不是本身製做。即便要定製,也應該基於別人的 image 文件進行加工,而不是從零開始製做。
官方有個鏡像庫 Docker Hub,不少環境鏡像均可以從上面拉取。
$ docker images
或者
$ docker image ls
剛安裝完 docker,是沒有任何鏡像的
$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE
只查看所有鏡像 id
$ docker images -q # 或 $ docker image ls -q
這裏咱們嘗試從官方庫下載一個 nginx 鏡像,鏡像有點相似與 npm 全局依賴,拉取後,後面全部須要使用的 nginx 的鏡像均可以依賴此 nginx,不用再從新下載,剛開始學習時,我還覺得每一個使用到 nginx 的鏡像都要從新下載呢。
下載 nginx 鏡像 https://hub.docker.com/_/nginx
$ docker pull nginx Using default tag: latest latest: Pulling from library/nginx 6ec7b7d162b2: Pull complete cb420a90068e: Pull complete 2766c0bf2b07: Pull complete e05167b6a99d: Pull complete 70ac9d795e79: Pull complete Digest: sha256:4cf620a5c81390ee209398ecc18e5fb9dd0f5155cd82adcbae532fec94006fb9 Status: Downloaded newer image for nginx:latest docker.io/library/nginx:latest $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE nginx latest ae2feff98a0c 13 hours ago 133MB
docker images 查看剛剛安裝的 nginx 鏡像,有 5 個title,分別爲鏡像名稱,標籤,id,建立時間,大小,其中 TAG 標籤默認爲 latest 最新版,若是下載指定版本,能夠 : 後跟版本號
$ docker pull nginx:1.19
刪除鏡像可使用以下命令
$ docker rmi [image]
或者
$ docker image rm [image]
[image] 能夠是鏡像名稱+標籤,也能夠是鏡像 id,ru
$ docker rmi nginx:latest $ docker rmi ae2feff98a0c
刪除全部鏡像
$ docker rmi $(docker images -q)
刪除全部 none 鏡像
後面有些操做會重複建立相同的鏡像,本來的鏡像就會被覆蓋變爲 <none> ,能夠批量刪除
$ docker rmi $(docker images | grep "none" | awk '{print $3}')
上面咱們下載了 nginx 鏡像,可是要想運行咱們本身的項目,咱們還要製做本身項目的鏡像,而後來生成容器才能運行項目。
製做鏡像須要藉助 Dockerfile 文件,以本項目 admin 後臺界面爲例(也能夠任何 html 文件),由於其打包後只需使用到 nginx 便可訪問。
先在 admin 下運行命令 npm run build
打包生成 build 文件夾,下面包好 index.html 文件,在 admin/docker 文件夾下建立 Dockerfile 文件,內容以下
FROM nginx COPY ../build /usr/share/nginx/html EXPOSE 80
將 build,docker 兩個文件夾放在服務器同一目錄下,如 /dockerProject/admin
├─admin └─build └─index.html └─docker └─Dockerfile
在 docker 目錄下運行命令
$ docker build ./ -t admin:v1 Sending build context to Docker daemon 4.096kB Step 1/3 : FROM nginx ---> ae2feff98a0c Step 2/3 : COPY ../build /usr/share/nginx/html COPY failed: forbidden path outside the build context: ../build ()
./ 基於當前目錄爲構建上下文, -t 指定製做的鏡像名稱。
能夠看到上面報錯了,
The path must be inside the context of the build; you cannot ADD ../something/something, because the first step of a docker build is to send the context directory (and subdirectories) to the docker daemon.
上面大意是肯定構建上下文後,中間的一些文件操做就只能在當前上下文之間進行,有兩種方式解決
Dockfile 與 build 同目錄
├─admin └─build └─index.html └─Dockerfile
Dockerfile:
FROM nginx COPY ./build /usr/share/nginx/html EXPOSE 80
在 admin 目錄下執行命令
$ docker build ./ -t admin:v1 Sending build context to Docker daemon 3.094MB Step 1/3 : FROM nginx ---> ae2feff98a0c Step 2/3 : COPY ./build /usr/share/nginx/html ---> Using cache ---> 0e54c36f5d9a Step 3/3 : EXPOSE 80 ---> Using cache ---> 60db346d30e3 Successfully built 60db346d30e3 Successfully tagged admin:v1
依然將 Dokcerfile 放入 docker 中統一管理
├─admin └─build └─index.html └─docker └─Dockerfile
Dockerfile:
FROM nginx COPY ./build /usr/share/nginx/html EXPOSE 80
在 admin 目錄下執行命令
$ docker build -f docker/Dockerfile ./ -t admin:v1 Sending build context to Docker daemon 3.094MB Step 1/3 : FROM nginx ---> ae2feff98a0c Step 2/3 : COPY ./build /usr/share/nginx/html ---> Using cache ---> 0e54c36f5d9a Step 3/3 : EXPOSE 80 ---> Using cache ---> 60db346d30e3 Successfully built 60db346d30e3 Successfully tagged admin:v1
注意這裏的 ./build 路徑。-f (-file)指定一個 Dockfile 文件,./ 以當前路徑爲構建上下文,因此 build 路徑仍是 ./build
上面使用到了 Dockerfile 文件,由於內容比較少,這裏先不介紹,後面部署 Next.js 時在稍做說明。
上面生成了 admin:v1 鏡像,咱們查看一下
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE admin v1 60db346d30e3 51 minutes ago 136MB nginx latest ae2feff98a0c 14 hours ago 133MB
能夠看到多了 admin:v1 鏡像,且在上面構建鏡像時步驟 Step 1 ,速度很快,直接使用了以前下載的 nginx 鏡像,若是以前沒下載,這裏就會去下載。
項目運行在容器內,咱們須要經過一個鏡像建立一個容器。
$ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
或
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
這兩個命令只顯示正在運行的容器,報錯中止的都不會顯示,加上 -a (--all) 參能夠顯示所有
$ docker container ls -a $ docker ps -a
只查看全部容器 id
$ docker ps -aq
這裏咱們經過 admin:v1 來生成一個容器
$ docker create -p 9001:80 --name admin admin:v1
:v1
默認爲 :latest
還有不少參數,可自行了解 https://docs.docker.com/engine/reference/commandline/create/#options
生成容器後,我們來看看
$ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8d755bab5c73 admin:v1 "/docker-entrypoint.…" 5 minutes ago Created admin
能夠看到容器已經生成,但尚未運行,全部使用 docker ps
是看不到的
運行容器:docker start [container iD]
,【】裏面可使用容器 ID,也可使用容器名稱,都是惟一的
$ docker start admin $ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8d755bab5c73 admin:v1 "/docker-entrypoint.…" 8 minutes ago Up 3 seconds 0.0.0.0:9001->80/tcp admin
容器已經運行,此時經過服務器ip + 9001 端口(Mac、Windows 直接 localhost:9001)便可訪問到容器內部。
以上生成容器,運行容器也能夠一條命令
$ docker run -p 9001:80 --name admin admin:v1
刪除容器可使用以下命令
$ docker rm admin # id 或 name
若是容器在運行中,要先中止容器
$ docker stop admin
或者強制刪除
$ docker rm -f admin
中止全部容器
$ docker stop $(docker ps -aq)
刪除全部容器
$ docker rm $(docker ps -aq)
中止並刪除全部容器
$ docker stop $(docker ps -aq) & docker rm $(docker ps -aq)
運行容器時若是失敗,能夠查看日誌定位錯位
$ docker logs admin
容器就像一個文件系統,咱們也能夠進去查看裏面的文件,使用如下命令進入容器內部
$ docker exec -it admin /bin/sh
-i
參數讓容器的標準輸入持續打開,--interactive-t
參數讓 Docker 分配一個僞終端,並綁定到容器的標準輸入上, --tty進入容器內部後,可使用 Linux 命令訪問內部文件
$ ls bin boot dev docker-entrypoint.d docker-entrypoint.sh etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var $ cd usr/share/nginx/html $ ls 50x.html asset-manifest.json favicon.ico index.html manifest.json robots.txt static
進入 nginx 默認 html 目錄 usr/share/nginx/html,能夠看到咱們經過 Dockfile 拷貝過來的文件
經過上面能夠發現每次製做鏡像,生成容器,運行容器,都要輸入不少命令,實在是很不方便,若是隻要一個簡單的命令就能完成就行了,docker-compose 就能夠實現,固然,這只是它很小的一部分功能。
官方簡介以下:
Compose 是用於定義和運行多容器 Docker 應用程序的工具。經過Compose,您可使用 YAML 文件來配置應用程序的服務。而後,使用一個命令,就能夠從配置中建立並啓動全部服務。
使用Compose基本上是一個三步過程:
- 使用 Dockerfile 定義應用程序的環境,以即可以在任何地方複製它。
- 在 docker-compose.yml 中定義組成您的應用程序的服務,以便它們能夠在隔離的環境中一塊兒運行。
- 運行 docker-compose up,而後 Compose 啓動並運行整個應用程序。
參考官方文檔 Install Docker Compose ,這裏簡單介紹 Linux 安裝
運行命令
sudo curl -L "https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
如果安裝慢,能夠用 daocloud 下載
sudo curl -L https://get.daocloud.io/docker/compose/releases/download/1.25.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
添加可執行權限
sudo chmod +x /usr/local/bin/docker-compose
檢查是否安裝完成
docker-compose --version
docker-compose.yml 是 docker-compose 運行時使用文件,裏面配置了鏡像和容器一些參數,這裏來實現上面建立鏡像,生成容器,運行容器。
version: '3' services: admin: build: context: ../ dockerfile: ./docker/Dockerfile image: admin:v1 ports: - 9001:80 container_name: admin
配置參數有不少 https://docs.docker.com/compose/compose-file/,官網能夠詳解,這裏以及後面只說說用到的一些配置。
services:服務組
admin:服務名稱,惟一,多個 docker-compose.yml 有相同名稱的,下面的容器會覆蓋
將 docker-compose.yml 放入 docker 目錄下
├─admin └─build └─index.html └─docker └─Dockerfile └─docker-compose.yml
在 docker 目錄下運行
$ docker-compose up -d --build
docker-compose 與 build 同目錄
├─admin └─build └─index.html └─Dockerfile └─docker-compose.yml
則,docker-compose.yml
version: '3' services: admin: build: ./ image: admin:v1 ports: - 9001:80 container_name: admin
在 build 目錄下運行
$ docker-compose up -d --build
上面簡單介紹了 docker 的一些用法,借用靜態 HTML 文件與 nignx 鏡像建立運行了一個容器,可是其遠遠不止這些,下面就經過部署本博客來做爲例子再探探裏面的一些知識點。
源碼地址:https://github.com/Moon-Futur...,可下載下來看着目錄更清晰。
爲了統一維護 docker 文件,如下將 docker 相關文件都放在各自目錄下 docker 文件下,因此要特別主題構建上下文(context)的肯定。
端口映射 9000:9000,服務器端口:容器端口,如果線上服務器,要先在安全組裏開通對應的端口號
在 blog 目錄下建立 docker 目錄,docker 目錄下建立三個文件
.dockerignore
node_modules .next
Dockefile
# node 鏡像 # apline 版本的node會小不少 FROM node:12-alpine # 在容器中建立目錄 RUN mkdir -p /usr/src/app # 指定工做空間,後面的指令都會在當前目錄下執行 WORKDIR /usr/src/app # 拷貝 package.json COPY package.json /usr/src/app # 安裝依賴 RUN npm i --production --registry=https://registry.npm.taobao.org # 拷貝其餘全部文件到容器(除了 .dockerignore 中的目錄和文件) COPY . /usr/src/app # build RUN npm run build # 暴露端口 9000 EXPOSE 9000 # 運行容器時執行命令,每一個 Dokcerfile 只能有一個 CMD 命令,多個的話只有最後一個會執行 CMD [ "npm", "start" ]
Docker 鏡像是分層的,下面這些知識點很是重要:
- Dockerfile 中的每一個指令都會建立一個新的鏡像層,每一個 RUN 都是一個指令 https://docs.docker.com/engin...
- 鏡像層將被緩存和複用
- 當 Dockerfile 的指令修改了,複製的文件變化了,或者構建鏡像時指定的變量不一樣了,對應的鏡像層緩存就會失效
- 某一層的鏡像緩存失效以後,它以後的鏡像層緩存都會失效
- 鏡像層是不可變的,若是咱們再某一層中添加一個文件,而後在下一層中刪除它,則鏡像中依然會包含該文件(只是這個文件在 Docker 容器中不可見了)。
因此咱們先拷貝 package.json,而後 RUN npm i 安裝依賴,造成一個鏡像層,再拷貝其餘全部文件,造成一個鏡像層,以後若是代碼有所變更,可是 package.json 沒有變更,再次執行時,就不會再安裝依賴了,能夠節省不少時間。package.json 有變更,纔會從新執行 RUN run i 安裝依賴。
假如生成了鏡像 imageA,此時要刪除 imageA,從新生成,記住先生成新的鏡像 imageB,這樣纔會複用 npm 包,若是先刪除了 imageA,再新生成 imageB,則又會從新安裝依賴。
在 blog 目錄下運行如下命令能夠生成鏡像 react_blog:blog
$ docker build -f docker/Dockerfile . -t react_blog:blog
第一次運行安裝依賴時有點慢(有個 sharp 特別慢...),剛開始我使用 node-sass 時,安裝老是報錯,後來索性就換成了 less,省心。若是想用 yarn 安裝的話,這裏 Dockerfile 裏 npm 相關的命令也能夠換成對於的 yarn 命令。
漫長的等待後終於 build 成功,下面一些信息就是 npm run build 生成的文件
看看生成的鏡像
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE react_blog blog fef06dfed97f 3 minutes ago 329MB nginx latest ae2feff98a0c 31 hours ago 133MB node 12-alpine 844f8bb6a3f8 3 weeks ago 89.7MB
而後來生成並運行容器
$ docker run -itd -p 9000:9000 --name react_blog_blog react_blog:blog
這裏參數再說明一下:
-i
參數讓容器的標準輸入持續打開,--interactive-t
參數讓 Docker 分配一個僞終端,並綁定到容器的標準輸入上, --tty-d
參數讓容器在後臺,以守護進程的方式執行,--detach(Run container in background and print container ID)--name
參數指定容器惟一名稱,若不指定,則隨機一個名稱-it 通常同時加上,-d 參數若是不加的話,運行容器成功時,會進入一個終端命令界面,要想退出的話只能 Ctrl + C,退出以後容器也就退出了,docker ps -a
能夠看到容器狀態是 Exited (0)
,可使用 docker start container
再次開啓。加上 -d 的話容器就會直接在後臺運行,通常的話就加上 -d。你們能夠試試,以後再刪除容器就能夠了。
以上容器運行成功的話,在瀏覽器經過 服務器ip:9000
就能夠訪問到頁面啦,Mac 或者 Windows 本地的話 localhost:9000
就能夠訪問啦。
docker-compose.yml
version: '3' services: web: build: context: ../ dockerfile: ./docker/Dockerfile image: react_blog:blog ports: - 9000:9000 container_name: react_blog_blog
上面我們經過 docker build
,docker run
等命令先生成容器,再生成並運行容器,是否是有點繁瑣,命令很差記,輸入也麻煩,這裏咱們就能夠利用 docker-compose 來簡化執行命令。
咱們看一下文件內容:
docker-compose
命令路徑要和 docker-compose.yml 同一路徑,因此這裏 context 構建上下文選擇上一層源碼目錄,dockerfile 就是當前目錄裏的 Dockerfile當前目錄_服務名_index
,index 數字(從1累加),若這裏爲 docker_web_1
能夠把上面用 Dockerfile 生成的容器刪了 docker rm -f react_blog_blog
,用 docker-compose up 生成試試
在 docker 目錄下執行命令
$ docker-compose up -d
要想從新生成鏡像能夠 docker-compose up -d --build
以上便把 blog 前端頁面部署好了,如今只是單獨部署學習,後面會刪了和後臺與接口一塊兒部署。
端口映射 9001:9001,服務器端口:容器端口,如果線上服務器,要先在安全組裏開通對應的端口號
如今來單獨部署 admin,在 Docker 篇時,咱們已經使用到 admin 來簡單部署學習製做鏡像和生成容器,這裏依然先在 admin 目錄下生成生成環境靜態文件
$ npm run build
在 admin 下建立 docker 目錄用來存放 docker 相關文件,docker 目錄下建立如下文件:
Dockerfile
FROM nginx # 刪除 Nginx 的默認配置 RUN rm /etc/nginx/conf.d/default.conf EXPOSE 80
注意這裏和上面的一些區別,
docker-compose.yml
version: '3' services: admin: build: context: ../ dockerfile: ./docker/Dockerfile image: react_blog:admin ports: - 9001:80 volumes: - ../build:/www - ./nginx.conf:/etc/nginx/conf.d/nginx.conf container_name: react_blog_admin
這裏多了 volumes (卷) 項,參數是數組,對應 宿主機文件:容器內文件
這樣作的好處是,當宿主機上的文件變更後,容器內的文件也會自動變更,相應的容器內文件變更,宿主機文件也會變更。這樣以後源代碼變更,從新打包生成 build 後,只須要放到服務器對應目錄下,容器類 /www 下的類容就會是最新的,而不須要一次次的去執行 Dockerfile 拷貝 build 文件到容器內,數據庫的數據一般也是這樣保存在宿主機內,而防止容器刪除時丟失數據。
同理 nginx.conf 配置文件也是同樣,不過改動 nginx 配置文件後,要重啓如下容器才生效 docker restart container
來運行容器吧,在 docker 目錄下執行命令
$ docker-compose up -d
查看容器是否運行成功
$ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 7db8ce1c6814 react_blog:admin "/docker-entrypoint.…" 16 minutes ago Up 16 minutes 0.0.0.0:9001->80/tcp react_blog_admin
運行失敗的能夠 docker logs container
查看日誌
運行成功的話,在瀏覽器經過 服務器ip:9001
就能夠訪問到頁面啦,Mac 或者 Windows 本地的話 localhost:9001
就能夠訪問啦。
nginx.conf
server { listen 80; sendfile on; sendfile_max_chunk 1M; tcp_nopush on; gzip_static on; location / { root /www; index index.html; } }
root 記得和上面掛在目錄相同
端口映射 9002:9002,服務器端口:容器端口,如果線上服務器,要先在安全組裏開通對應的端口號
如今咱們來部署服務接口,在 service 目錄下建立 docker 目錄,docker 目錄下建立如下文件:
.dockerignore
node_modules .github article
article 目錄用來存放博客內容文件
Dockerfile
FROM node:alpine # 配置環境變量 ENV NODE_ENV production # 這個是容器中的文件目錄 RUN mkdir -p /usr/src/app # 設置工做目錄 WORKDIR /usr/src/app # 拷貝package.json文件到工做目錄 # !!重要:package.json須要單獨添加。 # Docker在構建鏡像的時候,是一層一層構建的,僅當這一層有變化時,從新構建對應的層。 # 若是package.json和源代碼一塊兒添加到鏡像,則每次修改源碼都須要從新安裝npm模塊,這樣木有必要。 # 因此,正確的順序是: 添加package.json;安裝npm模塊;添加源代碼。 COPY package.json /usr/src/app/package.json # 安裝npm依賴(使用淘寶的鏡像源) # 若是使用的境外服務器,無需使用淘寶的鏡像源,即改成`RUN npm i`。 RUN npm i --production --registry=https://registry.npm.taobao.org # 拷貝全部源代碼到工做目 COPY . /usr/src/app # 暴露容器端口 EXPOSE 9002 CMD npm start
docker-compose.yml
version: '3' services: service: build: context: ../ dockerfile: ./docker/Dockerfile image: react_blog:service ports: - 9002:9002 depends_on: - db environment: MYSQL_HOST: localhost MYSQL_USER: root MYSQL_PASSWORD: 8023 volumes: - ../article:/usr/src/app/article container_name: react_blog_service db: image: mysql # volumes: # - /db_data:/var/lib/mysql ports: - 33061:3306 command: --default-authentication-plugin=mysql_native_password environment: MYSQL_ROOT_PASSWORD: 8023 # MYSQL_USER: root MYSQL_PASSWORD: 8023 MYSQL_DATABASE: react_blog container_name: react_blog_mysql
注意這裏有運行了兩個服務 service、db
service 服務是後端接口:
Client does not support authentication protocol requested by server; consider upgrading MySQL client
environment:環境變量,這裏會傳入代碼中,在代碼 /config/secret.js(secret-temp.js) 裏面能夠會使用到
/**
*/
module.exports = {
// mysql 鏈接配置 mysql: { host: process.env.MYSQL_HOST || 'localhost', port: process.env.MYSQL_PORT || '3306', user: process.env.MYSQL_USER || 'xxx', password: process.env.MYSQL_PASSWORD || 'xxx', database: process.env.MYSQL_DATABASE || 'xxxxxx', }, // jwt tokenConfig: { privateKey: 'xxxxxxxxxxxxxxxxxxxxxxxxx', },
}
- volumes:這裏我把文章寫入宿主機了,掛載到容器裏 **db** 服務是 Mysql 數據庫: - volumes:數據設置存儲在宿主機 - ports:端口映射,宿主機經過 33061 端口能夠訪問容器內部 Mysql,咱們以後就能夠經過 Navicat 或其餘數據庫可視化工具來鏈接 - environment:配置數據庫 - MYSQL_ROOT_PASSWORD 必需要帶上,設置 ROOT 帳號的密碼 - MYSQL_USER 容器登陸 MySQL 用戶名,**注意**,這裏若是是 root 會報錯 `ERROR 1396 (HY000): Operation CREATE USER failed for 'root'@'%' ` ,根據 https://github.com/docker-library/mysql/issues/129 可知,已經存在一個 root 用戶,沒法再建立,因此這個能夠不帶,就默認 root 用戶登陸,若是帶的話就不要是 root,會新建一個帳戶 - MYSQL_PASSWORD 容器登陸 Mysql 密碼,對用戶名 MYSQL_USER,若是是 ROOT,密碼就是 MYSQL_ROOT_PASSWORD,若是是其餘,就是設置新密碼 - MYSQL_DATABASE 建立一個 react_blog 數據庫,也能夠不填,後面再進入容器或者 Navicat 建立,可是這裏由於後端代碼要鏈接到 react_blog 數據庫,不建立的會鏈接會保存,因此仍是加上。(實在不想加也能夠後見建立好數據庫後,才運行兩個容器) 在 service/docker 目錄下執行命令
$ docker-compose up -d
運行成功的話,看看 images 和 container
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
react_blog service 89139d833458 About an hour ago 150MB
react_blog admin 1b5d6946f1fe 32 hours ago 133MB
react_blog blog fef06dfed97f 35 hours ago 329MB
nginx latest ae2feff98a0c 2 days ago 133MB
mysql latest ab2f358b8612 6 days ago 545MB
node 12-alpine 844f8bb6a3f8 3 weeks ago 89.7MB
能夠看到多了 Mysql 和 react_blog:blog 鏡像
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5878940d7626 react_blog:blog "docker..." 5 seconds ago Up 4 seconds 0.0.0.0:9000->9000/tcp react_blog_blog
3bff0060de19 react_blog:admin "/docker…" 3 minutes ago Up 18 seconds 0.0.0.0:9001->80/tcp react_blog_admin
d8a899232e8c react_blog:service "docker…" About a Exited (1) 5 minutes ago react_blog_service
a9da07ff5cae mysql "docker…" About an hr 33060/tcp, 0.0.0.0:33061->3306/tcp react_blog_mysql
能夠看到多了 react_blog_service 和 react_blog_mysql 容器,其中 react_blog_service 容器運行失敗了,顯示沒事失敗的先別高興,我們來看看日誌
$ docker logs react_blog_service
...
errno: "ECONNREFUSED"
code: "ECONNREFUSED"
syscall: "connect"
address: "127.0.0.1"
port: 3306
fatal: true
name: "ECONNREFUSEDError"
pid: 47
hostname: d8a899232e8c
...
能夠看出是數據庫鏈接失敗了,在上面 **docker-compose.yml** 中咱們定義的環境變量 `MYSQL_HOST=localhost` 傳給後端代碼來鏈接數據庫,每一個容器都至關一一個獨立的個體,localhost 是 react_blog_service 本身的 ip (127.0.0.1),固然是訪問不到 react_blog_mysql,這個問題咱們在下一節再來解決,先來講說 Mysql。 上面能夠看到 Mysql 容器已經成功運行,咱們能夠進入容器內部鏈接 Mysql,還記得怎麼進入容器嗎
$ docker exec -it react_blog_mysql /bin/sh
$ ls
bin boot dev docker-entrypoint-initdb.d entrypoint.sh etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
$ mysql -uroot -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 12
Server version: 8.0.22 MySQL Community Server - GPL
Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
能夠看到順利鏈接 Mysql,輸入 `exit` 能夠退出容器。咱們也可使用可視化工具來鏈接,我這裏使用 Navicat 來鏈接 ![image-20201218224818914](https://cloud-images-1255423800.cos.ap-guangzhou.myqcloud.com/Docker-03.png) ![image-20201218225257487](https://cloud-images-1255423800.cos.ap-guangzhou.myqcloud.com/Docker-04.png) 注意這裏的端口 33061,上面咱們經過端口映射,經過宿主機端口 33061 能夠訪問到 Mysql 容器內端口 3306,因此就鏈接上啦。 > 在這個過程當中,個人服務器(宿主機)上的 Mysql 出現了問題,鏈接時報錯 `2013 lost connection to mysql server at 'reading initial communication packet'`,我也不知道是什麼緣由引發的,解決方式是運行命令 `systemctl start mysqld.service` 啓動 Mysql 服務,也不知是哪裏影響到了,不事後面我會直接鏈接宿主機 Mysql,不使用容器,這樣能夠和其餘項目統一管理數據,我任務比較方便,且數據也較安全。 ### 4. 容器互聯 上面留了一個問題,service 鏈接數據庫失敗,如今咱們來嘗試解決。參考 [Docker 築夢師系列(一):實現容器互聯](https://tuture.co/2020/01/12/cd44c84/) #### 4.1 Network 類型 Network,顧名思義就是 「網絡」,可以讓不一樣的容器之間相互通訊。首先有必要要列舉一下 Docker Network 的五種驅動模式(driver): - `bridge`:默認的驅動模式,即 「網橋」,一般用於**單機**(更準確地說,是單個 Docker 守護進程) - `overlay`:Overlay 網絡可以鏈接多個 Docker 守護進程,一般用於**集羣**,後續講 Docker Swarm 的文章會重點講解 - `host`:直接使用主機(也就是運行 Docker 的機器)網絡,僅適用於 Docker 17.06+ 的集羣服務 - `macvlan`:Macvlan 網絡經過爲每一個容器分配一個 MAC 地址,使其可以被顯示爲一臺物理設備,適用於但願直連到物理網絡的應用程序(例如嵌入式系統、物聯網等等) - `none`:禁用此容器的全部網絡 默認狀況下,建立的容器都在 bridge 網絡下,以下如所示,各個容器經過 dokcer0 可鏈接到宿主機HOST,而且各自分配到 IP,這種狀況下,容器間互相訪問須要輸入對方的 IP 地址去鏈接。 ![image-20201220153944034](https://cloud-images-1255423800.cos.ap-guangzhou.myqcloud.com/Docker-05.png) 查看 network 列表
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
a75e040b03ed bridge bridge local
13545e6a3970 docker_default bridge local
5ec462838a1c host host local
c726e6887f10 none null local
這裏有 4 的 network,默認原本只有 3 個,沒有 docker_default,我也是寫到這裏才發現建立了一個 docker_default 網絡,查找官網([Networking in Compose](https://docs.docker.com/compose/networking/#:~:text=By%20default%20Compose%20sets%20up,identical%20to%20the%20container%20name.))才發現,經過 docker-compose 來生成運行容器時,若是沒指定 network,會自動建立一個 network,包含當前 docker-compose.yml 下的全部容器,network 名字默認爲 **`目錄_default`** ,這裏目錄就是 `docker` 剛好咱們這個幾個 docker-compose.yml 都是放在 docker 目錄下,因此建立的幾個容器都是在 docker_default 網絡裏。能夠一下命令查看網絡詳細信息
$ docker network inspect docker_default
[
{ "Name": "docker_default", "Id": "13545e6a39708344b363b7fc16eefeb6775c37773222804ebd5b5fb6f28c38bb", "Created": "2020-12-16T11:03:37.2152073+08:00", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": null, "Config": [ { "Subnet": "172.24.0.0/16", "Gateway": "172.24.0.1" } ] }, "Internal": false, "Attachable": true, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": { "23891d43187e046eea25936dc0ab703964cc6c7213bb150ae9529da3e2e57662": { "Name": "react_blog_mysql", "EndpointID": "649857f928e0444500cfd296035869678bf26162d429a4499b262776b2a1d264", "MacAddress": "02:42:ac:18:00:03", "IPv4Address": "172.24.0.3/16", "IPv6Address": "" }, "3bff0060de19fc973039c07c1931e2c1efe30c6707bcd77d2ff7ea4dc01aaf63": { "Name": "react_blog_admin", "EndpointID": "25d8fa518b0ce27498f562372c3424aee174cb1d8fbf9f2445f1c6af8e6aab7f", "MacAddress": "02:42:ac:18:00:02", "IPv4Address": "172.24.0.2/16", "IPv6Address": "" }, "5878940d7626a9fb20622cde4002075e390e5036036bafb99d80454d6cba594b": { "Name": "react_blog_blog", "EndpointID": "a3f8ee36eda09f524be7ea16a67a1e13e62cf558e5480218bb523f877d478e4a", "MacAddress": "02:42:ac:18:00:04", "IPv4Address": "172.24.0.4/16", "IPv6Address": "" } }, "Options": {}, "Labels": { "com.docker.compose.network": "default", "com.docker.compose.project": "docker", "com.docker.compose.version": "1.25.1" } }
]
能夠看到 docker_default 網關地址爲 `172.24.0.1` ,其餘幾個容器 IP 分別爲 `172.24.0.3`,`172.24.0.2`,`172.24.0.4`,因此這裏的狀況是這樣的 ![image-20201220154038077](https://cloud-images-1255423800.cos.ap-guangzhou.myqcloud.com/Docker-06.png) 上面說了默認網絡 bridge 下容器見訪問只能輸入 **IP 地址**來鏈接,而自定義的網絡還能夠經過**容器名**來鏈接 > On user-defined networks like `alpine-net`, containers can not only communicate by IP address, but can also resolve a container name to an IP address. This capability is called **automatic service discovery**. > > [Networking with standalone containers](https://docs.docker.com/network/network-tutorial-standalone/) 這就能夠避免每次生成容器 IP 會變的問題了。知道了這些,咱們在 service 接口裏就可已經過 react_blog_mysql 來鏈接 react_blog_mysql 容器了,service/docker/docker-compose.yml 修改以下:
version: '3'
services:
service:
build: context: ../ dockerfile: ./docker/Dockerfile image: react_blog:service ports: - 9002:9002 restart: on-failure depends_on: - db environment: MYSQL_HOST: react_blog_mysql # 此處 localhost 換爲 mysql 容器名,在同一個自定義網絡下,變會自動解析爲 IP 鏈接 MYSQL_USER: root MYSQL_PASSWORD: 8023 volumes: - ./article:/usr/src/app/article container_name: react_blog_service
db:
image: mysql ports: - 33061:3306 restart: on-failure command: --default-authentication-plugin=mysql_native_password environment: MYSQL_ROOT_PASSWORD: 8023 MYSQL_PASSWORD: 8023 MYSQL_DATABASE: react_blog container_name: react_blog_mysql
在此運行命令
$ docker-compose up -d --build
![image-20201219011335466](https://cloud-images-1255423800.cos.ap-guangzhou.myqcloud.com/Docker-07.png) 能夠看到服務容器已正常運行,`docker logs react_blog_service` 查看日誌也沒有報錯,說明已經鏈接數數據庫,在代碼你我加了一個 get 測試接口,在瀏覽器輸入 `IP:9002/api/test/get` 或者 `localhost:9002/api/test/get`,會返回一個 json 對象
{"message":"Hello You Got It"}
**這裏我試了 N 久,一直有問題,** - Mysql 建立失敗,environment 我加了一個 MYSQL_USER: root,結果一直報錯 `ERROR 1396 (HY000): Operation CREATE USER failed for 'root'@'%' ` ,根據 https://github.com/docker-library/mysql/issues/129 可知,已經存在一個 root 用戶,沒法再建立,因此這個能夠不帶,就默認 root 用戶登陸,若是帶的話就不要是 root,會新建一個帳戶。這裏直接去掉 MYSQL_USER,使用 root 登陸 - service 建立失敗,日誌報錯沒鏈接上 Mysql,我試試了很久,最後發現重啓一下 service `docker start react_blog_service` 就能夠了,因此我以爲應該是 Mysql 建立好後,數據口等一些配置還沒搞好,因此 service 還鏈接不上,就一直報錯,等一會從新運行 service 就行了,因此這裏加上了 restart 參數,報錯就從新啓動,這樣就不用本身去重啓了,等一會,看日誌沒問題,就是鏈接成功了。 明明使用了 depends_on,爲何還會有這種問題呢,我也不太清楚,不過官網有這段示例:
version: "3.9"
services:
web: build: . depends_on: - db - redis redis: image: redis db: image: postgres
> `depends_on` does not wait for `db` and `redis` to be 「ready」 before starting `web` - only until they have been started. If you need to wait for a service to be ready, see [Controlling startup order](https://docs.docker.com/compose/startup-order/) for more on this problem and strategies for solving it. > > Depends_on 在啓動 Web 以前不會等待 db 和 Redis 處於「就緒」狀態-僅在它們啓動以前。 > > 應該就這個緣由了~ 咱們再來看看 docker_default 網絡
$ docker network inspect docker_default
[
{ "Name": "docker_default", "Id": "13545e6a39708344b363b7fc16eefeb6775c37773222804ebd5b5fb6f28c38bb", "Created": "2020-12-16T11:03:37.2152073+08:00", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": null, "Config": [ { "Subnet": "172.24.0.0/16", "Gateway": "172.24.0.1" } ] }, "Internal": false, "Attachable": true, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": { "3bff0060de19fc973039c07c1931e2c1efe30c6707bcd77d2ff7ea4dc01aaf63": { "Name": "react_blog_admin", "EndpointID": "25d8fa518b0ce27498f562372c3424aee174cb1d8fbf9f2445f1c6af8e6aab7f", "MacAddress": "02:42:ac:18:00:02", "IPv4Address": "172.24.0.2/16", "IPv6Address": "" }, "5878940d7626a9fb20622cde4002075e390e5036036bafb99d80454d6cba594b": { "Name": "react_blog_blog", "EndpointID": "a3f8ee36eda09f524be7ea16a67a1e13e62cf558e5480218bb523f877d478e4a", "MacAddress": "02:42:ac:18:00:04", "IPv4Address": "172.24.0.4/16", "IPv6Address": "" }, "83005eec8d50071a6c23a2be4af8552983c09c532e937f04d79f02f8eb68acc9": { "Name": "react_blog_mysql", "EndpointID": "265ed7793c98287a05ccf8997e81671287a02ee8ea464984996083a34abe10dd", "MacAddress": "02:42:ac:18:00:03", "IPv4Address": "172.24.0.3/16", "IPv6Address": "" }, "937339a37ce726e704ec21b31b4028a97967a00de01438557e5a60d8538a51c8": { "Name": "react_blog_service", "EndpointID": "934d26f32a2b23e2cb4691020cb93d26c97b9647108047b492c3f7dd2be6faef", "MacAddress": "02:42:ac:18:00:05", "IPv4Address": "172.24.0.5/16", "IPv6Address": "" } }, "Options": {}, "Labels": { "com.docker.compose.network": "default", "com.docker.compose.project": "docker", "com.docker.compose.version": "1.25.1" } }
]
能夠看到 react_blog_service 也已正常加入網絡,IP 爲 172.24.0.5 #### 4.2 自定義 Network docker_default 網絡是根據目錄來建立的,恰巧咱們這幾個項目 docker-compose.yml 文件都放在 docker 目錄下,因此都在一個網絡,若是名稱變了就不在一個網絡,而且以後項目可能還會有 docker 目錄,所有都在一個網絡也是不太好的,因此這裏咱們來自定義本次項目的網絡。 ***blog/docker/docker-compose.yml***
version: '3'
services:
blog:
build: context: ../ dockerfile: ./docker/Dockerfile image: react_blog:blog ports: - 9000:9000 networks: - react_blog container_name: react_blog_blog
networks:
react_blog:
***admin/docker/docker-compose.yml***
version: '3'
services:
admin:
build: context: ../ dockerfile: ./docker/Dockerfile image: react_blog:admin ports: - 9001:80 volumes: - ../build:/www - ./nginx.conf:/etc/nginx/conf.d/nginx.conf networks: - react_blog container_name: react_blog_admin
networks:
react_blog:
***service/docker/docker-compose.yml***
version: '3'
services:
service:
build: context: ../ dockerfile: ./docker/Dockerfile image: react_blog:service ports: - 9002:9002 depends_on: - db environment: - MYSQL_HOST=react_blog_mysql # 此處 localhost 換爲 mysql 容器名,在同一個自定義網絡下,變會自動解析爲 IP 鏈接 - MYSQL_USER=root - MYSQL_PASSWORD=8023 volumes: - ./article:/usr/src/app/article networks: - react_blog container_name: react_blog_service
db:
image: mysql ports: - 33061:3306 command: --default-authentication-plugin=mysql_native_password environment: - MYSQL_ROOT_PASSWORD=8023 - MYSQL_USER=root - MYSQL_PASSWORD=8023 - MYSQL_DATABASE=react_blog networks: - react_blog container_name: react_blog_mysql
networks:
react_blog:
- 與services 同級的 networks:建立一個新的 network,這裏生成的 network 最終名稱也會加上目錄名,docker_react_blog。 - 服務內部的 networks:加入哪些網絡,參數帶 「-」 說明是數組,能夠加入多個網絡,這裏咱們所有加入 react_blog,不分先後端了 **注意:** 這樣在 dockor-compose.yml 裏生成的 network 都會加上當前目錄名,若想不帶,能夠本身先生成一個
$ docker network create my_net
而後在 dockor-compose.yml 裏
version: '3'
services:
service:
build: context: ../ dockerfile: ./docker/Dockerfile image: react_blog:service ports: - 9002:9002 depends_on: - db environment: - MYSQL_HOST=react_blog_mysql # 此處 localhost 換爲 mysql 容器名,在同一個自定義網絡下,變會自動解析爲 IP 鏈接 - MYSQL_USER=root - MYSQL_PASSWORD=8023 volumes: - ./article:/usr/src/app/article networks: - my_net container_name: react_blog_service
db:
image: mysql ports: - 33061:3306 command: --default-authentication-plugin=mysql_native_password environment: - MYSQL_ROOT_PASSWORD=8023 - MYSQL_USER=root - MYSQL_PASSWORD=8023 - MYSQL_DATABASE=react_blog networks: - my_net container_name: react_blog_mysql
networks:
my_net:
external: true
加個 external 參數則使用已經建立的 network(my_net),不會再去建立或加上目錄名。 咱們再來從新建立容器,先刪除所有容器
$ docker stop $(docker ps -aq)
$ docker rm $(docker ps -aq)
在進入各個目錄分別執行 `docker-compose up -d`,在運行第一個時會看到 `Creating network "docker_react_blog" with the default driver` 這句話,說明建立了一個新的 network,咱們來看看
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
a75e040b03ed bridge bridge local
13545e6a3970 docker_default bridge local
e1ceb437a4fd docker_react_blog bridge local
5ec462838a1c host host local
c726e6887f10 none null local
$ docker network inspect docker_react_blog
[
{ "Name": "docker_react_blog", "Id": "e1ceb437a4fdc5de91e51ff8831e21b565c92754159ad7057de36758e548a92f", "Created": "2020-12-19T01:39:02.201644444+08:00", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": null, "Config": [ { "Subnet": "172.18.0.0/16", "Gateway": "172.18.0.1" } ] }, "Internal": false, "Attachable": true, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": { "00da404f6f050b9b2f20e39bbb136fef614e8dfee85ec31bd6000bfd59cc2dab": { "Name": "react_blog_mysql", "EndpointID": "1cb966cc731eca3e9721e6d3edcfcac6152b66051faa934557f567e9e36c75c6", "MacAddress": "02:42:ac:12:00:04", "IPv4Address": "172.18.0.4/16", "IPv6Address": "" }, "ad1480e48e8e7ed160b1d4bcf7eed77d74505aea7581d48d8931206772b5d805": { "Name": "react_blog_service", "EndpointID": "8866c3457382d6baa945da09aef40da54c7dfdea0f393485001c35bb37d201a0", "MacAddress": "02:42:ac:12:00:05", "IPv4Address": "172.18.0.5/16", "IPv6Address": "" }, "b518d40b5021d3fdec7b7e62fbaa47b8a705a38346ccba2b9814174e46b67cd0": { "Name": "react_blog_admin", "EndpointID": "9a58ff20dc57d4d1fa6af83482051a68e80e22a5e37cf8e0cb3570b78102f107", "MacAddress": "02:42:ac:12:00:03", "IPv4Address": "172.18.0.3/16", "IPv6Address": "" }, "db0050257a8e8a0fa430ea04b009ae819dbf04ef001cf1027ec2b5565403b48e": { "Name": "react_blog_blog", "EndpointID": "664794ed292871bc7fd8e1c4eaa56f682a6be5d653209f84158f3334a4f30660", "MacAddress": "02:42:ac:12:00:02", "IPv4Address": "172.18.0.2/16", "IPv6Address": "" } }, "Options": {}, "Labels": { "com.docker.compose.network": "react_blog", "com.docker.compose.project": "docker", "com.docker.compose.version": "1.25.1" } }
]
#### 4.3 調用接口 如今還有一個問題,咱們在代碼中調用接口形式是 `http://localhost:9002/api/xxx` ,在 react_blog_blog 容器中調用接口 localhost 是自己本身,沒有調到 react_blog_service 裏面的接口。 **針對 admin** 在代碼中,咱們這樣來調接口
const HOST = process.env.NODE_ENV === 'development' ? 'http://localhost:9002' : ''
const API = {
getArticleList: HOST + '/api/getArticleList',
getArticle: HOST + '/api/getArticle',
addArticle: HOST + '/api/addArticle',
delArticle: HOST + '/api/delArticle',
getTagList: HOST + '/api/getTagList',
addTag: HOST + '/api/addTag',
delTag: HOST + '/api/delTag',
register: HOST + '/api/register',
login: HOST + '/api/login',
}
export default API
![image-20201219160058561](https://cloud-images-1255423800.cos.ap-guangzhou.myqcloud.com/Docker-08.png) 會發現接口 404,咱們經過 nginx 來代理接口請求 ***admin/docker/nginx.conf***
server {
listen 80;
sendfile on;
sendfile_max_chunk 1M;
tcp_nopush on;
gzip_static on;
location /api {
proxy_pass http://react_blog_service:9002;
}
location / {
root /www; index index.html;
}
}
以 /api 爲開頭的請求,咱們都轉發到 react_blog_service 容器 9002 端口,將 nginx.conf 拖到服務器,由於咱們是將此文件掛載到容器內部的,因此這裏只須要重啓一下容器
$ docker restart react_blog_admin
再看看請求接口,能夠看到請求 200 成功,返回數據,若是返回 500,說明數據庫還沒建表,將目錄下 react_blog.sql 導入數據庫就能夠了。 ![image-20201219160458357](https://cloud-images-1255423800.cos.ap-guangzhou.myqcloud.com/Docker-09.png) **針對 blog** 開始我覺得經過環境變量(Next 中要存儲在運行時變量裏 [Runtime Configuration](https://nextjs.org/docs/api-reference/next.config.js/runtime-configuration))來傳遞請求 HOST (react_blog_service || localhost) ,但發現 react_blog_service 直接拼在前端接口裏訪問是不可行的(getServerSideProps 可行),因此最後仍是改成 nginx 來代理請求,而且後面咱們確定仍是要經過域名來訪問網站的,因此仍是須要 nginx,那麼咱們就爲前臺頁面來加一個 nginx 容器。 **一、建立環境變量** ***blog/docker/Dockerfile***
FROM node:12-alpine
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY package.json /usr/src/app
RUN npm i --production --registry=https://registry.npm.taobao.org
COPY . /usr/src/app
ENV HOST react_blog_service ## 增長一個環境變量,在 build 階段可獲取到,必定放在 npm run build 前一行
RUN npm run build
EXPOSE 9000
CMD [ "npm", "start" ]
代碼中,設置運行是變量 ***blog/next.config.js***
const withCSS = require('@zeit/next-css')
const withLess = require('@zeit/next-less')
module.exports = () =>
withLess({
...withCSS(), // 改成 nginx 代理 publicRuntimeConfig: { HOST: process.env.HOST || 'localhost', // 若是是 docker build,此處 process.env.HOST,不然就 localhsot,不影響本地運行 },
})
在 ***blog/config/api***
import getConfig from 'next/config'
const { publicRuntimeConfig } = getConfig()
const SSRHOST = http://${publicRuntimeConfig.HOST}:9002
const HOST = http://localhost:9002
export const SSRAPI = {
getArticleList: SSRHOST + '/api/getArticleList',
getArticle: SSRHOST + '/api/getArticle',
}
export const API = {
getArticleList: HOST + '/api/getArticleList',
getArticle: HOST + '/api/getArticle',
}
這裏有點麻煩,我不知道個人理解對不對,但試了多種狀況只有這種本地和 docker 部署才均可以。 - 若是是本地運行(不使用 docker),服務端獲取數據(**getServerSideProps**)和頁面中獲取數據直接使用服務接口地址(localhost:9002)便可 - 若是是 docker 運行,服務端獲取數據(**getServerSideProps**)須要直接帶上服務接口容器地址,沒法經過 nginx 代理,頁面中獲取數據調用接口則職能經過 nginx 代理的方式 **二、nginx 代理** 修改 ***blog/docker/docker-compose.yml***,增長一個 nginx 容器
version: '3'
services:
blog:
build: context: ../ dockerfile: ./docker/Dockerfile image: react_blog:blog # ports: # - 9000:9000 networks: - react_blog container_name: react_blog_blog
nginx:
build: context: ../ dockerfile: ./docker/Dockerfile-nginx image: react_blog:nginx ports: - 9000:80 volumes: - ./nginx.conf:/etc/nginx/conf.d/nginx.conf networks: - react_blog container_name: react_blog_nginx
networks:
react_blog:
***blog/docker/Dockerfile-nginx***
FROM nginx
RUN rm /etc/nginx/conf.d/default.conf
EXPOSE 80
***blog/docker/nginx.conf***
server {
listen 80;
sendfile on;
sendfile_max_chunk 1M;
tcp_nopush on;
gzip_static on;
location /api {
proxy_pass http://react_blog_service:9002;
}
location / {
proxy_pass http://react_blog_blog:9000;
}
}
**三、生成容器** 由於 blog 的內容有變,因此須要從新生成鏡像,使用 `docker-compose up -d --build` 會從新下載 npm node_modules,比較慢,因此仍是先生成鏡像。 在 blog 目錄下執行
$ docker build -f docker/Dockerfile . -t react_blog:blog
在 blog/docker 下執行
$ docker-compose up -d
運行成功的話,再試試接口就能夠獲取數據啦。 ### 5. 鏈接宿主機 Mysql 上面遇到一個問題,在上面過程當中,個人服務器(宿主機)上的 Mysql 出現了問題,鏈接時報錯 `2013 lost connection to mysql server at 'reading initial communication packet'`,我也不知道是什麼緣由引發的,解決方式是運行命令 `systemctl start mysqld.service` 啓動 Mysql 服務,也不知是哪理影響到了。由於以前其餘項目都是單獨部署的,沒使用 docker,數據都在宿主機 Mysql 上,因此我仍是跟傾向於統一管理,自適應宿主機一個 Mysql,下面來看看怎麼實現吧。 這裏有兩種方式 **方式一:network_mode: host** 修改 ***service/docker/docker-compose.yml***
version: '3'
services:
service:
build: context: ../ dockerfile: ./docker/Dockerfile image: react_blog:service ports: - 9002:9002 restart: on-failure # depends_on: # - db environment: # MYSQL_HOST: react_blog_mysql # 此處 localhost 換爲 mysql 容器名,在同一個自定義網絡下,變會自動解析爲 IP 鏈接 MYSQL_USER: root MYSQL_PASSWORD: 8023 volumes: - ../article:/usr/src/app/article network_mode: host # networks: # - react_blog container_name: react_blog_service
# db:
# image: mysql
# ports:
# - 33061:3306
# restart: on-failure
# command: --default-authentication-plugin=mysql_native_password
# environment:
# MYSQL_ROOT_PASSWORD: 8023
# MYSQL_PASSWORD: 8023
# MYSQL_DATABASE: react_blog
# networks:
# - react_blog
# container_name: react_blog_mysql
networks:
react_blog:
service/docker 下執行命令
$ docker-compose up -d
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
af9e525e7d14 react_blog:service "docker-entrypoint.s…" 28 seconds ago Up 26 seconds react_blog_service
能夠看到 service 運行正常,且沒有端口映射,`docker inspect react_blog_service` 也沒有分配 IP,這種就至關於一個 Node 應用本身鏈接到宿主機 Mysql。可是對於頁面接口請求來講,由於 react_blog_service 已不在 docker_react_blog ,因此就要使用宿主機 IP 地址來訪問了。 ***nginx.conf***
server {
listen 80;
sendfile on;
sendfile_max_chunk 1M;
tcp_nopush on;
gzip_static on;
location /api {
# proxy_pass http://react_blog_service:9002; proxy_pass http://xxx.xx.xxx.x:9002; # xxx.xx.xxx.x 爲宿主機(服務器)IP
}
location / {
proxy_pass http://react_blog_blog:9000;
}
}
服務端渲染接口也是同樣
FROM node:12-alpine
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY package.json /usr/src/app
RUN npm i --production --registry=https://registry.npm.taobao.org
COPY . /usr/src/app
ENV HOST xxx.xx.xxx.x
RUN npm run build
EXPOSE 9000
CMD [ "npm", "start" ]
這種方式是否是很麻煩,還要暴露服務器 IP 地址,因此我選擇方式二 **方式二:** 修改 ***service/docker/docker-compose.yml***
version: '3'
services:
service:
build: context: ../ dockerfile: ./docker/Dockerfile image: react_blog:service ports: - 9002:9002 restart: on-failure # depends_on: # - db environment: MYSQL_HOST: 172.17.0.1 MYSQL_USER: root MYSQL_PASSWORD: 8023 volumes: - ../article:/usr/src/app/article networks: - react_blog container_name: react_blog_service
# db:
# image: mysql
# ports:
# - 33061:3306
# restart: on-failure
# command: --default-authentication-plugin=mysql_native_password
# environment:
# MYSQL_ROOT_PASSWORD: 8023
# MYSQL_PASSWORD: 8023
# MYSQL_DATABASE: react_blog
# networks:
# - react_blog
# container_name: react_blog_mysql
networks:
react_blog:
這裏 MYSQL_HOST 爲 172.17.0.1,上面也說了,容器能夠經過此 IP 來鏈接到宿主機,因此這就鏈接上宿主機的 Mysql 了,其餘的地方就不須要改了。 ### 6. 一個 docker-compoer.yml 前面用了 3 個 docker-compose.yml 來啓動各自的項目,仍是挺繁瑣的,咱們來寫一個彙總的,一個命令運行因此,固然後面某一個項目須要從新跑,也能夠進入各自目錄去運行本身的 docker-compose.yml 在項目根目錄建立 ***docker/docker-compose.yml***,建立 docker 目錄,是爲了建立的 network 和單個項目運行是建立的一致
version: '3'
services:
blog:
build: context: ../blog dockerfile: ./docker/Dockerfile image: react_blog:blog networks: - react_blog container_name: react_blog_blog
nginx:
build: context: ../blog dockerfile: ./docker/Dockerfile-nginx image: react_blog:nginx ports: - 9000:80 volumes: - ../blog/docker/nginx.conf:/etc/nginx/conf.d/nginx.conf networks: - react_blog container_name: react_blog_nginx
admin:
build: context: ../admin dockerfile: ./docker/Dockerfile image: react_blog:admin ports: - 9001:80 volumes: - ../admin/build:/www - ../admin/docker/nginx.conf:/etc/nginx/conf.d/nginx.conf networks: - react_blog container_name: react_blog_admin
service:
build: context: ../service dockerfile: ./docker/Dockerfile image: react_blog:service ports: - 9002:9002 restart: on-failure environment: MYSQL_HOST: 172.17.0.1 MYSQL_USER: root MYSQL_PASSWORD: 8023 volumes: - ../service/article:/usr/src/app/article networks: - react_blog container_name: react_blog_service
networks:
react_blog:
中止並刪除以前建立的全部容器
$ docker stop $(docker ps -aq)
$ docker rm $(docker ps -aq)
進入 /docker 目錄執行,
$ docker-compose up -d
Building nginx
Step 1/3 : FROM nginx
---> ae2feff98a0c
Step 2/3 : RUN rm /etc/nginx/conf.d/default.conf
---> Running in bb163c42c6b5
Removing intermediate container bb163c42c6b5
---> 282cb303dddf
Step 3/3 : EXPOSE 80
---> Running in 9b77ebd39952
Removing intermediate container 9b77ebd39952
---> fbb18dda70af
Successfully built fbb18dda70af
Successfully tagged react_blog:nginx
WARNING: Image for service nginx was built because it did not already exist. To rebuild this image you must use docker-compose build
or docker-compose up --build
.
Building admin
Step 1/3 : FROM nginx
---> ae2feff98a0c
Step 2/3 : RUN rm /etc/nginx/conf.d/default.conf
---> Using cache
---> 282cb303dddf
Step 3/3 : EXPOSE 80
---> Using cache
---> fbb18dda70af
Successfully built fbb18dda70af
Successfully tagged react_blog:admin
WARNING: Image for service admin was built because it did not already exist. To rebuild this image you must use docker-compose build
or docker-compose up --build
.
Creating react_blog_admin ... done
Creating react_blog_service ... done
Creating react_blog_blog ... done
Creating react_blog_nginx ... done
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1fbb15abdd30 react_blog:service "docker" 13 seconds ago Up 6 seconds 0.0.0.0:9002->9002/tcp react_blog_service
fbee53e25c3a react_blog:admin "/docker" 13 seconds ago Up 6 seconds 0.0.0.0:9001->80/tcp react_blog_admin
70cb25f87d14 react_blog:blog "docker" 13 seconds ago Up 6 seconds 9000/tcp react_blog_blog
aa9fbf2afea4 react_blog:nginx "/docker" 13 seconds ago Up 6 seconds 0.0.0.0:9000->80/tcp react_blog_nginx
運行成功~ ### 7. 域名 我如今是經過宿主機的 nginx 來代理域名訪問 IP:9000,而後訪問到 react_blog_nginx 容器,本想是直接在 react_blog_nginx 中作代理,可是試了沒成功。想了想,訪問 react_blog_nginx 是經過端口映射,宿主IP:9000 訪問到的,若是在 react_blog_nginx 內部配置域名,總感受是沒法訪問,這點還沒想過,這幾天再試試。 ## 結語 終於寫完了,寫以前已經學習嘗試了很久,覺得頗有把握了,結果在寫的過程當中又遇到一堆問題,一個問題可能都會卡很久天,各類百度,Google,油管都用上啦,總算解決了遇到的全部問題,固然這些問題可能只知足了我如今的部署需求,其中還有不少知識點,沒有接觸到,不過不要緊,我就是想成功部署前端項目就能夠了。