node服務端渲染(完整demo)

收藏待读

node服務端渲染(完整demo)

服務端渲染概念: 是指,瀏覽器向服務器發出請求頁面,服務端將準備好的模板和數據組裝成完整的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返回不同頁面內容

/** 新增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 nunjucksnunjucks中文文檔
/*
    *我向項目目錄下加入兩個準備好的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
  */
複製代碼
  
  
  
    
    {{ title }}
  
  
    
    {% block content %}
    {% endblock %}
  
  


  
  
複製代碼
  
  {% extends "../_layout/layout.html" %}
  {% block content %}
    
    {% include "../_component/nav.html" %}
    

待辦事項

    {% for item in todoList %}
  • {{item.name}} ---> {{item.time | timeFormate}}
  • {% endfor %}
{% endblock %} {% extends "../_layout/layout.html" %} {% block content %} {% include "../_component/nav.html" %} 用戶中心 {% endblock %} 複製代碼

第五步 靜態資源處理

目標: 處理頁面jscssimg等資源引入

  • 依賴
    1. 用webpack打包靜態資源 npm i webpack webpack-cli -D
    2. 處理js npm i @babel/core @babel/preset-env babel-loader -D
    3. 處理less npm i css-loader less-loader less mini-css-extract-plugin -D
    4. 處理文件 npm i file-loader copy-webpack-plugin -D
    5. 處理html npm i html-webpack-plugin -D
    6. 清理打包文件 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
  */
複製代碼
  
  
  
    
    
    
    {{title}}
  
  
    
    {% block content %}
    {% endblock %}
  
  
複製代碼
/* 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);
  // 這裡只做打包演示代碼 不具任何意義
複製代碼
  // 引入公共樣式
  @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')
      }))
    ]
  });

  
  "scripts": {
    "start": "node bin/www.js",
    "build": "webpack --env.production --config config/webpack.config.js"
  }

  運行 npm run build 後生成 dist views/_layout 兩個目錄
複製代碼
  
  
  
    
    
    
    {{title}}
    
  
  
    {% block content %}
    {% endblock %}
    
  
  
複製代碼
  
  {% extends "../_layout/home/home.html" %}
  {% block content %}
    
    {% include "../_component/nav.html" %}
    

待辦事項

    {% for item in todoList %}
  • {{item.name}} ---> {{item.time | timeFormate}}
  • {% endfor %}
背景圖
{% 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
/**項目中文件發生變動 需要重啟服務才能看到效果是一件蛋疼的事,故需要實時監聽變動 */
  
    "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請求接口數據 填充模板

/*上面的代碼中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
複製代碼

第八步 日誌打印

目標: 使程序運行可視

/* 在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"}

複製代碼

注: 僅共學習參考,生產配置自行斟酌!轉載請備註來源!

原文 : 稀土掘金

相關閱讀

免责声明:本文内容来源于稀土掘金,已注明原文出处和链接,文章观点不代表立场,如若侵犯到您的权益,或涉不实谣言,敬请向我们提出检举。