webpack学习入门

作者: IT干货 | 来源:发表于2018-08-22 17:38 被阅读9次

    内容提纲

    • electron-vue项目中的webpack工程实例
    • 思考
    • webpack与gulp/grunt
    • HMR

    electron-vue项目中的webpack工程实例

    从electron-vue项目中的实际使用例子来入手

    如下是 webpack.renderer.config.js 文件

    'use strict'
    
    process.env.BABEL_ENV = 'renderer'
    
    const path = require('path')
    const { dependencies } = require('../package.json')
    const webpack = require('webpack')
    
    const BabiliWebpackPlugin = require('babili-webpack-plugin')
    const CopyWebpackPlugin = require('copy-webpack-plugin')
    const ExtractTextPlugin = require('extract-text-webpack-plugin')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    
    let whiteListedModules = ['vue']
    
    let rendererConfig = {
      /**
       * 控制打包后代码的形式,影响调试。当前设置为cheap-module-eval-source-map,调试时仅可看到原始源代码      
       *(仅限行),如果同时使用了代码压缩插件,代码被合成一行,将无法获取有效的调试内容。
       */
      devtool: '#cheap-module-eval-source-map',
      
      /**
       * webpack处理项目的入口文件,以该文件作为构建其内部依赖图的开始。
       * 通常是项目的 index 或 main 文件
       * 注意 entry 可以是一个数组,即 webpack 可以处理多个入口文件,将其
       */
      entry: {
        renderer: path.join(__dirname, '../src/renderer/main.js')
      },
      
      /**
       * 声明外部依赖,在此声明的文件,即使在工程中使用import引入,也不会被打到bundle中。
       * 一般我们会把package.json中的dependencies排除在外。
       * 当我们需要使用cdn引用第三方库时,我们也要将其放在external中,比如使用jquery cdn
       * 思考1:node_modules中的三方库被排除在打包的bundle文件外,为什么我们的程序在打包后仍然能够使用
       * 三方库
       */
      externals: [
        ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d))
      ],
        
      /**
       * 模块是 webpack 的核心,webpack 帮我们把文件打包成模块,然后我们就可以使用 import 的方式来
       * 使用。webpack 通过 loader 来各种处理文件,因此我们可以在项目中使用模块化的方法引用任何类型的文件
       */
      module: {
        rules: [
          {
            /**
             * test 做正则匹配,所有符合该匹配规则的文件都将应用该 loader 规则
             */ 
            test: /\.(js|vue)$/,
            /**
             * enfore 用来定义规则的执行顺序,取值 ['pre', 'post'],此处为 pre,将在所有 loader 执行前
             * 执行。即在所有 loader 规则之前,使用 eslint-loader 对代码做静态检查
             */ 
            enforce: 'pre',
            /**
             * exclude 排除不想应用该规则的目录
             */ 
            exclude: /node_modules/,
            /**
             * useEntry 配置loader
             */ 
            use: {
              loader: 'eslint-loader',
              options: {
                formatter: require('eslint-friendly-formatter')
              }
            }
          },
          {
            test: /\.css$/,
            /**
             * ExtractTextPlugin 插件,用于将 *.css 分离到单独的 style.css 文件中,而不是作为样式
             * 放入 bundle.js 文件中
             * fallback 是当 CSS 没有被提取时应用的 loader
             * style-loader 的作用是使用 <style></style> 标签将样式添加到页面 dom 中
             */ 
            use: ExtractTextPlugin.extract({
              fallback: 'style-loader',
              use: 'css-loader'
            })
          },
          {
            test: /\.scss$/,
            /**
             * 不同于 use 的形式,loader 写成内联的方式
             * 执行顺序是“从右到左,从下到上“,即 sass-loader -> css-loader -> vue-style-loader
             * loader 之间通过 ! 分隔,如果需要为 loader 配置参数,则可以使用 ?key=value&foo=bar 的形
             * 式,类似于 url 的参数。
             */
            loader: 'vue-style-loader!css-loader!sass-loader'
          },
          {
            test: /\.html$/,
            use: 'vue-html-loader'
          },
          {
            test: /\.js$/,
            use: 'babel-loader',
            exclude: /node_modules/
          },
          {
            test: /\.json$/,
            use: 'json-loader'
          },
          {
            test: /\.node$/,
            use: 'node-loader'
          },
          {
            test: /\.vue$/,
            use: {
              loader: 'vue-loader',
              options: {
                extractCSS: process.env.NODE_ENV === 'production',
                loaders: {
                  /**
                   * 带参数的 loader
                   */
                  sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1',
                  scss: 'vue-style-loader!css-loader!sass-loader'
                }
              }
            }
          },
          {
            test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
            use: {
              loader: 'url-loader',
              /**
               * query 老的写法,等同于 options
               */
              query: {
                limit: 10000,
                name: 'imgs/[name].[ext]'
              }
            }
          },
          {
            test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
            use: {
              loader: 'url-loader',
              query: {
                limit: 10000,
                name: 'fonts/[name].[ext]'
              }
            }
          },
          {
            test: /\.(ogg)$/,
            use: 'file-loader'
          }
        ]
      },
      plugins: [
        new ExtractTextPlugin('styles.css'),
          
        /**
         * HtmlWebpackPlugin 这个插件的作用是依据一个简单的index.html模板,生成一个自动引用你打包后的JS文
         * 件的新index.html。这在每次生成的js文件名称不同时非常有用(比如添加了hash值)
         * 当我们发布一个网站应用时,我们需要通过生成带 hash 值的文件名来更新浏览器对静态文件的缓存
         * 这样会有一个问题,我们每次使用 webpack 打包后都需要手动修改 index.html 文件中引用的 
         * bundle.[hash].js 文件名,显然很麻烦。
         * HtmlWebpackPlugin 的引入就是为了解决此问题,通过配置一个 index 模板,webpack 会在每次打包完成
         * 后,将 bundle.[hash].js 的引用注入 index.html 中
         */
        new HtmlWebpackPlugin({
          filename: 'index.html',
          template: path.resolve(__dirname, '../src/index.ejs'),
          minify: {
            collapseWhitespace: true,
            removeAttributeQuotes: true,
            removeComments: true
          },
          nodeModules: process.env.NODE_ENV !== 'production'
            ? path.resolve(__dirname, '../node_modules')
            : false
        }),
          
        /**
         * HotModuleReplacementPlugin webpack 提供的最有用的功能之一。它允许在运行时更新各种模块,而无需
         * 进行页面刷新。使用 HMR 的方法:
         * 1.在webpack配置文件中添加HMR插件;
         * 2.在Webpack Dev Server中添加“hot”参数; 在我们的electron-vue应用中使用了接口调用的方式,查看
         * dev-runner.js 文件
         */
        new webpack.HotModuleReplacementPlugin(),
          
        new webpack.NoEmitOnErrorsPlugin(),
          
        /**
         * 自动加载模块,并定义到全局变量,随时可以引用。相当于 import 了一个全局对象
         * new webpack.ProvidePlugin({
         *     identifier: 'module1',
         *     identifier: ['module1', 'property1'],
         *     // ...
         * });
         * 任何时候,当 identifier 被当作未赋值的变量时,module 就会自动被加载,并且 identifier 会被这个
         * module 输出的内容所赋值。
         * 支持支持命名导出,如我们想全局使用 _.map 这个方法,那么我们可以这样来引入:
         * new webpack.ProvidePlugin({
         *     _map: ['lodash', 'map']
         * });
         * 在项目中即可以使用 _map 来使用 _.map 方法
         */
        new webpack.ProvidePlugin({
          jQuery: "jquery",
          jquery: "jquery",
          $: "jquery",
          "window.jQuery": "jquery"
        })
      ],
        
      /**
       * 定义打包文件名,[name] 参数由 entry 中的key决定,此处为render.js
       * 这里文件名没有使用 hash 值,是因为在 electron 项目中,升级不需要考虑浏览器缓存的问题,整个应用都会
       * 被替换
       */
      output: {
        filename: '[name].js',
        libraryTarget: 'commonjs2',
        path: path.join(__dirname, '../dist/electron')
      },
        
      /**
       * 在此可以定义 import 或 require 的别名,来确保模块引入变得更简单。
       * 当目录下的文件没有文件后缀时,使用 extensions 中的后缀依次尝试解析
       */
      resolve: {
        alias: {
          '@': path.join(__dirname, '../src/renderer'),
          'vue$': 'vue/dist/vue.esm.js',
          'config' : path.join(__dirname, '../config'),
          'components': path.join(__dirname, '../src/renderer/components'),
          'utils': path.join(__dirname, '../src/renderer/utils'),
          'renderer': path.join(__dirname, '../src/renderer'),
          'services': path.join(__dirname, '../src/renderer/services'),
          'store': path.join(__dirname, '../src/renderer/store'),
          'router': path.join(__dirname, '../src/renderer/router'),
          'plugins': path.join(__dirname, '../src/renderer/plugins'),
          'css': path.join(__dirname, '../src/renderer/css'),
          'images': path.join(__dirname, '../src/renderer/images'),
          'jquery': path.join(__dirname, '../node_modules/jquery/src/jquery')
        },
        extensions: ['.js', '.vue', '.json', '.css', '.node']
      },
      /**
       * 构建目标, https://webpack.docschina.org/configuration/target
       */
      target: 'electron-renderer'
    }
    
    /**
     * Adjust rendererConfig for development settings
     */
    if (process.env.NODE_ENV !== 'production') {
      /**
       * DefinePlugin 允许创建一个在编译时可以配置的全局常量。
       * 比如定义后端接口的url
       * https://webpack.docschina.org/plugins/define-plugin/
       */
      rendererConfig.plugins.push(
        new webpack.DefinePlugin({
          '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`
        })
      )
    }
    
    /**
     * Adjust rendererConfig for production settings
     */
    if (process.env.NODE_ENV === 'production') {
      rendererConfig.devtool = ''
    
      rendererConfig.plugins.push(
        /**
         * 这是一款基于 Babel 的压缩工具,支持 es6 的一些特性,取代 UglifyJS
         */
        new BabiliWebpackPlugin({
          removeConsole: true,
          removeDebugger: true
        }),
        /**
         * Copies individual files or entire directories to the build directory.
         * 一般会使用插件把需要使用的静态文件考到构建后对应的目录
         */
        new CopyWebpackPlugin([
          {
            from: path.join(__dirname, '../static'),
            to: path.join(__dirname, '../dist/electron/static'),
            ignore: ['.*']
          }
        ]),
        new webpack.DefinePlugin({
          'process.env.NODE_ENV': '"production"'
        }),
        /**
         * 用于 webpack2 对 webpack1 配置的兼容,抹平差异
         */
        new webpack.LoaderOptionsPlugin({
          minimize: true
        })
      )
    }
    
    module.exports = rendererConfig
    
    

    思考1:external

    node_modules中的三方库被排除在打包的bundle文件外,为什么我们的程序在打包后仍然能够使用三方库

    // package.json
    {
        ...
        "build": {
            ...
            "files": [
              "dist/electron",
              "node_modules/",
              "package.json"
            ]
        }
    }
    

    在 package.json 文件中,配置了 files 文件,electron-build 在打包时会将 files 中的文件夹全部打包到工程中,即 resources/app.asar 文件

    我们使用命令查看

    asar list ./resources/app.asar
    

    可以看到如下一系列文件

    /node_modules/argparse/lib/action/store/false.js
    /node_modules/argparse/lib/action/store/true.js
    /node_modules/argparse/lib/action/append
    /node_modules/argparse/lib/action/append/constant.js
    ...
    /dist
    /dist/electron
    /dist/electron/index.html
    /dist/electron/main.js
    /dist/electron/renderer.js
    /dist/electron/static/update/process.html
    /dist/electron/static/update/process.js
    /dist/electron/static/update/libs
    ...
    /dist/electron/fonts
    /dist/electron/fonts/element-icons.ttf
    

    webpack与gulp/grunt

    Gulp/Grunt是一种能够优化前端的开发流程的工具,而WebPack是一种模块化的解决方案,不过Webpack的优点使得Webpack在很多场景下可以替代Gulp/Grunt类的工具

    Grunt和Gulp的工作方式

    在一个配置文件中,指明对某些文件进行类似编译,组合,压缩等任务的具体步骤,工具之后可以自动替你完成这些任务。

    Webpack的工作方式是

    把你的项目当做一个整体,通过一个给定的主文件(如:index.js),Webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包为一个(或多个)浏览器可识别的JavaScript文件。

    如果实在要把二者进行比较,Webpack的处理速度更快更直接,能打包更多不同类型的文件。

    开发模式热更新

    开发模式热更新分为两部分:

    • 监控代码修改,并自动重新编译打包
    • 通知浏览器同步修改的内容(注意,不是刷新浏览器重新加载,是动态替换部分代码,保持原页面的数据状态,这是HMR的关键特性)

    通常使用webpack-dev-server

    • 该工具包括自动监控代码编译,以及HMR。
    • webpack-dev-server将代码打包在内存中,所以修改代码时在项目下找不到动态生成的编译文件。

    注意

    依靠HMR的热更新机制,我们可以享受修改的文件被实时同步到浏览器的便利,但对于页面来说,这并不是全部。我们仍然需要在页面的js代码中做一些处理。

    比如:我们在按钮上绑定了一个click事件的处理函数handleClick,我们修改了handleClick的代码,它被推送到浏览器中,但是页面并不会自动替换之前绑定的click事件,当我们点击按钮时,仍然得到旧的响应。

    所以除了使用webpack-dev-server,我们需要在js代码中监听HMR 的 accept 方法,在此方法中更新类似的处理函数。

    这也就是为什么在react项目中,我们需要使用react-hot-loader库,本质上是实现了HMR 的 accept 的处理。

    参考文档

    入门 Webpack,看这篇就够了

    官方中文文档

    Webpack Hot Module Replacement 的原理解析

    相关文章

      网友评论

        本文标题:webpack学习入门

        本文链接:https://www.haomeiwen.com/subject/uubyiftx.html