记前端从“刀耕火种”过渡到到“现代化”的自动构建工具(在ThinkPHP的项目里使用webpack)

我是14年入的程序员大军,当时主java兼具前端开发的活儿,在如今看来的一些流开发框架和新兴思想,早在node.js开始进入你们视野的时候就流行起来了,只是在那时博主并无关注前端的生态圈(然而java好像也并无关注,逃),因此仍是处在不少人所描述的刀耕火种的阶段,前端代码所有挂载到全局做用域,包括插件导出的变量。那更别提组件化模块化的编程思想了,甚至代码都不用压缩优化就直接上传到服务器发布了。javascript

后来换了一家公司,没有前端开发这个职位,是从javaer转过去的,由于项目须要,渐渐的也就坐实了这个岗位。项目到如今(2014年8月-2017年7月22日)一共出现了三个阶段php

  • 用着十年前的开发(或者叫整合)技术的简陋期
  • 经历四、5个月的半模块化改造的准现代期
  • 到如今能整合全局资源(仅限web静态资源),随意整合新技术的现代期(未实施)

为何要不断的去折腾,去改造?仅仅是为了跟上“现代”的步伐吗?下面我将讲述每一个阶段是如何无痛改造的,为何要改造。css

简陋期准现代期

举个例子,咱们之前的代码是这样的html

html页面部分

<html>
<head>

<link href="style.css" rel="stylesheet"/>
</head>
<body>

<!-- 通用的代码 -->
<script src="common.js"></script>
<!-- 第三方的插件代码 -->
<script src="plugin.js"></script>
<!-- 咱们的主入口 -->
<script src="individual.js"></script>
</body>
</html>

javascript部分

common.js里,是咱们的定义的通用函数,好比一些特定组件的部分代码如headerfooter,或者是字符串处理,日期格式化的函数等等,这些函数都以对象或函数的形式暴露在全局做用域里,很是的冗杂和不安全,随着代码量的增长,容易致使覆盖,出现难以预料的bug,还有一个致命的弱点就是没法按需加载资源,我哪怕只是用到了其中一个小小的常量,都须要引用整个文件,而后从全局做用域里拿。前端

// common.js
var Header = {
  var1: '',
  var2: {},
  fn1: function() {
    // some code
  },
  fn2: function() {
    // some code
  }
}

function strReplace() {
  // some code
}
...
// individual.js
// 也许咱们早已有觉悟 使用了自执行匿名函数来防止全局变量的污染
(function() {
  // 这里咱们须要用到commonjs的函数 常量等
  var afterHandleStr = strReplace(str);

  // 也许咱们忘记strReplace函数已存在全局做用域又或者换了一我的
  // 来维护这个文件可能又会定义一个函数叫strReplace
  function strReplace() {
    // 那么此时根据javascript特性,原先的函数已经被覆盖了,
    // 上面的调用逻辑优先从最近的做用域开始找,因而会执行到这里
  }

  ...
}());

由于项目是迭代开发的,功能一点点叠加上去,考虑到整个项目的生命周期,不至于到后期彻底没法维护,因此咱们必须以优雅的姿态去重构整个项目的资源引用方式,那就是模块化,一个模块作一件事情,并暴露它对外提供的接口以供具象化的页面来使用。好比headerfooternavsidebarutils等等。前端的模块化有俩个标准,一个AMD(Asynchronous Module Definition),一个是CMD(Common Module Definition),前者是异步模块定义,推崇依赖前置,后者是通用模块定义,推崇依赖就近,AMD的表明框架有requirejs,CMD的表明框架有seajs,都是很优秀的做品,这里对两者有详细的介绍。最后我选择了requirejs做为本次重构的基础,其实就当是的代码来讲,改造起来并无什么难度,就是须要细心,细心,细心,只须要将common.js这个通用模块进行拆分就行了,页面只须要引入一个js文件,以下面这样java

<html>
<head>

<link href="style.css" rel="stylesheet"/>
</head>
<body>
...
<script type="text/javascript" data-main="/individual.js" src="/require.js"></script>
</body>
</html>

data-main是咱们的代码主入口,srcrequireJs的源码。从文件引用来讲,至少咱们没必要再关心每次使用一个插件都要手动来加入一个script标签了,如何引用呢?我下面会介绍。
假如咱们之前的代码是这样的node

// common.js
(function(){

  var exportObj = {
    aa: 'aa',
    bb: 'bb'
    ...
  }

  var utils = {
    replaceStr: function() {

    }
    ...
  }

  // 放到全局做用域
  window.exportObj = exportObj;
  window.utils = utils;
}());



// individual。js
(function(){

  var aa = constants.aa;
  var bb = constants.bb;
  
  var tempStr = utils.replaceStr('tempStr');

}());

上面的代码使用了两个全局对象,constantsutils,那么改造后应该是:webpack

// constants.js
// 若是它不依赖于其余模块,就没必要声明依赖的数组
define( function() {
  var exportObj = {
    aa: 'aa',
    bb: 'bb'
    ...
  }

  // 返回咱们要暴露出来的对象,不用再放到全局做用域
  return exportObj;
} );

