写点什么

node 服务端渲染

作者:编程江湖
  • 2021 年 12 月 06 日
  • 本文字数:9550 字

    阅读完需:约 31 分钟

简介

nodejs 搭建多页面服务端渲染

  • 技术点 koa 搭建服务 koa-router 创建页面路由 nunjucks 模板引擎组合 htmlwebpack 打包多页面 node 端异步请求服务端日志打印

  • 运行 npm inpm 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 问题解决

二、 项目开始

确保你安装 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

  /** 新增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

  /*    *我向项目目录下加入两个准备好的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 buildnpm 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

  /*上面的代码中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

  /* 在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"}
复制代码


用户头像

编程江湖

关注

IT技术分享 2021.11.23 加入

还未添加个人简介

评论

发布
暂无评论
node服务端渲染