webpack踩坑记录
Posted 月饼少年
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了webpack踩坑记录相关的知识,希望对你有一定的参考价值。
前言
javascript是一门解释性语言,早期的JavaScript代码都是写完即用,一个html一个js文件,即可感受网页制作的魅力,很多人都是因为这种开发的便利性走上了学习前端的道路。
随着JavaScript脚本复杂性的提升,网页性能由于JavaScript体积收到了影响,开始有压缩合并的工具出现。如 uglify、YUI compose等。
前端进一步发展,模块化、静态资源的管理成为了大型工程必不可少的环节,于是诞生了gulp、grunt以及大而全的fis等工程化工具。webpack诞生在这一波前端构建工具之后,早期只是为了实现前端各种资源的统一模块化管理,然而由于其独特的设计,以及配套生态的发展,各种loader和plugin层出不穷,将前端引入了编译流的新时代,前端工程师也终于像java工程师一样有了自己喝茶的时间。
坑爹的是,webpack 1x的配置非常繁琐,而文档又晦涩难懂,于是迸发出很多webpack工程模板,类似Java工程模板一样,clone过来,开箱即用,既可以享受编译流带来的开发便利,又不需要关心繁琐的配置,我在工作中也是这样的。为了满足不同的场景,有些配置模板足足有几百行代码,一般使用者恐怕已经很难看懂了。乘着我把我的模板从webpack 1 迁移到webpack 2 的机会,我重新梳理了一下webpack的配置和应用场景。
需求
通过webpack + npm script其实已经能够实现大部分前端工程化所需功能,比如:
本地开发环境
文件合并/压缩
JavaScript模块化(Commonjs/AMD)
现代化JavaScript编写(ES6/ES7)
现代化css编写(sass/less/postcss)
静态资源处理(图片/fonts)
文件指纹
cdn分域(图片/js/css资源分域)
公共类库抽离
下面的代码都在git上:
本地开发环境
为区分开发环境和生产环境,一般情况下都会在npm scripts里面建立两个不同的命令,如 npm run dev
和 npm run build
。
dev环境我们利用到了 webpack-dev-server() ,它对于开发来说可以起到:
一个本地的服务器,用于通过http协议来访问 html/js/css 等资源,实现前后端分离。
实现编译输出webpack打包后的文件,方便开发调试。
hot reload实现热替换,可以不刷新更新修改内容。
可实现资源虚拟路径,修改源文件直接的相对关系。
可实现本地的 proxy,因为本地server都是起在127.0.0.1的,可通过配置解决开发时接口跨域问题。
文件合并/压缩
这个就不用多说了,webpack本身就是做模块化管理的,只需要有一个 entry 和 output,输出文件便是默认合并过的,压缩webpack自身已经集成,只需要开启 webpack -p ,即可实现。
JavaScript模块化
webpack本身就是为了模块化而生的,所以无论是 CommonJs (require) 还是 AMD (define 和 require) 或者是ES2015的import,都能得到很好的支持。这点是webpack原生支持的,只需要在文件中引入,打包时会自动处理。
现代化JavaScript编写
随着ES6/ES7概念的普及,现代JavaScript的开发方式受更多人的青睐。为了让ES6/ES7的语法能够正常使用,而又确保在浏览器中得到兼容,babel或者buble之类的编译方案受到推崇,它们可以让开发者正常使用所有的ES6/7语法,同时浏览器运行的是编译成ES5的代码,既享受了新语法的开发便利,又不需要过多的关心兼容问题。
webpack通过loader的机制来引入babel等编译方案。源代码通过babel-loader处理之后,即可变成ES5的代码,再通过webpack的打包机制,可以正常的运行在浏览器之中。
关于babel的配置可参看:
我们使用的ES2015,只需要配置 .babelrc 文件:
{
"presets": ["es2015", "stage-2"]
}
除了编译ES6的语法,我们需要使用一些内置的ES6变量但是低版本浏览器不支持时,如 Promise、Object.assgin、async等,还需引入babel-polyfill 文件。
然而,整个babel-polyfill文件100多kb大小,我们并不需要完整的引入所有的polyfill,所以babel提供了一个 transform-runtime () 的插件解决这个问题,它会自动将使用到的新语法植入 polyfill 代码进来,而不引入未使用的代码,减少文件体积。
{
"presets": ["es2015", "stage-2"],
"plugins": ["transform-runtime"]
}
不过据我测试,transform-runtime的解决方案,对于只用到个别方法的情况,还是会引入大量的代码,同时,一不注意自己还会引入三方 polyfill包如:bluebird 之类的包引起浪费,在实际使用中,还需要多多注意。
现代化css编写
现代化css如 less/sass/postcss 解决方案,对于webpack来说,都可以使用对应的loader解决:
如:sass-loader: ,loader会如babel一样,将sass/scss/less文件编译成正常的css再在浏览器环境运行。
关于loader的使用方法,参考webpack配置即可。
静态资源处理
同css,静态资源如图片,只需要使用对应的loader即可。如可使用 url-loader( ) 来处理图片。
文件指纹
在前端发布体系中,为了实现增量发布,我们一般都会对静态资源加上md5的文件后缀,保证每次发布的文件都没有缓存,同时对于未修改文件,不会受发布的影响,最大程度利用缓存。
在 webpack 中,实现文件名增加 md5 的指纹其实很简单,只需要将 output 的filename 修改为:
filename: "[name].[chunkhash].js"
其中 chunkhash 即为每个文件的指纹。但是这里我们会遇到两个问题:
由于生产的md5文件名会类似于:main.70b594fe8b07bcedaa98.js
,我们无法预测在html中的引入路径。我们可以借助插件来实现:
而对于html需要放在后端的项目,可以使用 assets-webpack-plugin 将生产的文件md5导出,然后在发布的时候同步给后端。
2、使用了md5指纹之后发现每次打包还是会发生变化?
这是由于webpack的处理机制导致的,webpack每次打包会把每个模块的配置信息如文件名、文件顺序、文件md5等信息作为配置打入js中,以便于其进行模块管理,而这部内容,每次打包都有可能发生变化,导致整个js文件名每次都会发生变化。
webpack提供了一个 manifest 的机制来剥出这个配置文件。我们需要使用 CommonsChunkPlugin
来将其剥离,同时使用 chunk-manifest-webpack-plugin 读取其内容导出另外一个文件,防止其内容变化导致整个js文件指纹发生变化:
// webpack.config.js
var ChunkManifestPlugin = require("chunk-manifest-webpack-plugin");
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ["manifest"], // extracted manifest
minChunks: Infinity,
}),
new ChunkManifestPlugin({
filename: "chunk-manifest.json",
manifestVariable: "webpackManifest"
})
]
};
当然,对于前后端分离的项目,你也可以将这部分内容直接打入html以减少一个请求,使用 inline-manifest-webpack-plugin( ) 即可。
cdn分域
本地开发时,静态文件都可以作为相对路径引用,方便省事。然而在生产环境,我们为了加速访问,基本都会讲资源放在cdn上面,而且为了突破浏览器对单域名并发数量的限制,一般还会将图片和 css/js 分域名放置。如js/css在 static.xxx.com , 而图片在 img.xxx.com 。
publicPath: "https://static.xxx.com/assets/"
配合上文提到的 html-webpack-plugin ,打包出来的html文件中的 js 引用路径会自动变为:
https://static.xxx.com/assets/main.70b594fe8b07bcedaa98.js
use: "url-loader?name=[name].[ext]&publicPath=img.xxx.com..."
公共类库抽离
在老的前端开发模式中,常常会对类库比如 jquery 单独做一个 script 引用,在 webpack 中,文件分离也很简单,只需要在 entry 中放置两个不同的入口即可:
entry : {
vendor: ['jquery', ...],
main: 'index.js'
}
需要注意的是,单独使用两个 entry 虽然会将 jquery 等文件单独打包出一个文件,但是 main.js 中如果使用 require 等方式引入的文件,还是会再次打包进 main.js 文件,造成重复打包,所以entry一定要配合我们前文提到的 CommonsChunkPlugin
来使用:
new webpack.optimize.CommonsChunkPlugin({
name: ["vendor", "manifest"], // vendor libs + extracted manifest
minChunks: Infinity,
}),
这样才能保证,打出来的 lib 文件不会被再次打包在 业务文件之中。
其他常用配置
源码打包
通过模块引入的方式在前端虽然方便,但也带来一些问题,经常使得打出来的文件非常庞大,有两个原因
1、随意的 npm install 引入了大量重复的模块,被反复打在了同一个bundle中,虽然 webpack 本身会对重复模块去重,但是我们一般会对 node_modules 中的包去除编译,而如果包提供者稍不注意,提供的打包版本中已经含有和项目中使用过的模块,就造成了资源浪费。
2、babel/webpack的打包辅助函数重复。如果依赖包也是使用webpack/babel打包,很多工具打进去的polyfill/helper代码,实际上会造成重复,如 Object.assgin 的 polyfill。
所以我们的实践是在内部提供的包,都推荐走源码打包的模式。
而我们在配置中经常会使用 exclude: /node_modules/
直接排除掉了依赖包,如果我们相对其中的某些包进行源码编译,光写正则很难满足需求,其实webpack此处是可以提供一个function的,如:
exclude: pather => {
if (needPass) {
return true;
} else {
return false;
}
}
注入环境变量
webpack.DefinePlugin({
'process.env': {
NODE_ENV: 'development'
}
})
我们在代码中只需要判断:
if (process.env.NODE_ENV === 'development') {
// 开发环境
}
而且这部分代码,在实际编译过程中,会直接编译成 if(true){} ,再配合 uglify 的废代码分析,直接去除,即可保留在代码中,又不需要担心增大上线文件体积。
alias
有了模块化之后,文件拆分很细,势必会带来路径管理问题,类似
require('../../../../index')
的代码很常见,alias就可以对常用文件夹做别名,减少路径查找的烦恼。
resolve: {
alias: {
src: ...// 写一个绝对地址
}
}
后语
通过 webpack 其实完全不需要配合其他任何攻击实现一个基本的工程化需求,然而带来便利的同时,我心痛的发现,现在的 js 代码打出来随随便便就几百k,有的甚至上m了,便利性和性能理论上并不是矛盾的,实践过程中,多关心打包的流程和打包后产物,让开发效率和性能双赢,才是对工具的合理应用。
以上是关于webpack踩坑记录的主要内容,如果未能解决你的问题,请参考以下文章