zoukankan      html  css  js  c++  java
  • node服务端渲染(完整demo)

    简介

    nodejs搭建多页面服务端渲染
    • 技术点

      1. koa 搭建服务
      2. koa-router 创建页面路由
      3. nunjucks 模板引擎组合html
      4. webpack打包多页面
      5. node端异步请求
      6. 服务端日志打印
      项目源码 git clone https://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 */ ```
      <!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>
    
      <!-- 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等资源引入
    • 依赖

      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
      */
    
      <!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 {
         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 两个目录
    
      <!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>
    
      <!-- 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"}
      
      
    注: 仅共学习参考,生产配置自行斟酌!转载请备注来源!

    来源:https://segmentfault.com/a/1190000017832572

  • 相关阅读:
    [源码解析] PyTorch 流水线并行实现 (4)--前向计算
    [源码解析] PyTorch 流水线并行实现 (3)--切分数据和运行时系统
    [源码解析] PyTorch 流水线并行实现 (2)--如何划分模型
    [源码解析] PyTorch 流水线并行实现 (1)--基础知识
    [源码解析] 深度学习分布式训练框架 horovod (21) --- 之如何恢复训练
    [源码解析] 深度学习流水线并行 PipeDream(6)--- 1F1B策略
    [源码解析] 深度学习流水线并行 PipeDream(5)--- 通信模块
    [源码解析] 深度学习流水线并行 PipeDream(4)--- 运行时引擎
    6.耐心——想象力决定生产力
    5.权利——自由不等于免费
  • 原文地址:https://www.cnblogs.com/lalalagq/p/10250616.html
Copyright © 2011-2022 走看看