webpack学习笔记——外行入门总结

435 阅读9分钟
原文链接: www.jianshu.com

一、前言

这是一篇鼓励外行入门 webpack 的总结性文章,尤其是像我这种从 Android 端被强迫转到前端来的大叔。所以里面不会包含具体的技术分析,而是一些感想和学习的方法。在我一开始的认知里,前端不就是写好 JavaScript , css ,html 以及把它们和资源文件如图片等按不同目录放在一起不就好了吗?但当我真正接触前端开发的时候才发现,什么?前端还要打包?还要压缩?还要混淆?特别是当我看到我们工程里的 build 文件夹时,我就不淡定了。里面各种配置文件,什么 dev-server.js,webpack.base.conf.js,webpack.dev.conf.js.....特别是一打开文件,什么鬼,这些字段都是什么意思,我能随便换一个吗?

image.png

二、概述

先是搜索了一番 webpack 相关的文章,我左看右看怎么都觉得这不是写给我看的。第一,我看不到它的全貌,除了文章介绍的这些还有其他的配置项吗?这些配置我能不要吗?默认会给什么值呢?第二,这些内容很多都结合了自身的业务代码,相对来说已经比较复杂了。

对于一个认真的程序员来说,官方文档永远是第一手最好的资料,官网 英文版 以及 中文版。有中文版的当然是优先看中文版的了,对于英语不是很 666 的同学,看中文版的效率要比英文版的高不少。打开官网后,大概得到如下的图。

image.png

总共有 6 个部分,分别是概念,配置,API,指南、Loaders 以及插件。学习的过程建议如下。

  • 首先通览一遍概念。如果真的认真通览了概念,那对 webpack 基本可以说是已经掌握一半了。因为后面像配置这些内容,你会发现都是重复的。
  • 然后再去通读一遍配置,了解该如何进行配置。
  • 最后按照指南操作一遍。基本上对整个 webpack 已经有一个非常深刻的理解了。

三 、概念

当然,认真的程序员在看完文档后,也应该有一个比较认真的结论。下面的图基本上总结了 webpack 里的基本面貌了,通过这个总结其实已经看出了 webpack 是什么,以及其能做什么了。

Webpack 概念.png

下面是对其核心概念做一个简要的解释。

  • 入口
    入口可以配置一个或者多个,webpack 会根据入口进行依赖关系的梳理,并构成最终的依赖图。
  • 出口
    出口主要是配置 bundle 输出的目标路径,文件名等。
  • Loader
    名字上看是加载器,但我是一直将其理解是成一个转换器。我觉得这样更合适,比如 vue-loader 主要就是将 vue 模板转换成浏览器能识别的 JavaScript。
  • 插件
    按照官网的理解,插件是相对于 Loader 来说的,除了具备模块转换功能还有更广泛的支持,如打包优化,压缩等。
  • 配置
    狭隘上来说主要就是对 webpack.config.js 进行配置。

五、配置

说实话,一开始看到 webpack 的配置文件 webpack.config.js 的时候有点头大,特别是整个配置文件压根就是一个 JS 文件。但在了解了 Node.js 及其相关包管理工具 NPM 后,再来看 webpack 的配置就简单了。其所谓的配置其实就是基于 Node.js 进行编程,比如其第一句就导入了 Node.js 中的一个模块 path。明白了这个道理后,你就不会觉得这个配置很奇怪了,当然也不简单。我们的分包,生产环境以及测试环境等等都可以通过配置来实现。下面是官网给的一个示例样板,里面已经有非常详细的注释了。好好过一遍,对其有个大概的轮廓,有些不理解的地方也可以先放一放,等到碰到问题的时候再深究也不迟。掌握一门技术最重要的是先要有一个基本面的理解和全局轮廓,一开始不用太在意对细节的把握。

const path = require('path');

