在日常的項目中,有時候還是不可避免的會維護一些jq官網項目等。面對此類需求,很多還是以前的老套路,前端寫頁面交給後端去套數據。很煩有木有~~而改動之後還得交給後端再次修改,時間和溝通都是個麻煩。同時開發中,寫靜態頁面也很麻煩,不能復用,不能使用現在的工具,心累有木有~~當然了,解決辦法很多 自己寫個 ...
-
自己寫個webpack腳本維護起來,把工程化的那一套東西搬過來。
-
使用現成的nust\next等服務端渲染框架
-
藉助於node+模板引擎等
-
...
而本文介紹一下node的egg.js框架配合模板引擎來快速開發項目的模式。上手簡單快速,一個人搞定前後端。PS:又可以學習新知識來,我好(草)開(泥)心(馬),奧利給~~~
初始化項目
# 初始化
cnpm init egg --type=simple
# 安裝依賴
cnpm i
# 啟動服務
npm run dev
簡單看下生成的文件目錄(ps:個別文件是沒有的,後期自己添加的)
基本介紹
先介紹一下egg中app下的這些文件的基本作用,有個大概的概念:
-
app/router.js中編寫路由
-
app/controller/下編寫對應的控制器
-
app/service/下編寫service
-
app/view/下編寫對應的模板
路由編寫
-
路由,類似前端項目中的路由。是用來定義請求的URL。
-
當用戶訪問該路由地址時,將映射到對應的controller進行進一步的處理。
-
由此可見,路由router就是定義和controller之間的一種映射關係。
定義路由,首先要打開app/router.js文件,在裡面進行定義,例如:
'use strict'; module.exports = app => { const { router, controller } = app; // 定義首頁的路由 // 即當直接訪問功能變數名稱的時候,將請求映射到controller.home.home中進行進一步的處理 router.get('/', controller.home.home); // 定義關於我們的路由 router.get('/about', controller.home.about); // 定義新聞詳情的路由 router.get('/details/:id', controller.home.details); };
-
此處的路由可以理解為就是我們訪問的功能變數名稱後面的具體的路徑地址。例如
xxx.com/about中的/about
-
controller.xx.xx是指當我們訪問了這個路由的時候,服務將當前路由映射到這個控制器中。具體的控制器作用,下麵會詳細介紹。
控制器Controller
-
控制器,主要的作用就是處理用戶請求的參數,然後可以調用service服務得到我們需要的數據,最後返回數據或者直接渲染出一個模板。
-
而我們這裡則是根據router(router參數非必須)渲染出一個頁面,也就是渲染一個html頁面返回,這樣瀏覽器打開的就直接是頁面了。
'use strict'; const Controller = require('egg').Controller; class HomeController extends Controller { async home() { const { ctx } = this; await ctx.render('index.njk') } } module.exports = HomeController // 或者你也可以簡化一下寫法 module.exports = class extends require('egg').Controller { // ... }
-
通過調用ctx.render('index.njk')方法,我們將渲染一個模板並返回(可以理解為返回給瀏覽器)
-
在渲染模板的時候我們也可以給模板傳遞一個數據對象,ctx.render('index.njk', {}),然後模板內就可以通過模板引擎的語法渲染我們的數據了。
-
一般這個數據是通過service讀取的資料庫或者調用其他介面得到的數據。這個會在下麵的service中講到。
下麵演示一個調用service的例子:
// app/controller/home.js 'use strict'; module.exports = class extends require('egg').Controller { async details() { const { ctx } = this; try { // 調用service的home模塊中的details服務 // 得到數據後,塞給靜態模板使用 const data = await ctx.service.home.details(ctx.params.id) // 渲染一個模板 await ctx.render('details.njk', data) } catch (error) { ctx.body = '新聞獲取出錯' } } }
-
關於模板相關的內會在下麵詳細講解
-
關於service的定義和使用接下來馬上講解
Service服務
service服務主要是用來做什麼的呢?上面在controller中也提到了,主要用來獲取數據,拿到數據之後也可以格式化再返回給controller使用。
下麵演示一個service調用某些介面服務得到數據並返回給controller使用:
// 首先需要在app/下新建service文件夾 // 然後在service下新建home.js,最終為app/service/home.js 'use strict'; module.exports = class extends require('egg').Service { // 根據文章id獲取文章數據 // 此處的id是controll調用service服務時傳遞的id async details(id) { try { const url = `https:xxxx.com/api/${id}` const { data } = await this.ctx.curl(url, { dataType: 'json' }) if (data.data && data.code === 200) { // 此處也可以對數據進行處理後再返回 // 返回數據 return data.data } throw '數據獲取失敗' } catch (error) { throw error } } }
-
之所以叫
home.js
,是因為我們controller
中使用的是ctx.service.home
-
service.home
中的home
就是service
文件的文件名稱 -
此處進行了最簡單的數據獲取,通過
this.ctx.curl
方法請求其他介面的數據。類似於使用axios
獲取數據的操作。 -
當然了,也可以進行資料庫數據的增刪改查操作,本次就不做演示了,後面會考慮新開一篇egg做項目開發的文章再詳細介紹吧。
-
此處對於介面的功能變數名稱配置可以放在配置文件中
-
對於curl方法也可以進一步的封裝在helper中
-
對於數據返回的攔截也可以封裝在中間件進行攔截
-
對於上面這些內容有興趣的可以翻看egg文件中相應的說明,還是蠻詳細的。
模板渲染
定義好了基本的路由、控制器和service之後,就剩下模板了。首先模板,可能是前端小伙伴寫的靜態頁面給到我們的(ps:這個前端充氣小伙伴或許就是我們自己!哦呸,不是充氣的)
好了,言歸正傳!Egg中有關於模板渲染的文檔,可以看一下。Egg本身內置了egg-view作為模板的解決方案,其中View支持插件egg-view-nunjucks,本文也是通過該插件進行的模板開發。
# 首先安裝 cnpm i egg-view-nunjucks --save # 然後在根目錄下的config/plugin.js中使用插件 'use strict'; module.exports = { nunjucks: { enable: true, package: 'egg-view-nunjucks', } }; # 完成了插件的安裝和引入,我們還需要配置插件的參數 # 根目錄下的config/config.default.js中 module.exports = appInfo => { // 其他代碼 // ... // 配置我們的插件參數 config.view = { // 定義映射的文件尾碼名 // 此處我們定義為.njk,那麼我們的模板都需要以.njk結束, // 這樣該類型的文件才會被我們的模板插件進行處理 mapping: { '.njk': 'nunjucks', } } // 其他代碼 // ... }
-
註意,如果不叫
.njk
也是可以的,這個是自定義的,只要滿足你的模板文件的尾碼名和你定義的一樣就行,這樣才會被插件處理。 -
egg-view-nunjucks
封裝的是nunjucks
,nunjucks
推薦叫.njk
egg-view-nunjucks文檔 nunjucks文檔
有了模板引擎,我們嵌套數據等就會方便很多了。下麵來簡單看下一個模板的文件夾:
-
如上圖所示,需要新建app/view文件夾,所有的模板放在該文件夾下。這個也是可以通過配置修改的(如果你有需求的話);
-
上面的模板文件,根據你的項目來,這裡僅作為演示。通常的,我們可能會把頁面一些通用的頭部啊、底部、菜單等單獨抽離處理復用,具體的還是看你的項目。
下麵我們簡單看下base.njk
這個模板,做了什麼事情:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>{% block title %}title預設內容{% endblock %}</title> <meta name="keywords" content="{% block keywords %}keywords預設內容{% endblock %}"> <meta name="description" content="{% block description %}description預設內容{% endblock %}"> <link rel="stylesheet" type="text/css" href="../../public/css/base.css"> {% block head %}{% endblock %} </head> <body> <!-- 主體內容部分 --> {% block content %}{% endblock %} <script src="../../public/js/jquery.js"></script> <!-- script部分 --> {% block script %}{% endblock %} </body> </html>
-
很多時候,靜態頁面基本的結構都是一樣的,或者說類似的。所以我們可以定義一個基本的模板,然後所有的頁面繼承我們這個基礎的模板就好。如何繼承,後面會講。
-
比如,這裡我們定義的基本模板中有頭部,但是頭部中的title等內容可能各個頁面不一樣,所以我們自定義一個
block
塊,這樣就可以在頁面中替換掉這塊的內容。有些類似於vue的slot
插槽。
// 定義block塊
{% block title %}預設內容{% endblock %}
-
簡單說一下這裡個人定義的幾個基本的block的作用吧:
名稱 | 說明 |
---|---|
head塊 | 用於在head內最底部插入一些代碼。例如,公共模板引入了公共css,但是每個頁面還有可能單獨引入css,全局只有一個css文件的除外 |
content塊 | 用於放置每個頁面的主體部分 |
script塊 | 用於放置每個頁面需要單獨引入的js文件 |
定義好了基本的模板和塊,下麵我們看下頁面中如何使用:
{% extends "./base/base.njk" %} {% block title %}這是一個新的title{% endblock %} {% block description %}這是一個新的description{% endblock %} {% block keywords %}壹沓科技,加入壹沓,聯繫我們{% endblock %} {% set navActive = "about" %} {% block head %} <link rel="stylesheet" href="../public/css/swiper.min.css"> {% endblock %} {% block content %} <div class="page-wrapper"> <!-- 導入公共的nav模板 --> {% include './base/nav.njk' %} <!-- 背景圖 --> <section class="banner-wrapper"> <img src="../public/img/icon/about-banner.jpg" alt="背景LOGO"> <span>{{ userName }}</span> </section> <!-- 渲染html模板演示 --> <div>{{content | safe}}</div> <!-- 導入公共的底部模板 --> {% include './base/foot.njk' %} </div> {% endblock %} {% block script %} <script src="../public/js/swiper.min.js"></script> <script src="../public/js/about.js"></script> {% endblock %}
-
通過上面一個簡單的模板,卻包含了最常用和最關鍵的很多特性。首先第一行,我們要讓當前頁面繼承我們定義的基礎模板:
// 相對路徑即可
{% extends "./base/base.njk" %}
-
覆蓋我們在基礎模板中自定義的塊。也可以理解為給vue的slot傳入新的內容:
// 例如,設置該頁面的title
// 如果不設置,就會使用基礎模板中預設的
{% block title %}這是一個新的title{% endblock %}
-
設置變數,在當前模板中都可以使用。包括引入的其他模板,只要是出現在了當前模板中,都可以取到該值進行使用。
// 定義了一個navActive變數
// 後面會講解一個常見的場景
{% set navActive = "about" %}
-
使用變數、渲染內容/html
// 變數的使用,和vue的{{}}插值一樣
// 比如這個userName是我們從controller中調用service獲取的數據對象中的一個屬性,
// 然後把對象傳遞給了模板,
// 那麼在模板中可以直接取對象中的屬性進行渲染
// 註意,傳遞給模板的是一個對象,例如{userName: 'jack'},但是我們使用的時候直接取userName
{{userName}}
// 同樣的,我們在模板內定義的變數也可以直接使用的
// 渲染html,比如很常見的富文本
// 類似vue的過濾器,這個safe是內置的,
// 過濾器也可以自定義,具體的查看文檔吧
{{content | safe}}
-
引入其他模板。比如上面可以看到,我們繼承了基礎的模板,但是我們還可以會提取一些公共的像nav模板進行復用。
// 導入我們的nav模板 {% include './base/nav.njk' %}
-
nav模板一般很常見的會有這麼一個場景,比如就是當前頁面選項需要高亮。來,先看一下nav模板:
<!-- 導航頭部 --> <nav class="nav-wrapper"> <ul class="menu-content"> <li> <a href="/news" class="{{ 'active' if navActive == 'news' else '' }}">動態資訊</a> </li> <li> <a href="/about" class="{{ 'active' if navActive == 'about' else '' }}">關於我們</a> </li> </ul> </nav>
可以看到,我們對class名
進行了一個if判斷,判斷當變數navActive
是某些值的時候給他增加一個active
類名,否則就是各空的class名
。這裡仔細註意一下寫法即可。 那麼我們怎麼定義變數呢?此時再回到上面定義變數的部分介紹,我們已經在頁面中定義了一個變數navActive
,所以我們只需要每個頁面定義一個navActive
且值為我們需要的即可了。
-
最後再補充說明一下,模板內的調整路徑,就直接寫成我們在
router.js
中定義的路由即可,而不是寫的模板地址。
// a標簽跳轉 <a href="/news"></a> // js調整也是一樣,使用router.js定義的路由 window.location.href="/news"
總結
文章是有點水,請輕噴