Vue 服務端渲染(SSR)、Nuxt.js - 從入門到實踐
10月初有幸接到公司官網改版需求,要求採用服務端渲染模式對原網站進行seo優化。
由於團隊一直使用的vue技術棧,所以我第一時間想到的就是採用vue 服務端渲染(SSR)來實現該需求,即能減少團隊其他成員後期維護的成本,又能把現有其他專案封裝好的內容稍微改改就能直接copy過來使用,大大節省了開發時間(除去ui中途調整,整體改造時間之花了2-3天)。
改造前後對比
1、原公司網站(改版前),採用 vue(SPA) 模式開發
網址:www2.nicomama.com/
2、新公司網站(改版後),採用 Vue 服務端渲染(SSR) 模式開發
網址:www.nicomama.com/
當然直接從瀏覽器開啟只能看到兩個網站只是在風格上和介面上做了升級。
接下來讓我們看看兩個網站區別在哪裡,使用chrome瀏覽器分別開啟兩個網站,右擊檢視原始碼。
截圖如下:
1、改版前

2、改版後

可以明顯看出改版後網站原始碼增加了不止幾倍之多,簡而言之服務端渲染的模式就是:在請求一個網址的時候,服務端收到請求之後把html的內容先生成好然後再返回給瀏覽器。這樣子搜尋引擎就可以通過你返回的a標籤抓取到網站的其他頁面了,依此類推搜尋引擎就可以收錄網站的所有(暴露出來的)路徑了,後面還會給大家看一下網站改版後的一些搜錄資料變化。
在看下面內容之前建議大家先去看下《Vue SSR指南》,這是文件地址 ofollow,noindex">ssr.vuejs.org/zh/#%E4%BB%…
正文
接下來這一塊Vue SSR的概念介紹和好處壞處對比的內容是對文件提及的概念摘要,看過文件的可以直接忽略~
Vue SSR
簡而言之就是將本來要放在瀏覽器執行建立的元件,放到服務端先建立好,然後生成對應的html將它們直接傳送到瀏覽器,最後將這些靜態標記"啟用"為客戶端上完全可互動的應用程式。
Vue SSR 相比 SPA(單頁應用)好處及壞處
1、好處
-
更好的 SEO,由於搜尋引擎爬蟲抓取工具可以直接檢視完全渲染的頁面。
-
更快的內容到達時間(time-to-content),特別是對於緩慢的網路情況或執行緩慢的裝置。
2、壞處
1)開發條件所限。瀏覽器特定的程式碼,只能在某些生命週期鉤子函式(lifecycle hook)中使用;一些外部擴充套件庫(external library)可能需要特殊處理,才能在伺服器渲染應用程式中執行。
2)涉及構建設定和部署的更多要求。與可以部署在任何靜態檔案伺服器上的完全靜態單頁面應用程式(SPA)不同,伺服器渲染應用程式,需要處於 Node.js server 執行環境。
3)更多的伺服器端負載。在 Node.js 中渲染完整的應用程式,顯然會比僅僅提供靜態檔案的 server 更加大量佔用 CPU 資源(CPU-intensive - CPU 密集),因此如果你預料在高流量環境(high traffic)下使用,請準備相應的伺服器負載,並明智地採用快取策略。
瞭解完概念之後,讓我們動手實現第一個Vue SSR例項把~
注意:一下內容需要有一定vue基礎
相比vue SPA(單頁應用),Vue增加了一些擴充套件工具,首先我們來看一下比較重要的一個工具vue-server-renderer,從名字可以看出它是在服務端渲染的時候用的。
讓我們來看一下它的功能和用法
1、建立一個空專案 mkdir vuessr && cd vuessr
2、執行 npm init
進行初始化
3、安裝我們需要的依賴 cnpm install vue vue-server-renderer --save
4、建立 index.js
程式碼如下:
// 第 1 步:建立一個 Vue 例項 const Vue = require('vue') const app = new Vue({ template: `<div>Hello World</div>` }) // 第 2 步:建立一個 renderer const renderer = require('vue-server-renderer').createRenderer() // 第 3 步:將 Vue 例項渲染為 HTML renderer.renderToString(app, (err, html) => { if (err) throw err console.log(html) // => <div data-server-rendered="true">Hello World</div> }) 複製程式碼
5、執行 node index.js
可以看到在控制檯輸出了
<div data-server-rendered="true">Hello World</div> 複製程式碼
我們再將生成好的html放到指定的html模版裡面再返回到瀏覽器不就實現服務端渲染功能了?
6、安裝依賴 cnpm install express --save
7、建立 app.js
程式碼如下:
const Vue = require('vue') const server = require('express')() const renderer = require('vue-server-renderer').createRenderer() server.get('*', (req, res) => { const app = new Vue({ data: { url: req.url }, template: `<div>訪問的 URL 是: {{ url }}</div>` }) renderer.renderToString(app, (err, html) => { if (err) { res.status(500).end('Internal Server Error') return } res.end(` <!DOCTYPE html> <html lang="en"> <head><title>Hello</title></head> <body>${html}</body> </html> `) }) }) server.listen(8080) 複製程式碼
8、執行 node app.js
9、開啟瀏覽器輸入 http://localhost:8080/
發現我們的內容已經顯示出來了,如果有同學發現有中文亂碼的問題,可以設定一下編碼:
renderer.renderToString(app, (err, html) => { if (err) { res.status(500).end('Internal Server Error') return } //設定編碼 res.writeHead(200, {'Content-Type':'text/html;charset=utf-8'}); res.end(` <!DOCTYPE html> <html lang="zh"> <head><title>Hello</title></head> <body>${html}</body> </html> `) }) 複製程式碼
然後重新執行一下試試。
10、當然直接採用字串來拼接html內容是非常不優雅的,而且容易出錯,我們可以改寫成模版形式,建立檔案 index.template.html
程式碼如下:
<!DOCTYPE html> <html lang="en"> <head><title>Hello</title></head> <body> <!--vue-ssr-outlet--> </body> </html> 複製程式碼
注意: <!--vue-ssr-outlet-->
註釋--這裡將是應用程式 HTML 標記注入的地方。
11、修改 app.js
const renderer = require('vue-server-renderer').createRenderer({ template: require('fs').readFileSync('./index.template.html', 'utf-8') }) //*** renderer.renderToString(app, (err, html) => { if (err) { res.status(500).end('Internal Server Error') return } res.writeHead(200, {'Content-Type':'text/html;charset=utf-8'}); res.end(html) }) 複製程式碼
12、重新執行一下試試,看看是否能正常執行。
13、它還支援模板插值操作,修改檔案 index.template.html
,程式碼如下:
<html> <head> <!-- 使用雙花括號(double-mustache)進行 HTML 轉義插值(HTML-escaped interpolation) --> <title>{{ title }}</title> <!-- 使用三花括號(triple-mustache)進行 HTML 不轉義插值(non-HTML-escaped interpolation) --> {{{ meta }}} </head> <body> <!--vue-ssr-outlet--> </body> </html> 複製程式碼
修改檔案 index.js
,需要調整的程式碼如下:
const Vue = require('vue') const server = require('express')() const renderer = require('vue-server-renderer').createRenderer({ template: require('fs').readFileSync('./index.template.html', 'utf-8') }) const context = { title: 'hello vuessr', meta: ` <meta charset="utf-8"> ` } server.get('*', (req, res) => { const app = new Vue({ data: { url: req.url }, template: `<div>訪問的 URL 是: {{ url }}</div>` }) renderer.renderToString(app, context, (err, html) => { if (err) { res.status(500).end('Internal Server Error') return } res.writeHead(200, {'Content-Type':'text/html;charset=utf-8'}); res.end(html) }) }) server.listen(8080) 複製程式碼
14、再次重新執行一下試試,看看是否能正常執行。以下是檢視瀏覽器原始碼的截圖,可以看到模板定義的內容已經替換成對應的資料了。

程式碼地址:
具體詳細的api介紹大家可以去看文件,以上只是簡單的使用。
到此為止我們已經實現一個最基礎的vue 服務端渲染的工程了。
是不是很簡單?不過要從頭搭建整套Vue SSR還是一個非常繁瑣的過程。
後續我也會給大家介紹VueSSR開箱即用的框架Nuxt.js,其實它就是對Vue SSR的一個封裝,概念還是一樣的。當然再沒了解Vue SSR的基本實現過程,直接去使用Nuxt.js還是會一頭霧水(大神忽略~)。
在搭建專案之前我們先來看看指南中提供的這張執行原理圖吧:

從圖中我們可以大致看出vue ssr的運作過程:我們首先通過 webpack 打包 - 伺服器需要「伺服器 bundle」然後用於伺服器端渲染(SSR),而「客戶端 bundle」會發送給瀏覽器,用於混合靜態標記。有了理論基礎之後讓我們一起實踐一下吧。
使用Vue-cli為基礎搭建VueSSR
選擇vue-cli的webpack模版生成的程式碼基本上可以複用到VueSSR可以省去繁瑣的webpack配置的過程。
1、安裝vue-cli,參考文件 cli.vuejs.org/zh/guide/cl… ,這裡就不做過多介紹,我採用的版本是2.9.6。
2、執行 vue init webpack vuessr-vuecli
選擇配置如下
? Project name vuessr-vuecli ? Project description A Vue.js project ? Author taoxinhua <[email protected]> ? Vue build standalone ? Install vue-router? Yes ? Use ESLint to lint your code? No ? Set up unit tests No ? Setup e2e tests with Nightwatch? No ? Should we run `npm install` for you after the project has been created? (recommended) npm 複製程式碼
3、進入資料夾,執行 cnpm run dev
;看下專案是否能正常執行
4、接下來讓我們一起對程式碼進行改造吧~
新增 src/components --about.vue --home.vue 複製程式碼
about.vue
<template> <div class="hello"> <h1>這是關於我頁面</h1> </div> </template> 複製程式碼
home.vue
<template> <div class="hello"> <h1>這是首頁</h1> </div> </template> 複製程式碼
修改 src/router --index.js 複製程式碼
index.js
import Vue from 'vue' import Router from 'vue-router' import home from '@/components/home' import about from '@/components/about' Vue.use(Router) export default () => { return new Router({ mode:'history', routes: [ { path: '/', name: 'home', component: home }, { path: '/about', name: 'about', component: about } ] }) } 複製程式碼
新增 src --app.js --App.vue --entry-client.js 客戶端打包入口檔案 --entry-server.js 服務端打包入口檔案 複製程式碼
app.js
import Vue from 'vue' import createRouter from './router' import App from './App.vue' // 例項 每次請求都會建立新的例項 export default (context) => { const router = createRouter() const app = new Vue({ router, components: { App }, template: '<App/>' }) return { router, app } } 複製程式碼
App.vue
<template> <div id="app"> <router-link to="/">首頁</router-link> <router-link to="/about">關於我</router-link> <router-view/> </div> </template> <script> export default { name: 'App' } </script> <style> #app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style> 複製程式碼
entry-client.js
import createApp from './app' let { app, router } = createApp() router.onReady(() => { app.$mount('#app') }) 複製程式碼
entry-server.js
// 服務端這邊,需要把訪問的路徑給到vue-router import createApp from './app' // 外面的express服務使用{url: / /about} export default (context) => { return new Promise((resolve, reject) => { let { app, router } = createApp(context); router.push(context.url); router.onReady(() => { // 訪問路徑,可定匹配到元件 let matchedCompoents = router.getMatchedComponents(); if (!matchedCompoents.length) { return reject({ code: 404 }) } resolve(app) }, reject) }) } 複製程式碼
新增 build/dev-server.js server.js 複製程式碼
dev-server.js
const serverConf = require('./webpack.server.conf'); const webpack = require('webpack') const fs=require('fs') const path = require('path'); const Mfs = require('memory-fs') const axios = require('axios') module.exports = (cb) => { const webpackComplier = webpack(serverConf); var mfs = new Mfs(); webpackComplier.outputFileSystem = mfs; webpackComplier.watch({}, async (error, stats) => { if (error) return console.log(error); stats = stats.toJson(); stats.errors.forEach(err => console.log(err)) stats.warnings.forEach(err => console.log(err)) // server Bundle json檔案 let serverBundlePath = path.join( serverConf.output.path, 'vue-ssr-server-bundle.json' ) let serverBundle = JSON.parse(mfs.readFileSync(serverBundlePath, "utf-8")) //console.log(serverBundle) // client Bundle json檔案 let clientBundle = await axios.get('http://localhost:8080/vue-ssr-client-manifest.json') // 模板 let template = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf-8'); cb(serverBundle, clientBundle, template) }) } 複製程式碼
server.js
const devServer = require('./build/dev-server'); const express = require('express'); const app = express(); const vueRenderer = require('vue-server-renderer') const path = require('path'); app.get('*', async (req, res) => { res.status(200); res.setHeader('Content-Type', 'text/html;charset=utf-8;') devServer(function(serverBundle,clientBundle,template){ let renderer = vueRenderer.createBundleRenderer(serverBundle,{ template, clientManifest: clientBundle.data, runInNewContext: false }) renderer.renderToString({ url: req.url }).then((html) => { res.end(html) }).catch(err => console.log(err)) }) }) app.listen(5000, () => { console.log('啟動成功') }) 複製程式碼
修改 build/webpack.dev.conf.js 複製程式碼
webpack.dev.conf.js
//...忽略 const portfinder = require('portfinder') //新增內容-start const vueSSRClientPlugin = require('vue-server-renderer/client-plugin') //新增內容-end const HOST = process.env.HOST //...忽略 new CopyWebpackPlugin([ { from: path.resolve(__dirname, '../static'), to: config.dev.assetsSubDirectory, ignore: ['.*'] } ]), //新增內容-start new vueSSRClientPlugin() //新增內容-end //...忽略 複製程式碼
新增檔案
build/webpack.server.conf.js 複製程式碼
webpack.server.conf.js
const webpack = require('webpack'); const merge = require('webpack-merge') const base = require('./webpack.base.conf'); const vueSSRServerPlugin = require('vue-server-renderer/server-plugin') const webpackNodeExternals = require('webpack-node-externals') module.exports = merge(base,{ target: 'node', devtool: 'source-map', entry: './src/entry-server.js', output: { filename: 'server-bundle.js', libraryTarget: 'commonjs2' }, externals: [webpackNodeExternals({ whitelist: /\.css$/ })], plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"devlopment"', 'process.env.VUE_ENV': '"server"' }), new vueSSRServerPlugin() ] }) 複製程式碼
修改 index.html 複製程式碼
index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>vuessr-vuecli</title> </head> <body> <div id="app"> <!--vue-ssr-outlet--> </div> <!-- built files will be auto injected --> </body> </html> 複製程式碼
修改 package.json 增加server指令碼 複製程式碼
package.json
"scripts": { "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", "start": "npm run dev", "build": "node build/build.js", "server": "node server.js" }, 複製程式碼
到此為止我們的檔案都已經調整完畢了,讓我們先來執行一下看看吧。
cnpm run dev cnpm run server 複製程式碼

可以看到我們的程式已經正常運行了,再來看看網頁原始碼是否是通過服務端渲染完畢之後再返回的。

沒錯到這一步我們算是大功高成了,接下來我們回過頭來看看整個專案的執行原理吧~ 畢竟這才是重點。
首先回想一下前面的執行原理圖,第一步是不是先通過webpack分別打包出給服務端用的bundle和客戶端用的bundle。
我們先來找一下客戶端用的bundle我們生成在哪裡吧
我們執行 cnpm run dev
實際上就是用來生成客戶端用的bundle。這一步比較簡單,回想一下我們是不是調整了 webpack.dev.conf.js
增加了一個外掛 vue-server-renderer/client-plugin
就是用來生成客戶端用的bundle, 只不過我們並沒有直接把這個bundle生成具體的檔案,而是放在了快取中,我們可以直接通過瀏覽器訪問 http://localhost:8080/vue-ssr-client-manifest.json
檢視到這份json檔案。
服務端用的bundle
其實它也生成在快取中, build/dev-server.js
首先我們通過 webpackComplier.outputFileSystem = mfs;
修改了webpack的輸出形式(改成輸出到快取中),然後在從快取中拿到該檔案
let serverBundlePath = path.join( serverConf.output.path, 'vue-ssr-server-bundle.json' ) let serverBundle = JSON.parse(mfs.readFileSync(serverBundlePath, "utf-8")) 複製程式碼
我們再來看一下 server.js
devServer(function(serverBundle,clientBundle,template){ let renderer = vueRenderer.createBundleRenderer(serverBundle,{ template, clientManifest: clientBundle.data, runInNewContext: false }) renderer.renderToString({ url: req.url }).then((html) => { res.end(html) }).catch(err => console.log(err)) }) 複製程式碼
可以看到我們將拿到的serverBundle(服務端Bundle),clientBundle(客戶端Bundle),template(index.html模版) 最終交由 vue-server-renderer
來進行最終處理。
程式碼地址
大功高成~ 原理大概就是這樣,程式碼的細節還需要大家自行去檢視和消化
講完原理之後,讓我們來看看Nuxt.js又是咋回事吧
在進入下一個環節之前,還是建議大家先去看官方文件zh.nuxtjs.org/guide/
Nuxt.js是什麼?
簡而言之Nuxt.js就是Vue SSR的一個開箱即用的框架。安裝好就可以直接寫業務程式碼,而不需要做過多的配置。
既然是一個框架,那我們的就要按照它的規則來配置和寫程式碼,前面提供的官方文件提供了非常詳細的介紹。這裡就不做過多介紹,我們直接進行實戰吧。 首先我們先建立一個新專案。
在改造官網期間剛好Nuxt.js 2.0釋出了,看了一下改動還是比較多了,為了確保專案正常上線,決定還是先採用穩定的1.0+版本進行編碼(畢竟新版本方釋出多少會有一點問題)。以下也是針對1.0+版本開發過程中遇到的一些問題,大家可以借鑑以下。
以下內容主要針對實戰過程中遇到的一些問題來進行分享:
第一步讓我們先建立一個新專案吧,安裝文件地址: zh.nuxtjs.org/guide/insta… 我們採用 create-nuxt-app
命令來安裝,執行
npx create-nuxt-app vuessr-nuxt 複製程式碼
或者
cnpm install -g create-nuxt-app create-nuxt-app vuessr-nuxt 複製程式碼
選擇配置如下
? Project name vuessr-nuxt ? Project description My unreal Nuxt.js project ? Use a custom server framework express ? Use a custom UI framework none ? Choose rendering mode Universal ? Use axios module yes ? Use eslint no ? Use prettier no ? Author name taoxinhua ? Choose a package manager npm 複製程式碼
然後進入進入專案執行, cnpm run dev
看看專案是否能正常執行,如果不能執行,第一步先檢查以下node版本,我的node版本是v8.12.0。第二步如果node升級之後還是不行,執行 cnpm install
看下是否有依賴包少安裝了。如果這兩步還不能解決問題,大家可以在評論區提問。
開啟瀏覽器進入 http://127.0.0.1:3000
,可以看到我們的專案已經可以運行了。沒錯就是這麼簡單

1、使用axios遇到的坑。
注意
在使用1.0+版本開發過程中發現每次修改檔案,服務端程式碼都會重新載入並執行一遍,如果直接把axios的鉤子函式放到plugin中去執行,會發現每次修改完畢之後鉤子函式都會重複新增一次,導致引數重複被處理,比如我傳送請求之前要把傳遞的data轉成字串的形式。會發現下面程式碼的config.data會出現重複累加的情況。
axios.interceptors.request.use(function (config) { let data = config.data || {}; let auth = buildHttpHeaders(); config.url = getApiUrl(config.url); config.data = qs.stringify({ data: JSON.stringify(data), auth: JSON.stringify(auth) }) //在請求發出之前進行一些操作 return config; }, function (err) { //Do something with request error return Promise.reject(err); }); 複製程式碼
解決方法
建立檔案
assets/js/config/config-axios.js
import Axios from 'axios' import qs from 'qs' import { getUUID } from '~/assets/js/tools/index' import { getApiUrl } from '~/assets/js/config/config-urls.js' function buildHttpHeaders() { return { "x-user-id": '', "x-access-token": '', "x-platform": 'pc', "x-client-token": getUUID(), "x-system-version": '10.1.1', "x-client-version": '2.0.1', "x-method-version": '1.0', "x-network-type": '3g', } } let axios = Axios.create(); // 新增一個請求攔截器 axios.interceptors.request.use(function (config) { let data = config.data || {}; let auth = buildHttpHeaders(); config.url = getApiUrl(config.url); config.data = qs.stringify({ data: JSON.stringify(data), auth: JSON.stringify(auth) }) //在請求發出之前進行一些操作 return config; }, function (err) { //Do something with request error return Promise.reject(err); }); //新增一個響應攔截器 axios.interceptors.response.use(function (res) { //在這裡對返回的資料進行處理 return res.data; }, function (err) { //Do something with response error return Promise.reject(err); }); export default axios 複製程式碼
每次通過Axios.create返回一個全新的axios例項。
以上是專案裡面拷貝出來的程式碼,無法單獨執行,大家可以針對各自的需求進行相應調整。
plugins/axios.js
import Vue from 'vue'; import axios from '~/assets/js/config/config-axios' Vue.prototype.$$axios = axios; export default ({ app }, inject) => { // Set the function directly on the context.app object app.$$axios = axios; } 複製程式碼
這樣每次程式碼進行熱更新就不會出現上面的問題了。
這裡還把axios掛到了Vue的原型下面和app物件下面,頁面不需要引用axios就進行呼叫了。
2、asyncData
在nuxt中元件檔案申明的asyncData方法會被忽略,所以所有資料的載入都要放到對應page的asyncData中。
3、如何增加額外的全域性js檔案
我們可以通過定製模版來處理,在根目錄下建立 app.html
模版檔案,nuxt預設的模版為
<!DOCTYPE html> <html {{ HTML_ATTRS }}> <head> {{ HEAD }} </head> <body {{ BODY_ATTRS }}> {{ APP }} </body> </html> 複製程式碼
比如我們要增加搜尋引擎的收錄程式碼我們可以
<!DOCTYPE html> <html {{ HTML_ATTRS }}> <head> {{ HEAD }} <!-- 百度自動收錄指令碼 --> <script> (function(){ var bp = document.createElement('script'); var curProtocol = window.location.protocol.split(':')[0]; if (curProtocol === 'https') { bp.src = 'https://zz.bdstatic.com/linksubmit/push.js'; } else { bp.src = 'http://push.zhanzhang.baidu.com/push.js'; } var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(bp, s); })(); </script> <!-- 360自動搜錄指令碼 --> <script>(function(){ var src = (document.location.protocol == "http:") ? "http://js.passport.qihucdn.com/11.0.1.js******":"https://jspassport.ssl.qhimg.com/11.0.1.js?******"; document.write('<script src="' + src + '" id="sozz"><\/script>'); })(); </script> <!-- 百度統計 --> <script> var _hmt = _hmt || []; (function() { var hm = document.createElement("script"); hm.src = "https://hm.baidu.com/hm.js?******"; var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(hm, s); })(); </script> </head> <body {{ BODY_ATTRS }}> {{ APP }} </body> </html> 複製程式碼
4、讓專案支援ip訪問 在 package.json
檔案中增加如下配置即可
"config": { "nuxt": { "host": "0.0.0.0", "port": "3000" } }, 複製程式碼
5、正式環境、測試環境區分配置
package.json
"scripts": { "dev": "cross-env API_ENV=local nuxt", "build_beta": "cross-env API_ENV=beta nuxt build", "build_pro": "cross-env API_ENV=pro nuxt build", "start": "nuxt start", "generate": "nuxt generate", "lint": "eslint --ext .js,.vue --ignore-path .gitignore .", "precommit": "npm run lint" }, 複製程式碼
新增build_beta、build_pro,然後通過cross-env來設定環境
nuxt.config.js
env: { API_ENV: process.env.API_ENV }, 複製程式碼
這樣就可以在程式碼中,通過 process.env.API_ENV
來獲取到環境變數的值了。
6、通過pm2啟動專案
pm2-config.json
{ "apps": [ { "name": "nicomama-site", "script": "npm", "args" : "start", "watch": [".nuxt"] } ] } 複製程式碼
7、專案部署
先執行 cnpm run build_beta
或者 cnpm run build_pro
進行打包,然後通過執行 pm2 start pm2-config.json 來執行專案
後續有空再給大家理一份專案的結構
那麼就先到這裡了