module.exports = {
  mode: "production", // "production" | "development" | "none"  mode: "production", // enable many optimizations for production builds
  mode: "development", // enabled useful tools for development
  mode: "none", // no defaults
  // Chosen mode tells webpack to use its built-in optimizations accordingly.

  entry: "./app/entry", // string | object | array  entry: ["./app/entry1", "./app/entry2"],
  entry: {
    a: "./app/entry-a",
    b: ["./app/entry-b1", "./app/entry-b2"]
  },
  // 这里应用程序开始执行
  // webpack 开始打包

  output: {
    // webpack 如何输出结果的相关选项

    path: path.resolve(__dirname, "dist"), // string
    // 所有输出文件的目标路径
    // 必须是绝对路径(使用 Node.js 的 path 模块)

    filename: "bundle.js", // string    filename: "[name].js", // 用于多个入口点(entry point)(出口点?)
    filename: "[chunkhash].js", // 用于长效缓存
    // 「入口分块(entry chunk)」的文件名模板(出口分块?)

    publicPath: "/assets/", // string    publicPath: "",
    publicPath: "https://cdn.example.com/",
    // 输出解析文件的目录,url 相对于 HTML 页面

    library: "MyLibrary", // string,
    // 导出库(exported library)的名称

    libraryTarget: "umd", // 通用模块定义        libraryTarget: "umd2", // 通用模块定义
        libraryTarget: "commonjs2", // exported with module.exports
        libraryTarget: "commonjs-module", // 使用 module.exports 导出
        libraryTarget: "commonjs", // 作为 exports 的属性导出
        libraryTarget: "amd", // 使用 AMD 定义方法来定义
        libraryTarget: "this", // 在 this 上设置属性
        libraryTarget: "var", // 变量定义于根作用域下
        libraryTarget: "assign", // 盲分配(blind assignment)
        libraryTarget: "window", // 在 window 对象上设置属性
        libraryTarget: "global", // property set to global object
        libraryTarget: "jsonp", // jsonp wrapper
    // 导出库(exported library)的类型

    /* 高级输出配置(点击显示) */
    pathinfo: true, // boolean
    // 在生成代码时,引入相关的模块、导出、请求等有帮助的路径信息。

    chunkFilename: "[id].js",
    chunkFilename: "[chunkhash].js", // 长效缓存(/guides/caching)
    // 「附加分块(additional chunk)」的文件名模板

    jsonpFunction: "myWebpackJsonp", // string
    // 用于加载分块的 JSONP 函数名

    sourceMapFilename: "[file].map", // string
    sourceMapFilename: "sourcemaps/[file].map", // string
    // 「source map 位置」的文件名模板

    devtoolModuleFilenameTemplate: "webpack:///[resource-path]", // string
    // 「devtool 中模块」的文件名模板

    devtoolFallbackModuleFilenameTemplate: "webpack:///[resource-path]?[hash]", // string
    // 「devtool 中模块」的文件名模板(用于冲突)

    umdNamedDefine: true, // boolean
    // 在 UMD 库中使用命名的 AMD 模块

    crossOriginLoading: "use-credentials", // 枚举
    crossOriginLoading: "anonymous",
    crossOriginLoading: false,
    // 指定运行时如何发出跨域请求问题

    /* 专家级输出配置(自行承担风险) */  },

  module: {
    // 关于模块配置

    rules: [
      // 模块规则(配置 loader、解析器等选项)

      {
        test: /\.jsx?$/,
        include: [
          path.resolve(__dirname, "app")
        ],
        exclude: [
          path.resolve(__dirname, "app/demo-files")
        ],
        // 这里是匹配条件,每个选项都接收一个正则表达式或字符串
        // test 和 include 具有相同的作用,都是必须匹配选项
        // exclude 是必不匹配选项(优先于 test 和 include)
        // 最佳实践:
        // - 只在 test 和 文件名匹配 中使用正则表达式
        // - 在 include 和 exclude 中使用绝对路径数组
        // - 尽量避免 exclude,更倾向于使用 include

        issuer: { test, include, exclude },
        // issuer 条件(导入源)

        enforce: "pre",
        enforce: "post",
        // 标识应用这些规则,即使规则覆盖(高级选项)

        loader: "babel-loader",
        // 应该应用的 loader,它相对上下文解析
        // 为了更清晰,`-loader` 后缀在 webpack 2 中不再是可选的
        // 查看 webpack 1 升级指南。

        options: {
          presets: ["es2015"]
        },
        // loader 的可选项
      },

      {
        test: /\.html$/,
        test: "\.html$"

        use: [
          // 应用多个 loader 和选项
          "htmllint-loader",
          {
            loader: "html-loader",
            options: {
              /* ... */
            }
          }
        ]
      },

      { oneOf: [ /* rules */ ] },
      // 只使用这些嵌套规则之一

      { rules: [ /* rules */ ] },
      // 使用所有这些嵌套规则(合并可用条件)

      { resource: { and: [ /* 条件 */ ] } },
      // 仅当所有条件都匹配时才匹配

      { resource: { or: [ /* 条件 */ ] } },
      { resource: [ /* 条件 */ ] },
      // 任意条件匹配时匹配(默认为数组)

      { resource: { not: /* 条件 */ } }
      // 条件不匹配时匹配
    ],

    /* 高级模块配置(点击展示) */
    noParse: [
      /special-library\.js$/
    ],
    // 不解析这里的模块

    unknownContextRequest: ".",
    unknownContextRecursive: true,
    unknownContextRegExp: /^\.\/.*$/,
    unknownContextCritical: true,
    exprContextRequest: ".",
    exprContextRegExp: /^\.\/.*$/,
    exprContextRecursive: true,
    exprContextCritical: true,
    wrappedContextRegExp: /.*/,
    wrappedContextRecursive: true,
    wrappedContextCritical: false,
    // specifies default behavior for dynamic requests
  },

  resolve: {
    // 解析模块请求的选项
    // (不适用于对 loader 解析)

    modules: [
      "node_modules",
      path.resolve(__dirname, "app")
    ],
    // 用于查找模块的目录

    extensions: [".js", ".json", ".jsx", ".css"],
    // 使用的扩展名

    alias: {
      // 模块别名列表

      "module": "new-module",
      // 起别名:"module" -> "new-module" 和 "module/path/file" -> "new-module/path/file"

      "only-module$": "new-module",
      // 起别名 "only-module" -> "new-module",但不匹配 "only-module/path/file" -> "new-module/path/file"

      "module": path.resolve(__dirname, "app/third/module.js"),
      // 起别名 "module" -> "./app/third/module.js" 和 "module/file" 会导致错误
      // 模块别名相对于当前上下文导入
    },
    /* 可供选择的别名语法(点击展示) */    alias: [
      {
        name: "module",
        // 旧的请求

        alias: "new-module",
        // 新的请求

        onlyModule: true
        // 如果为 true,只有 "module" 是别名
        // 如果为 false,"module/inner/path" 也是别名
      }
    ],

    /* 高级解析选项(点击展示) */
    symlinks: true,
    // 遵循符号链接(symlinks)到新位置

    descriptionFiles: ["package.json"],
    // 从 package 描述中读取的文件

    mainFields: ["main"],
    // 从描述文件中读取的属性
    // 当请求文件夹时

    aliasFields: ["browser"],
    // 从描述文件中读取的属性
    // 以对此 package 的请求起别名

    enforceExtension: false,
    // 如果为 true,请求必不包括扩展名
    // 如果为 false,请求可以包括扩展名

    moduleExtensions: ["-module"],
    enforceModuleExtension: false,
    // 类似 extensions/enforceExtension,但是用模块名替换文件

    unsafeCache: true,
    unsafeCache: {},
    // 为解析的请求启用缓存
    // 这是不安全,因为文件夹结构可能会改动
    // 但是性能改善是很大的

    cachePredicate: (path, request) => true,
    // predicate function which selects requests for caching

    plugins: [
      // ...
    ]
    // 应用于解析器的附加插件
  },

  performance: {
    hints: "warning", // 枚举    hints: "error", // 性能提示中抛出错误
    hints: false, // 关闭性能提示
    maxAssetSize: 200000, // 整数类型(以字节为单位)
    maxEntrypointSize: 400000, // 整数类型(以字节为单位)
    assetFilter: function(assetFilename) {
      // 提供资源文件名的断言函数
      return assetFilename.endsWith('.css') || assetFilename.endsWith('.js');
    }
  },

  devtool: "source-map", // enum  devtool: "inline-source-map", // 嵌入到源文件中
  devtool: "eval-source-map", // 将 SourceMap 嵌入到每个模块中
  devtool: "hidden-source-map", // SourceMap 不在源文件中引用
  devtool: "cheap-source-map", // 没有模块映射(module mappings)的 SourceMap 低级变体(cheap-variant)
  devtool: "cheap-module-source-map", // 有模块映射(module mappings)的 SourceMap 低级变体
  devtool: "eval", // 没有模块映射,而是命名模块。以牺牲细节达到最快。
  // 通过在浏览器调试工具(browser devtools)中添加元信息(meta info)增强调试
  // 牺牲了构建速度的 `source-map' 是最详细的。

  context: __dirname, // string(绝对路径!)
  // webpack 的主目录
  // entry 和 module.rules.loader 选项
  // 相对于此目录解析

  target: "web", // 枚举  target: "webworker", // WebWorker
  target: "node", // node.js 通过 require
  target: "async-node", // Node.js 通过 fs and vm
  target: "node-webkit", // nw.js
  target: "electron-main", // electron,主进程(main process)
  target: "electron-renderer", // electron,渲染进程(renderer process)
  target: (compiler) => { /* ... */ }, // 自定义
  // 包(bundle)应该运行的环境
  // 更改 块加载行为(chunk loading behavior) 和 可用模块(available module)

  externals: ["react", /^@angular\//],  externals: "react", // string(精确匹配)
  externals: /^[a-z\-]+($|\/)/, // 正则
  externals: { // 对象
    angular: "this angular", // this["angular"]
    react: { // UMD
      commonjs: "react",
      commonjs2: "react",
      amd: "react",
      root: "React"
    }
  },
  externals: (request) => { /* ... */ return "commonjs " + request }
  // 不要遵循/打包这些模块,而是在运行时从环境中请求他们

  stats: "errors-only",  stats: { //object
    assets: true,
    colors: true,
    errors: true,
    errorDetails: true,
    hash: true,
    // ...
  },
  // 精确控制要显示的 bundle 信息

  devServer: {
    proxy: { // proxy URLs to backend development server
      '/api': 'http://localhost:3000'
    },
    contentBase: path.join(__dirname, 'public'), // boolean | string | array, static file location
    compress: true, // enable gzip compression
    historyApiFallback: true, // true for index.html upon 404, object for multiple paths
    hot: true, // hot module replacement. Depends on HotModuleReplacementPlugin
    https: false, // true for self-signed, object for cert authority
    noInfo: true, // only errors & warns on hot reload
    // ...
  },

  plugins: [
    // ...
  ],
  // 附加插件列表

  /* 高级配置(点击展示) */
  resolveLoader: { /* 等同于 resolve */ }
  // 独立解析选项的 loader

  parallelism: 1, // number
  // 限制并行处理模块的数量

  profile: true, // boolean
  // 捕获时机信息

  bail: true, //boolean
  // 在第一个错误出错时抛出,而不是无视错误。

  cache: false, // boolean
  // 禁用/启用缓存

  watch: true, // boolean
  // 启用观察

  watchOptions: {
    aggregateTimeout: 1000, // in ms
    // 将多个更改聚合到单个重构建(rebuild)

    poll: true,
    poll: 500, // 间隔单位 ms
    // 启用轮询观察模式
    // 必须用在不通知更改的文件系统中
    // 即 nfs shares(译者注:Network FileSystem,最大的功能就是可以透過網路,讓不同的機器、不同的作業系統、可以彼此分享個別的檔案 ( share file ))
  },

  node: {
    // Polyfills and mocks to run Node.js-
    // environment code in non-Node environments.

    console: false, // boolean | "mock"
    global: true, // boolean | "mock"
    process: true, // boolean
    __filename: "mock", // boolean | "mock"
    __dirname: "mock", // boolean | "mock"
    Buffer: true, // boolean | "mock"
    setImmediate: true // boolean | "mock" | "empty"
  },

  recordsPath: path.resolve(__dirname, "build/records.json"),
  recordsInputPath: path.resolve(__dirname, "build/records.json"),
  recordsOutputPath: path.resolve(__dirname, "build/records.json"),
  // TODO

}

五、指南

官网的指南相当于是手把手地一步一步教我们从零开始构建一个 webpack 项目。如果正常的话,初始化后得到的项目结构应该如下。


image.png

上图中,对 webpack 项目来说,最重要的 package.json 文件和 webpack.config.js 文件。其中 package.json 文件是 NPM 的配置文件,而 webpack.config.js 文件是 webpack 的配置文件。一般地,我们通过在 package.json 中添加所要依赖的 Loader 和 plugin,然后通过 npm install 命令进行安装,那相应的组件就会被安装到 node_modules 中去了。而 webpack.config.js 就是用来配置如何组织 webpack 项目以及如何使用 npm 所安装的这些组件。

六、总结

作为一名 Android 端的开发者,之前在公司里就是一颗螺丝钉,真正能参与到实际前端开发的机会几乎没有,尤其是在手机类的公司里,其更偏重于中间层或者底层技术的应用与开发。虽然平时可能会自学一下,但往往由于缺乏方法,看了看 JavaScript ,看了看 html 标签和 css 样式,然后写一个简单的丑陋的界面,基本上耐心就耗完了。

对于想学习前端的 Android / IOS 的同学,建议找一个玩的好的前端同学系统讲述一下前端的知识和建议的学习路线,尤其是那些已经成功拥有从 native 端到前端开发的同学,应该能让你少走很多弯路。

那我的建议是什么呢?JavaScript,html,css 这些基本过一遍就行了,不要太强求自己去理解或者去记忆,只要有个印象即可。因为你不可能用上它们的全部,甚至有些用法上在实际项目可能和讲的都不一样。然后选择一个框架,建议 vue 或者 react 中的一个。对前端完全陌生的尤其建议 vue。然后拿一个项目练手,典型的就是 GitHub-Pages。练手时也不是说从零开始,而是找一个别人已经开发好的比较优秀且规范的项目进行 fork,然后去改一改,调试一下。尝试给自己增加一些业务需求,增加一些页面。

对于 js,html,css 建议多去看 MDN。而对于使用的框架,以及相关的组件建议多看官网,一定要熟知官网。过了这个槛儿,就算是前端入门了。

以上就是个人的浅见了。