天天看点

聊聊 Velocity

作者:闪念基因

1

导读

近期一次需求开发涉及到了Java Veloctiy,由于当前vue项目无法在本地编译运行velocity,且开发成本过高,如果需要调试则要发布后才能看到效果。之前有类似的案例,遂尝试继续采用这种方式解决,但是通过比对认为该方案迁移到本项目中的成本较高,且需要改动大量配置文件。

基于以上,我们便尝试将veloctiy本地工程化,尝试跟vue和webpack的结合,实现热更等开发常用需求。

本文把velocity工程化的心路历程记录下来,主要给大家描述我们在解决问题过程中的一些感悟,同时文中也详细介绍了具体方案。

2

背景

本文提到的“Velocity”指的是Java Velocity,后面全部以“velocity”代称。

本文不对Velocity基础概念做详细介绍,默认阅读本文的读者具备velocity基础相关知识。如果确有不清楚的地方,可依据参考文献提供的文档或搜索引擎进一步了解。

3

现状

行业现状:

随着前端技术的发展,页面的开发逐步形成了前后端分离的开发模式,形成了以vue、react等主流前端框架为主的MVVM模式。

回首再面对这些基于Velocity的旧系统,无论是后端还是前端人员维护,都会存在诸多问题:

1. RD不熟悉前端开发模式,需要花大量时间学习前端js和框架。

2. Velocity渲染依赖Java环境,FE需要花费大量精力学习Maven工程、环境配置,且前端MVC框架版本老,开发效率低。

内部业务现状:

原有项目涉及面较广,基于Velocity开发的情况分散在不同业务系统中,随着业务需求的迭代,维护成本越来越高。解决方案一般是针对高频迭代的业务模块进行前后端分离开发,但这样会带来新的问题:

1. 使用新项目重构原有项目成本高

2. 覆盖面不广

3. 周期长

4. 重构本身不能为业务带来更高的价值

如下图,我们在本地开发velocity模版页面时,目前基本处于盲写阶段,不能即时查看,造成开发时间大量浪费在部署和联调中。

聊聊 Velocity

基于以上问题,我们团队在维护的时候探索一种新的方式,旨在提升开发效率,解决本地编译运行velocity模版。

4

实现

以我们团队维护的本地服务大类页为例。

该项目基于vue,近期一次需求迭代中,需要把部分页面内容前置到服务端交由java预渲染,然而本地开发目前不支持模拟java 环境运行velocity模版。因此需把velocity和vue进行整合,降低开发成本,让FE无痛开发,不论是编译还是打包上线都跟普通vue项目无差异。

基于此目标,需要实现一个velocity+mvc+mvvm的混合架构,该架构需具备两个能力:

1. 把velocity模版页面进行本地渲染,然后跟vue的index.html模版页面进行整合

2.让vue把dom挂载到整合后的模版页面中

现在思考第一个问题,如何实现上面的架构?回顾刚才我们描述的架构需要具备的能力,可以分为两段:

第一段是velocity+mvc,上线后由服务端执行

第二段是mvvm,上线后由客户端执行

问题进一步明确,基于以上两段,我们需要做的事可以分为3步:

1. 本地实现一套velocity模版渲染引擎,实现velocity页面的渲染和mock数据填充

2. 把渲染好的velocity模版整合到vue的模版文件index.html中

3. 把整合了velocity页面的vue模版文件index.html交给vue进行dom挂载

好了,到此,我们不再停留在理论层面,而是需要考虑怎么实现以上三步。由于后面步骤依赖前面步骤的产物,所以我们按顺序实现。

现在思考第二个问题,velocity渲染引擎应该具备什么能力?

根据第一步的描述,该引擎需要在前端本地编译运行velocity模版页面,同时支持mock数据的渲染。

总结一下就是两点:1 渲染velocity 2 支持mock数据

目前本地渲染velocity的主流方式是在本地运行一套java服务,但是当前需求开发伙伴对java不太了解,并且一些配置项对我们来说较为复杂,这与我们在现状中描述的问题不谋而合,这条路不适合我们。换个思路,java能跑服务,js不能吗?答案很明显,可以,是nodejs。虽然对java不熟悉,但是nodejs我们熟啊。

通过调研,发现nodejs有类似java velocity的模版引擎,能运行velocity模版,同时能支持mock数据。接下来尝试把该引擎接入项目中。

下面是接入引擎时尝试的两种方式:

方法一:在vue执行入口文件index.js前启动node服务,然后通过在node服务中使用引擎提供的编译和运行方法把velocity模版页面加载好供后续使用。

方法二:把该引擎写在webpack的loader中,随着webpack的执行逻辑编译velocity。

如果大家对vue本地运行时各个页面加载的顺序有了解,应该能看出来方法一存在问题,我们逐个实现,看看问题在哪里。

方法一具体实现:

以下代码为当时在本地调试时临时编写,仅用于帮助大家理解思路。不具有参考性。核心代码会加注释。

step1: 改造vue入口文件index.js

// setup中启动node服务,编译velocity并生成一个html文件到指定文件夹下
import './setup';
// entry是原有vue的index.js入口文件内容
import './entry';           

step2: 实现setup.js

import Axios from 'axios';


// 该方法向本地启动的node服务发送请求,告诉node服务需要重新编译模版
(() => {
  new Promise((resolve, reject) => {
    Axios.get('initdom')
      .then(() => {
        resolve();
      })
      .catch((err) => {
        reject(err);
      });
  });
})();           

