【实战】webpack4 + ejs + egg 多页应用项目最终解决方案

前言

Github 完整项目地址html

很久都没有写过文章了,以前写过一篇 《【实战】webpack4 + ejs + express 带你撸一个多页应用项目架构》,发布后我发现你们对于 “简化多页应用开发流程” 这块需求强烈,而且,随着我将上一篇文章中介绍的多页开发模式运用到实际项目中后,发现仍是存在一些缺陷的。其中痛点之一就是,使用 express 做为后台开发框架。前端

express 当然不错,可是如今开发讲究效率,所谓伸手就来开箱即用,这么一对比,express 仍是偏向底层,做为服务端框架,不少东西仍是要本身费心费神找插件 install 看文档。因而,这一次我准备使用更上层的 egg 做为替代框架。node

虽然是上一版的进化版,可是不少主要的实现思路是没有变的,想要详细了解的朋友推荐先看下上一篇 《【实战】webpack4 + ejs + express 带你撸一个多页应用项目架构》,这篇只作关键步骤分析,详细代码可见 Github 完整项目地址jquery

项目结构

目录乍一看彷佛有点多,不要紧都是唬人的,最重要的几个目录我已在截图上标出,咱们能够展开看下主要目录的详细目录结构:webpack

egg 层

项目结构介绍完,下面就要开始改造以前的代码了,但是这么多代码从哪里动手呢?咱们此次主要目的就是将 express 换成 egg ,那固然是从 egg 开始着手改造。ios

改造以前,咱们还须要明白最重要的两个问题,这两个问题一旦被解决,能够说整个项目的改造也完成的差很少了。哪两个问题呢?nginx

  1. 做为一个服务端框架 egg 要怎样与 webpack 结合?
  2. 使用 ejs 做为模板引擎,要怎样在 dev 环境和 prod 环境正确将 ejs 渲染成 html 并显示在页面上?

egg + webpack

在动手处理 egg 层以前,咱们须要先去官方文档上了解一下这个框架。 因为是阿里旗下产品,因此框架自己的稳定性、生态建设程度和文档的友好性确定是有保证的。git

egg 是一款基于 koa 开发的 “企业级应用框架”,简单理解就是在 koa 上又封装了一层,把什么 requestresponse 以及相关的一切操做方法都简化封装了,让普通开发者能更容易的使用,将更多的精力放在 996 啊不是,是业务开发上,就是所谓的伸手就来。angularjs

egg 奉行 “约定优于配置” 的原则,这一点在和 express 一对比就立马体现出来。express 就约束程度而言和 jquery 差很少,随便写。心之所向,哪里都是 router ,至于 middlewareservicecontroller,那是什么东西??es6

对于 egg 来讲就不是这样,它牺牲了自由性,取而代之的是更加统一的写法:业务代码写到 controller 里,中间件写到 middleware 里,sql 写到 service,其他的插件和配置也有统一的入口,否则它就跑不起来。加之又有强大插件生态加持,灵活性也是不弱的。

egg-webpack 做为 egg 生态支持的 webpack 插件,直接就能够 npm install 一把梭。梭的时候注意,这个东西是 devDependencies,不要梭到 dependencies 里面。

开启插件

安装完成之后,须要写入 /config/plugin.js 的插件配置里,设置为 true 开启插件:

/** @type Egg.EggPlugin */
module.exports = {
  webpack: { // 开发环境,开启 egg-webpack 插件
    enable: process.env.NODE_ENV === 'development',
    package: 'egg-webpack',
  },

  ejs: {
    enable: true,
    package: 'egg-view-ejs',
  },

  static: {
    enable: true,
    package: 'egg-static',
  },
};

复制代码

至于其余两个 egg-view-ejsegg-static 你也看到了,一个是 ejs 的模板引擎插件,一个是静态资源插件,都梭过来。

配置插件所需的 webpack 配置文件

上面一步将插件安装并开启后,下面须要告诉 egg-webpack 去哪里找到原生 webpack 配置文件。

打开 /config/config.local.js 写入以下代码:

/* eslint valid-jsdoc: "off" */
 'use strict';

const path = require('path');

