聊聊 Webpack 使用

老早的时候就听说了 Webpack 这个工具, 当时大概的印象就是类似 Gulp 这样的东西, 并且看起来好像挺复杂的. 直到学习 React 的时候才开始接触 Webpack, 才知道 Webpack 更多的是做模块化的工作. 不过当时也是乱配置一通能用就行=.=.

现在 Vue 标配也是用 Webpack 了. Webpack 其实并没有想象中的那么复杂, 其实最核心的还是 loader 那一块. 这次就主要聊一聊 Webpack. 我用的是 Webpack 最新版本 2.1.0-beta.27.

what-is-webpack.png

Loader

Loader 是 Webpack 的核心, 它会自动查找项目中的我们指定的文件类型, 然后使用我们指定的 Loader 进行处理. 例如:

module: {
  rules: [{
    test:    /\.vue$/,
    loader:  'vue-loader',
    options: {
      loaders: {
        css: ExtractTextPlugin.extract({
          loader:         ['css-loader?minimize', 'postcss-loader'],
          fallbackLoader: 'vue-style-loader'
        })
      }
    }
  }, {
    test:    /\.js$/,
    loader:  'babel-loader',
    exclude: /node_modules/
  }, {
    test:   /\.css$/,
    loader: ExtractTextPlugin.extract({
      loader: ['css-loader?minimize', 'postcss-loader']
    })
  }, {
    test:   /\.(eot|woff|woff2|ttf)([\?]?.*)$/,
    loader: 'file-loader'
  }, {
    test:   /\.(png|jpg|gif|svg|ico)$/,
    loader: 'url-loader?limit=8192',
  }]
},

对于 Vue 文件, 我们要让 vue-loader 来处理, 这里可以先忽略 ExtractTextPlugin 部分, 它作用是提取 CSS 这个在后面会提. 对于 .js 文件, 我们使用 babel-loader 来处理, 我们可以在项目配置一个 .babelrc 文件来指定我们使用的 presets 和 plugins.

Webpack 我觉得一个不太好的地方就是写法很多, 而且那么多种写法大体是一样的, 但是在一些场景下它们可能又会有区别, 就不能统一一下吗? 例如, 如果我们使用了 Sass, 那常用的两种写法如下:

// 写法1
{
    test:   /\.scss$/,
    loader: 'css-loader!sass-loader'
}
// 写法2
{
    test:   /\.scss$/,
    loader: [
      'css-loader',
      'sass-loader'
    ]
}

我们可以使用 ! 来连接多个 loader, 它们会自右向左执行.

另外, -loader 可以省略不写, 但在 Webpack2 中推荐写上. 如果不加 -loader 的话在一些场景下它会出错.

devServer

这个是 webpack 另一个强大的地方了. Webpack-dev-server 是一个小型的 node.js Express 服务器, 通过 websocket 可以实现浏览器的模块热替换. 即前端代码变动的时候无需刷新整个页面, 而只是把变化的部分替换掉. 关于这个热替换, 其实也有好几种配置方法, 这里我只说我用的情况.

devServer: {
        hot:                true,	// 热替换
        historyApiFallback: true,	// HTML5 Mode
        port:               7000,	// 端口
        proxy:              {		// 代理
            '/api/*': {
                target: 'http://127.0.0.1:3000'
            },
            '/auth/*': {
                target: 'http://127.0.0.1:3000'
            },
            '/img/*': {
                target: 'http://127.0.0.1:3000'
            },
            '/css/*': {
                target: 'http://127.0.0.1:3000'
            },
            '/fonts/*': {
                target: 'http://127.0.0.1:3000'
            },
            '/js/*': {
                target: 'http://127.0.0.1:3000'
            },
            '/favicon/*': {
                target: 'http://127.0.0.1:3000'
            }
        },
    }
}

这里的 proxy 也是 webpack-dev-server 一个强大的地方之一, 我们可以配置一些代理来避免跨域问题和端口不一致的问题.

接着运行即可

webpack-dev-server --hot --open --inline --progress

