前端基于gulp后端基于freemarker的工作流程总结

1,496 阅读6分钟

前言

最近在做一个PC端的项目,由于项目需要兼容到IE8,所以从技术选型上采取了公司之前一直沿用的前端基于gulp后端基于freemarker的模式来进行开发。

那么gulp+freemarker这种开发模式的流程到底是怎样的呢?我这边就来简单的分析一下。

一、前端基于gulp

前端技术栈:

  • gulp
  • jquery
  • ajax
  • less...

前端项目结构:

├── README.md                       项目介绍
├── src                             源码目录
│   ├── common 
        ├── less                    公共样式
        ├── js                      公共js
        ├── plugins                 插件
项目公共文件   
│   ├── img                         图片
│   ├── js                          js
│   ├── less                        样式
├── .eslintrc.js                     eslint规则配置
├── package.json                     工程文件
├── gulpfile.js                      配置文件
├── server.js                        本地服务

从目录来看,非常简单,我这边就主要来分析一下gulpfile.jsserver.js

gulpfile.js

熟悉gulp的同学都知道,一般我们会将整个项目两种环境来调用,即开发环境生产环境

开发环境的配置:

var gulp = require("gulp"),
    less = require("gulp-less"),
    clean = require("gulp-clean"),
    header = require("gulp-header");

/**
 * less 编译
 * @return {[type]} [description]
 * 开发环境调用
 */
gulp.task("less", ["cleanCss"], function() {

    gulp.src(['src/less/*.less','src/common/less/*.less'])
        .pipe(plumber({
            errorHandler: errorHandler
        }))
        .pipe(less())
        .pipe(addHeader())
        .pipe(gulp.dest('dist/css'));

});
    
/**
 * js 编译
 * @return {[type]} [description]
 * 开发环境调用
 */
gulp.task('js', ['cleanJs'], function() {
    gulp.src(['src/js/*.js', 'src/common/js/*.js'])
        .pipe(plumber({
            errorHandler: errorHandler
        }))
        .pipe(addHeader())
        .pipe(gulp.dest('dist/js'));

    gulp.src('src/common/plugins/*.js')
        .pipe(gulp.dest("dist/js/plugins"))
})

/**
 * img 输出
 * @return {[type]} [description]
 * 开发环境调用
 */
gulp.task("imgOutput", ["cleanImg"], function(){

    gulp.src('src/img/**/*.*')
        .pipe(gulp.dest("dist/img"))

})

简析上述代码:

在开发环境中我们需要对我们项目的src下的业务less、js、img和common下的公共less、js、img进行编译打包,那么我们就需要借助gulp.task()这个方法来建立一个编译任务。创建完任务以后,我们就需要通过gulp.src()来指向我们需要编译的文件

最后我们再通过gulp.pipe()来创建一个又一个我们需要的管道,如

gulp.pipe(plumber({errorHandler: errorHandler}))

function errorHandler(e) {
    // 控制台发声,错误时beep一下
    gutil.beep();
    gutil.log(e);
}

编译的时候控制台打印错误信息。

gulp.pipe(addHeader())

/**
 * 在文件头部添加时间戳等信息
 */
var addHeader = function() {
    return header(banner, {
        pkg: config,
        moment: moment
    });
};

编译以后在文件的头部加上编译时间

gulp.pipe(gulp.dest('dist/js'))

将编译后的文件输出到dist目录下

生产环境的配置:

var gulp = require("gulp"),
    less = require("gulp-cssmin"),
    clean = require("gulp-uglify");
    header = require("gulp-header")
    
/**
 * css build
 * @return {[type]} [description]
 * 正式环境调用
 */
gulp.task("cssmin", ["cleanCss"], function() {

    gulp.src('src/common/less/all.base.less')
        .pipe(less())
        .pipe(cssmin())
        .pipe(rename({
            suffix: '.min'
        }))
        .pipe(addHeader())
        .pipe(gulp.dest("dist/css"));

    gulp.src('src/less/*.less')
        .pipe(less())
        .pipe(cssmin())
        .pipe(addHeader())
        .pipe(gulp.dest("dist/css"));

});

/**
 * js 编译
 * @return {[type]} [description]
 * 正式环境调用
 */
gulp.task('jsmin', ['cleanJs'], function() {
    gulp.src(['src/js/**/*.js', 'src/common/js/**/*.js'])
        .pipe(plumber({
            errorHandler: errorHandler
        }))
        .pipe(uglify())
        .pipe(addHeader())
        .pipe(gulp.dest('dist/js'));

    gulp.src('src/common/plugins/**/*.js')
        .pipe(uglify({
            mangle: true
        }))
        .pipe(addHeader())
        .pipe(gulp.dest("dist/js/plugins"))
})

