[TOC]

工程化的问题

  • 现在开发都是模块化开发,但是模块多了,模块之间的依赖管理究竟应该怎么做呢?
  • 页面复杂度提升之后,多页面、多系统、多状态怎么管理,页面间的公共部分应该如何维护,难道还是复制粘贴吗?
  • 团队扩大之后,团队合作怎么做?怎么解决多人研发中的性能、代码风格等问题?
  • 权衡研发效率和产品迭代的问题。

模块化

模块化是指解决一个复杂问题时,自顶向下逐层把系统划分成若干模块的过程,有多种属性,分别反映其内部特性。

CommonJS
是 Nodejs 广泛使用的一套模块化规范,是一种同步加载模块依赖的方式;
模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
require:引入一个模块
exports:导出模块内容
module:模块本身

AMD:是 JS 模块加载库RequireJS提出并且完善的一套模块化规范,AMD 是一种异步加载模块依赖的方式;

  • id:模块的 id
  • dependencies:模块依赖
  • factory:模块的工厂函数,即模块的初始化操作函数
  • require:引入模块

ES6 Module:ES6 推出的一套模块化规范。
ES6 Modules 输出的是值的引用
ES6模块是编译时加载

  • import:引入模块依赖
  • export:模块导出

CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用

  1. CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
  2. ES6 Modules 的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的 import 有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

    CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

  3. 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。

  4. 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”

CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块

工程化

当我们开发的 Web 应用越来越复杂的时候,会发现我们面临的问题会逐渐增多:

  1. 模块多了,依赖管理怎么做?
  2. 页面复杂度提升之后,多页面、多系统、多状态怎么办?
  3. 团队扩大之后,团队合作怎么做?
  4. 怎么解决多人研发中的性能、代码风格等问题?
  5. 如何权衡研发效率和产品迭代的问题?

前端工程化早期,是以 Grunt、Gulp 等构建工具为主的阶段,这个阶段解决的是重复任务的问题,它们将某些功能拆解成固定步骤的任务,然后编写工具来解决,比如:图片压缩、地址添加 hash、替换等,都是固定套路的重复工作。

而现阶段的 Webpack 则更像是从一套解决 JavaScript 模块化依赖打包开始,利用强大的插件机制,逐渐解决前端资源依赖管理问题,依附社区力量逐渐进化成一套前端工程化解决方案。

Webpack 是一个现代 JavaScript 应用程序的静态模块打包器(static module bundler)。在 Webpack 处理应用程序时,它会在内部创建一个依赖图(dependency graph),用于映射到项目需要的每个模块,然后将所有这些依赖生成到一个或多个 bundle。

webpack 解决什么问题?

Webpack 可以做到按需加载。像 Grunt、Gulp 这类构建工具,打包的思路是:遍历源文件→匹配规则→打包,这个过程中做不到按需加载,即对于打包起来的资源,到底页面用不用,打包过程中是不关心的。
Webpack 跟其他构建工具本质上不同之处在于:Webpack 是从入口文件开始,经过模块依赖加载、分析和打包三个流程完成项目的构建。在加载、分析和打包的三个过程中,可以针对性的做一些解决方案,达到按需加载的目的,比如code split(拆分公共代码等)。

当然,Webpack 还可以轻松的解决传统构建工具解决的问题:

  • 模块化打包,一切皆模块,JS 是模块,CSS 等也是模块;
  • 语法糖转换:比如 ES6 转 ES5、TypeScript;
  • 预处理器编译:比如 Less、Sass 等;
  • 项目优化:比如压缩、CDN;
  • 解决方案封装:通过强大的 Loader 和插件机制,可以完成解决方案的封装,比如 PWA;
  • 流程对接:比如测试流程、语法检测等。

NPM Scripts

NPM 不仅可以用于模块管理,还可以用于执行脚本。package.json 文件中可以添加 scripts 字段,用于指定脚本命令,供 NPM 直接调用。

webpack-cli 命令

  • –config:指定一个 Webpack 配置文件的路径;
  • –mode:指定打包环境的 mode,取值为development和production,分别对应着开发环境和生产环境;
  • –json:输mode出 Webpack 打包的结果,可以使用webpack —json > stats.json方式将打包结果输出到指定的文件;
  • –progress:显示 Webpack 打包进度;
  • –watch, -w:watch 模式打包,监控文件变化之后重新开始打包;
  • –color, —colors/–no-color, —no-colors:控制台输出的内容是否开启颜色;
  • –hot:开启 Hot Module Replacement模式,后面会详细介绍;
  • –profile:会详细的输出每个环节的用时(时间),方便排查打包速度瓶颈。

Webpack 配置

module.exports = (env, argv) => {
    return {
        mode: env.production ? 'production' : 'development',
        devtool: env.production ? 'source-maps' : 'eval',
        plugins: [
            new TerserPlugin({
                terserOptions: {
                    compress: argv['optimize-minimize'] // 只有传入 -p 或 --optimize-minimize
                }
            })
        ]
    };
};

Webpack 常见名词解释

参数 说明
entry 项目入口
module 开发中每一个文件都可以看做 module,模块不局限于 js,也包含 css、图片等
chunk 代码块,一个 chunk 可以由多个模块组成
loader 模块转化器,模块的处理器,对模块进行转换处理
plugin 扩展插件,插件可以处理 chunk,也可以对最后的打包结果进行处理,可以完成 loader 完不成的任务
bundle 最终打包完成的文件,一般就是和 chunk 一一对应的关系,bundle 就是对 chunk 进行便意压缩打包等处理后的产出

context

context即项目打包的相对路径上下文,如果指定了context=”/User/test/webpack”,那么我们设置的entry和output的相对路径都是相对于/User/test/webpack的,包括在 JavaScript 中引入模块也是从这个路径开始的。由于context的作用,决定了context值必须是一个绝对路径。

// webpack.config.js
module.exports = {
    context: '/Users/test/webpack'
};

out占位符

占位符 含义
[hash] 模块标识符的 hash
[chunkhash] chunk 内容的 hash
[name] 模块名称
[id] 模块标识符
[query] 模块的 query,例如,文件名 ? 后面的字符串
[function] 一个 return 出一个 string 作为 filename 的函数

不同hash对应的含义

[hash] 和 [chunkhash] 的长度可以使用 [hash:16](默认为 20)来指定。或者,通过指定 output.hashDigestLength 在全局配置长度。

  • [hash]:是整个项目的 hash 值,其根据每次编译内容计算得到,每次编译之后都会生成新的 hash,即修改任何文件都会导致所有文件的 hash 发生改变;在一个项目中虽然入口不同,但是 hash 是相同的;hash 无法实现前端静态资源在浏览器上长缓存,这时候应该使用 chunkhash;

  • [chunkhash]:根据不同的入口文件(entry)进行依赖文件解析,构建对应的 chunk,生成相应的 hash;只要组成 entry 的模块文件没有变化,则对应的 hash 也是不变的,所以一般项目优化时,会将公共库代码拆分到一起,因为公共库代码变动较少的,使用 chunkhash 可以发挥最长缓存的作用;

  • [contenthash]:使用 chunkhash 存在一个问题,当在一个 JS 文件中引入了 CSS 文件,编译后它们的 hash 是相同的。而且,只要 JS 文件内容发生改变,与其关联的 CSS 文件 hash 也会改变,针对这种情况,可以把 CSS 从 JS 中使用mini-css-extract-plugin 或 extract-text-webpack-plugin抽离出来并使用 contenthash。

output.publicPath

对于使用