nodejs搭建多頁面服務端渲染
技術點javascript
項目源碼 git clone https://gitee.com/wjj0720/nod...
運行css
服務端渲染概念: 是指,瀏覽器向服務器發出請求頁面,服務端將準備好的模板和數據組裝成完整的HTML返回給瀏覽器展現
一、前端後端分離html
早在七八年前,幾乎全部網站都使用 ASP、Java、PHP作後端渲染,隨着網絡的加快,客戶端性能提升以及js自己的性能提升,咱們開始往客戶端增長更多的功能邏輯和交互,前端再也不是簡單的html+css更多的是交互,前端頁在這是從後端分離出來「先後端正式分家」
二、客戶端渲染前端
隨着ajax技術的普及以及前端框架的崛起(jq、Angular、React、Vue) 框架的崛起,開始轉向了前端渲染,使用 JS 來渲染頁面大部份內容達到局部刷新的做用
優點java
缺點node
三、如今服務端渲染webpack
爲了解決上面客戶端渲染的缺點,然先後端分離後必不能合,若是要把先後端部門合併,拆掉的確定是前端部門
如今服務端渲染的特色git
優點github
四、前、後端渲染相關討論參考web
確保你安裝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中間件的使用
/* *我向項目目錄下加入兩個準備好的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 %}
目標: 處理頁面jscssimg等資源引入
依賴
> *相關插件使用 查看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"}
注: 僅共學習參考,生產配置自行斟酌!轉載請備註來源!