减少打包体积

Webpack 在开发环境打包的体积非常大, 因为其包含了 source-map 等. 我们在生产环境并不需要它, 可以如下配置:

{
  devtool: isProduction() ? false : '#eval-source-map'
}

除了这点, 有时候我们还想生产环境使用 CDN, 开发环境使用本地的资源. CDN 可以通过 externals 配置

{
  externals = {
    'vue':          'Vue',
    'underscore':   '_',
    'vue-resource': 'VueResource',
    'vue-router':   'VueRouter',
    'vuex':         'Vuex'
  }
}

同时不要忘记了在 index.html 中把各文件的 CDN 链接导入. 区分两个环境我们可以建立两个配置文件, 或者简单的通过条件语句判断. 这样处理后生产环境和开发环境的 index.html 就有了比较大的区别, 我处理方式是建立了两个 index.html 一个用于开发环境一个用于生产环境, 再者他们刚好也位于不同的位置, 开发环境从根目录加载 index.html , 而生产环境则有后端根据 UA 指向 public 下的 index.html

再有的优化就是进行 JavaScript 代码的压缩混淆, 当然这个也只推荐在生产环境中使用:

plugins.push(
  // 生产环境压缩 JavaScript 代码
  new webpack.optimize.UglifyJsPlugin({
    test:     /(\.vue|\.js)$/,
    compress: {
      warnings: false
    },
  })
)

导入这个插件即可

另外还有 CSS 的压缩:

{
    test:   /\.css$/,
    loader: ExtractTextPlugin.extract({
      loader: ['css-loader?minimize', 'postcss-loader']
    })
}

只要在 css-loader 后面加上 ?minimize 就好了.

提取 CSS

就上面那段代码, 用到了 ExtractTextPlugin 这个插件, 它就是用来分离 CSS 代码的, 我们需要安装这个插件, 然后在 Webpack 中导入. 使用方法就上面这样, 但还要做一个配置:

plugins.push(
  new ExtractTextPlugin({
    filename:  isProduction() ? 'style.[contenthash:4].css' : 'style.css',
    allChunks: true,
  })
)

导入这个插件, 并配置文件名. 之后他就会在我们的 output 处输出这个 CSS 文件.

我这里不仅仅是要处理 CSS 文件, 还要处理 .vue 中的 CSS 样式.

{
  test:    /\.vue$/,
  loader:  'vue-loader',
  options: {
    loaders: {
      css: ExtractTextPlugin.extract({
            loader:         ['css-loader?minimize', 'postcss-loader'],
            fallbackLoader: 'vue-style-loader'
          })
    }
  }
}

注意 Webpack2 最新版本 API 想比 Webpack1 有较大变化. Webpack2 不支持在配置文件中插入其他东西, 如果你想对这个 loader 进行进一步配置, 需要在 options 中配置. 这里对 vue-loader 进行了进一步配置, 加入了 postcss-loadercss-loader .

postcss-loader 也是一个比较坑的地方, 在 Webpack2 最新版本已经不支持使用 postcss.config.js 文件的配置, 你需要自己在 Webpack 中配置这个插件.

plugins.push(
  new webpack.LoaderOptionsPlugin({
    options: {
      postcss: [
        require('postcss-nested'),
        require('postcss-cssnext')
      ]
    }})
)

插入这个插件后后面才可以正常使用 postcss-loader

我们同时处理了 .css.vue 中的 css 并最终生成了一个 CSS 文件. 这里在生产环境会生成 style.[contenthash:4].css , contenthash 是根据文件内容生成的, 在文件名加入其哈希值后, 我们就可以大胆的最样式表进行长期缓存, 因为样式表内容一变化文件名也变了.

文件名嵌入哈希值

除了 CSS 处理外, 我们还要对 JavaScript 进行处理, 这个是在 output 中配置的:

{
      output = {
        path:       path.resolve(__dirname, './public/static/'),
        publicPath: '/static/',
        filename:   'build.[chunkhash:4].js'
    }
}