// utils.js
define( function() {

  // 返回咱们要暴露出来的对象,不用再放到全局做用域
  return  {
    replaceStr: function() {

    }
    ...
  };
} );

// individual
define( [
  'constants',
  'utils'
], function( consts, utils ) {
  var aa = consts.aa;
  var bb = consts.bb;
  
  var tempStr = utils.replaceStr('tempStr');
} );
// 或者
define( [
  'constants',
  'utils'
], function() {
  var consts = require('constants');
  var utils = require('utils');

  var aa = consts.aa;
  var bb = consts.bb;
  
  var tempStr = utils.replaceStr('tempStr');
} );

是否是感受毫无挑战性,对,这就是一个体力活,细心点就行了程序员

咱们没必要担忧还须要手动去改动第三方插件,如今的主流插件基本都会UMD方式去适配,也就是兼容了AMDCMD,因此只须要直接引用第三方插件就好了,没必要再去html文件里手动引用script标签了,其余具体实现细节和必备的配置能够参照requirejs官网的例子web

等到改造完,也尚未愉快的结束,咱们的准现代期增长了一个优化环节,官方提供了r.js这个优化器来帮咱们打包压缩代码(毕竟生产环境过多的请求数仍是不被容许的),此时的改造才真的作到了模块化能优化,从简陋期无痛过渡到准现代期。此时的代码,其实已经具有了进入现代期的要求,那就是规范模块化。下面是咱们即将进行的改造,顺利过渡到现代期,从而拥抱你想使用的新技术

准现代期现代期

其实这个阶段,由于对一些新工具新技术的不熟悉,绕了不少弯子,花费了很多的精力,好在弄出来了,基于webpack构建工具,解放键盘F5,加入代码风格和规范的检查工具,加入ECMAScript 6语法转换工具等等,为何要使用这些,归纳为主要如下几点:

  • 提高开发效率和代码质量
  • 新语法新和技术能解决开发上的不少痛点和盲点
  • 强大的整合性和包容性(相对于封闭的r.js优化器终于可定制了)
  • emmmmmm思考中

首先咱们介绍一下webapck是什么
webpack
这是webpack官方文档首页对其的简单描述(ps: 其实中间的正方体是会旋转的哦),强大的webpack能整合全部依赖的文件进行处理,如less编译(依赖less-loader),ES6语法转换(依赖babel-loader),文件hash添加,自动上传ftp发布生产环境等等。还有就是webpack-dev-server这个开发神器,热替换自动监测文件变化刷新浏览器,虽然现代期并无用到实际项目中去,可是到如今(2017年7月22日),我已经能彻底拿出一套方案,使现有项目平滑过分到webpack。(ps:网上的教程大可能是基于单页单入口的SPA应用,和后端彻底解耦的,咱们的项目是和后端处于半解耦状态,而且是多页多入口,因此并不能使用大多数的webpack配置文件,须要进行变通处理)

咱们先来讲说处理通常的SPA应用的配置参数

module.exports = {
  entry: {
    // webpack生成的文件名和入口文件
    // 咱们是多页入口,先以首页为例
    index: './Public/dev/page/main.js'
  },
  output: {
    path: config.build.assetsRoot,
    filename: '[name].js',
  },
  module: {
    rules: [  {
      // 各类loaders
    } ]
  },
  plugins: [
    new HtmlWebpackPlugin( {
      // 模板生成后的文件名(能够加上路径)
      filename: 'index.html',
      // 入口文件的模板(也就是承载你页面视图的地方)
      template: 'index.html',
      inject: true
    })
  ]
}

抛开其余杂七杂八配置不谈,上面的配置就是大多数的SPA应用的配置。用在咱们的项目里,在根目录运行webpack会发现发生错误,并提示缺乏不少的模块,由于这些咱们自定义的模块webpack自己并不能识别,因此这里有相当重要的一步,将现有的requirejs的配置文件里的paths同步迁移到webpack的配置文件里

// 在requirejs配置文件里多是这样写的
require.config( {

  paths: {
    header: './modules/header',
  }

} );

// 那么咱们就应该将此配置交给webpack
resolve: {
  alias: {
    header: 'modules/header' // 路径可能不必定是这个
  }
}

而后咱们再打包,运行,发现丫的竟然会报错了?最明显的错误就是define is not defined。让咱们来翻翻上面咱们准现代期的代码

// individual
define( [
  'constants',
  'utils'
], function( consts, utils ) {
  var aa = consts.aa;
  var bb = consts.bb;
  
  var tempStr = utils.replaceStr('tempStr');
} );

这里的define就是报错的缘由(webpack有时候并不能识别这里,有时候却又能正确转换成能运行的代码,没有深究这里的缘由,虽然webpack2已经支持AMD风格的代码打包,可是我仍是决定对这里稍做修改,变成CMD风格,即便是使用CMD风格的seajs依然是须要去掉外面那层包裹的函数的,无论怎样都得改),因而咱们只须要将上面的代码调整为:


2017年7月24日22点50分更新,通过个人尝试,只要配置依赖都正确,彻底能够直接打包,不用非得改为CMD,因而换成webpack更轻松了~~?


var consts = require('constants');
var utils = require('utils');