/** * @param {Egg.EggAppInfo} appInfo app info */
module.exports = appInfo => {
  /** * built-in config * @type {{}} **/
  const config = exports = {};

  // add your middleware config here
  config.middleware = [];

  // 开发环境下须要开启 webpack 编译
  config.webpack = {
    // port: 9000, // port: {Number}, default 9000. webpack dev server 的默认端口,默认为 9000,开启热更新时 websocket 的自动请求端口
    webpackConfigList: [ require('../build/webpack.dev.config') ],
  };

  // 开发环境下,将 egg-static 静态资源转发目录由默认的 /app/public 改成 /src/static (具体的转发地址能够自行定义)
  config.static = {
    prefix: '/public/',
    dir: path.join(appInfo.baseDir, 'src/static'),
  };

  // add your user config here
  const userConfig = {
    // myAppName: 'egg',
  };

  return {
    ...config,
    ...userConfig,
  };
};

复制代码

注意: egg-webpack 只有在开发环境下才须要开启,生产环境直接在 package.json 里配置 build 脚本就好

"build": "cross-env NODE_ENV=production webpack --config ./build/webpack.prod.config.js",
复制代码

egg 会自动根据 package.json 的脚本命令找到合适的配置文件,例如,开发模式下会找到 /config/config.default.js/config/config.local.js 文件进行合并;生产环境下会找到 /config/config.default.js/config/config.prod.js 文件进行合并。

至于 /build 里的 webpack 配置信息,前一篇文章已经详细说明,这里就不过多赘述了。

上述代码中,还有一块比较重要的配置:egg-static 的配置,config.static 的配置将前缀为 /public/ 的请求标记为静态资源请求,所有转发至 /src/static 目录下。

ejs 模板的获取和渲染

其实,如何在开发环境下获取到 ejs 模板而且将数据合成上去渲染成浏览器可以识别的 html 而后返回,才是真正的灵魂步骤。

开启 ejs 配置

这个在 egg-webpack 那块的 /config/plugin.js 就说过了。

配置 ejs 视图引擎

打开 /config/config.default.js 写入以下代码:

/* eslint valid-jsdoc: "off" */
 'use strict';

const path = require('path');

/** * @param {Egg.EggAppInfo} appInfo app info */
module.exports = appInfo => {
  /** * built-in config * @type {Egg.EggAppConfig} **/
  const config = exports = {};

  // use for cookie sign key, should change to your own and keep security
  config.keys = appInfo.name + '_1599807210902_4670';

  // add your middleware config here
  config.middleware = [];

  config.view = {
    mapping: {
      '.ejs': 'ejs',
    },
    defaultViewEngine: 'ejs',
  };

  // add your user config here
  const userConfig = {
    // myAppName: 'egg',
  };

  return {
    ...config,
    ...userConfig,
  };
};
复制代码

其中 config.view 用于配置模板文件后缀和默认模板引擎。

做为开发环境和生产环境都须要的代码片断,所以要写入 /config/config.default.js 配置文件中。

egg 服务端代码编写

在开启了 egg-view-ejs 相关配置后,咱们要开始进行 egg 的业务代码编写。

首先配置 /app/router.js 文件:

'use strict';

/** * @param {Egg.Application} app - egg application */
module.exports = app => {
  const { router, controller } = app;
  router.get('/', controller.home.index);
  router.get('/welcome', controller.welcome.index);
};

复制代码

好了,如今 //welcome 两个请求将被 egg 转发到对应的 controller 层进行处理,控制器通过数据的请求组装和处理,最后会给页面返回出一个可以渲染的 html ,下面咱们看看控制器作了什么。

因为此项目是个模板框架,后端代码并不会涉及到数据库和中间件,所以不须要 middlewareservice,不过若是你想以此为起点进行二次项目开发,这两个几乎是必不可少的。

因为 /app 中并不会直接存放原始的前端代码,全部的 es六、样式和模板文件最后都会被 webpack 编译成静态资源塞入其中,所以 /app/public/app/view 在初始状态下应该是空的。相似与下图

控制器文件以 /controller/home.js 为例分析:

const Controller = require('egg').Controller;
const { render } = require('../utils/utils.js');

class HomeController extends Controller {
  async index() {
    const { ctx } = this;
    await render(ctx, 'home.ejs', { title: '首页' });
    // ctx.render('home.ejs', { title: '首页' });
  }
}

module.exports = HomeController;

复制代码

能够看到自己代码很是简单,页面渲染的重点在于 render 方法,咱们看看/app/utils/utils.js 文件

神奇的 render

const axios = require('axios');
// const ejs = require('ejs');
const CONFIG = require('../../build/config.dev');
const isDev = process.env.NODE_ENV === 'development';

function getTemplateString(filename) {
  return new Promise((resolve, reject) => {
    axios.get(`http://localhost:${CONFIG.PORT}${CONFIG.PATH.PUBLIC_PATH}${CONFIG.DIR.VIEW}/${filename}`).then(res => {
      resolve(res.data);
    }).catch(reject);
  });
}

