【实战】webpack4|【实战】webpack4 + ejs + egg 多页应用项目最终解决方案
前言
Github 完整项目地址
好久都没有写过文章了,之前写过一篇 《【实战】webpack4 + ejs + express 带你撸一个多页应用项目架构》,发布后我发现大家对于 “简化多页应用开发流程” 这块需求强烈,并且,随着我将上一篇文章中介绍的多页开发模式运用到实际项目中后,发现还是存在一些缺陷的。其中痛点之一就是,使用 express
作为后台开发框架。
express
固然不错,但是现在开发讲究效率,所谓伸手就来开箱即用,这么一对比,express
还是偏向底层,作为服务端框架,很多东西还是要自己费心费神找插件 install
看文档。于是,这一次我准备使用更上层的 egg
作为替代框架。
虽然是上一版的进化版,但是很多主要的实现思路是没有变的,想要详细了解的朋友推荐先看下上一篇 《【实战】webpack4 + ejs + express 带你撸一个多页应用项目架构》,这篇只做关键步骤分析,详细代码可见 Github 完整项目地址项目结构
文章图片
1603243229(1).jpg 目录乍一看似乎有点多,没关系都是唬人的,最重要的几个目录我已在截图上标出,我们可以展开看下主要目录的详细目录结构:
文章图片
1603243252(1).jpg egg 层 项目结构介绍完,下面就要开始改造之前的代码了,可是这么多代码从哪里动手呢?我们这次主要目的就是将
express
换成 egg
,那当然是从 egg
开始着手改造。改造之前,我们还需要明白最重要的两个问题,这两个问题一旦被解决,可以说整个项目的改造也完成的差不多了。哪两个问题呢?
- 作为一个服务端框架
egg
要怎样与webpack
结合? - 使用
ejs
作为模板引擎,要怎样在dev
环境和prod
环境正确将ejs
渲染成html
并显示在页面上?
在动手处理
egg
层之前,我们需要先去官方文档上了解一下这个框架。 由于是阿里旗下产品,所以框架本身的稳定性、生态建设程度和文档的友好性肯定是有保证的。egg
是一款基于 koa
开发的 “企业级应用框架”,简单理解就是在 koa
上又封装了一层,把什么 request
、response
以及相关的一切操作方法都简化封装了,让普通开发者能更容易的使用,将更多的精力放在 egg
奉行 “约定优于配置” 的原则,这一点在和 express
一对比就立马体现出来。express
就约束程度而言和 jquery
差不多,随便写。心之所向,哪里都是 router
,至于 middleware
、service
和 controller
,那是什么东西??对于
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',
},
};
【【实战】webpack4|【实战】webpack4 + ejs + egg 多页应用项目最终解决方案】至于其他两个
egg-view-ejs
和 egg-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
配置信息,前一篇文章已经详细说明,这里就不过多赘述了。上述代码中,还有一块比较重要的配置:ejs 模板的获取和渲染egg-static
的配置,config.static
的配置将前缀为/public/
的请求标记为静态资源请求,全部转发至/src/static
目录下。
其实,如何在开发环境下获取到
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
用于配置模板文件后缀和默认模板引擎。作为开发环境和生产环境都需要的代码片段,因此要写入egg 服务端代码编写 在开启了/config/config.default.js
配置文件中。
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
,下面我们看看控制器做了什么。由于此项目是个模板框架,后端代码并不会涉及到数据库和中间件,因此不需要middleware
和service
,不过如果你想以此为起点进行二次项目开发,这两个几乎是必不可少的。
由于[图片上传失败...(image-83f039-1603243193542)]/app
中并不会直接存放原始的前端代码,所有的 es6、样式和模板文件最后都会被 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 = https://www.it610.com/article/{}) {
// 文件后缀
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
函数的内部实现逻辑:- 如果是生产环境,那就非常简单,只需要使用数据,然后用
egg
提供的ctx.render
渲染出指定的模板文件就可以了 - 如果是开发环境,则需要先请求自身
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-static
和 webpack
配置的不同地址下去找,找到哪个就返回哪个。当然,生产环境下的静态资源访问由于不会有
webpack
直接参与,就不会存在这个问题了,你可以使用 egg-static
配置在同项目下,也可以使用 nginx
跨项目进行静态资源转发配置。隐藏的细节彩蛋 写到这里,基本项目已经几乎完成了。剩下还有一些细节需要注意,我写在这里,提醒大家也提醒自己:
图片资源路径
我们如果在
ejs
中写入图片等静态资源,有两种方式:-
/public/
前缀这种绝对路径的手法,这种方法,需要注意的是:egg
的local
配置文件/config/config.local.js
中改写了egg-static
的静态资源指向为/src/static/
。所以在 dev 环境图片资源是能够正常访问到的。但是由于生产环境下的egg-static
的静态资源指向默认是/app/public
,并且绝对路径的图片引用形式不会被被webpack
识别处理,所以一定要保证生产环境下/app/public
文件夹下有该图片资源,否则就是 404 资源请求。如果使用这种图片引用方式,推荐使用copyWebpack
之类的插件做生产环境的静态资源的拷贝处理。 - 写成
../
的相对路径形式,相对路径的请求形式,是能够正常被 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 = https://www.it610.com/article/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?? ,多谢~ 你的点赞,将是我持续输出的动力??
推荐阅读
- 宽容谁
- 我要做大厨
- 增长黑客的海盗法则
- 画画吗()
- 2019-02-13——今天谈梦想()
- 远去的风筝
- 三十年后的广场舞大爷
- 叙述作文
- 20190302|20190302 复盘翻盘
- 学无止境,人生还很长