资讯专栏INFORMATION COLUMN

node服务端渲染(完整demo)

cheukyin / 1541人阅读

摘要:简介搭建多页面服务端渲染技术点搭建服务创建页面路由模板引擎组合打包多页面端异步请求服务端日志打印项目源码运行一现代服务端渲染的由来服务端渲染概念是指,浏览器向服务器发出请求页面,服务端将准备好的模板和数据组装成完整的返回给浏览器展示前端后

简介
nodejs搭建多页面服务端渲染

技术点

koa 搭建服务

koa-router 创建页面路由

nunjucks 模板引擎组合html

webpack打包多页面

node端异步请求

服务端日志打印

项目源码 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、前、后端渲染相关讨论参考

知乎问答:为什么现在又流行服务器端渲染html

精读前后端渲染之争

服务端渲染 vs 客户端渲染

二、 项目开始
确保你安装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
  */
  
  
  
  
    
    {{ 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等资源引入

依赖

用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
  */
  
  
  
  
    
    
    
    {{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请求接口数据 填充模板

依赖 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://www.ucloud.cn/yun/100863.html

相关文章

  • koa-cola:只需一个react组件,同时支持单页应用(SPA)和服务渲染(SSR)

    摘要:是一个基于和的服务器端和浏览器端的的前后端全栈应用框架。是的组件,并且会进行数据初始化不但可以支持的数据初始化,还可以合并和的,使用同一个,和的无缝结合。 koa-cola是一个基于koa和react的服务器端SSR(server side render)和浏览器端的SPA(single page application)的web前后端全栈应用框架。 koa-cola使用typescr...

    XGBCCC 评论0 收藏0
  • vue服务渲染demo将vue-cli生成的项目转为ssr

    摘要:无需使用服务器实时动态编译,而是使用预渲染方式,在构建时简单地生成针对特定路由的静态文件。与可以部署在任何静态文件服务器上的完全静态单页面应用程序不同,服务器渲染应用程序,需要处于运行环境。更多的服务器端负载。 目录结构 -no-ssr-demo 未做ssr之前的项目代码用于对比 -vuecli2ssr 将vuecli生成的项目转为ssr -prerender-demo 使用prer...

    whinc 评论0 收藏0
  • 从零开始搭建一个vue-ssr(上)

    摘要:从零开始搭建一个背景是什么全拼是,服务端渲染。大家不妨可以打开一些页面或者一些公司的网站,查看源代码,你会发现,也是有这个标记。这时候,我们发现页面的路由切换生效了,并且不同页面的源代码也不一样了。从零开始搭建一个下项目源码 从零开始搭建一个vue-ssr 背景 What?SSR是什么? SSR全拼是Server-Side Rendering,服务端渲染。 所谓服务端渲染,指的是把...

    Winer 评论0 收藏0
  • 前后分离的总结

    摘要:如何去解决这些问题前后端分离大部分的互联网公司都分成了前端团队和后端团队。方案一采用架构业界很多公司会采用,单页应用的架构,这种架构是天然的前后端分离的。方案二淘宝的大前端方案中途岛上图是淘宝基于的前后端分离分层,以及的职责范围。 我们遇到了什么问题? 1.前端无法调试后端未完成的 API:如果后端同学还没有完成 API 开发,那么前端同学就不能对这个 API 进行开发。之前我们都是在...

    enrecul101 评论0 收藏0
  • 理解vue ssr原理,自己搭建简单的ssr框架

    摘要:前言大多数项目要支持应该是为了考虑,毕竟对于应用来说,搜索引擎是一个很大的流量入口。引入是一个构建客户端应用的框架,即组件是在浏览器中进行渲染的。由于服务端渲染要用做中间层,所以部署项目时,需要处于运行环境。 前言 大多数Vue项目要支持SSR应该是为了SEO考虑,毕竟对于WEB应用来说,搜索引擎是一个很大的流量入口。Vue SSR现在已经比较成熟了,但是如果是把一个SPA应用改造成S...

    Riddler 评论0 收藏0

发表评论

0条评论

cheukyin

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<