/** * render 方法 * @param ctx egg 的 ctx 对象 * @param filename 须要渲染的文件名 * @param data ejs 渲染时须要用到的附加对象 * @return {Promise<*|undefined>} */
async function render(ctx, filename, data = {}) {
  // 文件后缀
  const ext = '.ejs';
  filename = filename.indexOf(ext) > -1 ? filename.split(ext)[0] : filename;
  try {
    if (isDev) {
      const template = await getTemplateString(`${filename}${ext}`);
      ctx.body = await ctx.renderString(template, data);
    } else {
      await ctx.render(`${filename}${ext}`, data);
    }
  } catch (e) {
    return Promise.reject(e);
  }
}

module.exports = {
  getTemplateString,
  render,
};

复制代码

能够看到 render 函数的内部实现逻辑:

  1. 若是是生产环境,那就很是简单,只须要使用数据,而后用 egg 提供的 ctx.render 渲染出指定的模板文件就能够了
  2. 若是是开发环境,则须要先请求自身 http://localhost:7001/public/view/*.ejs 获取到 ejs 的源文件字符串,而后使用 egg 提供的 ctx.renderString 将其渲染到页面上。

关于如何获取模板这一问题,我也看过不少老师的方法,其中一种就是调用 webpack 相关 API 直接一把揪出底层的 memory 内存文件,而后手动调用 js 编译一顿操做猛如虎,最后把它渲染出来,龟龟~ 反正我是看了半天没有学会,并且看代码量感受工做量不菲且要对 webpack 的编译原理研究颇深,方可有所建树。若是你们有兴趣也能够探究探究。

