前言
最近在做一个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.js和server.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>
这段代码中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),但是针对一些对于兼容性要求很高的多页项目,这种开发模式也是可取的。
缺点:对后端服务依赖太强,往往后端服务一旦出现报错或者挂掉后,前端的工作就没有办法开展下去了,从而加大了前后端的开发成本。