var aa = consts.aa;
var bb = consts.bb;
var tempStr = utils.replaceStr('tempStr');

// 若是这里有return的话须要将return obj调整为
module.exports = obj;

至此咱们再打包即可以轻轻松松合并了(固然若是你要提取公共代码的话又是另一个插件了,这里再也不赘述)

打包发布的问题解决了,最重要的一环开发环境的搭建呢?

其实机智的我早料到这种配置在咱们的项目并不完美,由于HtmlWebpackPlugin这个插件须要的模板是放在硬盘里的静态文件模板,它会自动插入构建好的jscss文件,咱们的模板不是静态的,是从php后端渲染的一段动态的html,仍是做死试了试,果真出现了如下状况

  • 动态引用的header、footer不见了
  • 页面出现一堆后端模板的语法{$xxx}{$yyy}{$zzz}

其实webpack-dev-server提供了一个代理功能,那这里的问题解决起来就美滋滋了。单纯的我最早的配置是这样的:

var express = require( 'express' )
var proxyMiddleware = require( 'http-proxy-middleware' )
var app = express();
// 这是代理的
var proxyTable = {
  '/': {
    target: 'http://xxxx.cn/'
  }
}
Object.keys( proxyTable ).forEach( function( context ) {
  var options = proxyTable[ context ]
  if ( typeof options === 'string' ) {
    options = {
      target: options
    }
  }
  // 应用代理地址和代理目标
  app.use( proxyMiddleware( options.filter || context, options ) )
} )

以上代码将咱们全部的请求路径一股脑所有代理给后端php服务了,HtmlWebpackPlugin这个插件会自动写入依赖的脚本文件和样式表文件,可是此时的文件是webpack-dev-server服务生成的,而且存在于内存里,因此此时咱们再运行webpack开启的服务,就会形成页面出来了(包括任何动态从服务端渲染的数据),可是样式和js都没有加载,由于请求被代理到了后端,后端的目录里并不存在这些文件(废话么),因此咱们须要过滤掉这些特定的请求不让http-proxy-middleware插件进行代理,为了区分这些特定的请求,咱们将entry字段里的文件名都加上一个前缀__webpack或任何独一无二的与后台请求开头不同的字符串,此时proxyTable里的filter函数就派上用场了,查看官方文档是这么描述这个函数的

For full control you can provide a custom function to determine which requests should be proxied or not.

为了彻底控制你的请求,你能够定义一个函数来肯定这些请求是否应该被提交

因而我终于拿出一个满意的代理配置文件,开心得我仿佛升职加薪了同样?

var proxyTable = {
  '/': {
    target: 'http://xxx.cn/',
    changeOrigin: true,
    filter( pathname, req ) {
      return !new RegExp( `^\/(__webpack|${assetsSubDirectory})` ).test( pathname );
    }
  }
}

让我来解释一下上面的代码:未匹配到以__webpack开头的请求,都进行代理,这里添加了一个assetsSubDirectory变量,这个变量实际上是webpack生成的图片、字体文件、json文件、svg等仍然存在于内存里的引用的路径,由于在内存里随着咱们的编码可能实时变更,因此它们仍是不须要作代理,直接过滤掉。

对了,遗漏了一个很重要的配置,代码以下:

plugins: [
  new HtmlWebpackPlugin( {
    alwaysWriteToDisk: true,
    // php端使用到的模板
    filename: `${ROOT}/Application/Home/View/Index/index.html`,
    // 模板文件
    template: `${ROOT}/Application/Home/View-template/Index/index.html`,
    chunks: [ '__webpack-indexController' ],
    inject: true
  })
]

机智的咱们确定能发现View-template这里的不一样,见名知意,这个文件夹里的html都是对应的后端的模板视图文件,咱们经过alwaysWriteToDisk这个参数(其实还须要配合另一个插件)以template字段的值为目标,实时写入到filename对应的文件里,而此时,由于浏览器访问的页面里由于咱们启动webpack-dev-server时已经编译了这个文件,js会主动和webpack服务创建一个eventSource长链接(这个链接也是排除在代理范围内的)来监听文件变化,因此就会自动刷新浏览器,从而实现咱们的live-reload

至此,从准现代期现代期的过渡方案就算是完成了,接下面即是寻找一个合适的时间点实施到项目中去。若你要问我那么多页面是否是全都一个个得配,固然是,可是为了方便易维护,能不侵入现有项目去修改文件名,咱们确定须要去手动编写一个map映射文件,来指明咱们的模板文件对应的入口文件,经过这个map咱们再来动态生成entryHtmlWebpackPlugin须要的模板路径,固然这里并非没有便捷的办法,咱们能够写一个脚本去读取View-template下面的目录来自动生成map可是由于咱们童鞋在命名的时候文件夹和对应的入口文件并不能对应上,就得修改,这并非推荐的作法,并且也不方便咱们在改造代码风格的时候进行单个调试。

上面的示例代码都不是完整的,由于我并非要提供一个webpack的教程,而是解决后端和前端html耦合的webpack-dev-server配置的问题。


以上内容都转自我本身的博客原文地址

相关文章
相关标签/搜索