注意这里用的是 chunkhash , 我们在 CSS 中用的则是 contenthash

最后我们就会生成如下的文件名:

style.dd51.css
build.84e5.js

但这样做是不够的, 我们不能每次都自己手动修改 index.html , 我们要让 index.html 中的文件哈希值也自动变化.

这个可以通过自定义插件来做, 我是直接参考了别人写的, 并没有深入去了解(仅供参考, 下面有说更好的方法)

plugins.push(
        function () {
            this.plugin('done', function (statsData) {
                var stats = statsData.toJson()
                if (!stats.errors.length) {
                    var html = fs.readFileSync('./public/index.html', 'utf8')
                    var htmlOutput = html.replace(
                        /static\/(.+?)">/g,
                        function (word) {
                            let filename = word.split('/')[1].split('.')[0]
                            for (let i = 0; i < stats.assetsByChunkName.main.length; i++) {
                                if (stats.assetsByChunkName.main[i].indexOf(filename) !== -1) {
                                    return 'static/' + stats.assetsByChunkName.main[i] + '">'
                                }
                            }
                        })
                    fs.writeFileSync(
                        './public/index.html',
                        htmlOutput)
                }
            })
        }
)

这里的正则和路径都是根据自己项目的情况做出来的.

大致的意思就是监听插件的 done 事件, 然后传入 statsData 到这个插件的回调函数里, 如果没有出错, 那么获取得到 webpack 生成的文件名即上面说的文件名如 style.dd51.css, 即 stats.assetsByChunkName.main 这个数组. 这个数组保存着 webpack 生成的文件名, 接着我们获取 index.html 并用正则获取所有的 scriptstyle , 我这里的处理措施是得到文件名如 style , 然后在 stats.assetsByChunkName 中查找包含这个串的输出文件名, 将这个文件名替换原来的即可.

// 2017.1.5 更新

其实还有更简便的方法, 使用 HtmlWebpackPlugin 插件, 然后进行下面的配置:

new HtmlWebpackPlugin({
  template: 'public/index.html',
  filename: '../index.html',
  inject: 'head'
})

Webpack 在运行过程提取出的 chunk, 自动输出到 public/index.htmlhead 中. 然后存储到 output 设置的 publicPath 中, 因为 index.html 通常存放在资源外面, 所以这里文件名进行了相对路劲的处理.

到这一步不得不感慨 Webpack 的强大, 上面我说的有点乱, 可以在这里(最新的配置文件已经发生了改变)查看我的详细 webpack 配置. 这里没有细说每个配置的每个选项, 这些选项有些我自己也还搞不太明白, 最近还要再好好看下里面一些选项的细微区别.

总结下, 上面的 Webpack 帮我们做了这些事情:

  • 模块化

    一切皆模块, 只要有 loader. 我们可以在我们的 JS 文件中导入 CSS, 图片等资源. Webpack 会自动帮我们做处理. 只要你想的话, 你还可以用 CSS in JS. 如果你单独分离 CSS, 那么最终生成的就是一个 JavaScript 文件.

  • 使用 Babel 和 PostCSS

    在 Webpack 中使用 babel-loader 处理 .js.vue 文件, 我们就可以任性的写 ES6 和 ES7 了. 给 .cssvue-loader 加入 post-loader 后我们就可以任性的使用 cssnext 等特性了. 原本我是用 sass-loader 的, 但是我主要用的嵌套功能其实 postcss-loader 也可以处理, 并且我挺喜欢 postcss-loader 的丰富插件这个特性. 从此抛开 CSS 预处理器.

  • 压缩合并 JS 和 CSS

    不需要使用 Gulp 了. Webpack 对 JS 和 CSS 的压缩合并处理不能再简单了.

  • 代理服务器

    反向代理了我们的 API, 避免了端口修改和跨域的问题.

  • 文件哈希名

    给 CSS 和 JS 嵌入了哈希值, 并且自动替换 index.html 中的路径文件.

恩, 不愧是前端模块化和自动化利器.