关于生产环境的配置其实跟上述的开发环境配置原理差不多,区别将在于生产环境中我们需要借助gulp-cssmin和gulp-uglify将css和js都进行压缩,缩小文件的体积。

这里提一下cleancss和cleanjs的意思,其实就是在我们每一次编译打包的时候将原来已经打包生成css和js都清理调,这样保证我们每次编译打包的代码都是最新的。

gulp.task("cleanCss", function() {
    return gulp.src('dist/css', {
        read: false
    }).pipe(clean());
});

gulp.task("cleanJs", function() {
    return gulp.src('dist/js', {
        read: false
    }).pipe(clean());
});

gulp.task("cleanImg", function() {
    return gulp.src('dist/img', {
        read: false
    }).pipe(clean());
});

开发环境监听

gulp.task("watch", function() {

    livereload.listen();

    // 调用gulp-watch插件实现编译有改动的LESS文件
    gulp.watch(['src/less/*.less','src/common/less/*.less'], function(file) {
        gulp.src(file.path)
            .pipe(plumber({
                errorHandler: errorHandler
            }))
            .pipe(less())
            .pipe(addHeader())
            .pipe(gulp.dest('dist/css'));
    });
    
    gulp.watch(['src/js/**/*.js', 'src/common/js/**/*.js'], function(file) {
        gulp.src(file.path)
            .pipe(gulp.dest("dist/js"))
    });

      // 监听图片改动
    gulp.watch('src/img/**/*.*', function(file){
        gulp.src(file.path)
            .pipe(gulp.dest("dist/img"))
    })

    // 监听有变化的css,js,ftl文件,自动刷新页面
    gulp.watch(['dist/**/*.css', 'dist/**/*.js',  ftlPath]).on('change', livereload.changed);

});

在开发项目的时候我们需要借助gulp.watch()来实时的监听项目中代码的改动,并且通过gulp-livereload这个插件来实时的刷新我们的页面,以提高我们的开发效率。

说完gulpfile.js后我们再来分析一下server.js

server.js

const path = require('path'),
	express = require('express'),
	proxy = require("express-http-proxy"),
	compress = require('compression'),
	app = express(),
	fs = require('fs'),
	config = require('./package.json'),
	projectName = config.name,
	port = process.env.PORT || '9091'

// GZIP压缩
app.use(compress());

// 设置响应头
app.use(function(req, res, next) {
	res.header('X-Powered-By', 'Express');
	res.header('Access-Control-Allow-Origin', '*');
	next();
})

// 当前静态项目的资源访问
app.use('/' + projectName, express.static('dist'));
app.use('/html',  express.static('src/pages'));

// 静态服务器监听
app.listen(port, '0.0.0.0', function() {
	console.log('static server running at ' + port)
})

这里我们通过node中express框架来为我们搭建本地服务,这里重点提一下静态资源项目的访问

通过app.use()方法传入两个参数,其中projectName代表的是我们在package.json中定义项目名称,如下phip_ehr

{
  "name": "phip_ehr",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "gulp && gulp watch",
    "build": "NODE_ENV=production gulp build",
    "server": "node server.js"
  }

第二个参数express.static('dist')意思是将我们的服务代理到编译打包后的dist文件下如:http://192.168.128.68:9091/phip_ehr/js/**.js

这样以来我们将可以轻松的获取到整个项目下的所有静态了。

二、后端基于freemarker

后端端技术栈:

  • java
  • freemarker ...

这里后端的项目结构我这边只截取跟我们前端相关的目录来说明

后端端项目结构:

├── templates                       项目模版
│   ├── home 
        ├── layouts                 页面布局
        ├── views                   业务代码(ftl)
        ├── widgets                 项目依赖

layouts

default.ftl

<!DOCTYPE HTML>
<html>
	<head>
		<link rel="dns-prefetch" href="${staticServer}">
		<meta http-equiv="X-UA-Compatible" content="IE=edge">
		${widget("headInner",page.bodyAttributes)}
   		<#list page.styles as style>
	        <#if (style?index_of('http') > -1) >
	            <link href="${style}?v=${version}" rel="stylesheet" type="text/css" />
	        <#else>
	            <link href="${staticServer}/phip_ehr/css/${style}?v=${version}" rel="stylesheet" type="text/css" />
	        </#if>
	    </#list>
	</head>
	<body>
		${widget("header",page.bodyAttributes)}
		<div id='gc'>
			${placeholder}
		</div>
		${widget("footerJs")}
	</body>
</html>

上述代码是整个项目页面的布局结构

${widget("headInner",page.bodyAttributes)}

这个方法意思是引入一些我们前端静态的公共样式和一些公共的meta标签。

