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踩坑记录的主要内容,如果未能解决你的问题,请参考以下文章

Fastlane 一键打包/发布APP - 使用记录及踩坑

Fastlane 一键打包/发布APP - 使用记录及踩坑

webpack踩坑之旅

webpack安装与配置初学者踩坑篇

Vue踩坑记录册

webpack之polyfill踩坑之路