工程化的问题
- 现在开发都是模块化开发,但是模块多了,模块之间的依赖管理究竟应该怎么做呢?
- 页面复杂度提升之后,多页面、多系统、多状态怎么管理,页面间的公共部分应该如何维护,难道还是复制粘贴吗?
- 团队扩大之后,团队合作怎么做?怎么解决多人研发中的性能、代码风格等问题?
- 权衡研发效率和产品迭代的问题。
模块化
模块化是指解决一个复杂问题时,自顶向下逐层把系统划分成若干模块的过程,有多种属性,分别反映其内部特性。
CommonJS
是 Nodejs 广泛使用的一套模块化规范,是一种同步加载模块依赖的方式;
模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
require:引入一个模块
exports:导出模块内容
module:模块本身
AMD:是 JS 模块加载库RequireJS提出并且完善的一套模块化规范,AMD 是一种异步加载模块依赖的方式;
- id:模块的 id
- dependencies:模块依赖
- factory:模块的工厂函数,即模块的初始化操作函数
- require:引入模块
ES6 Module:ES6 推出的一套模块化规范。
ES6 Modules 输出的是值的引用
ES6模块是编译时加载
- import:引入模块依赖
- export:模块导出
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
- CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
ES6 Modules 的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的 import 有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
- 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”
CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块
工程化
当我们开发的 Web 应用越来越复杂的时候,会发现我们面临的问题会逐渐增多:
- 模块多了,依赖管理怎么做?
- 页面复杂度提升之后,多页面、多系统、多状态怎么办?
- 团队扩大之后,团队合作怎么做?
- 怎么解决多人研发中的性能、代码风格等问题?
- 如何权衡研发效率和产品迭代的问题?
前端工程化早期,是以 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
对于使用