node服務端渲染(完整demo)
簡介
nodejs搭建多頁面服務端渲染
- 技術點
- koa 搭建服務
- koa-router 建立頁面路由
- nunjucks 模板引擎組合html
- webpack打包多頁面
- node端非同步請求
- 服務端日誌列印
專案原始碼 git clone gitee.com/wjj0720/nod…
- 執行
- npm i
- npm start
一、 現代服務端渲染的由來
服務端渲染概念: 是指,瀏覽器向伺服器發出請求頁面,服務端將準備好的模板和資料組裝成完整的HTML返回給瀏覽器展示
-
1、前端後端分離
早在七八年前,幾乎所有網站都使用 ASP、Java、PHP做後端渲染,隨著網路的加快,客戶端效能提高以及js本身的效能提高,我們開始往客戶端增加更多的功能邏輯和互動,前端不再是簡單的html+css更多的是互動,前端頁在這是從後端分離出來「前後端正式分家」
-
2、客戶端渲染
隨著ajax技術的普及以及前端框架的崛起(jq、Angular、React、Vue) 框架的崛起,開始轉向了前端渲染,使用 JS 來渲染頁面大部分內容達到區域性重新整理的作用
- 優勢
- 區域性重新整理,使用者體驗優
- 富互動
- 節約伺服器成本
- 缺點
- 不利於SEO(爬蟲無法爬取ajax)請求回來的資料
- 受瀏覽器效能限制、增加手機端的耗電
- 首屏渲染需要等js執行才能展示資料
- 優勢
-
3、現在服務端渲染
為了解決上面客戶端渲染的缺點,然前後端分離後必不能合,如果要把前後端部門合併,拆掉的肯定是前端部門
- 現在服務端渲染的特點
- 前端開發人員編寫html+css模板
- node中間服務負責前端模板和後臺資料的組合
- 資料依然由java等前服務端語言提供
- 優勢
- 前後端分工明確
- SEO問題解決
- 現在服務端渲染的特點
-
4、前、後端渲染相關討論參考
二、 專案開始
確保你安裝node
第一步 讓服務跑起來
目標: 建立node服務,通過瀏覽器訪問,返回'hello node!'(html頁面其實就是一串字串)
/** 建立專案目錄結構如下 */
│─ package-lock.json
│─ package.json
│─ README.md
├─bin
│─ www.js
// 1. 安裝依賴 npm i koa
// 2. 修改package.json檔案中 scripts 屬性如下
"scripts": {
"start": "node bin/www.js"
}
// 3. www.js寫入如下程式碼
const Koa = require('koa');
let app = new Koa();
app.use(ctx => {
ctx.body = 'hello node!'
});
app.listen(3000, () => {
console.log('伺服器啟動 http://127.0.0.1:3000');
});
// 4 npm start 瀏覽器訪問 http://127.0.0.1:3000 檢視效果
複製程式碼
第二步 路由的使用
目標:使用koa-router根據不同url返回不同頁面內容
- 依賴 npm i koa-router koa-router 更多細節 請至npm檢視
/** 新增routers資料夾 目錄結構如下
│─.gitignore
│─package.json
│─README.md
├─bin
│ │─www.js
├─node_modules
└─routers
│─home.js
│─index.js
│─user.js
*/
//專案中應按照模組對路由進行劃分,示例簡單將路由劃分為首頁(/)和使用者頁(/user) 在index中將路由集中管理導, 出並在app例項後掛載到app上
複製程式碼
/** router/home.js 檔案 */
// 引包
const homeRouter = require('koa-router')()
//建立路由規則
homeRouter.get(['/', '/index.html', '/index', '/home.html', '/home'], (ctx, next) => {
ctx.body = 'home'
});
// 匯出路由備用
module.exports = homeRouter
/** router/user.js 檔案 */
const userRouter = require('koa-router')()
userRouter.get('/user', (ctx, next) => {
ctx.body = 'user'
});
module.exports = userRouter
複製程式碼
/** router/index.js 檔案 */
// 路由集中點
const routers = [
require('./home.js'),
require('./user.js')
]
// 簡單封裝
module.exports = function (app) {
routers.forEach(router => {
app.use(router.routes())
})
return routers[0]
}
複製程式碼
/** www.js 檔案改寫 */
// 引入koa
const Koa = require('koa')
const Routers = require('../routers/index.js')
// 例項化koa物件
let app = new Koa()
// 掛載路由
app.use((new Routers(app)).allowedMethods())
// 監聽3000埠
app.listen(3000, () => {
console.log('伺服器啟動 http://127.0.0.1:3000')
})
複製程式碼
第三步 加入模板
目標: 1.使用nunjucks解析html模板返回頁面 2.瞭解koa中介軟體的使用
- 依賴 npm i nunjucks nunjucks中文文件
/*
*我向專案目錄下加入兩個準備好的html檔案 目錄結構如下
│─.gitignore
│─package.json
│─README.md
├─bin
│ │─www.js
│─middlewares //新增中介軟體目錄
│ ├─nunjucksMiddleware.js //nunjucks模板中介軟體
├─node_modules
│─routers
│ │─home.js
│ │─index.js
│ │─user.js
│─views //新增目錄 作為檢視層
├─home
│ ├─home.html
├─user
├─user.html
*/
複製程式碼
/* nunjucksMiddleware.js 中介軟體的編寫
*什麼是中介軟體: 中介軟體就是在程式執行過程中增加輔助功能
*nunjucksMiddleware作用: 給請求上下文加上render方法 將來在路由中使用
*/
const nunjucks = require('nunjucks')
const path = require('path')
const moment = require('moment')
let nunjucksEVN = new nunjucks.Environment(new nunjucks.FileSystemLoader('views'))
// 為nkj加入一個過濾器
nunjucksEVN.addFilter('timeFormate', (time, formate) => moment(time).format( formate || 'YYYY-MM-DD HH:mm:ss'))
// 判斷檔案是否有html字尾
let isHtmlReg = /\.html$/
let resolvePath = (params = {}, filePath) => {
filePath = isHtmlReg.test(filePath) ? filePath : filePath + (params.suffix || '.html')
return path.resolve(params.path || '', filePath)
}
/**
* @description nunjucks中介軟體 新增render到請求上下文
* @param params {}
*/
module.exports = (params) => {
return (ctx, next) => {
ctx.render = (filePath, renderData = {}) => {
ctx.type = 'text/html'
ctx.body = nunjucksEVN.render(resolvePath(params, filePath), Object.assign({}, ctx.state, renderData))
}
// 中介軟體本身執行完成 需要呼叫next去執行下一步計劃
return next()
}
}
複製程式碼
/* 中介軟體掛載 www.js中增加部分程式碼 */
// 頭部引入檔案
const nunjucksMiddleware = require('../middlewares/nunjucksMiddleware.js')
//在路由之前呼叫 因為我們的中介軟體是在路由中使用的 故應該在路由前加到請求上下文ctx中
app.use(nunjucksMiddleware({
// 指定模板資料夾
path: path.resolve(__dirname, '../views')
})
複製程式碼
/* 路由中呼叫 以routers/home.js 為例 修改程式碼如下*/
const homeRouter = require('koa-router')()
homeRouter.get(['/', '/index.html', '/index', '/home.html', '/home'], (ctx, next) => {
// 渲染頁面的資料
ctx.state.todoList = [
{name: '吃飯', time: '2019.1.4 12:00'},
{name: '下午茶', time: '2019.1.4 15:10'},
{name: '下班', time: '2019.1.4 18:30'}
]
// 這裡的ctx.render方法就是我們通過nunjucksMiddleware中介軟體新增的
ctx.render('home/home', {
title: '首頁'
})
})
module.exports = homeRouter
複製程式碼
第四步 抽取公共模板
目標: 抽取頁面的公用部分 如導航/底部/html模板等
/**views目錄下增加兩個資料夾_layout(公用模板) _component(公共元件) 目錄結構如下
│─.gitignore
│─package.json
│─README.md
├─bin
│ │─www.js /koa服務
│─middlewares //中介軟體目錄
│ ├─nunjucksMiddleware.js //nunjucks模板中介軟體
├─node_modules
│─routers //服務路由目錄
│ │─home.js
│ │─index.js
│ │─user.js
│─views //頁面檢視層
│─_component
│ │─nav.html (公用導航)
│─_layout
│ │─layout.html (公用html框架)
├─home
│ ├─home.html
├─user
├─user.html
*/
複製程式碼
<!-- layout.html 檔案程式碼 -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
</head>
<body>
<!-- 佔位 名稱為content的block將放在此處 -->
{% block content %}
{% endblock %}
</body>
</html>
<!-- nav.html 公用導航 -->
<ul>
<li><a href="/">首頁</a></li>
<li><a href="/user">使用者頁</a></li>
</ul>
複製程式碼
<!-- home.html 改寫 -->
<!-- njk繼承模板 -->
{% extends "../_layout/layout.html" %}
{% block content %}
<!-- njk引入公共模組 -->
{% include "../_component/nav.html" %}
<h1>待辦事項</h1>
<ul>
<!-- 過濾器的呼叫 timeFormate即我們在中介軟體中給njk加的過濾器 -->
{% for item in todoList %}
<li>{{item.name}} ---> {{item.time | timeFormate}}</li>
{% endfor %}
</ul>
{% endblock %}
<!-- user.html -->
{% extends "../_layout/layout.html" %}
{% block content %}
{% include "../_component/nav.html" %}
使用者中心
{% endblock %}
複製程式碼
第五步 靜態資源處理
目標: 處理頁面js\css\img等資源引入
- 依賴
- 用webpack打包靜態資源 npm i webpack webpack-cli -D
- 處理js npm i @babel/core @babel/preset-env babel-loader -D
- 處理less npm i css-loader less-loader less mini-css-extract-plugin -D
- 處理檔案 npm i file-loader copy-webpack-plugin -D
- 處理html npm i html-webpack-plugin -D
- 清理打包檔案 npm i clean-webpack-plugin -D
相關外掛使用 檢視npm相關文件
/* 專案目錄 變更
│ .gitignore
│ package.json
│ README.md
├─bin
│ www.js
├─config //增加webpack配置目錄
│ webpack.config.js
├─middlewares
│ nunjucksMiddleware.js
├─routers
│ home.js
│ index.js
│ user.js
├─src
│ │─template.html // + html模板 以此模板為每個入口生成 引入對應js的模板
│ ├─images // +圖資源目錄
│ │ ww.jpg
│ ├─js // + js目錄
│ │ ├─home
│ │ │ home.js
│ │ └─user
│ │ user.js
│ └─less // + css目錄
│ ├─common
│ │ common.less
│ │ nav.less
│ ├─home
│ │ home.less
│ └─user
│ user.less
└─views
├─home
│ home.html
├─user
│ user.html
├─_component
│ nav.html
└─_layout // webpac打包後的html模板
├─home
│ home.html
└─user
user.html
*/
複製程式碼
<!-- template.html 內容-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{{title}}</title>
</head>
<body>
<!-- njk模板繼承後填充 -->
{% block content %}
{% endblock %}
</body>
</html>
複製程式碼
/* src/js/home/home.js 一個入口檔案*/
import '../../less/home/home.less' //引入css
import img from '../../images/ww.jpg' //引入圖片
console.log(111);
let add = (a, b) => a + b; //箭頭函式
let a = 3, b = 4;
let c = add(a, b);
console.log(c);
// 這裡只做打包演示程式碼 不具任何意義
複製程式碼
<!-- less/home/home.less 內容 -->
// 引入公共樣式
@import '../common/common.less';
@import '../common/nav.less';
.list {
li {
color: rebeccapurple;
}
}
.bg-img {
width: 200px;
height: 200px;
background: url(../../images/ww.jpg); // 背景圖片
margin: 10px 0;
}
複製程式碼
/* webpack配置 webpack.config.js */
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CopyWebpackPlugin = require('copy-webpack-plugin');
// 多入口
let entry = {
home: 'src/js/home/home.js',
user: 'src/js/user/user.js'
}
module.exports = evn => ({
mode: evn.production ? 'production' : 'development',
// 給每個入口 path.reslove
entry: Object.keys(entry).reduce((obj, item) => (obj[item] = path.resolve(entry[item])) && obj, {}),
output: {
publicPath: '/',
filename: 'js/[name].js',
path: path.resolve('dist')
},
module: {
rules: [
{ // bable 根據需要轉換到對應版本
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{ // 轉換less 並交給MiniCssExtractPlug外掛提取到單獨檔案
test: /\.less$/,
loader: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'],
exclude: /node_modules/
},
{ //將css、js引入的圖片目錄指到dist目錄下的images 保持與頁面引入的一致
test: /\.(png|svg|jpg|gif)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: './images',
}
}]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: './font',
}
}]
}
]
},
plugins: [
// 刪除上一次打包目錄(一般來說刪除自己輸出過的目錄 )
new CleanWebpackPlugin(['dist', 'views/_layout'], {
// 當配置檔案與package.json不再同一目錄時候需要指定根目錄
root: path.resolve()
}),
new MiniCssExtractPlugin({
filename: "css/[name].css",
chunkFilename: "[id].css"
}),
// 將src下的圖片資源平移到dist目錄
new CopyWebpackPlugin(
[{
from: path.resolve('src/images'),
to: path.resolve('dist/images')
}
]),
// HtmlWebpackPlugin 每個入口生成一個html 並引入對應打包生產好的js
...Object.keys(entry).map(item => new HtmlWebpackPlugin({
// 模組名對應入口名稱
chunks: [item],
// 輸入目錄 (可自行定義 這邊輸入到views下面的_layout)
filename: path.resolve('views/_layout/' + entry[item].split('/').slice(-2).join('/').replace('js', 'html')),
// 基準模板
template: path.resolve('src/template.html')
}))
]
});
<!-- package.json中新增 -->
"scripts": {
"start": "node bin/www.js",
"build": "webpack --env.production --config config/webpack.config.js"
}
執行 npm run build 後生成 dist views/_layout 兩個目錄
複製程式碼
<!-- 檢視打包後生成的模板 views/_layout/home/home.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{{title}}</title>
<!-- 引入了css檔案 -->
<link href="/css/home.css" rel="stylesheet"></head>
<body>
{% block content %}
{% endblock %}
<!-- 引入了js檔案 此時打包後的js/css在dist目錄下面 -->
<script type="text/javascript" src="/js/home.js"></script></body>
</html>
複製程式碼
<!-- view/home/home.html 頁面改寫 -->
<!-- njk繼承模板 繼承的目標來自webpack打包生成 -->
{% extends "../_layout/home/home.html" %}
{% block content %}
<!-- njk引入公共模組 -->
{% include "../_component/nav.html" %}
<h1>待辦事項</h1>
<ul class="list">
<!-- 過濾器的呼叫 timeFormate即我們在中介軟體中給njk加的過濾器 -->
{% for item in todoList %}
<li>{{item.name}} ---> {{item.time | timeFormate}}</li>
{% endfor %}
</ul>
<div class="bg-img"> 背景圖</div>
<!-- 頁面圖片引入方式 -->
<img src="/images/ww.jpg"/>
{% endblock %}
複製程式碼
/**koa處理靜態資源
* 依賴 npm i 'koa-static
*/
// www.js 增加 將靜態資源目錄指向 打包後的dist目錄
app.use(require('koa-static')(path.resolve('dist')))
複製程式碼
執行 npm run build npm start 瀏覽器訪問127.0.0.1:3000 檢視頁面 js css img 效果
第六步 監聽編譯
目標: 檔案發生改實時編譯打包
- 依賴 npm i pm2 concurrently
/**專案中檔案發生變動 需要重啟服務才能看到效果是一件蛋疼的事,故需要實時監聽變動 */
<!-- 我們要監聽的有兩點 一是node服務 而是webpack打包 package.json變動如下 -->
"scripts": {
// concurrently 監聽同時監聽兩條命令
"start": "concurrently \"npm run build:dev\" \"npm run server:dev\"",
"dev": "npm start",
// 生產環境 執行兩條命令即可 無監聽
"product": "npm run build:pro && npm run server:pro",
// pm2 --watch引數監聽服務的程式碼變更
"server:dev": "pm2 start bin/www.js --watch",
// 生產不需要用監聽
"server:pro": "pm2 start bin/www.js",
// webpack --watch 對打包檔案監聽
"build:dev": "webpack --watch --env.production --config config/webpack.config.js",
"build:pro": "webpack --env.production --config config/webpack.config.js"
}
複製程式碼
第七步 資料請求
目標: node請求介面資料 填充模板
- 依賴 npm i node-fetch
/*上面的程式碼中routers/home.js首頁路由中我們向頁面渲染了下面的一組資料 */
ctx.state.todoList = [
{name: '吃飯', time: '2019.1.4 12:00'},
{name: '下午茶', time: '2019.1.4 15:10'},
{name: '下班1', time: '2019.1.4 18:30'}
]
/*但 資料是同步的 專案中我們必然會向java獲取其他後臺拿到渲染資料再填充頁面 我們來看看怎麼做*/
複製程式碼
/*我們在根目錄下建立一個util的目錄作為工具庫 並簡單封裝fetch.js請求資料*/
const nodeFetch = require('node-fetch')
module.exports = ({url, method, data = {}}) => {
// get請求 將引數拼到url
url = method === 'get' || !method ? "?" + Object.keys(data).map(item => `${item}=${data[item]}`).join('&') : url;
return nodeFetch(url, {
method: method || 'get',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
}).then(res => res.json())
}
複製程式碼
/*在根目錄下建立一個service的目錄作為資料層 並建立一個exampleService.js 作為示例*/
//引入封裝的 請求工具
const fetch = require('../util/fetch.js')
module.exports = {
getTodoList (params = {}) {
return fetch({
url: 'https://www.easy-mock.com/mock/5c35a2a2ce7b4303bd93fbda/example/todolist',
method: 'post',
data: params
})
},
//...
}
複製程式碼
/* 將請求加入到路由中 routers/home.js 改寫 */
const homeRouter = require('koa-router')()
let exampleService = require('../service/exampleService.js') // 引入service api
//將路由匹配回撥 改成async函式 並在請時候 await資料回來 再呼叫render
homeRouter.get(['/', '/index.html', '/index', '/home.html', '/home'], async (ctx, next) => {
// 請求資料
let todoList = await exampleService.getTodoList({name: 'ott'})
// 替換原來的靜態資料
ctx.state.todoList = todoList.data
ctx.render('home/home', {
title: '首頁'
})
})
// 匯出路由備用
module.exports = homeRouter
複製程式碼
第八步 日誌列印
目標: 使程式執行可視
- 依賴 npm i log4js
/* 在util目錄下建立 logger.js 程式碼如下 作簡單的logger封裝 */
const log4js = require('log4js');
const path = require('path')
// 定義log config
log4js.configure({
appenders: {
// 定義兩個輸出源
info: { type: 'file', filename: path.resolve('log/info.log') },
error: { type: 'file', filename: path.resolve('log/error.log') }
},
categories: {
// 為info/warn/debug 型別log呼叫info輸出源 error/fatal 呼叫error輸出源
default: { appenders: ['info'], level: 'info' },
info: { appenders: ['info'], level: 'info' },
warn: { appenders: ['info'], level: 'warn' },
debug: { appenders: ['info'], level: 'debug' },
error: { appenders: ['error'], level: 'error' },
fatal: { appenders: ['error'], level: 'fatal' },
}
});
// 匯出5種類型的 logger
module.exports = {
debug: (...params) => log4js.getLogger('debug').debug(...params),
info: (...params) => log4js.getLogger('info').info(...params),
warn: (...params) => log4js.getLogger('warn').warn(...params),
error: (...params) => log4js.getLogger('error').error(...params),
fatal: (...params) => log4js.getLogger('fatal').fatal(...params),
}
複製程式碼
/* 在fetch.js中是喲logger */
const nodeFetch = require('node-fetch')
const logger = require('./logger.js')
module.exports = ({url, method, data = {}}) => {
// 加入請求日誌
logger.info('請求url:', url , method||'get', JSON.stringify(data))
// get請求 將引數拼到url
url = method === 'get' || !method ? "?" + Object.keys(data).map(item => `${item}=${data[item]}`).join('&') : url;
return nodeFetch(url, {
method: method || 'get',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
}).then(res => res.json())
}
<!-- 日誌列印 -->
[2019-01-09T17:34:11.404] [INFO] info - 請求url: https://www.easy-mock.com/mock/5c35a2a2ce7b4303bd93fbda/example/todolist post {"name":"ott"}
複製程式碼
注: 僅共學習參考,生產配置自行斟酌!轉載請備註來源!