headInner.ftl

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta property="wb:webmaster" content="3b0138a4c935e0f6" />
<meta property="qc:admins" content="341606771467510176375" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />

<link rel="stylesheet" href="${staticServer}/phip_ehr/css/reset.css?v=${version}" type="text/css"/>
<link rel="stylesheet" href="${staticServer}/phip_ehr/css/common.css?v=${version}" type="text/css"/>

这里提一下${staticServer}这个当然指的就是我们静态域名了,需要在后端项目的配置文件config中来声明,即指向我们前端的静态服务

**.mursi.attributesMap.staticServer=http://192.168.128.68:9091
**.mursi.attributesMap.imgServer=http://192.168.128.68:9091

引入完我们的公共样式以后,那接下来我们业务样式怎么引入呢?

<#list page.styles as style>
    <#if (style?index_of('http') > -1) >
        <link href="${style}?v=${version}" rel="stylesheet" type="text/css" />
    <#else>
        <link href="${staticServer}/phip_ehr/css/${style}?v=${version}" rel="stylesheet" type="text/css" />
    </#if>
</#list>

这段代码就是用来引入我们的业务样式的,意思是利用后端框架封装的page这个对象中style属性,然后对所有页面的style标签进行遍历

然后在我们业务代码(ftl)中将可以通过addstyle这个方法来引入我们的业务样式了

${page.addStyle("audit.css")}

${widget("header",page.bodyAttributes)}

这个方法的意思是引入我们页面中公共的头部

${placeholder}

这个意思是引入我们页面主体内容部分

${widget("footerJs")}

这个意思是引入我们页面中js文件

footerJS.ftl

<script type="text/javascript">
    
    $GC = {
        debug: ${isDev!"false"},
        isLogined : ${isLogin!"false"},
        staticServer : '${staticServer}',
        imageServer : '${imageServer}',
        kanoServer : '${kanoServer}',
        version:"${version}",
        jspath:"${staticServer}" + "/phip_ehr/js"
    };

    // $GS { Array } - the init parameters for startup
    $GS = [$GC.jspath + "/plugins/jquery-1.8.1.min.js",
            $GC.jspath + "/GH.js?_=${version}",
            $GC.jspath + '/plugins/validator.js',function(){

            // load common module
            GL.load([GH.adaptModule("common")]);

             // load the modules defined in page
             var moduleName = $("#g-cfg").data("module");
             if(moduleName){
                var module = GH.modules[moduleName];
                if(!module) {
                    module = GH.adaptModule(moduleName);
                }
                if(module) {
                    GL.load([module]);
                }
            }

    }];
</script>

<!-- 引入js模块加载器 -->
<script type="text/javascript" src="${staticServer}/phip_ehr/js/GL.js?_=${version}" ></script>

<script src="http://127.0.0.1:35729/livereload.js"></script>


这段代码中GC就指的是初始化一些变量,然后GC就指的是初始化一些变量,然后GS中就是引入我们项目中依赖的公共js,如jquery、common.js等。其次是通过GL.js这个模块加载器来加载我们的业务js

这样我们就可以在我们的业务ftl中通过data-moduls来引入每个页面中的业务js了

a.ftl

 <div class="g-container gp-user-info J_UserInfo" id="g-cfg" data-module="a" data-fo-appcode="1" data-header-fixed="1" data-page="infirmary"></div>

a.js

GH.run(function() {
    GH.dispatcher('.J_Home', function() {
        return {
            init: function() {
                this.switchPatient();
            },

            /**
             * 
             * 切换就诊人档案
             */
             switchPatient: function() {
             	console.log(1);
             }
        }
    })
}, [GH.modules['validator'],GH.modules['datepicker']]);

dispatcher就是相当于页面的分发器,当然每个页面只能拥有一个独立的分发器,run()方法就是我们封装在GH.js的公共调用方法

那么我们项目中引入 一些公用的插件要怎么引入呢?

那么我们就在GH.js里封装了GH.modules()方法来引入插件,这里就不详细的说明了。

这里顺带也提一下ftl,什么是ftl?ftl就是类似于我们的html一样,但是它不同的地方就是它是基于freemarker语法来进行编写的一种后端模版引擎。我们项目中一些可以同步加载的数据都可以利用freemarker的语法在ftl中直接进行操作

小结:

这种前后端不分离的模式有哪些优缺点呢?

优点:虽然在开发效率上比不上纯前后端分离的模式(vue+webpack,react+webpack),但是针对一些对于兼容性要求很高的多页项目,这种开发模式也是可取的。

缺点:对后端服务依赖太强,往往后端服务一旦出现报错或者挂掉后,前端的工作就没有办法开展下去了,从而加大了前后端的开发成本。