1. 前端工程化项目打包历史
前端工程化之前的时代略过
1. 半自动执行脚本来压缩合并文件
自从xmlhttprequest被挖掘出来,网页能够和服务端通讯,js能做的事越来越多,文件体积越来越大,互相引用原来越多。
然而网速只有几兆的带宽。于是想到,要把js文件狠狠的压缩,能合并的就给合并起来
主要有的js压缩工具:
JSMin 使用简单、灵活,对不同语言、环境支持好。一般配合不同的环境、语言用命令行执行压缩
YUI Compressor 雅虎推出。有JAVA版本和.NET版本,需两者环境配合
UglifyJS 基于nodejs,压缩策略较为安全,所以不会对源代码进行大幅度的改造。
Closure Compiler 谷歌出品
在我理解,压缩主要做了局部变量命名简化、空格/换行/注释消除、自动优化可简化的语法等工作。
使用压缩工具用es6语法写的js在压缩测试比较中:
- UglifyJS压缩率高,可以自动格式化、优化代码。所以普及率高。现在也是主流的工具
- YUI compressor 好处就是压缩策略安全,相比UglifyJS,自动优化代码的程度较保守
- Closure Compiler 的Advanced模式直接破坏代码的结构,bug多
压缩有了,但是当a文件和b文件都引用了c文件的方法时,如果把c文件分别和a、b合并,这样就只有两个文件了。这就是最开始的合并方式。
一般是通过在windows上用bat脚本或者mac/linux上的shell脚本来决定合并哪些文件、用什么工具压缩、怎么压缩
- 进步:
- 解决当时网速普遍较慢的情况下网页加载资源较慢的问题
- 代码混淆了不容易被盗用
- 存在的问题:
- 工程化的项目里相互依赖关系变得非常复杂
- 合并的文件里可能会有很多无用代码
2. 自动构建的尝试阶段
通过脚本构建的项目里的文件相互依赖复杂,命名什么的完全有可能一不小心就冲突了,而且依赖可能是一层一层依赖下去的,维护起来要命呀。
所以先解决js相互依赖的问题
1. js依赖关系的规范探索
CommonJS规范解决了js模块化依赖的问题
CommonJs规范的简单介绍(nodejs版)
- 一个文件就是一个模块
- 每个模块内部,都有一个module对象,代表当前模块,有规定好的一些默认属性
- module.exports属性:初始值是一个空对象{},这个变量把定义的变量、方法暴露出去
- exports变量:Node为每个模块提供一个exports变量,指向module.exports。至于两者的区别,不用去了解,平常就用module.exports
- require引用:把module.exports定义的变量/方法来过来用
- 注意:它是同步的
既然CommonJS提供了模块化的思路,也已经在服务端(nodejs)里大展手脚。那么浏览器里可不可行?
浏览器不兼容CommonJS的原因首先是,缺少了4个NodeJs环境变量:
- module
- exports
- require
- global
那么只需要提供了这些个环境变量就行了吧。Browerify就是做这的
Browserify 是目前最常用的 CommonJS 格式转换的工具
Browserify的核心思路是讲module暴露出的模块放入一个数组,require时根据模块id找到相应的module执行,总之就是给上面缺少的变量写成可执行的es5的策略
那么是不是这样就能在浏览器上愉快使用CommonJS?
CommoJS是同步require的方式获取js模块,在浏览器上会阻断主线程。页面会因加载js可能卡住
这肯定是不能容忍的
于是AMD(异步模块定义)诞生
AMD也采用require()语句加载模块,但是要传两个参数require([module], callback)。是的,回调思路。
AMD规范的简单介绍(RequireJS)
- 解决两个问题:
(1)实现js文件的异步加载,避免网页失去响应
(2)管理模块之间的依赖性,便于代码的编写和维护
- 模块必须采用特定的define()函数来定义
- 非AMD的第三方库加载之前要用require.config()定义固有特征
CMD规范 和AMD大同小异,具体实现是seajs。没用过,应该都差不多吧,啊哈哈
2. html/css模块化的规范
less,sass,stylus 的 css 预处理器简化css语法
ejs,jade 等html的模板语法
这些真的是前端狗的福音,不多说,css-next来了,继续啃咯。
这样html/css/js 就都有了适合自动构建的扩展结构。但是这时候写一个构建这些依赖的命令太长太复杂,所以打包工具开始流行:
3.Grunt/Gulp 流处理构建工具让前端构建更容易
grunt 写法简单,插件还贼多
gulp 效率更高,可扩展性更强
nodejs配合这俩大佬做web项目的自动化构建用着都挺爽的
var gulp = require('gulp')
var nodemon = require('gulp-nodemon')
var browserSync = require('browser-sync').create()
gulp.task('nodemon', function(cb) {
var started = false
return nodemon({
script: 'mswadmin.js'
, ext: 'js'
, env: { 'NODE_ENV': 'default' }
}).on('start', function() {
if (!started) {
cb();
started = true;
}
})
});
gulp.task('serve', function(){
browserSync.init({
proxy: 'http://10.3.10.27:18282',
browser: 'chrome',
port: 18282
})
gulp.watch('static/**/*.+(scss|jade|ls)', ['inject'])
.on('change', browserSync.reload);
})
gulp.task('default', ['nodemon','serve']);
上面是一个用nodemon监控本地服务+watch代码热更新的配置。可以看出,以流任务的方式一个个执行。用起来也简单
2. SPA(Single-page application)来了
js 对应的 AMD 模块,然后该 AMD 模块渲染对应的 html 到容器内
这样网页不再是传统的文档一类的页面了。而是更像一个完整的程序。一个主入口,js完成的前端路由,AMD模块完成页面内重新渲染。
虽然是做出来这个SPA了,但是小问题多:
- 很多成熟的第三方库不支持AMD规范,引用起来贼麻烦
- RequireJS在加载html依赖时,html里的img路径要使用绝对路径
- 只能一次性加载所有css文件
- 分模块打包js文件时的通用依赖项很难配置
- 最重要的,AMD/CMD CommonJS规范太多造成很多第三方库对规范支出不够。。。而且ES6规范都要普及了,你不用???
3. webpack来解救你
首先,webpack是静态模块打包器(bundler),grunt/gulp是流任务执行器。
区分两者可以用grunt-webpack形象说明:你可以将 webpack 或 webpack-dev-server 作为一项任务(task)执行
webpack为啥好用:
- webpack 能够为ES6的 import/export 提供开箱即用般的支持
- 还支持CommonJS CMD/AMD模块规范,做到随时可用
这两点是我觉得最突出的地方,详细对比请参考对比
浏览器环境下,用了ES6规范的话,你应该不想用其他的了
webpack的工作步骤如下:
- 从入口文件开始递归地建立一个依赖关系图。
- 把所有文件都转化成模块函数。
- 根据依赖关系,按照配置文件把模块函数分组打包成若干个bundle。
- 通过script标签把打包的bundle注入到html中,通过manifest文件来管理bundle文件的运行和加载。
打包的规则为:一个入口文件对应一个bundle。该bundle包括入口文件模块和其依赖的模块。按需加载的模块或需单独加载的模块则分开打包成其他的bundle。
除了这些bundle外,还有一个特别重要的bundle,就是manifest.bundle.js文件,即webpackBootstrap。这个manifest文件是最先加载的,负责解析webpack打包的其他bundle文件,使其按要求进行加载和执行。
无论你选择哪种模块语法,那些 import 或 require 语句现在都已经转换为 webpack_require 方法,此方法指向模块标识符(module identifier)。通过使用 manifest 中的数据,runtime 将能够查询模块标识符,检索出背后对应的模块。
webpack 怎么入门
虽然网上有很多 十分钟入门webpack 的教程。但还是推荐去撸一遍webpack官方指南
个人觉得指南里你要注意的细节:
- webpack 不会更改代码中除 import 和 export 语句以外的部分。如果你在使用其它 ES2015 特性,请确保你在 webpack 的 loader 系统中使用了一个像是 Babel 或 Bublé 的转译器
- npm脚本运行时默认可以使用npx命令
- source map要合理使用
- 留意webpack-dev-middleware,配合express做服务端渲染要用到哦
- HMR(模块热替代)一般用你选用的框架自带的loader(vue-loader)
- 用UglifyJsPlugin插件自动移除 JavaScript 上下文中的未引用代码(dead-code)。webpack4里使用 mode=production 替代。要结合SideEffects使用,webpack4又提供了SideEffects插件使用的方式
- process.env.NODE_ENV === ‘production’ ? ‘[name].[hash].bundle.js’ : ‘[name].bundle.js’ 这样的条件语句在配置文件里无法使用,用if/else
- splitChunks优化,webpack4已经移除了CommonsChunkPlugin。下文会详细解释
- dynamic imports(动态导入)优化,chunkFilename决定非入口 chunk 的名称,vue里的运用实例就是路由懒加载(vue-lazyload),生成了新的bundle
4. webpack的优化点的补充说明
- 动态导入在vue里的时间注意点:
webpack 可以使用dynamic imports的方式引用模块,我们使用 async/ await 和 dynamic import 来实现。每一个dynamic import都将作为一个单独的chunk打包。在vue中的一个例子就是路由懒加载+babel-plugin-dynamic-import-node的构建方案。使用babel-plugin-dynamic-import-node是因为开发环境下触发热更新很慢,这个插件讲import异步全部改成require同步
- 打包生成的文件模块标识符的问题
一般来说我们在dist生成了一下三种bundle
main bundle 会随着自身的新增内容的修改,而发生变化。
vendor bundle 会随着自身的 module.id 的修改,而发生变化。
manifest bundle 会因为当前包含一个新模块的引用,而发生变化。
然而我们并不希望vendor每次构建都生成新的hash,毕竟我们希望用到缓存的。解决方法官方有两个插件NamedModulesPlugin和HashedModuleIdsPlugin
vue里使用的是HashedModuleIdsPlugin
相信很多人从webpack3升级到4会碰到问题,接下来
### 5. 升级到webpack4你该搞明白
1. 零配置的概念把配置门槛降低了
主要使用了模式的概念。
development 模式下,默认开启了NamedChunksPlugin 和NamedModulesPlugin方便调试,提供了更完整的错误信息,更快的重新编译的速度
production 模式下,由于提供了splitChunks和minimizer,所以基本零配置,代码就会自动分割、压缩、优化,同时 webpack 也会自动帮你 Scope hoisting(作用域提升) 和 Tree-shaking
相当于把一些基本的配置当成默认配置。只需要在命令行运行时带上mode参数就搞定
#### 2. 一些插件的废除和替换
废弃了 | 顶替者(用optimization属性) | 变化 |
---|---|---|
uglifyjs-webpack-plugin | minimizer | 压缩优化 |
CommonsChunkPlugin | splitChunks | 代码分割,下面详解 |
还有一些新的插件:Tree Shaking,SideEffects。我还不知道怎么用–
3. 要注意的新的优化点
- extract-text-webpack-plugin -> mini-css-extract-plugin
它与extract-text-webpack-plugin最大的区别是:它在code spliting的时候会将原先内联写在每一个 js chunk bundle的 css,单独拆成了一个个 css 文件。js变得更干净了,css是根据optimization.splitChunks的配置自动拆分css文件为单独的模块的规则拆分的,不用担心过多的httlp资源请求问题
- 所有的[chunkhash] ->[contenthash]
这是为了解决当css与js文件有依赖时,两者有相同的chunkhash。这样js修改了,css没改的情况下chunkhash页被修改了,没法缓存了呀
contenthash 你可以简单理解为是 moduleId + content 所生成的 hash
相关issue
- 代码的压缩优化改成了optimization.minimizer
在optimization.minimizer里推荐使用optimize-css-assets-webpack-plugin直接配置。但是vue-cli3里的配置自己配的。嗯…反正也不想看那些配置,就这样吧~~~
4. 第三方库和业务代码分开打包策略
上面多处提到了这个optimization.splitChunks
Webpack 4 最大的改进便是Code Splitting chunk。webpack3是通过CommonsChunkPlugin拆分的。然后现在直接被废弃了,我能怎么办?,跟着学呗。
开启Code Splitting很简单,使用production的mode就行,会自动开启。并有一个设置好了的一个很合理的配置
如果同时满足下列条件,chunk 就会被拆分:
- 新的 chunk 能被复用,或者模块是来自 node_modules 目录
- 新的 chunk 大于 30Kb(min+gz 压缩前)
- 按需加载 chunk 的并发请求数量小于等于 5 个
- 页面初始加载时的并发请求数量小于等于 3 个
默认配置已经很合理了,然而当出现如下情况:
已vue-cli创建的项目为例。项目用到了第三方的UI组件库,在main.js入口处依赖了第三方库。
因为在入口引入了,所以第三方库会被打包进app.js。这样,只要我修改了app.js里的其他代码,打出来的包的hash就变了。浏览器又得再次缓存app.js。第三库相当于又被缓存了一次,这显然不是我们想要的。
看一下花裤衩的配置
splitChunks: {
chunks: "all",
cacheGroups: {
libs: {
name: "chunk-libs",
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: "initial" // 只打包初始时依赖的第三方
},
elementUI: {
name: "chunk-elementUI", // 单独将 elementUI 拆包
priority: 20, // 权重要大于 libs 和 app 不然会被打包进 libs 或者 app
test: /[\\/]node_modules[\\/]element-ui[\\/]/
},
commons: {
name: "chunk-commons",
test: resolve("src/components"), // 可自定义拓展你的规则
minChunks: 2, // 最小共用次数
priority: 5,
reuseExistingChunk: true
}
}
};
主要思路就是
- 把初始化时依赖的第三方打包成基础类库,这一类改动小,又被全局需要
- 把类似elementUI这一类的比较大、改动较小的抽出来
- 全局公用的router、函数、svg图标、layout布局组件等这些不管,直接扔app.js
- 业务里会经常使用但没在main.js引入的的components被打包成一个common
- 业务里经常使用但是体积相当较小,就直接在main.js引入,打包进app.js
- 其他的低频使用的组件会自动按默认splitChunks的设置来拆分
提醒: 代码的拆分一定要结合项目的实际情况,比如你就用到element里的一两个组件,完全可以按需加载在main.js,然后直接打包进app.js。所以没有最合理的拆分规则,只有最适合你的。
5. Prefetching/Preloading modules
支持了Prefetching/Preloading浏览器资源加载优化
核心思想是减少JS下载时间
学不动了学不动了,先缓缓