step3: 实现node服务

// node服务在本地手动启动
// 这里需要在全局安装velocityjs包,用于后续命令执行
const http = require('http');
const fs = require('fs');
const { exec } = require('child_process');


const hostname = '127.0.0.1';
const port = 3000;


const server = http.createServer((req, res) => {
  if (req.url === '/initdom') {
    res.statusCode = 200;


    res.setHeader('Content-Type', 'text/plain');
    // 该命令用于执行velocity模版页面,并且生成html文件到执行目录下
    exec('velocity ./index.vm', { encoding: 'utf-8' }, (error, stdout, stderr) => {
      if (error) {
        console.error(error);
      }


      if (stderr) {
        console.error(stderr);
      }


      const isExist = fs.existsSync('模版文件地址');


      if (isExist) {
        console.log('file is exist');
        fs.rmSync('生成html文件地址');
      }


      console.log('file is rewriting');
      fs.writeFileSync('生成html文件地址', stdout);
      console.log('file is rewrited');
      // html文件生成成功后,通知前端,执行后续逻辑
      res.end('模版编译成功');
    });
  } else {
    console.log('url is not correct!!!');
  }
});


server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});           

step4: 本地启动node服务和vue项目

// 启动node服务
node node.js
// 启动vue
npm run dev           

至此,方法一整个流程实现,项目启动后确实也符合我们的预期,但是前文提到的问题在哪里?大家知道,我们平时本地开发中很重要的一个需求是热更新。当我改动mock数据后发现,生成的html文件中的mock数据确实变动了,但是我需要刷新两次浏览器才能在页面中体现出来,也就是说热更生效了,但又没完全生效。

问题在于vue读取html文件的时机早于html文件生成的时机,那么能把html文件生成的逻辑再前置吗?基于方法一很明显不行,而且方法一需要本地启动node服务,通过请求接口的方式实现服务启动,这很不优雅,因此考虑换个方式。

我们知道webpack提供了自定义loader的机制,允许我们创建loader预处理指定的文件。

聊聊 Velocity

vue cli支持通过webpack chain添加新的loader。

聊聊 Velocity

回顾方法一遇到的问题,尝试用webpack loader实现。

方法二具体实现:

下图为整体流程图:

聊聊 Velocity

实现方法二之前我们再思考一个问题,随着项目逐步迭代,可能会对velocity模版文件进行拆分,同时一些外部文件也需要注入到最后的html文件中,而且后续还会逐步把代码进行升级。好消息是velocity提供了#parse和#include能力,配合script和api接口我们能够实现velocity+vue的单文件组件化模式。

下图为文件基本结构:

聊聊 Velocity

基于这个结构,我们搭建一套前端沙盒环境,通过在本地json文件中保存mock服务端的数据,然后开发velocity-loader引擎来解析velocity模版,嵌入到webpack loader中,实时解析文件并注入mock数据到模版中,解决方法一中遇到的热更问题。如下图。

聊聊 Velocity

下面贴上方法二实现的相关代码。

step1: 配置webpack chain

config.module.rule('velocity')
      // 只处理.vm结尾的文件
      .test(/.vm$/)
      .exclude.add(/node_modules/).end()
      .use('html-loader')
      .loader('html-loader')
      .end()


      .use('velocity-loader')
      .loader(path.resolve(__dirname, './v-loader.js'))
      .options({
        basePath: path.join(__dirname, 'src'),
      });           

step2: 实现velocity-loader引擎

这里只写部分核心代码,其他的代码为其他配置项,跟本文关系不大。

module.exports = function (content) {
  const callback = this.async();


  const filePath = this.resourcePath;
  const fileDirPath = path.dirname(filePath);


  watcher = this.addDependency;


  // mock文件名称这里写死,用于描述,实际可进行配置
  const mockPath = path.join(fileDirPath, 'mock.js');
  let mock = {};


  // 判断mock文件是否存在,如果存在则进行监听,实现热更
  if (fs.existsSync(mockPath)) {
    watcher(mockPath);
    delete require.cache[mockPath];
    // eslint-disable-next-line global-require
    mock = require(mockPath);
  }


  // 使用compile解析velocity模版
  content = new Compile(parse(content), {
    escape: false,
  }).render(mock, macros(filePath, {}, mock));


  // 返回html内容给webpack,进行下一步处理
  callback(null, content);
};           
至此,我们解决了本地编译运行velocity模版的问题,也实现了mock数据和热更,提升了开发效率和开发体验,维护成本并没有增加。           

5

总结

其实到现在关于本地编译运行velocity已经有很多方案且大都成熟,只是我们要么直接使用了现有的方案,要么没遇到这种情况,写这篇文章的目的也是想把自己在解决问题时的思路描述出来,做个总结。           

参考文献

[1] velocity官网:https://velocity.apache.org/

[2] velocityjs官网: https://github.com/shepherdwind/velocity.js

[3] webpack 官网:https://www.webpackjs.com/loaders/

[4] vue官网:https://cli.vuejs.org/zh/guide/webpack.html

作者简介

雷鸣生:LBG-FE

来源:微信公众号:58技术

出处:https://mp.weixin.qq.com/s/LqBf6H-6o88tMfl59IRPbw