Github 完整項目地址html
很久都沒有寫過文章了,以前寫過一篇 《【實戰】webpack4 + ejs + express 帶你擼一個多頁應用項目架構》,發佈後我發現你們對於 「簡化多頁應用開發流程」 這塊需求強烈,而且,隨着我將上一篇文章中介紹的多頁開發模式運用到實際項目中後,發現仍是存在一些缺陷的。其中痛點之一就是,使用 express
做爲後臺開發框架。前端
express
當然不錯,可是如今開發講究效率,所謂伸手就來開箱即用,這麼一對比,express
仍是偏向底層,做爲服務端框架,不少東西仍是要本身費心費神找插件 install
看文檔。因而,這一次我準備使用更上層的 egg
做爲替代框架。node
雖然是上一版的進化版,可是不少主要的實現思路是沒有變的,想要詳細瞭解的朋友推薦先看下上一篇 《【實戰】webpack4 + ejs + express 帶你擼一個多頁應用項目架構》,這篇只作關鍵步驟分析,詳細代碼可見 Github 完整項目地址jquery
目錄乍一看彷佛有點多,不要緊都是唬人的,最重要的幾個目錄我已在截圖上標出,咱們能夠展開看下主要目錄的詳細目錄結構:webpack
項目結構介紹完,下面就要開始改造以前的代碼了,但是這麼多代碼從哪裏動手呢?咱們此次主要目的就是將 express
換成 egg
,那固然是從 egg
開始着手改造。ios
改造以前,咱們還須要明白最重要的兩個問題,這兩個問題一旦被解決,能夠說整個項目的改造也完成的差很少了。哪兩個問題呢?nginx
egg
要怎樣與 webpack
結合?ejs
做爲模板引擎,要怎樣在 dev
環境和 prod
環境正確將 ejs
渲染成 html
並顯示在頁面上?在動手處理 egg
層以前,咱們須要先去官方文檔上了解一下這個框架。 因爲是阿里旗下產品,因此框架自己的穩定性、生態建設程度和文檔的友好性確定是有保證的。git
egg
是一款基於 koa
開發的 「企業級應用框架」,簡單理解就是在 koa
上又封裝了一層,把什麼 request
、response
以及相關的一切操做方法都簡化封裝了,讓普通開發者能更容易的使用,將更多的精力放在 996 啊不是,是業務開發上,就是所謂的伸手就來。angularjs
egg
奉行 「約定優於配置」 的原則,這一點在和 express
一對比就立馬體現出來。express
就約束程度而言和 jquery
差很少,隨便寫。心之所向,哪裏都是 router
,至於 middleware
、service
和 controller
,那是什麼東西??es6
對於 egg
來講就不是這樣,它犧牲了自由性,取而代之的是更加統一的寫法:業務代碼寫到 controller
裏,中間件寫到 middleware
裏,sql
寫到 service
,其他的插件和配置也有統一的入口,否則它就跑不起來。加之又有強大插件生態加持,靈活性也是不弱的。
egg-webpack
做爲 egg
生態支持的 webpack
插件,直接就能夠 npm install
一把梭。梭的時候注意,這個東西是 devDependencies
,不要梭到 dependencies
裏面。
安裝完成之後,須要寫入 /config/plugin.js
的插件配置裏,設置爲 true
開啓插件:
/** @type Egg.EggPlugin */
module.exports = {
webpack: { // 開發環境,開啓 egg-webpack 插件
enable: process.env.NODE_ENV === 'development',
package: 'egg-webpack',
},
ejs: {
enable: true,
package: 'egg-view-ejs',
},
static: {
enable: true,
package: 'egg-static',
},
};
複製代碼
至於其餘兩個 egg-view-ejs
和 egg-static
你也看到了,一個是 ejs
的模板引擎插件,一個是靜態資源插件,都梭過來。
上面一步將插件安裝並開啓後,下面須要告訴 egg-webpack
去哪裏找到原生 webpack
配置文件。
打開 /config/config.local.js
寫入以下代碼:
/* eslint valid-jsdoc: "off" */
'use strict';
const path = require('path');
/** * @param {Egg.EggAppInfo} appInfo app info */
module.exports = appInfo => {
/** * built-in config * @type {{}} **/
const config = exports = {};
// add your middleware config here
config.middleware = [];
// 開發環境下須要開啓 webpack 編譯
config.webpack = {
// port: 9000, // port: {Number}, default 9000. webpack dev server 的默認端口,默認爲 9000,開啓熱更新時 websocket 的自動請求端口
webpackConfigList: [ require('../build/webpack.dev.config') ],
};
// 開發環境下,將 egg-static 靜態資源轉發目錄由默認的 /app/public 改成 /src/static (具體的轉發地址能夠自行定義)
config.static = {
prefix: '/public/',
dir: path.join(appInfo.baseDir, 'src/static'),
};
// add your user config here
const userConfig = {
// myAppName: 'egg',
};
return {
...config,
...userConfig,
};
};
複製代碼
注意:
egg-webpack
只有在開發環境下才須要開啓,生產環境直接在package.json
裏配置build
腳本就好
"build": "cross-env NODE_ENV=production webpack --config ./build/webpack.prod.config.js",
複製代碼
egg
會自動根據 package.json
的腳本命令找到合適的配置文件,例如,開發模式下會找到 /config/config.default.js
和 /config/config.local.js
文件進行合併;生產環境下會找到 /config/config.default.js
和 /config/config.prod.js
文件進行合併。
至於 /build
裏的 webpack
配置信息,前一篇文章已經詳細說明,這裏就不過多贅述了。
上述代碼中,還有一塊比較重要的配置:
egg-static
的配置,config.static
的配置將前綴爲/public/
的請求標記爲靜態資源請求,所有轉發至/src/static
目錄下。
其實,如何在開發環境下獲取到 ejs
模板而且將數據合成上去渲染成瀏覽器可以識別的 html
而後返回,纔是真正的靈魂步驟。
這個在 egg-webpack
那塊的 /config/plugin.js
就說過了。
打開 /config/config.default.js
寫入以下代碼:
/* eslint valid-jsdoc: "off" */
'use strict';
const path = require('path');
/** * @param {Egg.EggAppInfo} appInfo app info */
module.exports = appInfo => {
/** * built-in config * @type {Egg.EggAppConfig} **/
const config = exports = {};
// use for cookie sign key, should change to your own and keep security
config.keys = appInfo.name + '_1599807210902_4670';
// add your middleware config here
config.middleware = [];
config.view = {
mapping: {
'.ejs': 'ejs',
},
defaultViewEngine: 'ejs',
};
// add your user config here
const userConfig = {
// myAppName: 'egg',
};
return {
...config,
...userConfig,
};
};
複製代碼
其中 config.view
用於配置模板文件後綴和默認模板引擎。
做爲開發環境和生產環境都須要的代碼片斷,所以要寫入
/config/config.default.js
配置文件中。
在開啓了 egg-view-ejs
相關配置後,咱們要開始進行 egg
的業務代碼編寫。
首先配置 /app/router.js
文件:
'use strict';
/** * @param {Egg.Application} app - egg application */
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
router.get('/welcome', controller.welcome.index);
};
複製代碼
好了,如今 /
和/welcome
兩個請求將被 egg
轉發到對應的 controller
層進行處理,控制器通過數據的請求組裝和處理,最後會給頁面返回出一個可以渲染的 html
,下面咱們看看控制器作了什麼。
因爲此項目是個模板框架,後端代碼並不會涉及到數據庫和中間件,所以不須要
middleware
和service
,不過若是你想以此爲起點進行二次項目開發,這兩個幾乎是必不可少的。
因爲
/app
中並不會直接存放原始的前端代碼,全部的 es六、樣式和模板文件最後都會被 webpack 編譯成靜態資源塞入其中,所以/app/public
和/app/view
在初始狀態下應該是空的。相似與下圖
控制器文件以 /controller/home.js
爲例分析:
const Controller = require('egg').Controller;
const { render } = require('../utils/utils.js');
class HomeController extends Controller {
async index() {
const { ctx } = this;
await render(ctx, 'home.ejs', { title: '首頁' });
// ctx.render('home.ejs', { title: '首頁' });
}
}
module.exports = HomeController;
複製代碼
能夠看到自己代碼很是簡單,頁面渲染的重點在於 render
方法,咱們看看/app/utils/utils.js
文件
神奇的 render
const axios = require('axios');
// const ejs = require('ejs');
const CONFIG = require('../../build/config.dev');
const isDev = process.env.NODE_ENV === 'development';
function getTemplateString(filename) {
return new Promise((resolve, reject) => {
axios.get(`http://localhost:${CONFIG.PORT}${CONFIG.PATH.PUBLIC_PATH}${CONFIG.DIR.VIEW}/${filename}`).then(res => {
resolve(res.data);
}).catch(reject);
});
}
/** * render 方法 * @param ctx egg 的 ctx 對象 * @param filename 須要渲染的文件名 * @param data ejs 渲染時須要用到的附加對象 * @return {Promise<*|undefined>} */
async function render(ctx, filename, data = {}) {
// 文件後綴
const ext = '.ejs';
filename = filename.indexOf(ext) > -1 ? filename.split(ext)[0] : filename;
try {
if (isDev) {
const template = await getTemplateString(`${filename}${ext}`);
ctx.body = await ctx.renderString(template, data);
} else {
await ctx.render(`${filename}${ext}`, data);
}
} catch (e) {
return Promise.reject(e);
}
}
module.exports = {
getTemplateString,
render,
};
複製代碼
能夠看到 render
函數的內部實現邏輯:
egg
提供的 ctx.render
渲染出指定的模板文件就能夠了http://localhost:7001/public/view/*.ejs
獲取到 ejs
的源文件字符串,而後使用 egg
提供的 ctx.renderString
將其渲染到頁面上。關於如何獲取模板這一問題,我也看過不少老師的方法,其中一種就是調用
webpack
相關 API 直接一把揪出底層的memory
內存文件,而後手動調用 js 編譯一頓操做猛如虎,最後把它渲染出來,龜龜~ 反正我是看了半天沒有學會,並且看代碼量感受工做量不菲且要對webpack
的編譯原理研究頗深,方可有所建樹。若是你們有興趣也能夠探究探究。
**注意:**這裏有一處很是有意思的地方。你們仔細想一下就會發現不對:咱們在 egg-static
中配置的靜態資源映射路徑是前綴爲 /public/
的資源請求全都轉發到 /src/static
下,可是這個 /public/view/*.ejs
文件的原資源路徑是在 /src/view
裏的,這是怎麼映射過去的??
其實除了
egg-static
配置的靜態資源映射,webpack
本身也有一層資源映射,而我此處webpack.output.publicPath
寫的恰好也是/public/
,就是說,webpack
編譯並將文件生成到內存中的時候,內存的訪問地址前綴也須要加上/public/
;而這個/public/view/*.ejs
文件訪問到的正是webpack
內存中的資源文件。
我的感受開發環境中的靜態資源的訪問模式是:到 egg-static
和 webpack
配置的不一樣地址下去找,找到哪一個就返回哪一個。
固然,生產環境下的靜態資源訪問因爲不會有 webpack
直接參與,就不會存在這個問題了,你可使用 egg-static
配置在同項目下,也可使用 nginx
跨項目進行靜態資源轉發配置。
寫到這裏,基本項目已經幾乎完成了。剩下還有一些細節須要注意,我寫在這裏,提醒你們也提醒本身:
咱們若是在 ejs
中寫入圖片等靜態資源,有兩種方式:
/public/
前綴這種絕對路徑的手法,這種方法,須要注意的是:egg
的 local
配置文件 /config/config.local.js
中改寫了 egg-static
的靜態資源指向爲 /src/static/
。因此在 dev 環境圖片資源是可以正常訪問到的。可是因爲生產環境下的 egg-static
的靜態資源指向默認是 /app/public
,而且絕對路徑的圖片引用形式不會被被 webpack
識別處理,因此必定要保證生產環境下 /app/public
文件夾下有該圖片資源,不然就是 404 資源請求。若是使用這種圖片引用方式,推薦使用 copyWebpack
之類的插件作生產環境的靜態資源的拷貝處理。../
的相對路徑形式,相對路徑的請求形式,是可以正常被 webpack 識別處理和複製的,因此並不須要開發者作額外處理。只是,因爲 ejs
是由 includes
功能的,有時候咱們可能會引入一些公用的 ejs
代碼塊,而這些代碼塊中頗有多是有圖片等引用資源的。這個時候要注意,因爲這塊是 includes
的文件,最後 includes
文件會被拼接到主文件中,而後再丟給 html-loader
解析,因此這塊的圖片路徑須要寫主文件下的相對路徑,否則就找不到圖片。如上圖,兩種寫法得到的圖片明顯是不同的,上面一種未通過 webpack
打包,下面的明顯被 webpack
處理過了。
關於熱更新,這一版和上一版不太同樣,因此有些地方須要修改一下:
首先是 /build/webpack.base.config.js
文件:
module.exports = {
// ...
entry: (filepathList => {
const entry = {};
filepathList.forEach(filepath => {
const list = filepath.split(/(\/|\/\/|\\|\\\\)/g);
const key = list[list.length - 1].replace(/\.js/g, '');
// 若是是開發環境,才須要引入 hot module
entry[key] = isDev ?
[
filepath,
// 這邊注意端口號,之間安裝的 egg-webpack,會啓動 dev-server,默認端口號爲 9000
`webpack-hot-middleware/client?path=http://127.0.0.1:9000/__webpack_hmr&noInfo=false&reload=true&quiet=false`,
] : filepath;
});
return entry;
})(glob.sync(resolve(__dirname, '../src/js/*.js'))),
// ...
};
複製代碼
這邊的 entry
入口除了 filepath
,還須要把 webpack-hot-middleware
加上,並把相關配置以 queryString
的方式拼接,最重要的配置就是 path=http://127.0.0.1:9000/__webpack_hmr
,這句是指定了熱更新的 websocket
的地址的,因爲 egg
自己啓動的服務和 webpack-dev-server
啓動的服務並不同,這裏不配置的話,默認熱更新會去請求 7001
端口,也就是開發端口,那確定是拿不到東西的。
不知道你們有沒有注意到以前 /config/config.local.js
中的 webpack
配置,裏面有一項能夠設置 webpack-dev-server
的端口號:
// 開發環境下須要開啓 webpack 編譯
config.webpack = {
// port: 9000, // port: {Number}, default 9000. webpack dev server 的默認端口,默認爲 9000,開啓熱更新時 websocket 的自動請求端口
webpackConfigList: [ require('../build/webpack.dev.config') ],
};
複製代碼
若是不想用默認的 9000 ,更改這個 port
也是能夠的,只不過改了默認端口也要記得把 webpack
熱更新配置裏的默認端口也同時改掉。
最後,webpack-hot-module
原生是不支持模板文件的熱更新的,這點在上一篇中也說明了。因此每一個前端頁面的 js 入口文件中須要加上:
if (process.env.NODE_ENV === 'development') {
// 在開發環境下,使用 raw-loader 引入 ejs 模板文件,強制 webpack 將其視爲須要熱更新的一部分 bundle
require('raw-loader!../view/home.ejs');
if (module.hot) {
module.hot.accept();
/** * 監聽 hot module 完成事件,從新從服務端獲取模板,替換掉原來的 document * 這種熱更新方式須要注意: * 1. 若是你在元素上以前綁定了事件,那麼熱更新以後,這些事件可能會失效 * 2. 若是事件在模塊卸載以前未銷燬,可能會致使內存泄漏 * 上述兩個問題的解決方式,能夠在 document.body 內容替換以前,將事件手動解綁。 */
module.hot.dispose(() => {
const href = window.location.href;
axios.get(href).then(res => {
document.body.innerHTML = res.data;
}).catch(e => {
console.error(e);
});
});
}
}
複製代碼
注意:上面這一段熱更新代碼是不能拆成函數去引入使用的,沒有用,我試過,只能在每一個頁面的入口文件中
ctrlCV
,固然若是你以爲麻煩,徹底能夠不這麼作,頂多就是模板文件更改不會熱更新而已,本身刷新一下也不麻煩,效果同樣。
記得我在從業時的第一家公司的第一份工做,就是改寫官網。那個官網是用前人寫的 gulp
編譯腳本打包的,而 gulp
對於高階的 ES6+
語法的支持簡直就是一塌糊塗;更糟的是因爲純前端代碼沒有 node
層支持,只能靠 ajax
來獲取數據。在那個先後端分離尚未徹底推行的時代,在那個 angularjs
髒檢查瘋狂遍歷的年代,前端寫代碼還要開 eclipse
,等後端兄弟的服務起來才能動手。
我從那時便想,若是有一天,前端開發多頁應用能像拉屎同樣簡單。
那該多好。
完整項目地址能夠查看個人 Github 完整項目地址 ,喜歡的話給個 Star⭐️ ,多謝~ 你的點贊,將是我持續輸出的動力😃❤️