通过参考网上的一些构建方法,当然也在开发过程中进行了一番实践,最终搭建了一套适用于当前多页应用的构建方案,当然该方案还处于draft版本,会在后续的演进过程中不断的优化。
个人觉得该方案的演进过程相对于成果而言更值得记录。但在此之前,我们先简单介绍下项目的整体架构,这样大家会更明白为什么要采用这样的构建方式。下图列出了用户在浏览器中输入url到页面ready的过程,可以看出这是一个典型的服务端直出架构,其中作为前端工程师的我们主要关注点是放在浏览器端以及Node层。在Node层,我们对koa的进行了封装,并采用了类似于eggjs的MVC架构,同时使用pug作为模板引擎,技术栈其实并不复杂。
V0.0.1:使用webpack对前端资源进行构建
在构建过程中要做什么事呢?相信不同的人有不同的见解:
- 对静态资源进行压缩,减少传输字节;
- 为避免浏览器读取了旧的缓存文件,需要为静态资源添加MD5戳;
- 为CSS属性自动添加vendor prefix;
- ......
上面所列出的事项也是我们在构建过程中所需要考虑的。在项目早期的构建方案中,我们选择使用webpack作为构建工具,原因其实很简单:功能强大、用的人多,所以一拍脑袋就选择它了。当然,webpack也确实不负众望,通过它,我们可以像写Node一样直接引入其他的文件,在使用前期确实给我们带来了很多的便捷。
但是我们的项目毕竟和webpack主流的使用场景,如React、Vue等项目还是有很大的不同之处,在使用webpack的过程中陆续出现了一些水土不服的地方,虽然都最后都通过一些方式解决了,但是这也促使我们在思考,是否有其他更合适的方案。
问题一:
如何让webpack打包所有资源文件?
解决方法:
webpack会将entry作为入口起点,找到所有依赖项并对其进行构建。由于webpack只认识JavaScript文件,所以对于非JavaScript文件需要使用loader将其转为webpack能够处理的模块。所以说,如果某个资源文件需要被webpack构建,那么这个资源文件必须是从entry可达的。对我们的项目来说,最理想的情况是以pug模板文件作为入口,但是由于pug模板文件需要的数据是从server获取的,而在构建阶段是不可知的,所以,只能退而求其次,使用JavaScript文件作为入口。
对于不同类型的文件,我们采用了不同的策略:
- 对于图片,我们使用了webpack提供的require.context方法,它可以让我们使用正则的方式来引入相应的模块。所以我们添加了下述文件,并将其置于webpack的entry属性中。
require.context(‘./public‘, true, /\.*\.(jpg|png|jpeg|gif|ico)$/i)
- 对于css文件,当然也可以采用上述的方式,但是这会将所有的css编译到一个文件中,导致生成的文件过大,从而使页面加载耗时较长。所以我们只能选择在相应的JavaScript文件中添加 import ‘./xxxx.less‘ 来告诉webpack:你需要构建xxxx less/css文件了。这样做功能是实现了,但是却并不优雅,同时也会使JavaScript变得不纯粹,也没办法被其他不用webpack作为构建工具的项目所重用了。
问题二:
在Node从server获取数据后,会将pug模板渲染成html并将其发送回浏览器端,那么在模板中如何保持对静态资源的引用呢?因为构建工具会为所有的资源文件添加MD5戳,所以我们在编码时并不知道确切的文件名是什么。
解决方法:
利用webpack-manifest-plugin插件,它会在webpack构建完成后生成一个manifest.json文件,该文件会列出原始文件名与构建后的文件名之间的匹配关系。通过将manifest.json作为参数传入pug的render方法中,这样在pug模板中就可以通过类似于 img(src=manifest["logo.png"]) 的方式来保持对静态资源的引用。
V1.0.0-beta:使用gulp+webpack对前端资源进行构建
问题总是有方法解决的,但是这样写起来总有一些别扭,也感觉很不优雅,这致使我们思考webpack是否真的适用于我们的项目。通过一番讨论,最终决定尝试使用gulp+webpack的方式进行构建。
- 利用webpack构建JavaScript资源,可以方便的利用webpack模块的思想,使得JavaScript之间相互引用变得简单、便捷;
- 利用gulp构建css, images等其他资源。
使用webpack对JavaScript进行构建时,为了不至于每次添加一个新文件,都要修改webpack的配置,所以我们写了一个方法将所有的JavaScript都放入webpack的entry属性中。
function entries(globPath) { const files = glob.sync(globPath); let key, name, ext, entries = {}; for (let file of files) { ext = path.extname(file); name = path.basename(file, ext); if (name.startsWith(‘_‘)) { continue; } entries[name] = path.join(__dirname, file); } return entries }
使用时,只需要在globPath中输入指定的js路径就可以了,如
let webpackConfig = { entry: entries(‘./public/**/*.js‘), }
而在实际的开发中我们发现,有一些JavaScript文件存在的目的就是被其他文件引用,例如一些helper方法,它们是不会作为webpack的entry存在的,所以我们在entries方法中只寻找不是以下划线(_)开头的JavaScript文件,因此对于一些只有helper方法的JavaScript文件,只需要将其文件名以_开头,这样就不会被添加到webpack的entry属性中了,这也算是该构建方案中的一个小彩蛋~~~
在使用gulp对其他资源的构建过程中,我们用到了很多成熟的gulp插件,其中我认为比较重要的是gulp-rev和gulp-rev-replace。其中gulp-rev插件用来为css、images等资源文件添加MD5戳并生成相应的manifest.json文件,同时搭配使用gulp-rev-replace插件将pug、css等文件中存在于manifest.json里的文件名进行替换,这样我们在pug模板中引用资源文件就可以直接写 img(src=logo.png) 而不需要像以前那么复(chou)杂(lou)了。
由于配置文件的内容较长,所以就不粘贴到文章中了,如果想参考详细的配置内容,可以点击https://gitee.com/philipding/codes/4xi0plvhfa86onrmzwkes21进行查看。
再说一个题外话,其实在使用gulp-rev+gulp-rev-replace插件之前,我们尝试使用过gulp-md5-plus插件。gulp-md5-plus插件使用起来超级方便,它可以添加MD5戳,也可以替换文件名,但是该插件暂不支持添加自定义前缀的功能,而这个功能对我们确实必须的。因为在生产环境中,所有的资源文件会放到CDN上,而测试环境中的资源文件则放在对应的测试服务器上,所以说,同样的一张图片,在测试环境的地址可能是public/images/logo.png,而生产环境却是//cdn.demo.com/images/logo.png,所以我们需要支持自定义前缀功能,并使用类似下述的代码为其添加前缀 var prefix = process.env.NODE_ENV === ‘production‘ ? ‘//cdn.demo.com/‘: ‘/public/‘ 。
写在最后:
上述构建方案也许不是那么完美,但是对于当前的项目来说,确是一个相对而言较为合适的。当然也不排除后续该方案会被其他更优雅的方案所替代。但是通过这么多次的尝试、重构,才真正体会到了什么叫“适合的才是最好的??”。