最近在做一个PC端的项目,由于项目需要兼容到IE8,所以从技术选型上采取了公司之前一直沿用的前端基于gulp后端基于freemarker的模式来进行开发。
那么gulp+freemarker这种开发模式的流程到底是怎样的呢?我这边就来简单的分析一下。
前端技术栈:
前端项目结构:
├── 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
熟悉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
这样以来我们将可以轻松的获取到整个项目下的所有静态了。
后端端技术栈:
这里后端的项目结构我这边只截取跟我们前端相关的目录来说明
后端端项目结构:
├── 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就指的是初始化一些变量,然后$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),但是针对一些对于兼容性要求很高的多页项目,这种开发模式也是可取的。
缺点:对后端服务依赖太强,往往后端服务一旦出现报错或者挂掉后,前端的工作就没有办法开展下去了,从而加大了前后端的开发成本。