**注意:**这里有一处很是有意思的地方。你们仔细想一下就会发现不对:咱们在 egg-static 中配置的静态资源映射路径是前缀为 /public/ 的资源请求全都转发到 /src/static 下,可是这个 /public/view/*.ejs 文件的原资源路径是在 /src/view 里的,这是怎么映射过去的??

其实除了 egg-static 配置的静态资源映射,webpack 本身也有一层资源映射,而我此处 webpack.output.publicPath 写的恰好也是 /public/ ,就是说,webpack 编译并将文件生成到内存中的时候,内存的访问地址前缀也须要加上 /public/;而这个 /public/view/*.ejs 文件访问到的正是 webpack 内存中的资源文件。

我的感受开发环境中的静态资源的访问模式是:到 egg-staticwebpack 配置的不一样地址下去找,找到哪一个就返回哪一个。

固然,生产环境下的静态资源访问因为不会有 webpack 直接参与,就不会存在这个问题了,你可使用 egg-static 配置在同项目下,也可使用 nginx 跨项目进行静态资源转发配置。

隐藏的细节彩蛋

写到这里,基本项目已经几乎完成了。剩下还有一些细节须要注意,我写在这里,提醒你们也提醒本身:

图片资源路径

咱们若是在 ejs 中写入图片等静态资源,有两种方式:

  1. /public/ 前缀这种绝对路径的手法,这种方法,须要注意的是:egglocal 配置文件 /config/config.local.js 中改写了 egg-static 的静态资源指向为 /src/static/。因此在 dev 环境图片资源是可以正常访问到的。可是因为生产环境下的 egg-static 的静态资源指向默认是 /app/public ,而且绝对路径的图片引用形式不会被被 webpack 识别处理,因此必定要保证生产环境下 /app/public 文件夹下有该图片资源,不然就是 404 资源请求。若是使用这种图片引用方式,推荐使用 copyWebpack 之类的插件作生产环境的静态资源的拷贝处理。
  2. 写成 ../ 的相对路径形式,相对路径的请求形式,是可以正常被 webpack 识别处理和复制的,因此并不须要开发者作额外处理。只是,因为 ejs 是由 includes 功能的,有时候咱们可能会引入一些公用的 ejs 代码块,而这些代码块中颇有多是有图片等引用资源的。这个时候要注意,因为这块是 includes 的文件,最后 includes 文件会被拼接到主文件中,而后再丢给 html-loader 解析,因此这块的图片路径须要写主文件下的相对路径,否则就找不到图片。

如上图,两种写法得到的图片明显是不同的,上面一种未通过 webpack 打包,下面的明显被 webpack 处理过了。

热更新

关于热更新,这一版和上一版不太同样,因此有些地方须要修改一下:

首先是 /build/webpack.base.config.js 文件:

module.exports = {
	// ...
    
  entry: (filepathList => {
    const entry = {};
    filepathList.forEach(filepath => {
      const list = filepath.split(/(\/|\/\/|\\|\\\\)/g);
      const key = list[list.length - 1].replace(/\.js/g, '');
      // 若是是开发环境,才须要引入 hot module
      entry[key] = isDev ?
        [
          filepath,
          // 这边注意端口号,之间安装的 egg-webpack,会启动 dev-server,默认端口号为 9000
          `webpack-hot-middleware/client?path=http://127.0.0.1:9000/__webpack_hmr&noInfo=false&reload=true&quiet=false`,
        ] : filepath;
    });
    return entry;
  })(glob.sync(resolve(__dirname, '../src/js/*.js'))),
    
    // ...
};
复制代码

这边的 entry 入口除了 filepath ,还须要把 webpack-hot-middleware 加上,并把相关配置以 queryString 的方式拼接,最重要的配置就是 path=http://127.0.0.1:9000/__webpack_hmr,这句是指定了热更新的 websocket 的地址的,因为 egg 自己启动的服务和 webpack-dev-server 启动的服务并不同,这里不配置的话,默认热更新会去请求 7001 端口,也就是开发端口,那确定是拿不到东西的。

不知道你们有没有注意到以前 /config/config.local.js 中的 webpack 配置,里面有一项能够设置 webpack-dev-server 的端口号:

// 开发环境下须要开启 webpack 编译
  config.webpack = {
    // port: 9000, // port: {Number}, default 9000. webpack dev server 的默认端口,默认为 9000,开启热更新时 websocket 的自动请求端口
    webpackConfigList: [ require('../build/webpack.dev.config') ],
  };
复制代码

若是不想用默认的 9000 ,更改这个 port 也是能够的,只不过改了默认端口也要记得把 webpack 热更新配置里的默认端口也同时改掉。

最后,webpack-hot-module 原生是不支持模板文件的热更新的,这点在上一篇中也说明了。因此每一个前端页面的 js 入口文件中须要加上:

if (process.env.NODE_ENV === 'development') {
  // 在开发环境下,使用 raw-loader 引入 ejs 模板文件,强制 webpack 将其视为须要热更新的一部分 bundle
  require('raw-loader!../view/home.ejs');
  if (module.hot) {
    module.hot.accept();
    /** * 监听 hot module 完成事件,从新从服务端获取模板,替换掉原来的 document * 这种热更新方式须要注意: * 1. 若是你在元素上以前绑定了事件,那么热更新以后,这些事件可能会失效 * 2. 若是事件在模块卸载以前未销毁,可能会致使内存泄漏 * 上述两个问题的解决方式,能够在 document.body 内容替换以前,将事件手动解绑。 */
    module.hot.dispose(() => {
      const href = window.location.href;
      axios.get(href).then(res => {
        document.body.innerHTML = res.data;
      }).catch(e => {
        console.error(e);
      });
    });
  }
}
复制代码

注意:上面这一段热更新代码是不能拆成函数去引入使用的,没有用,我试过,只能在每一个页面的入口文件中 ctrlCV ,固然若是你以为麻烦,彻底能够不这么作,顶多就是模板文件更改不会热更新而已,本身刷新一下也不麻烦,效果同样。

总结

记得我在从业时的第一家公司的第一份工做,就是改写官网。那个官网是用前人写的 gulp 编译脚本打包的,而 gulp 对于高阶的 ES6+ 语法的支持简直就是一塌糊涂;更糟的是因为纯前端代码没有 node 层支持,只能靠 ajax 来获取数据。在那个先后端分离尚未彻底推行的时代,在那个 angularjs 脏检查疯狂遍历的年代,前端写代码还要开 eclipse ,等后端兄弟的服务起来才能动手。

我从那时便想,若是有一天,前端开发多页应用能像拉屎同样简单。

那该多好。


完整项目地址能够查看个人 Github 完整项目地址 ,喜欢的话给个 Star⭐️ ,多谢~ 你的点赞,将是我持续输出的动力😃❤️


往期内容推荐

  1. 【实战】webpack4 + ejs + express 带你撸一个多页应用项目架构
  2. 【基础】HTTP、TCP/IP 协议的原理及应用
  3. 完全弄懂节流和防抖
  4. 浏览器下的 Event Loop
  5. 面试官:说说做用域和闭包吧
  6. 面试官:说说执行上下文吧
  7. 面试官:说说原型链和继承吧
  8. 面试官:说说 JS 中的模块化吧
  9. 面试官:说说 let 和 const 吧
相关文章
相关标签/搜索